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