1 /++ 2 This is an extendible unix terminal emulator and some helper functions to help actually implement one. 3 4 You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it. 5 6 See minigui_addons/terminal_emulator_widget in arsd repo or nestedterminalemulator.d or main.d in my terminal-emulator repo for how I did it. 7 8 History: 9 Written September/October 2013ish. Moved to arsd 2020-03-26. 10 +/ 11 module arsd.terminalemulator; 12 13 /+ 14 FIXME 15 terminal optimization: 16 first invalidated + last invalidated to slice the array 17 when looking for things that need redrawing. 18 19 FIXME: writing a line in color then a line in ordinary does something 20 wrong. 21 22 huh if i do underline then change color it undoes the underline 23 24 FIXME: make shift+enter send something special to the application 25 and shift+space, etc. 26 identify itself somehow too for client extensions 27 ctrl+space is supposed to send char 0. 28 29 ctrl+click on url pattern could open in browser perhaps 30 31 FIXME: scroll stuff should be higher level in the implementation. 32 so like scroll Rect, DirectionAndAmount 33 34 There should be a redraw thing that is given batches of instructions 35 in here that the other thing just implements. 36 37 FIXME: the save stack stuff should do cursor style too 38 39 40 +/ 41 42 import arsd.color; 43 import std.algorithm : max; 44 45 enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:"; 46 47 /+ 48 The ;90 ones are my extensions. 49 50 90 - clipboard extensions 51 91 - image extensions 52 92 - hyperlink extensions 53 +/ 54 enum terminalIdCode = "\033[?64;1;2;6;9;15;16;17;18;21;22;28;90;91;92c"; 55 56 interface NonCharacterData { 57 //const(ubyte)[] serialize(); 58 } 59 60 struct BinaryDataTerminalRepresentation { 61 int width; 62 int height; 63 TerminalEmulator.TerminalCell[] representation; 64 } 65 66 // old name, don't use in new programs anymore. 67 deprecated alias BrokenUpImage = BinaryDataTerminalRepresentation; 68 69 struct CustomGlyph { 70 TrueColorImage image; 71 dchar substitute; 72 } 73 74 void unknownEscapeSequence(in char[] esc) { 75 import std.file; 76 version(Posix) { 77 debug append("/tmp/arsd-te-bad-esc-sequences.txt", esc ~ "\n"); 78 } else { 79 debug append("arsd-te-bad-esc-sequences.txt", esc ~ "\n"); 80 } 81 } 82 83 // This is used for the double-click word selection 84 bool isWordSeparator(dchar ch) { 85 return ch == ' ' || ch == '"' || ch == '<' || ch == '>' || ch == '(' || ch == ')' || ch == ','; 86 } 87 88 TerminalEmulator.TerminalCell[] sliceTrailingWhitespace(TerminalEmulator.TerminalCell[] t) { 89 size_t end = t.length; 90 while(end >= 1) { 91 if(t[end-1].hasNonCharacterData || t[end-1].ch != ' ') 92 break; 93 end--; 94 } 95 96 t = t[0 .. end]; 97 98 /* 99 import std.stdio; 100 foreach(ch; t) 101 write(ch.ch); 102 writeln("*"); 103 */ 104 105 return t; 106 } 107 108 struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) { 109 T[maxSize] bufferInternal; 110 T[] buffer; 111 size_t length; 112 bool isNull = true; 113 T[] opSlice() { return isNull ? null : buffer[0 .. length]; } 114 115 static if(is(T == char)) 116 void appendIntAsString(int n) { 117 import std.conv; 118 this ~= to!string(n); 119 } 120 121 void opOpAssign(string op : "~")(in T rhs) { 122 if(buffer is null) buffer = bufferInternal[]; 123 isNull = false; 124 static if(allowGrowth) { 125 if(this.length == buffer.length) 126 buffer.length = buffer.length * 2; 127 128 buffer[this.length++] = rhs; 129 } else { 130 if(this.length < buffer.length) // i am silently discarding more crap 131 buffer[this.length++] = rhs; 132 } 133 } 134 void opOpAssign(string op : "~")(in T[] rhs) { 135 if(buffer is null) buffer = bufferInternal[]; 136 isNull = false; 137 buffer[this.length .. this.length + rhs.length] = rhs[]; 138 this.length += rhs.length; 139 } 140 void opAssign(in T[] rhs) { 141 isNull = rhs is null; 142 if(buffer is null) buffer = bufferInternal[]; 143 buffer[0 .. rhs.length] = rhs[]; 144 this.length = rhs.length; 145 } 146 void opAssign(typeof(null)) { 147 isNull = true; 148 length = 0; 149 } 150 T opIndex(size_t idx) { 151 assert(!isNull); 152 assert(idx < length); 153 return buffer[idx]; 154 } 155 void clear() { 156 isNull = true; 157 length = 0; 158 } 159 } 160 161 /** 162 An abstract class that does terminal emulation. You'll have to subclass it to make it work. 163 164 The terminal implements a subset of what xterm does and then, optionally, some special features. 165 166 Its linear mode (normal) screen buffer is infinitely long and infinitely wide. It is the responsibility 167 of your subclass to do line wrapping, etc., for display. This i think is actually incompatible with xterm but meh. 168 169 actually maybe it *should* automatically wrap them. idk. I think GNU screen does both. FIXME decide. 170 171 Its cellular mode (alternate) screen buffer can be any size you want. 172 */ 173 class TerminalEmulator { 174 /* override these to do stuff on the interface. 175 You might be able to stub them out if there's no state maintained on the target, since TerminalEmulator maintains its own internal state */ 176 protected abstract void changeWindowTitle(string); /// the title of the window 177 protected abstract void changeIconTitle(string); /// the shorter window/iconified window 178 179 protected abstract void changeWindowIcon(IndexedImage); /// change the window icon. note this may be null 180 181 protected abstract void changeCursorStyle(CursorStyle); /// cursor style 182 183 protected abstract void changeTextAttributes(TextAttributes); /// current text output attributes 184 protected abstract void soundBell(); /// sounds the bell 185 protected abstract void sendToApplication(scope const(void)[]); /// send some data to the program running in the terminal, so keypresses etc. 186 187 protected abstract void copyToClipboard(string); /// copy the given data to the clipboard (or you can do nothing if you can't) 188 protected abstract void pasteFromClipboard(void delegate(in char[])); /// requests a paste. we pass it a delegate that should accept the data 189 190 protected abstract void copyToPrimary(string); /// copy the given data to the PRIMARY X selection (or you can do nothing if you can't) 191 protected abstract void pasteFromPrimary(void delegate(in char[])); /// requests a paste from PRIMARY. we pass it a delegate that should accept the data 192 193 abstract protected void requestExit(); /// the program is finished and the terminal emulator is requesting you to exit 194 195 /// Signal the UI that some attention should be given, e.g. blink the taskbar or sound the bell. 196 /// The default is to ignore the demand by instantly acknowledging it - if you override this, do NOT call super(). 197 protected void demandAttention() { 198 attentionReceived(); 199 } 200 201 /// After it demands attention, call this when the attention has been received 202 /// you may call it immediately to ignore the demand (the default) 203 public void attentionReceived() { 204 attentionDemanded = false; 205 } 206 207 protected final { 208 version(invalidator_2) { 209 int invalidatedMin; 210 int invalidatedMax; 211 } 212 213 void clearInvalidatedRange() { 214 version(invalidator_2) { 215 invalidatedMin = int.max; 216 invalidatedMax = 0; 217 } 218 } 219 220 void extendInvalidatedRange() { 221 version(invalidator_2) { 222 invalidatedMin = 0; 223 invalidatedMax = int.max; 224 } 225 } 226 227 void extendInvalidatedRange(int x, int y, int x2, int y2) { 228 version(invalidator_2) { 229 extendInvalidatedRange(y * screenWidth + x, y2 * screenWidth + x2); 230 } 231 } 232 233 void extendInvalidatedRange(int o1, int o2) { 234 version(invalidator_2) { 235 if(o1 < invalidatedMin) 236 invalidatedMin = o1; 237 if(o2 > invalidatedMax) 238 invalidatedMax = o2; 239 240 if(invalidatedMax < invalidatedMin) 241 invalidatedMin = invalidatedMax; 242 } 243 } 244 } 245 246 // I believe \033[50buffer[] and up are available for extensions everywhere. 247 // when keys are shifted, xterm sends them as \033[1;2F for example with end. but is this even sane? how would we do it with say, F5? 248 // apparently shifted F5 is ^[[15;2~ 249 // alt + f5 is ^[[15;3~ 250 // alt+shift+f5 is ^[[15;4~ 251 252 private string pasteDataPending = null; 253 254 protected void justRead() { 255 if(pasteDataPending.length) { 256 sendPasteData(pasteDataPending); 257 import core.thread; Thread.sleep(50.msecs); // hack to keep it from closing, broken pipe i think 258 } 259 } 260 261 // my custom extension.... the data is the text content of the link, the identifier is some bits attached to the unit 262 public void sendHyperlinkData(scope const(dchar)[] data, uint identifier) { 263 if(bracketedHyperlinkMode) { 264 sendToApplication("\033[220~"); 265 266 import std.conv; 267 // FIXME: that second 0 is a "command", like which menu option, which mouse button, etc. 268 sendToApplication(to!string(identifier) ~ ";0;" ~ to!string(data)); 269 270 sendToApplication("\033[221~"); 271 } else { 272 // without bracketed hyperlink, it simulates a paste 273 import std.conv; 274 sendPasteData(to!string(data)); 275 } 276 } 277 278 public void sendPasteData(scope const(char)[] data) { 279 //if(pasteDataPending.length) 280 //throw new Exception("paste data being discarded, wtf, shouldnt happen"); 281 282 // FIXME: i should put it all together so the brackets don't get separated by threads 283 284 if(bracketedPasteMode) 285 sendToApplication("\033[200~"); 286 287 version(use_libssh2) 288 enum MAX_PASTE_CHUNK = 1024 * 40; 289 else 290 enum MAX_PASTE_CHUNK = 1024 * 1024 * 10; 291 292 if(data.length > MAX_PASTE_CHUNK) { 293 // need to chunk it in order to receive echos, etc, 294 // to avoid deadlocks 295 pasteDataPending = data[MAX_PASTE_CHUNK .. $].idup; 296 data = data[0 .. MAX_PASTE_CHUNK]; 297 } else { 298 pasteDataPending = null; 299 } 300 301 if(data.length) 302 sendToApplication(data); 303 304 if(bracketedPasteMode) 305 sendToApplication("\033[201~"); 306 } 307 308 private string overriddenSelection; 309 protected void cancelOverriddenSelection() { 310 if(overriddenSelection.length == 0) 311 return; 312 overriddenSelection = null; 313 sendToApplication("\033[27;0;987136~"); // fake "select none" key, see terminal.d's ProprietaryPseudoKeys for values. 314 315 // The reason that proprietary thing is ok is setting the selection is itself a proprietary extension 316 // so if it was ever set, it implies the user code is familiar with our magic. 317 } 318 319 public string getSelectedText() { 320 if(overriddenSelection.length) 321 return overriddenSelection; 322 return getPlainText(selectionStart, selectionEnd); 323 } 324 325 bool dragging; 326 int lastDragX, lastDragY; 327 public bool sendMouseInputToApplication(int termX, int termY, MouseEventType type, MouseButton button, bool shift, bool ctrl, bool alt) { 328 if(termX < 0) 329 termX = 0; 330 if(termX >= screenWidth) 331 termX = screenWidth - 1; 332 if(termY < 0) 333 termY = 0; 334 if(termY >= screenHeight) 335 termY = screenHeight - 1; 336 337 /+ 338 version(Windows) { 339 // I'm swapping these because my laptop doesn't have a middle button, 340 // and putty swaps them too by default so whatevs. 341 if(button == MouseButton.right) 342 button = MouseButton.middle; 343 else if(button == MouseButton.middle) 344 button = MouseButton.right; 345 } 346 +/ 347 348 int baseEventCode() { 349 int b; 350 // lol the xterm mouse thing sucks like javascript! unbelievable 351 // it doesn't support two buttons at once... 352 if(button == MouseButton.left) 353 b = 0; 354 else if(button == MouseButton.right) 355 b = 2; 356 else if(button == MouseButton.middle) 357 b = 1; 358 else if(button == MouseButton.wheelUp) 359 b = 64 | 0; 360 else if(button == MouseButton.wheelDown) 361 b = 64 | 1; 362 else 363 b = 3; // none pressed or button released 364 365 if(shift) 366 b |= 4; 367 if(ctrl) 368 b |= 16; 369 if(alt) // sending alt as meta 370 b |= 8; 371 372 if(!sgrMouseMode) 373 b |= 32; // it just always does this 374 375 return b; 376 } 377 378 379 if(type == MouseEventType.buttonReleased) { 380 // X sends press and release on wheel events, but we certainly don't care about those 381 if(button == MouseButton.wheelUp || button == MouseButton.wheelDown) 382 return false; 383 384 if(dragging) { 385 auto text = getSelectedText(); 386 if(text.length) { 387 copyToPrimary(text); 388 } else if(!mouseButtonReleaseTracking || shift || (selectiveMouseTracking && ((!alternateScreenActive || scrollingBack) || termY != 0) && termY != cursorY)) { 389 // hyperlink check 390 int idx = termY * screenWidth + termX; 391 auto screen = (alternateScreenActive ? alternateScreen : normalScreen); 392 393 if(screen[idx].hyperlinkStatus & 0x01) { 394 // it is a link! need to find the beginning and the end 395 auto start = idx; 396 auto end = idx; 397 auto value = screen[idx].hyperlinkStatus; 398 while(start > 0 && screen[start].hyperlinkStatus == value) 399 start--; 400 if(screen[start].hyperlinkStatus != value) 401 start++; 402 while(end < screen.length && screen[end].hyperlinkStatus == value) 403 end++; 404 405 uint number; 406 dchar[64] buffer; 407 foreach(i, ch; screen[start .. end]) { 408 if(i >= buffer.length) 409 break; 410 if(!ch.hasNonCharacterData) 411 buffer[i] = ch.ch; 412 if(i < 16) { 413 number |= (ch.hyperlinkBit ? 1 : 0) << i; 414 } 415 } 416 417 if((cast(size_t) (end - start)) <= buffer.length) 418 sendHyperlinkData(buffer[0 .. end - start], number); 419 } 420 } 421 } 422 423 dragging = false; 424 if(mouseButtonReleaseTracking) { 425 int b = baseEventCode; 426 if(!sgrMouseMode) 427 b |= 3; // always send none / button released 428 sendMouseEvent(b, termX, termY, true); 429 } 430 } 431 432 if(type == MouseEventType.motion) { 433 if(termX != lastDragX || termY != lastDragY) { 434 lastDragY = termY; 435 lastDragX = termX; 436 if(mouseMotionTracking || (mouseButtonMotionTracking && button)) { 437 int b = baseEventCode; 438 sendMouseEvent(b + 32, termX, termY); 439 } 440 441 if(dragging) { 442 auto idx = termY * screenWidth + termX; 443 444 // the no-longer-selected portion needs to be invalidated 445 int start, end; 446 if(idx > selectionEnd) { 447 start = selectionEnd; 448 end = idx; 449 } else { 450 start = idx; 451 end = selectionEnd; 452 } 453 if(start < 0 || end >= ((alternateScreenActive ? alternateScreen.length : normalScreen.length))) 454 return false; 455 456 foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) { 457 cell.invalidated = true; 458 cell.selected = false; 459 } 460 461 extendInvalidatedRange(start, end); 462 463 cancelOverriddenSelection(); 464 selectionEnd = idx; 465 466 // and the freshly selected portion needs to be invalidated 467 if(selectionStart > selectionEnd) { 468 start = selectionEnd; 469 end = selectionStart; 470 } else { 471 start = selectionStart; 472 end = selectionEnd; 473 } 474 foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) { 475 cell.invalidated = true; 476 cell.selected = true; 477 } 478 479 extendInvalidatedRange(start, end); 480 481 return true; 482 } 483 } 484 } 485 486 if(type == MouseEventType.buttonPressed) { 487 // double click detection 488 import std.datetime; 489 static SysTime lastClickTime; 490 static int consecutiveClicks = 1; 491 492 if(button != MouseButton.wheelUp && button != MouseButton.wheelDown) { 493 if(Clock.currTime() - lastClickTime < dur!"msecs"(350)) 494 consecutiveClicks++; 495 else 496 consecutiveClicks = 1; 497 498 lastClickTime = Clock.currTime(); 499 } 500 // end dbl click 501 502 if(!(shift) && mouseButtonTracking) { 503 if(selectiveMouseTracking && termY != 0 && termY != cursorY) { 504 if(button == MouseButton.left || button == MouseButton.right) 505 goto do_default_behavior; 506 if((!alternateScreenActive || scrollingBack) && (button == MouseButton.wheelUp || button.MouseButton.wheelDown)) 507 goto do_default_behavior; 508 } 509 // top line only gets special cased on full screen apps 510 if(selectiveMouseTracking && (!alternateScreenActive || scrollingBack) && termY == 0 && cursorY != 0) 511 goto do_default_behavior; 512 513 int b = baseEventCode; 514 515 sendMouseEvent(b, termX, termY); 516 //buffer ~= cast(char) (x + 32); 517 //buffer ~= cast(char) (y + 32); 518 } else { 519 do_default_behavior: 520 if(button == MouseButton.middle) { 521 pasteFromPrimary(&sendPasteData); 522 } 523 524 if(button == MouseButton.wheelUp) { 525 scrollback(alt ? 0 : (ctrl ? 10 : 1), alt ? -(ctrl ? 10 : 1) : 0); 526 return true; 527 } 528 if(button == MouseButton.wheelDown) { 529 scrollback(alt ? 0 : -(ctrl ? 10 : 1), alt ? (ctrl ? 10 : 1) : 0); 530 return true; 531 } 532 533 if(button == MouseButton.left) { 534 // we invalidate the old selection since it should no longer be highlighted... 535 makeSelectionOffsetsSane(selectionStart, selectionEnd); 536 537 cancelOverriddenSelection(); 538 539 auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen); 540 foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) { 541 cell.invalidated = true; 542 cell.selected = false; 543 } 544 545 extendInvalidatedRange(selectionStart, selectionEnd); 546 547 if(consecutiveClicks == 1) { 548 selectionStart = termY * screenWidth + termX; 549 selectionEnd = selectionStart; 550 } else if(consecutiveClicks == 2) { 551 selectionStart = termY * screenWidth + termX; 552 selectionEnd = selectionStart; 553 while(selectionStart > 0 && !isWordSeparator((*activeScreen)[selectionStart-1].ch)) { 554 selectionStart--; 555 } 556 557 while(selectionEnd < (*activeScreen).length && !isWordSeparator((*activeScreen)[selectionEnd].ch)) { 558 selectionEnd++; 559 } 560 561 } else if(consecutiveClicks == 3) { 562 selectionStart = termY * screenWidth; 563 selectionEnd = selectionStart + screenWidth; 564 } 565 dragging = true; 566 lastDragX = termX; 567 lastDragY = termY; 568 569 // then invalidate the new selection as well since it should be highlighted 570 foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[selectionStart .. selectionEnd]) { 571 cell.invalidated = true; 572 cell.selected = true; 573 } 574 extendInvalidatedRange(selectionStart, selectionEnd); 575 576 return true; 577 } 578 if(button == MouseButton.right) { 579 580 int changed1; 581 int changed2; 582 583 cancelOverriddenSelection(); 584 585 auto click = termY * screenWidth + termX; 586 if(click < selectionStart) { 587 auto oldSelectionStart = selectionStart; 588 selectionStart = click; 589 changed1 = selectionStart; 590 changed2 = oldSelectionStart; 591 } else if(click > selectionEnd) { 592 auto oldSelectionEnd = selectionEnd; 593 selectionEnd = click; 594 595 changed1 = oldSelectionEnd; 596 changed2 = selectionEnd; 597 } 598 599 foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[changed1 .. changed2]) { 600 cell.invalidated = true; 601 cell.selected = true; 602 } 603 604 extendInvalidatedRange(changed1, changed2); 605 606 auto text = getPlainText(selectionStart, selectionEnd); 607 if(text.length) { 608 copyToPrimary(text); 609 } 610 return true; 611 } 612 } 613 } 614 615 return false; 616 } 617 618 private void sendMouseEvent(int b, int x, int y, bool isRelease = false) { 619 620 ScopeBuffer!(char, 16) buffer; 621 622 if(sgrMouseMode) { 623 buffer ~= "\033[<"; 624 buffer.appendIntAsString(b); 625 buffer ~= ";"; 626 buffer.appendIntAsString(x + 1); 627 buffer ~= ";"; 628 buffer.appendIntAsString(y + 1); 629 buffer ~= isRelease ? "m" : "M"; 630 } else { 631 buffer ~= "\033[M"; 632 buffer ~= cast(char) b; 633 634 // 1-based stuff and 32 is the base value 635 x += 1 + 32; 636 y += 1 + 32; 637 638 if(utf8MouseMode) { 639 import std.utf; 640 char[4] str; 641 642 foreach(char ch; str[0 .. encode(str, x)]) 643 buffer ~= ch; 644 645 foreach(char ch; str[0 .. encode(str, y)]) 646 buffer ~= ch; 647 } else { 648 buffer ~= cast(char) x; 649 buffer ~= cast(char) y; 650 } 651 } 652 653 sendToApplication(buffer[]); 654 } 655 656 protected void returnToNormalScreen() { 657 alternateScreenActive = false; 658 659 if(cueScrollback) { 660 showScrollbackOnScreen(normalScreen, 0, true, 0); 661 newLine(false); 662 cueScrollback = false; 663 } 664 665 notifyScrollbarRelevant(true, true); 666 extendInvalidatedRange(); 667 } 668 669 protected void outputOccurred() { } 670 671 private int selectionStart; // an offset into the screen buffer 672 private int selectionEnd; // ditto 673 674 void requestRedraw() {} 675 676 677 private bool skipNextChar; 678 // assuming Key is an enum with members just like the one in simpledisplay.d 679 // returns true if it was handled here 680 protected bool defaultKeyHandler(Key)(Key key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) { 681 enum bool KeyHasNamedAscii = is(typeof(Key.A)); 682 683 static string magic() { 684 string code; 685 foreach(member; __traits(allMembers, TerminalKey)) 686 if(member != "Escape") 687 code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ " 688 , shift ?true:false 689 , alt ?true:false 690 , ctrl ?true:false 691 , windows ?true:false 692 )) requestRedraw(); return true;"; 693 return code; 694 } 695 696 void specialAscii(dchar what) { 697 if(!alt) 698 skipNextChar = true; 699 if(sendKeyToApplication( 700 cast(TerminalKey) what 701 , shift ? true:false 702 , alt ? true:false 703 , ctrl ? true:false 704 , windows ? true:false 705 )) requestRedraw(); 706 } 707 708 static if(KeyHasNamedAscii) { 709 enum Space = Key.Space; 710 enum Enter = Key.Enter; 711 enum Backspace = Key.Backspace; 712 enum Tab = Key.Tab; 713 enum Escape = Key.Escape; 714 } else { 715 enum Space = ' '; 716 enum Enter = '\n'; 717 enum Backspace = '\b'; 718 enum Tab = '\t'; 719 enum Escape = '\033'; 720 } 721 722 723 switch(key) { 724 //// I want the escape key to send twice to differentiate it from 725 //// other escape sequences easily. 726 //case Key.Escape: sendToApplication("\033"); break; 727 728 /* 729 case Key.V: 730 case Key.C: 731 if(shift && ctrl) { 732 skipNextChar = true; 733 if(key == Key.V) 734 pasteFromClipboard(&sendPasteData); 735 else if(key == Key.C) 736 copyToClipboard(getSelectedText()); 737 } 738 break; 739 */ 740 741 // expansion of my own for like shift+enter to terminal.d users 742 case Enter, Backspace, Tab, Escape: 743 if(shift || alt || ctrl) { 744 static if(KeyHasNamedAscii) { 745 specialAscii( 746 cast(TerminalKey) ( 747 key == Key.Enter ? '\n' : 748 key == Key.Tab ? '\t' : 749 key == Key.Backspace ? '\b' : 750 key == Key.Escape ? '\033' : 751 0 /* assert(0) */ 752 ) 753 ); 754 } else { 755 specialAscii(key); 756 } 757 return true; 758 } 759 break; 760 case Space: 761 if(alt) { // it used to be shift || alt here, but like shift+space is more trouble than it is worth in actual usage experience. too easily to accidentally type it in the middle of something else to be unambiguously useful. I wouldn't even set a hotkey on it so gonna just send it as plain space always. 762 // ctrl+space sends 0 per normal translation char rules 763 specialAscii(' '); 764 return true; 765 } 766 break; 767 768 mixin(magic()); 769 770 static if(is(typeof(Key.Shift))) { 771 // modifiers are not ascii, ignore them here 772 case Key.Shift, Key.Ctrl, Key.Alt, Key.Windows, Key.Alt_r, Key.Shift_r, Key.Ctrl_r, Key.CapsLock, Key.NumLock: 773 // nor are these special keys that don't return characters 774 case Key.Menu, Key.Pause, Key.PrintScreen: 775 return false; 776 } 777 778 default: 779 // alt basically always get special treatment, since it doesn't 780 // generate anything from the char handler. but shift and ctrl 781 // do, so we'll just use that unless both are pressed, in which 782 // case I want to go custom to differentiate like ctrl+c from ctrl+shift+c and such. 783 784 // FIXME: xterm offers some control on this, see: https://invisible-island.net/xterm/xterm.faq.html#xterm_modother 785 if(alt || (shift && ctrl)) { 786 if(key >= 'A' && key <= 'Z') 787 key += 32; // always use lowercase for as much consistency as we can since the shift modifier need not apply here. Windows' keysyms are uppercase while X's are lowercase too 788 specialAscii(key); 789 if(!alt) 790 skipNextChar = true; 791 return true; 792 } 793 } 794 795 return true; 796 } 797 protected bool defaultCharHandler(dchar c) { 798 if(skipNextChar) { 799 skipNextChar = false; 800 return true; 801 } 802 803 endScrollback(); 804 char[4] str; 805 char[5] send; 806 807 import std.utf; 808 //if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10 809 auto data = str[0 .. encode(str, c)]; 810 811 // on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler. 812 if(c != 127) 813 sendToApplication(data); 814 815 return true; 816 } 817 818 /// Send a non-character key sequence 819 public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) { 820 bool redrawRequired = false; 821 822 if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) { 823 toggleScrollLock(); 824 return true; 825 } 826 827 /* 828 So ctrl + A-Z, [, \, ], ^, and _ are all chars 1-31 829 ctrl+5 send ^] 830 831 FIXME: for alt+keys and the other ctrl+them, send the xterm ascii magc thing terminal.d knows how to use 832 */ 833 834 // scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen 835 // doesn't have scrollback anyway. Thus the key will be forwarded to the application. 836 if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageUp && (shift || scrollLock)) { 837 scrollback(10); 838 return true; 839 } else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageDown && (shift || scrollLock)) { 840 scrollback(-10); 841 return true; 842 } else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Left && (shift || scrollLock)) { 843 scrollback(0, ctrl ? -10 : -1); 844 return true; 845 } else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Right && (shift || scrollLock)) { 846 scrollback(0, ctrl ? 10 : 1); 847 return true; 848 } else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Up && (shift || scrollLock)) { 849 scrollback(ctrl ? 10 : 1); 850 return true; 851 } else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Down && (shift || scrollLock)) { 852 scrollback(ctrl ? -10 : -1); 853 return true; 854 } else if((!alternateScreenActive || scrollingBack)) { // && ev.key != Key.Shift && ev.key != Key.Shift_r) { 855 if(endScrollback()) 856 redrawRequired = true; 857 } 858 859 860 861 void sendToApplicationModified(string s, int key = 0) { 862 bool anyModifier = shift || alt || ctrl || windows; 863 if(!anyModifier || applicationCursorKeys) 864 sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh 865 else { 866 ScopeBuffer!(char, 16) modifierNumber; 867 char otherModifier = 0; 868 if(shift && alt && ctrl) modifierNumber = "8"; 869 if(alt && ctrl && !shift) modifierNumber = "7"; 870 if(shift && ctrl && !alt) modifierNumber = "6"; 871 if(ctrl && !shift && !alt) modifierNumber = "5"; 872 if(shift && alt && !ctrl) modifierNumber = "4"; 873 if(alt && !shift && !ctrl) modifierNumber = "3"; 874 if(shift && !alt && !ctrl) modifierNumber = "2"; 875 // FIXME: meta and windows 876 // windows is an extension 877 if(windows) { 878 if(modifierNumber.length) 879 otherModifier = '2'; 880 else 881 modifierNumber = "20"; 882 /* // the below is what we're really doing 883 int mn = 0; 884 if(modifierNumber.length) 885 mn = modifierNumber[0] + '0'; 886 mn += 20; 887 */ 888 } 889 890 string keyNumber; 891 char terminator; 892 893 if(s[$-1] == '~') { 894 keyNumber = s[2 .. $-1]; 895 terminator = '~'; 896 } else { 897 keyNumber = "1"; 898 terminator = s[$ - 1]; 899 } 900 901 ScopeBuffer!(char, 32) buffer; 902 buffer ~= "\033["; 903 buffer ~= keyNumber; 904 buffer ~= ";"; 905 if(otherModifier) 906 buffer ~= otherModifier; 907 buffer ~= modifierNumber[]; 908 if(key) { 909 buffer ~= ";"; 910 import std.conv; 911 buffer ~= to!string(key); 912 } 913 buffer ~= terminator; 914 // the xterm style is last bit tell us what it is 915 sendToApplication(buffer[]); 916 } 917 } 918 919 alias TerminalKey Key; 920 import std.stdio; 921 // writefln("Key: %x", cast(int) key); 922 switch(key) { 923 case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break; 924 case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break; 925 case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break; 926 case Key.Right: sendToApplicationModified(applicationCursorKeys ? "\033OC" : "\033[C"); break; 927 928 case Key.Home: sendToApplicationModified(applicationCursorKeys ? "\033OH" : (1 ? "\033[H" : "\033[1~")); break; 929 case Key.Insert: sendToApplicationModified("\033[2~"); break; 930 case Key.Delete: sendToApplicationModified("\033[3~"); break; 931 932 // the 1? is xterm vs gnu screen. but i really want xterm compatibility. 933 case Key.End: sendToApplicationModified(applicationCursorKeys ? "\033OF" : (1 ? "\033[F" : "\033[4~")); break; 934 case Key.PageUp: sendToApplicationModified("\033[5~"); break; 935 case Key.PageDown: sendToApplicationModified("\033[6~"); break; 936 937 // the first one here is preferred, the second option is what xterm does if you turn on the "old function keys" option, which most apps don't actually expect 938 case Key.F1: sendToApplicationModified(1 ? "\033OP" : "\033[11~"); break; 939 case Key.F2: sendToApplicationModified(1 ? "\033OQ" : "\033[12~"); break; 940 case Key.F3: sendToApplicationModified(1 ? "\033OR" : "\033[13~"); break; 941 case Key.F4: sendToApplicationModified(1 ? "\033OS" : "\033[14~"); break; 942 case Key.F5: sendToApplicationModified("\033[15~"); break; 943 case Key.F6: sendToApplicationModified("\033[17~"); break; 944 case Key.F7: sendToApplicationModified("\033[18~"); break; 945 case Key.F8: sendToApplicationModified("\033[19~"); break; 946 case Key.F9: sendToApplicationModified("\033[20~"); break; 947 case Key.F10: sendToApplicationModified("\033[21~"); break; 948 case Key.F11: sendToApplicationModified("\033[23~"); break; 949 case Key.F12: sendToApplicationModified("\033[24~"); break; 950 951 case Key.Escape: sendToApplicationModified("\033"); break; 952 953 // my extensions, see terminator.d for the other side of it 954 case Key.ScrollLock: sendToApplicationModified("\033[70~"); break; 955 956 // xterm extension for arbitrary modified unicode chars 957 default: 958 sendToApplicationModified("\033[27~", key); 959 } 960 961 return redrawRequired; 962 } 963 964 /// if a binary extension is triggered, the implementing class is responsible for figuring out how it should be made to fit into the screen buffer 965 protected /*abstract*/ BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[]) { 966 return BinaryDataTerminalRepresentation(); 967 } 968 969 /// If you subclass this and return true, you can scroll on command without needing to redraw the entire screen; 970 /// returning true here suppresses the automatic invalidation of scrolled lines (except the new one). 971 protected bool scrollLines(int howMany, bool scrollUp) { 972 return false; 973 } 974 975 // might be worth doing the redraw magic in here too. 976 // FIXME: not implemented 977 @disable protected void drawTextSection(int x, int y, TextAttributes attributes, in dchar[] text, bool isAllSpaces) { 978 // if you implement this it will always give you a continuous block on a single line. note that text may be a bunch of spaces, in that case you can just draw the bg color to clear the area 979 // or you can redraw based on the invalidated flag on the buffer 980 } 981 // FIXME: what about image sections? maybe it is still necessary to loop through them 982 983 /// Style of the cursor 984 enum CursorStyle { 985 block, /// a solid block over the position (like default xterm or many gui replace modes) 986 underline, /// underlining the position (like the vga text mode default) 987 bar, /// a bar on the left side of the cursor position (like gui insert modes) 988 } 989 990 // these can be overridden, but don't have to be 991 TextAttributes defaultTextAttributes() { 992 TextAttributes ta; 993 994 ta.foregroundIndex = 256; // terminal.d uses this as Color.DEFAULT 995 ta.backgroundIndex = 256; 996 997 import std.process; 998 // I'm using the environment for this because my programs and scripts 999 // already know this variable and then it gets nicely inherited. It is 1000 // also easy to set without buggering with other arguments. So works for me. 1001 version(with_24_bit_color) { 1002 if(environment.get("ELVISBG") == "dark") { 1003 ta.foreground = Color.white; 1004 ta.background = Color.black; 1005 } else { 1006 ta.foreground = Color.black; 1007 ta.background = Color.white; 1008 } 1009 } 1010 1011 return ta; 1012 } 1013 1014 Color defaultForeground; 1015 Color defaultBackground; 1016 1017 Color[256] palette; 1018 1019 /// . 1020 static struct TextAttributes { 1021 align(1): 1022 bool bold() { return (attrStore & 1) ? true : false; } /// 1023 void bold(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } /// 1024 1025 bool blink() { return (attrStore & 2) ? true : false; } /// 1026 void blink(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } /// 1027 1028 bool invisible() { return (attrStore & 4) ? true : false; } /// 1029 void invisible(bool t) { attrStore &= ~4; if(t) attrStore |= 4; } /// 1030 1031 bool inverse() { return (attrStore & 8) ? true : false; } /// 1032 void inverse(bool t) { attrStore &= ~8; if(t) attrStore |= 8; } /// 1033 1034 bool underlined() { return (attrStore & 16) ? true : false; } /// 1035 void underlined(bool t) { attrStore &= ~16; if(t) attrStore |= 16; } /// 1036 1037 bool italic() { return (attrStore & 32) ? true : false; } /// 1038 void italic(bool t) { attrStore &= ~32; if(t) attrStore |= 32; } /// 1039 1040 bool strikeout() { return (attrStore & 64) ? true : false; } /// 1041 void strikeout(bool t) { attrStore &= ~64; if(t) attrStore |= 64; } /// 1042 1043 bool faint() { return (attrStore & 128) ? true : false; } /// 1044 void faint(bool t) { attrStore &= ~128; if(t) attrStore |= 128; } /// 1045 1046 // if the high bit here is set, you should use the full Color values if possible, and the value here sans the high bit if not 1047 1048 bool foregroundIsDefault() { return (attrStore & 256) ? true : false; } /// 1049 void foregroundIsDefault(bool t) { attrStore &= ~256; if(t) attrStore |= 256; } /// 1050 1051 bool backgroundIsDefault() { return (attrStore & 512) ? true : false; } /// 1052 void backgroundIsDefault(bool t) { attrStore &= ~512; if(t) attrStore |= 512; } /// 1053 1054 // I am doing all this to get the store a bit smaller but 1055 // I could go back to just plain `ushort foregroundIndex` etc. 1056 1057 /// 1058 @property ushort foregroundIndex() { 1059 if(foregroundIsDefault) 1060 return 256; 1061 else 1062 return foregroundIndexStore; 1063 } 1064 /// 1065 @property ushort backgroundIndex() { 1066 if(backgroundIsDefault) 1067 return 256; 1068 else 1069 return backgroundIndexStore; 1070 } 1071 /// 1072 @property void foregroundIndex(ushort v) { 1073 if(v == 256) 1074 foregroundIsDefault = true; 1075 else 1076 foregroundIsDefault = false; 1077 foregroundIndexStore = cast(ubyte) v; 1078 } 1079 /// 1080 @property void backgroundIndex(ushort v) { 1081 if(v == 256) 1082 backgroundIsDefault = true; 1083 else 1084 backgroundIsDefault = false; 1085 backgroundIndexStore = cast(ubyte) v; 1086 } 1087 1088 ubyte foregroundIndexStore; /// the internal storage 1089 ubyte backgroundIndexStore; /// ditto 1090 ushort attrStore = 0; /// ditto 1091 1092 version(with_24_bit_color) { 1093 Color foreground; /// ditto 1094 Color background; /// ditto 1095 } 1096 } 1097 1098 //pragma(msg, TerminalCell.sizeof); 1099 /// represents one terminal cell 1100 align((void*).sizeof) 1101 static struct TerminalCell { 1102 align(1): 1103 private union { 1104 // OMG the top 11 bits of a dchar are always 0 1105 // and i can reuse them!!! 1106 struct { 1107 dchar chStore = ' '; /// the character 1108 TextAttributes attributesStore; /// color, etc. 1109 } 1110 // 64 bit pointer also has unused 16 bits but meh. 1111 NonCharacterData nonCharacterDataStore; /// iff hasNonCharacterData 1112 } 1113 1114 dchar ch() { 1115 assert(!hasNonCharacterData); 1116 return chStore; 1117 } 1118 void ch(dchar c) { 1119 hasNonCharacterData = false; 1120 chStore = c; 1121 } 1122 ref TextAttributes attributes() return { 1123 assert(!hasNonCharacterData); 1124 return attributesStore; 1125 } 1126 NonCharacterData nonCharacterData() { 1127 assert(hasNonCharacterData); 1128 return nonCharacterDataStore; 1129 } 1130 void nonCharacterData(NonCharacterData c) { 1131 hasNonCharacterData = true; 1132 nonCharacterDataStore = c; 1133 } 1134 1135 // bits: RRHLLNSI 1136 // R = reserved, H = hyperlink ID bit, L = link, N = non-character data, S = selected, I = invalidated 1137 ubyte attrStore = 1; // just invalidated to start 1138 1139 bool invalidated() { return (attrStore & 1) ? true : false; } /// if it needs to be redrawn 1140 void invalidated(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } /// ditto 1141 1142 bool selected() { return (attrStore & 2) ? true : false; } /// if it is currently selected by the user (for being copied to the clipboard) 1143 void selected(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } /// ditto 1144 1145 bool hasNonCharacterData() { return (attrStore & 4) ? true : false; } /// 1146 void hasNonCharacterData(bool t) { attrStore &= ~4; if(t) attrStore |= 4; } 1147 1148 // 0 means it is not a hyperlink. Otherwise, it just alternates between 1 and 3 to tell adjacent links apart. 1149 // value of 2 is reserved for future use. 1150 ubyte hyperlinkStatus() { return (attrStore & 0b11000) >> 3; } 1151 void hyperlinkStatus(ubyte t) { assert(t < 4); attrStore &= ~0b11000; attrStore |= t << 3; } 1152 1153 bool hyperlinkBit() { return (attrStore & 0b100000) >> 5; } 1154 void hyperlinkBit(bool t) { (attrStore &= ~0b100000); if(t) attrStore |= 0b100000; } 1155 } 1156 1157 bool hyperlinkFlipper; 1158 bool hyperlinkActive; 1159 int hyperlinkNumber; 1160 1161 /// Cursor position, zero based. (0,0) == upper left. (0, 1) == second row, first column. 1162 static struct CursorPosition { 1163 int x; /// . 1164 int y; /// . 1165 alias y row; 1166 alias x column; 1167 } 1168 1169 // these public functions can be used to manipulate the terminal 1170 1171 /// clear the screen 1172 void cls() { 1173 TerminalCell plain; 1174 plain.ch = ' '; 1175 plain.attributes = currentAttributes; 1176 plain.invalidated = true; 1177 foreach(i, ref cell; alternateScreenActive ? alternateScreen : normalScreen) { 1178 cell = plain; 1179 } 1180 extendInvalidatedRange(0, 0, screenWidth, screenHeight); 1181 } 1182 1183 void makeSelectionOffsetsSane(ref int offsetStart, ref int offsetEnd) { 1184 auto buffer = &alternateScreen; 1185 1186 if(offsetStart < 0) 1187 offsetStart = 0; 1188 if(offsetEnd < 0) 1189 offsetEnd = 0; 1190 if(offsetStart > (*buffer).length) 1191 offsetStart = cast(int) (*buffer).length; 1192 if(offsetEnd > (*buffer).length) 1193 offsetEnd = cast(int) (*buffer).length; 1194 1195 // if it is backwards, we can flip it 1196 if(offsetEnd < offsetStart) { 1197 auto tmp = offsetStart; 1198 offsetStart = offsetEnd; 1199 offsetEnd = tmp; 1200 } 1201 } 1202 1203 public string getPlainText(int offsetStart, int offsetEnd) { 1204 auto buffer = alternateScreenActive ? &alternateScreen : &normalScreen; 1205 1206 makeSelectionOffsetsSane(offsetStart, offsetEnd); 1207 1208 if(offsetStart == offsetEnd) 1209 return null; 1210 1211 int x = offsetStart % screenWidth; 1212 int firstSpace = -1; 1213 string ret; 1214 foreach(cell; (*buffer)[offsetStart .. offsetEnd]) { 1215 if(cell.hasNonCharacterData) 1216 break; 1217 ret ~= cell.ch; 1218 1219 x++; 1220 if(x == screenWidth) { 1221 x = 0; 1222 if(firstSpace != -1) { 1223 // we ended with a bunch of spaces, let's replace them with a single newline so the next is more natural 1224 ret = ret[0 .. firstSpace]; 1225 ret ~= "\n"; 1226 firstSpace = -1; 1227 } 1228 } else { 1229 if(cell.ch == ' ' && firstSpace == -1) 1230 firstSpace = cast(int) ret.length - 1; 1231 else if(cell.ch != ' ') 1232 firstSpace = -1; 1233 } 1234 } 1235 if(firstSpace != -1) { 1236 bool allSpaces = true; 1237 foreach(item; ret[firstSpace .. $]) { 1238 if(item != ' ') { 1239 allSpaces = false; 1240 break; 1241 } 1242 } 1243 1244 if(allSpaces) 1245 ret = ret[0 .. firstSpace]; 1246 } 1247 1248 return ret; 1249 } 1250 1251 void scrollDown(int count = 1) { 1252 if(cursorY + 1 < screenHeight) { 1253 TerminalCell plain; 1254 plain.ch = ' '; 1255 plain.attributes = defaultTextAttributes(); 1256 plain.invalidated = true; 1257 foreach(i; 0 .. count) { 1258 // FIXME: should that be cursorY or scrollZoneTop? 1259 for(int y = scrollZoneBottom; y > cursorY; y--) 1260 foreach(x; 0 .. screenWidth) { 1261 ASS[y][x] = ASS[y - 1][x]; 1262 ASS[y][x].invalidated = true; 1263 } 1264 1265 foreach(x; 0 .. screenWidth) 1266 ASS[cursorY][x] = plain; 1267 } 1268 extendInvalidatedRange(0, cursorY, screenWidth, scrollZoneBottom); 1269 } 1270 } 1271 1272 void scrollUp(int count = 1) { 1273 if(cursorY + 1 < screenHeight) { 1274 TerminalCell plain; 1275 plain.ch = ' '; 1276 plain.attributes = defaultTextAttributes(); 1277 plain.invalidated = true; 1278 foreach(i; 0 .. count) { 1279 // FIXME: should that be cursorY or scrollZoneBottom? 1280 for(int y = scrollZoneTop; y < cursorY; y++) 1281 foreach(x; 0 .. screenWidth) { 1282 ASS[y][x] = ASS[y + 1][x]; 1283 ASS[y][x].invalidated = true; 1284 } 1285 1286 foreach(x; 0 .. screenWidth) 1287 ASS[cursorY][x] = plain; 1288 } 1289 1290 extendInvalidatedRange(0, scrollZoneTop, screenWidth, cursorY); 1291 } 1292 } 1293 1294 1295 int readingExtensionData = -1; 1296 string extensionData; 1297 1298 immutable(dchar[dchar])* characterSet = null; // null means use regular UTF-8 1299 1300 bool readingEsc = false; 1301 ScopeBuffer!(ubyte, 1024, true) esc; 1302 /// sends raw input data to the terminal as if the application printf()'d it or it echoed or whatever 1303 void sendRawInput(in ubyte[] datain) { 1304 const(ubyte)[] data = datain; 1305 //import std.array; 1306 //assert(!readingEsc, replace(cast(string) esc, "\033", "\\")); 1307 again: 1308 foreach(didx, b; data) { 1309 if(readingExtensionData >= 0) { 1310 if(readingExtensionData == extensionMagicIdentifier.length) { 1311 if(b) { 1312 switch(b) { 1313 case 13, 10: 1314 // ignore 1315 break; 1316 case 'A': .. case 'Z': 1317 case 'a': .. case 'z': 1318 case '0': .. case '9': 1319 case '=': 1320 case '+', '/': 1321 case '_', '-': 1322 // base64 ok 1323 extensionData ~= b; 1324 break; 1325 default: 1326 // others should abort the read 1327 readingExtensionData = -1; 1328 } 1329 } else { 1330 readingExtensionData = -1; 1331 import std.base64; 1332 auto got = handleBinaryExtensionData(Base64.decode(extensionData)); 1333 1334 auto rep = got.representation; 1335 foreach(y; 0 .. got.height) { 1336 foreach(x; 0 .. got.width) { 1337 addOutput(rep[0]); 1338 rep = rep[1 .. $]; 1339 } 1340 newLine(true); 1341 } 1342 } 1343 } else { 1344 if(b == extensionMagicIdentifier[readingExtensionData]) 1345 readingExtensionData++; 1346 else { 1347 // put the data back into the buffer, if possible 1348 // (if the data was split across two packets, this may 1349 // not be possible. but in that case, meh.) 1350 if(cast(int) didx - cast(int) readingExtensionData >= 0) 1351 data = data[didx - readingExtensionData .. $]; 1352 readingExtensionData = -1; 1353 goto again; 1354 } 1355 } 1356 1357 continue; 1358 } 1359 1360 if(b == 0) { 1361 readingExtensionData = 0; 1362 extensionData = null; 1363 continue; 1364 } 1365 1366 if(readingEsc) { 1367 if(b == 27) { 1368 // an esc in the middle of a sequence will 1369 // cancel the first one 1370 esc = null; 1371 continue; 1372 } 1373 1374 if(b == 10) { 1375 readingEsc = false; 1376 } 1377 esc ~= b; 1378 1379 if(esc.length == 1 && esc[0] == '7') { 1380 pushSavedCursor(cursorPosition); 1381 esc = null; 1382 readingEsc = false; 1383 } else if(esc.length == 1 && esc[0] == 'M') { 1384 // reverse index 1385 esc = null; 1386 readingEsc = false; 1387 if(cursorY <= scrollZoneTop) 1388 scrollDown(); 1389 else 1390 cursorY = cursorY - 1; 1391 } else if(esc.length == 1 && esc[0] == '=') { 1392 // application keypad 1393 esc = null; 1394 readingEsc = false; 1395 } else if(esc.length == 2 && esc[0] == '%' && esc[1] == 'G') { 1396 // UTF-8 mode 1397 esc = null; 1398 readingEsc = false; 1399 } else if(esc.length == 1 && esc[0] == '8') { 1400 cursorPosition = popSavedCursor; 1401 esc = null; 1402 readingEsc = false; 1403 } else if(esc.length == 1 && esc[0] == 'c') { 1404 // reset 1405 // FIXME 1406 esc = null; 1407 readingEsc = false; 1408 } else if(esc.length == 1 && esc[0] == '>') { 1409 // normal keypad 1410 esc = null; 1411 readingEsc = false; 1412 } else if(esc.length > 1 && ( 1413 (esc[0] == '[' && (b >= 64 && b <= 126)) || 1414 (esc[0] == ']' && b == '\007'))) 1415 { 1416 try { 1417 tryEsc(esc[]); 1418 } catch(Exception e) { 1419 unknownEscapeSequence(e.msg ~ " :: " ~ cast(char[]) esc[]); 1420 } 1421 esc = null; 1422 readingEsc = false; 1423 } else if(esc.length == 3 && esc[0] == '%' && esc[1] == 'G') { 1424 // UTF-8 mode. ignored because we're always in utf-8 mode (though should we be?) 1425 esc = null; 1426 readingEsc = false; 1427 } else if(esc.length == 2 && esc[0] == ')') { 1428 // more character set selection. idk exactly how this works 1429 esc = null; 1430 readingEsc = false; 1431 } else if(esc.length == 2 && esc[0] == '(') { 1432 // xterm command for character set 1433 // FIXME: handling esc[1] == '0' would be pretty boss 1434 // and esc[1] == 'B' == united states 1435 if(esc[1] == '0') 1436 characterSet = &lineDrawingCharacterSet; 1437 else 1438 characterSet = null; // our default is UTF-8 and i don't care much about others anyway. 1439 1440 esc = null; 1441 readingEsc = false; 1442 } else if(esc.length == 1 && esc[0] == 'Z') { 1443 // identify terminal 1444 sendToApplication(terminalIdCode); 1445 } 1446 continue; 1447 } 1448 1449 if(b == 27) { 1450 readingEsc = true; 1451 debug if(esc.isNull && esc.length) { 1452 import std.stdio; writeln("discarding esc ", cast(string) esc[]); 1453 } 1454 esc = null; 1455 continue; 1456 } 1457 1458 if(b == 13) { 1459 cursorX = 0; 1460 setTentativeScrollback(0); 1461 continue; 1462 } 1463 1464 if(b == 7) { 1465 soundBell(); 1466 continue; 1467 } 1468 1469 if(b == 8) { 1470 cursorX = cursorX - 1; 1471 extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY); 1472 setTentativeScrollback(cursorX); 1473 continue; 1474 } 1475 1476 if(b == 9) { 1477 int howMany = 8 - (cursorX % 8); 1478 // so apparently it is just supposed to move the cursor. 1479 // it breaks mutt to output spaces 1480 cursorX = cursorX + howMany; 1481 1482 if(!alternateScreenActive) 1483 foreach(i; 0 .. howMany) 1484 addScrollbackOutput(' '); // FIXME: it would be nice to actually put a tab character there for copy/paste accuracy (ditto with newlines actually) 1485 continue; 1486 } 1487 1488 // std.stdio.writeln("READ ", data[w]); 1489 addOutput(b); 1490 } 1491 } 1492 1493 1494 /// construct 1495 this(int width, int height) { 1496 // initialization 1497 1498 import std.process; 1499 if(environment.get("ELVISBG") == "dark") { 1500 defaultForeground = Color.white; 1501 defaultBackground = Color.black; 1502 } else { 1503 defaultForeground = Color.black; 1504 defaultBackground = Color.white; 1505 } 1506 1507 currentAttributes = defaultTextAttributes(); 1508 cursorColor = Color.white; 1509 1510 palette[] = xtermPalette[]; 1511 1512 resizeTerminal(width, height); 1513 1514 // update the other thing 1515 if(windowTitle.length == 0) 1516 windowTitle = "Terminal Emulator"; 1517 changeWindowTitle(windowTitle); 1518 changeIconTitle(iconTitle); 1519 changeTextAttributes(currentAttributes); 1520 } 1521 1522 1523 private { 1524 TerminalCell[] scrollbackMainScreen; 1525 bool scrollbackCursorShowing; 1526 int scrollbackCursorX; 1527 int scrollbackCursorY; 1528 } 1529 1530 protected { 1531 bool scrollingBack; 1532 1533 int currentScrollback; 1534 int currentScrollbackX; 1535 } 1536 1537 // FIXME: if it is resized while scrolling back, stuff can get messed up 1538 1539 private int scrollbackLength_; 1540 private void scrollbackLength(int i) { 1541 scrollbackLength_ = i; 1542 } 1543 1544 int scrollbackLength() { 1545 return scrollbackLength_; 1546 } 1547 1548 private int scrollbackWidth_; 1549 int scrollbackWidth() { 1550 return scrollbackWidth_ > screenWidth ? scrollbackWidth_ : screenWidth; 1551 } 1552 1553 /* virtual */ void notifyScrollbackAdded() {} 1554 /* virtual */ void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) {} 1555 /* virtual */ void notifyScrollbarPosition(int x, int y) {} 1556 1557 // coordinates are for a scroll bar, where 0,0 is the beginning of history 1558 void scrollbackTo(int x, int y) { 1559 if(alternateScreenActive && !scrollingBack) 1560 return; 1561 1562 if(!scrollingBack) { 1563 startScrollback(); 1564 } 1565 1566 if(y < 0) 1567 y = 0; 1568 if(x < 0) 1569 x = 0; 1570 1571 currentScrollbackX = x; 1572 currentScrollback = scrollbackLength - y; 1573 1574 if(currentScrollback < 0) 1575 currentScrollback = 0; 1576 if(currentScrollbackX < 0) 1577 currentScrollbackX = 0; 1578 1579 if(!scrollLock && currentScrollback == 0 && currentScrollbackX == 0) { 1580 endScrollback(); 1581 } else { 1582 cls(); 1583 showScrollbackOnScreen(alternateScreen, currentScrollback, false, currentScrollbackX); 1584 } 1585 } 1586 1587 void scrollback(int delta, int deltaX = 0) { 1588 if(alternateScreenActive && !scrollingBack) 1589 return; 1590 1591 if(!scrollingBack) { 1592 if(delta <= 0 && deltaX == 0) 1593 return; // it does nothing to scroll down when not scrolling back 1594 startScrollback(); 1595 } 1596 currentScrollback += delta; 1597 if(!scrollbackReflow && deltaX) { 1598 currentScrollbackX += deltaX; 1599 int max = scrollbackWidth - screenWidth; 1600 if(max < 0) 1601 max = 0; 1602 if(currentScrollbackX > max) 1603 currentScrollbackX = max; 1604 if(currentScrollbackX < 0) 1605 currentScrollbackX = 0; 1606 } 1607 1608 int max = cast(int) scrollbackBuffer.length - screenHeight; 1609 if(scrollbackReflow && max < 0) { 1610 foreach(line; scrollbackBuffer[]) { 1611 if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData)) 1612 max += 0; 1613 else 1614 max += cast(int) line.length / screenWidth; 1615 } 1616 } 1617 1618 if(max < 0) 1619 max = 0; 1620 1621 if(scrollbackReflow && currentScrollback > max) { 1622 foreach(line; scrollbackBuffer[]) { 1623 if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData)) 1624 max += 0; 1625 else 1626 max += cast(int) line.length / screenWidth; 1627 } 1628 } 1629 1630 if(currentScrollback > max) 1631 currentScrollback = max; 1632 if(currentScrollback < 0) 1633 currentScrollback = 0; 1634 1635 if(!scrollLock && currentScrollback <= 0 && currentScrollbackX <= 0) 1636 endScrollback(); 1637 else { 1638 cls(); 1639 showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX); 1640 notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight); 1641 } 1642 } 1643 1644 private void startScrollback() { 1645 if(scrollingBack) 1646 return; 1647 currentScrollback = 0; 1648 currentScrollbackX = 0; 1649 scrollingBack = true; 1650 scrollbackCursorX = cursorX; 1651 scrollbackCursorY = cursorY; 1652 scrollbackCursorShowing = cursorShowing; 1653 scrollbackMainScreen = alternateScreen.dup; 1654 alternateScreenActive = true; 1655 1656 cursorShowing = false; 1657 } 1658 1659 bool endScrollback() { 1660 //if(scrollLock) 1661 // return false; 1662 if(!scrollingBack) 1663 return false; 1664 scrollingBack = false; 1665 cursorX = scrollbackCursorX; 1666 cursorY = scrollbackCursorY; 1667 cursorShowing = scrollbackCursorShowing; 1668 alternateScreen = scrollbackMainScreen; 1669 alternateScreenActive = false; 1670 1671 currentScrollback = 0; 1672 currentScrollbackX = 0; 1673 1674 if(!scrollLock) { 1675 scrollbackReflow = true; 1676 recalculateScrollbackLength(); 1677 } 1678 1679 notifyScrollbarPosition(0, int.max); 1680 1681 return true; 1682 } 1683 1684 private bool scrollbackReflow = true; 1685 /* deprecated? */ 1686 public void toggleScrollbackWrap() { 1687 scrollbackReflow = !scrollbackReflow; 1688 recalculateScrollbackLength(); 1689 } 1690 1691 private bool scrollLockLockEnabled = false; 1692 package void scrollLockLock() { 1693 scrollLockLockEnabled = true; 1694 if(!scrollLock) 1695 toggleScrollLock(); 1696 } 1697 1698 private bool scrollLock = false; 1699 public void toggleScrollLock() { 1700 if(scrollLockLockEnabled && scrollLock) 1701 goto nochange; 1702 scrollLock = !scrollLock; 1703 scrollbackReflow = !scrollLock; 1704 1705 nochange: 1706 recalculateScrollbackLength(); 1707 1708 if(scrollLock) { 1709 startScrollback(); 1710 1711 cls(); 1712 currentScrollback = 0; 1713 currentScrollbackX = 0; 1714 showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX); 1715 notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight); 1716 } else { 1717 endScrollback(); 1718 } 1719 1720 //cls(); 1721 //drawScrollback(); 1722 } 1723 1724 private void recalculateScrollbackLength() { 1725 int count = cast(int) scrollbackBuffer.length; 1726 int max; 1727 if(scrollbackReflow) { 1728 foreach(line; scrollbackBuffer[]) { 1729 if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData)) 1730 {} // intentionally blank, the count is fine since this line isn't reflowed anyway 1731 else 1732 count += cast(int) line.length / screenWidth; 1733 } 1734 } else { 1735 foreach(line; scrollbackBuffer[]) { 1736 if(line.length > max) 1737 max = cast(int) line.length; 1738 } 1739 } 1740 scrollbackWidth_ = max; 1741 scrollbackLength = count; 1742 notifyScrollbackAdded(); 1743 notifyScrollbarPosition(currentScrollbackX, currentScrollback ? scrollbackLength - currentScrollback : int.max); 1744 } 1745 1746 /++ 1747 Writes the text in the scrollback buffer to the given file. 1748 1749 Discards formatting information and embedded images. 1750 1751 See_Also: 1752 [writeScrollbackToDelegate] 1753 +/ 1754 public void writeScrollbackToFile(string filename) { 1755 import std.stdio; 1756 auto file = File(filename, "wt"); 1757 foreach(line; scrollbackBuffer[]) { 1758 foreach(c; line) 1759 if(!c.hasNonCharacterData) 1760 file.write(c.ch); // I hope this is buffered 1761 file.writeln(); 1762 } 1763 } 1764 1765 /++ 1766 Writes the text in the scrollback buffer to the given delegate, one character at a time. 1767 1768 Discards formatting information and embedded images. 1769 1770 See_Also: 1771 [writeScrollbackToFile] 1772 History: 1773 Added March 14, 2021 (dub version 9.4) 1774 +/ 1775 public void writeScrollbackToDelegate(scope void delegate(dchar c) dg) { 1776 foreach(line; scrollbackBuffer[]) { 1777 foreach(c; line) 1778 if(!c.hasNonCharacterData) 1779 dg(c.ch); 1780 dg('\n'); 1781 } 1782 } 1783 1784 public void drawScrollback(bool useAltScreen = false) { 1785 showScrollbackOnScreen(useAltScreen ? alternateScreen : normalScreen, 0, true, 0); 1786 } 1787 1788 private void showScrollbackOnScreen(ref TerminalCell[] screen, int howFar, bool reflow, int howFarX) { 1789 int start; 1790 1791 cursorX = 0; 1792 cursorY = 0; 1793 1794 int excess = 0; 1795 1796 if(scrollbackReflow) { 1797 int numLines; 1798 int idx = cast(int) scrollbackBuffer.length - 1; 1799 foreach_reverse(line; scrollbackBuffer[]) { 1800 auto lineCount = 1 + line.length / screenWidth; 1801 1802 // if the line has an image in it, it cannot be reflowed. this hack to check just the first and last thing is the cheapest way rn 1803 if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData)) 1804 lineCount = 1; 1805 1806 numLines += lineCount; 1807 if(numLines >= (screenHeight + howFar)) { 1808 start = cast(int) idx; 1809 excess = numLines - (screenHeight + howFar); 1810 break; 1811 } 1812 idx--; 1813 } 1814 } else { 1815 auto termination = cast(int) scrollbackBuffer.length - howFar; 1816 if(termination < 0) 1817 termination = cast(int) scrollbackBuffer.length; 1818 1819 start = termination - screenHeight; 1820 if(start < 0) 1821 start = 0; 1822 } 1823 1824 TerminalCell overflowCell; 1825 overflowCell.ch = '\»'; 1826 overflowCell.attributes.backgroundIndex = 3; 1827 overflowCell.attributes.foregroundIndex = 0; 1828 version(with_24_bit_color) { 1829 overflowCell.attributes.foreground = Color(40, 40, 40); 1830 overflowCell.attributes.background = Color.yellow; 1831 } 1832 1833 outer: foreach(line; scrollbackBuffer[start .. $]) { 1834 if(excess) { 1835 line = line[excess * screenWidth .. $]; 1836 excess = 0; 1837 } 1838 1839 if(howFarX) { 1840 if(howFarX <= line.length) 1841 line = line[howFarX .. $]; 1842 else 1843 line = null; 1844 } 1845 1846 bool overflowed; 1847 foreach(cell; line) { 1848 cell.invalidated = true; 1849 if(overflowed) { 1850 screen[cursorY * screenWidth + cursorX] = overflowCell; 1851 break; 1852 } else { 1853 screen[cursorY * screenWidth + cursorX] = cell; 1854 } 1855 1856 if(cursorX == screenWidth-1) { 1857 if(scrollbackReflow) { 1858 // don't attempt to reflow images 1859 if(cell.hasNonCharacterData) 1860 break; 1861 cursorX = 0; 1862 if(cursorY + 1 == screenHeight) 1863 break outer; 1864 cursorY = cursorY + 1; 1865 } else { 1866 overflowed = true; 1867 } 1868 } else 1869 cursorX = cursorX + 1; 1870 } 1871 if(cursorY + 1 == screenHeight) 1872 break; 1873 cursorY = cursorY + 1; 1874 cursorX = 0; 1875 } 1876 1877 extendInvalidatedRange(); 1878 1879 cursorX = 0; 1880 } 1881 1882 protected bool cueScrollback; 1883 1884 public void resizeTerminal(int w, int h) { 1885 if(w == screenWidth && h == screenHeight) 1886 return; // we're already good, do nothing to avoid wasting time and possibly losing a line (bash doesn't seem to like being told it "resized" to the same size) 1887 1888 // do i like this? 1889 if(scrollLock) 1890 toggleScrollLock(); 1891 1892 // FIXME: hack 1893 endScrollback(); 1894 1895 screenWidth = w; 1896 screenHeight = h; 1897 1898 normalScreen.length = screenWidth * screenHeight; 1899 alternateScreen.length = screenWidth * screenHeight; 1900 scrollZoneBottom = screenHeight - 1; 1901 if(scrollZoneTop < 0 || scrollZoneTop >= scrollZoneBottom) 1902 scrollZoneTop = 0; 1903 1904 // we need to make sure the state is sane all across the board, so first we'll clear everything... 1905 TerminalCell plain; 1906 plain.ch = ' '; 1907 plain.attributes = defaultTextAttributes; 1908 plain.invalidated = true; 1909 normalScreen[] = plain; 1910 alternateScreen[] = plain; 1911 1912 extendInvalidatedRange(); 1913 1914 // then, in normal mode, we'll redraw using the scrollback buffer 1915 // 1916 // if we're in the alternate screen though, keep it blank because 1917 // while redrawing makes sense in theory, odds are the program in 1918 // charge of the normal screen didn't get the resize signal. 1919 if(!alternateScreenActive) 1920 showScrollbackOnScreen(normalScreen, 0, true, 0); 1921 else 1922 cueScrollback = true; 1923 // but in alternate mode, it is the application's responsibility 1924 1925 // the property ensures these are within bounds so this set just forces that 1926 cursorY = cursorY; 1927 cursorX = cursorX; 1928 1929 recalculateScrollbackLength(); 1930 } 1931 1932 private CursorPosition popSavedCursor() { 1933 CursorPosition pos; 1934 //import std.stdio; writeln("popped"); 1935 if(savedCursors.length) { 1936 pos = savedCursors[$-1]; 1937 savedCursors = savedCursors[0 .. $-1]; 1938 savedCursors.assumeSafeAppend(); // we never keep references elsewhere so might as well reuse the memory as much as we can 1939 } 1940 1941 // If the screen resized after this was saved, it might be restored to a bad amount, so we need to sanity test. 1942 if(pos.x < 0) 1943 pos.x = 0; 1944 if(pos.y < 0) 1945 pos.y = 0; 1946 if(pos.x > screenWidth) 1947 pos.x = screenWidth - 1; 1948 if(pos.y > screenHeight) 1949 pos.y = screenHeight - 1; 1950 1951 return pos; 1952 } 1953 1954 private void pushSavedCursor(CursorPosition pos) { 1955 //import std.stdio; writeln("pushed"); 1956 savedCursors ~= pos; 1957 } 1958 1959 public void clearScrollbackHistory() { 1960 if(scrollingBack) 1961 endScrollback(); 1962 scrollbackBuffer.clear(); 1963 scrollbackLength_ = 0; 1964 scrollbackWidth_ = 0; 1965 1966 notifyScrollbackAdded(); 1967 } 1968 1969 public void moveCursor(int x, int y) { 1970 cursorX = x; 1971 cursorY = y; 1972 } 1973 1974 /* FIXME: i want these to be private */ 1975 protected { 1976 TextAttributes currentAttributes; 1977 CursorPosition cursorPosition; 1978 CursorPosition[] savedCursors; // a stack 1979 CursorStyle cursorStyle; 1980 Color cursorColor; 1981 string windowTitle; 1982 string iconTitle; 1983 1984 bool attentionDemanded; 1985 1986 IndexedImage windowIcon; 1987 IndexedImage[] iconStack; 1988 1989 string[] titleStack; 1990 1991 bool bracketedPasteMode; 1992 bool bracketedHyperlinkMode; 1993 bool mouseButtonTracking; 1994 private bool _mouseMotionTracking; 1995 bool utf8MouseMode; 1996 bool sgrMouseMode; 1997 bool mouseButtonReleaseTracking; 1998 bool mouseButtonMotionTracking; 1999 bool selectiveMouseTracking; 2000 /+ 2001 When set, it causes xterm to send CSI I when the terminal gains focus, and CSI O when it loses focus. 2002 this is turned on by mode 1004 with mouse events. 2003 2004 FIXME: not implemented. 2005 +/ 2006 bool sendFocusEvents; 2007 2008 bool mouseMotionTracking() { 2009 return _mouseMotionTracking; 2010 } 2011 2012 void mouseMotionTracking(bool b) { 2013 _mouseMotionTracking = b; 2014 } 2015 2016 void allMouseTrackingOff() { 2017 selectiveMouseTracking = false; 2018 mouseMotionTracking = false; 2019 mouseButtonTracking = false; 2020 mouseButtonReleaseTracking = false; 2021 mouseButtonMotionTracking = false; 2022 sendFocusEvents = false; 2023 } 2024 2025 bool wraparoundMode = true; 2026 2027 bool alternateScreenActive; 2028 bool cursorShowing = true; 2029 2030 bool reverseVideo; 2031 bool applicationCursorKeys; 2032 2033 bool scrollingEnabled = true; 2034 int scrollZoneTop; 2035 int scrollZoneBottom; 2036 2037 int screenWidth; 2038 int screenHeight; 2039 // assert(alternateScreen.length = screenWidth * screenHeight); 2040 TerminalCell[] alternateScreen; 2041 TerminalCell[] normalScreen; 2042 2043 // the lengths can be whatever 2044 ScrollbackBuffer scrollbackBuffer; 2045 2046 static struct ScrollbackBuffer { 2047 TerminalCell[][] backing; 2048 2049 enum maxScrollback = 8192 / 2; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask... 2050 2051 int start; 2052 int length_; 2053 2054 size_t length() { 2055 return length_; 2056 } 2057 2058 void clear() { 2059 start = 0; 2060 length_ = 0; 2061 backing = null; 2062 } 2063 2064 // FIXME: if scrollback hits limits the scroll bar needs 2065 // to understand the circular buffer 2066 2067 void opOpAssign(string op : "~")(TerminalCell[] line) { 2068 if(length_ < maxScrollback) { 2069 backing.assumeSafeAppend(); 2070 backing ~= line; 2071 length_++; 2072 } else { 2073 backing[start] = line; 2074 start++; 2075 if(start == maxScrollback) 2076 start = 0; 2077 } 2078 } 2079 2080 /* 2081 int opApply(scope int delegate(ref TerminalCell[]) dg) { 2082 foreach(ref l; backing) 2083 if(auto res = dg(l)) 2084 return res; 2085 return 0; 2086 } 2087 2088 int opApplyReverse(scope int delegate(size_t, ref TerminalCell[]) dg) { 2089 foreach_reverse(idx, ref l; backing) 2090 if(auto res = dg(idx, l)) 2091 return res; 2092 return 0; 2093 } 2094 */ 2095 2096 TerminalCell[] opIndex(int idx) { 2097 return backing[(start + idx) % maxScrollback]; 2098 } 2099 2100 ScrollbackBufferRange opSlice(int startOfIteration, Dollar end) { 2101 return ScrollbackBufferRange(&this, startOfIteration); 2102 } 2103 ScrollbackBufferRange opSlice() { 2104 return ScrollbackBufferRange(&this, 0); 2105 } 2106 2107 static struct ScrollbackBufferRange { 2108 ScrollbackBuffer* item; 2109 int position; 2110 int remaining; 2111 this(ScrollbackBuffer* item, int startOfIteration) { 2112 this.item = item; 2113 position = startOfIteration; 2114 remaining = cast(int) item.length - startOfIteration; 2115 2116 } 2117 2118 TerminalCell[] front() { return (*item)[position]; } 2119 bool empty() { return remaining <= 0; } 2120 void popFront() { 2121 position++; 2122 remaining--; 2123 } 2124 2125 TerminalCell[] back() { return (*item)[remaining - 1 - position]; } 2126 void popBack() { 2127 remaining--; 2128 } 2129 } 2130 2131 static struct Dollar {}; 2132 Dollar opDollar() { return Dollar(); } 2133 2134 } 2135 2136 struct Helper2 { 2137 size_t row; 2138 TerminalEmulator t; 2139 this(TerminalEmulator t, size_t row) { 2140 this.t = t; 2141 this.row = row; 2142 } 2143 2144 ref TerminalCell opIndex(size_t cell) { 2145 auto thing = t.alternateScreenActive ? &(t.alternateScreen) : &(t.normalScreen); 2146 return (*thing)[row * t.screenWidth + cell]; 2147 } 2148 } 2149 2150 struct Helper { 2151 TerminalEmulator t; 2152 this(TerminalEmulator t) { 2153 this.t = t; 2154 } 2155 2156 Helper2 opIndex(size_t row) { 2157 return Helper2(t, row); 2158 } 2159 } 2160 2161 @property Helper ASS() { 2162 return Helper(this); 2163 } 2164 2165 @property int cursorX() { return cursorPosition.x; } 2166 @property int cursorY() { return cursorPosition.y; } 2167 @property void cursorX(int x) { 2168 if(x < 0) 2169 x = 0; 2170 if(x >= screenWidth) 2171 x = screenWidth - 1; 2172 cursorPosition.x = x; 2173 } 2174 @property void cursorY(int y) { 2175 if(y < 0) 2176 y = 0; 2177 if(y >= screenHeight) 2178 y = screenHeight - 1; 2179 cursorPosition.y = y; 2180 } 2181 2182 void addOutput(string b) { 2183 foreach(c; b) 2184 addOutput(c); 2185 } 2186 2187 TerminalCell[] currentScrollbackLine; 2188 ubyte[6] utf8SequenceBuffer; 2189 int utf8SequenceBufferPosition; 2190 // int scrollbackWrappingAt = 0; 2191 dchar utf8Sequence; 2192 int utf8BytesRemaining; 2193 int currentUtf8Shift; 2194 bool newLineOnNext; 2195 void addOutput(ubyte b) { 2196 2197 void addChar(dchar c) { 2198 if(newLineOnNext) { 2199 newLineOnNext = false; 2200 // only if we're still on the right side... 2201 if(cursorX == screenWidth - 1) 2202 newLine(false); 2203 } 2204 TerminalCell tc; 2205 2206 if(characterSet !is null) { 2207 if(auto replacement = utf8Sequence in *characterSet) 2208 utf8Sequence = *replacement; 2209 } 2210 tc.ch = utf8Sequence; 2211 tc.attributes = currentAttributes; 2212 tc.invalidated = true; 2213 2214 if(hyperlinkActive) { 2215 tc.hyperlinkStatus = hyperlinkFlipper ? 3 : 1; 2216 tc.hyperlinkBit = hyperlinkNumber & 0x01; 2217 hyperlinkNumber >>= 1; 2218 } 2219 2220 addOutput(tc); 2221 } 2222 2223 2224 // this takes in bytes at a time, but since the input encoding is assumed to be UTF-8, we need to gather the bytes 2225 if(utf8BytesRemaining == 0) { 2226 // we're at the beginning of a sequence 2227 utf8Sequence = 0; 2228 if(b < 128) { 2229 utf8Sequence = cast(dchar) b; 2230 // one byte thing, do nothing more... 2231 } else { 2232 // the number of bytes in the sequence is the number of set bits in the first byte... 2233 ubyte checkingBit = 7; 2234 while(b & (1 << checkingBit)) { 2235 utf8BytesRemaining++; 2236 checkingBit--; 2237 } 2238 uint shifted = b & ((1 << checkingBit) - 1); 2239 utf8BytesRemaining--; // since this current byte counts too 2240 currentUtf8Shift = utf8BytesRemaining * 6; 2241 2242 2243 shifted <<= currentUtf8Shift; 2244 utf8Sequence = cast(dchar) shifted; 2245 2246 utf8SequenceBufferPosition = 0; 2247 utf8SequenceBuffer[utf8SequenceBufferPosition++] = b; 2248 } 2249 } else { 2250 // add this to the byte we're doing right now... 2251 utf8BytesRemaining--; 2252 currentUtf8Shift -= 6; 2253 if((b & 0b11000000) != 0b10000000) { 2254 // invalid utf-8 sequence, 2255 // discard it and try to continue 2256 utf8BytesRemaining = 0; 2257 utf8Sequence = 0xfffd; 2258 foreach(i; 0 .. utf8SequenceBufferPosition) 2259 addChar(utf8Sequence); // put out replacement char for everything in there so far 2260 utf8SequenceBufferPosition = 0; 2261 addOutput(b); // retry sending this byte as a new sequence after abandoning the old crap 2262 return; 2263 } 2264 uint shifted = b; 2265 shifted &= 0b00111111; 2266 shifted <<= currentUtf8Shift; 2267 utf8Sequence |= shifted; 2268 2269 if(utf8SequenceBufferPosition < utf8SequenceBuffer.length) 2270 utf8SequenceBuffer[utf8SequenceBufferPosition++] = b; 2271 } 2272 2273 if(utf8BytesRemaining) 2274 return; // not enough data yet, wait for more before displaying anything 2275 2276 if(utf8Sequence == 10) { 2277 newLineOnNext = false; 2278 auto cx = cursorX; // FIXME: this cx thing is a hack, newLine should prolly just do the right thing 2279 2280 /* 2281 TerminalCell tc; 2282 tc.ch = utf8Sequence; 2283 tc.attributes = currentAttributes; 2284 tc.invalidated = true; 2285 addOutput(tc); 2286 */ 2287 2288 newLine(true); 2289 cursorX = cx; 2290 } else { 2291 addChar(utf8Sequence); 2292 } 2293 } 2294 2295 private int recalculationThreshold = 0; 2296 public void addScrollbackLine(TerminalCell[] line) { 2297 scrollbackBuffer ~= line; 2298 2299 if(scrollbackBuffer.length_ == ScrollbackBuffer.maxScrollback) { 2300 recalculationThreshold++; 2301 if(recalculationThreshold > 100) { 2302 recalculateScrollbackLength(); 2303 notifyScrollbackAdded(); 2304 recalculationThreshold = 0; 2305 } 2306 } else { 2307 if(!scrollbackReflow && line.length > scrollbackWidth_) 2308 scrollbackWidth_ = cast(int) line.length; 2309 2310 if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData)) 2311 scrollbackLength = scrollbackLength + 1; 2312 else 2313 scrollbackLength = cast(int) (scrollbackLength + 1 + (scrollbackBuffer[cast(int) scrollbackBuffer.length - 1].length) / screenWidth); 2314 notifyScrollbackAdded(); 2315 } 2316 2317 if(!alternateScreenActive) 2318 notifyScrollbarPosition(0, int.max); 2319 } 2320 2321 protected int maxScrollbackLength() pure const @nogc nothrow { 2322 return 1024; 2323 } 2324 2325 bool insertMode = false; 2326 void newLine(bool commitScrollback) { 2327 extendInvalidatedRange(); // FIXME 2328 if(!alternateScreenActive && commitScrollback) { 2329 // I am limiting this because obscenely long lines are kinda useless anyway and 2330 // i don't want it to eat excessive memory when i spam some thing accidentally 2331 if(currentScrollbackLine.length < maxScrollbackLength()) 2332 addScrollbackLine(currentScrollbackLine.sliceTrailingWhitespace); 2333 else 2334 addScrollbackLine(currentScrollbackLine[0 .. maxScrollbackLength()].sliceTrailingWhitespace); 2335 2336 currentScrollbackLine = null; 2337 currentScrollbackLine.reserve(64); 2338 // scrollbackWrappingAt = 0; 2339 } 2340 2341 cursorX = 0; 2342 if(scrollingEnabled && cursorY >= scrollZoneBottom) { 2343 size_t idx = scrollZoneTop * screenWidth; 2344 2345 // When we scroll up, we need to update the selection position too 2346 if(selectionStart != selectionEnd) { 2347 selectionStart -= screenWidth; 2348 selectionEnd -= screenWidth; 2349 } 2350 foreach(l; scrollZoneTop .. scrollZoneBottom) { 2351 if(alternateScreenActive) { 2352 if(idx + screenWidth * 2 > alternateScreen.length) 2353 break; 2354 alternateScreen[idx .. idx + screenWidth] = alternateScreen[idx + screenWidth .. idx + screenWidth * 2]; 2355 } else { 2356 if(screenWidth <= 0) 2357 break; 2358 if(idx + screenWidth * 2 > normalScreen.length) 2359 break; 2360 normalScreen[idx .. idx + screenWidth] = normalScreen[idx + screenWidth .. idx + screenWidth * 2]; 2361 } 2362 idx += screenWidth; 2363 } 2364 /* 2365 foreach(i; 0 .. screenWidth) { 2366 if(alternateScreenActive) { 2367 alternateScreen[idx] = alternateScreen[idx + screenWidth]; 2368 alternateScreen[idx].invalidated = true; 2369 } else { 2370 normalScreen[idx] = normalScreen[idx + screenWidth]; 2371 normalScreen[idx].invalidated = true; 2372 } 2373 idx++; 2374 } 2375 */ 2376 /* 2377 foreach(i; 0 .. screenWidth) { 2378 if(alternateScreenActive) { 2379 alternateScreen[idx].ch = ' '; 2380 alternateScreen[idx].attributes = currentAttributes; 2381 alternateScreen[idx].invalidated = true; 2382 } else { 2383 normalScreen[idx].ch = ' '; 2384 normalScreen[idx].attributes = currentAttributes; 2385 normalScreen[idx].invalidated = true; 2386 } 2387 idx++; 2388 } 2389 */ 2390 2391 TerminalCell plain; 2392 plain.ch = ' '; 2393 plain.attributes = currentAttributes; 2394 if(alternateScreenActive) { 2395 alternateScreen[idx .. idx + screenWidth] = plain; 2396 } else { 2397 normalScreen[idx .. idx + screenWidth] = plain; 2398 } 2399 } else { 2400 if(insertMode) { 2401 scrollDown(); 2402 } else 2403 cursorY = cursorY + 1; 2404 } 2405 2406 invalidateAll = true; 2407 } 2408 2409 protected bool invalidateAll; 2410 2411 void clearSelection() { 2412 clearSelectionInternal(); 2413 cancelOverriddenSelection(); 2414 } 2415 2416 private void clearSelectionInternal() { 2417 foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen) 2418 if(tc.selected) { 2419 tc.selected = false; 2420 tc.invalidated = true; 2421 } 2422 selectionStart = 0; 2423 selectionEnd = 0; 2424 2425 extendInvalidatedRange(); 2426 } 2427 2428 private int tentativeScrollback = int.max; 2429 private void setTentativeScrollback(int a) { 2430 tentativeScrollback = a; 2431 } 2432 2433 void addScrollbackOutput(dchar ch) { 2434 TerminalCell plain; 2435 plain.ch = ch; 2436 plain.attributes = currentAttributes; 2437 addScrollbackOutput(plain); 2438 } 2439 2440 void addScrollbackOutput(TerminalCell tc) { 2441 if(tentativeScrollback != int.max) { 2442 if(tentativeScrollback >= 0 && tentativeScrollback < currentScrollbackLine.length) { 2443 currentScrollbackLine = currentScrollbackLine[0 .. tentativeScrollback]; 2444 currentScrollbackLine.assumeSafeAppend(); 2445 } 2446 tentativeScrollback = int.max; 2447 } 2448 2449 /* 2450 TerminalCell plain; 2451 plain.ch = ' '; 2452 plain.attributes = currentAttributes; 2453 int lol = cursorX + scrollbackWrappingAt; 2454 while(lol >= currentScrollbackLine.length) 2455 currentScrollbackLine ~= plain; 2456 currentScrollbackLine[lol] = tc; 2457 */ 2458 2459 currentScrollbackLine ~= tc; 2460 2461 } 2462 2463 void addOutput(TerminalCell tc) { 2464 if(alternateScreenActive) { 2465 if(alternateScreen[cursorY * screenWidth + cursorX].selected) { 2466 clearSelection(); 2467 } 2468 alternateScreen[cursorY * screenWidth + cursorX] = tc; 2469 } else { 2470 if(normalScreen[cursorY * screenWidth + cursorX].selected) { 2471 clearSelection(); 2472 } 2473 // FIXME: make this more efficient if it is writing the same thing, 2474 // then it need not be invalidated. Same with above for the alt screen 2475 normalScreen[cursorY * screenWidth + cursorX] = tc; 2476 2477 addScrollbackOutput(tc); 2478 } 2479 2480 extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY); 2481 // FIXME: the wraparoundMode seems to help gnu screen but then it doesn't go away properly and that messes up bash... 2482 //if(wraparoundMode && cursorX == screenWidth - 1) { 2483 if(cursorX == screenWidth - 1) { 2484 // FIXME: should this check the scrolling zone instead? 2485 newLineOnNext = true; 2486 2487 //if(!alternateScreenActive || cursorY < screenHeight - 1) 2488 //newLine(false); 2489 2490 // scrollbackWrappingAt = cast(int) currentScrollbackLine.length; 2491 } else 2492 cursorX = cursorX + 1; 2493 2494 } 2495 2496 void tryEsc(ubyte[] esc) { 2497 bool[2] sidxProcessed; 2498 int[][2] argsAtSidx; 2499 int[12][2] argsAtSidxBuffer; 2500 2501 int[12][4] argsBuffer; 2502 int argsBufferLocation; 2503 2504 int[] getArgsBase(int sidx, int[] defaults) { 2505 assert(sidx == 1 || sidx == 2); 2506 2507 if(sidxProcessed[sidx - 1]) { 2508 int[] bfr = argsBuffer[argsBufferLocation++][]; 2509 if(argsBufferLocation == argsBuffer.length) 2510 argsBufferLocation = 0; 2511 bfr[0 .. defaults.length] = defaults[]; 2512 foreach(idx, v; argsAtSidx[sidx - 1]) 2513 if(v != int.min) 2514 bfr[idx] = v; 2515 return bfr[0 .. max(argsAtSidx[sidx - 1].length, defaults.length)]; 2516 } 2517 2518 auto end = esc.length - 1; 2519 foreach(iii, b; esc[sidx .. end]) { 2520 if(b >= 0x20 && b < 0x30) 2521 end = iii + sidx; 2522 } 2523 2524 auto argsSection = cast(char[]) esc[sidx .. end]; 2525 int[] args = argsAtSidxBuffer[sidx - 1][]; 2526 2527 import std.string : split; 2528 import std.conv : to; 2529 int lastIdx = 0; 2530 2531 foreach(i, arg; split(argsSection, ";")) { 2532 int value; 2533 if(arg.length) { 2534 //import std.stdio; writeln(esc); 2535 value = to!int(arg); 2536 } else 2537 value = int.min; // defaults[i]; 2538 2539 if(args.length > i) 2540 args[i] = value; 2541 else 2542 assert(0); 2543 lastIdx++; 2544 } 2545 2546 argsAtSidx[sidx - 1] = args[0 .. lastIdx]; 2547 sidxProcessed[sidx - 1] = true; 2548 2549 return getArgsBase(sidx, defaults); 2550 } 2551 int[] getArgs(int[] defaults...) { 2552 return getArgsBase(1, defaults); 2553 } 2554 2555 // FIXME 2556 // from http://invisible-island.net/xterm/ctlseqs/ctlseqs.html 2557 // check out this section: "Window manipulation (from dtterm, as well as extensions)" 2558 // especially the title stack, that should rock 2559 /* 2560 P s = 2 2 ; 0 → Save xterm icon and window title on stack. 2561 P s = 2 2 ; 1 → Save xterm icon title on stack. 2562 P s = 2 2 ; 2 → Save xterm window title on stack. 2563 P s = 2 3 ; 0 → Restore xterm icon and window title from stack. 2564 P s = 2 3 ; 1 → Restore xterm icon title from stack. 2565 P s = 2 3 ; 2 → Restore xterm window title from stack. 2566 2567 */ 2568 2569 if(esc[0] == ']' && esc.length > 1) { 2570 int idx = -1; 2571 foreach(i, e; esc) 2572 if(e == ';') { 2573 idx = cast(int) i; 2574 break; 2575 } 2576 if(idx != -1) { 2577 auto arg = cast(char[]) esc[idx + 1 .. $-1]; 2578 switch(cast(char[]) esc[1..idx]) { 2579 case "0": 2580 // icon name and window title 2581 windowTitle = iconTitle = arg.idup; 2582 changeWindowTitle(windowTitle); 2583 changeIconTitle(iconTitle); 2584 break; 2585 case "1": 2586 // icon name 2587 iconTitle = arg.idup; 2588 changeIconTitle(iconTitle); 2589 break; 2590 case "2": 2591 // window title 2592 windowTitle = arg.idup; 2593 changeWindowTitle(windowTitle); 2594 break; 2595 case "10": 2596 // change default text foreground color 2597 break; 2598 case "11": 2599 // change gui background color 2600 break; 2601 case "12": 2602 if(arg.length) 2603 arg = arg[1 ..$]; // skip past the thing 2604 if(arg.length) { 2605 cursorColor = Color.fromString(arg); 2606 foreach(ref p; cursorColor.components[0 .. 3]) 2607 p ^= 0xff; 2608 } else 2609 cursorColor = Color.white; 2610 break; 2611 case "50": 2612 // change font 2613 break; 2614 case "52": 2615 // copy/paste control 2616 // echo -e "\033]52;p;?\007" 2617 // the p == primary 2618 // c == clipboard 2619 // q == secondary 2620 // s == selection 2621 // 0-7, cut buffers 2622 // the data after it is either base64 stuff to copy or ? to request a paste 2623 2624 if(arg == "p;?") { 2625 // i'm using this to request a paste. not quite compatible with xterm, but kinda 2626 // because xterm tends not to answer anyway. 2627 pasteFromPrimary(&sendPasteData); 2628 } else if(arg.length > 2 && arg[0 .. 2] == "p;") { 2629 auto info = arg[2 .. $]; 2630 try { 2631 import std.base64; 2632 auto data = Base64.decode(info); 2633 copyToPrimary(cast(string) data); 2634 } catch(Exception e) {} 2635 } 2636 2637 if(arg == "c;?") { 2638 // i'm using this to request a paste. not quite compatible with xterm, but kinda 2639 // because xterm tends not to answer anyway. 2640 pasteFromClipboard(&sendPasteData); 2641 } else if(arg.length > 2 && arg[0 .. 2] == "c;") { 2642 auto info = arg[2 .. $]; 2643 try { 2644 import std.base64; 2645 auto data = Base64.decode(info); 2646 copyToClipboard(cast(string) data); 2647 } catch(Exception e) {} 2648 } 2649 2650 // selection 2651 if(arg.length > 2 && arg[0 .. 2] == "s;") { 2652 auto info = arg[2 .. $]; 2653 try { 2654 import std.base64; 2655 auto data = Base64.decode(info); 2656 clearSelectionInternal(); 2657 overriddenSelection = cast(string) data; 2658 } catch(Exception e) {} 2659 } 2660 break; 2661 case "4": 2662 // palette change or query 2663 // set color #0 == black 2664 // echo -e '\033]4;0;black\007' 2665 /* 2666 echo -e '\033]4;9;?\007' ; cat 2667 2668 ^[]4;9;rgb:ffff/0000/0000^G 2669 */ 2670 2671 // FIXME: if the palette changes, we should redraw so the change is immediately visible (as if we were using a real palette) 2672 break; 2673 case "104": 2674 // palette reset 2675 // reset color #0 2676 // echo -e '\033[104;0\007' 2677 break; 2678 /* Extensions */ 2679 case "5000": 2680 // change window icon (send a base64 encoded image or something) 2681 /* 2682 The format here is width and height as a single char each 2683 '0'-'9' == 0-9 2684 'a'-'z' == 10 - 36 2685 anything else is invalid 2686 2687 then a palette in hex rgba format (8 chars each), up to 26 entries 2688 2689 then a capital Z 2690 2691 if a palette entry == 'P', it means pull from the current palette (FIXME not implemented) 2692 2693 then 256 characters between a-z (must be lowercase!) which are the palette entries for 2694 the pixels, top to bottom, left to right, so the image is 16x16. if it ends early, the 2695 rest of the data is assumed to be zero 2696 2697 you can also do e.g. 22a, which means repeat a 22 times for some RLE. 2698 2699 anything out of range aborts the operation 2700 */ 2701 auto img = readSmallTextImage(arg); 2702 windowIcon = img; 2703 changeWindowIcon(img); 2704 break; 2705 case "5001": 2706 // demand attention 2707 attentionDemanded = true; 2708 demandAttention(); 2709 break; 2710 /+ 2711 // this might reduce flickering but would it really? idk. 2712 case "5002": 2713 // disable redraw 2714 break; 2715 case "5003": 2716 // re-enable redraw, force it now. 2717 break; 2718 +/ 2719 default: 2720 unknownEscapeSequence("" ~ cast(char) esc[1]); 2721 } 2722 } 2723 } else if(esc[0] == '[' && esc.length > 1) { 2724 switch(esc[$-1]) { 2725 case 'Z': 2726 // CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). 2727 // FIXME? 2728 break; 2729 case 'n': 2730 switch(esc[$-2]) { 2731 import std.string; 2732 // request status report, reply OK 2733 case '5': sendToApplication("\033[0n"); break; 2734 // request cursor position 2735 case '6': sendToApplication(format("\033[%d;%dR", cursorY + 1, cursorX + 1)); break; 2736 default: unknownEscapeSequence(cast(string) esc); 2737 } 2738 break; 2739 case 'A': if(cursorY) cursorY = cursorY - getArgs(1)[0]; break; 2740 case 'B': if(cursorY != this.screenHeight - 1) cursorY = cursorY + getArgs(1)[0]; break; 2741 case 'D': if(cursorX) cursorX = cursorX - getArgs(1)[0]; setTentativeScrollback(cursorX); break; 2742 case 'C': if(cursorX != this.screenWidth - 1) cursorX = cursorX + getArgs(1)[0]; break; 2743 2744 case 'd': cursorY = getArgs(1)[0]-1; break; 2745 2746 case 'E': cursorY = cursorY + getArgs(1)[0]; cursorX = 0; break; 2747 case 'F': cursorY = cursorY - getArgs(1)[0]; cursorX = 0; break; 2748 case 'G': cursorX = getArgs(1)[0] - 1; break; 2749 case 'f': // wikipedia says it is the same except it is a format func instead of editor func. idk what the diff is 2750 case 'H': 2751 auto got = getArgs(1, 1); 2752 cursorX = got[1] - 1; 2753 2754 if(got[0] - 1 == cursorY) 2755 setTentativeScrollback(cursorX); 2756 else 2757 setTentativeScrollback(0); 2758 2759 cursorY = got[0] - 1; 2760 newLineOnNext = false; 2761 break; 2762 case 'L': 2763 // insert lines 2764 scrollDown(getArgs(1)[0]); 2765 break; 2766 case 'M': 2767 // delete lines 2768 if(cursorY + 1 < screenHeight) { 2769 TerminalCell plain; 2770 plain.ch = ' '; 2771 plain.attributes = defaultTextAttributes(); 2772 foreach(i; 0 .. getArgs(1)[0]) { 2773 foreach(y; cursorY .. scrollZoneBottom) 2774 foreach(x; 0 .. screenWidth) { 2775 ASS[y][x] = ASS[y + 1][x]; 2776 ASS[y][x].invalidated = true; 2777 } 2778 foreach(x; 0 .. screenWidth) { 2779 ASS[scrollZoneBottom][x] = plain; 2780 } 2781 } 2782 2783 extendInvalidatedRange(); 2784 } 2785 break; 2786 case 'K': 2787 auto arg = getArgs(0)[0]; 2788 int start, end; 2789 if(arg == 0) { 2790 // clear from cursor to end of line 2791 start = cursorX; 2792 end = this.screenWidth; 2793 } else if(arg == 1) { 2794 // clear from cursor to beginning of line 2795 start = 0; 2796 end = cursorX + 1; 2797 } else if(arg == 2) { 2798 // clear entire line 2799 start = 0; 2800 end = this.screenWidth; 2801 } 2802 2803 TerminalCell plain; 2804 plain.ch = ' '; 2805 plain.attributes = currentAttributes; 2806 2807 for(int i = start; i < end; i++) { 2808 if(ASS[cursorY][i].selected) 2809 clearSelection(); 2810 ASS[cursorY] 2811 [i] = plain; 2812 } 2813 break; 2814 case 's': 2815 pushSavedCursor(cursorPosition); 2816 break; 2817 case 'u': 2818 cursorPosition = popSavedCursor(); 2819 break; 2820 case 'g': 2821 auto arg = getArgs(0)[0]; 2822 TerminalCell plain; 2823 plain.ch = ' '; 2824 plain.attributes = currentAttributes; 2825 if(arg == 0) { 2826 // clear current column 2827 for(int i = 0; i < this.screenHeight; i++) 2828 ASS[i] 2829 [cursorY] = plain; 2830 } else if(arg == 3) { 2831 // clear all 2832 cls(); 2833 } 2834 break; 2835 case 'q': 2836 // xterm also does blinks on the odd numbers (x-1) 2837 if(esc == "[0 q") 2838 cursorStyle = CursorStyle.block; // FIXME: restore default 2839 if(esc == "[2 q") 2840 cursorStyle = CursorStyle.block; 2841 else if(esc == "[4 q") 2842 cursorStyle = CursorStyle.underline; 2843 else if(esc == "[6 q") 2844 cursorStyle = CursorStyle.bar; 2845 2846 changeCursorStyle(cursorStyle); 2847 break; 2848 case 't': 2849 // window commands 2850 // i might support more of these but for now i just want the stack stuff. 2851 2852 auto args = getArgs(0, 0); 2853 if(args[0] == 22) { 2854 // save window title to stack 2855 // xterm says args[1] should tell if it is the window title, the icon title, or both, but meh 2856 titleStack ~= windowTitle; 2857 iconStack ~= windowIcon; 2858 } else if(args[0] == 23) { 2859 // restore from stack 2860 if(titleStack.length) { 2861 windowTitle = titleStack[$ - 1]; 2862 changeWindowTitle(titleStack[$ - 1]); 2863 titleStack = titleStack[0 .. $ - 1]; 2864 } 2865 2866 if(iconStack.length) { 2867 windowIcon = iconStack[$ - 1]; 2868 changeWindowIcon(iconStack[$ - 1]); 2869 iconStack = iconStack[0 .. $ - 1]; 2870 } 2871 } 2872 break; 2873 case 'm': 2874 // FIXME used by xterm to decide whether to construct 2875 // CSI > Pp ; Pv m CSI > Pp m Set/reset key modifier options, xterm. 2876 if(esc[1] == '>') 2877 goto default; 2878 // done 2879 argsLoop: foreach(argIdx, arg; getArgs(0)) 2880 switch(arg) { 2881 case 0: 2882 // normal 2883 currentAttributes = defaultTextAttributes; 2884 break; 2885 case 1: 2886 currentAttributes.bold = true; 2887 break; 2888 case 2: 2889 currentAttributes.faint = true; 2890 break; 2891 case 3: 2892 currentAttributes.italic = true; 2893 break; 2894 case 4: 2895 currentAttributes.underlined = true; 2896 break; 2897 case 5: 2898 currentAttributes.blink = true; 2899 break; 2900 case 6: 2901 // rapid blink, treating the same as regular blink 2902 currentAttributes.blink = true; 2903 break; 2904 case 7: 2905 currentAttributes.inverse = true; 2906 break; 2907 case 8: 2908 currentAttributes.invisible = true; 2909 break; 2910 case 9: 2911 currentAttributes.strikeout = true; 2912 break; 2913 case 10: 2914 // primary font 2915 break; 2916 case 11: .. case 19: 2917 // alternate fonts 2918 break; 2919 case 20: 2920 // Fraktur font 2921 break; 2922 case 21: 2923 // bold off and doubled underlined 2924 break; 2925 case 22: 2926 currentAttributes.bold = false; 2927 currentAttributes.faint = false; 2928 break; 2929 case 23: 2930 currentAttributes.italic = false; 2931 break; 2932 case 24: 2933 currentAttributes.underlined = false; 2934 break; 2935 case 25: 2936 currentAttributes.blink = false; 2937 break; 2938 case 26: 2939 // reserved 2940 break; 2941 case 27: 2942 currentAttributes.inverse = false; 2943 break; 2944 case 28: 2945 currentAttributes.invisible = false; 2946 break; 2947 case 29: 2948 currentAttributes.strikeout = false; 2949 break; 2950 case 30: 2951 .. 2952 case 37: 2953 // set foreground color 2954 /* 2955 Color nc; 2956 ubyte multiplier = currentAttributes.bold ? 255 : 127; 2957 nc.r = cast(ubyte)((arg - 30) & 1) * multiplier; 2958 nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier; 2959 nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier; 2960 nc.a = 255; 2961 */ 2962 currentAttributes.foregroundIndex = cast(ubyte)(arg - 30); 2963 version(with_24_bit_color) 2964 currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)]; 2965 break; 2966 case 38: 2967 // xterm 256 color set foreground color 2968 auto args = getArgs()[argIdx + 1 .. $]; 2969 if(args.length > 3 && args[0] == 2) { 2970 // set color to closest match in palette. but since we have full support, we'll just take it directly 2971 auto fg = Color(args[1], args[2], args[3]); 2972 version(with_24_bit_color) 2973 currentAttributes.foreground = fg; 2974 // and try to find a low default palette entry for maximum compatibility 2975 // 0x8000 == approximation 2976 currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg); 2977 } else if(args.length > 1 && args[0] == 5) { 2978 // set to palette index 2979 version(with_24_bit_color) 2980 currentAttributes.foreground = palette[args[1]]; 2981 currentAttributes.foregroundIndex = cast(ushort) args[1]; 2982 } 2983 break argsLoop; 2984 case 39: 2985 // default foreground color 2986 auto dflt = defaultTextAttributes(); 2987 2988 version(with_24_bit_color) 2989 currentAttributes.foreground = dflt.foreground; 2990 currentAttributes.foregroundIndex = dflt.foregroundIndex; 2991 break; 2992 case 40: 2993 .. 2994 case 47: 2995 // set background color 2996 /* 2997 Color nc; 2998 nc.r = cast(ubyte)((arg - 40) & 1) * 255; 2999 nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255; 3000 nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255; 3001 nc.a = 255; 3002 */ 3003 3004 currentAttributes.backgroundIndex = cast(ubyte)(arg - 40); 3005 //currentAttributes.background = nc; 3006 version(with_24_bit_color) 3007 currentAttributes.background = palette[arg-40]; 3008 break; 3009 case 48: 3010 // xterm 256 color set background color 3011 auto args = getArgs()[argIdx + 1 .. $]; 3012 if(args.length > 3 && args[0] == 2) { 3013 // set color to closest match in palette. but since we have full support, we'll just take it directly 3014 auto bg = Color(args[1], args[2], args[3]); 3015 version(with_24_bit_color) 3016 currentAttributes.background = Color(args[1], args[2], args[3]); 3017 3018 // and try to find a low default palette entry for maximum compatibility 3019 // 0x8000 == this is an approximation 3020 currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg); 3021 } else if(args.length > 1 && args[0] == 5) { 3022 // set to palette index 3023 version(with_24_bit_color) 3024 currentAttributes.background = palette[args[1]]; 3025 currentAttributes.backgroundIndex = cast(ushort) args[1]; 3026 } 3027 3028 break argsLoop; 3029 case 49: 3030 // default background color 3031 auto dflt = defaultTextAttributes(); 3032 3033 version(with_24_bit_color) 3034 currentAttributes.background = dflt.background; 3035 currentAttributes.backgroundIndex = dflt.backgroundIndex; 3036 break; 3037 case 51: 3038 // framed 3039 break; 3040 case 52: 3041 // encircled 3042 break; 3043 case 53: 3044 // overlined 3045 break; 3046 case 54: 3047 // not framed or encircled 3048 break; 3049 case 55: 3050 // not overlined 3051 break; 3052 case 90: .. case 97: 3053 // high intensity foreground color 3054 break; 3055 case 100: .. case 107: 3056 // high intensity background color 3057 break; 3058 default: 3059 unknownEscapeSequence(cast(string) esc); 3060 } 3061 break; 3062 case 'J': 3063 // erase in display 3064 auto arg = getArgs(0)[0]; 3065 switch(arg) { 3066 case 0: 3067 TerminalCell plain; 3068 plain.ch = ' '; 3069 plain.attributes = currentAttributes; 3070 // erase below 3071 foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) { 3072 if(alternateScreenActive) 3073 alternateScreen[i] = plain; 3074 else 3075 normalScreen[i] = plain; 3076 } 3077 break; 3078 case 1: 3079 // erase above 3080 unknownEscapeSequence("FIXME"); 3081 break; 3082 case 2: 3083 // erase all 3084 cls(); 3085 break; 3086 default: unknownEscapeSequence(cast(string) esc); 3087 } 3088 break; 3089 case 'r': 3090 if(esc[1] != '?') { 3091 // set scrolling zone 3092 // default should be full size of window 3093 auto args = getArgs(1, screenHeight); 3094 3095 // FIXME: these are supposed to be per-buffer 3096 scrollZoneTop = args[0] - 1; 3097 scrollZoneBottom = args[1] - 1; 3098 3099 if(scrollZoneTop < 0) 3100 scrollZoneTop = 0; 3101 if(scrollZoneBottom > screenHeight) 3102 scrollZoneBottom = screenHeight - 1; 3103 } else { 3104 // restore... something FIXME 3105 } 3106 break; 3107 case 'h': 3108 if(esc[1] != '?') 3109 foreach(arg; getArgs()) 3110 switch(arg) { 3111 case 4: 3112 insertMode = true; 3113 break; 3114 case 34: 3115 // no idea. vim inside screen sends it 3116 break; 3117 default: unknownEscapeSequence(cast(string) esc); 3118 } 3119 else 3120 //import std.stdio; writeln("h magic ", cast(string) esc); 3121 foreach(arg; getArgsBase(2, null)) { 3122 if(arg > 65535) { 3123 /* Extensions */ 3124 if(arg < 65536 + 65535) { 3125 // activate hyperlink 3126 hyperlinkFlipper = !hyperlinkFlipper; 3127 hyperlinkActive = true; 3128 hyperlinkNumber = arg - 65536; 3129 } 3130 } else 3131 switch(arg) { 3132 case 1: 3133 // application cursor keys 3134 applicationCursorKeys = true; 3135 break; 3136 case 3: 3137 // 132 column mode 3138 break; 3139 case 4: 3140 // smooth scroll 3141 break; 3142 case 5: 3143 // reverse video 3144 reverseVideo = true; 3145 break; 3146 case 6: 3147 // origin mode 3148 break; 3149 case 7: 3150 // wraparound mode 3151 wraparoundMode = false; 3152 // FIXME: wraparoundMode i think is supposed to be off by default but then bash doesn't work right so idk, this gives the best results 3153 break; 3154 case 9: 3155 allMouseTrackingOff(); 3156 mouseButtonTracking = true; 3157 break; 3158 case 12: 3159 // start blinking cursor 3160 break; 3161 case 1034: 3162 // meta keys???? 3163 break; 3164 case 1049: 3165 // Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first. 3166 alternateScreenActive = true; 3167 scrollLock = false; 3168 pushSavedCursor(cursorPosition); 3169 cls(); 3170 notifyScrollbarRelevant(false, false); 3171 break; 3172 case 1000: 3173 // send mouse X&Y on button press and release 3174 allMouseTrackingOff(); 3175 mouseButtonTracking = true; 3176 mouseButtonReleaseTracking = true; 3177 break; 3178 case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it 3179 break; 3180 case 1002: 3181 allMouseTrackingOff(); 3182 mouseButtonTracking = true; 3183 mouseButtonReleaseTracking = true; 3184 mouseButtonMotionTracking = true; 3185 // use cell motion mouse tracking 3186 break; 3187 case 1003: 3188 // ALL motion is sent 3189 allMouseTrackingOff(); 3190 mouseButtonTracking = true; 3191 mouseButtonReleaseTracking = true; 3192 mouseMotionTracking = true; 3193 break; 3194 case 1004: 3195 sendFocusEvents = true; 3196 break; 3197 case 1005: 3198 utf8MouseMode = true; 3199 // enable utf-8 mouse mode 3200 /* 3201 UTF-8 (1005) 3202 This enables UTF-8 encoding for Cx and Cy under all tracking 3203 modes, expanding the maximum encodable position from 223 to 3204 2015. For positions less than 95, the resulting output is 3205 identical under both modes. Under extended mouse mode, posi- 3206 tions greater than 95 generate "extra" bytes which will con- 3207 fuse applications which do not treat their input as a UTF-8 3208 stream. Likewise, Cb will be UTF-8 encoded, to reduce confu- 3209 sion with wheel mouse events. 3210 Under normal mouse mode, positions outside (160,94) result in 3211 byte pairs which can be interpreted as a single UTF-8 charac- 3212 ter; applications which do treat their input as UTF-8 will 3213 almost certainly be confused unless extended mouse mode is 3214 active. 3215 This scheme has the drawback that the encoded coordinates will 3216 not pass through luit unchanged, e.g., for locales using non- 3217 UTF-8 encoding. 3218 */ 3219 break; 3220 case 1006: 3221 /* 3222 SGR (1006) 3223 The normal mouse response is altered to use CSI < followed by 3224 semicolon-separated encoded button value, the Cx and Cy ordi- 3225 nates and a final character which is M for button press and m 3226 for button release. 3227 o The encoded button value in this case does not add 32 since 3228 that was useful only in the X10 scheme for ensuring that the 3229 byte containing the button value is a printable code. 3230 o The modifiers are encoded in the same way. 3231 o A different final character is used for button release to 3232 resolve the X10 ambiguity regarding which button was 3233 released. 3234 The highlight tracking responses are also modified to an SGR- 3235 like format, using the same SGR-style scheme and button-encod- 3236 ings. 3237 3238 Note that M is used for motion; m is only release 3239 */ 3240 sgrMouseMode = true; 3241 break; 3242 case 1014: 3243 // ARSD extension: it is 1002 but selective, only 3244 // on top row, row with cursor, or else if middle click/wheel. 3245 // 3246 // Quite specifically made for my getline function! 3247 allMouseTrackingOff(); 3248 3249 mouseButtonMotionTracking = true; 3250 mouseButtonTracking = true; 3251 mouseButtonReleaseTracking = true; 3252 selectiveMouseTracking = true; 3253 break; 3254 case 1015: 3255 /* 3256 URXVT (1015) 3257 The normal mouse response is altered to use CSI followed by 3258 semicolon-separated encoded button value, the Cx and Cy ordi- 3259 nates and final character M . 3260 This uses the same button encoding as X10, but printing it as 3261 a decimal integer rather than as a single byte. 3262 However, CSI M can be mistaken for DL (delete lines), while 3263 the highlight tracking CSI T can be mistaken for SD (scroll 3264 down), and the Window manipulation controls. For these rea- 3265 sons, the 1015 control is not recommended; it is not an 3266 improvement over 1005. 3267 */ 3268 break; 3269 case 1048: 3270 pushSavedCursor(cursorPosition); 3271 break; 3272 case 2004: 3273 bracketedPasteMode = true; 3274 break; 3275 case 3004: 3276 bracketedHyperlinkMode = true; 3277 break; 3278 case 1047: 3279 case 47: 3280 alternateScreenActive = true; 3281 scrollLock = false; 3282 cls(); 3283 notifyScrollbarRelevant(false, false); 3284 break; 3285 case 25: 3286 cursorShowing = true; 3287 break; 3288 3289 /* Done */ 3290 default: unknownEscapeSequence(cast(string) esc); 3291 } 3292 } 3293 break; 3294 case 'p': 3295 // it is asking a question... and tbh i don't care. 3296 break; 3297 case 'l': 3298 //import std.stdio; writeln("l magic ", cast(string) esc); 3299 if(esc[1] != '?') 3300 foreach(arg; getArgs()) 3301 switch(arg) { 3302 case 4: 3303 insertMode = false; 3304 break; 3305 case 34: 3306 // no idea. vim inside screen sends it 3307 break; 3308 case 1004: 3309 sendFocusEvents = false; 3310 break; 3311 case 1005: 3312 // turn off utf-8 mouse 3313 utf8MouseMode = false; 3314 break; 3315 case 1006: 3316 // turn off sgr mouse 3317 sgrMouseMode = false; 3318 break; 3319 case 1015: 3320 // turn off urxvt mouse 3321 break; 3322 default: unknownEscapeSequence(cast(string) esc); 3323 } 3324 else 3325 foreach(arg; getArgsBase(2, null)) { 3326 if(arg > 65535) { 3327 /* Extensions */ 3328 if(arg < 65536 + 65535) 3329 hyperlinkActive = false; 3330 } else 3331 switch(arg) { 3332 case 1: 3333 // normal cursor keys 3334 applicationCursorKeys = false; 3335 break; 3336 case 3: 3337 // 80 column mode 3338 break; 3339 case 4: 3340 // smooth scroll 3341 break; 3342 case 5: 3343 // normal video 3344 reverseVideo = false; 3345 break; 3346 case 6: 3347 // normal cursor mode 3348 break; 3349 case 7: 3350 // wraparound mode 3351 wraparoundMode = true; 3352 break; 3353 case 12: 3354 // stop blinking cursor 3355 break; 3356 case 1034: 3357 // meta keys???? 3358 break; 3359 case 1049: 3360 cursorPosition = popSavedCursor; 3361 wraparoundMode = true; 3362 3363 returnToNormalScreen(); 3364 break; 3365 case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it 3366 break; 3367 case 9: 3368 case 1000: 3369 case 1002: 3370 case 1003: 3371 case 1014: // arsd extension 3372 allMouseTrackingOff(); 3373 break; 3374 case 1005: 3375 case 1006: 3376 // idk 3377 break; 3378 case 1048: 3379 cursorPosition = popSavedCursor; 3380 break; 3381 case 2004: 3382 bracketedPasteMode = false; 3383 break; 3384 case 3004: 3385 bracketedHyperlinkMode = false; 3386 break; 3387 case 1047: 3388 case 47: 3389 returnToNormalScreen(); 3390 break; 3391 case 25: 3392 cursorShowing = false; 3393 break; 3394 default: unknownEscapeSequence(cast(string) esc); 3395 } 3396 } 3397 break; 3398 case 'X': 3399 // erase characters 3400 auto count = getArgs(1)[0]; 3401 TerminalCell plain; 3402 plain.ch = ' '; 3403 plain.attributes = currentAttributes; 3404 foreach(cnt; 0 .. count) { 3405 ASS[cursorY][cnt + cursorX] = plain; 3406 } 3407 break; 3408 case 'S': 3409 auto count = getArgs(1)[0]; 3410 // scroll up 3411 scrollUp(count); 3412 break; 3413 case 'T': 3414 auto count = getArgs(1)[0]; 3415 // scroll down 3416 scrollDown(count); 3417 break; 3418 case 'P': 3419 auto count = getArgs(1)[0]; 3420 // delete characters 3421 3422 foreach(cnt; 0 .. count) { 3423 for(int i = cursorX; i < this.screenWidth-1; i++) { 3424 if(ASS[cursorY][i].selected) 3425 clearSelection(); 3426 ASS[cursorY][i] = ASS[cursorY][i + 1]; 3427 ASS[cursorY][i].invalidated = true; 3428 } 3429 3430 if(ASS[cursorY][this.screenWidth - 1].selected) 3431 clearSelection(); 3432 ASS[cursorY][this.screenWidth-1].ch = ' '; 3433 ASS[cursorY][this.screenWidth-1].invalidated = true; 3434 } 3435 3436 extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY); 3437 break; 3438 case '@': 3439 // insert blank characters 3440 auto count = getArgs(1)[0]; 3441 foreach(idx; 0 .. count) { 3442 for(int i = this.screenWidth - 1; i > cursorX; i--) { 3443 ASS[cursorY][i] = ASS[cursorY][i - 1]; 3444 ASS[cursorY][i].invalidated = true; 3445 } 3446 ASS[cursorY][cursorX].ch = ' '; 3447 ASS[cursorY][cursorX].invalidated = true; 3448 } 3449 3450 extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY); 3451 break; 3452 case 'c': 3453 // send device attributes 3454 // FIXME: what am i supposed to do here? 3455 //sendToApplication("\033[>0;138;0c"); 3456 //sendToApplication("\033[?62;"); 3457 sendToApplication(terminalIdCode); 3458 break; 3459 default: 3460 // [42\esc] seems to have gotten here once somehow 3461 // also [24\esc] 3462 unknownEscapeSequence("" ~ cast(string) esc); 3463 } 3464 } else { 3465 unknownEscapeSequence(cast(string) esc); 3466 } 3467 } 3468 } 3469 } 3470 3471 // These match the numbers in terminal.d, so you can just cast it back and forth 3472 // and the names match simpledisplay.d so you can convert that automatically too 3473 enum TerminalKey : int { 3474 Escape = 0x1b + 0xF0000, /// . 3475 F1 = 0x70 + 0xF0000, /// . 3476 F2 = 0x71 + 0xF0000, /// . 3477 F3 = 0x72 + 0xF0000, /// . 3478 F4 = 0x73 + 0xF0000, /// . 3479 F5 = 0x74 + 0xF0000, /// . 3480 F6 = 0x75 + 0xF0000, /// . 3481 F7 = 0x76 + 0xF0000, /// . 3482 F8 = 0x77 + 0xF0000, /// . 3483 F9 = 0x78 + 0xF0000, /// . 3484 F10 = 0x79 + 0xF0000, /// . 3485 F11 = 0x7A + 0xF0000, /// . 3486 F12 = 0x7B + 0xF0000, /// . 3487 Left = 0x25 + 0xF0000, /// . 3488 Right = 0x27 + 0xF0000, /// . 3489 Up = 0x26 + 0xF0000, /// . 3490 Down = 0x28 + 0xF0000, /// . 3491 Insert = 0x2d + 0xF0000, /// . 3492 Delete = 0x2e + 0xF0000, /// . 3493 Home = 0x24 + 0xF0000, /// . 3494 End = 0x23 + 0xF0000, /// . 3495 PageUp = 0x21 + 0xF0000, /// . 3496 PageDown = 0x22 + 0xF0000, /// . 3497 ScrollLock = 0x91 + 0xF0000, 3498 } 3499 3500 /* These match simpledisplay.d which match terminal.d, so you can just cast them */ 3501 3502 enum MouseEventType : int { 3503 motion = 0, 3504 buttonPressed = 1, 3505 buttonReleased = 2, 3506 } 3507 3508 enum MouseButton : int { 3509 // these names assume a right-handed mouse 3510 left = 1, 3511 right = 2, 3512 middle = 4, 3513 wheelUp = 8, 3514 wheelDown = 16, 3515 } 3516 3517 3518 3519 /* 3520 mixin template ImageSupport() { 3521 import arsd.png; 3522 import arsd.bmp; 3523 } 3524 */ 3525 3526 3527 /* helper functions that are generally useful but not necessarily required */ 3528 3529 version(use_libssh2) { 3530 import arsd.libssh2; 3531 void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) { 3532 3533 int tries = 0; 3534 try_again: 3535 try { 3536 import std.socket; 3537 3538 if(libssh2_init(0)) 3539 throw new Exception("libssh2_init"); 3540 scope(exit) 3541 libssh2_exit(); 3542 3543 auto socket = new Socket(AddressFamily.INET, SocketType.STREAM); 3544 socket.connect(new InternetAddress(host, port)); 3545 scope(exit) socket.close(); 3546 3547 auto session = libssh2_session_init_ex(null, null, null, null); 3548 if(session is null) throw new Exception("init session"); 3549 scope(exit) 3550 libssh2_session_disconnect_ex(session, 0, "normal", "EN"); 3551 3552 libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1); 3553 3554 if(libssh2_session_handshake(session, socket.handle)) 3555 throw new Exception("handshake"); 3556 3557 auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1); 3558 if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint) 3559 throw new Exception("fingerprint"); 3560 3561 import std.string : toStringz; 3562 if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, cast(int) username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null)) 3563 throw new Exception("auth"); 3564 3565 3566 auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0); 3567 3568 if(channel is null) 3569 throw new Exception("channel open"); 3570 3571 scope(exit) 3572 libssh2_channel_free(channel); 3573 3574 // libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length); 3575 3576 if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0)) 3577 throw new Exception("pty"); 3578 3579 if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0)) 3580 throw new Exception("process_startup"); 3581 3582 libssh2_keepalive_config(session, 0, 60); 3583 libssh2_session_set_blocking(session, 0); 3584 3585 masterFunc(socket, session, channel); 3586 } catch(Exception e) { 3587 if(e.msg == "handshake") { 3588 tries++; 3589 import core.thread; 3590 Thread.sleep(200.msecs); 3591 if(tries < 10) 3592 goto try_again; 3593 } 3594 3595 throw e; 3596 } 3597 } 3598 3599 } else 3600 version(Posix) { 3601 extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp); 3602 pragma(lib, "util"); 3603 3604 /// this is good 3605 void startChild(alias masterFunc)(string program, string[] args) { 3606 import core.sys.posix.termios; 3607 import core.sys.posix.signal; 3608 import core.sys.posix.sys.wait; 3609 __gshared static int childrenAlive = 0; 3610 extern(C) nothrow static @nogc 3611 void childdead(int) { 3612 childrenAlive--; 3613 3614 wait(null); 3615 3616 version(with_eventloop) 3617 try { 3618 import arsd.eventloop; 3619 if(childrenAlive <= 0) 3620 exit(); 3621 } catch(Exception e){} 3622 } 3623 3624 signal(SIGCHLD, &childdead); 3625 3626 int master; 3627 int pid = forkpty(&master, null, null, null); 3628 if(pid == -1) 3629 throw new Exception("forkpty"); 3630 if(pid == 0) { 3631 import std.process; 3632 environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes 3633 environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions 3634 3635 import std.string; 3636 if(environment["LANG"].indexOf("UTF-8") == -1) 3637 environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?) 3638 3639 import core.sys.posix.unistd; 3640 3641 import core.stdc.stdlib; 3642 char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1)); 3643 if(argv is null) throw new Exception("malloc"); 3644 foreach(i, arg; args) { 3645 argv[i] = cast(char*) malloc(arg.length + 1); 3646 if(argv[i] is null) throw new Exception("malloc"); 3647 argv[i][0 .. arg.length] = arg[]; 3648 argv[i][arg.length] = 0; 3649 } 3650 3651 argv[args.length] = null; 3652 3653 termios info; 3654 ubyte[128] hack; // jic that druntime definition is still wrong 3655 tcgetattr(master, &info); 3656 info.c_cc[VERASE] = '\b'; 3657 tcsetattr(master, TCSANOW, &info); 3658 3659 core.sys.posix.unistd.execv(argv[0], argv); 3660 } else { 3661 childrenAlive = 1; 3662 import arsd.core; 3663 auto ep = new ExternalProcess(pid); 3664 ep.oncomplete = (ep) { 3665 childrenAlive = 0; 3666 ICoreEventLoop.exitApplication(); 3667 }; 3668 masterFunc(master); 3669 } 3670 } 3671 } else 3672 version(Windows) { 3673 import core.sys.windows.windows; 3674 3675 version(winpty) { 3676 alias HPCON = HANDLE; 3677 extern(Windows) 3678 HRESULT function(HPCON, COORD) ResizePseudoConsole; 3679 extern(Windows) 3680 HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole; 3681 extern(Windows) 3682 void function(HPCON) ClosePseudoConsole; 3683 } 3684 3685 extern(Windows) 3686 BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD); 3687 extern(Windows) 3688 BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL); 3689 extern(Windows) 3690 private BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*); 3691 extern(Windows) 3692 BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam); 3693 3694 extern(Windows) 3695 BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM); 3696 extern(Windows) 3697 BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags); 3698 extern(Windows) 3699 BOOL SetHandleInformation(HANDLE, DWORD, DWORD); 3700 extern(Windows) 3701 HANDLE CreateNamedPipeA( 3702 const(char)* lpName, 3703 DWORD dwOpenMode, 3704 DWORD dwPipeMode, 3705 DWORD nMaxInstances, 3706 DWORD nOutBufferSize, 3707 DWORD nInBufferSize, 3708 DWORD nDefaultTimeOut, 3709 LPSECURITY_ATTRIBUTES lpSecurityAttributes 3710 ); 3711 extern(Windows) 3712 BOOL UnregisterWait(HANDLE); 3713 3714 struct STARTUPINFOEXA { 3715 STARTUPINFOA StartupInfo; 3716 void* lpAttributeList; 3717 } 3718 3719 enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016; 3720 enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000; 3721 3722 extern(Windows) 3723 BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T); 3724 extern(Windows) 3725 BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T); 3726 3727 __gshared HANDLE waitHandle; 3728 __gshared bool childDead; 3729 extern(Windows) 3730 void childCallback(void* tidp, bool) { 3731 auto tid = cast(DWORD) tidp; 3732 UnregisterWait(waitHandle); 3733 3734 PostThreadMessageA(tid, WM_QUIT, 0, 0); 3735 childDead = true; 3736 //stupidThreadAlive = false; 3737 } 3738 3739 3740 3741 extern(Windows) 3742 void SetLastError(DWORD); 3743 3744 /// this is good. best to call it with plink.exe so it can talk to unix 3745 /// note that plink asks for the password out of band, so it won't actually work like that. 3746 /// thus specify the password on the command line or better yet, use a private key file 3747 /// e.g. 3748 /// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\""); 3749 void startChild(alias masterFunc)(string program, string commandLine) { 3750 import core.sys.windows.windows; 3751 3752 import arsd.core : MyCreatePipeEx; 3753 3754 import std.conv; 3755 3756 SECURITY_ATTRIBUTES saAttr; 3757 saAttr.nLength = SECURITY_ATTRIBUTES.sizeof; 3758 saAttr.bInheritHandle = true; 3759 saAttr.lpSecurityDescriptor = null; 3760 3761 HANDLE inreadPipe; 3762 HANDLE inwritePipe; 3763 if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0) 3764 throw new Exception("CreatePipe"); 3765 if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 3766 throw new Exception("SetHandleInformation"); 3767 HANDLE outreadPipe; 3768 HANDLE outwritePipe; 3769 3770 version(winpty) 3771 auto flags = 0; 3772 else 3773 auto flags = FILE_FLAG_OVERLAPPED; 3774 3775 if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0) 3776 throw new Exception("CreatePipe"); 3777 if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 3778 throw new Exception("SetHandleInformation"); 3779 3780 version(winpty) { 3781 3782 auto lib = LoadLibrary("kernel32.dll"); 3783 if(lib is null) throw new Exception("holy wtf batman"); 3784 scope(exit) FreeLibrary(lib); 3785 3786 CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole"); 3787 ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole"); 3788 ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole"); 3789 3790 if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null) 3791 throw new Exception("Windows pseudo console not available on this version"); 3792 3793 initPipeHack(outreadPipe); 3794 3795 HPCON hpc; 3796 auto result = CreatePseudoConsole( 3797 COORD(80, 24), 3798 inreadPipe, 3799 outwritePipe, 3800 0, // flags 3801 &hpc 3802 ); 3803 3804 assert(result == S_OK); 3805 3806 scope(exit) 3807 ClosePseudoConsole(hpc); 3808 } 3809 3810 STARTUPINFOEXA siex; 3811 siex.StartupInfo.cb = siex.sizeof; 3812 3813 version(winpty) { 3814 size_t size; 3815 InitializeProcThreadAttributeList(null, 1, 0, &size); 3816 ubyte[] wtf = new ubyte[](size); 3817 siex.lpAttributeList = wtf.ptr; 3818 InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size); 3819 UpdateProcThreadAttribute( 3820 siex.lpAttributeList, 3821 0, 3822 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, 3823 hpc, 3824 hpc.sizeof, 3825 null, 3826 null 3827 ); 3828 } {//else { 3829 siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES; 3830 siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe; 3831 siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe; 3832 siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe; 3833 } 3834 3835 PROCESS_INFORMATION pi; 3836 import std.conv; 3837 3838 if(commandLine.length > 255) 3839 throw new Exception("command line too long"); 3840 char[256] cmdLine; 3841 cmdLine[0 .. commandLine.length] = commandLine[]; 3842 cmdLine[commandLine.length] = 0; 3843 import std.string; 3844 if(CreateProcessA(program is null ? null : toStringz(program), cmdLine.ptr, null, null, true, EXTENDED_STARTUPINFO_PRESENT /*0x08000000 /* CREATE_NO_WINDOW */, null /* environment */, null, cast(STARTUPINFOA*) &siex, &pi) == 0) 3845 throw new Exception("CreateProcess " ~ to!string(GetLastError())); 3846 3847 if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0) 3848 throw new Exception("RegisterWaitForSingleObject"); 3849 3850 version(winpty) 3851 masterFunc(hpc, inwritePipe, outreadPipe); 3852 else 3853 masterFunc(inwritePipe, outreadPipe); 3854 3855 //stupidThreadAlive = false; 3856 3857 //term.stupidThread.join(); 3858 3859 /* // FIXME: we should close but only if we're legit done 3860 // masterFunc typically runs an event loop but it might not. 3861 CloseHandle(inwritePipe); 3862 CloseHandle(outreadPipe); 3863 3864 CloseHandle(pi.hThread); 3865 CloseHandle(pi.hProcess); 3866 */ 3867 } 3868 } 3869 3870 /// Implementation of TerminalEmulator's abstract functions that forward them to output 3871 mixin template ForwardVirtuals(alias writer) { 3872 static import arsd.color; 3873 3874 protected override void changeCursorStyle(CursorStyle style) { 3875 // FIXME: this should probably just import utility 3876 final switch(style) { 3877 case TerminalEmulator.CursorStyle.block: 3878 writer("\033[2 q"); 3879 break; 3880 case TerminalEmulator.CursorStyle.underline: 3881 writer("\033[4 q"); 3882 break; 3883 case TerminalEmulator.CursorStyle.bar: 3884 writer("\033[6 q"); 3885 break; 3886 } 3887 } 3888 3889 protected override void changeWindowTitle(string t) { 3890 import std.process; 3891 if(t.length && environment["TERM"] != "linux") 3892 writer("\033]0;"~t~"\007"); 3893 } 3894 3895 protected override void changeWindowIcon(arsd.color.IndexedImage t) { 3896 if(t !is null) { 3897 // forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux 3898 import std.process; 3899 if(environment["TERM"] != "linux") 3900 writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007"); 3901 } 3902 } 3903 3904 protected override void changeIconTitle(string) {} // FIXME 3905 protected override void changeTextAttributes(TextAttributes) {} // FIXME 3906 protected override void soundBell() { 3907 writer("\007"); 3908 } 3909 protected override void demandAttention() { 3910 import std.process; 3911 if(environment["TERM"] != "linux") 3912 writer("\033]5001;1\007"); // the 1 there means true but is currently ignored 3913 } 3914 protected override void copyToClipboard(string text) { 3915 // this is xterm compatible, though xterm rarely implements it 3916 import std.base64; 3917 // idk why the cast is needed here 3918 writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007"); 3919 } 3920 protected override void pasteFromClipboard(void delegate(in char[]) dg) { 3921 // this is a slight extension. xterm invented the string - it means request the primary selection - 3922 // but it generally doesn't actually get a reply. so i'm using it to request the primary which will be 3923 // sent as a pasted strong. 3924 // (xterm prolly doesn't do it by default because it is potentially insecure, letting a naughty app steal your clipboard data, but meh, any X application can do that too and it is useful here for nesting.) 3925 writer("\033]52;c;?\007"); 3926 } 3927 protected override void copyToPrimary(string text) { 3928 import std.base64; 3929 writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007"); 3930 } 3931 protected override void pasteFromPrimary(void delegate(in char[]) dg) { 3932 writer("\033]52;p;?\007"); 3933 } 3934 3935 } 3936 3937 /// you can pass this as PtySupport's arguments when you just don't care 3938 final void doNothing() {} 3939 3940 version(winpty) { 3941 __gshared static HANDLE inputEvent; 3942 __gshared static HANDLE magicEvent; 3943 __gshared static ubyte[] helperBuffer; 3944 __gshared static HANDLE helperThread; 3945 3946 static void initPipeHack(void* ptr) { 3947 inputEvent = CreateEvent(null, false, false, null); 3948 assert(inputEvent !is null); 3949 magicEvent = CreateEvent(null, false, true, null); 3950 assert(magicEvent !is null); 3951 3952 helperThread = CreateThread( 3953 null, 3954 0, 3955 &actuallyRead, 3956 ptr, 3957 0, 3958 null 3959 ); 3960 3961 assert(helperThread !is null); 3962 } 3963 3964 extern(Windows) static 3965 uint actuallyRead(void* ptr) { 3966 ubyte[4096] buffer; 3967 DWORD got; 3968 while(true) { 3969 // wait for the other thread to tell us they 3970 // are done... 3971 WaitForSingleObject(magicEvent, INFINITE); 3972 auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null); 3973 helperBuffer = buffer[0 .. got]; 3974 // tells the other thread it is allowed to read 3975 // readyToReadPty 3976 SetEvent(inputEvent); 3977 } 3978 assert(0); 3979 } 3980 3981 3982 } 3983 3984 /// You must implement a function called redraw() and initialize the members in your constructor 3985 mixin template PtySupport(alias resizeHelper) { 3986 // Initialize these! 3987 3988 final void redraw_() { 3989 if(invalidateAll) { 3990 extendInvalidatedRange(0, 0, this.screenWidth, this.screenHeight); 3991 if(alternateScreenActive) 3992 foreach(ref t; alternateScreen) 3993 t.invalidated = true; 3994 else 3995 foreach(ref t; normalScreen) 3996 t.invalidated = true; 3997 invalidateAll = false; 3998 } 3999 redraw(); 4000 //soundBell(); 4001 } 4002 4003 version(use_libssh2) { 4004 import arsd.libssh2; 4005 LIBSSH2_CHANNEL* sshChannel; 4006 } else version(Windows) { 4007 import core.sys.windows.windows; 4008 HANDLE stdin; 4009 HANDLE stdout; 4010 } else version(Posix) { 4011 int master; 4012 } 4013 4014 version(use_libssh2) { } 4015 else version(Posix) { 4016 int previousProcess = 0; 4017 int activeProcess = 0; 4018 int activeProcessWhenResized = 0; 4019 bool resizedRecently; 4020 4021 /* 4022 so, this isn't perfect, but it is meant to send the resize signal to an existing process 4023 when it isn't in the front when you resize. 4024 4025 For example, open vim and resize. Then exit vim. We want bash to be updated. 4026 4027 But also don't want to do too many spurious signals. 4028 4029 It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but 4030 the outer bash won't see it. I guess I need some kind of process stack. 4031 4032 but it is okish. 4033 */ 4034 override void outputOccurred() { 4035 import core.sys.posix.unistd; 4036 auto pgrp = tcgetpgrp(master); 4037 if(pgrp != -1) { 4038 if(pgrp != activeProcess) { 4039 auto previousProcessAtStartup = previousProcess; 4040 4041 previousProcess = activeProcess; 4042 activeProcess = pgrp; 4043 4044 if(resizedRecently) { 4045 if(activeProcess != activeProcessWhenResized) { 4046 resizedRecently = false; 4047 4048 if(activeProcess == previousProcessAtStartup) { 4049 //import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight); 4050 4051 import core.sys.posix.signal; 4052 kill(-activeProcess, 28 /* 28 == SIGWINCH*/); 4053 } 4054 } 4055 } 4056 } 4057 } 4058 4059 4060 super.outputOccurred(); 4061 } 4062 //return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline"); 4063 } 4064 4065 4066 override void resizeTerminal(int w, int h) { 4067 version(Posix) { 4068 activeProcessWhenResized = activeProcess; 4069 resizedRecently = true; 4070 } 4071 4072 resizeHelper(); 4073 4074 super.resizeTerminal(w, h); 4075 4076 version(use_libssh2) { 4077 libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0); 4078 } else version(Posix) { 4079 import core.sys.posix.sys.ioctl; 4080 winsize win; 4081 win.ws_col = cast(ushort) w; 4082 win.ws_row = cast(ushort) h; 4083 4084 ioctl(master, TIOCSWINSZ, &win); 4085 } else version(Windows) { 4086 version(winpty) { 4087 COORD coord; 4088 coord.X = cast(ushort) w; 4089 coord.Y = cast(ushort) h; 4090 ResizePseudoConsole(hpc, coord); 4091 } else { 4092 sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]); 4093 } 4094 } else static assert(0); 4095 } 4096 4097 protected override void sendToApplication(scope const(void)[] data) { 4098 version(use_libssh2) { 4099 while(data.length) { 4100 auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length); 4101 if(sent < 0) 4102 throw new Exception("libssh2_channel_write_ex"); 4103 data = data[sent .. $]; 4104 } 4105 } else version(Windows) { 4106 import std.conv; 4107 uint written; 4108 if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0) 4109 throw new Exception("WriteFile " ~ to!string(GetLastError())); 4110 } else version(Posix) { 4111 import core.sys.posix.unistd; 4112 int frozen; 4113 while(data.length) { 4114 enum MAX_SEND = 1024 * 20; 4115 auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length); 4116 //import std.stdio; writeln("ROFL ", sent, " ", data.length); 4117 4118 import core.stdc.errno; 4119 if(sent == -1 && errno == 11) { 4120 import core.thread; 4121 if(frozen == 50) 4122 throw new Exception("write froze up"); 4123 frozen++; 4124 Thread.sleep(10.msecs); 4125 //import std.stdio; writeln("lol"); 4126 continue; // just try again 4127 } 4128 4129 frozen = 0; 4130 4131 import std.conv; 4132 if(sent < 0) 4133 throw new Exception("write " ~ to!string(errno)); 4134 4135 data = data[sent .. $]; 4136 } 4137 } else static assert(0); 4138 } 4139 4140 version(use_libssh2) { 4141 int readyToRead(int fd) { 4142 int count = 0; // if too much stuff comes at once, we still want to be responsive 4143 while(true) { 4144 ubyte[4096] buffer; 4145 auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length); 4146 if(got == LIBSSH2_ERROR_EAGAIN) 4147 break; // got it all for now 4148 if(got < 0) 4149 throw new Exception("libssh2_channel_read_ex"); 4150 if(got == 0) 4151 break; // NOT an error! 4152 4153 super.sendRawInput(buffer[0 .. got]); 4154 count++; 4155 4156 if(count == 5) { 4157 count = 0; 4158 redraw_(); 4159 justRead(); 4160 } 4161 } 4162 4163 if(libssh2_channel_eof(sshChannel)) { 4164 libssh2_channel_close(sshChannel); 4165 libssh2_channel_wait_closed(sshChannel); 4166 4167 return 1; 4168 } 4169 4170 if(count != 0) { 4171 redraw_(); 4172 justRead(); 4173 } 4174 return 0; 4175 } 4176 } else version(winpty) { 4177 void readyToReadPty() { 4178 super.sendRawInput(helperBuffer); 4179 SetEvent(magicEvent); // tell the other thread we have finished 4180 redraw_(); 4181 justRead(); 4182 } 4183 } else version(Windows) { 4184 OVERLAPPED* overlapped; 4185 bool overlappedBufferLocked; 4186 ubyte[4096] overlappedBuffer; 4187 extern(Windows) 4188 static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) { 4189 assert(overlapped !is null); 4190 typeof(this) w = cast(typeof(this)) overlapped.hEvent; 4191 4192 if(numberOfBytes) { 4193 w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]); 4194 w.redraw_(); 4195 } 4196 import std.conv; 4197 4198 if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) { 4199 if(GetLastError() == 997) 4200 { } // there's pending i/o, let's just ignore for now and it should tell us later that it completed 4201 else 4202 throw new Exception("ReadFileEx " ~ to!string(GetLastError())); 4203 } else { 4204 } 4205 4206 w.justRead(); 4207 } 4208 } else version(Posix) { 4209 void readyToRead(int fd) { 4210 import core.sys.posix.unistd; 4211 static ubyte[] buffer; 4212 if(buffer is null) 4213 buffer = new ubyte[](1024 * 32); 4214 4215 // the count is to limit how long we spend in this loop 4216 // when it runs out, it goes back to the main event loop 4217 // for a while (btw use level triggered events so the remaining 4218 // data continues to get processed!) giving a chance to redraw 4219 // and process user input periodically during insanely long and 4220 // rapid output. 4221 int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests 4222 4223 version(arsd_te_conservative_draws) 4224 cnt = 400; 4225 4226 // FIXME: if connected by ssh, up the count so we don't redraw as frequently. 4227 // it'd save bandwidth 4228 4229 while(--cnt) { 4230 auto len = read(fd, buffer.ptr, buffer.length); 4231 if(len < 0) { 4232 import core.stdc.errno; 4233 if(errno == EAGAIN || errno == EWOULDBLOCK) { 4234 break; // we got it all 4235 } else { 4236 //import std.conv; 4237 //throw new Exception("read failed " ~ to!string(errno)); 4238 return; 4239 } 4240 } 4241 4242 if(len == 0) { 4243 close(fd); 4244 requestExit(); 4245 break; 4246 } 4247 4248 auto data = buffer[0 .. len]; 4249 4250 if(debugMode) { 4251 import std.array; import std.stdio; writeln("GOT ", data, "\nOR ", 4252 replace(cast(string) data, "\033", "\\") 4253 .replace("\010", "^H") 4254 .replace("\r", "^M") 4255 .replace("\n", "^J") 4256 ); 4257 } 4258 super.sendRawInput(data); 4259 } 4260 4261 outputOccurred(); 4262 4263 redraw_(); 4264 4265 // HACK: I don't even know why this works, but with this 4266 // sleep in place, it gives X events from that socket a 4267 // chance to be processed. It can add a few seconds to a huge 4268 // output (like `find /usr`), but meh, that's worth it to me 4269 // to have a chance to ctrl+c. 4270 import core.thread; 4271 Thread.sleep(dur!"msecs"(5)); 4272 4273 justRead(); 4274 } 4275 } 4276 } 4277 4278 mixin template SdpyImageSupport() { 4279 class NonCharacterData_Image : NonCharacterData { 4280 Image data; 4281 int imageOffsetX; 4282 int imageOffsetY; 4283 4284 this(Image data, int x, int y) { 4285 this.data = data; 4286 this.imageOffsetX = x; 4287 this.imageOffsetY = y; 4288 } 4289 } 4290 4291 version(TerminalDirectToEmulator) 4292 class NonCharacterData_Widget : NonCharacterData { 4293 this(void* data, size_t idx, int width, int height) { 4294 this.window = cast(SimpleWindow) data; 4295 this.idx = idx; 4296 this.width = width; 4297 this.height = height; 4298 } 4299 4300 void position(int posx, int posy, int width, int height) { 4301 if(posx == this.posx && posy == this.posy && width == this.pixelWidth && height == this.pixelHeight) 4302 return; 4303 this.posx = posx; 4304 this.posy = posy; 4305 this.pixelWidth = width; 4306 this.pixelHeight = height; 4307 4308 window.moveResize(posx, posy, width, height); 4309 import std.stdio; writeln(posx, " ", posy, " ", width, " ", height); 4310 4311 auto painter = this.window.draw; 4312 painter.outlineColor = Color.red; 4313 painter.fillColor = Color.green; 4314 painter.drawRectangle(Point(0, 0), width, height); 4315 4316 4317 } 4318 4319 SimpleWindow window; 4320 size_t idx; 4321 int width; 4322 int height; 4323 4324 int posx; 4325 int posy; 4326 int pixelWidth; 4327 int pixelHeight; 4328 } 4329 4330 private struct CachedImage { 4331 ulong hash; 4332 BinaryDataTerminalRepresentation bui; 4333 int timesSeen; 4334 import core.time; 4335 MonoTime lastUsed; 4336 } 4337 private CachedImage[] imageCache; 4338 private CachedImage* findInCache(ulong hash) { 4339 if(hash == 0) 4340 return null; 4341 4342 /* 4343 import std.stdio; 4344 writeln("***"); 4345 foreach(cache; imageCache) { 4346 writeln(cache.hash, " ", cache.timesSeen, " ", cache.lastUsed); 4347 } 4348 */ 4349 4350 foreach(ref i; imageCache) 4351 if(i.hash == hash) { 4352 import core.time; 4353 i.lastUsed = MonoTime.currTime; 4354 i.timesSeen++; 4355 return &i; 4356 } 4357 return null; 4358 } 4359 private BinaryDataTerminalRepresentation addImageCache(ulong hash, BinaryDataTerminalRepresentation bui) { 4360 import core.time; 4361 if(imageCache.length == 0) 4362 imageCache.length = 8; 4363 4364 auto now = MonoTime.currTime; 4365 4366 size_t oldestIndex; 4367 MonoTime oldestTime = now; 4368 4369 size_t leastUsedIndex; 4370 int leastUsedCount = int.max; 4371 foreach(idx, ref cached; imageCache) { 4372 if(cached.hash == 0) { 4373 cached.hash = hash; 4374 cached.bui = bui; 4375 cached.timesSeen = 1; 4376 cached.lastUsed = now; 4377 4378 return bui; 4379 } else { 4380 if(cached.timesSeen < leastUsedCount) { 4381 leastUsedCount = cached.timesSeen; 4382 leastUsedIndex = idx; 4383 } 4384 if(cached.lastUsed < oldestTime) { 4385 oldestTime = cached.lastUsed; 4386 oldestIndex = idx; 4387 } 4388 } 4389 } 4390 4391 // need to overwrite one of the cached items, I'll just use the oldest one here 4392 // but maybe that could be smarter later 4393 4394 imageCache[oldestIndex].hash = hash; 4395 imageCache[oldestIndex].bui = bui; 4396 imageCache[oldestIndex].timesSeen = 1; 4397 imageCache[oldestIndex].lastUsed = now; 4398 4399 return bui; 4400 } 4401 4402 // It has a cache of the 8 most recently used items right now so if there's a loop of 9 you get pwned 4403 // but still the cache does an ok job at helping things while balancing out the big memory consumption it 4404 // could do if just left to grow and grow. i hope. 4405 protected override BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[] binaryData) { 4406 4407 version(none) { 4408 //version(TerminalDirectToEmulator) 4409 //if(binaryData.length == size_t.sizeof + 10) { 4410 //if((cast(uint[]) binaryData[0 .. 4])[0] == 0xdeadbeef && (cast(uint[]) binaryData[$-4 .. $])[0] == 0xabcdef32) { 4411 //auto widthInCharacterCells = binaryData[4]; 4412 //auto heightInCharacterCells = binaryData[5]; 4413 //auto pointer = (cast(void*[]) binaryData[6 .. $-4])[0]; 4414 4415 auto widthInCharacterCells = 30; 4416 auto heightInCharacterCells = 20; 4417 SimpleWindow pwin; 4418 foreach(k, v; SimpleWindow.nativeMapping) { 4419 if(v.type == WindowTypes.normal) 4420 pwin = v; 4421 } 4422 auto pointer = cast(void*) (new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin)); 4423 4424 BinaryDataTerminalRepresentation bi; 4425 bi.width = widthInCharacterCells; 4426 bi.height = heightInCharacterCells; 4427 bi.representation.length = bi.width * bi.height; 4428 4429 foreach(idx, ref cell; bi.representation) { 4430 cell.nonCharacterData = new NonCharacterData_Widget(pointer, idx, widthInCharacterCells, heightInCharacterCells); 4431 } 4432 4433 return bi; 4434 //} 4435 } 4436 4437 import std.digest.md; 4438 4439 ulong hash = * (cast(ulong*) md5Of(binaryData).ptr); 4440 4441 if(auto cached = findInCache(hash)) 4442 return cached.bui; 4443 4444 TrueColorImage mi; 4445 4446 if(binaryData.length > 8 && binaryData[1] == 'P' && binaryData[2] == 'N' && binaryData[3] == 'G') { 4447 import arsd.png; 4448 mi = imageFromPng(readPng(binaryData)).getAsTrueColorImage(); 4449 } else if(binaryData.length > 8 && binaryData[0] == 'B' && binaryData[1] == 'M') { 4450 import arsd.bmp; 4451 mi = readBmp(binaryData).getAsTrueColorImage(); 4452 } else if(binaryData.length > 2 && binaryData[0] == 0xff && binaryData[1] == 0xd8) { 4453 import arsd.jpeg; 4454 mi = readJpegFromMemory(binaryData).getAsTrueColorImage(); 4455 } else if(binaryData.length > 2 && binaryData[0] == '<') { 4456 import arsd.svg; 4457 NSVG* image = nsvgParse(cast(const(char)[]) binaryData); 4458 if(image is null) 4459 return BinaryDataTerminalRepresentation(); 4460 4461 int w = cast(int) image.width + 1; 4462 int h = cast(int) image.height + 1; 4463 NSVGrasterizer rast = nsvgCreateRasterizer(); 4464 mi = new TrueColorImage(w, h); 4465 rasterize(rast, image, 0, 0, 1, mi.imageData.bytes.ptr, w, h, w*4); 4466 image.kill(); 4467 } else { 4468 return BinaryDataTerminalRepresentation(); 4469 } 4470 4471 BinaryDataTerminalRepresentation bi; 4472 bi.width = mi.width / fontWidth + ((mi.width%fontWidth) ? 1 : 0); 4473 bi.height = mi.height / fontHeight + ((mi.height%fontHeight) ? 1 : 0); 4474 4475 bi.representation.length = bi.width * bi.height; 4476 4477 Image data = Image.fromMemoryImage(mi); 4478 4479 int ix, iy; 4480 foreach(ref cell; bi.representation) { 4481 /* 4482 Image data = new Image(fontWidth, fontHeight); 4483 foreach(y; 0 .. fontHeight) { 4484 foreach(x; 0 .. fontWidth) { 4485 if(x + ix >= mi.width || y + iy >= mi.height) { 4486 data.putPixel(x, y, defaultTextAttributes.background); 4487 continue; 4488 } 4489 data.putPixel(x, y, mi.imageData.colors[(iy + y) * mi.width + (ix + x)]); 4490 } 4491 } 4492 */ 4493 4494 cell.nonCharacterData = new NonCharacterData_Image(data, ix, iy); 4495 4496 ix += fontWidth; 4497 4498 if(ix >= mi.width) { 4499 ix = 0; 4500 iy += fontHeight; 4501 } 4502 } 4503 4504 return addImageCache(hash, bi); 4505 //return bi; 4506 } 4507 4508 } 4509 4510 // this assumes you have imported arsd.simpledisplay and/or arsd.minigui in the mixin scope 4511 mixin template SdpyDraw() { 4512 4513 // black bg, make the colors more visible 4514 static Color contrastify(Color c) { 4515 if(c == Color(0xcd, 0, 0)) 4516 return Color.fromHsl(0, 1.0, 0.75); 4517 else if(c == Color(0, 0, 0xcd)) 4518 return Color.fromHsl(240, 1.0, 0.75); 4519 else if(c == Color(229, 229, 229)) 4520 return Color(0x99, 0x99, 0x99); 4521 else if(c == Color.black) 4522 return Color(128, 128, 128); 4523 else return c; 4524 } 4525 4526 // white bg, make them more visible 4527 static Color antiContrastify(Color c) { 4528 if(c == Color(0xcd, 0xcd, 0)) 4529 return Color.fromHsl(60, 1.0, 0.25); 4530 else if(c == Color(0, 0xcd, 0xcd)) 4531 return Color.fromHsl(180, 1.0, 0.25); 4532 else if(c == Color(229, 229, 229)) 4533 return Color(0x99, 0x99, 0x99); 4534 else if(c == Color.white) 4535 return Color(128, 128, 128); 4536 else return c; 4537 } 4538 4539 struct SRectangle { 4540 int left; 4541 int top; 4542 int right; 4543 int bottom; 4544 } 4545 4546 mixin SdpyImageSupport; 4547 4548 OperatingSystemFont font; 4549 int fontWidth; 4550 int fontHeight; 4551 4552 enum paddingLeft = 2; 4553 enum paddingTop = 1; 4554 4555 void loadDefaultFont(int size = 14) { 4556 static if(UsingSimpledisplayX11) { 4557 font = new OperatingSystemFont("core:fixed", size, FontWeight.medium); 4558 //font = new OperatingSystemFont("monospace", size, FontWeight.medium); 4559 if(font.isNull) { 4560 // didn't work, it is using a 4561 // fallback, prolly fixed-13 is best 4562 font = new OperatingSystemFont("core:fixed", 13, FontWeight.medium); 4563 } 4564 } else version(Windows) { 4565 this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium); 4566 if(!this.font.isNull && !this.font.isMonospace) 4567 this.font.unload(); // non-monospace fonts are unusable here. This should never happen anyway though as Courier New comes with Windows 4568 } else version(OSX) { 4569 this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium); 4570 if(!this.font.isNull && !this.font.isMonospace) 4571 throw new Exception("non monospace"); 4572 //this.font.unload(); 4573 } 4574 4575 if(font.isNull) { 4576 // no way to really tell... just guess so it doesn't crash but like eeek. 4577 fontWidth = size / 2; 4578 fontHeight = size; 4579 } else { 4580 fontWidth = cast(int) font.averageWidth; 4581 fontHeight = cast(int) font.height; 4582 // import std.stdio; writeln(fontWidth, " x ", fontHeight); 4583 } 4584 } 4585 4586 bool lastDrawAlternativeScreen; 4587 final SRectangle redrawPainter(T)(T painter, bool forceRedraw) { 4588 SRectangle invalidated; 4589 4590 // FIXME: anything we can do to make this faster is good 4591 // on both, the XImagePainter could use optimizations 4592 // on both, drawing blocks would probably be good too - not just one cell at a time, find whole blocks of stuff 4593 // on both it might also be good to keep scroll commands high level somehow. idk. 4594 4595 // FIXME on Windows it would definitely help a lot to do just one ExtTextOutW per line, if possible. the current code is brutally slow 4596 4597 // Or also see https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-polytextoutw 4598 4599 static if(is(T == WidgetPainter) || is(T == ScreenPainter)) { 4600 if(font) 4601 painter.setFont(font); 4602 } 4603 4604 4605 int posx = paddingLeft; 4606 int posy = paddingTop; 4607 4608 4609 char[512] bufferText; 4610 bool hasBufferedInfo; 4611 int bufferTextLength; 4612 Color bufferForeground; 4613 Color bufferBackground; 4614 int bufferX = -1; 4615 int bufferY = -1; 4616 bool bufferReverse; 4617 void flushBuffer() { 4618 if(!hasBufferedInfo) { 4619 return; 4620 } 4621 4622 assert(posx - bufferX - 1 > 0); 4623 4624 painter.fillColor = bufferReverse ? bufferForeground : bufferBackground; 4625 painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground; 4626 4627 painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight); 4628 painter.fillColor = Color.transparent; 4629 // Hack for contrast! 4630 if(bufferBackground == Color.black && !bufferReverse) { 4631 // brighter than normal in some cases so i can read it easily 4632 painter.outlineColor = contrastify(bufferForeground); 4633 } else if(bufferBackground == Color.white && !bufferReverse) { 4634 // darker than normal so i can read it 4635 painter.outlineColor = antiContrastify(bufferForeground); 4636 } else if(bufferForeground == bufferBackground) { 4637 // color on itself, I want it visible too 4638 auto hsl = toHsl(bufferForeground, true); 4639 if(hsl[0] == 240) { 4640 // blue is a bit special, it generally looks darker 4641 // so we want to get very bright or very dark 4642 if(hsl[2] < 0.7) 4643 hsl[2] = 0.9; 4644 else 4645 hsl[2] = 0.1; 4646 } else { 4647 if(hsl[2] < 0.5) 4648 hsl[2] += 0.5; 4649 else 4650 hsl[2] -= 0.5; 4651 } 4652 painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]); 4653 } else { 4654 auto drawColor = bufferReverse ? bufferBackground : bufferForeground; 4655 ///+ 4656 // try to ensure legible contrast with any arbitrary combination 4657 auto bgColor = bufferReverse ? bufferForeground : bufferBackground; 4658 auto fghsl = toHsl(drawColor, true); 4659 auto bghsl = toHsl(bgColor, true); 4660 4661 if(fghsl[2] > 0.5 && bghsl[2] > 0.5) { 4662 // bright color on bright background 4663 painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2); 4664 } else if(fghsl[2] < 0.5 && bghsl[2] < 0.5) { 4665 // dark color on dark background 4666 if(fghsl[0] == 240 && bghsl[0] >= 60 && bghsl[0] <= 180) 4667 // blue on green looks dark to the algorithm but isn't really 4668 painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2); 4669 else 4670 painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.8); 4671 } else { 4672 // normal 4673 painter.outlineColor = drawColor; 4674 } 4675 //+/ 4676 4677 // normal 4678 //painter.outlineColor = drawColor; 4679 } 4680 4681 // FIXME: make sure this clips correctly 4682 painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]); 4683 4684 // import std.stdio; writeln(bufferX, " ", bufferY); 4685 4686 hasBufferedInfo = false; 4687 4688 bufferReverse = false; 4689 bufferTextLength = 0; 4690 bufferX = -1; 4691 bufferY = -1; 4692 } 4693 4694 4695 4696 int x; 4697 auto bfr = alternateScreenActive ? alternateScreen : normalScreen; 4698 4699 version(invalidator_2) { 4700 if(invalidatedMax > bfr.length) 4701 invalidatedMax = cast(int) bfr.length; 4702 if(invalidatedMin > invalidatedMax) 4703 invalidatedMin = invalidatedMax; 4704 if(invalidatedMin >= 0) 4705 bfr = bfr[invalidatedMin .. invalidatedMax]; 4706 4707 posx += (invalidatedMin % screenWidth) * fontWidth; 4708 posy += (invalidatedMin / screenWidth) * fontHeight; 4709 4710 //import std.stdio; writeln(invalidatedMin, " to ", invalidatedMax, " ", posx, "x", posy); 4711 invalidated.left = posx; 4712 invalidated.top = posy; 4713 invalidated.right = posx; 4714 invalidated.top = posy; 4715 4716 clearInvalidatedRange(); 4717 } 4718 4719 foreach(idx, ref cell; bfr) { 4720 if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) { 4721 flushBuffer(); 4722 goto skipDrawing; 4723 } 4724 cell.invalidated = false; 4725 version(none) if(bufferX == -1) { // why was this ever here? 4726 bufferX = posx; 4727 bufferY = posy; 4728 } 4729 4730 if(!cell.hasNonCharacterData) { 4731 4732 invalidated.left = posx < invalidated.left ? posx : invalidated.left; 4733 invalidated.top = posy < invalidated.top ? posy : invalidated.top; 4734 int xmax = posx + fontWidth; 4735 int ymax = posy + fontHeight; 4736 invalidated.right = xmax > invalidated.right ? xmax : invalidated.right; 4737 invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom; 4738 4739 // FIXME: this could be more efficient, simpledisplay could get better graphics context handling 4740 { 4741 4742 bool reverse = (cell.attributes.inverse != reverseVideo); 4743 if(cell.selected) 4744 reverse = !reverse; 4745 4746 version(with_24_bit_color) { 4747 auto fgc = cell.attributes.foreground; 4748 auto bgc = cell.attributes.background; 4749 4750 if(!(cell.attributes.foregroundIndex & 0xff00)) { 4751 // this refers to a specific palette entry, which may change, so we should use that 4752 fgc = palette[cell.attributes.foregroundIndex]; 4753 } 4754 if(!(cell.attributes.backgroundIndex & 0xff00)) { 4755 // this refers to a specific palette entry, which may change, so we should use that 4756 bgc = palette[cell.attributes.backgroundIndex]; 4757 } 4758 4759 } else { 4760 auto fgc = cell.attributes.foregroundIndex == 256 ? defaultForeground : palette[cell.attributes.foregroundIndex & 0xff]; 4761 auto bgc = cell.attributes.backgroundIndex == 256 ? defaultBackground : palette[cell.attributes.backgroundIndex & 0xff]; 4762 } 4763 4764 if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse) 4765 flushBuffer(); 4766 bufferReverse = reverse; 4767 bufferBackground = bgc; 4768 bufferForeground = fgc; 4769 } 4770 } 4771 4772 if(!cell.hasNonCharacterData) { 4773 char[4] str; 4774 import std.utf; 4775 // now that it is buffered, we do want to draw it this way... 4776 //if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing 4777 try { 4778 auto stride = encode(str, cell.ch); 4779 if(bufferTextLength + stride > bufferText.length) 4780 flushBuffer(); 4781 bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride]; 4782 bufferTextLength += stride; 4783 4784 if(bufferX == -1) { 4785 bufferX = posx; 4786 bufferY = posy; 4787 } 4788 hasBufferedInfo = true; 4789 } catch(Exception e) { 4790 // import std.stdio; writeln(cast(uint) cell.ch, " :: ", e.msg); 4791 } 4792 //} 4793 } else if(cell.nonCharacterData !is null) { 4794 //import std.stdio; writeln(cast(void*) cell.nonCharacterData); 4795 if(auto ncdi = cast(NonCharacterData_Image) cell.nonCharacterData) { 4796 flushBuffer(); 4797 painter.outlineColor = defaultBackground; 4798 painter.fillColor = defaultBackground; 4799 painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight); 4800 painter.drawImage(Point(posx, posy), ncdi.data, Point(ncdi.imageOffsetX, ncdi.imageOffsetY), fontWidth, fontHeight); 4801 } 4802 version(TerminalDirectToEmulator) 4803 if(auto wdi = cast(NonCharacterData_Widget) cell.nonCharacterData) { 4804 flushBuffer(); 4805 if(wdi.idx == 0) { 4806 wdi.position(posx, posy, fontWidth * wdi.width, fontHeight * wdi.height); 4807 /* 4808 painter.outlineColor = defaultBackground; 4809 painter.fillColor = defaultBackground; 4810 painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight); 4811 */ 4812 } 4813 4814 } 4815 } 4816 4817 if(!cell.hasNonCharacterData) 4818 if(cell.attributes.underlined) { 4819 // the posx adjustment is because the buffer assumes it is going 4820 // to be flushed after advancing, but here, we're doing it mid-character 4821 // FIXME: we should just underline the whole thing consecutively, with the buffer 4822 posx += fontWidth; 4823 flushBuffer(); 4824 posx -= fontWidth; 4825 painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1)); 4826 } 4827 skipDrawing: 4828 4829 posx += fontWidth; 4830 x++; 4831 if(x == screenWidth) { 4832 flushBuffer(); 4833 x = 0; 4834 posy += fontHeight; 4835 posx = paddingLeft; 4836 } 4837 } 4838 4839 flushBuffer(); 4840 4841 if(cursorShowing) { 4842 painter.fillColor = cursorColor; 4843 painter.outlineColor = cursorColor; 4844 painter.rasterOp = RasterOp.xor; 4845 4846 posx = cursorPosition.x * fontWidth + paddingLeft; 4847 posy = cursorPosition.y * fontHeight + paddingTop; 4848 4849 int cursorWidth = fontWidth; 4850 int cursorHeight = fontHeight; 4851 4852 final switch(cursorStyle) { 4853 case CursorStyle.block: 4854 painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight); 4855 break; 4856 case CursorStyle.underline: 4857 painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2); 4858 break; 4859 case CursorStyle.bar: 4860 painter.drawRectangle(Point(posx, posy), 2, cursorHeight); 4861 break; 4862 } 4863 painter.rasterOp = RasterOp.normal; 4864 4865 painter.notifyCursorPosition(posx, posy, cursorWidth, cursorHeight); 4866 4867 // since the cursor draws over the cell, we need to make sure it is redrawn each time too 4868 auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen); 4869 if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) { 4870 (*buffer)[cursorY * screenWidth + cursorX].invalidated = true; 4871 } 4872 4873 extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY); 4874 4875 invalidated.left = posx < invalidated.left ? posx : invalidated.left; 4876 invalidated.top = posy < invalidated.top ? posy : invalidated.top; 4877 int xmax = posx + fontWidth; 4878 int ymax = xmax + fontHeight; 4879 invalidated.right = xmax > invalidated.right ? xmax : invalidated.right; 4880 invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom; 4881 } 4882 4883 lastDrawAlternativeScreen = alternateScreenActive; 4884 4885 return invalidated; 4886 } 4887 } 4888 4889 string encodeSmallTextImage(IndexedImage ii) { 4890 char encodeNumeric(int c) { 4891 if(c < 10) 4892 return cast(char)(c + '0'); 4893 if(c < 10 + 26) 4894 return cast(char)(c - 10 + 'a'); 4895 assert(0); 4896 } 4897 4898 string s; 4899 s ~= encodeNumeric(ii.width); 4900 s ~= encodeNumeric(ii.height); 4901 4902 foreach(entry; ii.palette) 4903 s ~= entry.toRgbaHexString(); 4904 s ~= "Z"; 4905 4906 ubyte rleByte; 4907 int rleCount; 4908 4909 void rleCommit() { 4910 if(rleByte >= 26) 4911 assert(0); // too many colors for us to handle 4912 if(rleCount == 0) 4913 goto finish; 4914 if(rleCount == 1) { 4915 s ~= rleByte + 'a'; 4916 goto finish; 4917 } 4918 4919 import std.conv; 4920 s ~= to!string(rleCount); 4921 s ~= rleByte + 'a'; 4922 4923 finish: 4924 rleByte = 0; 4925 rleCount = 0; 4926 } 4927 4928 foreach(b; ii.data) { 4929 if(b == rleByte) 4930 rleCount++; 4931 else { 4932 rleCommit(); 4933 rleByte = b; 4934 rleCount = 1; 4935 } 4936 } 4937 4938 rleCommit(); 4939 4940 return s; 4941 } 4942 4943 IndexedImage readSmallTextImage(scope const(char)[] arg) { 4944 auto origArg = arg; 4945 int width; 4946 int height; 4947 4948 int readNumeric(char c) { 4949 if(c >= '0' && c <= '9') 4950 return c - '0'; 4951 if(c >= 'a' && c <= 'z') 4952 return c - 'a' + 10; 4953 return 0; 4954 } 4955 4956 if(arg.length > 2) { 4957 width = readNumeric(arg[0]); 4958 height = readNumeric(arg[1]); 4959 arg = arg[2 .. $]; 4960 } 4961 4962 import std.conv; 4963 assert(width == 16, to!string(width)); 4964 assert(height == 16, to!string(width)); 4965 4966 Color[] palette; 4967 ubyte[256] data; 4968 int didx = 0; 4969 bool readingPalette = true; 4970 outer: while(arg.length) { 4971 if(readingPalette) { 4972 if(arg[0] == 'Z') { 4973 readingPalette = false; 4974 arg = arg[1 .. $]; 4975 continue; 4976 } 4977 if(arg.length < 8) 4978 break; 4979 foreach(a; arg[0..8]) { 4980 // if not strict hex, forget it 4981 if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z'))) 4982 break outer; 4983 } 4984 palette ~= Color.fromString(arg[0 .. 8]); 4985 arg = arg[8 .. $]; 4986 } else { 4987 char[3] rleChars; 4988 int rlePos; 4989 while(arg.length && arg[0] >= '0' && arg[0] <= '9') { 4990 rleChars[rlePos] = arg[0]; 4991 arg = arg[1 .. $]; 4992 rlePos++; 4993 if(rlePos >= rleChars.length) 4994 break; 4995 } 4996 if(arg.length == 0) 4997 break; 4998 4999 int rle; 5000 if(rlePos == 0) 5001 rle = 1; 5002 else { 5003 // 100 5004 // rleChars[0] == '1' 5005 foreach(c; rleChars[0 .. rlePos]) { 5006 rle *= 10; 5007 rle += c - '0'; 5008 } 5009 } 5010 5011 foreach(i; 0 .. rle) { 5012 if(arg[0] >= 'a' && arg[0] <= 'z') 5013 data[didx] = cast(ubyte)(arg[0] - 'a'); 5014 5015 didx++; 5016 if(didx == data.length) 5017 break outer; 5018 } 5019 5020 arg = arg[1 .. $]; 5021 } 5022 } 5023 5024 // width, height, palette, data is set up now 5025 5026 if(palette.length) { 5027 auto ii = new IndexedImage(width, height); 5028 ii.palette = palette; 5029 ii.data = data.dup; 5030 5031 return ii; 5032 }// else assert(0, origArg); 5033 return null; 5034 } 5035 5036 5037 // workaround dmd bug fixed in next release 5038 //static immutable Color[256] xtermPalette = [ 5039 immutable(Color)[] xtermPalette() { 5040 5041 // This is an approximation too for a few entries, but a very close one. 5042 Color xtermPaletteIndexToColor(int paletteIdx) { 5043 Color color; 5044 color.a = 255; 5045 5046 if(paletteIdx < 16) { 5047 if(paletteIdx == 7) 5048 return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see 5049 else if(paletteIdx == 8) 5050 return Color(0x80, 0x80, 0x80); 5051 5052 // real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see 5053 color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00; 5054 color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00; 5055 color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00; 5056 5057 } else if(paletteIdx < 232) { 5058 // color ramp, 6x6x6 cube 5059 color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); 5060 color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); 5061 color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); 5062 5063 if(color.r == 55) color.r = 0; 5064 if(color.g == 55) color.g = 0; 5065 if(color.b == 55) color.b = 0; 5066 } else { 5067 // greyscale ramp, from 0x8 to 0xee 5068 color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); 5069 color.g = color.r; 5070 color.b = color.g; 5071 } 5072 5073 return color; 5074 } 5075 5076 static immutable(Color)[] ret; 5077 if(ret.length == 256) 5078 return ret; 5079 5080 ret.reserve(256); 5081 foreach(i; 0 .. 256) 5082 ret ~= xtermPaletteIndexToColor(i); 5083 5084 return ret; 5085 } 5086 5087 static shared immutable dchar[dchar] lineDrawingCharacterSet; 5088 shared static this() { 5089 lineDrawingCharacterSet = [ 5090 'a' : ':', 5091 'j' : '+', 5092 'k' : '+', 5093 'l' : '+', 5094 'm' : '+', 5095 'n' : '+', 5096 'q' : '-', 5097 't' : '+', 5098 'u' : '+', 5099 'v' : '+', 5100 'w' : '+', 5101 'x' : '|', 5102 ]; 5103 5104 // this is what they SHOULD be but the font i use doesn't support all these 5105 // the ascii fallback above looks pretty good anyway though. 5106 version(none) 5107 lineDrawingCharacterSet = [ 5108 'a' : '\u2592', 5109 'j' : '\u2518', 5110 'k' : '\u2510', 5111 'l' : '\u250c', 5112 'm' : '\u2514', 5113 'n' : '\u253c', 5114 'q' : '\u2500', 5115 't' : '\u251c', 5116 'u' : '\u2524', 5117 'v' : '\u2534', 5118 'w' : '\u252c', 5119 'x' : '\u2502', 5120 ]; 5121 } 5122 5123 /+ 5124 Copyright: Adam D. Ruppe, 2013 - 2020 5125 License: [http://www.boost.org/LICENSE_1_0.txt|Boost Software License 1.0] 5126 Authors: Adam D. Ruppe 5127 +/