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