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