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