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