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