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