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