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