1 module dlangide.ui.terminal;
2 
3 import dlangui.widgets.widget;
4 import dlangui.widgets.controls;
5 import dlangui.widgets.scrollbar;
6 
7 struct TerminalAttr {
8     ubyte bgColor = 0;
9     ubyte textColor = 7;
10     ubyte flags = 0;
11 }
12 
13 struct TerminalChar {
14     TerminalAttr attr;
15     dchar ch = ' ';
16 }
17 
18 __gshared static uint[16] TERMINAL_PALETTE = [
19     0x000000, // black
20     0xFF0000,
21     0x00FF00,
22     0xFFFF00,
23     0x0000FF,
24     0xFF00FF,
25     0x00FFFF,
26     0xC0C0C0, // white
27     0x808080,
28     0x800000,
29     0x008000,
30     0x808000,
31     0x000080,
32     0x800080,
33     0x008080,
34     0xFFFFFF,
35 ];
36 
37 uint attrToColor(ubyte v) {
38     if (v >= 16)
39         return 0;
40     return TERMINAL_PALETTE[v];
41 }
42 
43 struct TerminalLine {
44     TerminalChar[] line;
45     bool overflowFlag;
46     bool eolFlag;
47     void clear() {
48         line.length = 0;
49         overflowFlag = false;
50         eolFlag = false;
51     }
52     void markLineOverflow() {}
53     void markLineEol() {}
54     void putCharAt(dchar ch, int x, TerminalAttr currentAttr) {
55         if (x >= line.length) {
56             TerminalChar d;
57             d.attr = currentAttr;
58             d.ch = ' ';
59             while (x >= line.length) {
60                 line.assumeSafeAppend;
61                 line ~= d;
62             }
63         }
64         line[x].attr = currentAttr;
65         line[x].ch = ch;
66     }
67 }
68 
69 struct TerminalContent {
70     TerminalLine[] lines;
71     Rect rc;
72     FontRef font;
73     TerminalAttr currentAttr;
74     TerminalAttr defAttr;
75     int maxBufferLines = 3000;
76     int topLine;
77     int width; // width in chars
78     int height; // height in chars
79     int charw; // single char width
80     int charh; // single char height
81     int cursorx;
82     int cursory;
83     bool focused;
84     bool _lineWrap = true;
85     @property void lineWrap(bool v) {
86         _lineWrap = v;
87     }
88     void clear() {
89         lines.length = 0;
90         cursorx = cursory = topLine= 0;
91     }
92     void resetTerminal() {
93         for (int i = topLine; i < cast(int)lines.length; i++) {
94             lines[i] = TerminalLine.init;
95         }
96         cursorx = 0;
97         cursory = topLine;
98     }
99     @property int screenTopLine() {
100         int y = cast(int)lines.length - height;
101         if (y < 0)
102             y = 0;
103         return y;
104     }
105     void eraseScreen(int direction, bool forLine) {
106         if (forLine) {
107             for (int x = 0; x < width; x++) {
108                 if ((direction == 1 && x <= cursorx) || (direction < 1 && x >= cursorx) || (direction == 2))
109                     putCharAt(' ', x, cursory);
110             }
111         } else {
112             int screenTop = screenTopLine;
113             for (int y = 0; y < height; y++) {
114                 int yy = screenTop + y;
115                 if ((direction == 1 && yy <= cursory) || (direction < 1 && yy >= cursory) || (direction == 2)) {
116                     for (int x = 0; x < width; x++) {
117                         putCharAt(' ', x, yy);
118                     }
119                 }
120             }
121             if (direction == 2) {
122                 cursorx = 0;
123                 cursory = screenTop;
124             }
125         }
126     }
127     void moveCursorBy(int x, int y) {
128         if (x) {
129             cursorx += x;
130             if (cursorx < 0)
131                 cursorx = 0;
132             if (cursorx > width)
133                 cursorx = width;
134         } else if (y) {
135             int screenTop = screenTopLine;
136             cursory += y;
137             if (cursory < screenTop)
138                 cursory = screenTop;
139             else if (cursory >= screenTop + height)
140                 cursory = screenTop + height - 1;
141         }
142     }
143     void setAttributes(int[] attrs) {
144         foreach (attr; attrs) {
145             if (attr < 0)
146                 continue;
147             if (attr >= 30 && attr <= 37) {
148                 currentAttr.textColor = cast(ubyte)(attr - 30);
149             } else if (attr >= 40 && attr <= 47) {
150                 currentAttr.bgColor = cast(ubyte)(attr - 40);
151             } else if (attr >= 0 && attr <= 10) {
152                 switch(attr) {
153                     case 0:
154                         currentAttr = defAttr;
155                         break;
156                     case 1:
157                     case 2:
158                     case 4:
159                     case 5:
160                     case 7:
161                     case 8:
162                     default:
163                         break;
164 
165                 }
166             }
167         }
168     }
169     void moveCursorTo(int x, int y) {
170         int screenTop = screenTopLine;
171         if (x < 0 || y < 0) {
172             cursorx = 0;
173             cursory = screenTop;
174             return;
175         }
176         if (x >= 1 && x <= width + 1 && y >= 1 && x <= height) {
177             cursorx = x - 1;
178             cursory = screenTop + y - 1;
179         }
180     }
181     void layout(FontRef font, Rect rc) {
182         this.rc = rc;
183         this.font = font;
184         this.charw = font.charWidth('0');
185         this.charh = font.height;
186         int w = rc.width / charw;
187         int h = rc.height / charh;
188         setViewSize(w, h);
189     }
190     void setViewSize(int w, int h) {
191         if (h < 2)
192             h = 2;
193         if (w < 16)
194             w = 16;
195         width = w;
196         height = h;
197     }
198     void draw(DrawBuf buf) {
199         Rect lineRect = rc;
200         dchar[] text;
201         text.length = 1;
202         text[0] = ' ';
203         int screenTopLine = cast(int)lines.length - height;
204         if (screenTopLine < 0)
205             screenTopLine = 0;
206         for (uint i = 0; i < height && i + topLine < lines.length; i++) {
207             lineRect.bottom = lineRect.top + charh;
208             TerminalLine * p = &lines[i + topLine];
209             // draw line in rect
210             for (int x = 0; x < width; x++) {
211                 bool isCursorPos = x == cursorx && i + topLine == cursory;
212                 TerminalChar ch = x < p.line.length ? p.line[x] : TerminalChar.init;
213                 uint bgcolor = attrToColor(ch.attr.bgColor);
214                 uint textcolor = attrToColor(ch.attr.textColor);
215                 if (isCursorPos && focused) {
216                     // invert
217                     uint tmp = bgcolor;
218                     bgcolor = textcolor;
219                     textcolor = tmp;
220                 }
221                 Rect charrc = lineRect;
222                 charrc.left = lineRect.left + x * charw;
223                 charrc.right = charrc.left + charw;
224                 charrc.bottom = charrc.top + charh;
225                 buf.fillRect(charrc, bgcolor);
226                 if (isCursorPos) {
227                     buf.drawFrame(charrc, focused ? (textcolor | 0xC0000000) : (textcolor | 0x80000000), Rect(1,1,1,1));
228                 }
229                 if (ch.ch >= ' ') {
230                     text[0] = ch.ch;
231                     font.drawText(buf, charrc.left, charrc.top, text, textcolor);
232                 }
233             }
234             lineRect.top = lineRect.bottom;
235         }
236     }
237 
238     void clearExtraLines(ref int yy) {
239         int y = cast(int)lines.length;
240         if (y >= maxBufferLines) {
241             int delta = y - maxBufferLines;
242             for (uint i = 0; i + delta < maxBufferLines && i + delta < lines.length; i++) {
243                 lines[i] = lines[i + delta];
244             }
245             lines.length = lines.length - delta;
246             yy -= delta;
247             topLine -= delta;
248             if (topLine < 0)
249                 topLine = 0;
250         }
251     }
252 
253     TerminalLine * getLine(ref int yy) {
254         if (yy < 0)
255             yy = 0;
256         while(yy >= cast(int)lines.length) {
257             lines ~= TerminalLine.init;
258         }
259         clearExtraLines(yy);
260         return &lines[yy];
261     }
262     void putCharAt(dchar ch, ref int x, ref int y) {
263         if (x < 0)
264             x = 0;
265         TerminalLine * line = getLine(y);
266         if (x >= width) {
267             line.markLineOverflow();
268             y++;
269             line = getLine(y);
270             x = 0;
271         }
272         line.putCharAt(ch, x, currentAttr);
273         ensureCursorIsVisible();
274     }
275     int tabSize = 8;
276     // supports printed characters and \r \n \t
277     void putChar(dchar ch) {
278         if (ch == '\a') {
279             // bell
280             return;
281         }
282         if (ch == '\b') {
283             // backspace
284             if (cursorx > 0) {
285                 cursorx--;
286                 putCharAt(' ', cursorx, cursory);
287                 ensureCursorIsVisible();
288             }
289             return;
290         }
291         if (ch == '\r') {
292             cursorx = 0;
293             ensureCursorIsVisible();
294             return;
295         }
296         if (ch == '\n' || ch == '\f' || ch == '\v') {
297             TerminalLine * line = getLine(cursory);
298             line.markLineEol();
299             cursory++;
300             line = getLine(cursory);
301             cursorx = 0;
302             ensureCursorIsVisible();
303             return;
304         }
305         if (ch == '\t') {
306             int newx = (cursorx + tabSize) / tabSize * tabSize;
307             if (newx > width) {
308                 TerminalLine * line = getLine(cursory);
309                 line.markLineEol();
310                 cursory++;
311                 line = getLine(cursory);
312                 cursorx = 0;
313             } else {
314                 for (int x = cursorx; x < newx; x++) {
315                     putCharAt(' ', cursorx, cursory);
316                     cursorx++;
317                 }
318             }
319             ensureCursorIsVisible();
320             return;
321         }
322         putCharAt(ch, cursorx, cursory);
323         cursorx++;
324         ensureCursorIsVisible();
325     }
326 
327     void ensureCursorIsVisible() {
328         topLine = cast(int)lines.length - height;
329         if (topLine < 0)
330             topLine = 0;
331         if (cursory < topLine)
332             cursory = topLine;
333     }
334 
335     void updateScrollBar(ScrollBar sb) {
336         sb.pageSize = height;
337         sb.maxValue = cast(int)lines.length;
338         sb.position = topLine;
339     }
340 
341     void scrollTo(int y) {
342         if (y + height > lines.length)
343             y = cast(int)lines.length - height;
344         if (y < 0)
345             y = 0;
346         topLine = y;
347     }
348 
349 }
350 
351 class TerminalWidget : WidgetGroup, OnScrollHandler {
352     protected ScrollBar _verticalScrollBar;
353     protected TerminalContent _content;
354     protected TerminalDevice _device;
355     this() {
356         this(null);
357     }
358     this(string ID) {
359         super(ID);
360         styleId = "TERMINAL";
361         focusable = true;
362         _verticalScrollBar = new ScrollBar("VERTICAL_SCROLLBAR", Orientation.Vertical);
363         _verticalScrollBar.minValue = 0;
364         _verticalScrollBar.scrollEvent = this;
365         addChild(_verticalScrollBar);
366         _device = new TerminalDevice();
367         TerminalWidget _this = this;
368         if (_device.create()) {
369             _device.onBytesRead = delegate (string data) {
370                 import dlangui.platforms.common.platform;
371                 Window w = window;
372                 if (w) {
373                     //w.exe
374                     w.executeInUiThread(delegate() {
375                         if (w.isChild(_this)) {
376                             write(data);
377                             w.update(true);
378                         }
379                     });
380                 }
381             };
382         }
383     }
384     ~this() {
385         if (_device)
386             destroy(_device);
387     }
388 
389     /// returns terminal/tty device (or named pipe for windows) name
390     @property string deviceName() { return _device ? _device.deviceName : null; }
391 
392     void scrollTo(int y) {
393         _content.scrollTo(y);
394     }
395 
396     /// handle scroll event
397     bool onScrollEvent(AbstractSlider source, ScrollEvent event) {
398         switch(event.action) {
399             /// space above indicator pressed
400             case ScrollAction.PageUp:
401                 scrollTo(_content.topLine - (_content.height ? _content.height - 1 : 1));
402                 break;
403             /// space below indicator pressed
404             case ScrollAction.PageDown:
405                 scrollTo(_content.topLine + (_content.height ? _content.height - 1 : 1));
406                 break;
407             /// up/left button pressed
408             case ScrollAction.LineUp:
409                 scrollTo(_content.topLine - 1);
410                 break;
411             /// down/right button pressed
412             case ScrollAction.LineDown:
413                 scrollTo(_content.topLine + 1);
414                 break;
415             /// slider pressed
416             case ScrollAction.SliderPressed:
417                 break;
418             /// dragging in progress
419             case ScrollAction.SliderMoved:
420                 scrollTo(event.position);
421                 break;
422             /// dragging finished
423             case ScrollAction.SliderReleased:
424                 break;
425             default:
426                 break;
427         }
428         return true;
429     }
430 
431     /// check echo mode
432     @property bool echo() { return _echo; }
433     /// set echo mode
434     @property void echo(bool b) { _echo = b; }
435 
436     Signal!TerminalInputHandler onBytesRead;
437     protected bool _echo = false;
438     bool handleTextInput(dstring str) {
439         import std.utf;
440         string s8 = toUTF8(str);
441         if (_echo)
442             write(s8);
443         if (_device)
444             _device.write(s8);
445         if (onBytesRead.assigned) {
446             onBytesRead(s8);
447         }
448         return true;
449     }
450 
451     override bool onKeyEvent(KeyEvent event) {
452         if (event.action == KeyAction.Text) {
453             dstring text = event.text;
454             if (text.length)
455                 handleTextInput(text);
456             return true;
457         }
458         if (event.action == KeyAction.KeyDown) {
459             dstring flagsstr;
460             dstring flagsstr2;
461             dstring flagsstr3;
462             if ((event.flags & KeyFlag.MainFlags) == KeyFlag.Menu) {
463                 flagsstr = "1;1";
464                 flagsstr2 = ";1";
465                 flagsstr3 = "1";
466             }
467             if ((event.flags & KeyFlag.MainFlags) == KeyFlag.Shift) {
468                 flagsstr = "1;2";
469                 flagsstr2 = ";2";
470                 flagsstr3 = "2";
471             }
472             if ((event.flags & KeyFlag.MainFlags) == KeyFlag.Alt) {
473                 flagsstr = "1;3";
474                 flagsstr2 = ";3";
475                 flagsstr3 = "3";
476             }
477             if ((event.flags & KeyFlag.MainFlags) == KeyFlag.Control) {
478                 flagsstr = "1;5";
479                 flagsstr2 = ";5";
480                 flagsstr3 = ";5";
481             }
482             switch (event.keyCode) {
483                 case KeyCode.ESCAPE:
484                     return handleTextInput("\x1b");
485                 case KeyCode.RETURN:
486                     return handleTextInput("\n");
487                 case KeyCode.TAB:
488                     return handleTextInput("\t");
489                 case KeyCode.BACK:
490                     return handleTextInput("\t");
491                 case KeyCode.F1:
492                     return handleTextInput("\x1bO" ~ flagsstr3 ~ "P");
493                 case KeyCode.F2:
494                     return handleTextInput("\x1bO" ~ flagsstr3 ~ "Q");
495                 case KeyCode.F3:
496                     return handleTextInput("\x1bO" ~ flagsstr3 ~ "R");
497                 case KeyCode.F4:
498                     return handleTextInput("\x1bO" ~ flagsstr3 ~ "S");
499                 case KeyCode.F5:
500                     return handleTextInput("\x1b[15" ~ flagsstr2 ~ "~");
501                 case KeyCode.F6:
502                     return handleTextInput("\x1b[17" ~ flagsstr2 ~ "~");
503                 case KeyCode.F7:
504                     return handleTextInput("\x1b[18" ~ flagsstr2 ~ "~");
505                 case KeyCode.F8:
506                     return handleTextInput("\x1b[19" ~ flagsstr2 ~ "~");
507                 case KeyCode.F9:
508                     return handleTextInput("\x1b[20" ~ flagsstr2 ~ "~");
509                 case KeyCode.F10:
510                     return handleTextInput("\x1b[21" ~ flagsstr2 ~ "~");
511                 case KeyCode.F11:
512                     return handleTextInput("\x1b[23" ~ flagsstr2 ~ "~");
513                 case KeyCode.F12:
514                     return handleTextInput("\x1b[24" ~ flagsstr2 ~ "~");
515                 case KeyCode.LEFT:
516                     return handleTextInput("\x1b[" ~ flagsstr ~ "D");
517                 case KeyCode.RIGHT:
518                     return handleTextInput("\x1b[" ~ flagsstr ~ "C");
519                 case KeyCode.UP:
520                     return handleTextInput("\x1b[" ~ flagsstr ~ "A");
521                 case KeyCode.DOWN:
522                     return handleTextInput("\x1b[" ~ flagsstr ~ "B");
523                 case KeyCode.INS:
524                     return handleTextInput("\x1b[2" ~ flagsstr2 ~ "~");
525                 case KeyCode.DEL:
526                     return handleTextInput("\x1b[3" ~ flagsstr2 ~ "~");
527                 case KeyCode.HOME:
528                     return handleTextInput("\x1b[" ~ flagsstr ~ "H");
529                 case KeyCode.END:
530                     return handleTextInput("\x1b[" ~ flagsstr ~ "F");
531                 case KeyCode.PAGEUP:
532                     return handleTextInput("\x1b[5" ~ flagsstr2 ~ "~");
533                 case KeyCode.PAGEDOWN:
534                     return handleTextInput("\x1b[6" ~ flagsstr2 ~ "~");
535                 default:
536                     break;
537             }
538         }
539         if (event.action == KeyAction.KeyUp) {
540             switch (event.keyCode) {
541                 case KeyCode.ESCAPE:
542                 case KeyCode.RETURN:
543                 case KeyCode.TAB:
544                 case KeyCode.BACK:
545                 case KeyCode.F1:
546                 case KeyCode.F2:
547                 case KeyCode.F3:
548                 case KeyCode.F4:
549                 case KeyCode.F5:
550                 case KeyCode.F6:
551                 case KeyCode.F7:
552                 case KeyCode.F8:
553                 case KeyCode.F9:
554                 case KeyCode.F10:
555                 case KeyCode.F11:
556                 case KeyCode.F12:
557                 case KeyCode.UP:
558                 case KeyCode.DOWN:
559                 case KeyCode.LEFT:
560                 case KeyCode.RIGHT:
561                 case KeyCode.HOME:
562                 case KeyCode.END:
563                 case KeyCode.PAGEUP:
564                 case KeyCode.PAGEDOWN:
565                     return true;
566                 default:
567                     break;
568             }
569         }
570         return super.onKeyEvent(event);
571     }
572 
573     /** 
574     Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 
575 
576     */
577     override void measure(int parentWidth, int parentHeight) {
578         int w = (parentWidth == SIZE_UNSPECIFIED) ? font.charWidth('0') * 80 : parentWidth;
579         int h = (parentHeight == SIZE_UNSPECIFIED) ? font.height * 10 : parentHeight;
580         Rect rc = Rect(0, 0, w, h);
581         applyMargins(rc);
582         applyPadding(rc);
583         _verticalScrollBar.measure(rc.width, rc.height);
584         rc.right -= _verticalScrollBar.measuredWidth;
585         measuredContent(parentWidth, parentHeight, rc.width, rc.height);
586     }
587 
588     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
589     override void layout(Rect rc) {
590         if (visibility == Visibility.Gone) {
591             return;
592         }
593         _pos = rc;
594         _needLayout = false;
595         applyMargins(rc);
596         applyPadding(rc);
597         Rect sbrc = rc;
598         sbrc.left = sbrc.right - _verticalScrollBar.measuredWidth;
599         _verticalScrollBar.layout(sbrc);
600         rc.right = sbrc.left;
601         _content.layout(font, rc);
602         if (outputChars.length) {
603             // push buffered text
604             write(""d);
605             _needLayout = false;
606         }
607     }
608     /// Draw widget at its position to buffer
609     override void onDraw(DrawBuf buf) {
610         if (visibility != Visibility.Visible)
611             return;
612         Rect rc = _pos;
613         applyMargins(rc);
614         auto saver = ClipRectSaver(buf, rc, alpha);
615         DrawableRef bg = backgroundDrawable;
616         if (!bg.isNull) {
617             bg.drawTo(buf, rc, state);
618         }
619         applyPadding(rc);
620         _verticalScrollBar.onDraw(buf);
621         _content.draw(buf);
622     }
623 
624     private char[] outputBuffer;
625     // write utf 8
626     void write(string bytes) {
627         if (!bytes.length)
628             return;
629         import std.utf;
630         outputBuffer.assumeSafeAppend;
631         outputBuffer ~= bytes;
632         size_t index = 0;
633         dchar[] decoded;
634         decoded.assumeSafeAppend;
635         dchar ch = 0;
636         while (index < outputBuffer.length) {
637             size_t oldindex = index;
638             try {
639                 ch = decode(outputBuffer, index);
640                 decoded ~= ch;
641             } catch (UTFException e) {
642                 if (index + 4 <= outputBuffer.length) {
643                     // just append invalid character
644                     ch = '?';
645                     index++;
646                 }
647             }
648             if (oldindex == index)
649                 break;
650         }
651         if (index > 0) {
652             // move content
653             for (size_t i = 0; i + index < outputBuffer.length; i++)
654                 outputBuffer[i] = outputBuffer[i + index];
655             outputBuffer.length = outputBuffer.length - index;
656         }
657         if (decoded.length)
658             write(cast(dstring)decoded);
659     }
660 
661     static bool parseParam(dchar[] buf, ref int index, ref int value) {
662         if (index >= buf.length)
663             return false;
664         if (buf[index] < '0' || buf[index] > '9')
665             return false;
666         value = 0;
667         while (index < buf.length && buf[index] >= '0' && buf[index] <= '9') {
668             value = value * 10 + (buf[index] - '0');
669             index++;
670         }
671         return true;
672     }
673 
674     void handleInput(dstring chars) {
675         import std.utf;
676         _device.write(chars.toUTF8);
677     }
678 
679     void resetTerminal() {
680         _content.clear();
681         _content.updateScrollBar(_verticalScrollBar);
682     }
683 
684     private dchar[] outputChars;
685     // write utf32
686     void write(dstring chars) {
687         if (!chars.length && !outputChars.length)
688             return;
689         outputChars.assumeSafeAppend;
690         outputChars ~= chars;
691         if (!_content.width)
692             return;
693         uint i = 0;
694         for (; i < outputChars.length; i++) {
695             bool unfinished = false;
696             dchar ch = outputChars[i];
697             dchar ch2 = i + 1 < outputChars.length ? outputChars[i + 1] : 0;
698             dchar ch3 = i + 2 < outputChars.length ? outputChars[i + 2] : 0;
699             //dchar ch4 = i + 3 < outputChars.length ? outputChars[i + 3] : 0;
700             if (ch < ' ') {
701                 // control character
702                 if (ch == 27) {
703                     if (ch2 == 0)
704                         break; // unfinished ESC sequence
705                     // ESC sequence
706                     if (ch2 == '[') {
707                         // ESC [
708                         if (!ch3)
709                             break; // unfinished
710                         int param1 = -1;
711                         int param2 = -1;
712                         int[] extraParams;
713                         int index = i + 2;
714                         bool questionMark = false;
715                         if (index < outputChars.length && outputChars[index] == '?') {
716                             questionMark = true;
717                             index++;
718                         }
719                         parseParam(outputChars, index, param1);
720                         if (index < outputChars.length && outputChars[index] == ';') {
721                             index++;
722                             parseParam(outputChars, index, param2);
723                         }
724                         while (index < outputChars.length && outputChars[index] == ';') {
725                             index++;
726                             int n = -1;
727                             parseParam(outputChars, index, n);
728                             if (n >= 0)
729                                 extraParams ~= n;
730                         }
731                         if (index >= outputChars.length)
732                             break; // unfinished sequence: not enough chars
733                         int param1def1 = param1 >= 1 ? param1 : 1;
734                         ch3 = outputChars[index];
735                         i = index;
736                         if (ch3 == 'm') {
737                             // set attributes
738                             _content.setAttributes([param1, param2]);
739                             if (extraParams.length)
740                                 _content.setAttributes(extraParams);
741                         }
742                         // command is parsed completely, ch3 == command type char
743 
744                         // ESC[7h and ESC[7l -- enable/disable line wrap
745                         if (param1 == '7' && (ch3 == 'h' || ch3 == 'l')) {
746                             _content.lineWrap(ch3 == 'h');
747                             continue;
748                         }
749                         if (ch3 == 'H' || ch3 == 'f') {
750                             _content.moveCursorTo(param2, param1);
751                             continue;
752                         }
753                         if (ch3 == 'A') { // cursor up
754                             _content.moveCursorBy(0, -param1def1);
755                             continue;
756                         }
757                         if (ch3 == 'B') { // cursor down
758                             _content.moveCursorBy(0, param1def1);
759                             continue;
760                         }
761                         if (ch3 == 'C') { // cursor forward
762                             _content.moveCursorBy(param1def1, 0);
763                             continue;
764                         }
765                         if (ch3 == 'D') { // cursor back
766                             _content.moveCursorBy(-param1def1, 0);
767                             continue;
768                         }
769                         if (ch3 == 'K' || ch3 == 'J') {
770                             _content.eraseScreen(param1, ch3 == 'K');
771                             continue;
772                         }
773                     } else switch(ch2) {
774                     case 'c':
775                         _content.resetTerminal();
776                         i++;
777                         break;
778                     case '=': // Set alternate keypad mode
779                     case '>': // Set numeric keypad mode
780                     case 'N': // Set single shift 2
781                     case 'O': // Set single shift 3
782                     case 'H': // Set a tab at the current column
783                     case '<': // Enter/exit ANSI mode (VT52)
784                         i++;
785                         // ignore
786                         break;
787                     case '(': // default font
788                     case ')': // alternate font
789                         i++;
790                         i++;
791                         // ignore
792                         break;
793                     default:
794                         // unsupported
795                         break;
796                     }
797                     if (unfinished)
798                         break;
799                 } else switch(ch) {
800                     case '\a': // bell
801                     case '\f': // form feed
802                     case '\v': // vtab
803                     case '\r': // cr
804                     case '\n': // lf
805                     case '\t': // tab
806                     case '\b': // backspace
807                         _content.putChar(ch);
808                         break;
809                     default:
810                         break;
811                 }
812             } else {
813                 _content.putChar(ch);
814             }
815         }
816         if (i > 0) {
817             if (i == outputChars.length)
818                 outputChars.length = 0;
819             else {
820                 for (uint j = 0; j + i < outputChars.length; j++)
821                     outputChars[j] = outputChars[j + i];
822                 outputChars.length = outputChars.length - i;
823             }
824         }
825         _content.updateScrollBar(_verticalScrollBar);
826     }
827 
828     /// override to handle focus changes
829     override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
830         if (focused)
831             _content.focused = true;
832         else {
833             _content.focused = false;
834         }
835         super.handleFocusChange(focused);
836     }
837 
838 }
839 
840 import core.thread;
841 
842 interface TerminalInputHandler {
843     void onBytesReceived(string data);
844 }
845 
846 class TerminalDevice : Thread {
847     Signal!TerminalInputHandler onBytesRead;
848     version (Windows) {
849         import core.sys.windows.windows;
850         HANDLE hpipe;
851     } else {
852         int masterfd;
853         import core.sys.posix.fcntl: open_=open, O_WRONLY;
854         import core.sys.posix.unistd: close_=close;
855         import core.sys.posix.unistd: write_=write;
856         import core.sys.posix.unistd: read_=read;
857     }
858     @property string deviceName() { return _name; }
859     private string _name;
860     private bool started;
861     private bool closed;
862     private bool connected;
863 
864     this() {
865         super(&threadProc);
866     }
867     ~this() {
868         close();
869     }
870 
871     void threadProc() {
872         started = true;
873         Log.d("TerminalDevice threadProc() enter");
874         version(Windows) {
875             while (!closed) {
876                 Log.d("TerminalDevice -- Waiting for client");
877                 if (ConnectNamedPipe(hpipe, null)) {
878                     connected = true;
879                     // accept client
880                     Log.d("TerminalDevice client connected");
881                     char[16384] buf;
882                     for (;;) {
883                         if (closed)
884                             break;
885                         DWORD bytesRead = 0;
886                         DWORD bytesAvail = 0;
887                         DWORD bytesInMessage = 0;
888                         // read data from client
889                         //Log.d("TerminalDevice reading from pipe");
890                         if (!PeekNamedPipe(hpipe, buf.ptr, cast(DWORD)1 /*buf.length*/, &bytesRead, &bytesAvail, &bytesInMessage)) {
891                             break;
892                         }
893                         if (closed)
894                             break;
895                         if (!bytesRead) {
896                             Sleep(10);
897                             continue;
898                         }
899                         if (ReadFile(hpipe, &buf, 1, &bytesRead, null)) { //buf.length
900                             Log.d("TerminalDevice bytes read: ", bytesRead);
901                             if (closed)
902                                 break;
903                             if (bytesRead && onBytesRead.assigned) {
904                                 onBytesRead(buf[0 .. bytesRead].dup);
905                             }
906                         } else {
907                             break;
908                         }
909                     }
910                     Log.d("TerminalDevice client disconnecting");
911                     connected = false;
912                     // disconnect client
913                     FlushFileBuffers(hpipe); 
914                     DisconnectNamedPipe(hpipe);
915                 }
916             }
917         } else {
918             // posix
919             char[4096] buf;
920             while(!closed) {
921                 auto bytesRead = read_(masterfd, buf.ptr, buf.length);
922                 if (closed)
923                     break;
924                 if (bytesRead > 0 && onBytesRead.assigned) {
925                     onBytesRead(buf[0 .. bytesRead].dup);
926                 }
927             }
928         }
929         Log.d("TerminalDevice threadProc() exit");
930     }
931 
932     bool write(string msg) {
933         if (!msg.length)
934             return true;
935         if (closed || !started)
936             return false;
937         version (Windows) {
938             if (!connected)
939                 return false;
940             for (;;) {
941                 DWORD bytesWritten = 0;
942                 if (WriteFile(hpipe, cast(char*)msg.ptr, cast(int)msg.length, &bytesWritten, null) != TRUE) {
943                     return false;
944                 }
945                 if (bytesWritten < msg.length)
946                     msg = msg[bytesWritten .. $];
947                 else
948                     break;
949             }
950         } else {
951             // linux/posix
952             if (masterfd && masterfd != -1) {
953                 auto bytesRead = write_(masterfd, msg.ptr, msg.length);
954             }
955             
956         }
957         return true;
958     }
959     void close() {
960         if (closed)
961             return;
962         closed = true;
963         if (!started)
964             return;
965         version (Windows) {
966             import std..string;
967             // ping terminal to handle closed flag
968             HANDLE h = CreateFileA(
969                                _name.toStringz,   // pipe name 
970                                GENERIC_READ |  // read and write access 
971                                GENERIC_WRITE,
972                                0,              // no sharing 
973                                null,           // default security attributes
974                                OPEN_EXISTING,  // opens existing pipe 
975                                0,              // default attributes 
976                                null);
977             if (h != INVALID_HANDLE_VALUE) {
978                 DWORD bytesWritten = 0;
979                 WriteFile(h, "stop".ptr, 4, &bytesWritten, null);
980                 CloseHandle(h);
981             }
982         } else {
983             if (masterfd && masterfd != -1) {
984                 import std..string;
985                 int clientfd = open_(_name.toStringz, O_WRONLY);
986                 write_(clientfd, "1".ptr, 1);
987                 close_(clientfd);
988             }
989         }
990         join(false);
991         version (Windows) {
992             if (hpipe && hpipe != INVALID_HANDLE_VALUE) {
993                 CloseHandle(hpipe);
994                 hpipe = null;
995             }
996         } else {
997             if (masterfd && masterfd != -1) {
998                 close_(masterfd);
999                 masterfd = 0;
1000             }
1001         }
1002             _name = null;
1003     }
1004     bool create() {
1005         import std..string;
1006         version (Windows) {
1007             import std.uuid;
1008             _name = "\\\\.\\pipe\\dlangide-terminal-" ~ randomUUID().toString;
1009             SECURITY_ATTRIBUTES sa;
1010             sa.nLength = sa.sizeof;
1011             sa.bInheritHandle = TRUE;
1012             hpipe = CreateNamedPipeA(cast(const(char)*)_name.toStringz, 
1013                              PIPE_ACCESS_DUPLEX | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_FIRST_PIPE_INSTANCE, // dwOpenMode
1014                              //PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, // | PIPE_REJECT_REMOTE_CLIENTS,
1015                              PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, // | PIPE_REJECT_REMOTE_CLIENTS,
1016                              1,
1017                              1, //16384,
1018                              1, //16384,
1019                              20,
1020                              &sa);
1021             if (hpipe == INVALID_HANDLE_VALUE) {
1022                 Log.e("Failed to create named pipe for terminal, error=", GetLastError());
1023                 close();
1024                 return false;
1025             }
1026         } else {
1027             const(char) * s = null;
1028             {
1029                 import core.sys.posix.fcntl;
1030                 import core.sys.posix.stdio;
1031                 import core.sys.posix.stdlib;
1032                 //import core.sys.posix.unistd;
1033                 masterfd = posix_openpt(O_RDWR | O_NOCTTY | O_SYNC);
1034                 if (masterfd == -1) {
1035                     Log.e("posix_openpt failed - cannot open terminal");
1036                     close();
1037                     return false;
1038                 }
1039                 if (grantpt(masterfd) == -1 || unlockpt(masterfd) == -1) {
1040                     Log.e("grantpt / unlockpt failed - cannot open terminal");
1041                     close();
1042                     return false;
1043                 }
1044                 s = ptsname(masterfd);
1045                 if (!s) {
1046                     Log.e("ptsname failed - cannot open terminal");
1047                     close();
1048                     return false;
1049                 }
1050             }
1051             _name = fromStringz(s).dup;
1052         }
1053         Log.i("ptty device created: ", _name);
1054         start();
1055         return true;
1056     }
1057 }
1058