1 /** 2 * Module for supporting cursor and color manipulation on the console. 3 * 4 * The main interface for this module is the Terminal struct, which 5 * encapsulates the functions of the terminal. Creating an instance of 6 * this struct will perform console initialization; when the struct 7 * goes out of scope, any changes in console settings will be automatically 8 * reverted. 9 * 10 * Note: on Posix, it traps SIGINT and translates it into an input event. You should 11 * keep your event loop moving and keep an eye open for this to exit cleanly; simply break 12 * your event loop upon receiving a UserInterruptionEvent. (Without 13 * the signal handler, ctrl+c can leave your terminal in a bizarre state.) 14 * 15 * As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\ 16 */ 17 module terminal; 18 19 // FIXME: ctrl+d eof on stdin 20 21 // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx 22 23 version(linux) 24 enum SIGWINCH = 28; // FIXME: confirm this is correct on other posix 25 26 version(Posix) { 27 __gshared bool windowSizeChanged = false; 28 __gshared bool interrupted = false; /// you might periodically check this in a long operation and abort if it is set. Remember it is volatile. It is also sent through the input event loop via RealTimeConsoleInput 29 __gshared bool hangedUp = false; /// similar to interrupted. 30 31 version(with_eventloop) 32 struct SignalFired {} 33 34 extern(C) 35 void sizeSignalHandler(int sigNumber) nothrow { 36 windowSizeChanged = true; 37 version(with_eventloop) { 38 import arsd.eventloop; 39 try 40 send(SignalFired()); 41 catch(Exception) {} 42 } 43 } 44 extern(C) 45 void interruptSignalHandler(int sigNumber) nothrow { 46 interrupted = true; 47 version(with_eventloop) { 48 import arsd.eventloop; 49 try 50 send(SignalFired()); 51 catch(Exception) {} 52 } 53 } 54 extern(C) 55 void hangupSignalHandler(int sigNumber) nothrow { 56 hangedUp = true; 57 version(with_eventloop) { 58 import arsd.eventloop; 59 try 60 send(SignalFired()); 61 catch(Exception) {} 62 } 63 } 64 65 } 66 67 // parts of this were taken from Robik's ConsoleD 68 // https://github.com/robik/ConsoleD/blob/master/consoled.d 69 70 // Uncomment this line to get a main() to demonstrate this module's 71 // capabilities. 72 //version = Demo 73 74 version(Windows) { 75 import core.sys.windows.windows; 76 import std..string : toStringz; 77 private { 78 enum RED_BIT = 4; 79 enum GREEN_BIT = 2; 80 enum BLUE_BIT = 1; 81 } 82 } 83 84 version(Posix) { 85 } 86 87 enum Bright = 0x08; 88 89 /// Defines the list of standard colors understood by Terminal. 90 enum Color : ushort { 91 black = 0, /// . 92 red = RED_BIT, /// . 93 green = GREEN_BIT, /// . 94 yellow = red | green, /// . 95 blue = BLUE_BIT, /// . 96 magenta = red | blue, /// . 97 cyan = blue | green, /// . 98 white = red | green | blue, /// . 99 DEFAULT = 256, 100 } 101 102 /// When capturing input, what events are you interested in? 103 /// 104 /// Note: these flags can be OR'd together to select more than one option at a time. 105 /// 106 /// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw. 107 /// The rationale for that is to ensure the Terminal destructor has a chance to run, since the terminal is a shared resource and should be put back before the program terminates. 108 enum ConsoleInputFlags { 109 raw = 0, /// raw input returns keystrokes immediately, without line buffering 110 echo = 1, /// do you want to automatically echo input back to the user? 111 mouse = 2, /// capture mouse events 112 paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes) 113 size = 8, /// window resize events 114 115 releasedKeys = 64, /// key release events. Not reliable on Posix. 116 117 allInputEvents = 8|4|2, /// subscribe to all input events. Note: in previous versions, this also returned release events. It no longer does, use allInputEventsWithRelease if you want them. 118 allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events. 119 } 120 121 /// Defines how terminal output should be handled. 122 enum ConsoleOutputType { 123 linear = 0, /// do you want output to work one line at a time? 124 cellular = 1, /// or do you want access to the terminal screen as a grid of characters? 125 //truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges 126 127 minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here 128 } 129 130 /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present 131 enum ForceOption { 132 automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) 133 neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution. 134 alwaysSend = 1, /// always send the data, even if it doesn't seem necessary 135 } 136 137 // we could do it with termcap too, getenv("TERMCAP") then split on : and replace \E with \033 and get the pieces 138 139 /// Encapsulates the I/O capabilities of a terminal. 140 /// 141 /// Warning: do not write out escape sequences to the terminal. This won't work 142 /// on Windows and will confuse Terminal's internal state on Posix. 143 struct Terminal { 144 // @disable this(); 145 @disable this(this); 146 private ConsoleOutputType type; 147 148 version(Posix) { 149 private int fdOut; 150 private int fdIn; 151 private int[] delegate() getSizeOverride; 152 } 153 154 version(Posix) { 155 bool terminalInFamily(string[] terms...) { 156 import std.process; 157 import std..string; 158 auto term = environment.get("TERM"); 159 foreach(t; terms) 160 if(indexOf(term, t) != -1) 161 return true; 162 163 return false; 164 } 165 166 static string[string] termcapDatabase; 167 static void readTermcapFile(bool useBuiltinTermcap = false) { 168 import std.file; 169 import std.stdio; 170 import std..string; 171 172 if(!exists("/etc/termcap")) 173 useBuiltinTermcap = true; 174 175 string current; 176 177 void commitCurrentEntry() { 178 if(current is null) 179 return; 180 181 string names = current; 182 auto idx = indexOf(names, ":"); 183 if(idx != -1) 184 names = names[0 .. idx]; 185 186 foreach(name; split(names, "|")) 187 termcapDatabase[name] = current; 188 189 current = null; 190 } 191 192 void handleTermcapLine(in char[] line) { 193 if(line.length == 0) { // blank 194 commitCurrentEntry(); 195 return; // continue 196 } 197 if(line[0] == '#') // comment 198 return; // continue 199 size_t termination = line.length; 200 if(line[$-1] == '\\') 201 termination--; // cut off the \\ 202 current ~= strip(line[0 .. termination]); 203 // termcap entries must be on one logical line, so if it isn't continued, we know we're done 204 if(line[$-1] != '\\') 205 commitCurrentEntry(); 206 } 207 208 if(useBuiltinTermcap) { 209 foreach(line; splitLines(builtinTermcap)) { 210 handleTermcapLine(line); 211 } 212 } else { 213 foreach(line; File("/etc/termcap").byLine()) { 214 handleTermcapLine(line); 215 } 216 } 217 } 218 219 static string getTermcapDatabase(string terminal) { 220 import std..string; 221 222 if(termcapDatabase is null) 223 readTermcapFile(); 224 225 auto data = terminal in termcapDatabase; 226 if(data is null) 227 return null; 228 229 auto tc = *data; 230 auto more = indexOf(tc, ":tc="); 231 if(more != -1) { 232 auto tcKey = tc[more + ":tc=".length .. $]; 233 auto end = indexOf(tcKey, ":"); 234 if(end != -1) 235 tcKey = tcKey[0 .. end]; 236 tc = getTermcapDatabase(tcKey) ~ tc; 237 } 238 239 return tc; 240 } 241 242 string[string] termcap; 243 void readTermcap() { 244 import std.process; 245 import std..string; 246 import std.array; 247 248 string termcapData = environment.get("TERMCAP"); 249 if(termcapData.length == 0) { 250 termcapData = getTermcapDatabase(environment.get("TERM")); 251 } 252 253 auto e = replace(termcapData, "\\\n", "\n"); 254 termcap = null; 255 256 foreach(part; split(e, ":")) { 257 // FIXME: handle numeric things too 258 259 auto things = split(part, "="); 260 if(things.length) 261 termcap[things[0]] = 262 things.length > 1 ? things[1] : null; 263 } 264 } 265 266 string findSequenceInTermcap(in char[] sequenceIn) { 267 char[10] sequenceBuffer; 268 char[] sequence; 269 if(sequenceIn.length > 0 && sequenceIn[0] == '\033') { 270 if(!(sequenceIn.length < sequenceBuffer.length - 1)) 271 return null; 272 sequenceBuffer[1 .. sequenceIn.length + 1] = sequenceIn[]; 273 sequenceBuffer[0] = '\\'; 274 sequenceBuffer[1] = 'E'; 275 sequence = sequenceBuffer[0 .. sequenceIn.length + 1]; 276 } else { 277 sequence = sequenceBuffer[1 .. sequenceIn.length + 1]; 278 } 279 280 import std.array; 281 foreach(k, v; termcap) 282 if(v == sequence) 283 return k; 284 return null; 285 } 286 287 string getTermcap(string key) { 288 auto k = key in termcap; 289 if(k !is null) return *k; 290 return null; 291 } 292 293 // Looks up a termcap item and tries to execute it. Returns false on failure 294 bool doTermcap(T...)(string key, T t) { 295 import std.conv; 296 auto fs = getTermcap(key); 297 if(fs is null) 298 return false; 299 300 int swapNextTwo = 0; 301 302 R getArg(R)(int idx) { 303 if(swapNextTwo == 2) { 304 idx ++; 305 swapNextTwo--; 306 } else if(swapNextTwo == 1) { 307 idx --; 308 swapNextTwo--; 309 } 310 311 foreach(i, arg; t) { 312 if(i == idx) 313 return to!R(arg); 314 } 315 assert(0, to!string(idx) ~ " is out of bounds working " ~ fs); 316 } 317 318 char[256] buffer; 319 int bufferPos = 0; 320 321 void addChar(char c) { 322 import std.exception; 323 enforce(bufferPos < buffer.length); 324 buffer[bufferPos++] = c; 325 } 326 327 void addString(in char[] c) { 328 import std.exception; 329 enforce(bufferPos + c.length < buffer.length); 330 buffer[bufferPos .. bufferPos + c.length] = c[]; 331 bufferPos += c.length; 332 } 333 334 void addInt(int c, int minSize) { 335 import std..string; 336 auto str = format("%0"~(minSize ? to!string(minSize) : "")~"d", c); 337 addString(str); 338 } 339 340 bool inPercent; 341 int argPosition = 0; 342 int incrementParams = 0; 343 bool skipNext; 344 bool nextIsChar; 345 bool inBackslash; 346 347 foreach(char c; fs) { 348 if(inBackslash) { 349 if(c == 'E') 350 addChar('\033'); 351 else 352 addChar(c); 353 inBackslash = false; 354 } else if(nextIsChar) { 355 if(skipNext) 356 skipNext = false; 357 else 358 addChar(cast(char) (c + getArg!int(argPosition) + (incrementParams ? 1 : 0))); 359 if(incrementParams) incrementParams--; 360 argPosition++; 361 inPercent = false; 362 } else if(inPercent) { 363 switch(c) { 364 case '%': 365 addChar('%'); 366 inPercent = false; 367 break; 368 case '2': 369 case '3': 370 case 'd': 371 if(skipNext) 372 skipNext = false; 373 else 374 addInt(getArg!int(argPosition) + (incrementParams ? 1 : 0), 375 c == 'd' ? 0 : (c - '0') 376 ); 377 if(incrementParams) incrementParams--; 378 argPosition++; 379 inPercent = false; 380 break; 381 case '.': 382 if(skipNext) 383 skipNext = false; 384 else 385 addChar(cast(char) (getArg!int(argPosition) + (incrementParams ? 1 : 0))); 386 if(incrementParams) incrementParams--; 387 argPosition++; 388 break; 389 case '+': 390 nextIsChar = true; 391 inPercent = false; 392 break; 393 case 'i': 394 incrementParams = 2; 395 inPercent = false; 396 break; 397 case 's': 398 skipNext = true; 399 inPercent = false; 400 break; 401 case 'b': 402 argPosition--; 403 inPercent = false; 404 break; 405 case 'r': 406 swapNextTwo = 2; 407 inPercent = false; 408 break; 409 // FIXME: there's more 410 // http://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html 411 412 default: 413 assert(0, "not supported " ~ c); 414 } 415 } else { 416 if(c == '%') 417 inPercent = true; 418 else if(c == '\\') 419 inBackslash = true; 420 else 421 addChar(c); 422 } 423 } 424 425 writeStringRaw(buffer[0 .. bufferPos]); 426 return true; 427 } 428 } 429 430 version(Posix) 431 /** 432 * Constructs an instance of Terminal representing the capabilities of 433 * the current terminal. 434 * 435 * While it is possible to override the stdin+stdout file descriptors, remember 436 * that is not portable across platforms and be sure you know what you're doing. 437 * 438 * ditto on getSizeOverride. That's there so you can do something instead of ioctl. 439 */ 440 this(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { 441 this.fdIn = fdIn; 442 this.fdOut = fdOut; 443 this.getSizeOverride = getSizeOverride; 444 this.type = type; 445 446 readTermcap(); 447 448 if(type == ConsoleOutputType.minimalProcessing) { 449 _suppressDestruction = true; 450 return; 451 } 452 453 if(type == ConsoleOutputType.cellular) { 454 doTermcap("ti"); 455 moveTo(0, 0, ForceOption.alwaysSend); // we need to know where the cursor is for some features to work, and moving it is easier than querying it 456 } 457 458 if(terminalInFamily("xterm", "rxvt", "screen")) { 459 writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it) 460 } 461 } 462 463 version(Windows) 464 HANDLE hConsole; 465 466 version(Windows) 467 /// ditto 468 this(ConsoleOutputType type) { 469 hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 470 if(type == ConsoleOutputType.cellular) { 471 /* 472 http://msdn.microsoft.com/en-us/library/windows/desktop/ms686125%28v=vs.85%29.aspx 473 http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.aspx 474 */ 475 COORD size; 476 /* 477 CONSOLE_SCREEN_BUFFER_INFO sbi; 478 GetConsoleScreenBufferInfo(hConsole, &sbi); 479 size.X = cast(short) GetSystemMetrics(SM_CXMIN); 480 size.Y = cast(short) GetSystemMetrics(SM_CYMIN); 481 */ 482 483 // FIXME: this sucks, maybe i should just revert it. but there shouldn't be scrollbars in cellular mode 484 size.X = 80; 485 size.Y = 24; 486 SetConsoleScreenBufferSize(hConsole, size); 487 moveTo(0, 0, ForceOption.alwaysSend); // we need to know where the cursor is for some features to work, and moving it is easier than querying it 488 } 489 } 490 491 // only use this if you are sure you know what you want, since the terminal is a shared resource you generally really want to reset it to normal when you leave... 492 bool _suppressDestruction; 493 494 version(Posix) 495 ~this() { 496 if(_suppressDestruction) { 497 flush(); 498 return; 499 } 500 if(type == ConsoleOutputType.cellular) { 501 doTermcap("te"); 502 } 503 if(terminalInFamily("xterm", "rxvt", "screen")) { 504 writeStringRaw("\033[23;0t"); // restore window title from the stack 505 } 506 showCursor(); 507 reset(); 508 flush(); 509 510 if(lineGetter !is null) 511 lineGetter.dispose(); 512 } 513 514 version(Windows) 515 ~this() { 516 reset(); 517 flush(); 518 showCursor(); 519 520 if(lineGetter !is null) 521 lineGetter.dispose(); 522 } 523 524 // lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit) 525 // and some history storage. 526 LineGetter lineGetter; 527 528 int _currentForeground = Color.DEFAULT; 529 int _currentBackground = Color.DEFAULT; 530 bool reverseVideo = false; 531 532 /// Changes the current color. See enum Color for the values. 533 void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { 534 if(force != ForceOption.neverSend) { 535 version(Windows) { 536 // assuming a dark background on windows, so LowContrast == dark which means the bit is NOT set on hardware 537 /* 538 foreground ^= LowContrast; 539 background ^= LowContrast; 540 */ 541 542 ushort setTof = cast(ushort) foreground; 543 ushort setTob = cast(ushort) background; 544 545 // this isn't necessarily right but meh 546 if(background == Color.DEFAULT) 547 setTob = Color.black; 548 if(foreground == Color.DEFAULT) 549 setTof = Color.white; 550 551 if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 552 flush(); // if we don't do this now, the buffering can screw up the colors... 553 if(reverseVideo) { 554 if(background == Color.DEFAULT) 555 setTof = Color.black; 556 else 557 setTof = cast(ushort) background | (foreground & Bright); 558 559 if(background == Color.DEFAULT) 560 setTob = Color.white; 561 else 562 setTob = cast(ushort) (foreground & ~Bright); 563 } 564 SetConsoleTextAttribute( 565 GetStdHandle(STD_OUTPUT_HANDLE), 566 cast(ushort)((setTob << 4) | setTof)); 567 } 568 } else { 569 import std.process; 570 // I started using this envvar for my text editor, but now use it elsewhere too 571 // if we aren't set to dark, assume light 572 /* 573 if(getenv("ELVISBG") == "dark") { 574 // LowContrast on dark bg menas 575 } else { 576 foreground ^= LowContrast; 577 background ^= LowContrast; 578 } 579 */ 580 581 ushort setTof = cast(ushort) foreground & ~Bright; 582 ushort setTob = cast(ushort) background & ~Bright; 583 584 if(foreground & Color.DEFAULT) 585 setTof = 9; // ansi sequence for reset 586 if(background == Color.DEFAULT) 587 setTob = 9; 588 589 import std..string; 590 591 if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 592 writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm\033[%dm", 593 (foreground != Color.DEFAULT && (foreground & Bright)) ? 1 : 0, 594 cast(int) setTof, 595 cast(int) setTob, 596 reverseVideo ? 7 : 27 597 )); 598 } 599 } 600 } 601 602 _currentForeground = foreground; 603 _currentBackground = background; 604 this.reverseVideo = reverseVideo; 605 } 606 607 private bool _underlined = false; 608 609 /// Note: the Windows console does not support underlining 610 void underline(bool set, ForceOption force = ForceOption.automatic) { 611 if(set == _underlined && force != ForceOption.alwaysSend) 612 return; 613 version(Posix) { 614 if(set) 615 writeStringRaw("\033[4m"); 616 else 617 writeStringRaw("\033[24m"); 618 } 619 _underlined = set; 620 } 621 // FIXME: do I want to do bold and italic? 622 623 /// Returns the terminal to normal output colors 624 void reset() { 625 version(Windows) 626 SetConsoleTextAttribute( 627 GetStdHandle(STD_OUTPUT_HANDLE), 628 cast(ushort)((Color.black << 4) | Color.white)); 629 else 630 writeStringRaw("\033[0m"); 631 632 _underlined = false; 633 _currentForeground = Color.DEFAULT; 634 _currentBackground = Color.DEFAULT; 635 reverseVideo = false; 636 } 637 638 // FIXME: add moveRelative 639 640 /// The current x position of the output cursor. 0 == leftmost column 641 @property int cursorX() { 642 return _cursorX; 643 } 644 645 /// The current y position of the output cursor. 0 == topmost row 646 @property int cursorY() { 647 return _cursorY; 648 } 649 650 private int _cursorX; 651 private int _cursorY; 652 653 /// Moves the output cursor to the given position. (0, 0) is the upper left corner of the screen. The force parameter can be used to force an update, even if Terminal doesn't think it is necessary 654 void moveTo(int x, int y, ForceOption force = ForceOption.automatic) { 655 if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) { 656 executeAutoHideCursor(); 657 version(Posix) 658 doTermcap("cm", y, x); 659 else version(Windows) { 660 661 flush(); // if we don't do this now, the buffering can screw up the position 662 COORD coord = {cast(short) x, cast(short) y}; 663 SetConsoleCursorPosition(hConsole, coord); 664 } else static assert(0); 665 } 666 667 _cursorX = x; 668 _cursorY = y; 669 } 670 671 /// shows the cursor 672 void showCursor() { 673 version(Posix) 674 doTermcap("ve"); 675 else { 676 CONSOLE_CURSOR_INFO info; 677 GetConsoleCursorInfo(hConsole, &info); 678 info.bVisible = true; 679 SetConsoleCursorInfo(hConsole, &info); 680 } 681 } 682 683 /// hides the cursor 684 void hideCursor() { 685 version(Posix) { 686 doTermcap("vi"); 687 } else { 688 CONSOLE_CURSOR_INFO info; 689 GetConsoleCursorInfo(hConsole, &info); 690 info.bVisible = false; 691 SetConsoleCursorInfo(hConsole, &info); 692 } 693 694 } 695 696 private bool autoHidingCursor; 697 private bool autoHiddenCursor; 698 // explicitly not publicly documented 699 // Sets the cursor to automatically insert a hide command at the front of the output buffer iff it is moved. 700 // Call autoShowCursor when you are done with the batch update. 701 void autoHideCursor() { 702 autoHidingCursor = true; 703 } 704 705 private void executeAutoHideCursor() { 706 if(autoHidingCursor) { 707 version(Windows) 708 hideCursor(); 709 else version(Posix) { 710 // prepend the hide cursor command so it is the first thing flushed 711 writeBuffer = "\033[?25l" ~ writeBuffer; 712 } 713 714 autoHiddenCursor = true; 715 autoHidingCursor = false; // already been done, don't insert the command again 716 } 717 } 718 719 // explicitly not publicly documented 720 // Shows the cursor if it was automatically hidden by autoHideCursor and resets the internal auto hide state. 721 void autoShowCursor() { 722 if(autoHiddenCursor) 723 showCursor(); 724 725 autoHidingCursor = false; 726 autoHiddenCursor = false; 727 } 728 729 /* 730 // alas this doesn't work due to a bunch of delegate context pointer and postblit problems 731 // instead of using: auto input = terminal.captureInput(flags) 732 // use: auto input = RealTimeConsoleInput(&terminal, flags); 733 /// Gets real time input, disabling line buffering 734 RealTimeConsoleInput captureInput(ConsoleInputFlags flags) { 735 return RealTimeConsoleInput(&this, flags); 736 } 737 */ 738 739 /// Changes the terminal's title 740 void setTitle(string t) { 741 version(Windows) { 742 SetConsoleTitleA(toStringz(t)); 743 } else { 744 import std..string; 745 if(terminalInFamily("xterm", "rxvt", "screen")) 746 writeStringRaw(format("\033]0;%s\007", t)); 747 } 748 } 749 750 /// Flushes your updates to the terminal. 751 /// It is important to call this when you are finished writing for now if you are using the version=with_eventloop 752 void flush() { 753 version(Posix) { 754 ssize_t written; 755 756 while(writeBuffer.length) { 757 written = unix.write(this.fdOut, writeBuffer.ptr, writeBuffer.length); 758 if(written < 0) 759 throw new Exception("write failed for some reason"); 760 writeBuffer = writeBuffer[written .. $]; 761 } 762 } else version(Windows) { 763 while(writeBuffer.length) { 764 DWORD written; 765 /* FIXME: WriteConsoleW */ 766 WriteConsoleA(hConsole, writeBuffer.ptr, writeBuffer.length, &written, null); 767 writeBuffer = writeBuffer[written .. $]; 768 } 769 } 770 // not buffering right now on Windows, since it probably isn't on ssh anyway 771 } 772 773 int[] getSize() { 774 version(Windows) { 775 CONSOLE_SCREEN_BUFFER_INFO info; 776 GetConsoleScreenBufferInfo( hConsole, &info ); 777 778 int cols, rows; 779 780 cols = (info.srWindow.Right - info.srWindow.Left + 1); 781 rows = (info.srWindow.Bottom - info.srWindow.Top + 1); 782 783 return [cols, rows]; 784 } else { 785 if(getSizeOverride is null) { 786 winsize w; 787 ioctl(0, TIOCGWINSZ, &w); 788 return [w.ws_col, w.ws_row]; 789 } else return getSizeOverride(); 790 } 791 } 792 793 void updateSize() { 794 auto size = getSize(); 795 _width = size[0]; 796 _height = size[1]; 797 } 798 799 private int _width; 800 private int _height; 801 802 /// The current width of the terminal (the number of columns) 803 @property int width() { 804 if(_width == 0 || _height == 0) 805 updateSize(); 806 return _width; 807 } 808 809 /// The current height of the terminal (the number of rows) 810 @property int height() { 811 if(_width == 0 || _height == 0) 812 updateSize(); 813 return _height; 814 } 815 816 /* 817 void write(T...)(T t) { 818 foreach(arg; t) { 819 writeStringRaw(to!string(arg)); 820 } 821 } 822 */ 823 824 /// Writes to the terminal at the current cursor position. 825 void writef(T...)(string f, T t) { 826 import std..string; 827 writePrintableString(format(f, t)); 828 } 829 830 /// ditto 831 void writefln(T...)(string f, T t) { 832 writef(f ~ "\n", t); 833 } 834 835 /// ditto 836 void write(T...)(T t) { 837 import std.conv; 838 string data; 839 foreach(arg; t) { 840 data ~= to!string(arg); 841 } 842 843 writePrintableString(data); 844 } 845 846 /// ditto 847 void writeln(T...)(T t) { 848 write(t, "\n"); 849 } 850 851 /+ 852 /// A combined moveTo and writef that puts the cursor back where it was before when it finishes the write. 853 /// Only works in cellular mode. 854 /// Might give better performance than moveTo/writef because if the data to write matches the internal buffer, it skips sending anything (to override the buffer check, you can use moveTo and writePrintableString with ForceOption.alwaysSend) 855 void writefAt(T...)(int x, int y, string f, T t) { 856 import std.string; 857 auto toWrite = format(f, t); 858 859 auto oldX = _cursorX; 860 auto oldY = _cursorY; 861 862 writeAtWithoutReturn(x, y, toWrite); 863 864 moveTo(oldX, oldY); 865 } 866 867 void writeAtWithoutReturn(int x, int y, in char[] data) { 868 moveTo(x, y); 869 writeStringRaw(toWrite, ForceOption.alwaysSend); 870 } 871 +/ 872 873 void writePrintableString(in char[] s, ForceOption force = ForceOption.automatic) { 874 // an escape character is going to mess things up. Actually any non-printable character could, but meh 875 // assert(s.indexOf("\033") == -1); 876 877 // tracking cursor position 878 foreach(ch; s) { 879 switch(ch) { 880 case '\n': 881 _cursorX = 0; 882 _cursorY++; 883 break; 884 case '\r': 885 _cursorX = 0; 886 break; 887 case '\t': 888 _cursorX ++; 889 _cursorX += _cursorX % 8; // FIXME: get the actual tabstop, if possible 890 break; 891 default: 892 if(ch <= 127) // way of only advancing once per dchar instead of per code unit 893 _cursorX++; 894 } 895 896 if(_wrapAround && _cursorX > width) { 897 _cursorX = 0; 898 _cursorY++; 899 } 900 901 if(_cursorY == height) 902 _cursorY--; 903 904 /+ 905 auto index = getIndex(_cursorX, _cursorY); 906 if(data[index] != ch) { 907 data[index] = ch; 908 } 909 +/ 910 } 911 912 writeStringRaw(s); 913 } 914 915 /* private */ bool _wrapAround = true; 916 917 deprecated alias writePrintableString writeString; /// use write() or writePrintableString instead 918 919 private string writeBuffer; 920 921 // you really, really shouldn't use this unless you know what you are doing 922 /*private*/ void writeStringRaw(in char[] s) { 923 // FIXME: make sure all the data is sent, check for errors 924 version(Posix) { 925 writeBuffer ~= s; // buffer it to do everything at once in flush() calls 926 } else version(Windows) { 927 writeBuffer ~= s; 928 } else static assert(0); 929 } 930 931 /// Clears the screen. 932 void clear() { 933 version(Posix) { 934 doTermcap("cl"); 935 } else version(Windows) { 936 // TBD: copy the code from here and test it: 937 // http://support.microsoft.com/kb/99261 938 assert(0, "clear not yet implemented"); 939 } 940 941 _cursorX = 0; 942 _cursorY = 0; 943 } 944 945 /// gets a line, including user editing. Convenience method around the LineGetter class and RealTimeConsoleInput facilities - use them if you need more control. 946 /// You really shouldn't call this if stdin isn't actually a user-interactive terminal! So if you expect people to pipe data to your app, check for that or use something else. 947 // FIXME: add a method to make it easy to check if stdin is actually a tty and use other methods there. 948 string getline(string prompt = null) { 949 if(lineGetter is null) 950 lineGetter = new LineGetter(&this); 951 // since the struct might move (it shouldn't, this should be unmovable!) but since 952 // it technically might, I'm updating the pointer before using it just in case. 953 lineGetter.terminal = &this; 954 955 lineGetter.prompt = prompt; 956 957 auto line = lineGetter.getline(); 958 959 // lineGetter leaves us exactly where it was when the user hit enter, giving best 960 // flexibility to real-time input and cellular programs. The convenience function, 961 // however, wants to do what is right in most the simple cases, which is to actually 962 // print the line (echo would be enabled without RealTimeConsoleInput anyway and they 963 // did hit enter), so we'll do that here too. 964 writePrintableString("\n"); 965 966 return line; 967 } 968 969 } 970 971 /+ 972 struct ConsoleBuffer { 973 int cursorX; 974 int cursorY; 975 int width; 976 int height; 977 dchar[] data; 978 979 void actualize(Terminal* t) { 980 auto writer = t.getBufferedWriter(); 981 982 this.copyTo(&(t.onScreen)); 983 } 984 985 void copyTo(ConsoleBuffer* buffer) { 986 buffer.cursorX = this.cursorX; 987 buffer.cursorY = this.cursorY; 988 buffer.width = this.width; 989 buffer.height = this.height; 990 buffer.data[] = this.data[]; 991 } 992 } 993 +/ 994 995 /** 996 * Encapsulates the stream of input events received from the terminal input. 997 */ 998 struct RealTimeConsoleInput { 999 @disable this(); 1000 @disable this(this); 1001 1002 version(Posix) { 1003 private int fdOut; 1004 private int fdIn; 1005 private sigaction_t oldSigWinch; 1006 private sigaction_t oldSigIntr; 1007 private sigaction_t oldHupIntr; 1008 private termios old; 1009 ubyte[128] hack; 1010 // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... 1011 // tcgetattr smashed other variables in here too that could create random problems 1012 // so this hack is just to give some room for that to happen without destroying the rest of the world 1013 } 1014 1015 version(Windows) { 1016 private DWORD oldInput; 1017 private DWORD oldOutput; 1018 HANDLE inputHandle; 1019 } 1020 1021 private ConsoleInputFlags flags; 1022 private Terminal* terminal; 1023 private void delegate()[] destructor; 1024 1025 /// To capture input, you need to provide a terminal and some flags. 1026 public this(Terminal* terminal, ConsoleInputFlags flags) { 1027 this.flags = flags; 1028 this.terminal = terminal; 1029 1030 version(Windows) { 1031 inputHandle = GetStdHandle(STD_INPUT_HANDLE); 1032 1033 GetConsoleMode(inputHandle, &oldInput); 1034 1035 DWORD mode = 0; 1036 mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C which we probably want to be similar to linux 1037 //if(flags & ConsoleInputFlags.size) 1038 mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc 1039 if(flags & ConsoleInputFlags.echo) 1040 mode |= ENABLE_ECHO_INPUT; // 0x4 1041 if(flags & ConsoleInputFlags.mouse) 1042 mode |= ENABLE_MOUSE_INPUT; // 0x10 1043 // if(flags & ConsoleInputFlags.raw) // FIXME: maybe that should be a separate flag for ENABLE_LINE_INPUT 1044 1045 SetConsoleMode(inputHandle, mode); 1046 destructor ~= { SetConsoleMode(inputHandle, oldInput); }; 1047 1048 1049 GetConsoleMode(terminal.hConsole, &oldOutput); 1050 mode = 0; 1051 // we want this to match linux too 1052 mode |= ENABLE_PROCESSED_OUTPUT; /* 0x01 */ 1053 mode |= ENABLE_WRAP_AT_EOL_OUTPUT; /* 0x02 */ 1054 SetConsoleMode(terminal.hConsole, mode); 1055 destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); }; 1056 1057 // FIXME: change to UTF8 as well 1058 } 1059 1060 version(Posix) { 1061 this.fdIn = terminal.fdIn; 1062 this.fdOut = terminal.fdOut; 1063 1064 if(fdIn != -1) { 1065 tcgetattr(fdIn, &old); 1066 auto n = old; 1067 1068 auto f = ICANON; 1069 if(!(flags & ConsoleInputFlags.echo)) 1070 f |= ECHO; 1071 1072 n.c_lflag &= ~f; 1073 tcsetattr(fdIn, TCSANOW, &n); 1074 } 1075 1076 // some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3 1077 //destructor ~= { tcsetattr(fdIn, TCSANOW, &old); }; 1078 1079 if(flags & ConsoleInputFlags.size) { 1080 import core.sys.posix.signal; 1081 sigaction_t n; 1082 n.sa_handler = &sizeSignalHandler; 1083 n.sa_mask = cast(sigset_t) 0; 1084 n.sa_flags = 0; 1085 sigaction(SIGWINCH, &n, &oldSigWinch); 1086 } 1087 1088 { 1089 import core.sys.posix.signal; 1090 sigaction_t n; 1091 n.sa_handler = &interruptSignalHandler; 1092 n.sa_mask = cast(sigset_t) 0; 1093 n.sa_flags = 0; 1094 sigaction(SIGINT, &n, &oldSigIntr); 1095 } 1096 1097 { 1098 import core.sys.posix.signal; 1099 sigaction_t n; 1100 n.sa_handler = &hangupSignalHandler; 1101 n.sa_mask = cast(sigset_t) 0; 1102 n.sa_flags = 0; 1103 sigaction(SIGHUP, &n, &oldHupIntr); 1104 } 1105 1106 1107 1108 if(flags & ConsoleInputFlags.mouse) { 1109 // basic button press+release notification 1110 1111 // FIXME: try to get maximum capabilities from all terminals 1112 // right now this works well on xterm but rxvt isn't sending movements... 1113 1114 terminal.writeStringRaw("\033[?1000h"); 1115 destructor ~= { terminal.writeStringRaw("\033[?1000l"); }; 1116 if(terminal.terminalInFamily("xterm")) { 1117 // this is vt200 mouse with full motion tracking, supported by xterm 1118 terminal.writeStringRaw("\033[?1003h"); 1119 destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; 1120 } else if(terminal.terminalInFamily("rxvt", "screen")) { 1121 terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed 1122 destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; 1123 } 1124 } 1125 if(flags & ConsoleInputFlags.paste) { 1126 if(terminal.terminalInFamily("xterm", "rxvt", "screen")) { 1127 terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode 1128 destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; 1129 } 1130 } 1131 1132 // try to ensure the terminal is in UTF-8 mode 1133 if(terminal.terminalInFamily("xterm", "screen", "linux")) { 1134 terminal.writeStringRaw("\033%G"); 1135 } 1136 1137 terminal.flush(); 1138 } 1139 1140 1141 version(with_eventloop) { 1142 import arsd.eventloop; 1143 version(Windows) 1144 auto listenTo = inputHandle; 1145 else version(Posix) 1146 auto listenTo = this.fdIn; 1147 else static assert(0, "idk about this OS"); 1148 1149 version(Posix) 1150 addListener(&signalFired); 1151 1152 if(listenTo != -1) { 1153 addFileEventListeners(listenTo, &eventListener, null, null); 1154 destructor ~= { removeFileEventListeners(listenTo); }; 1155 } 1156 addOnIdle(&terminal.flush); 1157 destructor ~= { removeOnIdle(&terminal.flush); }; 1158 } 1159 } 1160 1161 version(with_eventloop) { 1162 version(Posix) 1163 void signalFired(SignalFired) { 1164 if(interrupted) { 1165 interrupted = false; 1166 send(InputEvent(UserInterruptionEvent())); 1167 } 1168 if(windowSizeChanged) 1169 send(checkWindowSizeChanged()); 1170 if(hangedUp) { 1171 hangedUp = false; 1172 send(InputEvent(HangupEvent())); 1173 } 1174 } 1175 1176 import arsd.eventloop; 1177 void eventListener(OsFileHandle fd) { 1178 auto queue = readNextEvents(); 1179 foreach(event; queue) 1180 send(event); 1181 } 1182 } 1183 1184 ~this() { 1185 // the delegate thing doesn't actually work for this... for some reason 1186 version(Posix) 1187 if(fdIn != -1) 1188 tcsetattr(fdIn, TCSANOW, &old); 1189 1190 version(Posix) { 1191 if(flags & ConsoleInputFlags.size) { 1192 // restoration 1193 sigaction(SIGWINCH, &oldSigWinch, null); 1194 } 1195 sigaction(SIGINT, &oldSigIntr, null); 1196 sigaction(SIGHUP, &oldHupIntr, null); 1197 } 1198 1199 // we're just undoing everything the constructor did, in reverse order, same criteria 1200 foreach_reverse(d; destructor) 1201 d(); 1202 } 1203 1204 /// Returns true if there is input available now 1205 bool kbhit() { 1206 return timedCheckForInput(0); 1207 } 1208 1209 /// Check for input, waiting no longer than the number of milliseconds 1210 bool timedCheckForInput(int milliseconds) { 1211 version(Windows) { 1212 auto response = WaitForSingleObject(terminal.hConsole, milliseconds); 1213 if(response == 0) 1214 return true; // the object is ready 1215 return false; 1216 } else version(Posix) { 1217 if(fdIn == -1) 1218 return false; 1219 1220 timeval tv; 1221 tv.tv_sec = 0; 1222 tv.tv_usec = milliseconds * 1000; 1223 1224 fd_set fs; 1225 FD_ZERO(&fs); 1226 1227 FD_SET(fdIn, &fs); 1228 select(fdIn + 1, &fs, null, null, &tv); 1229 1230 return FD_ISSET(fdIn, &fs); 1231 } 1232 } 1233 1234 /// Get one character from the terminal, discarding other 1235 /// events in the process. Returns dchar.init upon receiving end-of-file. 1236 dchar getch() { 1237 auto event = nextEvent(); 1238 while(event.type != InputEvent.Type.CharacterEvent || event.characterEvent.eventType == CharacterEvent.Type.Released) { 1239 if(event.type == InputEvent.Type.UserInterruptionEvent) 1240 throw new Exception("Ctrl+c"); 1241 if(event.type == InputEvent.Type.HangupEvent) 1242 throw new Exception("Hangup"); 1243 if(event.type == InputEvent.Type.EndOfFileEvent) 1244 return dchar.init; 1245 event = nextEvent(); 1246 } 1247 return event.characterEvent.character; 1248 } 1249 1250 //char[128] inputBuffer; 1251 //int inputBufferPosition; 1252 version(Posix) 1253 int nextRaw(bool interruptable = false) { 1254 if(fdIn == -1) 1255 return 0; 1256 1257 char[1] buf; 1258 try_again: 1259 auto ret = read(fdIn, buf.ptr, buf.length); 1260 if(ret == 0) 1261 return 0; // input closed 1262 if(ret == -1) { 1263 import core.stdc.errno; 1264 if(errno == EINTR) 1265 // interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop 1266 if(interruptable) 1267 return -1; 1268 else 1269 goto try_again; 1270 else 1271 throw new Exception("read failed"); 1272 } 1273 1274 //terminal.writef("RAW READ: %d\n", buf[0]); 1275 1276 if(ret == 1) 1277 return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; 1278 else 1279 assert(0); // read too much, should be impossible 1280 } 1281 1282 version(Posix) 1283 int delegate(char) inputPrefilter; 1284 1285 version(Posix) 1286 dchar nextChar(int starting) { 1287 if(starting <= 127) 1288 return cast(dchar) starting; 1289 char[6] buffer; 1290 int pos = 0; 1291 buffer[pos++] = cast(char) starting; 1292 1293 // see the utf-8 encoding for details 1294 int remaining = 0; 1295 ubyte magic = starting & 0xff; 1296 while(magic & 0b1000_000) { 1297 remaining++; 1298 magic <<= 1; 1299 } 1300 1301 while(remaining && pos < buffer.length) { 1302 buffer[pos++] = cast(char) nextRaw(); 1303 remaining--; 1304 } 1305 1306 import std.utf; 1307 size_t throwAway; // it insists on the index but we don't care 1308 return decode(buffer[], throwAway); 1309 } 1310 1311 InputEvent checkWindowSizeChanged() { 1312 auto oldWidth = terminal.width; 1313 auto oldHeight = terminal.height; 1314 terminal.updateSize(); 1315 version(Posix) 1316 windowSizeChanged = false; 1317 return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height)); 1318 } 1319 1320 1321 // character event 1322 // non-character key event 1323 // paste event 1324 // mouse event 1325 // size event maybe, and if appropriate focus events 1326 1327 /// Returns the next event. 1328 /// 1329 /// Experimental: It is also possible to integrate this into 1330 /// a generic event loop, currently under -version=with_eventloop and it will 1331 /// require the module arsd.eventloop (Linux only at this point) 1332 InputEvent nextEvent() { 1333 terminal.flush(); 1334 if(inputQueue.length) { 1335 auto e = inputQueue[0]; 1336 inputQueue = inputQueue[1 .. $]; 1337 return e; 1338 } 1339 1340 wait_for_more: 1341 version(Posix) 1342 if(interrupted) { 1343 interrupted = false; 1344 return InputEvent(UserInterruptionEvent()); 1345 } 1346 1347 /* if(hangedUp) { 1348 hangedUp = false; 1349 return InputEvent(HangupEvent()); 1350 } 1351 */ 1352 version(Posix) 1353 if(windowSizeChanged) { 1354 return checkWindowSizeChanged(); 1355 } 1356 1357 auto more = readNextEvents(); 1358 if(!more.length) 1359 goto wait_for_more; // i used to do a loop (readNextEvents can read something, but it might be discarded by the input filter) but now it goto's above because readNextEvents might be interrupted by a SIGWINCH aka size event so we want to check that at least 1360 1361 assert(more.length); 1362 1363 auto e = more[0]; 1364 inputQueue = more[1 .. $]; 1365 return e; 1366 } 1367 1368 InputEvent* peekNextEvent() { 1369 if(inputQueue.length) 1370 return &(inputQueue[0]); 1371 return null; 1372 } 1373 1374 enum InjectionPosition { head, tail } 1375 void injectEvent(InputEvent ev, InjectionPosition where) { 1376 final switch(where) { 1377 case InjectionPosition.head: 1378 inputQueue = ev ~ inputQueue; 1379 break; 1380 case InjectionPosition.tail: 1381 inputQueue ~= ev; 1382 break; 1383 } 1384 } 1385 1386 InputEvent[] inputQueue; 1387 1388 version(Windows) 1389 InputEvent[] readNextEvents() { 1390 terminal.flush(); // make sure all output is sent out before waiting for anything 1391 1392 INPUT_RECORD[32] buffer; 1393 DWORD actuallyRead; 1394 // FIXME: ReadConsoleInputW 1395 auto success = ReadConsoleInputA(inputHandle, buffer.ptr, buffer.length, &actuallyRead); 1396 if(success == 0) 1397 throw new Exception("ReadConsoleInput"); 1398 1399 InputEvent[] newEvents; 1400 input_loop: foreach(record; buffer[0 .. actuallyRead]) { 1401 switch(record.EventType) { 1402 case KEY_EVENT: 1403 auto ev = record.KeyEvent; 1404 CharacterEvent e; 1405 NonCharacterKeyEvent ne; 1406 1407 e.eventType = ev.bKeyDown ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; 1408 ne.eventType = ev.bKeyDown ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released; 1409 1410 // only send released events when specifically requested 1411 if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) 1412 break; 1413 1414 e.modifierState = ev.dwControlKeyState; 1415 ne.modifierState = ev.dwControlKeyState; 1416 1417 if(ev.UnicodeChar) { 1418 e.character = cast(dchar) cast(wchar) ev.UnicodeChar; 1419 newEvents ~= InputEvent(e); 1420 } else { 1421 ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; 1422 1423 // FIXME: make this better. the goal is to make sure the key code is a valid enum member 1424 // Windows sends more keys than Unix and we're doing lowest common denominator here 1425 foreach(member; __traits(allMembers, NonCharacterKeyEvent.Key)) 1426 if(__traits(getMember, NonCharacterKeyEvent.Key, member) == ne.key) { 1427 newEvents ~= InputEvent(ne); 1428 break; 1429 } 1430 } 1431 break; 1432 case MOUSE_EVENT: 1433 auto ev = record.MouseEvent; 1434 MouseEvent e; 1435 1436 e.modifierState = ev.dwControlKeyState; 1437 e.x = ev.dwMousePosition.X; 1438 e.y = ev.dwMousePosition.Y; 1439 1440 switch(ev.dwEventFlags) { 1441 case 0: 1442 //press or release 1443 e.eventType = MouseEvent.Type.Pressed; 1444 static DWORD lastButtonState; 1445 auto lastButtonState2 = lastButtonState; 1446 e.buttons = ev.dwButtonState; 1447 lastButtonState = e.buttons; 1448 1449 // this is sent on state change. if fewer buttons are pressed, it must mean released 1450 if(cast(DWORD) e.buttons < lastButtonState2) { 1451 e.eventType = MouseEvent.Type.Released; 1452 // if last was 101 and now it is 100, then button far right was released 1453 // so we flip the bits, ~100 == 011, then and them: 101 & 011 == 001, the 1454 // button that was released 1455 e.buttons = lastButtonState2 & ~e.buttons; 1456 } 1457 break; 1458 case MOUSE_MOVED: 1459 e.eventType = MouseEvent.Type.Moved; 1460 e.buttons = ev.dwButtonState; 1461 break; 1462 case 0x0004/*MOUSE_WHEELED*/: 1463 e.eventType = MouseEvent.Type.Pressed; 1464 if(ev.dwButtonState > 0) 1465 e.buttons = MouseEvent.Button.ScrollDown; 1466 else 1467 e.buttons = MouseEvent.Button.ScrollUp; 1468 break; 1469 default: 1470 continue input_loop; 1471 } 1472 1473 newEvents ~= InputEvent(e); 1474 break; 1475 case WINDOW_BUFFER_SIZE_EVENT: 1476 auto ev = record.WindowBufferSizeEvent; 1477 auto oldWidth = terminal.width; 1478 auto oldHeight = terminal.height; 1479 terminal._width = ev.dwSize.X; 1480 terminal._height = ev.dwSize.Y; 1481 newEvents ~= InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height)); 1482 break; 1483 // FIXME: can we catch ctrl+c here too? 1484 default: 1485 // ignore 1486 } 1487 } 1488 1489 return newEvents; 1490 } 1491 1492 version(Posix) 1493 InputEvent[] readNextEvents() { 1494 terminal.flush(); // make sure all output is sent out before we try to get input 1495 1496 InputEvent[] charPressAndRelease(dchar character) { 1497 if((flags & ConsoleInputFlags.releasedKeys)) 1498 return [ 1499 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)), 1500 InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0)), 1501 ]; 1502 else return [ InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0)) ]; 1503 } 1504 InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { 1505 if((flags & ConsoleInputFlags.releasedKeys)) 1506 return [ 1507 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)), 1508 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers)), 1509 ]; 1510 else return [ InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers)) ]; 1511 } 1512 1513 char[30] sequenceBuffer; 1514 1515 // this assumes you just read "\033[" 1516 char[] readEscapeSequence(char[] sequence) { 1517 int sequenceLength = 2; 1518 sequence[0] = '\033'; 1519 sequence[1] = '['; 1520 1521 while(sequenceLength < sequence.length) { 1522 auto n = nextRaw(); 1523 sequence[sequenceLength++] = cast(char) n; 1524 // I think a [ is supposed to termiate a CSI sequence 1525 // but the Linux console sends CSI[A for F1, so I'm 1526 // hacking it to accept that too 1527 if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) 1528 break; 1529 } 1530 1531 return sequence[0 .. sequenceLength]; 1532 } 1533 1534 InputEvent[] translateTermcapName(string cap) { 1535 switch(cap) { 1536 //case "k0": 1537 //return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 1538 case "k1": 1539 return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 1540 case "k2": 1541 return keyPressAndRelease(NonCharacterKeyEvent.Key.F2); 1542 case "k3": 1543 return keyPressAndRelease(NonCharacterKeyEvent.Key.F3); 1544 case "k4": 1545 return keyPressAndRelease(NonCharacterKeyEvent.Key.F4); 1546 case "k5": 1547 return keyPressAndRelease(NonCharacterKeyEvent.Key.F5); 1548 case "k6": 1549 return keyPressAndRelease(NonCharacterKeyEvent.Key.F6); 1550 case "k7": 1551 return keyPressAndRelease(NonCharacterKeyEvent.Key.F7); 1552 case "k8": 1553 return keyPressAndRelease(NonCharacterKeyEvent.Key.F8); 1554 case "k9": 1555 return keyPressAndRelease(NonCharacterKeyEvent.Key.F9); 1556 case "k;": 1557 case "k0": 1558 return keyPressAndRelease(NonCharacterKeyEvent.Key.F10); 1559 case "F1": 1560 return keyPressAndRelease(NonCharacterKeyEvent.Key.F11); 1561 case "F2": 1562 return keyPressAndRelease(NonCharacterKeyEvent.Key.F12); 1563 1564 1565 case "kb": 1566 return charPressAndRelease('\b'); 1567 case "kD": 1568 return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete); 1569 1570 case "kd": 1571 case "do": 1572 return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow); 1573 case "ku": 1574 case "up": 1575 return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow); 1576 case "kl": 1577 return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow); 1578 case "kr": 1579 case "nd": 1580 return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow); 1581 1582 case "kN": 1583 case "K5": 1584 return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown); 1585 case "kP": 1586 case "K2": 1587 return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp); 1588 1589 case "kh": 1590 case "K1": 1591 return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); 1592 case "kH": 1593 return keyPressAndRelease(NonCharacterKeyEvent.Key.End); 1594 case "kI": 1595 return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); 1596 default: 1597 // don't know it, just ignore 1598 //import std.stdio; 1599 //writeln(cap); 1600 } 1601 1602 return null; 1603 } 1604 1605 1606 InputEvent[] doEscapeSequence(in char[] sequence) { 1607 switch(sequence) { 1608 case "\033[200~": 1609 // bracketed paste begin 1610 // we want to keep reading until 1611 // "\033[201~": 1612 // and build a paste event out of it 1613 1614 1615 string data; 1616 for(;;) { 1617 auto n = nextRaw(); 1618 if(n == '\033') { 1619 n = nextRaw(); 1620 if(n == '[') { 1621 auto esc = readEscapeSequence(sequenceBuffer); 1622 if(esc == "\033[201~") { 1623 // complete! 1624 break; 1625 } else { 1626 // was something else apparently, but it is pasted, so keep it 1627 data ~= esc; 1628 } 1629 } else { 1630 data ~= '\033'; 1631 data ~= cast(char) n; 1632 } 1633 } else { 1634 data ~= cast(char) n; 1635 } 1636 } 1637 return [InputEvent(PasteEvent(data))]; 1638 case "\033[M": 1639 // mouse event 1640 auto buttonCode = nextRaw() - 32; 1641 // nextChar is commented because i'm not using UTF-8 mouse mode 1642 // cuz i don't think it is as widely supported 1643 auto x = cast(int) (/*nextChar*/(nextRaw())) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */ 1644 auto y = cast(int) (/*nextChar*/(nextRaw())) - 33; /* ditto */ 1645 1646 1647 bool isRelease = (buttonCode & 0b11) == 3; 1648 int buttonNumber; 1649 if(!isRelease) { 1650 buttonNumber = (buttonCode & 0b11); 1651 if(buttonCode & 64) 1652 buttonNumber += 3; // button 4 and 5 are sent as like button 1 and 2, but code | 64 1653 // so button 1 == button 4 here 1654 1655 // note: buttonNumber == 0 means button 1 at this point 1656 buttonNumber++; // hence this 1657 1658 1659 // apparently this considers middle to be button 2. but i want middle to be button 3. 1660 if(buttonNumber == 2) 1661 buttonNumber = 3; 1662 else if(buttonNumber == 3) 1663 buttonNumber = 2; 1664 } 1665 1666 auto modifiers = buttonCode & (0b0001_1100); 1667 // 4 == shift 1668 // 8 == meta 1669 // 16 == control 1670 1671 MouseEvent m; 1672 1673 if(buttonCode & 32) 1674 m.eventType = MouseEvent.Type.Moved; 1675 else 1676 m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; 1677 1678 // ugh, if no buttons are pressed, released and moved are indistinguishable... 1679 // so we'll count the buttons down, and if we get a release 1680 static int buttonsDown = 0; 1681 if(!isRelease && buttonNumber <= 3) // exclude wheel "presses"... 1682 buttonsDown++; 1683 1684 if(isRelease && m.eventType != MouseEvent.Type.Moved) { 1685 if(buttonsDown) 1686 buttonsDown--; 1687 else // no buttons down, so this should be a motion instead.. 1688 m.eventType = MouseEvent.Type.Moved; 1689 } 1690 1691 1692 if(buttonNumber == 0) 1693 m.buttons = 0; // we don't actually know :( 1694 else 1695 m.buttons = 1 << (buttonNumber - 1); // I prefer flags so that's how we do it 1696 m.x = x; 1697 m.y = y; 1698 m.modifierState = modifiers; 1699 1700 return [InputEvent(m)]; 1701 default: 1702 // look it up in the termcap key database 1703 auto cap = terminal.findSequenceInTermcap(sequence); 1704 if(cap !is null) { 1705 return translateTermcapName(cap); 1706 } else { 1707 if(terminal.terminalInFamily("xterm")) { 1708 import std.conv, std..string; 1709 auto terminator = sequence[$ - 1]; 1710 auto parts = sequence[2 .. $ - 1].split(";"); 1711 // parts[0] and terminator tells us the key 1712 // parts[1] tells us the modifierState 1713 1714 uint modifierState; 1715 1716 int modGot; 1717 if(parts.length > 1) 1718 modGot = to!int(parts[1]); 1719 mod_switch: switch(modGot) { 1720 case 2: modifierState |= ModifierState.shift; break; 1721 case 3: modifierState |= ModifierState.alt; break; 1722 case 4: modifierState |= ModifierState.shift | ModifierState.alt; break; 1723 case 5: modifierState |= ModifierState.control; break; 1724 case 6: modifierState |= ModifierState.shift | ModifierState.control; break; 1725 case 7: modifierState |= ModifierState.alt | ModifierState.control; break; 1726 case 8: modifierState |= ModifierState.shift | ModifierState.alt | ModifierState.control; break; 1727 case 9: 1728 .. 1729 case 16: 1730 modifierState |= ModifierState.meta; 1731 if(modGot != 9) { 1732 modGot -= 8; 1733 goto mod_switch; 1734 } 1735 break; 1736 1737 // this is an extension in my own terminal emulator 1738 case 20: 1739 .. 1740 case 36: 1741 modifierState |= ModifierState.windows; 1742 modGot -= 20; 1743 goto mod_switch; 1744 default: 1745 } 1746 1747 switch(terminator) { 1748 case 'A': return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow, modifierState); 1749 case 'B': return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow, modifierState); 1750 case 'C': return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow, modifierState); 1751 case 'D': return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow, modifierState); 1752 1753 case 'H': return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); 1754 case 'F': return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); 1755 1756 case 'P': return keyPressAndRelease(NonCharacterKeyEvent.Key.F1, modifierState); 1757 case 'Q': return keyPressAndRelease(NonCharacterKeyEvent.Key.F2, modifierState); 1758 case 'R': return keyPressAndRelease(NonCharacterKeyEvent.Key.F3, modifierState); 1759 case 'S': return keyPressAndRelease(NonCharacterKeyEvent.Key.F4, modifierState); 1760 1761 case '~': // others 1762 switch(parts[0]) { 1763 case "5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp, modifierState); 1764 case "6": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown, modifierState); 1765 case "2": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert, modifierState); 1766 case "3": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete, modifierState); 1767 1768 case "15": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5, modifierState); 1769 case "17": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6, modifierState); 1770 case "18": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7, modifierState); 1771 case "19": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8, modifierState); 1772 case "20": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9, modifierState); 1773 case "21": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10, modifierState); 1774 case "23": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11, modifierState); 1775 case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); 1776 default: 1777 } 1778 break; 1779 1780 default: 1781 } 1782 } else if(terminal.terminalInFamily("rxvt")) { 1783 // FIXME: figure these out. rxvt seems to just change the terminator while keeping the rest the same 1784 // though it isn't consistent. ugh. 1785 } else { 1786 // maybe we could do more terminals, but linux doesn't even send it and screen just seems to pass through, so i don't think so; xterm prolly covers most them anyway 1787 // so this space is semi-intentionally left blank 1788 } 1789 } 1790 } 1791 1792 return null; 1793 } 1794 1795 auto c = nextRaw(true); 1796 if(c == -1) 1797 return null; // interrupted; give back nothing so the other level can recheck signal flags 1798 if(c == 0) 1799 return [InputEvent(EndOfFileEvent())]; 1800 if(c == '\033') { 1801 if(timedCheckForInput(50)) { 1802 // escape sequence 1803 c = nextRaw(); 1804 if(c == '[') { // CSI, ends on anything >= 'A' 1805 return doEscapeSequence(readEscapeSequence(sequenceBuffer)); 1806 } else if(c == 'O') { 1807 // could be xterm function key 1808 auto n = nextRaw(); 1809 1810 char[3] thing; 1811 thing[0] = '\033'; 1812 thing[1] = 'O'; 1813 thing[2] = cast(char) n; 1814 1815 auto cap = terminal.findSequenceInTermcap(thing); 1816 if(cap is null) { 1817 return charPressAndRelease('\033') ~ 1818 charPressAndRelease('O') ~ 1819 charPressAndRelease(thing[2]); 1820 } else { 1821 return translateTermcapName(cap); 1822 } 1823 } else { 1824 // I don't know, probably unsupported terminal or just quick user input or something 1825 return charPressAndRelease('\033') ~ charPressAndRelease(nextChar(c)); 1826 } 1827 } else { 1828 // user hit escape (or super slow escape sequence, but meh) 1829 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); 1830 } 1831 } else { 1832 // FIXME: what if it is neither? we should check the termcap 1833 auto next = nextChar(c); 1834 if(next == 127) // some terminals send 127 on the backspace. Let's normalize that. 1835 next = '\b'; 1836 return charPressAndRelease(next); 1837 } 1838 } 1839 } 1840 1841 /// Input event for characters 1842 struct CharacterEvent { 1843 /// . 1844 enum Type { 1845 Released, /// . 1846 Pressed /// . 1847 } 1848 1849 Type eventType; /// . 1850 dchar character; /// . 1851 uint modifierState; /// Don't depend on this to be available for character events 1852 } 1853 1854 struct NonCharacterKeyEvent { 1855 /// . 1856 enum Type { 1857 Released, /// . 1858 Pressed /// . 1859 } 1860 Type eventType; /// . 1861 1862 // these match Windows virtual key codes numerically for simplicity of translation there 1863 //http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 1864 /// . 1865 enum Key : int { 1866 escape = 0x1b, /// . 1867 F1 = 0x70, /// . 1868 F2 = 0x71, /// . 1869 F3 = 0x72, /// . 1870 F4 = 0x73, /// . 1871 F5 = 0x74, /// . 1872 F6 = 0x75, /// . 1873 F7 = 0x76, /// . 1874 F8 = 0x77, /// . 1875 F9 = 0x78, /// . 1876 F10 = 0x79, /// . 1877 F11 = 0x7A, /// . 1878 F12 = 0x7B, /// . 1879 LeftArrow = 0x25, /// . 1880 RightArrow = 0x27, /// . 1881 UpArrow = 0x26, /// . 1882 DownArrow = 0x28, /// . 1883 Insert = 0x2d, /// . 1884 Delete = 0x2e, /// . 1885 Home = 0x24, /// . 1886 End = 0x23, /// . 1887 PageUp = 0x21, /// . 1888 PageDown = 0x22, /// . 1889 } 1890 Key key; /// . 1891 1892 uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms 1893 1894 } 1895 1896 /// . 1897 struct PasteEvent { 1898 string pastedText; /// . 1899 } 1900 1901 /// . 1902 struct MouseEvent { 1903 // these match simpledisplay.d numerically as well 1904 /// . 1905 enum Type { 1906 Moved = 0, /// . 1907 Pressed = 1, /// . 1908 Released = 2, /// . 1909 Clicked, /// . 1910 } 1911 1912 Type eventType; /// . 1913 1914 // note: these should numerically match simpledisplay.d for maximum beauty in my other code 1915 /// . 1916 enum Button : uint { 1917 None = 0, /// . 1918 Left = 1, /// . 1919 Middle = 4, /// . 1920 Right = 2, /// . 1921 ScrollUp = 8, /// . 1922 ScrollDown = 16 /// . 1923 } 1924 uint buttons; /// A mask of Button 1925 int x; /// 0 == left side 1926 int y; /// 0 == top 1927 uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something 1928 } 1929 1930 /// . 1931 struct SizeChangedEvent { 1932 int oldWidth; 1933 int oldHeight; 1934 int newWidth; 1935 int newHeight; 1936 } 1937 1938 /// the user hitting ctrl+c will send this 1939 /// You should drop what you're doing and perhaps exit when this happens. 1940 struct UserInterruptionEvent {} 1941 1942 /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. 1943 /// If you receive it, you should generally cleanly exit. 1944 struct HangupEvent {} 1945 1946 /// Sent upon receiving end-of-file from stdin. 1947 struct EndOfFileEvent {} 1948 1949 interface CustomEvent {} 1950 1951 version(Windows) 1952 enum ModifierState : uint { 1953 shift = 0x10, 1954 control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl 1955 1956 // i'm not sure if the next two are available 1957 alt = 2 | 1, //2 ==left alt, 1 == right alt 1958 1959 // FIXME: I don't think these are actually available 1960 windows = 512, 1961 meta = 4096, // FIXME sanity 1962 1963 // I don't think this is available on Linux.... 1964 scrollLock = 0x40, 1965 } 1966 else 1967 enum ModifierState : uint { 1968 shift = 4, 1969 alt = 2, 1970 control = 16, 1971 meta = 8, 1972 1973 windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones 1974 } 1975 1976 /// GetNextEvent returns this. Check the type, then use get to get the more detailed input 1977 struct InputEvent { 1978 /// . 1979 enum Type { 1980 CharacterEvent, ///. 1981 NonCharacterKeyEvent, /// . 1982 PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. 1983 MouseEvent, /// only sent if you subscribed to mouse events 1984 SizeChangedEvent, /// only sent if you subscribed to size events 1985 UserInterruptionEvent, /// the user hit ctrl+c 1986 EndOfFileEvent, /// stdin has received an end of file 1987 HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator 1988 CustomEvent /// . 1989 } 1990 1991 /// . 1992 @property Type type() { return t; } 1993 1994 /// . 1995 @property auto get(Type T)() { 1996 if(type != T) 1997 throw new Exception("Wrong event type"); 1998 static if(T == Type.CharacterEvent) 1999 return characterEvent; 2000 else static if(T == Type.NonCharacterKeyEvent) 2001 return nonCharacterKeyEvent; 2002 else static if(T == Type.PasteEvent) 2003 return pasteEvent; 2004 else static if(T == Type.MouseEvent) 2005 return mouseEvent; 2006 else static if(T == Type.SizeChangedEvent) 2007 return sizeChangedEvent; 2008 else static if(T == Type.UserInterruptionEvent) 2009 return userInterruptionEvent; 2010 else static if(T == Type.EndOfFileEvent) 2011 return endOfFileEvent; 2012 else static if(T == Type.HangupEvent) 2013 return hangupEvent; 2014 else static if(T == Type.CustomEvent) 2015 return customEvent; 2016 else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); 2017 } 2018 2019 private { 2020 this(CharacterEvent c) { 2021 t = Type.CharacterEvent; 2022 characterEvent = c; 2023 } 2024 this(NonCharacterKeyEvent c) { 2025 t = Type.NonCharacterKeyEvent; 2026 nonCharacterKeyEvent = c; 2027 } 2028 this(PasteEvent c) { 2029 t = Type.PasteEvent; 2030 pasteEvent = c; 2031 } 2032 this(MouseEvent c) { 2033 t = Type.MouseEvent; 2034 mouseEvent = c; 2035 } 2036 this(SizeChangedEvent c) { 2037 t = Type.SizeChangedEvent; 2038 sizeChangedEvent = c; 2039 } 2040 this(UserInterruptionEvent c) { 2041 t = Type.UserInterruptionEvent; 2042 userInterruptionEvent = c; 2043 } 2044 this(HangupEvent c) { 2045 t = Type.HangupEvent; 2046 hangupEvent = c; 2047 } 2048 this(EndOfFileEvent c) { 2049 t = Type.EndOfFileEvent; 2050 endOfFileEvent = c; 2051 } 2052 this(CustomEvent c) { 2053 t = Type.CustomEvent; 2054 customEvent = c; 2055 } 2056 2057 Type t; 2058 2059 union { 2060 CharacterEvent characterEvent; 2061 NonCharacterKeyEvent nonCharacterKeyEvent; 2062 PasteEvent pasteEvent; 2063 MouseEvent mouseEvent; 2064 SizeChangedEvent sizeChangedEvent; 2065 UserInterruptionEvent userInterruptionEvent; 2066 HangupEvent hangupEvent; 2067 EndOfFileEvent endOfFileEvent; 2068 CustomEvent customEvent; 2069 } 2070 } 2071 } 2072 2073 version(Demo) 2074 void main() { 2075 auto terminal = Terminal(ConsoleOutputType.cellular); 2076 2077 //terminal.color(Color.DEFAULT, Color.DEFAULT); 2078 2079 // 2080 /* 2081 auto getter = new LineGetter(&terminal, "test"); 2082 getter.prompt = "> "; 2083 terminal.writeln("\n" ~ getter.getline()); 2084 terminal.writeln("\n" ~ getter.getline()); 2085 terminal.writeln("\n" ~ getter.getline()); 2086 getter.dispose(); 2087 */ 2088 2089 terminal.writeln(terminal.getline()); 2090 terminal.writeln(terminal.getline()); 2091 terminal.writeln(terminal.getline()); 2092 2093 //input.getch(); 2094 2095 // return; 2096 // 2097 2098 terminal.setTitle("Basic I/O"); 2099 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents); 2100 terminal.color(Color.green | Bright, Color.black); 2101 2102 terminal.write("test some long string to see if it wraps or what because i dont really know what it is going to do so i just want to test i think it will wrap but gotta be sure lolololololololol"); 2103 terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 2104 2105 int centerX = terminal.width / 2; 2106 int centerY = terminal.height / 2; 2107 2108 bool timeToBreak = false; 2109 2110 void handleEvent(InputEvent event) { 2111 terminal.writef("%s\n", event.type); 2112 final switch(event.type) { 2113 case InputEvent.Type.UserInterruptionEvent: 2114 case InputEvent.Type.HangupEvent: 2115 case InputEvent.Type.EndOfFileEvent: 2116 timeToBreak = true; 2117 version(with_eventloop) { 2118 import arsd.eventloop; 2119 exit(); 2120 } 2121 break; 2122 case InputEvent.Type.SizeChangedEvent: 2123 auto ev = event.get!(InputEvent.Type.SizeChangedEvent); 2124 terminal.writeln(ev); 2125 break; 2126 case InputEvent.Type.CharacterEvent: 2127 auto ev = event.get!(InputEvent.Type.CharacterEvent); 2128 terminal.writef("\t%s\n", ev); 2129 if(ev.character == 'Q') { 2130 timeToBreak = true; 2131 version(with_eventloop) { 2132 import arsd.eventloop; 2133 exit(); 2134 } 2135 } 2136 2137 if(ev.character == 'C') 2138 terminal.clear(); 2139 break; 2140 case InputEvent.Type.NonCharacterKeyEvent: 2141 terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent)); 2142 break; 2143 case InputEvent.Type.PasteEvent: 2144 terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent)); 2145 break; 2146 case InputEvent.Type.MouseEvent: 2147 terminal.writef("\t%s\n", event.get!(InputEvent.Type.MouseEvent)); 2148 break; 2149 case InputEvent.Type.CustomEvent: 2150 break; 2151 } 2152 2153 terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 2154 2155 /* 2156 if(input.kbhit()) { 2157 auto c = input.getch(); 2158 if(c == 'q' || c == 'Q') 2159 break; 2160 terminal.moveTo(centerX, centerY); 2161 terminal.writef("%c", c); 2162 terminal.flush(); 2163 } 2164 usleep(10000); 2165 */ 2166 } 2167 2168 version(with_eventloop) { 2169 import arsd.eventloop; 2170 addListener(&handleEvent); 2171 loop(); 2172 } else { 2173 loop: while(true) { 2174 auto event = input.nextEvent(); 2175 handleEvent(event); 2176 if(timeToBreak) 2177 break loop; 2178 } 2179 } 2180 } 2181 2182 /** 2183 FIXME: support lines that wrap 2184 FIXME: better controls maybe 2185 2186 FIXME: fix lengths on prompt and suggestion 2187 2188 A note on history: 2189 2190 To save history, you must call LineGetter.dispose() when you're done with it. 2191 History will not be automatically saved without that call! 2192 2193 The history saving and loading as a trivially encountered race condition: if you 2194 open two programs that use the same one at the same time, the one that closes second 2195 will overwrite any history changes the first closer saved. 2196 2197 GNU Getline does this too... and it actually kinda drives me nuts. But I don't know 2198 what a good fix is except for doing a transactional commit straight to the file every 2199 time and that seems like hitting the disk way too often. 2200 2201 We could also do like a history server like a database daemon that keeps the order 2202 correct but I don't actually like that either because I kinda like different bashes 2203 to have different history, I just don't like it all to get lost. 2204 2205 Regardless though, this isn't even used in bash anyway, so I don't think I care enough 2206 to put that much effort into it. Just using separate files for separate tasks is good 2207 enough I think. 2208 */ 2209 class LineGetter { 2210 /* A note on the assumeSafeAppends in here: since these buffers are private, we can be 2211 pretty sure that stomping isn't an issue, so I'm using this liberally to keep the 2212 append/realloc code simple and hopefully reasonably fast. */ 2213 2214 // saved to file 2215 string[] history; 2216 2217 // not saved 2218 Terminal* terminal; 2219 string historyFilename; 2220 2221 /// Make sure that the parent terminal struct remains in scope for the duration 2222 /// of LineGetter's lifetime, as it does hold on to and use the passed pointer 2223 /// throughout. 2224 /// 2225 /// historyFilename will load and save an input history log to a particular folder. 2226 /// Leaving it null will mean no file will be used and history will not be saved across sessions. 2227 this(Terminal* tty, string historyFilename = null) { 2228 this.terminal = tty; 2229 this.historyFilename = historyFilename; 2230 2231 line.reserve(128); 2232 2233 if(historyFilename.length) 2234 loadSettingsAndHistoryFromFile(); 2235 2236 regularForeground = cast(Color) terminal._currentForeground; 2237 background = cast(Color) terminal._currentBackground; 2238 suggestionForeground = Color.blue; 2239 } 2240 2241 /// Call this before letting LineGetter die so it can do any necessary 2242 /// cleanup and save the updated history to a file. 2243 void dispose() { 2244 if(historyFilename.length) 2245 saveSettingsAndHistoryToFile(); 2246 } 2247 2248 /// Override this to change the directory where history files are stored 2249 /// 2250 /// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows. 2251 string historyFileDirectory() { 2252 version(Windows) { 2253 char[1024] path; 2254 // FIXME: this doesn't link because the crappy dmd lib doesn't have it 2255 if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) { 2256 import core.stdc..string; 2257 return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline"; 2258 } else { 2259 import std.process; 2260 return environment["APPDATA"] ~ "\\arsd-getline"; 2261 } 2262 } else version(Posix) { 2263 import std.process; 2264 return environment["HOME"] ~ "/.arsd-getline"; 2265 } 2266 } 2267 2268 /// You can customize the colors here. You should set these after construction, but before 2269 /// calling startGettingLine or getline. 2270 Color suggestionForeground; 2271 Color regularForeground; /// . 2272 Color background; /// . 2273 //bool reverseVideo; 2274 2275 /// Set this if you want a prompt to be drawn with the line. It does NOT support color in string. 2276 string prompt; 2277 2278 /// Turn on auto suggest if you want a greyed thing of what tab 2279 /// would be able to fill in as you type. 2280 /// 2281 /// You might want to turn it off if generating a completion list is slow. 2282 bool autoSuggest = true; 2283 2284 2285 /// Override this if you don't want all lines added to the history. 2286 /// You can return null to not add it at all, or you can transform it. 2287 string historyFilter(string candidate) { 2288 return candidate; 2289 } 2290 2291 /// You may override this to do nothing 2292 void saveSettingsAndHistoryToFile() { 2293 import std.file; 2294 if(!exists(historyFileDirectory)) 2295 mkdir(historyFileDirectory); 2296 auto fn = historyPath(); 2297 import std.stdio; 2298 auto file = File(fn, "wt"); 2299 foreach(item; history) 2300 file.writeln(item); 2301 } 2302 2303 private string historyPath() { 2304 import std.path; 2305 auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ ".history"; 2306 return filename; 2307 } 2308 2309 /// You may override this to do nothing 2310 void loadSettingsAndHistoryFromFile() { 2311 import std.file; 2312 history = null; 2313 auto fn = historyPath(); 2314 if(exists(fn)) { 2315 import std.stdio; 2316 foreach(line; File(fn, "rt").byLine) 2317 history ~= line.idup; 2318 2319 } 2320 } 2321 2322 /** 2323 Override this to provide tab completion. You may use the candidate 2324 argument to filter the list, but you don't have to (LineGetter will 2325 do it for you on the values you return). 2326 2327 Ideally, you wouldn't return more than about ten items since the list 2328 gets difficult to use if it is too long. 2329 2330 Default is to provide recent command history as autocomplete. 2331 */ 2332 protected string[] tabComplete(in dchar[] candidate) { 2333 return history.length > 20 ? history[0 .. 20] : history; 2334 } 2335 2336 private string[] filterTabCompleteList(string[] list) { 2337 if(list.length == 0) 2338 return list; 2339 2340 string[] f; 2341 f.reserve(list.length); 2342 2343 foreach(item; list) { 2344 import std.algorithm; 2345 if(startsWith(item, line[0 .. cursorPosition])) 2346 f ~= item; 2347 } 2348 2349 return f; 2350 } 2351 2352 /// Override this to provide a custom display of the tab completion list 2353 protected void showTabCompleteList(string[] list) { 2354 if(list.length) { 2355 // FIXME: allow mouse clicking of an item, that would be cool 2356 2357 //if(terminal.type == ConsoleOutputType.linear) { 2358 terminal.writeln(); 2359 foreach(item; list) { 2360 terminal.color(suggestionForeground, background); 2361 import std.utf; 2362 auto idx = codeLength!char(line[0 .. cursorPosition]); 2363 terminal.write(" ", item[0 .. idx]); 2364 terminal.color(regularForeground, background); 2365 terminal.writeln(item[idx .. $]); 2366 } 2367 updateCursorPosition(); 2368 redraw(); 2369 //} 2370 } 2371 } 2372 2373 /// One-call shop for the main workhorse 2374 /// If you already have a RealTimeConsoleInput ready to go, you 2375 /// should pass a pointer to yours here. Otherwise, LineGetter will 2376 /// make its own. 2377 public string getline(RealTimeConsoleInput* input = null) { 2378 startGettingLine(); 2379 if(input is null) { 2380 auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents); 2381 while(workOnLine(i.nextEvent())) {} 2382 } else 2383 while(workOnLine(input.nextEvent())) {} 2384 return finishGettingLine(); 2385 } 2386 2387 private int currentHistoryViewPosition = 0; 2388 private dchar[] uncommittedHistoryCandidate; 2389 void loadFromHistory(int howFarBack) { 2390 if(howFarBack < 0) 2391 howFarBack = 0; 2392 if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around. 2393 howFarBack = cast(int) history.length; 2394 if(howFarBack == currentHistoryViewPosition) 2395 return; 2396 if(currentHistoryViewPosition == 0) { 2397 // save the current line so we can down arrow back to it later 2398 if(uncommittedHistoryCandidate.length < line.length) { 2399 uncommittedHistoryCandidate.length = line.length; 2400 } 2401 2402 uncommittedHistoryCandidate[0 .. line.length] = line[]; 2403 uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length]; 2404 uncommittedHistoryCandidate.assumeSafeAppend(); 2405 } 2406 2407 currentHistoryViewPosition = howFarBack; 2408 2409 if(howFarBack == 0) { 2410 line.length = uncommittedHistoryCandidate.length; 2411 line.assumeSafeAppend(); 2412 line[] = uncommittedHistoryCandidate[]; 2413 } else { 2414 line = line[0 .. 0]; 2415 line.assumeSafeAppend(); 2416 foreach(dchar ch; history[$ - howFarBack]) 2417 line ~= ch; 2418 } 2419 2420 cursorPosition = cast(int) line.length; 2421 } 2422 2423 bool insertMode = true; 2424 2425 private dchar[] line; 2426 private int cursorPosition = 0; 2427 2428 // used for redrawing the line in the right place 2429 // and detecting mouse events on our line. 2430 private int startOfLineX; 2431 private int startOfLineY; 2432 2433 private string suggestion(string[] list = null) { 2434 import std.algorithm, std.utf; 2435 auto relevantLineSection = line[0 .. cursorPosition]; 2436 // FIXME: see about caching the list if we easily can 2437 if(list is null) 2438 list = filterTabCompleteList(tabComplete(relevantLineSection)); 2439 2440 if(list.length) { 2441 string commonality = list[0]; 2442 foreach(item; list[1 .. $]) { 2443 commonality = commonPrefix(commonality, item); 2444 } 2445 2446 if(commonality.length) { 2447 return commonality[codeLength!char(relevantLineSection) .. $]; 2448 } 2449 } 2450 2451 return null; 2452 } 2453 2454 /// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something. 2455 /// You'll probably want to call redraw() after adding chars. 2456 void addChar(dchar ch) { 2457 assert(cursorPosition >= 0 && cursorPosition <= line.length); 2458 if(cursorPosition == line.length) 2459 line ~= ch; 2460 else { 2461 assert(line.length); 2462 if(insertMode) { 2463 line ~= ' '; 2464 for(int i = cast(int) line.length - 2; i >= cursorPosition; i --) 2465 line[i + 1] = line[i]; 2466 } 2467 line[cursorPosition] = ch; 2468 } 2469 cursorPosition++; 2470 } 2471 2472 /// . 2473 void addString(string s) { 2474 // FIXME: this could be more efficient 2475 // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) 2476 foreach(dchar ch; s) 2477 addChar(ch); 2478 } 2479 2480 /// Deletes the character at the current position in the line. 2481 /// You'll probably want to call redraw() after deleting chars. 2482 void deleteChar() { 2483 if(cursorPosition == line.length) 2484 return; 2485 for(int i = cursorPosition; i < line.length - 1; i++) 2486 line[i] = line[i + 1]; 2487 line = line[0 .. $-1]; 2488 line.assumeSafeAppend(); 2489 } 2490 2491 int lastDrawLength = 0; 2492 void redraw() { 2493 terminal.moveTo(startOfLineX, startOfLineY); 2494 2495 terminal.write(prompt); 2496 2497 terminal.write(line); 2498 auto suggestion = ((cursorPosition == line.length) && autoSuggest) ? this.suggestion() : null; 2499 if(suggestion.length) { 2500 terminal.color(suggestionForeground, background); 2501 terminal.write(suggestion); 2502 terminal.color(regularForeground, background); 2503 } 2504 if(line.length < lastDrawLength) 2505 foreach(i; line.length + suggestion.length + prompt.length .. lastDrawLength) 2506 terminal.write(" "); 2507 lastDrawLength = cast(int) (line.length + suggestion.length + prompt.length); // FIXME: graphemes and utf-8 on suggestion/prompt 2508 2509 // FIXME: wrapping 2510 terminal.moveTo(startOfLineX + cursorPosition + cast(int) prompt.length, startOfLineY); 2511 } 2512 2513 /// Starts getting a new line. Call workOnLine and finishGettingLine afterward. 2514 /// 2515 /// Make sure that you've flushed your input and output before calling this 2516 /// function or else you might lose events or get exceptions from this. 2517 void startGettingLine() { 2518 // reset from any previous call first 2519 cursorPosition = 0; 2520 lastDrawLength = 0; 2521 justHitTab = false; 2522 currentHistoryViewPosition = 0; 2523 if(line.length) { 2524 line = line[0 .. 0]; 2525 line.assumeSafeAppend(); 2526 } 2527 2528 updateCursorPosition(); 2529 terminal.showCursor(); 2530 2531 redraw(); 2532 } 2533 2534 private void updateCursorPosition() { 2535 terminal.flush(); 2536 2537 // then get the current cursor position to start fresh 2538 version(Windows) { 2539 CONSOLE_SCREEN_BUFFER_INFO info; 2540 GetConsoleScreenBufferInfo(terminal.hConsole, &info); 2541 startOfLineX = info.dwCursorPosition.X; 2542 startOfLineY = info.dwCursorPosition.Y; 2543 } else { 2544 // request current cursor position 2545 2546 // we have to turn off cooked mode to get this answer, otherwise it will all 2547 // be messed up. (I hate unix terminals, the Windows way is so much easer.) 2548 RealTimeConsoleInput input = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw); 2549 2550 terminal.writeStringRaw("\033[6n"); 2551 terminal.flush(); 2552 2553 import core.sys.posix.unistd; 2554 // reading directly to bypass any buffering 2555 ubyte[16] buffer; 2556 auto len = read(terminal.fdIn, buffer.ptr, buffer.length); 2557 if(len <= 0) 2558 throw new Exception("Couldn't get cursor position to initialize get line"); 2559 auto got = buffer[0 .. len]; 2560 if(got.length < 6) 2561 throw new Exception("not enough cursor reply answer"); 2562 if(got[0] != '\033' || got[1] != '[' || got[$-1] != 'R') 2563 throw new Exception("wrong answer for cursor position"); 2564 auto gots = cast(char[]) got[2 .. $-1]; 2565 2566 import std.conv; 2567 import std..string; 2568 2569 auto pieces = split(gots, ";"); 2570 if(pieces.length != 2) throw new Exception("wtf wrong answer on cursor position"); 2571 2572 startOfLineX = to!int(pieces[1]) - 1; 2573 startOfLineY = to!int(pieces[0]) - 1; 2574 } 2575 2576 // updating these too because I can with the more accurate info from above 2577 terminal._cursorX = startOfLineX; 2578 terminal._cursorY = startOfLineY; 2579 } 2580 2581 private bool justHitTab; 2582 2583 /// for integrating into another event loop 2584 /// you can pass individual events to this and 2585 /// the line getter will work on it 2586 /// 2587 /// returns false when there's nothing more to do 2588 bool workOnLine(InputEvent e) { 2589 switch(e.type) { 2590 case InputEvent.Type.EndOfFileEvent: 2591 justHitTab = false; 2592 return false; 2593 break; 2594 case InputEvent.Type.CharacterEvent: 2595 if(e.characterEvent.eventType == CharacterEvent.Type.Released) 2596 return true; 2597 /* Insert the character (unless it is backspace, tab, or some other control char) */ 2598 auto ch = e.characterEvent.character; 2599 switch(ch) { 2600 case 4: // ctrl+d will also send a newline-equivalent 2601 case '\r': 2602 case '\n': 2603 justHitTab = false; 2604 return false; 2605 case '\t': 2606 auto relevantLineSection = line[0 .. cursorPosition]; 2607 auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection)); 2608 import std.utf; 2609 2610 if(possibilities.length == 1) { 2611 auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $]; 2612 if(toFill.length) { 2613 addString(toFill); 2614 redraw(); 2615 } 2616 justHitTab = false; 2617 } else { 2618 if(justHitTab) { 2619 justHitTab = false; 2620 showTabCompleteList(possibilities); 2621 } else { 2622 justHitTab = true; 2623 /* fill it in with as much commonality as there is amongst all the suggestions */ 2624 auto suggestion = this.suggestion(possibilities); 2625 if(suggestion.length) { 2626 addString(suggestion); 2627 redraw(); 2628 } 2629 } 2630 } 2631 break; 2632 case '\b': 2633 justHitTab = false; 2634 if(cursorPosition) { 2635 cursorPosition--; 2636 for(int i = cursorPosition; i < line.length - 1; i++) 2637 line[i] = line[i + 1]; 2638 line = line[0 .. $ - 1]; 2639 line.assumeSafeAppend(); 2640 redraw(); 2641 } 2642 break; 2643 default: 2644 justHitTab = false; 2645 addChar(ch); 2646 redraw(); 2647 } 2648 break; 2649 case InputEvent.Type.NonCharacterKeyEvent: 2650 if(e.nonCharacterKeyEvent.eventType == NonCharacterKeyEvent.Type.Released) 2651 return true; 2652 justHitTab = false; 2653 /* Navigation */ 2654 auto key = e.nonCharacterKeyEvent.key; 2655 switch(key) { 2656 case NonCharacterKeyEvent.Key.LeftArrow: 2657 if(cursorPosition) 2658 cursorPosition--; 2659 redraw(); 2660 break; 2661 case NonCharacterKeyEvent.Key.RightArrow: 2662 if(cursorPosition < line.length) 2663 cursorPosition++; 2664 redraw(); 2665 break; 2666 case NonCharacterKeyEvent.Key.UpArrow: 2667 loadFromHistory(currentHistoryViewPosition + 1); 2668 redraw(); 2669 break; 2670 case NonCharacterKeyEvent.Key.DownArrow: 2671 loadFromHistory(currentHistoryViewPosition - 1); 2672 redraw(); 2673 break; 2674 case NonCharacterKeyEvent.Key.PageUp: 2675 loadFromHistory(cast(int) history.length); 2676 redraw(); 2677 break; 2678 case NonCharacterKeyEvent.Key.PageDown: 2679 loadFromHistory(0); 2680 redraw(); 2681 break; 2682 case NonCharacterKeyEvent.Key.Home: 2683 cursorPosition = 0; 2684 redraw(); 2685 break; 2686 case NonCharacterKeyEvent.Key.End: 2687 cursorPosition = cast(int) line.length; 2688 redraw(); 2689 break; 2690 case NonCharacterKeyEvent.Key.Insert: 2691 insertMode = !insertMode; 2692 // FIXME: indicate this on the UI somehow 2693 // like change the cursor or something 2694 break; 2695 case NonCharacterKeyEvent.Key.Delete: 2696 deleteChar(); 2697 redraw(); 2698 break; 2699 default: 2700 /* ignore */ 2701 } 2702 break; 2703 case InputEvent.Type.PasteEvent: 2704 justHitTab = false; 2705 addString(e.pasteEvent.pastedText); 2706 redraw(); 2707 break; 2708 case InputEvent.Type.MouseEvent: 2709 /* Clicking with the mouse to move the cursor is so much easier than arrowing 2710 or even emacs/vi style movements much of the time, so I'ma support it. */ 2711 2712 auto me = e.mouseEvent; 2713 if(me.eventType == MouseEvent.Type.Pressed) { 2714 if(me.buttons & MouseEvent.Button.Left) { 2715 if(me.y == startOfLineY) { 2716 // FIXME: prompt.length should be graphemes or at least code poitns 2717 int p = me.x - startOfLineX - cast(int) prompt.length; 2718 if(p >= 0 && p < line.length) { 2719 justHitTab = false; 2720 cursorPosition = p; 2721 redraw(); 2722 } 2723 } 2724 } 2725 } 2726 break; 2727 case InputEvent.Type.SizeChangedEvent: 2728 /* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent 2729 yourself and then don't pass it to this function. */ 2730 // FIXME 2731 break; 2732 case InputEvent.Type.UserInterruptionEvent: 2733 /* I'll take this as canceling the line. */ 2734 throw new Exception("user canceled"); // FIXME 2735 break; 2736 case InputEvent.Type.HangupEvent: 2737 /* I'll take this as canceling the line. */ 2738 throw new Exception("user hanged up"); // FIXME 2739 break; 2740 default: 2741 /* ignore. ideally it wouldn't be passed to us anyway! */ 2742 } 2743 2744 return true; 2745 } 2746 2747 string finishGettingLine() { 2748 import std.conv; 2749 auto f = to!string(line); 2750 auto history = historyFilter(f); 2751 if(history !is null) 2752 this.history ~= history; 2753 2754 // FIXME: we should hide the cursor if it was hidden in the call to startGettingLine 2755 return f; 2756 } 2757 } 2758 2759 version(Windows) { 2760 // to get the directory for saving history in the line things 2761 enum CSIDL_APPDATA = 26; 2762 extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); 2763 } 2764 2765 /* 2766 2767 // more efficient scrolling 2768 http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx 2769 // and the unix sequences 2770 2771 2772 rxvt documentation: 2773 use this to finish the input magic for that 2774 2775 2776 For the keypad, use Shift to temporarily override Application-Keypad 2777 setting use Num_Lock to toggle Application-Keypad setting if Num_Lock 2778 is off, toggle Application-Keypad setting. Also note that values of 2779 Home, End, Delete may have been compiled differently on your system. 2780 2781 Normal Shift Control Ctrl+Shift 2782 Tab ^I ESC [ Z ^I ESC [ Z 2783 BackSpace ^H ^? ^? ^? 2784 Find ESC [ 1 ~ ESC [ 1 $ ESC [ 1 ^ ESC [ 1 @ 2785 Insert ESC [ 2 ~ paste ESC [ 2 ^ ESC [ 2 @ 2786 Execute ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 2787 Select ESC [ 4 ~ ESC [ 4 $ ESC [ 4 ^ ESC [ 4 @ 2788 Prior ESC [ 5 ~ scroll-up ESC [ 5 ^ ESC [ 5 @ 2789 Next ESC [ 6 ~ scroll-down ESC [ 6 ^ ESC [ 6 @ 2790 Home ESC [ 7 ~ ESC [ 7 $ ESC [ 7 ^ ESC [ 7 @ 2791 End ESC [ 8 ~ ESC [ 8 $ ESC [ 8 ^ ESC [ 8 @ 2792 Delete ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 2793 F1 ESC [ 11 ~ ESC [ 23 ~ ESC [ 11 ^ ESC [ 23 ^ 2794 F2 ESC [ 12 ~ ESC [ 24 ~ ESC [ 12 ^ ESC [ 24 ^ 2795 F3 ESC [ 13 ~ ESC [ 25 ~ ESC [ 13 ^ ESC [ 25 ^ 2796 F4 ESC [ 14 ~ ESC [ 26 ~ ESC [ 14 ^ ESC [ 26 ^ 2797 F5 ESC [ 15 ~ ESC [ 28 ~ ESC [ 15 ^ ESC [ 28 ^ 2798 F6 ESC [ 17 ~ ESC [ 29 ~ ESC [ 17 ^ ESC [ 29 ^ 2799 F7 ESC [ 18 ~ ESC [ 31 ~ ESC [ 18 ^ ESC [ 31 ^ 2800 F8 ESC [ 19 ~ ESC [ 32 ~ ESC [ 19 ^ ESC [ 32 ^ 2801 F9 ESC [ 20 ~ ESC [ 33 ~ ESC [ 20 ^ ESC [ 33 ^ 2802 F10 ESC [ 21 ~ ESC [ 34 ~ ESC [ 21 ^ ESC [ 34 ^ 2803 F11 ESC [ 23 ~ ESC [ 23 $ ESC [ 23 ^ ESC [ 23 @ 2804 F12 ESC [ 24 ~ ESC [ 24 $ ESC [ 24 ^ ESC [ 24 @ 2805 F13 ESC [ 25 ~ ESC [ 25 $ ESC [ 25 ^ ESC [ 25 @ 2806 F14 ESC [ 26 ~ ESC [ 26 $ ESC [ 26 ^ ESC [ 26 @ 2807 F15 (Help) ESC [ 28 ~ ESC [ 28 $ ESC [ 28 ^ ESC [ 28 @ 2808 F16 (Menu) ESC [ 29 ~ ESC [ 29 $ ESC [ 29 ^ ESC [ 29 @ 2809 2810 F17 ESC [ 31 ~ ESC [ 31 $ ESC [ 31 ^ ESC [ 31 @ 2811 F18 ESC [ 32 ~ ESC [ 32 $ ESC [ 32 ^ ESC [ 32 @ 2812 F19 ESC [ 33 ~ ESC [ 33 $ ESC [ 33 ^ ESC [ 33 @ 2813 F20 ESC [ 34 ~ ESC [ 34 $ ESC [ 34 ^ ESC [ 34 @ 2814 Application 2815 Up ESC [ A ESC [ a ESC O a ESC O A 2816 Down ESC [ B ESC [ b ESC O b ESC O B 2817 Right ESC [ C ESC [ c ESC O c ESC O C 2818 Left ESC [ D ESC [ d ESC O d ESC O D 2819 KP_Enter ^M ESC O M 2820 KP_F1 ESC O P ESC O P 2821 KP_F2 ESC O Q ESC O Q 2822 KP_F3 ESC O R ESC O R 2823 KP_F4 ESC O S ESC O S 2824 XK_KP_Multiply * ESC O j 2825 XK_KP_Add + ESC O k 2826 XK_KP_Separator , ESC O l 2827 XK_KP_Subtract - ESC O m 2828 XK_KP_Decimal . ESC O n 2829 XK_KP_Divide / ESC O o 2830 XK_KP_0 0 ESC O p 2831 XK_KP_1 1 ESC O q 2832 XK_KP_2 2 ESC O r 2833 XK_KP_3 3 ESC O s 2834 XK_KP_4 4 ESC O t 2835 XK_KP_5 5 ESC O u 2836 XK_KP_6 6 ESC O v 2837 XK_KP_7 7 ESC O w 2838 XK_KP_8 8 ESC O x 2839 XK_KP_9 9 ESC O y 2840 */