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