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