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