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