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 'f': // wikipedia says it is the same except it is a format func instead of editor func. idk what the diff is 2734 case 'H': 2735 auto got = getArgs(1, 1); 2736 cursorX = got[1] - 1; 2737 2738 if(got[0] - 1 == cursorY) 2739 setTentativeScrollback(cursorX); 2740 else 2741 setTentativeScrollback(0); 2742 2743 cursorY = got[0] - 1; 2744 newLineOnNext = false; 2745 break; 2746 case 'L': 2747 // insert lines 2748 scrollDown(getArgs(1)[0]); 2749 break; 2750 case 'M': 2751 // delete lines 2752 if(cursorY + 1 < screenHeight) { 2753 TerminalCell plain; 2754 plain.ch = ' '; 2755 plain.attributes = defaultTextAttributes(); 2756 foreach(i; 0 .. getArgs(1)[0]) { 2757 foreach(y; cursorY .. scrollZoneBottom) 2758 foreach(x; 0 .. screenWidth) { 2759 ASS[y][x] = ASS[y + 1][x]; 2760 ASS[y][x].invalidated = true; 2761 } 2762 foreach(x; 0 .. screenWidth) { 2763 ASS[scrollZoneBottom][x] = plain; 2764 } 2765 } 2766 2767 extendInvalidatedRange(); 2768 } 2769 break; 2770 case 'K': 2771 auto arg = getArgs(0)[0]; 2772 int start, end; 2773 if(arg == 0) { 2774 // clear from cursor to end of line 2775 start = cursorX; 2776 end = this.screenWidth; 2777 } else if(arg == 1) { 2778 // clear from cursor to beginning of line 2779 start = 0; 2780 end = cursorX + 1; 2781 } else if(arg == 2) { 2782 // clear entire line 2783 start = 0; 2784 end = this.screenWidth; 2785 } 2786 2787 TerminalCell plain; 2788 plain.ch = ' '; 2789 plain.attributes = currentAttributes; 2790 2791 for(int i = start; i < end; i++) { 2792 if(ASS[cursorY][i].selected) 2793 clearSelection(); 2794 ASS[cursorY] 2795 [i] = plain; 2796 } 2797 break; 2798 case 's': 2799 pushSavedCursor(cursorPosition); 2800 break; 2801 case 'u': 2802 cursorPosition = popSavedCursor(); 2803 break; 2804 case 'g': 2805 auto arg = getArgs(0)[0]; 2806 TerminalCell plain; 2807 plain.ch = ' '; 2808 plain.attributes = currentAttributes; 2809 if(arg == 0) { 2810 // clear current column 2811 for(int i = 0; i < this.screenHeight; i++) 2812 ASS[i] 2813 [cursorY] = plain; 2814 } else if(arg == 3) { 2815 // clear all 2816 cls(); 2817 } 2818 break; 2819 case 'q': 2820 // xterm also does blinks on the odd numbers (x-1) 2821 if(esc == "[0 q") 2822 cursorStyle = CursorStyle.block; // FIXME: restore default 2823 if(esc == "[2 q") 2824 cursorStyle = CursorStyle.block; 2825 else if(esc == "[4 q") 2826 cursorStyle = CursorStyle.underline; 2827 else if(esc == "[6 q") 2828 cursorStyle = CursorStyle.bar; 2829 2830 changeCursorStyle(cursorStyle); 2831 break; 2832 case 't': 2833 // window commands 2834 // i might support more of these but for now i just want the stack stuff. 2835 2836 auto args = getArgs(0, 0); 2837 if(args[0] == 22) { 2838 // save window title to stack 2839 // xterm says args[1] should tell if it is the window title, the icon title, or both, but meh 2840 titleStack ~= windowTitle; 2841 iconStack ~= windowIcon; 2842 } else if(args[0] == 23) { 2843 // restore from stack 2844 if(titleStack.length) { 2845 windowTitle = titleStack[$ - 1]; 2846 changeWindowTitle(titleStack[$ - 1]); 2847 titleStack = titleStack[0 .. $ - 1]; 2848 } 2849 2850 if(iconStack.length) { 2851 windowIcon = iconStack[$ - 1]; 2852 changeWindowIcon(iconStack[$ - 1]); 2853 iconStack = iconStack[0 .. $ - 1]; 2854 } 2855 } 2856 break; 2857 case 'm': 2858 // FIXME used by xterm to decide whether to construct 2859 // CSI > Pp ; Pv m CSI > Pp m Set/reset key modifier options, xterm. 2860 if(esc[1] == '>') 2861 goto default; 2862 // done 2863 argsLoop: foreach(argIdx, arg; getArgs(0)) 2864 switch(arg) { 2865 case 0: 2866 // normal 2867 currentAttributes = defaultTextAttributes; 2868 break; 2869 case 1: 2870 currentAttributes.bold = true; 2871 break; 2872 case 2: 2873 currentAttributes.faint = true; 2874 break; 2875 case 3: 2876 currentAttributes.italic = true; 2877 break; 2878 case 4: 2879 currentAttributes.underlined = true; 2880 break; 2881 case 5: 2882 currentAttributes.blink = true; 2883 break; 2884 case 6: 2885 // rapid blink, treating the same as regular blink 2886 currentAttributes.blink = true; 2887 break; 2888 case 7: 2889 currentAttributes.inverse = true; 2890 break; 2891 case 8: 2892 currentAttributes.invisible = true; 2893 break; 2894 case 9: 2895 currentAttributes.strikeout = true; 2896 break; 2897 case 10: 2898 // primary font 2899 break; 2900 case 11: .. case 19: 2901 // alternate fonts 2902 break; 2903 case 20: 2904 // Fraktur font 2905 break; 2906 case 21: 2907 // bold off and doubled underlined 2908 break; 2909 case 22: 2910 currentAttributes.bold = false; 2911 currentAttributes.faint = false; 2912 break; 2913 case 23: 2914 currentAttributes.italic = false; 2915 break; 2916 case 24: 2917 currentAttributes.underlined = false; 2918 break; 2919 case 25: 2920 currentAttributes.blink = false; 2921 break; 2922 case 26: 2923 // reserved 2924 break; 2925 case 27: 2926 currentAttributes.inverse = false; 2927 break; 2928 case 28: 2929 currentAttributes.invisible = false; 2930 break; 2931 case 29: 2932 currentAttributes.strikeout = false; 2933 break; 2934 case 30: 2935 .. 2936 case 37: 2937 // set foreground color 2938 /* 2939 Color nc; 2940 ubyte multiplier = currentAttributes.bold ? 255 : 127; 2941 nc.r = cast(ubyte)((arg - 30) & 1) * multiplier; 2942 nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier; 2943 nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier; 2944 nc.a = 255; 2945 */ 2946 currentAttributes.foregroundIndex = cast(ubyte)(arg - 30); 2947 version(with_24_bit_color) 2948 currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)]; 2949 break; 2950 case 38: 2951 // xterm 256 color set foreground color 2952 auto args = getArgs()[argIdx + 1 .. $]; 2953 if(args.length > 3 && args[0] == 2) { 2954 // set color to closest match in palette. but since we have full support, we'll just take it directly 2955 auto fg = Color(args[1], args[2], args[3]); 2956 version(with_24_bit_color) 2957 currentAttributes.foreground = fg; 2958 // and try to find a low default palette entry for maximum compatibility 2959 // 0x8000 == approximation 2960 currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg); 2961 } else if(args.length > 1 && args[0] == 5) { 2962 // set to palette index 2963 version(with_24_bit_color) 2964 currentAttributes.foreground = palette[args[1]]; 2965 currentAttributes.foregroundIndex = cast(ushort) args[1]; 2966 } 2967 break argsLoop; 2968 case 39: 2969 // default foreground color 2970 auto dflt = defaultTextAttributes(); 2971 2972 version(with_24_bit_color) 2973 currentAttributes.foreground = dflt.foreground; 2974 currentAttributes.foregroundIndex = dflt.foregroundIndex; 2975 break; 2976 case 40: 2977 .. 2978 case 47: 2979 // set background color 2980 /* 2981 Color nc; 2982 nc.r = cast(ubyte)((arg - 40) & 1) * 255; 2983 nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255; 2984 nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255; 2985 nc.a = 255; 2986 */ 2987 2988 currentAttributes.backgroundIndex = cast(ubyte)(arg - 40); 2989 //currentAttributes.background = nc; 2990 version(with_24_bit_color) 2991 currentAttributes.background = palette[arg-40]; 2992 break; 2993 case 48: 2994 // xterm 256 color set background color 2995 auto args = getArgs()[argIdx + 1 .. $]; 2996 if(args.length > 3 && args[0] == 2) { 2997 // set color to closest match in palette. but since we have full support, we'll just take it directly 2998 auto bg = Color(args[1], args[2], args[3]); 2999 version(with_24_bit_color) 3000 currentAttributes.background = Color(args[1], args[2], args[3]); 3001 3002 // and try to find a low default palette entry for maximum compatibility 3003 // 0x8000 == this is an approximation 3004 currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg); 3005 } else if(args.length > 1 && args[0] == 5) { 3006 // set to palette index 3007 version(with_24_bit_color) 3008 currentAttributes.background = palette[args[1]]; 3009 currentAttributes.backgroundIndex = cast(ushort) args[1]; 3010 } 3011 3012 break argsLoop; 3013 case 49: 3014 // default background color 3015 auto dflt = defaultTextAttributes(); 3016 3017 version(with_24_bit_color) 3018 currentAttributes.background = dflt.background; 3019 currentAttributes.backgroundIndex = dflt.backgroundIndex; 3020 break; 3021 case 51: 3022 // framed 3023 break; 3024 case 52: 3025 // encircled 3026 break; 3027 case 53: 3028 // overlined 3029 break; 3030 case 54: 3031 // not framed or encircled 3032 break; 3033 case 55: 3034 // not overlined 3035 break; 3036 case 90: .. case 97: 3037 // high intensity foreground color 3038 break; 3039 case 100: .. case 107: 3040 // high intensity background color 3041 break; 3042 default: 3043 unknownEscapeSequence(cast(string) esc); 3044 } 3045 break; 3046 case 'J': 3047 // erase in display 3048 auto arg = getArgs(0)[0]; 3049 switch(arg) { 3050 case 0: 3051 TerminalCell plain; 3052 plain.ch = ' '; 3053 plain.attributes = currentAttributes; 3054 // erase below 3055 foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) { 3056 if(alternateScreenActive) 3057 alternateScreen[i] = plain; 3058 else 3059 normalScreen[i] = plain; 3060 } 3061 break; 3062 case 1: 3063 // erase above 3064 unknownEscapeSequence("FIXME"); 3065 break; 3066 case 2: 3067 // erase all 3068 cls(); 3069 break; 3070 default: unknownEscapeSequence(cast(string) esc); 3071 } 3072 break; 3073 case 'r': 3074 if(esc[1] != '?') { 3075 // set scrolling zone 3076 // default should be full size of window 3077 auto args = getArgs(1, screenHeight); 3078 3079 // FIXME: these are supposed to be per-buffer 3080 scrollZoneTop = args[0] - 1; 3081 scrollZoneBottom = args[1] - 1; 3082 3083 if(scrollZoneTop < 0) 3084 scrollZoneTop = 0; 3085 if(scrollZoneBottom > screenHeight) 3086 scrollZoneBottom = screenHeight - 1; 3087 } else { 3088 // restore... something FIXME 3089 } 3090 break; 3091 case 'h': 3092 if(esc[1] != '?') 3093 foreach(arg; getArgs()) 3094 switch(arg) { 3095 case 4: 3096 insertMode = true; 3097 break; 3098 case 34: 3099 // no idea. vim inside screen sends it 3100 break; 3101 default: unknownEscapeSequence(cast(string) esc); 3102 } 3103 else 3104 //import std.stdio; writeln("h magic ", cast(string) esc); 3105 foreach(arg; getArgsBase(2, null)) { 3106 if(arg > 65535) { 3107 /* Extensions */ 3108 if(arg < 65536 + 65535) { 3109 // activate hyperlink 3110 hyperlinkFlipper = !hyperlinkFlipper; 3111 hyperlinkActive = true; 3112 hyperlinkNumber = arg - 65536; 3113 } 3114 } else 3115 switch(arg) { 3116 case 1: 3117 // application cursor keys 3118 applicationCursorKeys = true; 3119 break; 3120 case 3: 3121 // 132 column mode 3122 break; 3123 case 4: 3124 // smooth scroll 3125 break; 3126 case 5: 3127 // reverse video 3128 reverseVideo = true; 3129 break; 3130 case 6: 3131 // origin mode 3132 break; 3133 case 7: 3134 // wraparound mode 3135 wraparoundMode = false; 3136 // 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 3137 break; 3138 case 9: 3139 allMouseTrackingOff(); 3140 mouseButtonTracking = true; 3141 break; 3142 case 12: 3143 // start blinking cursor 3144 break; 3145 case 1034: 3146 // meta keys???? 3147 break; 3148 case 1049: 3149 // Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first. 3150 alternateScreenActive = true; 3151 scrollLock = false; 3152 pushSavedCursor(cursorPosition); 3153 cls(); 3154 notifyScrollbarRelevant(false, false); 3155 break; 3156 case 1000: 3157 // send mouse X&Y on button press and release 3158 allMouseTrackingOff(); 3159 mouseButtonTracking = true; 3160 mouseButtonReleaseTracking = true; 3161 break; 3162 case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it 3163 break; 3164 case 1002: 3165 allMouseTrackingOff(); 3166 mouseButtonTracking = true; 3167 mouseButtonReleaseTracking = true; 3168 mouseButtonMotionTracking = true; 3169 // use cell motion mouse tracking 3170 break; 3171 case 1003: 3172 // ALL motion is sent 3173 allMouseTrackingOff(); 3174 mouseButtonTracking = true; 3175 mouseButtonReleaseTracking = true; 3176 mouseMotionTracking = true; 3177 break; 3178 case 1004: 3179 sendFocusEvents = true; 3180 break; 3181 case 1005: 3182 utf8MouseMode = true; 3183 // enable utf-8 mouse mode 3184 /* 3185 UTF-8 (1005) 3186 This enables UTF-8 encoding for Cx and Cy under all tracking 3187 modes, expanding the maximum encodable position from 223 to 3188 2015. For positions less than 95, the resulting output is 3189 identical under both modes. Under extended mouse mode, posi- 3190 tions greater than 95 generate "extra" bytes which will con- 3191 fuse applications which do not treat their input as a UTF-8 3192 stream. Likewise, Cb will be UTF-8 encoded, to reduce confu- 3193 sion with wheel mouse events. 3194 Under normal mouse mode, positions outside (160,94) result in 3195 byte pairs which can be interpreted as a single UTF-8 charac- 3196 ter; applications which do treat their input as UTF-8 will 3197 almost certainly be confused unless extended mouse mode is 3198 active. 3199 This scheme has the drawback that the encoded coordinates will 3200 not pass through luit unchanged, e.g., for locales using non- 3201 UTF-8 encoding. 3202 */ 3203 break; 3204 case 1006: 3205 /* 3206 SGR (1006) 3207 The normal mouse response is altered to use CSI < followed by 3208 semicolon-separated encoded button value, the Cx and Cy ordi- 3209 nates and a final character which is M for button press and m 3210 for button release. 3211 o The encoded button value in this case does not add 32 since 3212 that was useful only in the X10 scheme for ensuring that the 3213 byte containing the button value is a printable code. 3214 o The modifiers are encoded in the same way. 3215 o A different final character is used for button release to 3216 resolve the X10 ambiguity regarding which button was 3217 released. 3218 The highlight tracking responses are also modified to an SGR- 3219 like format, using the same SGR-style scheme and button-encod- 3220 ings. 3221 */ 3222 break; 3223 case 1014: 3224 // ARSD extension: it is 1002 but selective, only 3225 // on top row, row with cursor, or else if middle click/wheel. 3226 // 3227 // Quite specifically made for my getline function! 3228 allMouseTrackingOff(); 3229 3230 mouseButtonMotionTracking = true; 3231 mouseButtonTracking = true; 3232 mouseButtonReleaseTracking = true; 3233 selectiveMouseTracking = true; 3234 break; 3235 case 1015: 3236 /* 3237 URXVT (1015) 3238 The normal mouse response is altered to use CSI followed by 3239 semicolon-separated encoded button value, the Cx and Cy ordi- 3240 nates and final character M . 3241 This uses the same button encoding as X10, but printing it as 3242 a decimal integer rather than as a single byte. 3243 However, CSI M can be mistaken for DL (delete lines), while 3244 the highlight tracking CSI T can be mistaken for SD (scroll 3245 down), and the Window manipulation controls. For these rea- 3246 sons, the 1015 control is not recommended; it is not an 3247 improvement over 1005. 3248 */ 3249 break; 3250 case 1048: 3251 pushSavedCursor(cursorPosition); 3252 break; 3253 case 2004: 3254 bracketedPasteMode = true; 3255 break; 3256 case 3004: 3257 bracketedHyperlinkMode = true; 3258 break; 3259 case 1047: 3260 case 47: 3261 alternateScreenActive = true; 3262 scrollLock = false; 3263 cls(); 3264 notifyScrollbarRelevant(false, false); 3265 break; 3266 case 25: 3267 cursorShowing = true; 3268 break; 3269 3270 /* Done */ 3271 default: unknownEscapeSequence(cast(string) esc); 3272 } 3273 } 3274 break; 3275 case 'p': 3276 // it is asking a question... and tbh i don't care. 3277 break; 3278 case 'l': 3279 //import std.stdio; writeln("l magic ", cast(string) esc); 3280 if(esc[1] != '?') 3281 foreach(arg; getArgs()) 3282 switch(arg) { 3283 case 4: 3284 insertMode = false; 3285 break; 3286 case 34: 3287 // no idea. vim inside screen sends it 3288 break; 3289 case 1004: 3290 sendFocusEvents = false; 3291 break; 3292 case 1005: 3293 // turn off utf-8 mouse 3294 utf8MouseMode = false; 3295 break; 3296 case 1006: 3297 // turn off sgr mouse 3298 break; 3299 case 1015: 3300 // turn off urxvt mouse 3301 break; 3302 default: unknownEscapeSequence(cast(string) esc); 3303 } 3304 else 3305 foreach(arg; getArgsBase(2, null)) { 3306 if(arg > 65535) { 3307 /* Extensions */ 3308 if(arg < 65536 + 65535) 3309 hyperlinkActive = false; 3310 } else 3311 switch(arg) { 3312 case 1: 3313 // normal cursor keys 3314 applicationCursorKeys = false; 3315 break; 3316 case 3: 3317 // 80 column mode 3318 break; 3319 case 4: 3320 // smooth scroll 3321 break; 3322 case 5: 3323 // normal video 3324 reverseVideo = false; 3325 break; 3326 case 6: 3327 // normal cursor mode 3328 break; 3329 case 7: 3330 // wraparound mode 3331 wraparoundMode = true; 3332 break; 3333 case 12: 3334 // stop blinking cursor 3335 break; 3336 case 1034: 3337 // meta keys???? 3338 break; 3339 case 1049: 3340 cursorPosition = popSavedCursor; 3341 wraparoundMode = true; 3342 3343 returnToNormalScreen(); 3344 break; 3345 case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it 3346 break; 3347 case 9: 3348 case 1000: 3349 case 1002: 3350 case 1003: 3351 case 1014: // arsd extension 3352 allMouseTrackingOff(); 3353 break; 3354 case 1005: 3355 case 1006: 3356 // idk 3357 break; 3358 case 1048: 3359 cursorPosition = popSavedCursor; 3360 break; 3361 case 2004: 3362 bracketedPasteMode = false; 3363 break; 3364 case 3004: 3365 bracketedHyperlinkMode = false; 3366 break; 3367 case 1047: 3368 case 47: 3369 returnToNormalScreen(); 3370 break; 3371 case 25: 3372 cursorShowing = false; 3373 break; 3374 default: unknownEscapeSequence(cast(string) esc); 3375 } 3376 } 3377 break; 3378 case 'X': 3379 // erase characters 3380 auto count = getArgs(1)[0]; 3381 TerminalCell plain; 3382 plain.ch = ' '; 3383 plain.attributes = currentAttributes; 3384 foreach(cnt; 0 .. count) { 3385 ASS[cursorY][cnt + cursorX] = plain; 3386 } 3387 break; 3388 case 'S': 3389 auto count = getArgs(1)[0]; 3390 // scroll up 3391 scrollUp(count); 3392 break; 3393 case 'T': 3394 auto count = getArgs(1)[0]; 3395 // scroll down 3396 scrollDown(count); 3397 break; 3398 case 'P': 3399 auto count = getArgs(1)[0]; 3400 // delete characters 3401 3402 foreach(cnt; 0 .. count) { 3403 for(int i = cursorX; i < this.screenWidth-1; i++) { 3404 if(ASS[cursorY][i].selected) 3405 clearSelection(); 3406 ASS[cursorY][i] = ASS[cursorY][i + 1]; 3407 ASS[cursorY][i].invalidated = true; 3408 } 3409 3410 if(ASS[cursorY][this.screenWidth - 1].selected) 3411 clearSelection(); 3412 ASS[cursorY][this.screenWidth-1].ch = ' '; 3413 ASS[cursorY][this.screenWidth-1].invalidated = true; 3414 } 3415 3416 extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY); 3417 break; 3418 case '@': 3419 // insert blank characters 3420 auto count = getArgs(1)[0]; 3421 foreach(idx; 0 .. count) { 3422 for(int i = this.screenWidth - 1; i > cursorX; i--) { 3423 ASS[cursorY][i] = ASS[cursorY][i - 1]; 3424 ASS[cursorY][i].invalidated = true; 3425 } 3426 ASS[cursorY][cursorX].ch = ' '; 3427 ASS[cursorY][cursorX].invalidated = true; 3428 } 3429 3430 extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY); 3431 break; 3432 case 'c': 3433 // send device attributes 3434 // FIXME: what am i supposed to do here? 3435 //sendToApplication("\033[>0;138;0c"); 3436 //sendToApplication("\033[?62;"); 3437 sendToApplication(terminalIdCode); 3438 break; 3439 default: 3440 // [42\esc] seems to have gotten here once somehow 3441 // also [24\esc] 3442 unknownEscapeSequence("" ~ cast(string) esc); 3443 } 3444 } else { 3445 unknownEscapeSequence(cast(string) esc); 3446 } 3447 } 3448 } 3449 } 3450 3451 // These match the numbers in terminal.d, so you can just cast it back and forth 3452 // and the names match simpledisplay.d so you can convert that automatically too 3453 enum TerminalKey : int { 3454 Escape = 0x1b + 0xF0000, /// . 3455 F1 = 0x70 + 0xF0000, /// . 3456 F2 = 0x71 + 0xF0000, /// . 3457 F3 = 0x72 + 0xF0000, /// . 3458 F4 = 0x73 + 0xF0000, /// . 3459 F5 = 0x74 + 0xF0000, /// . 3460 F6 = 0x75 + 0xF0000, /// . 3461 F7 = 0x76 + 0xF0000, /// . 3462 F8 = 0x77 + 0xF0000, /// . 3463 F9 = 0x78 + 0xF0000, /// . 3464 F10 = 0x79 + 0xF0000, /// . 3465 F11 = 0x7A + 0xF0000, /// . 3466 F12 = 0x7B + 0xF0000, /// . 3467 Left = 0x25 + 0xF0000, /// . 3468 Right = 0x27 + 0xF0000, /// . 3469 Up = 0x26 + 0xF0000, /// . 3470 Down = 0x28 + 0xF0000, /// . 3471 Insert = 0x2d + 0xF0000, /// . 3472 Delete = 0x2e + 0xF0000, /// . 3473 Home = 0x24 + 0xF0000, /// . 3474 End = 0x23 + 0xF0000, /// . 3475 PageUp = 0x21 + 0xF0000, /// . 3476 PageDown = 0x22 + 0xF0000, /// . 3477 ScrollLock = 0x91 + 0xF0000, 3478 } 3479 3480 /* These match simpledisplay.d which match terminal.d, so you can just cast them */ 3481 3482 enum MouseEventType : int { 3483 motion = 0, 3484 buttonPressed = 1, 3485 buttonReleased = 2, 3486 } 3487 3488 enum MouseButton : int { 3489 // these names assume a right-handed mouse 3490 left = 1, 3491 right = 2, 3492 middle = 4, 3493 wheelUp = 8, 3494 wheelDown = 16, 3495 } 3496 3497 3498 3499 /* 3500 mixin template ImageSupport() { 3501 import arsd.png; 3502 import arsd.bmp; 3503 } 3504 */ 3505 3506 3507 /* helper functions that are generally useful but not necessarily required */ 3508 3509 version(use_libssh2) { 3510 import arsd.libssh2; 3511 void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) { 3512 3513 int tries = 0; 3514 try_again: 3515 try { 3516 import std.socket; 3517 3518 if(libssh2_init(0)) 3519 throw new Exception("libssh2_init"); 3520 scope(exit) 3521 libssh2_exit(); 3522 3523 auto socket = new Socket(AddressFamily.INET, SocketType.STREAM); 3524 socket.connect(new InternetAddress(host, port)); 3525 scope(exit) socket.close(); 3526 3527 auto session = libssh2_session_init_ex(null, null, null, null); 3528 if(session is null) throw new Exception("init session"); 3529 scope(exit) 3530 libssh2_session_disconnect_ex(session, 0, "normal", "EN"); 3531 3532 libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1); 3533 3534 if(libssh2_session_handshake(session, socket.handle)) 3535 throw new Exception("handshake"); 3536 3537 auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1); 3538 if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint) 3539 throw new Exception("fingerprint"); 3540 3541 import std.string : toStringz; 3542 if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null)) 3543 throw new Exception("auth"); 3544 3545 3546 auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0); 3547 3548 if(channel is null) 3549 throw new Exception("channel open"); 3550 3551 scope(exit) 3552 libssh2_channel_free(channel); 3553 3554 // libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length); 3555 3556 if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0)) 3557 throw new Exception("pty"); 3558 3559 if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0)) 3560 throw new Exception("process_startup"); 3561 3562 libssh2_keepalive_config(session, 0, 60); 3563 libssh2_session_set_blocking(session, 0); 3564 3565 masterFunc(socket, session, channel); 3566 } catch(Exception e) { 3567 if(e.msg == "handshake") { 3568 tries++; 3569 import core.thread; 3570 Thread.sleep(200.msecs); 3571 if(tries < 10) 3572 goto try_again; 3573 } 3574 3575 throw e; 3576 } 3577 } 3578 3579 } else 3580 version(Posix) { 3581 extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp); 3582 pragma(lib, "util"); 3583 3584 /// this is good 3585 void startChild(alias masterFunc)(string program, string[] args) { 3586 import core.sys.posix.termios; 3587 import core.sys.posix.signal; 3588 import core.sys.posix.sys.wait; 3589 __gshared static int childrenAlive = 0; 3590 extern(C) nothrow static @nogc 3591 void childdead(int) { 3592 childrenAlive--; 3593 3594 wait(null); 3595 3596 version(with_eventloop) 3597 try { 3598 import arsd.eventloop; 3599 if(childrenAlive <= 0) 3600 exit(); 3601 } catch(Exception e){} 3602 } 3603 3604 signal(SIGCHLD, &childdead); 3605 3606 int master; 3607 int pid = forkpty(&master, null, null, null); 3608 if(pid == -1) 3609 throw new Exception("forkpty"); 3610 if(pid == 0) { 3611 import std.process; 3612 environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes 3613 environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions 3614 3615 import std.string; 3616 if(environment["LANG"].indexOf("UTF-8") == -1) 3617 environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?) 3618 3619 import core.sys.posix.unistd; 3620 3621 import core.stdc.stdlib; 3622 char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1)); 3623 if(argv is null) throw new Exception("malloc"); 3624 foreach(i, arg; args) { 3625 argv[i] = cast(char*) malloc(arg.length + 1); 3626 if(argv[i] is null) throw new Exception("malloc"); 3627 argv[i][0 .. arg.length] = arg[]; 3628 argv[i][arg.length] = 0; 3629 } 3630 3631 argv[args.length] = null; 3632 3633 termios info; 3634 ubyte[128] hack; // jic that druntime definition is still wrong 3635 tcgetattr(master, &info); 3636 info.c_cc[VERASE] = '\b'; 3637 tcsetattr(master, TCSANOW, &info); 3638 3639 core.sys.posix.unistd.execv(argv[0], argv); 3640 } else { 3641 childrenAlive = 1; 3642 masterFunc(master); 3643 } 3644 } 3645 } else 3646 version(Windows) { 3647 import core.sys.windows.windows; 3648 3649 version(winpty) { 3650 alias HPCON = HANDLE; 3651 extern(Windows) 3652 HRESULT function(HPCON, COORD) ResizePseudoConsole; 3653 extern(Windows) 3654 HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole; 3655 extern(Windows) 3656 void function(HPCON) ClosePseudoConsole; 3657 } 3658 3659 extern(Windows) 3660 BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD); 3661 extern(Windows) 3662 BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL); 3663 extern(Windows) 3664 private BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*); 3665 extern(Windows) 3666 BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam); 3667 3668 extern(Windows) 3669 BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM); 3670 extern(Windows) 3671 BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags); 3672 extern(Windows) 3673 BOOL SetHandleInformation(HANDLE, DWORD, DWORD); 3674 extern(Windows) 3675 HANDLE CreateNamedPipeA( 3676 const(char)* lpName, 3677 DWORD dwOpenMode, 3678 DWORD dwPipeMode, 3679 DWORD nMaxInstances, 3680 DWORD nOutBufferSize, 3681 DWORD nInBufferSize, 3682 DWORD nDefaultTimeOut, 3683 LPSECURITY_ATTRIBUTES lpSecurityAttributes 3684 ); 3685 extern(Windows) 3686 BOOL UnregisterWait(HANDLE); 3687 3688 struct STARTUPINFOEXA { 3689 STARTUPINFOA StartupInfo; 3690 void* lpAttributeList; 3691 } 3692 3693 enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016; 3694 enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000; 3695 3696 extern(Windows) 3697 BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T); 3698 extern(Windows) 3699 BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T); 3700 3701 __gshared HANDLE waitHandle; 3702 __gshared bool childDead; 3703 extern(Windows) 3704 void childCallback(void* tidp, bool) { 3705 auto tid = cast(DWORD) tidp; 3706 UnregisterWait(waitHandle); 3707 3708 PostThreadMessageA(tid, WM_QUIT, 0, 0); 3709 childDead = true; 3710 //stupidThreadAlive = false; 3711 } 3712 3713 3714 3715 extern(Windows) 3716 void SetLastError(DWORD); 3717 3718 /// this is good. best to call it with plink.exe so it can talk to unix 3719 /// note that plink asks for the password out of band, so it won't actually work like that. 3720 /// thus specify the password on the command line or better yet, use a private key file 3721 /// e.g. 3722 /// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\""); 3723 void startChild(alias masterFunc)(string program, string commandLine) { 3724 import core.sys.windows.windows; 3725 3726 import arsd.core : MyCreatePipeEx; 3727 3728 import std.conv; 3729 3730 SECURITY_ATTRIBUTES saAttr; 3731 saAttr.nLength = SECURITY_ATTRIBUTES.sizeof; 3732 saAttr.bInheritHandle = true; 3733 saAttr.lpSecurityDescriptor = null; 3734 3735 HANDLE inreadPipe; 3736 HANDLE inwritePipe; 3737 if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0) 3738 throw new Exception("CreatePipe"); 3739 if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 3740 throw new Exception("SetHandleInformation"); 3741 HANDLE outreadPipe; 3742 HANDLE outwritePipe; 3743 3744 version(winpty) 3745 auto flags = 0; 3746 else 3747 auto flags = FILE_FLAG_OVERLAPPED; 3748 3749 if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0) 3750 throw new Exception("CreatePipe"); 3751 if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0)) 3752 throw new Exception("SetHandleInformation"); 3753 3754 version(winpty) { 3755 3756 auto lib = LoadLibrary("kernel32.dll"); 3757 if(lib is null) throw new Exception("holy wtf batman"); 3758 scope(exit) FreeLibrary(lib); 3759 3760 CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole"); 3761 ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole"); 3762 ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole"); 3763 3764 if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null) 3765 throw new Exception("Windows pseudo console not available on this version"); 3766 3767 initPipeHack(outreadPipe); 3768 3769 HPCON hpc; 3770 auto result = CreatePseudoConsole( 3771 COORD(80, 24), 3772 inreadPipe, 3773 outwritePipe, 3774 0, // flags 3775 &hpc 3776 ); 3777 3778 assert(result == S_OK); 3779 3780 scope(exit) 3781 ClosePseudoConsole(hpc); 3782 } 3783 3784 STARTUPINFOEXA siex; 3785 siex.StartupInfo.cb = siex.sizeof; 3786 3787 version(winpty) { 3788 size_t size; 3789 InitializeProcThreadAttributeList(null, 1, 0, &size); 3790 ubyte[] wtf = new ubyte[](size); 3791 siex.lpAttributeList = wtf.ptr; 3792 InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size); 3793 UpdateProcThreadAttribute( 3794 siex.lpAttributeList, 3795 0, 3796 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, 3797 hpc, 3798 hpc.sizeof, 3799 null, 3800 null 3801 ); 3802 } {//else { 3803 siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES; 3804 siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe; 3805 siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe; 3806 siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe; 3807 } 3808 3809 PROCESS_INFORMATION pi; 3810 import std.conv; 3811 3812 if(commandLine.length > 255) 3813 throw new Exception("command line too long"); 3814 char[256] cmdLine; 3815 cmdLine[0 .. commandLine.length] = commandLine[]; 3816 cmdLine[commandLine.length] = 0; 3817 import std.string; 3818 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) 3819 throw new Exception("CreateProcess " ~ to!string(GetLastError())); 3820 3821 if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0) 3822 throw new Exception("RegisterWaitForSingleObject"); 3823 3824 version(winpty) 3825 masterFunc(hpc, inwritePipe, outreadPipe); 3826 else 3827 masterFunc(inwritePipe, outreadPipe); 3828 3829 //stupidThreadAlive = false; 3830 3831 //term.stupidThread.join(); 3832 3833 /* // FIXME: we should close but only if we're legit done 3834 // masterFunc typically runs an event loop but it might not. 3835 CloseHandle(inwritePipe); 3836 CloseHandle(outreadPipe); 3837 3838 CloseHandle(pi.hThread); 3839 CloseHandle(pi.hProcess); 3840 */ 3841 } 3842 } 3843 3844 /// Implementation of TerminalEmulator's abstract functions that forward them to output 3845 mixin template ForwardVirtuals(alias writer) { 3846 static import arsd.color; 3847 3848 protected override void changeCursorStyle(CursorStyle style) { 3849 // FIXME: this should probably just import utility 3850 final switch(style) { 3851 case TerminalEmulator.CursorStyle.block: 3852 writer("\033[2 q"); 3853 break; 3854 case TerminalEmulator.CursorStyle.underline: 3855 writer("\033[4 q"); 3856 break; 3857 case TerminalEmulator.CursorStyle.bar: 3858 writer("\033[6 q"); 3859 break; 3860 } 3861 } 3862 3863 protected override void changeWindowTitle(string t) { 3864 import std.process; 3865 if(t.length && environment["TERM"] != "linux") 3866 writer("\033]0;"~t~"\007"); 3867 } 3868 3869 protected override void changeWindowIcon(arsd.color.IndexedImage t) { 3870 if(t !is null) { 3871 // forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux 3872 import std.process; 3873 if(environment["TERM"] != "linux") 3874 writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007"); 3875 } 3876 } 3877 3878 protected override void changeIconTitle(string) {} // FIXME 3879 protected override void changeTextAttributes(TextAttributes) {} // FIXME 3880 protected override void soundBell() { 3881 writer("\007"); 3882 } 3883 protected override void demandAttention() { 3884 import std.process; 3885 if(environment["TERM"] != "linux") 3886 writer("\033]5001;1\007"); // the 1 there means true but is currently ignored 3887 } 3888 protected override void copyToClipboard(string text) { 3889 // this is xterm compatible, though xterm rarely implements it 3890 import std.base64; 3891 // idk why the cast is needed here 3892 writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007"); 3893 } 3894 protected override void pasteFromClipboard(void delegate(in char[]) dg) { 3895 // this is a slight extension. xterm invented the string - it means request the primary selection - 3896 // but it generally doesn't actually get a reply. so i'm using it to request the primary which will be 3897 // sent as a pasted strong. 3898 // (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.) 3899 writer("\033]52;c;?\007"); 3900 } 3901 protected override void copyToPrimary(string text) { 3902 import std.base64; 3903 writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007"); 3904 } 3905 protected override void pasteFromPrimary(void delegate(in char[]) dg) { 3906 writer("\033]52;p;?\007"); 3907 } 3908 3909 } 3910 3911 /// you can pass this as PtySupport's arguments when you just don't care 3912 final void doNothing() {} 3913 3914 version(winpty) { 3915 __gshared static HANDLE inputEvent; 3916 __gshared static HANDLE magicEvent; 3917 __gshared static ubyte[] helperBuffer; 3918 __gshared static HANDLE helperThread; 3919 3920 static void initPipeHack(void* ptr) { 3921 inputEvent = CreateEvent(null, false, false, null); 3922 assert(inputEvent !is null); 3923 magicEvent = CreateEvent(null, false, true, null); 3924 assert(magicEvent !is null); 3925 3926 helperThread = CreateThread( 3927 null, 3928 0, 3929 &actuallyRead, 3930 ptr, 3931 0, 3932 null 3933 ); 3934 3935 assert(helperThread !is null); 3936 } 3937 3938 extern(Windows) static 3939 uint actuallyRead(void* ptr) { 3940 ubyte[4096] buffer; 3941 DWORD got; 3942 while(true) { 3943 // wait for the other thread to tell us they 3944 // are done... 3945 WaitForSingleObject(magicEvent, INFINITE); 3946 auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null); 3947 helperBuffer = buffer[0 .. got]; 3948 // tells the other thread it is allowed to read 3949 // readyToReadPty 3950 SetEvent(inputEvent); 3951 } 3952 assert(0); 3953 } 3954 3955 3956 } 3957 3958 /// You must implement a function called redraw() and initialize the members in your constructor 3959 mixin template PtySupport(alias resizeHelper) { 3960 // Initialize these! 3961 3962 final void redraw_() { 3963 if(invalidateAll) { 3964 extendInvalidatedRange(0, 0, this.screenWidth, this.screenHeight); 3965 if(alternateScreenActive) 3966 foreach(ref t; alternateScreen) 3967 t.invalidated = true; 3968 else 3969 foreach(ref t; normalScreen) 3970 t.invalidated = true; 3971 invalidateAll = false; 3972 } 3973 redraw(); 3974 //soundBell(); 3975 } 3976 3977 version(use_libssh2) { 3978 import arsd.libssh2; 3979 LIBSSH2_CHANNEL* sshChannel; 3980 } else version(Windows) { 3981 import core.sys.windows.windows; 3982 HANDLE stdin; 3983 HANDLE stdout; 3984 } else version(Posix) { 3985 int master; 3986 } 3987 3988 version(use_libssh2) { } 3989 else version(Posix) { 3990 int previousProcess = 0; 3991 int activeProcess = 0; 3992 int activeProcessWhenResized = 0; 3993 bool resizedRecently; 3994 3995 /* 3996 so, this isn't perfect, but it is meant to send the resize signal to an existing process 3997 when it isn't in the front when you resize. 3998 3999 For example, open vim and resize. Then exit vim. We want bash to be updated. 4000 4001 But also don't want to do too many spurious signals. 4002 4003 It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but 4004 the outer bash won't see it. I guess I need some kind of process stack. 4005 4006 but it is okish. 4007 */ 4008 override void outputOccurred() { 4009 import core.sys.posix.unistd; 4010 auto pgrp = tcgetpgrp(master); 4011 if(pgrp != -1) { 4012 if(pgrp != activeProcess) { 4013 auto previousProcessAtStartup = previousProcess; 4014 4015 previousProcess = activeProcess; 4016 activeProcess = pgrp; 4017 4018 if(resizedRecently) { 4019 if(activeProcess != activeProcessWhenResized) { 4020 resizedRecently = false; 4021 4022 if(activeProcess == previousProcessAtStartup) { 4023 //import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight); 4024 4025 import core.sys.posix.signal; 4026 kill(-activeProcess, 28 /* 28 == SIGWINCH*/); 4027 } 4028 } 4029 } 4030 } 4031 } 4032 4033 4034 super.outputOccurred(); 4035 } 4036 //return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline"); 4037 } 4038 4039 4040 override void resizeTerminal(int w, int h) { 4041 version(Posix) { 4042 activeProcessWhenResized = activeProcess; 4043 resizedRecently = true; 4044 } 4045 4046 resizeHelper(); 4047 4048 super.resizeTerminal(w, h); 4049 4050 version(use_libssh2) { 4051 libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0); 4052 } else version(Posix) { 4053 import core.sys.posix.sys.ioctl; 4054 winsize win; 4055 win.ws_col = cast(ushort) w; 4056 win.ws_row = cast(ushort) h; 4057 4058 ioctl(master, TIOCSWINSZ, &win); 4059 } else version(Windows) { 4060 version(winpty) { 4061 COORD coord; 4062 coord.X = cast(ushort) w; 4063 coord.Y = cast(ushort) h; 4064 ResizePseudoConsole(hpc, coord); 4065 } else { 4066 sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]); 4067 } 4068 } else static assert(0); 4069 } 4070 4071 protected override void sendToApplication(scope const(void)[] data) { 4072 version(use_libssh2) { 4073 while(data.length) { 4074 auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length); 4075 if(sent < 0) 4076 throw new Exception("libssh2_channel_write_ex"); 4077 data = data[sent .. $]; 4078 } 4079 } else version(Windows) { 4080 import std.conv; 4081 uint written; 4082 if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0) 4083 throw new Exception("WriteFile " ~ to!string(GetLastError())); 4084 } else version(Posix) { 4085 import core.sys.posix.unistd; 4086 int frozen; 4087 while(data.length) { 4088 enum MAX_SEND = 1024 * 20; 4089 auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length); 4090 //import std.stdio; writeln("ROFL ", sent, " ", data.length); 4091 4092 import core.stdc.errno; 4093 if(sent == -1 && errno == 11) { 4094 import core.thread; 4095 if(frozen == 50) 4096 throw new Exception("write froze up"); 4097 frozen++; 4098 Thread.sleep(10.msecs); 4099 //import std.stdio; writeln("lol"); 4100 continue; // just try again 4101 } 4102 4103 frozen = 0; 4104 4105 import std.conv; 4106 if(sent < 0) 4107 throw new Exception("write " ~ to!string(errno)); 4108 4109 data = data[sent .. $]; 4110 } 4111 } else static assert(0); 4112 } 4113 4114 version(use_libssh2) { 4115 int readyToRead(int fd) { 4116 int count = 0; // if too much stuff comes at once, we still want to be responsive 4117 while(true) { 4118 ubyte[4096] buffer; 4119 auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length); 4120 if(got == LIBSSH2_ERROR_EAGAIN) 4121 break; // got it all for now 4122 if(got < 0) 4123 throw new Exception("libssh2_channel_read_ex"); 4124 if(got == 0) 4125 break; // NOT an error! 4126 4127 super.sendRawInput(buffer[0 .. got]); 4128 count++; 4129 4130 if(count == 5) { 4131 count = 0; 4132 redraw_(); 4133 justRead(); 4134 } 4135 } 4136 4137 if(libssh2_channel_eof(sshChannel)) { 4138 libssh2_channel_close(sshChannel); 4139 libssh2_channel_wait_closed(sshChannel); 4140 4141 return 1; 4142 } 4143 4144 if(count != 0) { 4145 redraw_(); 4146 justRead(); 4147 } 4148 return 0; 4149 } 4150 } else version(winpty) { 4151 void readyToReadPty() { 4152 super.sendRawInput(helperBuffer); 4153 SetEvent(magicEvent); // tell the other thread we have finished 4154 redraw_(); 4155 justRead(); 4156 } 4157 } else version(Windows) { 4158 OVERLAPPED* overlapped; 4159 bool overlappedBufferLocked; 4160 ubyte[4096] overlappedBuffer; 4161 extern(Windows) 4162 static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) { 4163 assert(overlapped !is null); 4164 typeof(this) w = cast(typeof(this)) overlapped.hEvent; 4165 4166 if(numberOfBytes) { 4167 w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]); 4168 w.redraw_(); 4169 } 4170 import std.conv; 4171 4172 if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) { 4173 if(GetLastError() == 997) 4174 { } // there's pending i/o, let's just ignore for now and it should tell us later that it completed 4175 else 4176 throw new Exception("ReadFileEx " ~ to!string(GetLastError())); 4177 } else { 4178 } 4179 4180 w.justRead(); 4181 } 4182 } else version(Posix) { 4183 void readyToRead(int fd) { 4184 import core.sys.posix.unistd; 4185 ubyte[4096] buffer; 4186 4187 // the count is to limit how long we spend in this loop 4188 // when it runs out, it goes back to the main event loop 4189 // for a while (btw use level triggered events so the remaining 4190 // data continues to get processed!) giving a chance to redraw 4191 // and process user input periodically during insanely long and 4192 // rapid output. 4193 int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests 4194 4195 version(arsd_te_conservative_draws) 4196 cnt = 400; 4197 4198 // FIXME: if connected by ssh, up the count so we don't redraw as frequently. 4199 // it'd save bandwidth 4200 4201 while(--cnt) { 4202 auto len = read(fd, buffer.ptr, 4096); 4203 if(len < 0) { 4204 import core.stdc.errno; 4205 if(errno == EAGAIN || errno == EWOULDBLOCK) { 4206 break; // we got it all 4207 } else { 4208 //import std.conv; 4209 //throw new Exception("read failed " ~ to!string(errno)); 4210 return; 4211 } 4212 } 4213 4214 if(len == 0) { 4215 close(fd); 4216 requestExit(); 4217 break; 4218 } 4219 4220 auto data = buffer[0 .. len]; 4221 4222 if(debugMode) { 4223 import std.array; import std.stdio; writeln("GOT ", data, "\nOR ", 4224 replace(cast(string) data, "\033", "\\") 4225 .replace("\010", "^H") 4226 .replace("\r", "^M") 4227 .replace("\n", "^J") 4228 ); 4229 } 4230 super.sendRawInput(data); 4231 } 4232 4233 outputOccurred(); 4234 4235 redraw_(); 4236 4237 // HACK: I don't even know why this works, but with this 4238 // sleep in place, it gives X events from that socket a 4239 // chance to be processed. It can add a few seconds to a huge 4240 // output (like `find /usr`), but meh, that's worth it to me 4241 // to have a chance to ctrl+c. 4242 import core.thread; 4243 Thread.sleep(dur!"msecs"(5)); 4244 4245 justRead(); 4246 } 4247 } 4248 } 4249 4250 mixin template SdpyImageSupport() { 4251 class NonCharacterData_Image : NonCharacterData { 4252 Image data; 4253 int imageOffsetX; 4254 int imageOffsetY; 4255 4256 this(Image data, int x, int y) { 4257 this.data = data; 4258 this.imageOffsetX = x; 4259 this.imageOffsetY = y; 4260 } 4261 } 4262 4263 version(TerminalDirectToEmulator) 4264 class NonCharacterData_Widget : NonCharacterData { 4265 this(void* data, size_t idx, int width, int height) { 4266 this.window = cast(SimpleWindow) data; 4267 this.idx = idx; 4268 this.width = width; 4269 this.height = height; 4270 } 4271 4272 void position(int posx, int posy, int width, int height) { 4273 if(posx == this.posx && posy == this.posy && width == this.pixelWidth && height == this.pixelHeight) 4274 return; 4275 this.posx = posx; 4276 this.posy = posy; 4277 this.pixelWidth = width; 4278 this.pixelHeight = height; 4279 4280 window.moveResize(posx, posy, width, height); 4281 import std.stdio; writeln(posx, " ", posy, " ", width, " ", height); 4282 4283 auto painter = this.window.draw; 4284 painter.outlineColor = Color.red; 4285 painter.fillColor = Color.green; 4286 painter.drawRectangle(Point(0, 0), width, height); 4287 4288 4289 } 4290 4291 SimpleWindow window; 4292 size_t idx; 4293 int width; 4294 int height; 4295 4296 int posx; 4297 int posy; 4298 int pixelWidth; 4299 int pixelHeight; 4300 } 4301 4302 private struct CachedImage { 4303 ulong hash; 4304 BinaryDataTerminalRepresentation bui; 4305 int timesSeen; 4306 import core.time; 4307 MonoTime lastUsed; 4308 } 4309 private CachedImage[] imageCache; 4310 private CachedImage* findInCache(ulong hash) { 4311 if(hash == 0) 4312 return null; 4313 4314 /* 4315 import std.stdio; 4316 writeln("***"); 4317 foreach(cache; imageCache) { 4318 writeln(cache.hash, " ", cache.timesSeen, " ", cache.lastUsed); 4319 } 4320 */ 4321 4322 foreach(ref i; imageCache) 4323 if(i.hash == hash) { 4324 import core.time; 4325 i.lastUsed = MonoTime.currTime; 4326 i.timesSeen++; 4327 return &i; 4328 } 4329 return null; 4330 } 4331 private BinaryDataTerminalRepresentation addImageCache(ulong hash, BinaryDataTerminalRepresentation bui) { 4332 import core.time; 4333 if(imageCache.length == 0) 4334 imageCache.length = 8; 4335 4336 auto now = MonoTime.currTime; 4337 4338 size_t oldestIndex; 4339 MonoTime oldestTime = now; 4340 4341 size_t leastUsedIndex; 4342 int leastUsedCount = int.max; 4343 foreach(idx, ref cached; imageCache) { 4344 if(cached.hash == 0) { 4345 cached.hash = hash; 4346 cached.bui = bui; 4347 cached.timesSeen = 1; 4348 cached.lastUsed = now; 4349 4350 return bui; 4351 } else { 4352 if(cached.timesSeen < leastUsedCount) { 4353 leastUsedCount = cached.timesSeen; 4354 leastUsedIndex = idx; 4355 } 4356 if(cached.lastUsed < oldestTime) { 4357 oldestTime = cached.lastUsed; 4358 oldestIndex = idx; 4359 } 4360 } 4361 } 4362 4363 // need to overwrite one of the cached items, I'll just use the oldest one here 4364 // but maybe that could be smarter later 4365 4366 imageCache[oldestIndex].hash = hash; 4367 imageCache[oldestIndex].bui = bui; 4368 imageCache[oldestIndex].timesSeen = 1; 4369 imageCache[oldestIndex].lastUsed = now; 4370 4371 return bui; 4372 } 4373 4374 // It has a cache of the 8 most recently used items right now so if there's a loop of 9 you get pwned 4375 // but still the cache does an ok job at helping things while balancing out the big memory consumption it 4376 // could do if just left to grow and grow. i hope. 4377 protected override BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[] binaryData) { 4378 4379 version(none) { 4380 //version(TerminalDirectToEmulator) 4381 //if(binaryData.length == size_t.sizeof + 10) { 4382 //if((cast(uint[]) binaryData[0 .. 4])[0] == 0xdeadbeef && (cast(uint[]) binaryData[$-4 .. $])[0] == 0xabcdef32) { 4383 //auto widthInCharacterCells = binaryData[4]; 4384 //auto heightInCharacterCells = binaryData[5]; 4385 //auto pointer = (cast(void*[]) binaryData[6 .. $-4])[0]; 4386 4387 auto widthInCharacterCells = 30; 4388 auto heightInCharacterCells = 20; 4389 SimpleWindow pwin; 4390 foreach(k, v; SimpleWindow.nativeMapping) { 4391 if(v.type == WindowTypes.normal) 4392 pwin = v; 4393 } 4394 auto pointer = cast(void*) (new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin)); 4395 4396 BinaryDataTerminalRepresentation bi; 4397 bi.width = widthInCharacterCells; 4398 bi.height = heightInCharacterCells; 4399 bi.representation.length = bi.width * bi.height; 4400 4401 foreach(idx, ref cell; bi.representation) { 4402 cell.nonCharacterData = new NonCharacterData_Widget(pointer, idx, widthInCharacterCells, heightInCharacterCells); 4403 } 4404 4405 return bi; 4406 //} 4407 } 4408 4409 import std.digest.md; 4410 4411 ulong hash = * (cast(ulong*) md5Of(binaryData).ptr); 4412 4413 if(auto cached = findInCache(hash)) 4414 return cached.bui; 4415 4416 TrueColorImage mi; 4417 4418 if(binaryData.length > 8 && binaryData[1] == 'P' && binaryData[2] == 'N' && binaryData[3] == 'G') { 4419 import arsd.png; 4420 mi = imageFromPng(readPng(binaryData)).getAsTrueColorImage(); 4421 } else if(binaryData.length > 8 && binaryData[0] == 'B' && binaryData[1] == 'M') { 4422 import arsd.bmp; 4423 mi = readBmp(binaryData).getAsTrueColorImage(); 4424 } else if(binaryData.length > 2 && binaryData[0] == 0xff && binaryData[1] == 0xd8) { 4425 import arsd.jpeg; 4426 mi = readJpegFromMemory(binaryData).getAsTrueColorImage(); 4427 } else if(binaryData.length > 2 && binaryData[0] == '<') { 4428 import arsd.svg; 4429 NSVG* image = nsvgParse(cast(const(char)[]) binaryData); 4430 if(image is null) 4431 return BinaryDataTerminalRepresentation(); 4432 4433 int w = cast(int) image.width + 1; 4434 int h = cast(int) image.height + 1; 4435 NSVGrasterizer rast = nsvgCreateRasterizer(); 4436 mi = new TrueColorImage(w, h); 4437 rasterize(rast, image, 0, 0, 1, mi.imageData.bytes.ptr, w, h, w*4); 4438 image.kill(); 4439 } else { 4440 return BinaryDataTerminalRepresentation(); 4441 } 4442 4443 BinaryDataTerminalRepresentation bi; 4444 bi.width = mi.width / fontWidth + ((mi.width%fontWidth) ? 1 : 0); 4445 bi.height = mi.height / fontHeight + ((mi.height%fontHeight) ? 1 : 0); 4446 4447 bi.representation.length = bi.width * bi.height; 4448 4449 Image data = Image.fromMemoryImage(mi); 4450 4451 int ix, iy; 4452 foreach(ref cell; bi.representation) { 4453 /* 4454 Image data = new Image(fontWidth, fontHeight); 4455 foreach(y; 0 .. fontHeight) { 4456 foreach(x; 0 .. fontWidth) { 4457 if(x + ix >= mi.width || y + iy >= mi.height) { 4458 data.putPixel(x, y, defaultTextAttributes.background); 4459 continue; 4460 } 4461 data.putPixel(x, y, mi.imageData.colors[(iy + y) * mi.width + (ix + x)]); 4462 } 4463 } 4464 */ 4465 4466 cell.nonCharacterData = new NonCharacterData_Image(data, ix, iy); 4467 4468 ix += fontWidth; 4469 4470 if(ix >= mi.width) { 4471 ix = 0; 4472 iy += fontHeight; 4473 } 4474 } 4475 4476 return addImageCache(hash, bi); 4477 //return bi; 4478 } 4479 4480 } 4481 4482 // this assumes you have imported arsd.simpledisplay and/or arsd.minigui in the mixin scope 4483 mixin template SdpyDraw() { 4484 4485 // black bg, make the colors more visible 4486 static Color contrastify(Color c) { 4487 if(c == Color(0xcd, 0, 0)) 4488 return Color.fromHsl(0, 1.0, 0.75); 4489 else if(c == Color(0, 0, 0xcd)) 4490 return Color.fromHsl(240, 1.0, 0.75); 4491 else if(c == Color(229, 229, 229)) 4492 return Color(0x99, 0x99, 0x99); 4493 else if(c == Color.black) 4494 return Color(128, 128, 128); 4495 else return c; 4496 } 4497 4498 // white bg, make them more visible 4499 static Color antiContrastify(Color c) { 4500 if(c == Color(0xcd, 0xcd, 0)) 4501 return Color.fromHsl(60, 1.0, 0.25); 4502 else if(c == Color(0, 0xcd, 0xcd)) 4503 return Color.fromHsl(180, 1.0, 0.25); 4504 else if(c == Color(229, 229, 229)) 4505 return Color(0x99, 0x99, 0x99); 4506 else if(c == Color.white) 4507 return Color(128, 128, 128); 4508 else return c; 4509 } 4510 4511 struct SRectangle { 4512 int left; 4513 int top; 4514 int right; 4515 int bottom; 4516 } 4517 4518 mixin SdpyImageSupport; 4519 4520 OperatingSystemFont font; 4521 int fontWidth; 4522 int fontHeight; 4523 4524 enum paddingLeft = 2; 4525 enum paddingTop = 1; 4526 4527 void loadDefaultFont(int size = 14) { 4528 static if(UsingSimpledisplayX11) { 4529 font = new OperatingSystemFont("core:fixed", size, FontWeight.medium); 4530 //font = new OperatingSystemFont("monospace", size, FontWeight.medium); 4531 if(font.isNull) { 4532 // didn't work, it is using a 4533 // fallback, prolly fixed-13 is best 4534 font = new OperatingSystemFont("core:fixed", 13, FontWeight.medium); 4535 } 4536 } else version(Windows) { 4537 this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium); 4538 if(!this.font.isNull && !this.font.isMonospace) 4539 this.font.unload(); // non-monospace fonts are unusable here. This should never happen anyway though as Courier New comes with Windows 4540 } 4541 4542 if(font.isNull) { 4543 // no way to really tell... just guess so it doesn't crash but like eeek. 4544 fontWidth = size / 2; 4545 fontHeight = size; 4546 } else { 4547 fontWidth = font.averageWidth; 4548 fontHeight = font.height; 4549 } 4550 } 4551 4552 bool lastDrawAlternativeScreen; 4553 final SRectangle redrawPainter(T)(T painter, bool forceRedraw) { 4554 SRectangle invalidated; 4555 4556 // FIXME: anything we can do to make this faster is good 4557 // on both, the XImagePainter could use optimizations 4558 // on both, drawing blocks would probably be good too - not just one cell at a time, find whole blocks of stuff 4559 // on both it might also be good to keep scroll commands high level somehow. idk. 4560 4561 // FIXME on Windows it would definitely help a lot to do just one ExtTextOutW per line, if possible. the current code is brutally slow 4562 4563 // Or also see https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-polytextoutw 4564 4565 static if(is(T == WidgetPainter) || is(T == ScreenPainter)) { 4566 if(font) 4567 painter.setFont(font); 4568 } 4569 4570 4571 int posx = paddingLeft; 4572 int posy = paddingTop; 4573 4574 4575 char[512] bufferText; 4576 bool hasBufferedInfo; 4577 int bufferTextLength; 4578 Color bufferForeground; 4579 Color bufferBackground; 4580 int bufferX = -1; 4581 int bufferY = -1; 4582 bool bufferReverse; 4583 void flushBuffer() { 4584 if(!hasBufferedInfo) { 4585 return; 4586 } 4587 4588 assert(posx - bufferX - 1 > 0); 4589 4590 painter.fillColor = bufferReverse ? bufferForeground : bufferBackground; 4591 painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground; 4592 4593 painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight); 4594 painter.fillColor = Color.transparent; 4595 // Hack for contrast! 4596 if(bufferBackground == Color.black && !bufferReverse) { 4597 // brighter than normal in some cases so i can read it easily 4598 painter.outlineColor = contrastify(bufferForeground); 4599 } else if(bufferBackground == Color.white && !bufferReverse) { 4600 // darker than normal so i can read it 4601 painter.outlineColor = antiContrastify(bufferForeground); 4602 } else if(bufferForeground == bufferBackground) { 4603 // color on itself, I want it visible too 4604 auto hsl = toHsl(bufferForeground, true); 4605 if(hsl[0] == 240) { 4606 // blue is a bit special, it generally looks darker 4607 // so we want to get very bright or very dark 4608 if(hsl[2] < 0.7) 4609 hsl[2] = 0.9; 4610 else 4611 hsl[2] = 0.1; 4612 } else { 4613 if(hsl[2] < 0.5) 4614 hsl[2] += 0.5; 4615 else 4616 hsl[2] -= 0.5; 4617 } 4618 painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]); 4619 } else { 4620 auto drawColor = bufferReverse ? bufferBackground : bufferForeground; 4621 ///+ 4622 // try to ensure legible contrast with any arbitrary combination 4623 auto bgColor = bufferReverse ? bufferForeground : bufferBackground; 4624 auto fghsl = toHsl(drawColor, true); 4625 auto bghsl = toHsl(bgColor, true); 4626 4627 if(fghsl[2] > 0.5 && bghsl[2] > 0.5) { 4628 // bright color on bright background 4629 painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2); 4630 } else if(fghsl[2] < 0.5 && bghsl[2] < 0.5) { 4631 // dark color on dark background 4632 if(fghsl[0] == 240 && bghsl[0] >= 60 && bghsl[0] <= 180) 4633 // blue on green looks dark to the algorithm but isn't really 4634 painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2); 4635 else 4636 painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.8); 4637 } else { 4638 // normal 4639 painter.outlineColor = drawColor; 4640 } 4641 //+/ 4642 4643 // normal 4644 //painter.outlineColor = drawColor; 4645 } 4646 4647 // FIXME: make sure this clips correctly 4648 painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]); 4649 4650 // import std.stdio; writeln(bufferX, " ", bufferY); 4651 4652 hasBufferedInfo = false; 4653 4654 bufferReverse = false; 4655 bufferTextLength = 0; 4656 bufferX = -1; 4657 bufferY = -1; 4658 } 4659 4660 4661 4662 int x; 4663 auto bfr = alternateScreenActive ? alternateScreen : normalScreen; 4664 4665 version(invalidator_2) { 4666 if(invalidatedMax > bfr.length) 4667 invalidatedMax = cast(int) bfr.length; 4668 if(invalidatedMin > invalidatedMax) 4669 invalidatedMin = invalidatedMax; 4670 if(invalidatedMin >= 0) 4671 bfr = bfr[invalidatedMin .. invalidatedMax]; 4672 4673 posx += (invalidatedMin % screenWidth) * fontWidth; 4674 posy += (invalidatedMin / screenWidth) * fontHeight; 4675 4676 //import std.stdio; writeln(invalidatedMin, " to ", invalidatedMax, " ", posx, "x", posy); 4677 invalidated.left = posx; 4678 invalidated.top = posy; 4679 invalidated.right = posx; 4680 invalidated.top = posy; 4681 4682 clearInvalidatedRange(); 4683 } 4684 4685 foreach(idx, ref cell; bfr) { 4686 if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) { 4687 flushBuffer(); 4688 goto skipDrawing; 4689 } 4690 cell.invalidated = false; 4691 version(none) if(bufferX == -1) { // why was this ever here? 4692 bufferX = posx; 4693 bufferY = posy; 4694 } 4695 4696 if(!cell.hasNonCharacterData) { 4697 4698 invalidated.left = posx < invalidated.left ? posx : invalidated.left; 4699 invalidated.top = posy < invalidated.top ? posy : invalidated.top; 4700 int xmax = posx + fontWidth; 4701 int ymax = posy + fontHeight; 4702 invalidated.right = xmax > invalidated.right ? xmax : invalidated.right; 4703 invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom; 4704 4705 // FIXME: this could be more efficient, simpledisplay could get better graphics context handling 4706 { 4707 4708 bool reverse = (cell.attributes.inverse != reverseVideo); 4709 if(cell.selected) 4710 reverse = !reverse; 4711 4712 version(with_24_bit_color) { 4713 auto fgc = cell.attributes.foreground; 4714 auto bgc = cell.attributes.background; 4715 4716 if(!(cell.attributes.foregroundIndex & 0xff00)) { 4717 // this refers to a specific palette entry, which may change, so we should use that 4718 fgc = palette[cell.attributes.foregroundIndex]; 4719 } 4720 if(!(cell.attributes.backgroundIndex & 0xff00)) { 4721 // this refers to a specific palette entry, which may change, so we should use that 4722 bgc = palette[cell.attributes.backgroundIndex]; 4723 } 4724 4725 } else { 4726 auto fgc = cell.attributes.foregroundIndex == 256 ? defaultForeground : palette[cell.attributes.foregroundIndex & 0xff]; 4727 auto bgc = cell.attributes.backgroundIndex == 256 ? defaultBackground : palette[cell.attributes.backgroundIndex & 0xff]; 4728 } 4729 4730 if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse) 4731 flushBuffer(); 4732 bufferReverse = reverse; 4733 bufferBackground = bgc; 4734 bufferForeground = fgc; 4735 } 4736 } 4737 4738 if(!cell.hasNonCharacterData) { 4739 char[4] str; 4740 import std.utf; 4741 // now that it is buffered, we do want to draw it this way... 4742 //if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing 4743 try { 4744 auto stride = encode(str, cell.ch); 4745 if(bufferTextLength + stride > bufferText.length) 4746 flushBuffer(); 4747 bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride]; 4748 bufferTextLength += stride; 4749 4750 if(bufferX == -1) { 4751 bufferX = posx; 4752 bufferY = posy; 4753 } 4754 hasBufferedInfo = true; 4755 } catch(Exception e) { 4756 // import std.stdio; writeln(cast(uint) cell.ch, " :: ", e.msg); 4757 } 4758 //} 4759 } else if(cell.nonCharacterData !is null) { 4760 //import std.stdio; writeln(cast(void*) cell.nonCharacterData); 4761 if(auto ncdi = cast(NonCharacterData_Image) cell.nonCharacterData) { 4762 flushBuffer(); 4763 painter.outlineColor = defaultBackground; 4764 painter.fillColor = defaultBackground; 4765 painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight); 4766 painter.drawImage(Point(posx, posy), ncdi.data, Point(ncdi.imageOffsetX, ncdi.imageOffsetY), fontWidth, fontHeight); 4767 } 4768 version(TerminalDirectToEmulator) 4769 if(auto wdi = cast(NonCharacterData_Widget) cell.nonCharacterData) { 4770 flushBuffer(); 4771 if(wdi.idx == 0) { 4772 wdi.position(posx, posy, fontWidth * wdi.width, fontHeight * wdi.height); 4773 /* 4774 painter.outlineColor = defaultBackground; 4775 painter.fillColor = defaultBackground; 4776 painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight); 4777 */ 4778 } 4779 4780 } 4781 } 4782 4783 if(!cell.hasNonCharacterData) 4784 if(cell.attributes.underlined) { 4785 // the posx adjustment is because the buffer assumes it is going 4786 // to be flushed after advancing, but here, we're doing it mid-character 4787 // FIXME: we should just underline the whole thing consecutively, with the buffer 4788 posx += fontWidth; 4789 flushBuffer(); 4790 posx -= fontWidth; 4791 painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1)); 4792 } 4793 skipDrawing: 4794 4795 posx += fontWidth; 4796 x++; 4797 if(x == screenWidth) { 4798 flushBuffer(); 4799 x = 0; 4800 posy += fontHeight; 4801 posx = paddingLeft; 4802 } 4803 } 4804 4805 flushBuffer(); 4806 4807 if(cursorShowing) { 4808 painter.fillColor = cursorColor; 4809 painter.outlineColor = cursorColor; 4810 painter.rasterOp = RasterOp.xor; 4811 4812 posx = cursorPosition.x * fontWidth + paddingLeft; 4813 posy = cursorPosition.y * fontHeight + paddingTop; 4814 4815 int cursorWidth = fontWidth; 4816 int cursorHeight = fontHeight; 4817 4818 final switch(cursorStyle) { 4819 case CursorStyle.block: 4820 painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight); 4821 break; 4822 case CursorStyle.underline: 4823 painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2); 4824 break; 4825 case CursorStyle.bar: 4826 painter.drawRectangle(Point(posx, posy), 2, cursorHeight); 4827 break; 4828 } 4829 painter.rasterOp = RasterOp.normal; 4830 4831 painter.notifyCursorPosition(posx, posy, cursorWidth, cursorHeight); 4832 4833 // since the cursor draws over the cell, we need to make sure it is redrawn each time too 4834 auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen); 4835 if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) { 4836 (*buffer)[cursorY * screenWidth + cursorX].invalidated = true; 4837 } 4838 4839 extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY); 4840 4841 invalidated.left = posx < invalidated.left ? posx : invalidated.left; 4842 invalidated.top = posy < invalidated.top ? posy : invalidated.top; 4843 int xmax = posx + fontWidth; 4844 int ymax = xmax + fontHeight; 4845 invalidated.right = xmax > invalidated.right ? xmax : invalidated.right; 4846 invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom; 4847 } 4848 4849 lastDrawAlternativeScreen = alternateScreenActive; 4850 4851 return invalidated; 4852 } 4853 } 4854 4855 string encodeSmallTextImage(IndexedImage ii) { 4856 char encodeNumeric(int c) { 4857 if(c < 10) 4858 return cast(char)(c + '0'); 4859 if(c < 10 + 26) 4860 return cast(char)(c - 10 + 'a'); 4861 assert(0); 4862 } 4863 4864 string s; 4865 s ~= encodeNumeric(ii.width); 4866 s ~= encodeNumeric(ii.height); 4867 4868 foreach(entry; ii.palette) 4869 s ~= entry.toRgbaHexString(); 4870 s ~= "Z"; 4871 4872 ubyte rleByte; 4873 int rleCount; 4874 4875 void rleCommit() { 4876 if(rleByte >= 26) 4877 assert(0); // too many colors for us to handle 4878 if(rleCount == 0) 4879 goto finish; 4880 if(rleCount == 1) { 4881 s ~= rleByte + 'a'; 4882 goto finish; 4883 } 4884 4885 import std.conv; 4886 s ~= to!string(rleCount); 4887 s ~= rleByte + 'a'; 4888 4889 finish: 4890 rleByte = 0; 4891 rleCount = 0; 4892 } 4893 4894 foreach(b; ii.data) { 4895 if(b == rleByte) 4896 rleCount++; 4897 else { 4898 rleCommit(); 4899 rleByte = b; 4900 rleCount = 1; 4901 } 4902 } 4903 4904 rleCommit(); 4905 4906 return s; 4907 } 4908 4909 IndexedImage readSmallTextImage(scope const(char)[] arg) { 4910 auto origArg = arg; 4911 int width; 4912 int height; 4913 4914 int readNumeric(char c) { 4915 if(c >= '0' && c <= '9') 4916 return c - '0'; 4917 if(c >= 'a' && c <= 'z') 4918 return c - 'a' + 10; 4919 return 0; 4920 } 4921 4922 if(arg.length > 2) { 4923 width = readNumeric(arg[0]); 4924 height = readNumeric(arg[1]); 4925 arg = arg[2 .. $]; 4926 } 4927 4928 import std.conv; 4929 assert(width == 16, to!string(width)); 4930 assert(height == 16, to!string(width)); 4931 4932 Color[] palette; 4933 ubyte[256] data; 4934 int didx = 0; 4935 bool readingPalette = true; 4936 outer: while(arg.length) { 4937 if(readingPalette) { 4938 if(arg[0] == 'Z') { 4939 readingPalette = false; 4940 arg = arg[1 .. $]; 4941 continue; 4942 } 4943 if(arg.length < 8) 4944 break; 4945 foreach(a; arg[0..8]) { 4946 // if not strict hex, forget it 4947 if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z'))) 4948 break outer; 4949 } 4950 palette ~= Color.fromString(arg[0 .. 8]); 4951 arg = arg[8 .. $]; 4952 } else { 4953 char[3] rleChars; 4954 int rlePos; 4955 while(arg.length && arg[0] >= '0' && arg[0] <= '9') { 4956 rleChars[rlePos] = arg[0]; 4957 arg = arg[1 .. $]; 4958 rlePos++; 4959 if(rlePos >= rleChars.length) 4960 break; 4961 } 4962 if(arg.length == 0) 4963 break; 4964 4965 int rle; 4966 if(rlePos == 0) 4967 rle = 1; 4968 else { 4969 // 100 4970 // rleChars[0] == '1' 4971 foreach(c; rleChars[0 .. rlePos]) { 4972 rle *= 10; 4973 rle += c - '0'; 4974 } 4975 } 4976 4977 foreach(i; 0 .. rle) { 4978 if(arg[0] >= 'a' && arg[0] <= 'z') 4979 data[didx] = cast(ubyte)(arg[0] - 'a'); 4980 4981 didx++; 4982 if(didx == data.length) 4983 break outer; 4984 } 4985 4986 arg = arg[1 .. $]; 4987 } 4988 } 4989 4990 // width, height, palette, data is set up now 4991 4992 if(palette.length) { 4993 auto ii = new IndexedImage(width, height); 4994 ii.palette = palette; 4995 ii.data = data.dup; 4996 4997 return ii; 4998 }// else assert(0, origArg); 4999 return null; 5000 } 5001 5002 5003 // workaround dmd bug fixed in next release 5004 //static immutable Color[256] xtermPalette = [ 5005 immutable(Color)[] xtermPalette() { 5006 5007 // This is an approximation too for a few entries, but a very close one. 5008 Color xtermPaletteIndexToColor(int paletteIdx) { 5009 Color color; 5010 color.a = 255; 5011 5012 if(paletteIdx < 16) { 5013 if(paletteIdx == 7) 5014 return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see 5015 else if(paletteIdx == 8) 5016 return Color(0x80, 0x80, 0x80); 5017 5018 // real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see 5019 color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00; 5020 color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00; 5021 color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00; 5022 5023 } else if(paletteIdx < 232) { 5024 // color ramp, 6x6x6 cube 5025 color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); 5026 color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); 5027 color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); 5028 5029 if(color.r == 55) color.r = 0; 5030 if(color.g == 55) color.g = 0; 5031 if(color.b == 55) color.b = 0; 5032 } else { 5033 // greyscale ramp, from 0x8 to 0xee 5034 color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); 5035 color.g = color.r; 5036 color.b = color.g; 5037 } 5038 5039 return color; 5040 } 5041 5042 static immutable(Color)[] ret; 5043 if(ret.length == 256) 5044 return ret; 5045 5046 ret.reserve(256); 5047 foreach(i; 0 .. 256) 5048 ret ~= xtermPaletteIndexToColor(i); 5049 5050 return ret; 5051 } 5052 5053 static shared immutable dchar[dchar] lineDrawingCharacterSet; 5054 shared static this() { 5055 lineDrawingCharacterSet = [ 5056 'a' : ':', 5057 'j' : '+', 5058 'k' : '+', 5059 'l' : '+', 5060 'm' : '+', 5061 'n' : '+', 5062 'q' : '-', 5063 't' : '+', 5064 'u' : '+', 5065 'v' : '+', 5066 'w' : '+', 5067 'x' : '|', 5068 ]; 5069 5070 // this is what they SHOULD be but the font i use doesn't support all these 5071 // the ascii fallback above looks pretty good anyway though. 5072 version(none) 5073 lineDrawingCharacterSet = [ 5074 'a' : '\u2592', 5075 'j' : '\u2518', 5076 'k' : '\u2510', 5077 'l' : '\u250c', 5078 'm' : '\u2514', 5079 'n' : '\u253c', 5080 'q' : '\u2500', 5081 't' : '\u251c', 5082 'u' : '\u2524', 5083 'v' : '\u2534', 5084 'w' : '\u252c', 5085 'x' : '\u2502', 5086 ]; 5087 } 5088 5089 /+ 5090 Copyright: Adam D. Ruppe, 2013 - 2020 5091 License: [http://www.boost.org/LICENSE_1_0.txt|Boost Software License 1.0] 5092 Authors: Adam D. Ruppe 5093 +/