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