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