1 // for optional dependency 2 // for VT on Windows P s = 1 8 → Report the size of the text area in characters as CSI 8 ; height ; width t 3 // could be used to have the TE volunteer the size 4 /++ 5 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]. 6 7 8 The main interface for this module is the Terminal struct, which 9 encapsulates the output functions and line-buffered input of the terminal, and 10 RealTimeConsoleInput, which gives real time input. 11 12 Creating an instance of these structs will perform console initialization. When the struct 13 goes out of scope, any changes in console settings will be automatically reverted. 14 15 Note: on Posix, it traps SIGINT and translates it into an input event. You should 16 keep your event loop moving and keep an eye open for this to exit cleanly; simply break 17 your event loop upon receiving a UserInterruptionEvent. (Without 18 the signal handler, ctrl+c can leave your terminal in a bizarre state.) 19 20 As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\ 21 22 On old Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work. Most functions basically 23 work now with newer Mac OS versions though. 24 25 Future_Roadmap: 26 $(LIST 27 * The CharacterEvent and NonCharacterKeyEvent types will be removed. Instead, use KeyboardEvent 28 on new programs. 29 30 * The ScrollbackBuffer will be expanded to be easier to use to partition your screen. It might even 31 handle input events of some sort. Its API may change. 32 33 * getline I want to be really easy to use both for code and end users. It will need multi-line support 34 eventually. 35 36 * 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.) 37 38 * More advanced terminal features as functions, where available, like cursor changing and full-color functions. 39 40 * More documentation. 41 ) 42 43 WHAT I WON'T DO: 44 $(LIST 45 * support everything under the sun. If it isn't default-installed on an OS I or significant number of other people 46 might actually use, and isn't written by me, I don't really care about it. This means the only supported terminals are: 47 $(LIST 48 49 * xterm (and decently xterm compatible emulators like Konsole) 50 * Windows console 51 * rxvt (to a lesser extent) 52 * Linux console 53 * My terminal emulator family of applications https://github.com/adamdruppe/terminal-emulator 54 ) 55 56 Anything else is cool if it does work, but I don't want to go out of my way for it. 57 58 * Use other libraries, unless strictly optional. terminal.d is a stand-alone module by default and 59 always will be. 60 61 * Do a full TUI widget set. I might do some basics and lay a little groundwork, but a full TUI 62 is outside the scope of this module (unless I can do it really small.) 63 ) 64 +/ 65 module arsd.terminal; 66 67 // FIXME: needs to support VT output on Windows too in certain situations 68 // detect VT on windows by trying to set the flag. if this succeeds, ask it for caps. if this replies with my code we good to do extended output. 69 70 /++ 71 $(H3 Get Line) 72 73 This example will demonstrate the high-level getline interface. 74 75 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. 76 +/ 77 version(demos) unittest { 78 import arsd.terminal; 79 80 void main() { 81 auto terminal = Terminal(ConsoleOutputType.linear); 82 string line = terminal.getline(); 83 terminal.writeln("You wrote: ", line); 84 } 85 86 main; // exclude from docs 87 } 88 89 /++ 90 $(H3 Color) 91 92 This example demonstrates color output, using [Terminal.color] 93 and the output functions like [Terminal.writeln]. 94 +/ 95 version(demos) unittest { 96 import arsd.terminal; 97 98 void main() { 99 auto terminal = Terminal(ConsoleOutputType.linear); 100 terminal.color(Color.green, Color.black); 101 terminal.writeln("Hello world, in green on black!"); 102 terminal.color(Color.DEFAULT, Color.DEFAULT); 103 terminal.writeln("And back to normal."); 104 } 105 106 main; // exclude from docs 107 } 108 109 /++ 110 $(H3 Single Key) 111 112 This shows how to get one single character press using 113 the [RealTimeConsoleInput] structure. 114 +/ 115 version(demos) unittest { 116 import arsd.terminal; 117 118 void main() { 119 auto terminal = Terminal(ConsoleOutputType.linear); 120 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw); 121 122 terminal.writeln("Press any key to continue..."); 123 auto ch = input.getch(); 124 terminal.writeln("You pressed ", ch); 125 } 126 127 main; // exclude from docs 128 } 129 130 /* 131 Widgets: 132 tab widget 133 scrollback buffer 134 partitioned canvas 135 */ 136 137 // FIXME: ctrl+d eof on stdin 138 139 // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx 140 141 142 /++ 143 A function the sigint handler will call (if overridden - which is the 144 case when [RealTimeConsoleInput] is active on Posix or if you compile with 145 `TerminalDirectToEmulator` version on any platform at this time) in addition 146 to the library's default handling, which is to set a flag for the event loop 147 to inform you. 148 149 Remember, this is called from a signal handler and/or from a separate thread, 150 so you are not allowed to do much with it and need care when setting TLS variables. 151 152 I suggest you only set a `__gshared bool` flag as many other operations will risk 153 undefined behavior. 154 155 $(WARNING 156 This function is never called on the default Windows console 157 configuration in the current implementation. You can use 158 `-version=TerminalDirectToEmulator` to guarantee it is called there 159 too by causing the library to pop up a gui window for your application. 160 ) 161 162 History: 163 Added March 30, 2020. Included in release v7.1.0. 164 165 +/ 166 __gshared void delegate() nothrow @nogc sigIntExtension; 167 168 169 version(TerminalDirectToEmulator) { 170 version=WithEncapsulatedSignals; 171 } 172 173 version(Posix) { 174 enum SIGWINCH = 28; 175 __gshared bool windowSizeChanged = false; 176 __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 177 __gshared bool hangedUp = false; /// similar to interrupted. 178 version=WithSignals; 179 180 version(with_eventloop) 181 struct SignalFired {} 182 183 extern(C) 184 void sizeSignalHandler(int sigNumber) nothrow { 185 windowSizeChanged = true; 186 version(with_eventloop) { 187 import arsd.eventloop; 188 try 189 send(SignalFired()); 190 catch(Exception) {} 191 } 192 } 193 extern(C) 194 void interruptSignalHandler(int sigNumber) nothrow { 195 interrupted = true; 196 version(with_eventloop) { 197 import arsd.eventloop; 198 try 199 send(SignalFired()); 200 catch(Exception) {} 201 } 202 203 if(sigIntExtension) 204 sigIntExtension(); 205 } 206 extern(C) 207 void hangupSignalHandler(int sigNumber) nothrow { 208 hangedUp = true; 209 version(with_eventloop) { 210 import arsd.eventloop; 211 try 212 send(SignalFired()); 213 catch(Exception) {} 214 } 215 } 216 } 217 218 // parts of this were taken from Robik's ConsoleD 219 // https://github.com/robik/ConsoleD/blob/master/consoled.d 220 221 // Uncomment this line to get a main() to demonstrate this module's 222 // capabilities. 223 //version = Demo 224 225 version(TerminalDirectToEmulator) { 226 version=VtEscapeCodes; 227 } else version(Windows) { 228 version(VtEscapeCodes) {} // cool 229 version=Win32Console; 230 } 231 232 version(Windows) 233 import core.sys.windows.windows; 234 235 version(Win32Console) { 236 private { 237 enum RED_BIT = 4; 238 enum GREEN_BIT = 2; 239 enum BLUE_BIT = 1; 240 } 241 242 pragma(lib, "user32"); 243 } 244 245 version(Posix) { 246 247 version=VtEscapeCodes; 248 249 import core.sys.posix.termios; 250 import core.sys.posix.unistd; 251 import unix = core.sys.posix.unistd; 252 import core.sys.posix.sys.types; 253 import core.sys.posix.sys.time; 254 import core.stdc.stdio; 255 256 import core.sys.posix.sys.ioctl; 257 } 258 259 version(VtEscapeCodes) { 260 261 enum UseVtSequences = true; 262 263 version(TerminalDirectToEmulator) { 264 private { 265 enum RED_BIT = 1; 266 enum GREEN_BIT = 2; 267 enum BLUE_BIT = 4; 268 } 269 } else version(Windows) {} else 270 private { 271 enum RED_BIT = 1; 272 enum GREEN_BIT = 2; 273 enum BLUE_BIT = 4; 274 } 275 276 struct winsize { 277 ushort ws_row; 278 ushort ws_col; 279 ushort ws_xpixel; 280 ushort ws_ypixel; 281 } 282 283 // 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). 284 285 // this way we'll have some definitions for 99% of typical PC cases even without any help from the local operating system 286 287 enum string builtinTermcap = ` 288 # Generic VT entry. 289 vg|vt-generic|Generic VT entries:\ 290 :bs:mi:ms:pt:xn:xo:it#8:\ 291 :RA=\E[?7l:SA=\E?7h:\ 292 :bl=^G:cr=^M:ta=^I:\ 293 :cm=\E[%i%d;%dH:\ 294 :le=^H:up=\E[A:do=\E[B:nd=\E[C:\ 295 :LE=\E[%dD:RI=\E[%dC:UP=\E[%dA:DO=\E[%dB:\ 296 :ho=\E[H:cl=\E[H\E[2J:ce=\E[K:cb=\E[1K:cd=\E[J:sf=\ED:sr=\EM:\ 297 :ct=\E[3g:st=\EH:\ 298 :cs=\E[%i%d;%dr:sc=\E7:rc=\E8:\ 299 :ei=\E[4l:ic=\E[@:IC=\E[%d@:al=\E[L:AL=\E[%dL:\ 300 :dc=\E[P:DC=\E[%dP:dl=\E[M:DL=\E[%dM:\ 301 :so=\E[7m:se=\E[m:us=\E[4m:ue=\E[m:\ 302 :mb=\E[5m:mh=\E[2m:md=\E[1m:mr=\E[7m:me=\E[m:\ 303 :sc=\E7:rc=\E8:kb=\177:\ 304 :ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D: 305 306 307 # Slackware 3.1 linux termcap entry (Sat Apr 27 23:03:58 CDT 1996): 308 lx|linux|console|con80x25|LINUX System Console:\ 309 :do=^J:co#80:li#25:cl=\E[H\E[J:sf=\ED:sb=\EM:\ 310 :le=^H:bs:am:cm=\E[%i%d;%dH:nd=\E[C:up=\E[A:\ 311 :ce=\E[K:cd=\E[J:so=\E[7m:se=\E[27m:us=\E[36m:ue=\E[m:\ 312 :md=\E[1m:mr=\E[7m:mb=\E[5m:me=\E[m:is=\E[1;25r\E[25;1H:\ 313 :ll=\E[1;25r\E[25;1H:al=\E[L:dc=\E[P:dl=\E[M:\ 314 :it#8:ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D:kb=^H:ti=\E[r\E[H:\ 315 :ho=\E[H:kP=\E[5~:kN=\E[6~:kH=\E[4~:kh=\E[1~:kD=\E[3~:kI=\E[2~:\ 316 :k1=\E[[A:k2=\E[[B:k3=\E[[C:k4=\E[[D:k5=\E[[E:k6=\E[17~:\ 317 :F1=\E[23~:F2=\E[24~:\ 318 :k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:K1=\E[1~:K2=\E[5~:\ 319 :K4=\E[4~:K5=\E[6~:\ 320 :pt:sr=\EM:vt#3:xn:km:bl=^G:vi=\E[?25l:ve=\E[?25h:vs=\E[?25h:\ 321 :sc=\E7:rc=\E8:cs=\E[%i%d;%dr:\ 322 :r1=\Ec:r2=\Ec:r3=\Ec: 323 324 # Some other, commonly used linux console entries. 325 lx|con80x28:co#80:li#28:tc=linux: 326 lx|con80x43:co#80:li#43:tc=linux: 327 lx|con80x50:co#80:li#50:tc=linux: 328 lx|con100x37:co#100:li#37:tc=linux: 329 lx|con100x40:co#100:li#40:tc=linux: 330 lx|con132x43:co#132:li#43:tc=linux: 331 332 # vt102 - vt100 + insert line etc. VT102 does not have insert character. 333 v2|vt102|DEC vt102 compatible:\ 334 :co#80:li#24:\ 335 :ic@:IC@:\ 336 :is=\E[m\E[?1l\E>:\ 337 :rs=\E[m\E[?1l\E>:\ 338 :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 339 :ks=:ke=:\ 340 :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:\ 341 :tc=vt-generic: 342 343 # vt100 - really vt102 without insert line, insert char etc. 344 vt|vt100|DEC vt100 compatible:\ 345 :im@:mi@:al@:dl@:ic@:dc@:AL@:DL@:IC@:DC@:\ 346 :tc=vt102: 347 348 349 # Entry for an xterm. Insert mode has been disabled. 350 vs|xterm|tmux|tmux-256color|xterm-kitty|screen|screen.xterm|screen-256color|screen.xterm-256color|xterm-color|xterm-256color|vs100|xterm terminal emulator (X Window System):\ 351 :am:bs:mi@:km:co#80:li#55:\ 352 :im@:ei@:\ 353 :cl=\E[H\E[J:\ 354 :ct=\E[3k:ue=\E[m:\ 355 :is=\E[m\E[?1l\E>:\ 356 :rs=\E[m\E[?1l\E>:\ 357 :vi=\E[?25l:ve=\E[?25h:\ 358 :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 359 :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ 360 :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:\ 361 :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ 362 :F1=\E[23~:F2=\E[24~:\ 363 :kh=\E[H:kH=\E[F:\ 364 :ks=:ke=:\ 365 :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ 366 :tc=vt-generic: 367 368 369 #rxvt, added by me 370 rxvt|rxvt-unicode|rxvt-unicode-256color:\ 371 :am:bs:mi@:km:co#80:li#55:\ 372 :im@:ei@:\ 373 :ct=\E[3k:ue=\E[m:\ 374 :is=\E[m\E[?1l\E>:\ 375 :rs=\E[m\E[?1l\E>:\ 376 :vi=\E[?25l:\ 377 :ve=\E[?25h:\ 378 :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 379 :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ 380 :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ 381 :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ 382 :F1=\E[23~:F2=\E[24~:\ 383 :kh=\E[7~:kH=\E[8~:\ 384 :ks=:ke=:\ 385 :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ 386 :tc=vt-generic: 387 388 389 # Some other entries for the same xterm. 390 v2|xterms|vs100s|xterm small window:\ 391 :co#80:li#24:tc=xterm: 392 vb|xterm-bold|xterm with bold instead of underline:\ 393 :us=\E[1m:tc=xterm: 394 vi|xterm-ins|xterm with insert mode:\ 395 :mi:im=\E[4h:ei=\E[4l:tc=xterm: 396 397 Eterm|Eterm Terminal Emulator (X11 Window System):\ 398 :am:bw:eo:km:mi:ms:xn:xo:\ 399 :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:\ 400 :AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:IC=\E[%d@:\ 401 :K1=\E[7~:K2=\EOu:K3=\E[5~:K4=\E[8~:K5=\E[6~:LE=\E[%dD:\ 402 :RI=\E[%dC:UP=\E[%dA:ae=^O:al=\E[L:as=^N:bl=^G:cd=\E[J:\ 403 :ce=\E[K:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=^M:\ 404 :cs=\E[%i%d;%dr:ct=\E[3g:dc=\E[P:dl=\E[M:do=\E[B:\ 405 :ec=\E[%dX:ei=\E[4l:ho=\E[H:i1=\E[?47l\E>\E[?1l:ic=\E[@:\ 406 :im=\E[4h:is=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l:\ 407 :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ 408 :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:kD=\E[3~:\ 409 :kI=\E[2~:kN=\E[6~:kP=\E[5~:kb=^H:kd=\E[B:ke=:kh=\E[7~:\ 410 :kl=\E[D:kr=\E[C:ks=:ku=\E[A:le=^H:mb=\E[5m:md=\E[1m:\ 411 :me=\E[m\017:mr=\E[7m:nd=\E[C:rc=\E8:\ 412 :sc=\E7:se=\E[27m:sf=^J:so=\E[7m:sr=\EM:st=\EH:ta=^I:\ 413 :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:ue=\E[24m:up=\E[A:\ 414 :us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?25h:vi=\E[?25l:\ 415 :ac=aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~: 416 417 # DOS terminal emulator such as Telix or TeleMate. 418 # This probably also works for the SCO console, though it's incomplete. 419 an|ansi|ansi-bbs|ANSI terminals (emulators):\ 420 :co#80:li#24:am:\ 421 :is=:rs=\Ec:kb=^H:\ 422 :as=\E[m:ae=:eA=:\ 423 :ac=0\333+\257,\256.\031-\030a\261f\370g\361j\331k\277l\332m\300n\305q\304t\264u\303v\301w\302x\263~\025:\ 424 :kD=\177:kH=\E[Y:kN=\E[U:kP=\E[V:kh=\E[H:\ 425 :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\EOT:\ 426 :k6=\EOU:k7=\EOV:k8=\EOW:k9=\EOX:k0=\EOY:\ 427 :tc=vt-generic: 428 429 `; 430 } else { 431 enum UseVtSequences = false; 432 } 433 434 /// A modifier for [Color] 435 enum Bright = 0x08; 436 437 /// Defines the list of standard colors understood by Terminal. 438 /// See also: [Bright] 439 enum Color : ushort { 440 black = 0, /// . 441 red = RED_BIT, /// . 442 green = GREEN_BIT, /// . 443 yellow = red | green, /// . 444 blue = BLUE_BIT, /// . 445 magenta = red | blue, /// . 446 cyan = blue | green, /// . 447 white = red | green | blue, /// . 448 DEFAULT = 256, 449 } 450 451 /// When capturing input, what events are you interested in? 452 /// 453 /// Note: these flags can be OR'd together to select more than one option at a time. 454 /// 455 /// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw. 456 /// 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. 457 enum ConsoleInputFlags { 458 raw = 0, /// raw input returns keystrokes immediately, without line buffering 459 echo = 1, /// do you want to automatically echo input back to the user? 460 mouse = 2, /// capture mouse events 461 paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes) 462 size = 8, /// window resize events 463 464 releasedKeys = 64, /// key release events. Not reliable on Posix. 465 466 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. 467 allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events. 468 469 noEolWrap = 128, 470 } 471 472 /// Defines how terminal output should be handled. 473 enum ConsoleOutputType { 474 linear = 0, /// do you want output to work one line at a time? 475 cellular = 1, /// or do you want access to the terminal screen as a grid of characters? 476 //truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges 477 478 minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here 479 } 480 481 alias ConsoleOutputMode = ConsoleOutputType; 482 483 /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present 484 enum ForceOption { 485 automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) 486 neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution. 487 alwaysSend = 1, /// always send the data, even if it doesn't seem necessary 488 } 489 490 /// 491 enum TerminalCursor { 492 DEFAULT = 0, /// 493 insert = 1, /// 494 block = 2 /// 495 } 496 497 // we could do it with termcap too, getenv("TERMCAP") then split on : and replace \E with \033 and get the pieces 498 499 /// Encapsulates the I/O capabilities of a terminal. 500 /// 501 /// Warning: do not write out escape sequences to the terminal. This won't work 502 /// on Windows and will confuse Terminal's internal state on Posix. 503 struct Terminal { 504 /// 505 @disable this(); 506 @disable this(this); 507 private ConsoleOutputType type; 508 509 version(TerminalDirectToEmulator) { 510 private bool windowSizeChanged = false; 511 private 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 512 private bool hangedUp = false; /// similar to interrupted. 513 } 514 515 private TerminalCursor currentCursor_; 516 version(Windows) private CONSOLE_CURSOR_INFO originalCursorInfo; 517 518 /++ 519 Changes the current cursor. 520 +/ 521 void cursor(TerminalCursor what, ForceOption force = ForceOption.automatic) { 522 if(force == ForceOption.neverSend) { 523 currentCursor_ = what; 524 return; 525 } else { 526 if(what != currentCursor_ || force == ForceOption.alwaysSend) { 527 currentCursor_ = what; 528 version(Win32Console) { 529 final switch(what) { 530 case TerminalCursor.DEFAULT: 531 SetConsoleCursorInfo(hConsole, &originalCursorInfo); 532 break; 533 case TerminalCursor.insert: 534 case TerminalCursor.block: 535 CONSOLE_CURSOR_INFO info; 536 GetConsoleCursorInfo(hConsole, &info); 537 info.dwSize = what == TerminalCursor.insert ? 1 : 100; 538 SetConsoleCursorInfo(hConsole, &info); 539 break; 540 } 541 } else { 542 final switch(what) { 543 case TerminalCursor.DEFAULT: 544 if(terminalInFamily("linux")) 545 writeStringRaw("\033[?0c"); 546 else 547 writeStringRaw("\033[0 q"); 548 break; 549 case TerminalCursor.insert: 550 if(terminalInFamily("linux")) 551 writeStringRaw("\033[?2c"); 552 else if(terminalInFamily("xterm")) 553 writeStringRaw("\033[6 q"); 554 else 555 writeStringRaw("\033[4 q"); 556 break; 557 case TerminalCursor.block: 558 if(terminalInFamily("linux")) 559 writeStringRaw("\033[?6c"); 560 else 561 writeStringRaw("\033[2 q"); 562 break; 563 } 564 } 565 } 566 } 567 } 568 569 /++ 570 Terminal is only valid to use on an actual console device or terminal 571 handle. You should not attempt to construct a Terminal instance if this 572 returns false. Real time input is similarly impossible if `!stdinIsTerminal`. 573 +/ 574 static bool stdoutIsTerminal() { 575 version(TerminalDirectToEmulator) { 576 version(Windows) { 577 // if it is null, it was a gui subsystem exe. But otherwise, it 578 // might be explicitly redirected and we should respect that for 579 // compatibility with normal console expectations (even though like 580 // we COULD pop up a gui and do both, really that isn't the normal 581 // use of this library so don't wanna go too nuts) 582 auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 583 return hConsole is null || GetFileType(hConsole) == FILE_TYPE_CHAR; 584 } else version(Posix) { 585 // same as normal here since thee is no gui subsystem really 586 import core.sys.posix.unistd; 587 return cast(bool) isatty(1); 588 } else static assert(0); 589 } else version(Posix) { 590 import core.sys.posix.unistd; 591 return cast(bool) isatty(1); 592 } else version(Win32Console) { 593 auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 594 return GetFileType(hConsole) == FILE_TYPE_CHAR; 595 /+ 596 auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 597 CONSOLE_SCREEN_BUFFER_INFO originalSbi; 598 if(GetConsoleScreenBufferInfo(hConsole, &originalSbi) == 0) 599 return false; 600 else 601 return true; 602 +/ 603 } else static assert(0); 604 } 605 606 /// 607 static bool stdinIsTerminal() { 608 version(TerminalDirectToEmulator) { 609 version(Windows) { 610 auto hConsole = GetStdHandle(STD_INPUT_HANDLE); 611 return hConsole is null || GetFileType(hConsole) == FILE_TYPE_CHAR; 612 } else version(Posix) { 613 // same as normal here since thee is no gui subsystem really 614 import core.sys.posix.unistd; 615 return cast(bool) isatty(0); 616 } else static assert(0); 617 } else version(Posix) { 618 import core.sys.posix.unistd; 619 return cast(bool) isatty(0); 620 } else version(Win32Console) { 621 auto hConsole = GetStdHandle(STD_INPUT_HANDLE); 622 return GetFileType(hConsole) == FILE_TYPE_CHAR; 623 } else static assert(0); 624 } 625 626 version(Posix) { 627 private int fdOut; 628 private int fdIn; 629 private int[] delegate() getSizeOverride; 630 void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically 631 } 632 633 bool terminalInFamily(string[] terms...) { 634 import std.process; 635 import std.string; 636 version(TerminalDirectToEmulator) 637 auto term = "xterm"; 638 else 639 auto term = environment.get("TERM"); 640 foreach(t; terms) 641 if(indexOf(term, t) != -1) 642 return true; 643 644 return false; 645 } 646 647 version(Posix) { 648 // This is a filthy hack because Terminal.app and OS X are garbage who don't 649 // work the way they're advertised. I just have to best-guess hack and hope it 650 // doesn't break anything else. (If you know a better way, let me know!) 651 bool isMacTerminal() { 652 // it gives 1,2 in getTerminalCapabilities... 653 // FIXME 654 import std.process; 655 import std.string; 656 auto term = environment.get("TERM"); 657 return term == "xterm-256color"; 658 } 659 } else 660 bool isMacTerminal() { return false; } 661 662 static string[string] termcapDatabase; 663 static void readTermcapFile(bool useBuiltinTermcap = false) { 664 import std.file; 665 import std.stdio; 666 import std.string; 667 668 //if(!exists("/etc/termcap")) 669 useBuiltinTermcap = true; 670 671 string current; 672 673 void commitCurrentEntry() { 674 if(current is null) 675 return; 676 677 string names = current; 678 auto idx = indexOf(names, ":"); 679 if(idx != -1) 680 names = names[0 .. idx]; 681 682 foreach(name; split(names, "|")) 683 termcapDatabase[name] = current; 684 685 current = null; 686 } 687 688 void handleTermcapLine(in char[] line) { 689 if(line.length == 0) { // blank 690 commitCurrentEntry(); 691 return; // continue 692 } 693 if(line[0] == '#') // comment 694 return; // continue 695 size_t termination = line.length; 696 if(line[$-1] == '\\') 697 termination--; // cut off the \\ 698 current ~= strip(line[0 .. termination]); 699 // termcap entries must be on one logical line, so if it isn't continued, we know we're done 700 if(line[$-1] != '\\') 701 commitCurrentEntry(); 702 } 703 704 if(useBuiltinTermcap) { 705 version(VtEscapeCodes) 706 foreach(line; splitLines(builtinTermcap)) { 707 handleTermcapLine(line); 708 } 709 } else { 710 foreach(line; File("/etc/termcap").byLine()) { 711 handleTermcapLine(line); 712 } 713 } 714 } 715 716 static string getTermcapDatabase(string terminal) { 717 import std.string; 718 719 if(termcapDatabase is null) 720 readTermcapFile(); 721 722 auto data = terminal in termcapDatabase; 723 if(data is null) 724 return null; 725 726 auto tc = *data; 727 auto more = indexOf(tc, ":tc="); 728 if(more != -1) { 729 auto tcKey = tc[more + ":tc=".length .. $]; 730 auto end = indexOf(tcKey, ":"); 731 if(end != -1) 732 tcKey = tcKey[0 .. end]; 733 tc = getTermcapDatabase(tcKey) ~ tc; 734 } 735 736 return tc; 737 } 738 739 string[string] termcap; 740 void readTermcap(string t = null) { 741 version(TerminalDirectToEmulator) 742 if(usingDirectEmulator) 743 t = "xterm"; 744 import std.process; 745 import std.string; 746 import std.array; 747 748 string termcapData = environment.get("TERMCAP"); 749 if(termcapData.length == 0) { 750 if(t is null) { 751 t = environment.get("TERM"); 752 } 753 754 // loosen the check so any xterm variety gets 755 // the same termcap. odds are this is right 756 // almost always 757 if(t.indexOf("xterm") != -1) 758 t = "xterm"; 759 if(t.indexOf("putty") != -1) 760 t = "xterm"; 761 if(t.indexOf("tmux") != -1) 762 t = "tmux"; 763 if(t.indexOf("screen") != -1) 764 t = "screen"; 765 766 termcapData = getTermcapDatabase(t); 767 } 768 769 auto e = replace(termcapData, "\\\n", "\n"); 770 termcap = null; 771 772 foreach(part; split(e, ":")) { 773 // FIXME: handle numeric things too 774 775 auto things = split(part, "="); 776 if(things.length) 777 termcap[things[0]] = 778 things.length > 1 ? things[1] : null; 779 } 780 } 781 782 string findSequenceInTermcap(in char[] sequenceIn) { 783 char[10] sequenceBuffer; 784 char[] sequence; 785 if(sequenceIn.length > 0 && sequenceIn[0] == '\033') { 786 if(!(sequenceIn.length < sequenceBuffer.length - 1)) 787 return null; 788 sequenceBuffer[1 .. sequenceIn.length + 1] = sequenceIn[]; 789 sequenceBuffer[0] = '\\'; 790 sequenceBuffer[1] = 'E'; 791 sequence = sequenceBuffer[0 .. sequenceIn.length + 1]; 792 } else { 793 sequence = sequenceBuffer[1 .. sequenceIn.length + 1]; 794 } 795 796 import std.array; 797 foreach(k, v; termcap) 798 if(v == sequence) 799 return k; 800 return null; 801 } 802 803 string getTermcap(string key) { 804 auto k = key in termcap; 805 if(k !is null) return *k; 806 return null; 807 } 808 809 // Looks up a termcap item and tries to execute it. Returns false on failure 810 bool doTermcap(T...)(string key, T t) { 811 import std.conv; 812 auto fs = getTermcap(key); 813 if(fs is null) 814 return false; 815 816 int swapNextTwo = 0; 817 818 R getArg(R)(int idx) { 819 if(swapNextTwo == 2) { 820 idx ++; 821 swapNextTwo--; 822 } else if(swapNextTwo == 1) { 823 idx --; 824 swapNextTwo--; 825 } 826 827 foreach(i, arg; t) { 828 if(i == idx) 829 return to!R(arg); 830 } 831 assert(0, to!string(idx) ~ " is out of bounds working " ~ fs); 832 } 833 834 char[256] buffer; 835 int bufferPos = 0; 836 837 void addChar(char c) { 838 import std.exception; 839 enforce(bufferPos < buffer.length); 840 buffer[bufferPos++] = c; 841 } 842 843 void addString(in char[] c) { 844 import std.exception; 845 enforce(bufferPos + c.length < buffer.length); 846 buffer[bufferPos .. bufferPos + c.length] = c[]; 847 bufferPos += c.length; 848 } 849 850 void addInt(int c, int minSize) { 851 import std.string; 852 auto str = format("%0"~(minSize ? to!string(minSize) : "")~"d", c); 853 addString(str); 854 } 855 856 bool inPercent; 857 int argPosition = 0; 858 int incrementParams = 0; 859 bool skipNext; 860 bool nextIsChar; 861 bool inBackslash; 862 863 foreach(char c; fs) { 864 if(inBackslash) { 865 if(c == 'E') 866 addChar('\033'); 867 else 868 addChar(c); 869 inBackslash = false; 870 } else if(nextIsChar) { 871 if(skipNext) 872 skipNext = false; 873 else 874 addChar(cast(char) (c + getArg!int(argPosition) + (incrementParams ? 1 : 0))); 875 if(incrementParams) incrementParams--; 876 argPosition++; 877 inPercent = false; 878 } else if(inPercent) { 879 switch(c) { 880 case '%': 881 addChar('%'); 882 inPercent = false; 883 break; 884 case '2': 885 case '3': 886 case 'd': 887 if(skipNext) 888 skipNext = false; 889 else 890 addInt(getArg!int(argPosition) + (incrementParams ? 1 : 0), 891 c == 'd' ? 0 : (c - '0') 892 ); 893 if(incrementParams) incrementParams--; 894 argPosition++; 895 inPercent = false; 896 break; 897 case '.': 898 if(skipNext) 899 skipNext = false; 900 else 901 addChar(cast(char) (getArg!int(argPosition) + (incrementParams ? 1 : 0))); 902 if(incrementParams) incrementParams--; 903 argPosition++; 904 break; 905 case '+': 906 nextIsChar = true; 907 inPercent = false; 908 break; 909 case 'i': 910 incrementParams = 2; 911 inPercent = false; 912 break; 913 case 's': 914 skipNext = true; 915 inPercent = false; 916 break; 917 case 'b': 918 argPosition--; 919 inPercent = false; 920 break; 921 case 'r': 922 swapNextTwo = 2; 923 inPercent = false; 924 break; 925 // FIXME: there's more 926 // http://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html 927 928 default: 929 assert(0, "not supported " ~ c); 930 } 931 } else { 932 if(c == '%') 933 inPercent = true; 934 else if(c == '\\') 935 inBackslash = true; 936 else 937 addChar(c); 938 } 939 } 940 941 writeStringRaw(buffer[0 .. bufferPos]); 942 return true; 943 } 944 945 uint tcaps; 946 947 bool inlineImagesSupported() { 948 return (tcaps & TerminalCapabilities.arsdImage) ? true : false; 949 } 950 bool clipboardSupported() { 951 version(Win32Console) return true; 952 else return (tcaps & TerminalCapabilities.arsdImage) ? true : false; 953 } 954 955 // only supported on my custom terminal emulator. guarded behind if(inlineImagesSupported) 956 // though that isn't even 100% accurate but meh 957 void changeWindowIcon()(string filename) { 958 if(inlineImagesSupported()) { 959 import arsd.png; 960 auto image = readPng(filename); 961 auto ii = cast(IndexedImage) image; 962 assert(ii !is null); 963 964 // copy/pasted from my terminalemulator.d 965 string encodeSmallTextImage(IndexedImage ii) { 966 char encodeNumeric(int c) { 967 if(c < 10) 968 return cast(char)(c + '0'); 969 if(c < 10 + 26) 970 return cast(char)(c - 10 + 'a'); 971 assert(0); 972 } 973 974 string s; 975 s ~= encodeNumeric(ii.width); 976 s ~= encodeNumeric(ii.height); 977 978 foreach(entry; ii.palette) 979 s ~= entry.toRgbaHexString(); 980 s ~= "Z"; 981 982 ubyte rleByte; 983 int rleCount; 984 985 void rleCommit() { 986 if(rleByte >= 26) 987 assert(0); // too many colors for us to handle 988 if(rleCount == 0) 989 goto finish; 990 if(rleCount == 1) { 991 s ~= rleByte + 'a'; 992 goto finish; 993 } 994 995 import std.conv; 996 s ~= to!string(rleCount); 997 s ~= rleByte + 'a'; 998 999 finish: 1000 rleByte = 0; 1001 rleCount = 0; 1002 } 1003 1004 foreach(b; ii.data) { 1005 if(b == rleByte) 1006 rleCount++; 1007 else { 1008 rleCommit(); 1009 rleByte = b; 1010 rleCount = 1; 1011 } 1012 } 1013 1014 rleCommit(); 1015 1016 return s; 1017 } 1018 1019 this.writeStringRaw("\033]5000;"~encodeSmallTextImage(ii)~"\007"); 1020 } 1021 } 1022 1023 // dependent on tcaps... 1024 void displayInlineImage()(ubyte[] imageData) { 1025 if(inlineImagesSupported) { 1026 import std.base64; 1027 1028 // I might change this protocol later! 1029 enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:"; 1030 1031 this.writeStringRaw("\000"); 1032 this.writeStringRaw(extensionMagicIdentifier); 1033 this.writeStringRaw(Base64.encode(imageData)); 1034 this.writeStringRaw("\000"); 1035 } 1036 } 1037 1038 void demandUserAttention() { 1039 if(UseVtSequences) { 1040 if(!terminalInFamily("linux")) 1041 writeStringRaw("\033]5001;1\007"); 1042 } 1043 } 1044 1045 void requestCopyToClipboard(string text) { 1046 if(clipboardSupported) { 1047 import std.base64; 1048 writeStringRaw("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007"); 1049 } 1050 } 1051 1052 void requestCopyToPrimary(string text) { 1053 if(clipboardSupported) { 1054 import std.base64; 1055 writeStringRaw("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007"); 1056 } 1057 } 1058 1059 bool hasDefaultDarkBackground() { 1060 version(Win32Console) { 1061 return !(defaultBackgroundColor & 0xf); 1062 } else { 1063 version(TerminalDirectToEmulator) 1064 if(usingDirectEmulator) 1065 return integratedTerminalEmulatorConfiguration.defaultBackground.g < 100; 1066 // FIXME: there is probably a better way to do this 1067 // but like idk how reliable it is. 1068 if(terminalInFamily("linux")) 1069 return true; 1070 else 1071 return false; 1072 } 1073 } 1074 1075 version(TerminalDirectToEmulator) { 1076 TerminalEmulatorWidget tew; 1077 private __gshared Window mainWindow; 1078 import core.thread; 1079 version(Posix) 1080 ThreadID threadId; 1081 else version(Windows) 1082 HANDLE threadId; 1083 private __gshared Thread guiThread; 1084 1085 private static class NewTerminalEvent { 1086 Terminal* t; 1087 this(Terminal* t) { 1088 this.t = t; 1089 } 1090 } 1091 1092 bool usingDirectEmulator; 1093 } 1094 1095 version(TerminalDirectToEmulator) 1096 /++ 1097 +/ 1098 this(ConsoleOutputType type) { 1099 this.type = type; 1100 1101 if(type == ConsoleOutputType.minimalProcessing) { 1102 readTermcap("xterm"); 1103 _suppressDestruction = true; 1104 return; 1105 } 1106 1107 import arsd.simpledisplay; 1108 static if(UsingSimpledisplayX11) { 1109 try { 1110 if(arsd.simpledisplay.librariesSuccessfullyLoaded) { 1111 XDisplayConnection.get(); 1112 this.usingDirectEmulator = true; 1113 } else if(!integratedTerminalEmulatorConfiguration.fallbackToDegradedTerminal) { 1114 throw new Exception("Unable to load X libraries to create custom terminal."); 1115 } 1116 } catch(Exception e) { 1117 if(!integratedTerminalEmulatorConfiguration.fallbackToDegradedTerminal) 1118 throw e; 1119 1120 } 1121 } else { 1122 this.usingDirectEmulator = true; 1123 } 1124 1125 if(!usingDirectEmulator) { 1126 version(Posix) { 1127 posixInitialize(type, 0, 1, null); 1128 return; 1129 } else { 1130 throw new Exception("Total wtf - are you on a windows system without a gui?!?"); 1131 } 1132 assert(0); 1133 } 1134 1135 tcaps = uint.max; // all capabilities 1136 import core.thread; 1137 1138 version(Posix) 1139 threadId = Thread.getThis.id; 1140 else version(Windows) 1141 threadId = GetCurrentThread(); 1142 1143 if(guiThread is null) { 1144 guiThread = new Thread( { 1145 auto window = new TerminalEmulatorWindow(&this, null); 1146 mainWindow = window; 1147 mainWindow.win.addEventListener((NewTerminalEvent t) { 1148 auto nw = new TerminalEmulatorWindow(t.t, null); 1149 t.t.tew = nw.tew; 1150 t.t = null; 1151 nw.show(); 1152 }); 1153 tew = window.tew; 1154 //try 1155 window.loop(); 1156 /* 1157 catch(Throwable t) { 1158 import std.stdio; 1159 stdout.writeln(t); 1160 stdout.flush(); 1161 } 1162 */ 1163 }); 1164 guiThread.start(); 1165 guiThread.priority = Thread.PRIORITY_MAX; // gui thread needs responsiveness 1166 } else { 1167 // FIXME: 64 bit builds on linux segfault with multiple terminals 1168 // so that isn't really supported as of yet. 1169 while(cast(shared) mainWindow is null) { 1170 import core.thread; 1171 Thread.sleep(5.msecs); 1172 } 1173 mainWindow.win.postEvent(new NewTerminalEvent(&this)); 1174 } 1175 1176 // need to wait until it is properly initialized 1177 while(cast(shared) tew is null) { 1178 import core.thread; 1179 Thread.sleep(5.msecs); 1180 } 1181 1182 initializeVt(); 1183 1184 } 1185 else 1186 1187 version(Posix) 1188 /** 1189 * Constructs an instance of Terminal representing the capabilities of 1190 * the current terminal. 1191 * 1192 * While it is possible to override the stdin+stdout file descriptors, remember 1193 * that is not portable across platforms and be sure you know what you're doing. 1194 * 1195 * ditto on getSizeOverride. That's there so you can do something instead of ioctl. 1196 */ 1197 this(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { 1198 posixInitialize(type, fdIn, fdOut, getSizeOverride); 1199 } 1200 1201 version(Posix) 1202 private void posixInitialize(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { 1203 this.fdIn = fdIn; 1204 this.fdOut = fdOut; 1205 this.getSizeOverride = getSizeOverride; 1206 this.type = type; 1207 1208 if(type == ConsoleOutputType.minimalProcessing) { 1209 readTermcap(); 1210 _suppressDestruction = true; 1211 return; 1212 } 1213 1214 tcaps = getTerminalCapabilities(fdIn, fdOut); 1215 //writeln(tcaps); 1216 1217 initializeVt(); 1218 } 1219 1220 void initializeVt() { 1221 readTermcap(); 1222 1223 if(type == ConsoleOutputType.cellular) { 1224 doTermcap("ti"); 1225 clear(); 1226 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 1227 } 1228 1229 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 1230 writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it) 1231 } 1232 1233 } 1234 1235 // EXPERIMENTAL do not use yet 1236 Terminal alternateScreen() { 1237 assert(this.type != ConsoleOutputType.cellular); 1238 1239 this.flush(); 1240 return Terminal(ConsoleOutputType.cellular); 1241 } 1242 1243 version(Windows) { 1244 HANDLE hConsole; 1245 CONSOLE_SCREEN_BUFFER_INFO originalSbi; 1246 } 1247 1248 version(Win32Console) 1249 /// ditto 1250 this(ConsoleOutputType type) { 1251 if(UseVtSequences) { 1252 hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 1253 initializeVt(); 1254 } else { 1255 if(type == ConsoleOutputType.cellular) { 1256 hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, null, CONSOLE_TEXTMODE_BUFFER, null); 1257 if(hConsole == INVALID_HANDLE_VALUE) { 1258 import std.conv; 1259 throw new Exception(to!string(GetLastError())); 1260 } 1261 1262 SetConsoleActiveScreenBuffer(hConsole); 1263 /* 1264 http://msdn.microsoft.com/en-us/library/windows/desktop/ms686125%28v=vs.85%29.aspx 1265 http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.aspx 1266 */ 1267 COORD size; 1268 /* 1269 CONSOLE_SCREEN_BUFFER_INFO sbi; 1270 GetConsoleScreenBufferInfo(hConsole, &sbi); 1271 size.X = cast(short) GetSystemMetrics(SM_CXMIN); 1272 size.Y = cast(short) GetSystemMetrics(SM_CYMIN); 1273 */ 1274 1275 // FIXME: this sucks, maybe i should just revert it. but there shouldn't be scrollbars in cellular mode 1276 //size.X = 80; 1277 //size.Y = 24; 1278 //SetConsoleScreenBufferSize(hConsole, size); 1279 1280 GetConsoleCursorInfo(hConsole, &originalCursorInfo); 1281 1282 clear(); 1283 } else { 1284 hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 1285 } 1286 1287 if(GetConsoleScreenBufferInfo(hConsole, &originalSbi) == 0) 1288 throw new Exception("not a user-interactive terminal"); 1289 1290 defaultForegroundColor = cast(Color) (originalSbi.wAttributes & 0x0f); 1291 defaultBackgroundColor = cast(Color) ((originalSbi.wAttributes >> 4) & 0x0f); 1292 1293 // this is unnecessary since I use the W versions of other functions 1294 // and can cause weird font bugs, so I'm commenting unless some other 1295 // need comes up. 1296 /* 1297 oldCp = GetConsoleOutputCP(); 1298 SetConsoleOutputCP(65001); // UTF-8 1299 1300 oldCpIn = GetConsoleCP(); 1301 SetConsoleCP(65001); // UTF-8 1302 */ 1303 } 1304 } 1305 1306 version(Win32Console) { 1307 private Color defaultBackgroundColor = Color.black; 1308 private Color defaultForegroundColor = Color.white; 1309 UINT oldCp; 1310 UINT oldCpIn; 1311 } 1312 1313 // 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... 1314 bool _suppressDestruction; 1315 1316 ~this() { 1317 if(_suppressDestruction) { 1318 flush(); 1319 return; 1320 } 1321 1322 if(UseVtSequences) { 1323 if(type == ConsoleOutputType.cellular) { 1324 doTermcap("te"); 1325 } 1326 version(TerminalDirectToEmulator) { 1327 if(usingDirectEmulator) { 1328 writeln("\n\n<exited>"); 1329 setTitle(tew.terminalEmulator.currentTitle ~ " <exited>"); 1330 tew.term = null; 1331 1332 if(integratedTerminalEmulatorConfiguration.closeOnExit) 1333 tew.parentWindow.close(); 1334 } else { 1335 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 1336 writeStringRaw("\033[23;0t"); // restore window title from the stack 1337 } 1338 } 1339 } else 1340 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 1341 writeStringRaw("\033[23;0t"); // restore window title from the stack 1342 } 1343 cursor = TerminalCursor.DEFAULT; 1344 showCursor(); 1345 reset(); 1346 flush(); 1347 1348 if(lineGetter !is null) 1349 lineGetter.dispose(); 1350 } else version(Win32Console) { 1351 flush(); // make sure user data is all flushed before resetting 1352 reset(); 1353 showCursor(); 1354 1355 if(lineGetter !is null) 1356 lineGetter.dispose(); 1357 1358 1359 SetConsoleOutputCP(oldCp); 1360 SetConsoleCP(oldCpIn); 1361 1362 auto stdo = GetStdHandle(STD_OUTPUT_HANDLE); 1363 SetConsoleActiveScreenBuffer(stdo); 1364 if(hConsole !is stdo) 1365 CloseHandle(hConsole); 1366 } 1367 } 1368 1369 // lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit) 1370 // and some history storage. 1371 LineGetter lineGetter; 1372 1373 int _currentForeground = Color.DEFAULT; 1374 int _currentBackground = Color.DEFAULT; 1375 RGB _currentForegroundRGB; 1376 RGB _currentBackgroundRGB; 1377 bool reverseVideo = false; 1378 1379 /++ 1380 Attempts to set color according to a 24 bit value (r, g, b, each >= 0 and < 256). 1381 1382 1383 This is not supported on all terminals. It will attempt to fall back to a 256-color 1384 or 8-color palette in those cases automatically. 1385 1386 Returns: true if it believes it was successful (note that it cannot be completely sure), 1387 false if it had to use a fallback. 1388 +/ 1389 bool setTrueColor(RGB foreground, RGB background, ForceOption force = ForceOption.automatic) { 1390 if(force == ForceOption.neverSend) { 1391 _currentForeground = -1; 1392 _currentBackground = -1; 1393 _currentForegroundRGB = foreground; 1394 _currentBackgroundRGB = background; 1395 return true; 1396 } 1397 1398 if(force == ForceOption.automatic && _currentForeground == -1 && _currentBackground == -1 && (_currentForegroundRGB == foreground && _currentBackgroundRGB == background)) 1399 return true; 1400 1401 _currentForeground = -1; 1402 _currentBackground = -1; 1403 _currentForegroundRGB = foreground; 1404 _currentBackgroundRGB = background; 1405 1406 version(Win32Console) { 1407 flush(); 1408 ushort setTob = cast(ushort) approximate16Color(background); 1409 ushort setTof = cast(ushort) approximate16Color(foreground); 1410 SetConsoleTextAttribute( 1411 hConsole, 1412 cast(ushort)((setTob << 4) | setTof)); 1413 return false; 1414 } else { 1415 // FIXME: if the terminal reliably does support 24 bit color, use it 1416 // instead of the round off. But idk how to detect that yet... 1417 1418 // fallback to 16 color for term that i know don't take it well 1419 import std.process; 1420 import std.string; 1421 version(TerminalDirectToEmulator) 1422 if(usingDirectEmulator) 1423 goto skip_approximation; 1424 1425 if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") { 1426 // not likely supported, use 16 color fallback 1427 auto setTof = approximate16Color(foreground); 1428 auto setTob = approximate16Color(background); 1429 1430 writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm", 1431 (setTof & Bright) ? 1 : 0, 1432 cast(int) (setTof & ~Bright), 1433 cast(int) (setTob & ~Bright) 1434 )); 1435 1436 return false; 1437 } 1438 1439 skip_approximation: 1440 1441 // otherwise, assume it is probably supported and give it a try 1442 writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm", 1443 colorToXTermPaletteIndex(foreground), 1444 colorToXTermPaletteIndex(background) 1445 )); 1446 1447 /+ // this is the full 24 bit color sequence 1448 writeStringRaw(format("\033[38;2;%d;%d;%dm", foreground.r, foreground.g, foreground.b)); 1449 writeStringRaw(format("\033[48;2;%d;%d;%dm", background.r, background.g, background.b)); 1450 +/ 1451 1452 return true; 1453 } 1454 } 1455 1456 /// Changes the current color. See enum Color for the values. 1457 void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { 1458 if(force != ForceOption.neverSend) { 1459 version(Win32Console) { 1460 // assuming a dark background on windows, so LowContrast == dark which means the bit is NOT set on hardware 1461 /* 1462 foreground ^= LowContrast; 1463 background ^= LowContrast; 1464 */ 1465 1466 ushort setTof = cast(ushort) foreground; 1467 ushort setTob = cast(ushort) background; 1468 1469 // this isn't necessarily right but meh 1470 if(background == Color.DEFAULT) 1471 setTob = defaultBackgroundColor; 1472 if(foreground == Color.DEFAULT) 1473 setTof = defaultForegroundColor; 1474 1475 if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 1476 flush(); // if we don't do this now, the buffering can screw up the colors... 1477 if(reverseVideo) { 1478 if(background == Color.DEFAULT) 1479 setTof = defaultBackgroundColor; 1480 else 1481 setTof = cast(ushort) background | (foreground & Bright); 1482 1483 if(background == Color.DEFAULT) 1484 setTob = defaultForegroundColor; 1485 else 1486 setTob = cast(ushort) (foreground & ~Bright); 1487 } 1488 SetConsoleTextAttribute( 1489 hConsole, 1490 cast(ushort)((setTob << 4) | setTof)); 1491 } 1492 } else { 1493 import std.process; 1494 // I started using this envvar for my text editor, but now use it elsewhere too 1495 // if we aren't set to dark, assume light 1496 /* 1497 if(getenv("ELVISBG") == "dark") { 1498 // LowContrast on dark bg menas 1499 } else { 1500 foreground ^= LowContrast; 1501 background ^= LowContrast; 1502 } 1503 */ 1504 1505 ushort setTof = cast(ushort) foreground & ~Bright; 1506 ushort setTob = cast(ushort) background & ~Bright; 1507 1508 if(foreground & Color.DEFAULT) 1509 setTof = 9; // ansi sequence for reset 1510 if(background == Color.DEFAULT) 1511 setTob = 9; 1512 1513 import std.string; 1514 1515 if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 1516 writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm\033[%dm", 1517 (foreground != Color.DEFAULT && (foreground & Bright)) ? 1 : 0, 1518 cast(int) setTof, 1519 cast(int) setTob, 1520 reverseVideo ? 7 : 27 1521 )); 1522 } 1523 } 1524 } 1525 1526 _currentForeground = foreground; 1527 _currentBackground = background; 1528 this.reverseVideo = reverseVideo; 1529 } 1530 1531 private bool _underlined = false; 1532 1533 /++ 1534 Outputs a hyperlink to my custom terminal (v0.0.7 or later) or to version 1535 `TerminalDirectToEmulator`. The way it works is a bit strange... 1536 1537 1538 If using a terminal that supports it, it outputs the given text with the 1539 given identifier attached (one bit of identifier per grapheme of text!). When 1540 the user clicks on it, it will send a [LinkEvent] with the text and the identifier 1541 for you to respond, if in real-time input mode, or a simple paste event with the 1542 text if not (you will not be able to distinguish this from a user pasting the 1543 same text). 1544 1545 If the user's terminal does not support my feature, it writes plain text instead. 1546 1547 It is important that you make sure your program still works even if the hyperlinks 1548 never work - ideally, make them out of text the user can type manually or copy/paste 1549 into your command line somehow too. 1550 1551 Hyperlinks may not work correctly after your program exits or if you are capturing 1552 mouse input (the user will have to hold shift in that case). It is really designed 1553 for linear mode with direct to emulator mode. If you are using cellular mode with 1554 full input capturing, you should manage the clicks yourself. 1555 1556 Similarly, if it horizontally scrolls off the screen, it can be corrupted since it 1557 packs your text and identifier into free bits in the screen buffer itself. I may be 1558 able to fix that later. 1559 1560 Params: 1561 text = text displayed in the terminal 1562 identifier = an additional number attached to the text and returned to you in a [LinkEvent] 1563 autoStyle = set to `false` to suppress the automatic color and underlining of the text. 1564 1565 Bugs: 1566 there's no keyboard interaction with it at all right now. i might make the terminal 1567 emulator offer the ids or something through a hold ctrl or something interface. idk. 1568 or tap ctrl twice to turn that on. 1569 1570 History: 1571 Added March 18, 2020 1572 +/ 1573 void hyperlink(string text, ushort identifier = 0, bool autoStyle = true) { 1574 if((tcaps & TerminalCapabilities.arsdHyperlinks)) { 1575 bool previouslyUnderlined = _underlined; 1576 int fg = _currentForeground, bg = _currentBackground; 1577 if(autoStyle) { 1578 color(Color.blue, Color.white); 1579 underline = true; 1580 } 1581 1582 import std.conv; 1583 writeStringRaw("\033[?" ~ to!string(65536 + identifier) ~ "h"); 1584 write(text); 1585 writeStringRaw("\033[?65536l"); 1586 1587 if(autoStyle) { 1588 underline = previouslyUnderlined; 1589 color(fg, bg); 1590 } 1591 } else { 1592 write(text); // graceful degrade 1593 } 1594 } 1595 1596 /// Note: the Windows console does not support underlining 1597 void underline(bool set, ForceOption force = ForceOption.automatic) { 1598 if(set == _underlined && force != ForceOption.alwaysSend) 1599 return; 1600 if(UseVtSequences) { 1601 if(set) 1602 writeStringRaw("\033[4m"); 1603 else 1604 writeStringRaw("\033[24m"); 1605 } 1606 _underlined = set; 1607 } 1608 // FIXME: do I want to do bold and italic? 1609 1610 /// Returns the terminal to normal output colors 1611 void reset() { 1612 version(Win32Console) 1613 SetConsoleTextAttribute( 1614 hConsole, 1615 originalSbi.wAttributes); 1616 else 1617 writeStringRaw("\033[0m"); 1618 1619 _underlined = false; 1620 _currentForeground = Color.DEFAULT; 1621 _currentBackground = Color.DEFAULT; 1622 reverseVideo = false; 1623 } 1624 1625 // FIXME: add moveRelative 1626 1627 /// The current x position of the output cursor. 0 == leftmost column 1628 @property int cursorX() { 1629 return _cursorX; 1630 } 1631 1632 /// The current y position of the output cursor. 0 == topmost row 1633 @property int cursorY() { 1634 return _cursorY; 1635 } 1636 1637 private int _cursorX; 1638 private int _cursorY; 1639 1640 /// 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 1641 void moveTo(int x, int y, ForceOption force = ForceOption.automatic) { 1642 if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) { 1643 executeAutoHideCursor(); 1644 if(UseVtSequences) { 1645 doTermcap("cm", y, x); 1646 } else version(Win32Console) { 1647 1648 flush(); // if we don't do this now, the buffering can screw up the position 1649 COORD coord = {cast(short) x, cast(short) y}; 1650 SetConsoleCursorPosition(hConsole, coord); 1651 } 1652 } 1653 1654 _cursorX = x; 1655 _cursorY = y; 1656 } 1657 1658 /// shows the cursor 1659 void showCursor() { 1660 if(UseVtSequences) 1661 doTermcap("ve"); 1662 else version(Win32Console) { 1663 CONSOLE_CURSOR_INFO info; 1664 GetConsoleCursorInfo(hConsole, &info); 1665 info.bVisible = true; 1666 SetConsoleCursorInfo(hConsole, &info); 1667 } 1668 } 1669 1670 /// hides the cursor 1671 void hideCursor() { 1672 if(UseVtSequences) { 1673 doTermcap("vi"); 1674 } else version(Win32Console) { 1675 CONSOLE_CURSOR_INFO info; 1676 GetConsoleCursorInfo(hConsole, &info); 1677 info.bVisible = false; 1678 SetConsoleCursorInfo(hConsole, &info); 1679 } 1680 1681 } 1682 1683 private bool autoHidingCursor; 1684 private bool autoHiddenCursor; 1685 // explicitly not publicly documented 1686 // Sets the cursor to automatically insert a hide command at the front of the output buffer iff it is moved. 1687 // Call autoShowCursor when you are done with the batch update. 1688 void autoHideCursor() { 1689 autoHidingCursor = true; 1690 } 1691 1692 private void executeAutoHideCursor() { 1693 if(autoHidingCursor) { 1694 version(Win32Console) 1695 hideCursor(); 1696 else if(UseVtSequences) { 1697 // prepend the hide cursor command so it is the first thing flushed 1698 writeBuffer = "\033[?25l" ~ writeBuffer; 1699 } 1700 1701 autoHiddenCursor = true; 1702 autoHidingCursor = false; // already been done, don't insert the command again 1703 } 1704 } 1705 1706 // explicitly not publicly documented 1707 // Shows the cursor if it was automatically hidden by autoHideCursor and resets the internal auto hide state. 1708 void autoShowCursor() { 1709 if(autoHiddenCursor) 1710 showCursor(); 1711 1712 autoHidingCursor = false; 1713 autoHiddenCursor = false; 1714 } 1715 1716 /* 1717 // alas this doesn't work due to a bunch of delegate context pointer and postblit problems 1718 // instead of using: auto input = terminal.captureInput(flags) 1719 // use: auto input = RealTimeConsoleInput(&terminal, flags); 1720 /// Gets real time input, disabling line buffering 1721 RealTimeConsoleInput captureInput(ConsoleInputFlags flags) { 1722 return RealTimeConsoleInput(&this, flags); 1723 } 1724 */ 1725 1726 /// Changes the terminal's title 1727 void setTitle(string t) { 1728 version(Win32Console) { 1729 wchar[256] buffer; 1730 size_t bufferLength; 1731 foreach(wchar ch; t) 1732 if(bufferLength < buffer.length) 1733 buffer[bufferLength++] = ch; 1734 if(bufferLength < buffer.length) 1735 buffer[bufferLength++] = 0; 1736 else 1737 buffer[$-1] = 0; 1738 SetConsoleTitleW(buffer.ptr); 1739 } else { 1740 import std.string; 1741 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) 1742 writeStringRaw(format("\033]0;%s\007", t)); 1743 } 1744 } 1745 1746 /// Flushes your updates to the terminal. 1747 /// It is important to call this when you are finished writing for now if you are using the version=with_eventloop 1748 void flush() { 1749 if(writeBuffer.length == 0) 1750 return; 1751 1752 version(TerminalDirectToEmulator) { 1753 if(usingDirectEmulator) { 1754 tew.sendRawInput(cast(ubyte[]) writeBuffer); 1755 writeBuffer = null; 1756 } else { 1757 interiorFlush(); 1758 } 1759 } else { 1760 interiorFlush(); 1761 } 1762 } 1763 1764 private void interiorFlush() { 1765 version(Posix) { 1766 if(_writeDelegate !is null) { 1767 _writeDelegate(writeBuffer); 1768 } else { 1769 ssize_t written; 1770 1771 while(writeBuffer.length) { 1772 written = unix.write(this.fdOut, writeBuffer.ptr, writeBuffer.length); 1773 if(written < 0) 1774 throw new Exception("write failed for some reason"); 1775 writeBuffer = writeBuffer[written .. $]; 1776 } 1777 } 1778 } else version(Win32Console) { 1779 import std.conv; 1780 // FIXME: I'm not sure I'm actually happy with this allocation but 1781 // it probably isn't a big deal. At least it has unicode support now. 1782 wstring writeBufferw = to!wstring(writeBuffer); 1783 while(writeBufferw.length) { 1784 DWORD written; 1785 WriteConsoleW(hConsole, writeBufferw.ptr, cast(DWORD)writeBufferw.length, &written, null); 1786 writeBufferw = writeBufferw[written .. $]; 1787 } 1788 1789 writeBuffer = null; 1790 } 1791 } 1792 1793 int[] getSize() { 1794 version(TerminalDirectToEmulator) { 1795 if(usingDirectEmulator) 1796 return [tew.terminalEmulator.width, tew.terminalEmulator.height]; 1797 else 1798 return getSizeInternal(); 1799 } else { 1800 return getSizeInternal(); 1801 } 1802 } 1803 1804 private int[] getSizeInternal() { 1805 version(Windows) { 1806 CONSOLE_SCREEN_BUFFER_INFO info; 1807 GetConsoleScreenBufferInfo( hConsole, &info ); 1808 1809 int cols, rows; 1810 1811 cols = (info.srWindow.Right - info.srWindow.Left + 1); 1812 rows = (info.srWindow.Bottom - info.srWindow.Top + 1); 1813 1814 return [cols, rows]; 1815 } else { 1816 if(getSizeOverride is null) { 1817 winsize w; 1818 ioctl(0, TIOCGWINSZ, &w); 1819 return [w.ws_col, w.ws_row]; 1820 } else return getSizeOverride(); 1821 } 1822 } 1823 1824 void updateSize() { 1825 auto size = getSize(); 1826 _width = size[0]; 1827 _height = size[1]; 1828 } 1829 1830 private int _width; 1831 private int _height; 1832 1833 /// The current width of the terminal (the number of columns) 1834 @property int width() { 1835 if(_width == 0 || _height == 0) 1836 updateSize(); 1837 return _width; 1838 } 1839 1840 /// The current height of the terminal (the number of rows) 1841 @property int height() { 1842 if(_width == 0 || _height == 0) 1843 updateSize(); 1844 return _height; 1845 } 1846 1847 /* 1848 void write(T...)(T t) { 1849 foreach(arg; t) { 1850 writeStringRaw(to!string(arg)); 1851 } 1852 } 1853 */ 1854 1855 /// Writes to the terminal at the current cursor position. 1856 void writef(T...)(string f, T t) { 1857 import std.string; 1858 writePrintableString(format(f, t)); 1859 } 1860 1861 /// ditto 1862 void writefln(T...)(string f, T t) { 1863 writef(f ~ "\n", t); 1864 } 1865 1866 /// ditto 1867 void write(T...)(T t) { 1868 import std.conv; 1869 string data; 1870 foreach(arg; t) { 1871 data ~= to!string(arg); 1872 } 1873 1874 writePrintableString(data); 1875 } 1876 1877 /// ditto 1878 void writeln(T...)(T t) { 1879 write(t, "\n"); 1880 } 1881 1882 /+ 1883 /// A combined moveTo and writef that puts the cursor back where it was before when it finishes the write. 1884 /// Only works in cellular mode. 1885 /// 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) 1886 void writefAt(T...)(int x, int y, string f, T t) { 1887 import std.string; 1888 auto toWrite = format(f, t); 1889 1890 auto oldX = _cursorX; 1891 auto oldY = _cursorY; 1892 1893 writeAtWithoutReturn(x, y, toWrite); 1894 1895 moveTo(oldX, oldY); 1896 } 1897 1898 void writeAtWithoutReturn(int x, int y, in char[] data) { 1899 moveTo(x, y); 1900 writeStringRaw(toWrite, ForceOption.alwaysSend); 1901 } 1902 +/ 1903 1904 void writePrintableString(const(char)[] s, ForceOption force = ForceOption.automatic) { 1905 // an escape character is going to mess things up. Actually any non-printable character could, but meh 1906 // assert(s.indexOf("\033") == -1); 1907 1908 if(s.length == 0) 1909 return; 1910 1911 // tracking cursor position 1912 // FIXME: by grapheme? 1913 foreach(dchar ch; s) { 1914 switch(ch) { 1915 case '\n': 1916 _cursorX = 0; 1917 _cursorY++; 1918 break; 1919 case '\r': 1920 _cursorX = 0; 1921 break; 1922 case '\t': 1923 _cursorX ++; 1924 _cursorX += _cursorX % 8; // FIXME: get the actual tabstop, if possible 1925 break; 1926 default: 1927 _cursorX++; 1928 } 1929 1930 if(_wrapAround && _cursorX > width) { 1931 _cursorX = 0; 1932 _cursorY++; 1933 } 1934 1935 if(_cursorY == height) 1936 _cursorY--; 1937 1938 /+ 1939 auto index = getIndex(_cursorX, _cursorY); 1940 if(data[index] != ch) { 1941 data[index] = ch; 1942 } 1943 +/ 1944 } 1945 1946 version(TerminalDirectToEmulator) { 1947 // this breaks up extremely long output a little as an aid to the 1948 // gui thread; by breaking it up, it helps to avoid monopolizing the 1949 // event loop. Easier to do here than in the thread itself because 1950 // this one doesn't have escape sequences to break up so it avoids work. 1951 while(s.length) { 1952 auto len = s.length; 1953 if(len > 1024 * 32) { 1954 len = 1024 * 32; 1955 // get to the start of a utf-8 sequence. kidna sorta. 1956 while(len && (s[len] & 0x1000_0000)) 1957 len--; 1958 } 1959 auto next = s[0 .. len]; 1960 s = s[len .. $]; 1961 writeStringRaw(next); 1962 } 1963 } else { 1964 writeStringRaw(s); 1965 } 1966 } 1967 1968 /* private */ bool _wrapAround = true; 1969 1970 deprecated alias writePrintableString writeString; /// use write() or writePrintableString instead 1971 1972 private string writeBuffer; 1973 1974 // you really, really shouldn't use this unless you know what you are doing 1975 /*private*/ void writeStringRaw(in char[] s) { 1976 writeBuffer ~= s; // buffer it to do everything at once in flush() calls 1977 if(writeBuffer.length > 1024 * 32) 1978 flush(); 1979 } 1980 1981 /// Clears the screen. 1982 void clear() { 1983 if(UseVtSequences) { 1984 doTermcap("cl"); 1985 } else version(Win32Console) { 1986 // http://support.microsoft.com/kb/99261 1987 flush(); 1988 1989 DWORD c; 1990 CONSOLE_SCREEN_BUFFER_INFO csbi; 1991 DWORD conSize; 1992 GetConsoleScreenBufferInfo(hConsole, &csbi); 1993 conSize = csbi.dwSize.X * csbi.dwSize.Y; 1994 COORD coordScreen; 1995 FillConsoleOutputCharacterA(hConsole, ' ', conSize, coordScreen, &c); 1996 FillConsoleOutputAttribute(hConsole, csbi.wAttributes, conSize, coordScreen, &c); 1997 moveTo(0, 0, ForceOption.alwaysSend); 1998 } 1999 2000 _cursorX = 0; 2001 _cursorY = 0; 2002 } 2003 2004 /// gets a line, including user editing. Convenience method around the LineGetter class and RealTimeConsoleInput facilities - use them if you need more control. 2005 /// 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. 2006 // FIXME: add a method to make it easy to check if stdin is actually a tty and use other methods there. 2007 string getline(string prompt = null) { 2008 if(lineGetter is null) 2009 lineGetter = new LineGetter(&this); 2010 // since the struct might move (it shouldn't, this should be unmovable!) but since 2011 // it technically might, I'm updating the pointer before using it just in case. 2012 lineGetter.terminal = &this; 2013 2014 if(prompt !is null) 2015 lineGetter.prompt = prompt; 2016 2017 auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw); 2018 auto line = lineGetter.getline(&input); 2019 2020 // lineGetter leaves us exactly where it was when the user hit enter, giving best 2021 // flexibility to real-time input and cellular programs. The convenience function, 2022 // however, wants to do what is right in most the simple cases, which is to actually 2023 // print the line (echo would be enabled without RealTimeConsoleInput anyway and they 2024 // did hit enter), so we'll do that here too. 2025 writePrintableString("\n"); 2026 2027 return line; 2028 } 2029 2030 } 2031 2032 /++ 2033 Removes terminal color, bold, etc. sequences from a string, 2034 making it plain text suitable for output to a normal .txt 2035 file. 2036 +/ 2037 inout(char)[] removeTerminalGraphicsSequences(inout(char)[] s) { 2038 import std.string; 2039 2040 auto at = s.indexOf("\033["); 2041 if(at == -1) 2042 return s; 2043 2044 inout(char)[] ret; 2045 2046 do { 2047 ret ~= s[0 .. at]; 2048 s = s[at + 2 .. $]; 2049 while(s.length && !((s[0] >= 'a' && s[0] <= 'z') || s[0] >= 'A' && s[0] <= 'Z')) { 2050 s = s[1 .. $]; 2051 } 2052 if(s.length) 2053 s = s[1 .. $]; // skip the terminator 2054 at = s.indexOf("\033["); 2055 } while(at != -1); 2056 2057 ret ~= s; 2058 2059 return ret; 2060 } 2061 2062 unittest { 2063 assert("foo".removeTerminalGraphicsSequences == "foo"); 2064 assert("\033[34mfoo".removeTerminalGraphicsSequences == "foo"); 2065 assert("\033[34mfoo\033[39m".removeTerminalGraphicsSequences == "foo"); 2066 assert("\033[34m\033[45mfoo\033[39mbar\033[49m".removeTerminalGraphicsSequences == "foobar"); 2067 } 2068 2069 2070 /+ 2071 struct ConsoleBuffer { 2072 int cursorX; 2073 int cursorY; 2074 int width; 2075 int height; 2076 dchar[] data; 2077 2078 void actualize(Terminal* t) { 2079 auto writer = t.getBufferedWriter(); 2080 2081 this.copyTo(&(t.onScreen)); 2082 } 2083 2084 void copyTo(ConsoleBuffer* buffer) { 2085 buffer.cursorX = this.cursorX; 2086 buffer.cursorY = this.cursorY; 2087 buffer.width = this.width; 2088 buffer.height = this.height; 2089 buffer.data[] = this.data[]; 2090 } 2091 } 2092 +/ 2093 2094 /** 2095 * Encapsulates the stream of input events received from the terminal input. 2096 */ 2097 struct RealTimeConsoleInput { 2098 @disable this(); 2099 @disable this(this); 2100 2101 /++ 2102 Requests the system to send paste data as a [PasteEvent] to this stream, if possible. 2103 2104 See_Also: 2105 [Terminal.requestCopyToPrimary] 2106 [Terminal.requestCopyToClipboard] 2107 [Terminal.clipboardSupported] 2108 2109 History: 2110 Added February 17, 2020. 2111 2112 It was in Terminal briefly during an undocumented period, but it had to be moved here to have the context needed to send the real time paste event. 2113 +/ 2114 void requestPasteFromClipboard() { 2115 version(Win32Console) { 2116 HWND hwndOwner = null; 2117 if(OpenClipboard(hwndOwner) == 0) 2118 throw new Exception("OpenClipboard"); 2119 scope(exit) 2120 CloseClipboard(); 2121 if(auto dataHandle = GetClipboardData(CF_UNICODETEXT)) { 2122 2123 if(auto data = cast(wchar*) GlobalLock(dataHandle)) { 2124 scope(exit) 2125 GlobalUnlock(dataHandle); 2126 2127 int len = 0; 2128 auto d = data; 2129 while(*d) { 2130 d++; 2131 len++; 2132 } 2133 string s; 2134 s.reserve(len); 2135 foreach(idx, dchar ch; data[0 .. len]) { 2136 // CR/LF -> LF 2137 if(ch == '\r' && idx + 1 < len && data[idx + 1] == '\n') 2138 continue; 2139 s ~= ch; 2140 } 2141 2142 injectEvent(InputEvent(PasteEvent(s), terminal), InjectionPosition.tail); 2143 } 2144 } 2145 } else 2146 if(terminal.clipboardSupported) { 2147 if(UseVtSequences) 2148 terminal.writeStringRaw("\033]52;c;?\007"); 2149 } 2150 } 2151 2152 /// ditto 2153 void requestPasteFromPrimary() { 2154 if(terminal.clipboardSupported) { 2155 if(UseVtSequences) 2156 terminal.writeStringRaw("\033]52;p;?\007"); 2157 } 2158 } 2159 2160 2161 version(Posix) { 2162 private int fdOut; 2163 private int fdIn; 2164 private sigaction_t oldSigWinch; 2165 private sigaction_t oldSigIntr; 2166 private sigaction_t oldHupIntr; 2167 private termios old; 2168 ubyte[128] hack; 2169 // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... 2170 // tcgetattr smashed other variables in here too that could create random problems 2171 // so this hack is just to give some room for that to happen without destroying the rest of the world 2172 } 2173 2174 version(Windows) { 2175 private DWORD oldInput; 2176 private DWORD oldOutput; 2177 HANDLE inputHandle; 2178 } 2179 2180 private ConsoleInputFlags flags; 2181 private Terminal* terminal; 2182 private void delegate()[] destructor; 2183 2184 /// To capture input, you need to provide a terminal and some flags. 2185 public this(Terminal* terminal, ConsoleInputFlags flags) { 2186 this.flags = flags; 2187 this.terminal = terminal; 2188 2189 version(Windows) { 2190 inputHandle = GetStdHandle(STD_INPUT_HANDLE); 2191 2192 } 2193 2194 version(Win32Console) { 2195 2196 GetConsoleMode(inputHandle, &oldInput); 2197 2198 DWORD mode = 0; 2199 //mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C and automatic paste... which we probably want to be similar to linux 2200 //if(flags & ConsoleInputFlags.size) 2201 mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc 2202 if(flags & ConsoleInputFlags.echo) 2203 mode |= ENABLE_ECHO_INPUT; // 0x4 2204 if(flags & ConsoleInputFlags.mouse) 2205 mode |= ENABLE_MOUSE_INPUT; // 0x10 2206 // if(flags & ConsoleInputFlags.raw) // FIXME: maybe that should be a separate flag for ENABLE_LINE_INPUT 2207 2208 SetConsoleMode(inputHandle, mode); 2209 destructor ~= { SetConsoleMode(inputHandle, oldInput); }; 2210 2211 2212 GetConsoleMode(terminal.hConsole, &oldOutput); 2213 mode = 0; 2214 // we want this to match linux too 2215 mode |= ENABLE_PROCESSED_OUTPUT; /* 0x01 */ 2216 if(!(flags & ConsoleInputFlags.noEolWrap)) 2217 mode |= ENABLE_WRAP_AT_EOL_OUTPUT; /* 0x02 */ 2218 SetConsoleMode(terminal.hConsole, mode); 2219 destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); }; 2220 } 2221 2222 version(TerminalDirectToEmulator) { 2223 if(terminal.usingDirectEmulator) 2224 terminal.tew.terminalEmulator.echo = (flags & ConsoleInputFlags.echo) ? true : false; 2225 else version(Posix) 2226 posixInit(); 2227 } else version(Posix) { 2228 posixInit(); 2229 } 2230 2231 if(UseVtSequences) { 2232 if(flags & ConsoleInputFlags.mouse) { 2233 // basic button press+release notification 2234 2235 // FIXME: try to get maximum capabilities from all terminals 2236 // right now this works well on xterm but rxvt isn't sending movements... 2237 2238 terminal.writeStringRaw("\033[?1000h"); 2239 destructor ~= { terminal.writeStringRaw("\033[?1000l"); }; 2240 // the MOUSE_HACK env var is for the case where I run screen 2241 // but set TERM=xterm (which I do from putty). The 1003 mouse mode 2242 // doesn't work there, breaking mouse support entirely. So by setting 2243 // MOUSE_HACK=1002 it tells us to use the other mode for a fallback. 2244 import std.process : environment; 2245 if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { 2246 // this is vt200 mouse with full motion tracking, supported by xterm 2247 terminal.writeStringRaw("\033[?1003h"); 2248 destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; 2249 } else if(terminal.terminalInFamily("rxvt", "screen", "tmux") || environment.get("MOUSE_HACK") == "1002") { 2250 terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed 2251 destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; 2252 } 2253 } 2254 if(flags & ConsoleInputFlags.paste) { 2255 if(terminal.terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 2256 terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode 2257 destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; 2258 } 2259 } 2260 2261 if(terminal.tcaps & TerminalCapabilities.arsdHyperlinks) { 2262 terminal.writeStringRaw("\033[?3004h"); // bracketed link mode 2263 destructor ~= { terminal.writeStringRaw("\033[?3004l"); }; 2264 } 2265 2266 // try to ensure the terminal is in UTF-8 mode 2267 if(terminal.terminalInFamily("xterm", "screen", "linux", "tmux") && !terminal.isMacTerminal()) { 2268 terminal.writeStringRaw("\033%G"); 2269 } 2270 2271 terminal.flush(); 2272 } 2273 2274 2275 version(with_eventloop) { 2276 import arsd.eventloop; 2277 version(Win32Console) 2278 auto listenTo = inputHandle; 2279 else version(Posix) 2280 auto listenTo = this.fdIn; 2281 else static assert(0, "idk about this OS"); 2282 2283 version(Posix) 2284 addListener(&signalFired); 2285 2286 if(listenTo != -1) { 2287 addFileEventListeners(listenTo, &eventListener, null, null); 2288 destructor ~= { removeFileEventListeners(listenTo); }; 2289 } 2290 addOnIdle(&terminal.flush); 2291 destructor ~= { removeOnIdle(&terminal.flush); }; 2292 } 2293 } 2294 2295 version(Posix) 2296 private void posixInit() { 2297 this.fdIn = terminal.fdIn; 2298 this.fdOut = terminal.fdOut; 2299 2300 if(fdIn != -1) { 2301 tcgetattr(fdIn, &old); 2302 auto n = old; 2303 2304 auto f = ICANON; 2305 if(!(flags & ConsoleInputFlags.echo)) 2306 f |= ECHO; 2307 2308 // \033Z or \033[c 2309 2310 n.c_lflag &= ~f; 2311 tcsetattr(fdIn, TCSANOW, &n); 2312 } 2313 2314 // some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3 2315 //destructor ~= { tcsetattr(fdIn, TCSANOW, &old); }; 2316 2317 if(flags & ConsoleInputFlags.size) { 2318 import core.sys.posix.signal; 2319 sigaction_t n; 2320 n.sa_handler = &sizeSignalHandler; 2321 n.sa_mask = cast(sigset_t) 0; 2322 n.sa_flags = 0; 2323 sigaction(SIGWINCH, &n, &oldSigWinch); 2324 } 2325 2326 { 2327 import core.sys.posix.signal; 2328 sigaction_t n; 2329 n.sa_handler = &interruptSignalHandler; 2330 n.sa_mask = cast(sigset_t) 0; 2331 n.sa_flags = 0; 2332 sigaction(SIGINT, &n, &oldSigIntr); 2333 } 2334 2335 { 2336 import core.sys.posix.signal; 2337 sigaction_t n; 2338 n.sa_handler = &hangupSignalHandler; 2339 n.sa_mask = cast(sigset_t) 0; 2340 n.sa_flags = 0; 2341 sigaction(SIGHUP, &n, &oldHupIntr); 2342 } 2343 } 2344 2345 void fdReadyReader() { 2346 auto queue = readNextEvents(); 2347 foreach(event; queue) 2348 userEventHandler(event); 2349 } 2350 2351 void delegate(InputEvent) userEventHandler; 2352 2353 /++ 2354 If you are using [arsd.simpledisplay] and want terminal interop too, you can call 2355 this function to add it to the sdpy event loop and get the callback called on new 2356 input. 2357 2358 Note that you will probably need to call `terminal.flush()` when you are doing doing 2359 output, as the sdpy event loop doesn't know to do that (yet). I will probably change 2360 that in a future version, but it doesn't hurt to call it twice anyway, so I recommend 2361 calling flush yourself in any code you write using this. 2362 +/ 2363 void integrateWithSimpleDisplayEventLoop()(void delegate(InputEvent) userEventHandler) { 2364 this.userEventHandler = userEventHandler; 2365 import arsd.simpledisplay; 2366 version(Win32Console) 2367 auto listener = new WindowsHandleReader(&fdReadyReader, terminal.hConsole); 2368 else version(linux) 2369 auto listener = new PosixFdReader(&fdReadyReader, fdIn); 2370 else static assert(0, "sdpy event loop integration not implemented on this platform"); 2371 } 2372 2373 version(with_eventloop) { 2374 version(Posix) 2375 void signalFired(SignalFired) { 2376 if(interrupted) { 2377 interrupted = false; 2378 send(InputEvent(UserInterruptionEvent(), terminal)); 2379 } 2380 if(windowSizeChanged) 2381 send(checkWindowSizeChanged()); 2382 if(hangedUp) { 2383 hangedUp = false; 2384 send(InputEvent(HangupEvent(), terminal)); 2385 } 2386 } 2387 2388 import arsd.eventloop; 2389 void eventListener(OsFileHandle fd) { 2390 auto queue = readNextEvents(); 2391 foreach(event; queue) 2392 send(event); 2393 } 2394 } 2395 2396 bool _suppressDestruction; 2397 2398 ~this() { 2399 if(_suppressDestruction) 2400 return; 2401 2402 // the delegate thing doesn't actually work for this... for some reason 2403 2404 version(TerminalDirectToEmulator) { 2405 if(terminal && terminal.usingDirectEmulator) 2406 goto skip_extra; 2407 } 2408 2409 version(Posix) { 2410 if(fdIn != -1) 2411 tcsetattr(fdIn, TCSANOW, &old); 2412 2413 if(flags & ConsoleInputFlags.size) { 2414 // restoration 2415 sigaction(SIGWINCH, &oldSigWinch, null); 2416 } 2417 sigaction(SIGINT, &oldSigIntr, null); 2418 sigaction(SIGHUP, &oldHupIntr, null); 2419 } 2420 2421 skip_extra: 2422 2423 // we're just undoing everything the constructor did, in reverse order, same criteria 2424 foreach_reverse(d; destructor) 2425 d(); 2426 } 2427 2428 /** 2429 Returns true if there iff getch() would not block. 2430 2431 WARNING: kbhit might consume input that would be ignored by getch. This 2432 function is really only meant to be used in conjunction with getch. Typically, 2433 you should use a full-fledged event loop if you want all kinds of input. kbhit+getch 2434 are just for simple keyboard driven applications. 2435 */ 2436 bool kbhit() { 2437 auto got = getch(true); 2438 2439 if(got == dchar.init) 2440 return false; 2441 2442 getchBuffer = got; 2443 return true; 2444 } 2445 2446 /// Check for input, waiting no longer than the number of milliseconds 2447 bool timedCheckForInput(int milliseconds) { 2448 if(inputQueue.length || timedCheckForInput_bypassingBuffer(milliseconds)) 2449 return true; 2450 version(WithEncapsulatedSignals) 2451 if(terminal.interrupted || terminal.windowSizeChanged || terminal.hangedUp) 2452 return true; 2453 version(WithSignals) 2454 if(interrupted || windowSizeChanged || hangedUp) 2455 return true; 2456 return false; 2457 } 2458 2459 /* private */ bool anyInput_internal(int timeout = 0) { 2460 return timedCheckForInput(timeout); 2461 } 2462 2463 bool timedCheckForInput_bypassingBuffer(int milliseconds) { 2464 version(TerminalDirectToEmulator) { 2465 if(!terminal.usingDirectEmulator) 2466 return timedCheckForInput_bypassingBuffer_impl(milliseconds); 2467 2468 import core.time; 2469 if(terminal.tew.terminalEmulator.pendingForApplication.length) 2470 return true; 2471 if(terminal.tew.terminalEmulator.outgoingSignal.wait(milliseconds.msecs)) 2472 // it was notified, but it could be left over from stuff we 2473 // already processed... so gonna check the blocking conditions here too 2474 // (FIXME: this sucks and is surely a race condition of pain) 2475 return terminal.tew.terminalEmulator.pendingForApplication.length || terminal.interrupted || terminal.windowSizeChanged || terminal.hangedUp; 2476 else 2477 return false; 2478 } else 2479 return timedCheckForInput_bypassingBuffer_impl(milliseconds); 2480 } 2481 2482 private bool timedCheckForInput_bypassingBuffer_impl(int milliseconds) { 2483 version(Windows) { 2484 auto response = WaitForSingleObject(inputHandle, milliseconds); 2485 if(response == 0) 2486 return true; // the object is ready 2487 return false; 2488 } else version(Posix) { 2489 if(fdIn == -1) 2490 return false; 2491 2492 timeval tv; 2493 tv.tv_sec = 0; 2494 tv.tv_usec = milliseconds * 1000; 2495 2496 fd_set fs; 2497 FD_ZERO(&fs); 2498 2499 FD_SET(fdIn, &fs); 2500 int tries = 0; 2501 try_again: 2502 auto ret = select(fdIn + 1, &fs, null, null, &tv); 2503 if(ret == -1) { 2504 import core.stdc.errno; 2505 if(errno == EINTR) { 2506 tries++; 2507 if(tries < 3) 2508 goto try_again; 2509 } 2510 return false; 2511 } 2512 if(ret == 0) 2513 return false; 2514 2515 return FD_ISSET(fdIn, &fs); 2516 } 2517 } 2518 2519 private dchar getchBuffer; 2520 2521 /// Get one key press from the terminal, discarding other 2522 /// events in the process. Returns dchar.init upon receiving end-of-file. 2523 /// 2524 /// 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. 2525 dchar getch(bool nonblocking = false) { 2526 if(getchBuffer != dchar.init) { 2527 auto a = getchBuffer; 2528 getchBuffer = dchar.init; 2529 return a; 2530 } 2531 2532 if(nonblocking && !anyInput_internal()) 2533 return dchar.init; 2534 2535 auto event = nextEvent(); 2536 while(event.type != InputEvent.Type.KeyboardEvent || event.keyboardEvent.pressed == false) { 2537 if(event.type == InputEvent.Type.UserInterruptionEvent) 2538 throw new UserInterruptionException(); 2539 if(event.type == InputEvent.Type.HangupEvent) 2540 throw new HangupException(); 2541 if(event.type == InputEvent.Type.EndOfFileEvent) 2542 return dchar.init; 2543 2544 if(nonblocking && !anyInput_internal()) 2545 return dchar.init; 2546 2547 event = nextEvent(); 2548 } 2549 return event.keyboardEvent.which; 2550 } 2551 2552 //char[128] inputBuffer; 2553 //int inputBufferPosition; 2554 int nextRaw(bool interruptable = false) { 2555 version(TerminalDirectToEmulator) { 2556 if(!terminal.usingDirectEmulator) 2557 return nextRaw_impl(interruptable); 2558 moar: 2559 //if(interruptable && inputQueue.length) 2560 //return -1; 2561 if(terminal.tew.terminalEmulator.pendingForApplication.length == 0) 2562 terminal.tew.terminalEmulator.outgoingSignal.wait(); 2563 synchronized(terminal.tew.terminalEmulator) { 2564 if(terminal.tew.terminalEmulator.pendingForApplication.length == 0) { 2565 if(interruptable) 2566 return -1; 2567 else 2568 goto moar; 2569 } 2570 auto a = terminal.tew.terminalEmulator.pendingForApplication[0]; 2571 terminal.tew.terminalEmulator.pendingForApplication = terminal.tew.terminalEmulator.pendingForApplication[1 .. $]; 2572 return a; 2573 } 2574 } else 2575 return nextRaw_impl(interruptable); 2576 } 2577 private int nextRaw_impl(bool interruptable = false) { 2578 version(Posix) { 2579 if(fdIn == -1) 2580 return 0; 2581 2582 char[1] buf; 2583 try_again: 2584 auto ret = read(fdIn, buf.ptr, buf.length); 2585 if(ret == 0) 2586 return 0; // input closed 2587 if(ret == -1) { 2588 import core.stdc.errno; 2589 if(errno == EINTR) 2590 // interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop 2591 if(interruptable) 2592 return -1; 2593 else 2594 goto try_again; 2595 else 2596 throw new Exception("read failed"); 2597 } 2598 2599 //terminal.writef("RAW READ: %d\n", buf[0]); 2600 2601 if(ret == 1) 2602 return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; 2603 else 2604 assert(0); // read too much, should be impossible 2605 } else version(Windows) { 2606 char[1] buf; 2607 DWORD d; 2608 import std.conv; 2609 if(!ReadFile(inputHandle, buf.ptr, cast(int) buf.length, &d, null)) 2610 throw new Exception("ReadFile " ~ to!string(GetLastError())); 2611 return buf[0]; 2612 } 2613 } 2614 2615 version(Posix) 2616 int delegate(char) inputPrefilter; 2617 2618 // for VT 2619 dchar nextChar(int starting) { 2620 if(starting <= 127) 2621 return cast(dchar) starting; 2622 char[6] buffer; 2623 int pos = 0; 2624 buffer[pos++] = cast(char) starting; 2625 2626 // see the utf-8 encoding for details 2627 int remaining = 0; 2628 ubyte magic = starting & 0xff; 2629 while(magic & 0b1000_000) { 2630 remaining++; 2631 magic <<= 1; 2632 } 2633 2634 while(remaining && pos < buffer.length) { 2635 buffer[pos++] = cast(char) nextRaw(); 2636 remaining--; 2637 } 2638 2639 import std.utf; 2640 size_t throwAway; // it insists on the index but we don't care 2641 return decode(buffer[], throwAway); 2642 } 2643 2644 InputEvent checkWindowSizeChanged() { 2645 auto oldWidth = terminal.width; 2646 auto oldHeight = terminal.height; 2647 terminal.updateSize(); 2648 version(WithSignals) 2649 windowSizeChanged = false; 2650 version(WithEncapsulatedSignals) 2651 terminal.windowSizeChanged = false; 2652 return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); 2653 } 2654 2655 2656 // character event 2657 // non-character key event 2658 // paste event 2659 // mouse event 2660 // size event maybe, and if appropriate focus events 2661 2662 /// Returns the next event. 2663 /// 2664 /// Experimental: It is also possible to integrate this into 2665 /// a generic event loop, currently under -version=with_eventloop and it will 2666 /// require the module arsd.eventloop (Linux only at this point) 2667 InputEvent nextEvent() { 2668 terminal.flush(); 2669 2670 wait_for_more: 2671 version(WithSignals) { 2672 if(interrupted) { 2673 interrupted = false; 2674 return InputEvent(UserInterruptionEvent(), terminal); 2675 } 2676 2677 if(hangedUp) { 2678 hangedUp = false; 2679 return InputEvent(HangupEvent(), terminal); 2680 } 2681 2682 if(windowSizeChanged) { 2683 return checkWindowSizeChanged(); 2684 } 2685 } 2686 2687 version(WithEncapsulatedSignals) { 2688 if(terminal.interrupted) { 2689 terminal.interrupted = false; 2690 return InputEvent(UserInterruptionEvent(), terminal); 2691 } 2692 2693 if(terminal.hangedUp) { 2694 terminal.hangedUp = false; 2695 return InputEvent(HangupEvent(), terminal); 2696 } 2697 2698 if(terminal.windowSizeChanged) { 2699 return checkWindowSizeChanged(); 2700 } 2701 } 2702 2703 if(inputQueue.length) { 2704 auto e = inputQueue[0]; 2705 inputQueue = inputQueue[1 .. $]; 2706 return e; 2707 } 2708 2709 auto more = readNextEvents(); 2710 if(!more.length) 2711 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 2712 2713 assert(more.length); 2714 2715 auto e = more[0]; 2716 inputQueue = more[1 .. $]; 2717 return e; 2718 } 2719 2720 InputEvent* peekNextEvent() { 2721 if(inputQueue.length) 2722 return &(inputQueue[0]); 2723 return null; 2724 } 2725 2726 enum InjectionPosition { head, tail } 2727 void injectEvent(InputEvent ev, InjectionPosition where) { 2728 final switch(where) { 2729 case InjectionPosition.head: 2730 inputQueue = ev ~ inputQueue; 2731 break; 2732 case InjectionPosition.tail: 2733 inputQueue ~= ev; 2734 break; 2735 } 2736 } 2737 2738 InputEvent[] inputQueue; 2739 2740 InputEvent[] readNextEvents() { 2741 if(UseVtSequences) 2742 return readNextEventsVt(); 2743 else version(Win32Console) 2744 return readNextEventsWin32(); 2745 else 2746 assert(0); 2747 } 2748 2749 version(Win32Console) 2750 InputEvent[] readNextEventsWin32() { 2751 terminal.flush(); // make sure all output is sent out before waiting for anything 2752 2753 INPUT_RECORD[32] buffer; 2754 DWORD actuallyRead; 2755 // FIXME: ReadConsoleInputW 2756 auto success = ReadConsoleInputW(inputHandle, buffer.ptr, buffer.length, &actuallyRead); 2757 if(success == 0) 2758 throw new Exception("ReadConsoleInput"); 2759 2760 InputEvent[] newEvents; 2761 input_loop: foreach(record; buffer[0 .. actuallyRead]) { 2762 switch(record.EventType) { 2763 case KEY_EVENT: 2764 auto ev = record.KeyEvent; 2765 KeyboardEvent ke; 2766 CharacterEvent e; 2767 NonCharacterKeyEvent ne; 2768 2769 ke.pressed = ev.bKeyDown ? true : false; 2770 2771 // only send released events when specifically requested 2772 // terminal.writefln("got %s %s", ev.UnicodeChar, ev.bKeyDown); 2773 if(ev.UnicodeChar && ev.wVirtualKeyCode == VK_MENU && ev.bKeyDown == 0) { 2774 // this indicates Windows is actually sending us 2775 // an alt+xxx key sequence, may also be a unicode paste. 2776 // either way, it cool. 2777 ke.pressed = true; 2778 } else { 2779 if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) 2780 break; 2781 } 2782 2783 e.eventType = ke.pressed ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; 2784 ne.eventType = ke.pressed ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released; 2785 2786 e.modifierState = ev.dwControlKeyState; 2787 ne.modifierState = ev.dwControlKeyState; 2788 ke.modifierState = ev.dwControlKeyState; 2789 2790 if(ev.UnicodeChar) { 2791 // new style event goes first 2792 2793 if(ev.UnicodeChar == 3) { 2794 // handling this internally for linux compat too 2795 newEvents ~= InputEvent(UserInterruptionEvent(), terminal); 2796 } else if(ev.UnicodeChar == '\r') { 2797 // translating \r to \n for same result as linux... 2798 ke.which = cast(dchar) cast(wchar) '\n'; 2799 newEvents ~= InputEvent(ke, terminal); 2800 2801 // old style event then follows as the fallback 2802 e.character = cast(dchar) cast(wchar) '\n'; 2803 newEvents ~= InputEvent(e, terminal); 2804 } else if(ev.wVirtualKeyCode == 0x1b) { 2805 ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 2806 newEvents ~= InputEvent(ke, terminal); 2807 2808 ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; 2809 newEvents ~= InputEvent(ne, terminal); 2810 } else { 2811 ke.which = cast(dchar) cast(wchar) ev.UnicodeChar; 2812 newEvents ~= InputEvent(ke, terminal); 2813 2814 // old style event then follows as the fallback 2815 e.character = cast(dchar) cast(wchar) ev.UnicodeChar; 2816 newEvents ~= InputEvent(e, terminal); 2817 } 2818 } else { 2819 // old style event 2820 ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; 2821 2822 // new style event. See comment on KeyboardEvent.Key 2823 ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 2824 2825 // FIXME: make this better. the goal is to make sure the key code is a valid enum member 2826 // Windows sends more keys than Unix and we're doing lowest common denominator here 2827 foreach(member; __traits(allMembers, NonCharacterKeyEvent.Key)) 2828 if(__traits(getMember, NonCharacterKeyEvent.Key, member) == ne.key) { 2829 newEvents ~= InputEvent(ke, terminal); 2830 newEvents ~= InputEvent(ne, terminal); 2831 break; 2832 } 2833 } 2834 break; 2835 case MOUSE_EVENT: 2836 auto ev = record.MouseEvent; 2837 MouseEvent e; 2838 2839 e.modifierState = ev.dwControlKeyState; 2840 e.x = ev.dwMousePosition.X; 2841 e.y = ev.dwMousePosition.Y; 2842 2843 switch(ev.dwEventFlags) { 2844 case 0: 2845 //press or release 2846 e.eventType = MouseEvent.Type.Pressed; 2847 static DWORD lastButtonState; 2848 auto lastButtonState2 = lastButtonState; 2849 e.buttons = ev.dwButtonState; 2850 lastButtonState = e.buttons; 2851 2852 // this is sent on state change. if fewer buttons are pressed, it must mean released 2853 if(cast(DWORD) e.buttons < lastButtonState2) { 2854 e.eventType = MouseEvent.Type.Released; 2855 // if last was 101 and now it is 100, then button far right was released 2856 // so we flip the bits, ~100 == 011, then and them: 101 & 011 == 001, the 2857 // button that was released 2858 e.buttons = lastButtonState2 & ~e.buttons; 2859 } 2860 break; 2861 case MOUSE_MOVED: 2862 e.eventType = MouseEvent.Type.Moved; 2863 e.buttons = ev.dwButtonState; 2864 break; 2865 case 0x0004/*MOUSE_WHEELED*/: 2866 e.eventType = MouseEvent.Type.Pressed; 2867 if(ev.dwButtonState > 0) 2868 e.buttons = MouseEvent.Button.ScrollDown; 2869 else 2870 e.buttons = MouseEvent.Button.ScrollUp; 2871 break; 2872 default: 2873 continue input_loop; 2874 } 2875 2876 newEvents ~= InputEvent(e, terminal); 2877 break; 2878 case WINDOW_BUFFER_SIZE_EVENT: 2879 auto ev = record.WindowBufferSizeEvent; 2880 auto oldWidth = terminal.width; 2881 auto oldHeight = terminal.height; 2882 terminal._width = ev.dwSize.X; 2883 terminal._height = ev.dwSize.Y; 2884 newEvents ~= InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); 2885 break; 2886 // FIXME: can we catch ctrl+c here too? 2887 default: 2888 // ignore 2889 } 2890 } 2891 2892 return newEvents; 2893 } 2894 2895 // for UseVtSequences.... 2896 InputEvent[] readNextEventsVt() { 2897 terminal.flush(); // make sure all output is sent out before we try to get input 2898 2899 // we want to starve the read, especially if we're called from an edge-triggered 2900 // epoll (which might happen in version=with_eventloop.. impl detail there subject 2901 // to change). 2902 auto initial = readNextEventsHelper(); 2903 2904 // lol this calls select() inside a function prolly called from epoll but meh, 2905 // it is the simplest thing that can possibly work. The alternative would be 2906 // doing non-blocking reads and buffering in the nextRaw function (not a bad idea 2907 // btw, just a bit more of a hassle). 2908 while(timedCheckForInput_bypassingBuffer(0)) { 2909 auto ne = readNextEventsHelper(); 2910 initial ~= ne; 2911 foreach(n; ne) 2912 if(n.type == InputEvent.Type.EndOfFileEvent) 2913 return initial; // hit end of file, get out of here lest we infinite loop 2914 // (select still returns info available even after we read end of file) 2915 } 2916 return initial; 2917 } 2918 2919 // The helper reads just one actual event from the pipe... 2920 // for UseVtSequences.... 2921 InputEvent[] readNextEventsHelper(int remainingFromLastTime = int.max) { 2922 InputEvent[] charPressAndRelease(dchar character) { 2923 if((flags & ConsoleInputFlags.releasedKeys)) 2924 return [ 2925 // new style event 2926 InputEvent(KeyboardEvent(true, character, 0), terminal), 2927 InputEvent(KeyboardEvent(false, character, 0), terminal), 2928 // old style event 2929 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal), 2930 InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0), terminal), 2931 ]; 2932 else return [ 2933 // new style event 2934 InputEvent(KeyboardEvent(true, character, 0), terminal), 2935 // old style event 2936 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal) 2937 ]; 2938 } 2939 InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { 2940 if((flags & ConsoleInputFlags.releasedKeys)) 2941 return [ 2942 // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! 2943 InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), 2944 InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal), 2945 // old style event 2946 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal), 2947 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal), 2948 ]; 2949 else return [ 2950 // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! 2951 InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), 2952 // old style event 2953 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal) 2954 ]; 2955 } 2956 2957 InputEvent[] keyPressAndRelease2(dchar c, uint modifiers = 0) { 2958 if((flags & ConsoleInputFlags.releasedKeys)) 2959 return [ 2960 InputEvent(KeyboardEvent(true, c, modifiers), terminal), 2961 InputEvent(KeyboardEvent(false, c, modifiers), terminal), 2962 // old style event 2963 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, c, modifiers), terminal), 2964 InputEvent(CharacterEvent(CharacterEvent.Type.Released, c, modifiers), terminal), 2965 ]; 2966 else return [ 2967 InputEvent(KeyboardEvent(true, c, modifiers), terminal), 2968 // old style event 2969 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, c, modifiers), terminal) 2970 ]; 2971 2972 } 2973 2974 char[30] sequenceBuffer; 2975 2976 // this assumes you just read "\033[" 2977 char[] readEscapeSequence(char[] sequence) { 2978 int sequenceLength = 2; 2979 sequence[0] = '\033'; 2980 sequence[1] = '['; 2981 2982 while(sequenceLength < sequence.length) { 2983 auto n = nextRaw(); 2984 sequence[sequenceLength++] = cast(char) n; 2985 // I think a [ is supposed to termiate a CSI sequence 2986 // but the Linux console sends CSI[A for F1, so I'm 2987 // hacking it to accept that too 2988 if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) 2989 break; 2990 } 2991 2992 return sequence[0 .. sequenceLength]; 2993 } 2994 2995 InputEvent[] translateTermcapName(string cap) { 2996 switch(cap) { 2997 //case "k0": 2998 //return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 2999 case "k1": 3000 return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 3001 case "k2": 3002 return keyPressAndRelease(NonCharacterKeyEvent.Key.F2); 3003 case "k3": 3004 return keyPressAndRelease(NonCharacterKeyEvent.Key.F3); 3005 case "k4": 3006 return keyPressAndRelease(NonCharacterKeyEvent.Key.F4); 3007 case "k5": 3008 return keyPressAndRelease(NonCharacterKeyEvent.Key.F5); 3009 case "k6": 3010 return keyPressAndRelease(NonCharacterKeyEvent.Key.F6); 3011 case "k7": 3012 return keyPressAndRelease(NonCharacterKeyEvent.Key.F7); 3013 case "k8": 3014 return keyPressAndRelease(NonCharacterKeyEvent.Key.F8); 3015 case "k9": 3016 return keyPressAndRelease(NonCharacterKeyEvent.Key.F9); 3017 case "k;": 3018 case "k0": 3019 return keyPressAndRelease(NonCharacterKeyEvent.Key.F10); 3020 case "F1": 3021 return keyPressAndRelease(NonCharacterKeyEvent.Key.F11); 3022 case "F2": 3023 return keyPressAndRelease(NonCharacterKeyEvent.Key.F12); 3024 3025 3026 case "kb": 3027 return charPressAndRelease('\b'); 3028 case "kD": 3029 return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete); 3030 3031 case "kd": 3032 case "do": 3033 return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow); 3034 case "ku": 3035 case "up": 3036 return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow); 3037 case "kl": 3038 return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow); 3039 case "kr": 3040 case "nd": 3041 return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow); 3042 3043 case "kN": 3044 case "K5": 3045 return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown); 3046 case "kP": 3047 case "K2": 3048 return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp); 3049 3050 case "ho": // this might not be a key but my thing sometimes returns it... weird... 3051 case "kh": 3052 case "K1": 3053 return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); 3054 case "kH": 3055 return keyPressAndRelease(NonCharacterKeyEvent.Key.End); 3056 case "kI": 3057 return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); 3058 default: 3059 // don't know it, just ignore 3060 //import std.stdio; 3061 //terminal.writeln(cap); 3062 } 3063 3064 return null; 3065 } 3066 3067 3068 InputEvent[] doEscapeSequence(in char[] sequence) { 3069 switch(sequence) { 3070 case "\033[200~": 3071 // bracketed paste begin 3072 // we want to keep reading until 3073 // "\033[201~": 3074 // and build a paste event out of it 3075 3076 3077 string data; 3078 for(;;) { 3079 auto n = nextRaw(); 3080 if(n == '\033') { 3081 n = nextRaw(); 3082 if(n == '[') { 3083 auto esc = readEscapeSequence(sequenceBuffer); 3084 if(esc == "\033[201~") { 3085 // complete! 3086 break; 3087 } else { 3088 // was something else apparently, but it is pasted, so keep it 3089 data ~= esc; 3090 } 3091 } else { 3092 data ~= '\033'; 3093 data ~= cast(char) n; 3094 } 3095 } else { 3096 data ~= cast(char) n; 3097 } 3098 } 3099 return [InputEvent(PasteEvent(data), terminal)]; 3100 case "\033[220~": 3101 // bracketed hyperlink begin (arsd extension) 3102 3103 string data; 3104 for(;;) { 3105 auto n = nextRaw(); 3106 if(n == '\033') { 3107 n = nextRaw(); 3108 if(n == '[') { 3109 auto esc = readEscapeSequence(sequenceBuffer); 3110 if(esc == "\033[221~") { 3111 // complete! 3112 break; 3113 } else { 3114 // was something else apparently, but it is pasted, so keep it 3115 data ~= esc; 3116 } 3117 } else { 3118 data ~= '\033'; 3119 data ~= cast(char) n; 3120 } 3121 } else { 3122 data ~= cast(char) n; 3123 } 3124 } 3125 3126 import std.string, std.conv; 3127 auto idx = data.indexOf(";"); 3128 auto id = data[0 .. idx].to!ushort; 3129 data = data[idx + 1 .. $]; 3130 idx = data.indexOf(";"); 3131 auto cmd = data[0 .. idx].to!ushort; 3132 data = data[idx + 1 .. $]; 3133 3134 return [InputEvent(LinkEvent(data, id, cmd), terminal)]; 3135 case "\033[M": 3136 // mouse event 3137 auto buttonCode = nextRaw() - 32; 3138 // nextChar is commented because i'm not using UTF-8 mouse mode 3139 // cuz i don't think it is as widely supported 3140 auto x = cast(int) (/*nextChar*/(nextRaw())) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */ 3141 auto y = cast(int) (/*nextChar*/(nextRaw())) - 33; /* ditto */ 3142 3143 3144 bool isRelease = (buttonCode & 0b11) == 3; 3145 int buttonNumber; 3146 if(!isRelease) { 3147 buttonNumber = (buttonCode & 0b11); 3148 if(buttonCode & 64) 3149 buttonNumber += 3; // button 4 and 5 are sent as like button 1 and 2, but code | 64 3150 // so button 1 == button 4 here 3151 3152 // note: buttonNumber == 0 means button 1 at this point 3153 buttonNumber++; // hence this 3154 3155 3156 // apparently this considers middle to be button 2. but i want middle to be button 3. 3157 if(buttonNumber == 2) 3158 buttonNumber = 3; 3159 else if(buttonNumber == 3) 3160 buttonNumber = 2; 3161 } 3162 3163 auto modifiers = buttonCode & (0b0001_1100); 3164 // 4 == shift 3165 // 8 == meta 3166 // 16 == control 3167 3168 MouseEvent m; 3169 3170 if(buttonCode & 32) 3171 m.eventType = MouseEvent.Type.Moved; 3172 else 3173 m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; 3174 3175 // ugh, if no buttons are pressed, released and moved are indistinguishable... 3176 // so we'll count the buttons down, and if we get a release 3177 static int buttonsDown = 0; 3178 if(!isRelease && buttonNumber <= 3) // exclude wheel "presses"... 3179 buttonsDown++; 3180 3181 if(isRelease && m.eventType != MouseEvent.Type.Moved) { 3182 if(buttonsDown) 3183 buttonsDown--; 3184 else // no buttons down, so this should be a motion instead.. 3185 m.eventType = MouseEvent.Type.Moved; 3186 } 3187 3188 3189 if(buttonNumber == 0) 3190 m.buttons = 0; // we don't actually know :( 3191 else 3192 m.buttons = 1 << (buttonNumber - 1); // I prefer flags so that's how we do it 3193 m.x = x; 3194 m.y = y; 3195 m.modifierState = modifiers; 3196 3197 return [InputEvent(m, terminal)]; 3198 default: 3199 // screen doesn't actually do the modifiers, but 3200 // it uses the same format so this branch still works fine. 3201 if(terminal.terminalInFamily("xterm", "screen", "tmux")) { 3202 import std.conv, std.string; 3203 auto terminator = sequence[$ - 1]; 3204 auto parts = sequence[2 .. $ - 1].split(";"); 3205 // parts[0] and terminator tells us the key 3206 // parts[1] tells us the modifierState 3207 3208 uint modifierState; 3209 3210 int modGot; 3211 if(parts.length > 1) 3212 modGot = to!int(parts[1]); 3213 mod_switch: switch(modGot) { 3214 case 2: modifierState |= ModifierState.shift; break; 3215 case 3: modifierState |= ModifierState.alt; break; 3216 case 4: modifierState |= ModifierState.shift | ModifierState.alt; break; 3217 case 5: modifierState |= ModifierState.control; break; 3218 case 6: modifierState |= ModifierState.shift | ModifierState.control; break; 3219 case 7: modifierState |= ModifierState.alt | ModifierState.control; break; 3220 case 8: modifierState |= ModifierState.shift | ModifierState.alt | ModifierState.control; break; 3221 case 9: 3222 .. 3223 case 16: 3224 modifierState |= ModifierState.meta; 3225 if(modGot != 9) { 3226 modGot -= 8; 3227 goto mod_switch; 3228 } 3229 break; 3230 3231 // this is an extension in my own terminal emulator 3232 case 20: 3233 .. 3234 case 36: 3235 modifierState |= ModifierState.windows; 3236 modGot -= 20; 3237 goto mod_switch; 3238 default: 3239 } 3240 3241 switch(terminator) { 3242 case 'A': return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow, modifierState); 3243 case 'B': return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow, modifierState); 3244 case 'C': return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow, modifierState); 3245 case 'D': return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow, modifierState); 3246 3247 case 'H': return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); 3248 case 'F': return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); 3249 3250 case 'P': return keyPressAndRelease(NonCharacterKeyEvent.Key.F1, modifierState); 3251 case 'Q': return keyPressAndRelease(NonCharacterKeyEvent.Key.F2, modifierState); 3252 case 'R': return keyPressAndRelease(NonCharacterKeyEvent.Key.F3, modifierState); 3253 case 'S': return keyPressAndRelease(NonCharacterKeyEvent.Key.F4, modifierState); 3254 3255 case '~': // others 3256 switch(parts[0]) { 3257 case "1": return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); 3258 case "4": return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); 3259 case "5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp, modifierState); 3260 case "6": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown, modifierState); 3261 case "2": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert, modifierState); 3262 case "3": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete, modifierState); 3263 3264 case "15": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5, modifierState); 3265 case "17": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6, modifierState); 3266 case "18": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7, modifierState); 3267 case "19": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8, modifierState); 3268 case "20": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9, modifierState); 3269 case "21": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10, modifierState); 3270 case "23": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11, modifierState); 3271 case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); 3272 3273 // starting at 70 i do some magic for like shift+enter etc. 3274 // this only happens on my own terminal emulator. 3275 case "70": return keyPressAndRelease(NonCharacterKeyEvent.Key.ScrollLock, modifierState); 3276 case "78": return keyPressAndRelease2('\b', modifierState); 3277 case "79": return keyPressAndRelease2('\t', modifierState); 3278 case "83": return keyPressAndRelease2('\n', modifierState); 3279 default: 3280 } 3281 break; 3282 3283 default: 3284 } 3285 } else if(terminal.terminalInFamily("rxvt")) { 3286 // look it up in the termcap key database 3287 string cap = terminal.findSequenceInTermcap(sequence); 3288 if(cap !is null) { 3289 //terminal.writeln("found in termcap " ~ cap); 3290 return translateTermcapName(cap); 3291 } 3292 // FIXME: figure these out. rxvt seems to just change the terminator while keeping the rest the same 3293 // though it isn't consistent. ugh. 3294 } else { 3295 // 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 3296 // so this space is semi-intentionally left blank 3297 //terminal.writeln("wtf ", sequence[1..$]); 3298 3299 // look it up in the termcap key database 3300 string cap = terminal.findSequenceInTermcap(sequence); 3301 if(cap !is null) { 3302 //terminal.writeln("found in termcap " ~ cap); 3303 return translateTermcapName(cap); 3304 } 3305 } 3306 } 3307 3308 return null; 3309 } 3310 3311 auto c = remainingFromLastTime == int.max ? nextRaw(true) : remainingFromLastTime; 3312 if(c == -1) 3313 return null; // interrupted; give back nothing so the other level can recheck signal flags 3314 if(c == 0) 3315 return [InputEvent(EndOfFileEvent(), terminal)]; 3316 if(c == '\033') { 3317 if(timedCheckForInput_bypassingBuffer(50)) { 3318 // escape sequence 3319 c = nextRaw(); 3320 if(c == '[') { // CSI, ends on anything >= 'A' 3321 return doEscapeSequence(readEscapeSequence(sequenceBuffer)); 3322 } else if(c == 'O') { 3323 // could be xterm function key 3324 auto n = nextRaw(); 3325 3326 char[3] thing; 3327 thing[0] = '\033'; 3328 thing[1] = 'O'; 3329 thing[2] = cast(char) n; 3330 3331 auto cap = terminal.findSequenceInTermcap(thing); 3332 if(cap is null) { 3333 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ 3334 charPressAndRelease('O') ~ 3335 charPressAndRelease(thing[2]); 3336 } else { 3337 return translateTermcapName(cap); 3338 } 3339 } else if(c == '\033') { 3340 // could be escape followed by an escape sequence! 3341 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ readNextEventsHelper(c); 3342 } else { 3343 // I don't know, probably unsupported terminal or just quick user input or something 3344 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ charPressAndRelease(nextChar(c)); 3345 } 3346 } else { 3347 // user hit escape (or super slow escape sequence, but meh) 3348 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); 3349 } 3350 } else { 3351 // FIXME: what if it is neither? we should check the termcap 3352 auto next = nextChar(c); 3353 if(next == 127) // some terminals send 127 on the backspace. Let's normalize that. 3354 next = '\b'; 3355 return charPressAndRelease(next); 3356 } 3357 } 3358 } 3359 3360 /// The new style of keyboard event 3361 struct KeyboardEvent { 3362 bool pressed; /// 3363 dchar which; /// 3364 alias key = which; /// I often use this when porting old to new so i took it 3365 alias character = which; /// I often use this when porting old to new so i took it 3366 uint modifierState; /// 3367 3368 /// 3369 bool isCharacter() { 3370 return !(which >= Key.min && which <= Key.max); 3371 } 3372 3373 // these match Windows virtual key codes numerically for simplicity of translation there 3374 // but are plus a unicode private use area offset so i can cram them in the dchar 3375 // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 3376 /// . 3377 enum Key : dchar { 3378 escape = 0x1b + 0xF0000, /// . 3379 F1 = 0x70 + 0xF0000, /// . 3380 F2 = 0x71 + 0xF0000, /// . 3381 F3 = 0x72 + 0xF0000, /// . 3382 F4 = 0x73 + 0xF0000, /// . 3383 F5 = 0x74 + 0xF0000, /// . 3384 F6 = 0x75 + 0xF0000, /// . 3385 F7 = 0x76 + 0xF0000, /// . 3386 F8 = 0x77 + 0xF0000, /// . 3387 F9 = 0x78 + 0xF0000, /// . 3388 F10 = 0x79 + 0xF0000, /// . 3389 F11 = 0x7A + 0xF0000, /// . 3390 F12 = 0x7B + 0xF0000, /// . 3391 LeftArrow = 0x25 + 0xF0000, /// . 3392 RightArrow = 0x27 + 0xF0000, /// . 3393 UpArrow = 0x26 + 0xF0000, /// . 3394 DownArrow = 0x28 + 0xF0000, /// . 3395 Insert = 0x2d + 0xF0000, /// . 3396 Delete = 0x2e + 0xF0000, /// . 3397 Home = 0x24 + 0xF0000, /// . 3398 End = 0x23 + 0xF0000, /// . 3399 PageUp = 0x21 + 0xF0000, /// . 3400 PageDown = 0x22 + 0xF0000, /// . 3401 ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator 3402 } 3403 3404 3405 } 3406 3407 /// Deprecated: use KeyboardEvent instead in new programs 3408 /// Input event for characters 3409 struct CharacterEvent { 3410 /// . 3411 enum Type { 3412 Released, /// . 3413 Pressed /// . 3414 } 3415 3416 Type eventType; /// . 3417 dchar character; /// . 3418 uint modifierState; /// Don't depend on this to be available for character events 3419 } 3420 3421 /// Deprecated: use KeyboardEvent instead in new programs 3422 struct NonCharacterKeyEvent { 3423 /// . 3424 enum Type { 3425 Released, /// . 3426 Pressed /// . 3427 } 3428 Type eventType; /// . 3429 3430 // these match Windows virtual key codes numerically for simplicity of translation there 3431 //http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 3432 /// . 3433 enum Key : int { 3434 escape = 0x1b, /// . 3435 F1 = 0x70, /// . 3436 F2 = 0x71, /// . 3437 F3 = 0x72, /// . 3438 F4 = 0x73, /// . 3439 F5 = 0x74, /// . 3440 F6 = 0x75, /// . 3441 F7 = 0x76, /// . 3442 F8 = 0x77, /// . 3443 F9 = 0x78, /// . 3444 F10 = 0x79, /// . 3445 F11 = 0x7A, /// . 3446 F12 = 0x7B, /// . 3447 LeftArrow = 0x25, /// . 3448 RightArrow = 0x27, /// . 3449 UpArrow = 0x26, /// . 3450 DownArrow = 0x28, /// . 3451 Insert = 0x2d, /// . 3452 Delete = 0x2e, /// . 3453 Home = 0x24, /// . 3454 End = 0x23, /// . 3455 PageUp = 0x21, /// . 3456 PageDown = 0x22, /// . 3457 ScrollLock = 0x91, /// unlikely to work outside my terminal emulator 3458 } 3459 Key key; /// . 3460 3461 uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms 3462 3463 } 3464 3465 /// . 3466 struct PasteEvent { 3467 string pastedText; /// . 3468 } 3469 3470 /++ 3471 Indicates a hyperlink was clicked in my custom terminal emulator 3472 or with version `TerminalDirectToEmulator`. 3473 3474 You can simply ignore this event in a `final switch` if you aren't 3475 using the feature. 3476 3477 History: 3478 Added March 18, 2020 3479 +/ 3480 struct LinkEvent { 3481 string text; /// 3482 ushort identifier; /// 3483 ushort command; /// set by the terminal to indicate how it was clicked. values tbd 3484 } 3485 3486 /// . 3487 struct MouseEvent { 3488 // these match simpledisplay.d numerically as well 3489 /// . 3490 enum Type { 3491 Moved = 0, /// . 3492 Pressed = 1, /// . 3493 Released = 2, /// . 3494 Clicked, /// . 3495 } 3496 3497 Type eventType; /// . 3498 3499 // note: these should numerically match simpledisplay.d for maximum beauty in my other code 3500 /// . 3501 enum Button : uint { 3502 None = 0, /// . 3503 Left = 1, /// . 3504 Middle = 4, /// . 3505 Right = 2, /// . 3506 ScrollUp = 8, /// . 3507 ScrollDown = 16 /// . 3508 } 3509 uint buttons; /// A mask of Button 3510 int x; /// 0 == left side 3511 int y; /// 0 == top 3512 uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something 3513 } 3514 3515 /// When you get this, check terminal.width and terminal.height to see the new size and react accordingly. 3516 struct SizeChangedEvent { 3517 int oldWidth; 3518 int oldHeight; 3519 int newWidth; 3520 int newHeight; 3521 } 3522 3523 /// the user hitting ctrl+c will send this 3524 /// You should drop what you're doing and perhaps exit when this happens. 3525 struct UserInterruptionEvent {} 3526 3527 /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. 3528 /// If you receive it, you should generally cleanly exit. 3529 struct HangupEvent {} 3530 3531 /// Sent upon receiving end-of-file from stdin. 3532 struct EndOfFileEvent {} 3533 3534 interface CustomEvent {} 3535 3536 version(Win32Console) 3537 enum ModifierState : uint { 3538 shift = 0x10, 3539 control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl 3540 3541 // i'm not sure if the next two are available 3542 alt = 2 | 1, //2 ==left alt, 1 == right alt 3543 3544 // FIXME: I don't think these are actually available 3545 windows = 512, 3546 meta = 4096, // FIXME sanity 3547 3548 // I don't think this is available on Linux.... 3549 scrollLock = 0x40, 3550 } 3551 else 3552 enum ModifierState : uint { 3553 shift = 4, 3554 alt = 2, 3555 control = 16, 3556 meta = 8, 3557 3558 windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones 3559 } 3560 3561 version(DDoc) 3562 /// 3563 enum ModifierState : uint { 3564 /// 3565 shift = 4, 3566 /// 3567 alt = 2, 3568 /// 3569 control = 16, 3570 3571 } 3572 3573 /++ 3574 [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. 3575 ++/ 3576 struct InputEvent { 3577 /// . 3578 enum Type { 3579 KeyboardEvent, /// Keyboard key pressed (or released, where supported) 3580 CharacterEvent, /// Do not use this in new programs, use KeyboardEvent instead 3581 NonCharacterKeyEvent, /// Do not use this in new programs, use KeyboardEvent instead 3582 PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. 3583 LinkEvent, /// User clicked a hyperlink you created. Simply ignore if you are not using that feature. 3584 MouseEvent, /// only sent if you subscribed to mouse events 3585 SizeChangedEvent, /// only sent if you subscribed to size events 3586 UserInterruptionEvent, /// the user hit ctrl+c 3587 EndOfFileEvent, /// stdin has received an end of file 3588 HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator 3589 CustomEvent /// . 3590 } 3591 3592 /// If this event is deprecated, you should filter it out in new programs 3593 bool isDeprecated() { 3594 return type == Type.CharacterEvent || type == Type.NonCharacterKeyEvent; 3595 } 3596 3597 /// . 3598 @property Type type() { return t; } 3599 3600 /// Returns a pointer to the terminal associated with this event. 3601 /// (You can usually just ignore this as there's only one terminal typically.) 3602 /// 3603 /// It may be null in the case of program-generated events; 3604 @property Terminal* terminal() { return term; } 3605 3606 /++ 3607 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. 3608 3609 See_Also: 3610 3611 The event types: 3612 [KeyboardEvent], [MouseEvent], [SizeChangedEvent], 3613 [PasteEvent], [UserInterruptionEvent], 3614 [EndOfFileEvent], [HangupEvent], [CustomEvent] 3615 3616 And associated functions: 3617 [RealTimeConsoleInput], [ConsoleInputFlags] 3618 ++/ 3619 @property auto get(Type T)() { 3620 if(type != T) 3621 throw new Exception("Wrong event type"); 3622 static if(T == Type.CharacterEvent) 3623 return characterEvent; 3624 else static if(T == Type.KeyboardEvent) 3625 return keyboardEvent; 3626 else static if(T == Type.NonCharacterKeyEvent) 3627 return nonCharacterKeyEvent; 3628 else static if(T == Type.PasteEvent) 3629 return pasteEvent; 3630 else static if(T == Type.LinkEvent) 3631 return linkEvent; 3632 else static if(T == Type.MouseEvent) 3633 return mouseEvent; 3634 else static if(T == Type.SizeChangedEvent) 3635 return sizeChangedEvent; 3636 else static if(T == Type.UserInterruptionEvent) 3637 return userInterruptionEvent; 3638 else static if(T == Type.EndOfFileEvent) 3639 return endOfFileEvent; 3640 else static if(T == Type.HangupEvent) 3641 return hangupEvent; 3642 else static if(T == Type.CustomEvent) 3643 return customEvent; 3644 else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); 3645 } 3646 3647 /// custom event is public because otherwise there's no point at all 3648 this(CustomEvent c, Terminal* p = null) { 3649 t = Type.CustomEvent; 3650 customEvent = c; 3651 } 3652 3653 private { 3654 this(CharacterEvent c, Terminal* p) { 3655 t = Type.CharacterEvent; 3656 characterEvent = c; 3657 } 3658 this(KeyboardEvent c, Terminal* p) { 3659 t = Type.KeyboardEvent; 3660 keyboardEvent = c; 3661 } 3662 this(NonCharacterKeyEvent c, Terminal* p) { 3663 t = Type.NonCharacterKeyEvent; 3664 nonCharacterKeyEvent = c; 3665 } 3666 this(PasteEvent c, Terminal* p) { 3667 t = Type.PasteEvent; 3668 pasteEvent = c; 3669 } 3670 this(LinkEvent c, Terminal* p) { 3671 t = Type.LinkEvent; 3672 linkEvent = c; 3673 } 3674 this(MouseEvent c, Terminal* p) { 3675 t = Type.MouseEvent; 3676 mouseEvent = c; 3677 } 3678 this(SizeChangedEvent c, Terminal* p) { 3679 t = Type.SizeChangedEvent; 3680 sizeChangedEvent = c; 3681 } 3682 this(UserInterruptionEvent c, Terminal* p) { 3683 t = Type.UserInterruptionEvent; 3684 userInterruptionEvent = c; 3685 } 3686 this(HangupEvent c, Terminal* p) { 3687 t = Type.HangupEvent; 3688 hangupEvent = c; 3689 } 3690 this(EndOfFileEvent c, Terminal* p) { 3691 t = Type.EndOfFileEvent; 3692 endOfFileEvent = c; 3693 } 3694 3695 Type t; 3696 Terminal* term; 3697 3698 union { 3699 KeyboardEvent keyboardEvent; 3700 CharacterEvent characterEvent; 3701 NonCharacterKeyEvent nonCharacterKeyEvent; 3702 PasteEvent pasteEvent; 3703 MouseEvent mouseEvent; 3704 SizeChangedEvent sizeChangedEvent; 3705 UserInterruptionEvent userInterruptionEvent; 3706 HangupEvent hangupEvent; 3707 EndOfFileEvent endOfFileEvent; 3708 LinkEvent linkEvent; 3709 CustomEvent customEvent; 3710 } 3711 } 3712 } 3713 3714 version(Demo) 3715 /// View the source of this! 3716 void main() { 3717 auto terminal = Terminal(ConsoleOutputType.cellular); 3718 3719 //terminal.color(Color.DEFAULT, Color.DEFAULT); 3720 3721 // 3722 ///* 3723 auto getter = new FileLineGetter(&terminal, "test"); 3724 getter.prompt = "> "; 3725 getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; 3726 terminal.writeln("\n" ~ getter.getline()); 3727 terminal.writeln("\n" ~ getter.getline()); 3728 terminal.writeln("\n" ~ getter.getline()); 3729 getter.dispose(); 3730 //*/ 3731 3732 terminal.writeln(terminal.getline()); 3733 terminal.writeln(terminal.getline()); 3734 terminal.writeln(terminal.getline()); 3735 3736 //input.getch(); 3737 3738 // return; 3739 // 3740 3741 terminal.setTitle("Basic I/O"); 3742 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEventsWithRelease); 3743 terminal.color(Color.green | Bright, Color.black); 3744 3745 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"); 3746 terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 3747 3748 terminal.color(Color.DEFAULT, Color.DEFAULT); 3749 3750 int centerX = terminal.width / 2; 3751 int centerY = terminal.height / 2; 3752 3753 bool timeToBreak = false; 3754 3755 terminal.hyperlink("test", 4); 3756 terminal.hyperlink("another", 7); 3757 3758 void handleEvent(InputEvent event) { 3759 //terminal.writef("%s\n", event.type); 3760 final switch(event.type) { 3761 case InputEvent.Type.LinkEvent: 3762 auto ev = event.get!(InputEvent.Type.LinkEvent); 3763 terminal.writeln(ev); 3764 break; 3765 case InputEvent.Type.UserInterruptionEvent: 3766 case InputEvent.Type.HangupEvent: 3767 case InputEvent.Type.EndOfFileEvent: 3768 timeToBreak = true; 3769 version(with_eventloop) { 3770 import arsd.eventloop; 3771 exit(); 3772 } 3773 break; 3774 case InputEvent.Type.SizeChangedEvent: 3775 auto ev = event.get!(InputEvent.Type.SizeChangedEvent); 3776 terminal.writeln(ev); 3777 break; 3778 case InputEvent.Type.KeyboardEvent: 3779 auto ev = event.get!(InputEvent.Type.KeyboardEvent); 3780 terminal.writef("\t%s", ev); 3781 terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which); 3782 terminal.writeln(); 3783 if(ev.which == 'Q') { 3784 timeToBreak = true; 3785 version(with_eventloop) { 3786 import arsd.eventloop; 3787 exit(); 3788 } 3789 } 3790 3791 if(ev.which == 'C') 3792 terminal.clear(); 3793 break; 3794 case InputEvent.Type.CharacterEvent: // obsolete 3795 auto ev = event.get!(InputEvent.Type.CharacterEvent); 3796 terminal.writef("\t%s\n", ev); 3797 break; 3798 case InputEvent.Type.NonCharacterKeyEvent: // obsolete 3799 terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent)); 3800 break; 3801 case InputEvent.Type.PasteEvent: 3802 terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent)); 3803 break; 3804 case InputEvent.Type.MouseEvent: 3805 //terminal.writef("\t%s\n", event.get!(InputEvent.Type.MouseEvent)); 3806 break; 3807 case InputEvent.Type.CustomEvent: 3808 break; 3809 } 3810 3811 //terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 3812 3813 /* 3814 if(input.kbhit()) { 3815 auto c = input.getch(); 3816 if(c == 'q' || c == 'Q') 3817 break; 3818 terminal.moveTo(centerX, centerY); 3819 terminal.writef("%c", c); 3820 terminal.flush(); 3821 } 3822 usleep(10000); 3823 */ 3824 } 3825 3826 version(with_eventloop) { 3827 import arsd.eventloop; 3828 addListener(&handleEvent); 3829 loop(); 3830 } else { 3831 loop: while(true) { 3832 auto event = input.nextEvent(); 3833 handleEvent(event); 3834 if(timeToBreak) 3835 break loop; 3836 } 3837 } 3838 } 3839 3840 enum TerminalCapabilities : uint { 3841 minimal = 0, 3842 vt100 = 1 << 0, 3843 3844 // my special terminal emulator extensions 3845 arsdClipboard = 1 << 15, // 90 in caps 3846 arsdImage = 1 << 16, // 91 in caps 3847 arsdHyperlinks = 1 << 17, // 92 in caps 3848 } 3849 3850 version(Posix) 3851 private uint /* TerminalCapabilities bitmask */ getTerminalCapabilities(int fdIn, int fdOut) { 3852 if(fdIn == -1 || fdOut == -1) 3853 return TerminalCapabilities.minimal; 3854 3855 import std.conv; 3856 import core.stdc.errno; 3857 import core.sys.posix.unistd; 3858 3859 ubyte[128] hack2; 3860 termios old; 3861 ubyte[128] hack; 3862 tcgetattr(fdIn, &old); 3863 auto n = old; 3864 n.c_lflag &= ~(ICANON | ECHO); 3865 tcsetattr(fdIn, TCSANOW, &n); 3866 scope(exit) 3867 tcsetattr(fdIn, TCSANOW, &old); 3868 3869 // drain the buffer? meh 3870 3871 string cmd = "\033[c"; 3872 auto err = write(fdOut, cmd.ptr, cmd.length); 3873 if(err != cmd.length) { 3874 throw new Exception("couldn't ask terminal for ID"); 3875 } 3876 3877 // reading directly to bypass any buffering 3878 int retries = 16; 3879 int len; 3880 ubyte[96] buffer; 3881 try_again: 3882 3883 3884 timeval tv; 3885 tv.tv_sec = 0; 3886 tv.tv_usec = 250 * 1000; // 250 ms 3887 3888 fd_set fs; 3889 FD_ZERO(&fs); 3890 3891 FD_SET(fdIn, &fs); 3892 if(select(fdIn + 1, &fs, null, null, &tv) == -1) { 3893 goto try_again; 3894 } 3895 3896 if(FD_ISSET(fdIn, &fs)) { 3897 auto len2 = read(fdIn, &buffer[len], buffer.length - len); 3898 if(len2 <= 0) { 3899 retries--; 3900 if(retries > 0) 3901 goto try_again; 3902 throw new Exception("can't get terminal id"); 3903 } else { 3904 len += len2; 3905 } 3906 } else { 3907 // no data... assume terminal doesn't support giving an answer 3908 return TerminalCapabilities.minimal; 3909 } 3910 3911 ubyte[] answer; 3912 bool hasAnswer(ubyte[] data) { 3913 if(data.length < 4) 3914 return false; 3915 answer = null; 3916 size_t start; 3917 int position = 0; 3918 foreach(idx, ch; data) { 3919 switch(position) { 3920 case 0: 3921 if(ch == '\033') { 3922 start = idx; 3923 position++; 3924 } 3925 break; 3926 case 1: 3927 if(ch == '[') 3928 position++; 3929 else 3930 position = 0; 3931 break; 3932 case 2: 3933 if(ch == '?') 3934 position++; 3935 else 3936 position = 0; 3937 break; 3938 case 3: 3939 // body 3940 if(ch == 'c') { 3941 answer = data[start .. idx + 1]; 3942 return true; 3943 } else if(ch == ';' || (ch >= '0' && ch <= '9')) { 3944 // good, keep going 3945 } else { 3946 // invalid, drop it 3947 position = 0; 3948 } 3949 break; 3950 default: assert(0); 3951 } 3952 } 3953 return false; 3954 } 3955 3956 auto got = buffer[0 .. len]; 3957 if(!hasAnswer(got)) { 3958 goto try_again; 3959 } 3960 auto gots = cast(char[]) answer[3 .. $-1]; 3961 3962 import std.string; 3963 3964 auto pieces = split(gots, ";"); 3965 uint ret = TerminalCapabilities.vt100; 3966 foreach(p; pieces) 3967 switch(p) { 3968 case "90": 3969 ret |= TerminalCapabilities.arsdClipboard; 3970 break; 3971 case "91": 3972 ret |= TerminalCapabilities.arsdImage; 3973 break; 3974 case "92": 3975 ret |= TerminalCapabilities.arsdHyperlinks; 3976 break; 3977 default: 3978 } 3979 return ret; 3980 } 3981 3982 private extern(C) int mkstemp(char *templ); 3983 3984 /** 3985 FIXME: support lines that wrap 3986 FIXME: better controls maybe 3987 3988 FIXME: support multi-line "lines" and some form of line continuation, both 3989 from the user (if permitted) and from the application, so like the user 3990 hits "class foo { \n" and the app says "that line needs continuation" automatically. 3991 3992 FIXME: fix lengths on prompt and suggestion 3993 3994 A note on history: 3995 3996 To save history, you must call LineGetter.dispose() when you're done with it. 3997 History will not be automatically saved without that call! 3998 3999 The history saving and loading as a trivially encountered race condition: if you 4000 open two programs that use the same one at the same time, the one that closes second 4001 will overwrite any history changes the first closer saved. 4002 4003 GNU Getline does this too... and it actually kinda drives me nuts. But I don't know 4004 what a good fix is except for doing a transactional commit straight to the file every 4005 time and that seems like hitting the disk way too often. 4006 4007 We could also do like a history server like a database daemon that keeps the order 4008 correct but I don't actually like that either because I kinda like different bashes 4009 to have different history, I just don't like it all to get lost. 4010 4011 Regardless though, this isn't even used in bash anyway, so I don't think I care enough 4012 to put that much effort into it. Just using separate files for separate tasks is good 4013 enough I think. 4014 */ 4015 class LineGetter { 4016 /* A note on the assumeSafeAppends in here: since these buffers are private, we can be 4017 pretty sure that stomping isn't an issue, so I'm using this liberally to keep the 4018 append/realloc code simple and hopefully reasonably fast. */ 4019 4020 // saved to file 4021 string[] history; 4022 4023 // not saved 4024 Terminal* terminal; 4025 string historyFilename; 4026 4027 /// Make sure that the parent terminal struct remains in scope for the duration 4028 /// of LineGetter's lifetime, as it does hold on to and use the passed pointer 4029 /// throughout. 4030 /// 4031 /// historyFilename will load and save an input history log to a particular folder. 4032 /// Leaving it null will mean no file will be used and history will not be saved across sessions. 4033 this(Terminal* tty, string historyFilename = null) { 4034 this.terminal = tty; 4035 this.historyFilename = historyFilename; 4036 4037 line.reserve(128); 4038 4039 if(historyFilename.length) 4040 loadSettingsAndHistoryFromFile(); 4041 4042 regularForeground = cast(Color) terminal._currentForeground; 4043 background = cast(Color) terminal._currentBackground; 4044 suggestionForeground = Color.blue; 4045 } 4046 4047 /// Call this before letting LineGetter die so it can do any necessary 4048 /// cleanup and save the updated history to a file. 4049 void dispose() { 4050 if(historyFilename.length) 4051 saveSettingsAndHistoryToFile(); 4052 } 4053 4054 /// Override this to change the directory where history files are stored 4055 /// 4056 /// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows. 4057 /* virtual */ string historyFileDirectory() { 4058 version(Windows) { 4059 char[1024] path; 4060 // FIXME: this doesn't link because the crappy dmd lib doesn't have it 4061 if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) { 4062 import core.stdc.string; 4063 return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline"; 4064 } else { 4065 import std.process; 4066 return environment["APPDATA"] ~ "\\arsd-getline"; 4067 } 4068 } else version(Posix) { 4069 import std.process; 4070 return environment["HOME"] ~ "/.arsd-getline"; 4071 } 4072 } 4073 4074 /// You can customize the colors here. You should set these after construction, but before 4075 /// calling startGettingLine or getline. 4076 Color suggestionForeground = Color.blue; 4077 Color regularForeground = Color.DEFAULT; /// ditto 4078 Color background = Color.DEFAULT; /// ditto 4079 Color promptColor = Color.DEFAULT; /// ditto 4080 Color specialCharBackground = Color.green; /// ditto 4081 //bool reverseVideo; 4082 4083 /// Set this if you want a prompt to be drawn with the line. It does NOT support color in string. 4084 @property void prompt(string p) { 4085 this.prompt_ = p; 4086 4087 promptLength = 0; 4088 foreach(dchar c; p) 4089 promptLength++; 4090 } 4091 4092 /// ditto 4093 @property string prompt() { 4094 return this.prompt_; 4095 } 4096 4097 private string prompt_; 4098 private int promptLength; 4099 4100 /++ 4101 Turn on auto suggest if you want a greyed thing of what tab 4102 would be able to fill in as you type. 4103 4104 You might want to turn it off if generating a completion list is slow. 4105 4106 Or if you know you want it, be sure to turn it on explicitly in your 4107 code because I reserve the right to change the default without advance notice. 4108 4109 History: 4110 On March 4, 2020, I changed the default to `false` because it 4111 is kinda slow and not useful in all cases. 4112 +/ 4113 bool autoSuggest = false; 4114 4115 /++ 4116 Returns true if there was any input in the buffer. Can be 4117 checked in the case of a [UserInterruptionException]. 4118 +/ 4119 bool hadInput() { 4120 return line.length > 0; 4121 } 4122 4123 /// Override this if you don't want all lines added to the history. 4124 /// You can return null to not add it at all, or you can transform it. 4125 /* virtual */ string historyFilter(string candidate) { 4126 return candidate; 4127 } 4128 4129 /// You may override this to do nothing 4130 /* virtual */ void saveSettingsAndHistoryToFile() { 4131 import std.file; 4132 if(!exists(historyFileDirectory)) 4133 mkdir(historyFileDirectory); 4134 auto fn = historyPath(); 4135 import std.stdio; 4136 auto file = File(fn, "wt"); 4137 foreach(item; history) 4138 file.writeln(item); 4139 } 4140 4141 /++ 4142 History: 4143 Introduced on January 31, 2020 4144 +/ 4145 /* virtual */ string historyFileExtension() { 4146 return ".history"; 4147 } 4148 4149 private string historyPath() { 4150 import std.path; 4151 auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension(); 4152 return filename; 4153 } 4154 4155 /// You may override this to do nothing 4156 /* virtual */ void loadSettingsAndHistoryFromFile() { 4157 import std.file; 4158 history = null; 4159 auto fn = historyPath(); 4160 if(exists(fn)) { 4161 import std.stdio; 4162 foreach(line; File(fn, "rt").byLine) 4163 history ~= line.idup; 4164 4165 } 4166 } 4167 4168 /++ 4169 Override this to provide tab completion. You may use the candidate 4170 argument to filter the list, but you don't have to (LineGetter will 4171 do it for you on the values you return). This means you can ignore 4172 the arguments if you like. 4173 4174 Ideally, you wouldn't return more than about ten items since the list 4175 gets difficult to use if it is too long. 4176 4177 Tab complete cannot modify text before or after the cursor at this time. 4178 I *might* change that later to allow tab complete to fuzzy search and spell 4179 check fix before. But right now it ONLY inserts. 4180 4181 Default is to provide recent command history as autocomplete. 4182 4183 Returns: 4184 This function should return the full string to replace 4185 `candidate[tabCompleteStartPoint(args) .. $]`. 4186 For example, if your user wrote `wri<tab>` and you want to complete 4187 it to `write` or `writeln`, you should return `["write", "writeln"]`. 4188 4189 If you offer different tab complete in different places, you still 4190 need to return the whole string. For example, a file competition of 4191 a second argument, when the user writes `terminal.d term<tab>` and you 4192 want it to complete to an additional `terminal.d`, you should return 4193 `["terminal.d terminal.d"]`; in other words, `candidate ~ completion` 4194 for each completion. 4195 4196 It does this so you can simply return an array of words without having 4197 to rebuild that array for each combination. 4198 4199 To choose the word separator, override [tabCompleteStartPoint]. 4200 4201 Params: 4202 candidate = the text of the line up to the text cursor, after 4203 which the completed text would be inserted 4204 4205 afterCursor = the remaining text after the cursor. You can inspect 4206 this, but cannot change it - this will be appended to the line 4207 after completion, keeping the cursor in the same relative location. 4208 4209 History: 4210 Prior to January 30, 2020, this method took only one argument, 4211 `candidate`. It now takes `afterCursor` as well, to allow you to 4212 make more intelligent completions with full context. 4213 +/ 4214 /* virtual */ protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { 4215 return history.length > 20 ? history[0 .. 20] : history; 4216 } 4217 4218 /++ 4219 Override this to provide a different tab competition starting point. The default 4220 is `0`, always completing the complete line, but you may return the index of another 4221 character of `candidate` to provide a new split. 4222 4223 Returns: 4224 The index of `candidate` where we should start the slice to keep in [tabComplete]. 4225 It must be `>= 0 && <= candidate.length`. 4226 4227 History: 4228 Added on February 1, 2020. Initial default is to return 0 to maintain 4229 old behavior. 4230 +/ 4231 /* virtual */ protected size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { 4232 return 0; 4233 } 4234 4235 /++ 4236 This gives extra information for an item when displaying tab competition details. 4237 4238 History: 4239 Added January 31, 2020. 4240 4241 +/ 4242 /* virtual */ protected string tabCompleteHelp(string candidate) { 4243 return null; 4244 } 4245 4246 private string[] filterTabCompleteList(string[] list, size_t start) { 4247 if(list.length == 0) 4248 return list; 4249 4250 string[] f; 4251 f.reserve(list.length); 4252 4253 foreach(item; list) { 4254 import std.algorithm; 4255 if(startsWith(item, line[start .. cursorPosition])) 4256 f ~= item; 4257 } 4258 4259 /+ 4260 // if it is excessively long, let's trim it down by trying to 4261 // group common sub-sequences together. 4262 if(f.length > terminal.height * 3 / 4) { 4263 import std.algorithm; 4264 f.sort(); 4265 4266 // see how many can be saved by just keeping going until there is 4267 // no more common prefix. then commit that and keep on down the list. 4268 // since it is sorted, if there is a commonality, it should appear quickly 4269 string[] n; 4270 string commonality = f[0]; 4271 size_t idx = 1; 4272 while(idx < f.length) { 4273 auto c = commonPrefix(commonality, f[idx]); 4274 if(c.length > cursorPosition - start) { 4275 commonality = c; 4276 } else { 4277 n ~= commonality; 4278 commonality = f[idx]; 4279 } 4280 idx++; 4281 } 4282 if(commonality.length) 4283 n ~= commonality; 4284 4285 if(n.length) 4286 f = n; 4287 } 4288 +/ 4289 4290 return f; 4291 } 4292 4293 /++ 4294 Override this to provide a custom display of the tab completion list. 4295 4296 History: 4297 Prior to January 31, 2020, it only displayed the list. After 4298 that, it would call [tabCompleteHelp] for each candidate and display 4299 that string (if present) as well. 4300 +/ 4301 protected void showTabCompleteList(string[] list) { 4302 if(list.length) { 4303 // FIXME: allow mouse clicking of an item, that would be cool 4304 4305 auto start = tabCompleteStartPoint(line[0 .. cursorPosition], line[cursorPosition .. $]); 4306 4307 // FIXME: scroll 4308 //if(terminal.type == ConsoleOutputType.linear) { 4309 terminal.writeln(); 4310 foreach(item; list) { 4311 terminal.color(suggestionForeground, background); 4312 import std.utf; 4313 auto idx = codeLength!char(line[start .. cursorPosition]); 4314 terminal.write(" ", item[0 .. idx]); 4315 terminal.color(regularForeground, background); 4316 terminal.write(item[idx .. $]); 4317 auto help = tabCompleteHelp(item); 4318 if(help !is null) { 4319 import std.string; 4320 help = help.replace("\t", " ").replace("\n", " ").replace("\r", " "); 4321 terminal.write("\t\t"); 4322 int remaining; 4323 if(terminal.cursorX + 2 < terminal.width) { 4324 remaining = terminal.width - terminal.cursorX - 2; 4325 } 4326 if(remaining > 8) 4327 terminal.write(remaining < help.length ? help[0 .. remaining] : help); 4328 } 4329 terminal.writeln(); 4330 4331 } 4332 updateCursorPosition(); 4333 redraw(); 4334 //} 4335 } 4336 } 4337 4338 /++ 4339 Called by the default event loop when the user presses F1. Override 4340 `showHelp` to change the UI, override [helpMessage] if you just want 4341 to change the message. 4342 4343 History: 4344 Introduced on January 30, 2020 4345 +/ 4346 protected void showHelp() { 4347 terminal.writeln(); 4348 terminal.writeln(helpMessage); 4349 updateCursorPosition(); 4350 redraw(); 4351 } 4352 4353 /++ 4354 History: 4355 Introduced on January 30, 2020 4356 +/ 4357 protected string helpMessage() { 4358 return "Press F2 to edit current line in your editor. F3 searches. F9 runs current line while maintaining current edit state."; 4359 } 4360 4361 /++ 4362 History: 4363 Introduced on January 30, 2020 4364 +/ 4365 protected dchar[] editLineInEditor(in dchar[] line, in size_t cursorPosition) { 4366 import std.conv; 4367 import std.process; 4368 import std.file; 4369 4370 char[] tmpName; 4371 4372 version(Windows) { 4373 import core.stdc.string; 4374 char[280] path; 4375 auto l = GetTempPathA(cast(DWORD) path.length, path.ptr); 4376 if(l == 0) throw new Exception("GetTempPathA"); 4377 path[l] = 0; 4378 char[280] name; 4379 auto r = GetTempFileNameA(path.ptr, "adr", 0, name.ptr); 4380 if(r == 0) throw new Exception("GetTempFileNameA"); 4381 tmpName = name[0 .. strlen(name.ptr)]; 4382 scope(exit) 4383 std.file.remove(tmpName); 4384 std.file.write(tmpName, to!string(line)); 4385 4386 string editor = environment.get("EDITOR", "notepad.exe"); 4387 } else { 4388 import core.stdc.stdlib; 4389 import core.sys.posix.unistd; 4390 char[120] name; 4391 string p = "/tmp/adrXXXXXX"; 4392 name[0 .. p.length] = p[]; 4393 name[p.length] = 0; 4394 auto fd = mkstemp(name.ptr); 4395 tmpName = name[0 .. p.length]; 4396 if(fd == -1) throw new Exception("mkstemp"); 4397 scope(exit) 4398 close(fd); 4399 scope(exit) 4400 std.file.remove(tmpName); 4401 4402 string s = to!string(line); 4403 while(s.length) { 4404 auto x = write(fd, s.ptr, s.length); 4405 if(x == -1) throw new Exception("write"); 4406 s = s[x .. $]; 4407 } 4408 string editor = environment.get("EDITOR", "vi"); 4409 } 4410 4411 // FIXME the spawned process changes terminal state! 4412 4413 spawnProcess([editor, tmpName]).wait; 4414 import std.string; 4415 return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp; 4416 } 4417 4418 //private RealTimeConsoleInput* rtci; 4419 4420 /// One-call shop for the main workhorse 4421 /// If you already have a RealTimeConsoleInput ready to go, you 4422 /// should pass a pointer to yours here. Otherwise, LineGetter will 4423 /// make its own. 4424 public string getline(RealTimeConsoleInput* input = null) { 4425 startGettingLine(); 4426 if(input is null) { 4427 auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents | ConsoleInputFlags.noEolWrap); 4428 //rtci = &i; 4429 //scope(exit) rtci = null; 4430 while(workOnLine(i.nextEvent(), &i)) {} 4431 } else { 4432 //rtci = input; 4433 //scope(exit) rtci = null; 4434 while(workOnLine(input.nextEvent(), input)) {} 4435 } 4436 return finishGettingLine(); 4437 } 4438 4439 private int currentHistoryViewPosition = 0; 4440 private dchar[] uncommittedHistoryCandidate; 4441 void loadFromHistory(int howFarBack) { 4442 if(howFarBack < 0) 4443 howFarBack = 0; 4444 if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around. 4445 howFarBack = cast(int) history.length; 4446 if(howFarBack == currentHistoryViewPosition) 4447 return; 4448 if(currentHistoryViewPosition == 0) { 4449 // save the current line so we can down arrow back to it later 4450 if(uncommittedHistoryCandidate.length < line.length) { 4451 uncommittedHistoryCandidate.length = line.length; 4452 } 4453 4454 uncommittedHistoryCandidate[0 .. line.length] = line[]; 4455 uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length]; 4456 uncommittedHistoryCandidate.assumeSafeAppend(); 4457 } 4458 4459 currentHistoryViewPosition = howFarBack; 4460 4461 if(howFarBack == 0) { 4462 line.length = uncommittedHistoryCandidate.length; 4463 line.assumeSafeAppend(); 4464 line[] = uncommittedHistoryCandidate[]; 4465 } else { 4466 line = line[0 .. 0]; 4467 line.assumeSafeAppend(); 4468 foreach(dchar ch; history[$ - howFarBack]) 4469 line ~= ch; 4470 } 4471 4472 cursorPosition = cast(int) line.length; 4473 scrollToEnd(); 4474 } 4475 4476 bool insertMode = true; 4477 bool multiLineMode = false; 4478 4479 private dchar[] line; 4480 private int cursorPosition = 0; 4481 private int horizontalScrollPosition = 0; 4482 4483 private void scrollToEnd() { 4484 horizontalScrollPosition = (cast(int) line.length); 4485 horizontalScrollPosition -= availableLineLength(); 4486 if(horizontalScrollPosition < 0) 4487 horizontalScrollPosition = 0; 4488 } 4489 4490 // used for redrawing the line in the right place 4491 // and detecting mouse events on our line. 4492 private int startOfLineX; 4493 private int startOfLineY; 4494 4495 // private string[] cachedCompletionList; 4496 4497 // FIXME 4498 // /// Note that this assumes the tab complete list won't change between actual 4499 // /// presses of tab by the user. If you pass it a list, it will use it, but 4500 // /// otherwise it will keep track of the last one to avoid calls to tabComplete. 4501 private string suggestion(string[] list = null) { 4502 import std.algorithm, std.utf; 4503 auto relevantLineSection = line[0 .. cursorPosition]; 4504 auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); 4505 relevantLineSection = relevantLineSection[start .. $]; 4506 // FIXME: see about caching the list if we easily can 4507 if(list is null) 4508 list = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); 4509 4510 if(list.length) { 4511 string commonality = list[0]; 4512 foreach(item; list[1 .. $]) { 4513 commonality = commonPrefix(commonality, item); 4514 } 4515 4516 if(commonality.length) { 4517 return commonality[codeLength!char(relevantLineSection) .. $]; 4518 } 4519 } 4520 4521 return null; 4522 } 4523 4524 /// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something. 4525 /// You'll probably want to call redraw() after adding chars. 4526 void addChar(dchar ch) { 4527 assert(cursorPosition >= 0 && cursorPosition <= line.length); 4528 if(cursorPosition == line.length) 4529 line ~= ch; 4530 else { 4531 assert(line.length); 4532 if(insertMode) { 4533 line ~= ' '; 4534 for(int i = cast(int) line.length - 2; i >= cursorPosition; i --) 4535 line[i + 1] = line[i]; 4536 } 4537 line[cursorPosition] = ch; 4538 } 4539 cursorPosition++; 4540 4541 if(cursorPosition >= horizontalScrollPosition + availableLineLength()) 4542 horizontalScrollPosition++; 4543 } 4544 4545 /// . 4546 void addString(string s) { 4547 // FIXME: this could be more efficient 4548 // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) 4549 foreach(dchar ch; s) 4550 addChar(ch); 4551 } 4552 4553 /// Deletes the character at the current position in the line. 4554 /// You'll probably want to call redraw() after deleting chars. 4555 void deleteChar() { 4556 if(cursorPosition == line.length) 4557 return; 4558 for(int i = cursorPosition; i < line.length - 1; i++) 4559 line[i] = line[i + 1]; 4560 line = line[0 .. $-1]; 4561 line.assumeSafeAppend(); 4562 } 4563 4564 /// 4565 void deleteToEndOfLine() { 4566 line = line[0 .. cursorPosition]; 4567 line.assumeSafeAppend(); 4568 //while(cursorPosition < line.length) 4569 //deleteChar(); 4570 } 4571 4572 int availableLineLength() { 4573 return terminal.width - startOfLineX - promptLength - 1; 4574 } 4575 4576 private int lastDrawLength = 0; 4577 void redraw() { 4578 terminal.hideCursor(); 4579 scope(exit) { 4580 version(Win32Console) { 4581 // on Windows, we want to make sure all 4582 // is displayed before the cursor jumps around 4583 terminal.flush(); 4584 terminal.showCursor(); 4585 } else { 4586 // but elsewhere, the showCursor is itself buffered, 4587 // so we can do it all at once for a slight speed boost 4588 terminal.showCursor(); 4589 //import std.string; import std.stdio; writeln(terminal.writeBuffer.replace("\033", "\\e")); 4590 terminal.flush(); 4591 } 4592 } 4593 terminal.moveTo(startOfLineX, startOfLineY); 4594 4595 auto lineLength = availableLineLength(); 4596 if(lineLength < 0) 4597 throw new Exception("too narrow terminal to draw"); 4598 4599 terminal.color(promptColor, background); 4600 terminal.write(prompt); 4601 terminal.color(regularForeground, background); 4602 4603 auto towrite = line[horizontalScrollPosition .. $]; 4604 auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; 4605 auto cursorPositionToDrawY = 0; 4606 4607 int written = promptLength; 4608 4609 void specialChar(char c) { 4610 terminal.color(regularForeground, specialCharBackground); 4611 terminal.write(c); 4612 terminal.color(regularForeground, background); 4613 4614 written++; 4615 lineLength--; 4616 } 4617 4618 void regularChar(dchar ch) { 4619 import std.utf; 4620 char[4] buffer; 4621 auto l = encode(buffer, ch); 4622 // note the Terminal buffers it so meh 4623 terminal.write(buffer[0 .. l]); 4624 4625 written++; 4626 lineLength--; 4627 } 4628 4629 // FIXME: if there is a color at the end of the line it messes up as you scroll 4630 // FIXME: need a way to go to multi-line editing 4631 4632 foreach(dchar ch; towrite) { 4633 if(lineLength == 0) 4634 break; 4635 switch(ch) { 4636 case '\n': specialChar('n'); break; 4637 case '\r': specialChar('r'); break; 4638 case '\a': specialChar('a'); break; 4639 case '\t': specialChar('t'); break; 4640 case '\b': specialChar('b'); break; 4641 case '\033': specialChar('e'); break; 4642 default: 4643 regularChar(ch); 4644 } 4645 } 4646 4647 string suggestion; 4648 4649 if(lineLength >= 0) { 4650 suggestion = ((cursorPosition == towrite.length) && autoSuggest) ? this.suggestion() : null; 4651 if(suggestion.length) { 4652 terminal.color(suggestionForeground, background); 4653 foreach(dchar ch; suggestion) { 4654 if(lineLength == 0) 4655 break; 4656 regularChar(ch); 4657 } 4658 terminal.color(regularForeground, background); 4659 } 4660 } 4661 4662 // FIXME: graphemes 4663 4664 if(written < lastDrawLength) 4665 foreach(i; written .. lastDrawLength) 4666 terminal.write(" "); 4667 lastDrawLength = written; 4668 4669 terminal.moveTo(startOfLineX + cursorPositionToDrawX + promptLength, startOfLineY + cursorPositionToDrawY); 4670 } 4671 4672 /// Starts getting a new line. Call workOnLine and finishGettingLine afterward. 4673 /// 4674 /// Make sure that you've flushed your input and output before calling this 4675 /// function or else you might lose events or get exceptions from this. 4676 void startGettingLine() { 4677 // reset from any previous call first 4678 if(!maintainBuffer) { 4679 cursorPosition = 0; 4680 horizontalScrollPosition = 0; 4681 justHitTab = false; 4682 currentHistoryViewPosition = 0; 4683 if(line.length) { 4684 line = line[0 .. 0]; 4685 line.assumeSafeAppend(); 4686 } 4687 } 4688 4689 maintainBuffer = false; 4690 4691 initializeWithSize(true); 4692 4693 terminal.cursor = TerminalCursor.insert; 4694 terminal.showCursor(); 4695 } 4696 4697 private void positionCursor() { 4698 if(cursorPosition == 0) 4699 horizontalScrollPosition = 0; 4700 else if(cursorPosition == line.length) 4701 scrollToEnd(); 4702 else { 4703 // otherwise just try to center it in the screen 4704 horizontalScrollPosition = cursorPosition; 4705 horizontalScrollPosition -= terminal.width / 2; 4706 // align on a code point boundary 4707 aligned(horizontalScrollPosition, -1); 4708 if(horizontalScrollPosition < 0) 4709 horizontalScrollPosition = 0; 4710 } 4711 } 4712 4713 private void aligned(ref int what, int direction) { 4714 // whereas line is right now dchar[] no need for this 4715 // at least until we go by grapheme... 4716 /* 4717 while(what > 0 && what < line.length && ((line[what] & 0b1100_0000) == 0b1000_0000)) 4718 what += direction; 4719 */ 4720 } 4721 4722 private void initializeWithSize(bool firstEver = false) { 4723 auto x = startOfLineX; 4724 4725 updateCursorPosition(); 4726 4727 if(!firstEver) { 4728 startOfLineX = x; 4729 positionCursor(); 4730 } 4731 4732 lastDrawLength = terminal.width - terminal.cursorX; 4733 version(Win32Console) 4734 lastDrawLength -= 1; // I don't like this but Windows resizing is different anyway and it is liable to scroll if i go over.. 4735 4736 redraw(); 4737 } 4738 4739 private void updateCursorPosition() { 4740 terminal.flush(); 4741 4742 // then get the current cursor position to start fresh 4743 version(TerminalDirectToEmulator) { 4744 if(!terminal.usingDirectEmulator) 4745 return updateCursorPosition_impl(); 4746 startOfLineX = terminal.tew.terminalEmulator.cursorX; 4747 startOfLineY = terminal.tew.terminalEmulator.cursorY; 4748 } else 4749 updateCursorPosition_impl(); 4750 } 4751 private void updateCursorPosition_impl() { 4752 version(Win32Console) { 4753 CONSOLE_SCREEN_BUFFER_INFO info; 4754 GetConsoleScreenBufferInfo(terminal.hConsole, &info); 4755 startOfLineX = info.dwCursorPosition.X; 4756 startOfLineY = info.dwCursorPosition.Y; 4757 } else version(Posix) { 4758 // request current cursor position 4759 4760 // we have to turn off cooked mode to get this answer, otherwise it will all 4761 // be messed up. (I hate unix terminals, the Windows way is so much easer.) 4762 4763 // We also can't use RealTimeConsoleInput here because it also does event loop stuff 4764 // which would be broken by the child destructor :( (maybe that should be a FIXME) 4765 4766 /+ 4767 if(rtci !is null) { 4768 while(rtci.timedCheckForInput_bypassingBuffer(1000)) 4769 rtci.inputQueue ~= rtci.readNextEvents(); 4770 } 4771 +/ 4772 4773 ubyte[128] hack2; 4774 termios old; 4775 ubyte[128] hack; 4776 tcgetattr(terminal.fdIn, &old); 4777 auto n = old; 4778 n.c_lflag &= ~(ICANON | ECHO); 4779 tcsetattr(terminal.fdIn, TCSANOW, &n); 4780 scope(exit) 4781 tcsetattr(terminal.fdIn, TCSANOW, &old); 4782 4783 4784 terminal.writeStringRaw("\033[6n"); 4785 terminal.flush(); 4786 4787 import std.conv; 4788 import core.stdc.errno; 4789 4790 import core.sys.posix.unistd; 4791 4792 ubyte readOne() { 4793 ubyte[1] buffer; 4794 int tries = 0; 4795 try_again: 4796 if(tries > 30) 4797 throw new Exception("terminal reply timed out"); 4798 auto len = read(terminal.fdIn, buffer.ptr, buffer.length); 4799 if(len == -1) { 4800 if(errno == EINTR) 4801 goto try_again; 4802 if(errno == EAGAIN || errno == EWOULDBLOCK) { 4803 import core.thread; 4804 Thread.sleep(10.msecs); 4805 tries++; 4806 goto try_again; 4807 } 4808 } else if(len == 0) { 4809 throw new Exception("Couldn't get cursor position to initialize get line " ~ to!string(len) ~ " " ~ to!string(errno)); 4810 } 4811 4812 return buffer[0]; 4813 } 4814 4815 nextEscape: 4816 while(readOne() != '\033') {} 4817 if(readOne() != '[') 4818 goto nextEscape; 4819 4820 int x, y; 4821 4822 // now we should have some numbers being like yyy;xxxR 4823 // but there may be a ? in there too; DEC private mode format 4824 // of the very same data. 4825 4826 x = 0; 4827 y = 0; 4828 4829 auto b = readOne(); 4830 4831 if(b == '?') 4832 b = readOne(); // no big deal, just ignore and continue 4833 4834 nextNumberY: 4835 if(b >= '0' || b <= '9') { 4836 y *= 10; 4837 y += b - '0'; 4838 } else goto nextEscape; 4839 4840 b = readOne(); 4841 if(b != ';') 4842 goto nextNumberY; 4843 4844 nextNumberX: 4845 b = readOne(); 4846 if(b >= '0' || b <= '9') { 4847 x *= 10; 4848 x += b - '0'; 4849 } else goto nextEscape; 4850 4851 b = readOne(); 4852 if(b != 'R') 4853 goto nextEscape; // it wasn't the right thing it after all 4854 4855 startOfLineX = x - 1; 4856 startOfLineY = y - 1; 4857 } 4858 4859 // updating these too because I can with the more accurate info from above 4860 terminal._cursorX = startOfLineX; 4861 terminal._cursorY = startOfLineY; 4862 } 4863 4864 private bool justHitTab; 4865 private bool eof; 4866 4867 /// 4868 string delegate(string s) pastePreprocessor; 4869 4870 string defaultPastePreprocessor(string s) { 4871 return s; 4872 } 4873 4874 void showIndividualHelp(string help) { 4875 terminal.writeln(); 4876 terminal.writeln(help); 4877 } 4878 4879 private bool maintainBuffer; 4880 4881 /++ 4882 for integrating into another event loop 4883 you can pass individual events to this and 4884 the line getter will work on it 4885 4886 returns false when there's nothing more to do 4887 4888 History: 4889 On February 17, 2020, it was changed to take 4890 a new argument which should be the input source 4891 where the event came from. 4892 +/ 4893 bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) { 4894 switch(e.type) { 4895 case InputEvent.Type.EndOfFileEvent: 4896 justHitTab = false; 4897 eof = true; 4898 // FIXME: this should be distinct from an empty line when hit at the beginning 4899 return false; 4900 //break; 4901 case InputEvent.Type.KeyboardEvent: 4902 auto ev = e.keyboardEvent; 4903 if(ev.pressed == false) 4904 return true; 4905 /* Insert the character (unless it is backspace, tab, or some other control char) */ 4906 auto ch = ev.which; 4907 switch(ch) { 4908 version(Windows) case 26: // and this is really for Windows 4909 goto case; 4910 case 4: // ctrl+d will also send a newline-equivalent 4911 if(line.length == 0) 4912 eof = true; 4913 goto case; 4914 case '\r': 4915 case '\n': 4916 justHitTab = false; 4917 return false; 4918 case '\t': 4919 auto relevantLineSection = line[0 .. cursorPosition]; 4920 auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); 4921 relevantLineSection = relevantLineSection[start .. $]; 4922 auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); 4923 import std.utf; 4924 4925 if(possibilities.length == 1) { 4926 auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $]; 4927 if(toFill.length) { 4928 addString(toFill); 4929 redraw(); 4930 } else { 4931 auto help = this.tabCompleteHelp(possibilities[0]); 4932 if(help.length) { 4933 showIndividualHelp(help); 4934 updateCursorPosition(); 4935 redraw(); 4936 } 4937 } 4938 justHitTab = false; 4939 } else { 4940 if(justHitTab) { 4941 justHitTab = false; 4942 showTabCompleteList(possibilities); 4943 } else { 4944 justHitTab = true; 4945 /* fill it in with as much commonality as there is amongst all the suggestions */ 4946 auto suggestion = this.suggestion(possibilities); 4947 if(suggestion.length) { 4948 addString(suggestion); 4949 redraw(); 4950 } 4951 } 4952 } 4953 break; 4954 case '\b': 4955 justHitTab = false; 4956 if(cursorPosition) { 4957 cursorPosition--; 4958 for(int i = cursorPosition; i < line.length - 1; i++) 4959 line[i] = line[i + 1]; 4960 line = line[0 .. $ - 1]; 4961 line.assumeSafeAppend(); 4962 4963 if(!multiLineMode) { 4964 if(horizontalScrollPosition > cursorPosition - 1) 4965 horizontalScrollPosition = cursorPosition - 1 - availableLineLength(); 4966 if(horizontalScrollPosition < 0) 4967 horizontalScrollPosition = 0; 4968 } 4969 4970 redraw(); 4971 } 4972 break; 4973 case KeyboardEvent.Key.escape: 4974 justHitTab = false; 4975 cursorPosition = 0; 4976 horizontalScrollPosition = 0; 4977 line = line[0 .. 0]; 4978 line.assumeSafeAppend(); 4979 redraw(); 4980 break; 4981 case KeyboardEvent.Key.F1: 4982 justHitTab = false; 4983 showHelp(); 4984 break; 4985 case KeyboardEvent.Key.F2: 4986 justHitTab = false; 4987 line = editLineInEditor(line, cursorPosition); 4988 if(cursorPosition > line.length) 4989 cursorPosition = cast(int) line.length; 4990 if(horizontalScrollPosition > line.length) 4991 horizontalScrollPosition = cast(int) line.length; 4992 positionCursor(); 4993 redraw(); 4994 break; 4995 case KeyboardEvent.Key.F3: 4996 // case 'r' - 'a' + 1: // ctrl+r 4997 justHitTab = false; 4998 // search in history 4999 // FIXME: what about search in completion too? 5000 break; 5001 case KeyboardEvent.Key.F4: 5002 justHitTab = false; 5003 // FIXME: clear line 5004 break; 5005 case KeyboardEvent.Key.F9: 5006 justHitTab = false; 5007 // compile and run analog; return the current string 5008 // but keep the buffer the same 5009 maintainBuffer = true; 5010 return false; 5011 case 0x1d: // ctrl+5, because of vim % shortcut 5012 justHitTab = false; 5013 // FIXME: find matching delimiter 5014 break; 5015 case KeyboardEvent.Key.LeftArrow: 5016 justHitTab = false; 5017 if(cursorPosition) 5018 cursorPosition--; 5019 if(ev.modifierState & ModifierState.control) { 5020 while(cursorPosition && line[cursorPosition - 1] != ' ') 5021 cursorPosition--; 5022 } 5023 aligned(cursorPosition, -1); 5024 5025 if(cursorPosition < horizontalScrollPosition) 5026 positionCursor(); 5027 5028 redraw(); 5029 break; 5030 case KeyboardEvent.Key.RightArrow: 5031 justHitTab = false; 5032 if(cursorPosition < line.length) 5033 cursorPosition++; 5034 5035 if(ev.modifierState & ModifierState.control) { 5036 while(cursorPosition + 1 < line.length && line[cursorPosition + 1] != ' ') 5037 cursorPosition++; 5038 cursorPosition += 2; 5039 if(cursorPosition > line.length) 5040 cursorPosition = cast(int) line.length; 5041 } 5042 aligned(cursorPosition, 1); 5043 5044 if(cursorPosition > horizontalScrollPosition + availableLineLength()) 5045 positionCursor(); 5046 5047 redraw(); 5048 break; 5049 case KeyboardEvent.Key.UpArrow: 5050 justHitTab = false; 5051 loadFromHistory(currentHistoryViewPosition + 1); 5052 redraw(); 5053 break; 5054 case KeyboardEvent.Key.DownArrow: 5055 justHitTab = false; 5056 loadFromHistory(currentHistoryViewPosition - 1); 5057 redraw(); 5058 break; 5059 case KeyboardEvent.Key.PageUp: 5060 justHitTab = false; 5061 loadFromHistory(cast(int) history.length); 5062 redraw(); 5063 break; 5064 case KeyboardEvent.Key.PageDown: 5065 justHitTab = false; 5066 loadFromHistory(0); 5067 redraw(); 5068 break; 5069 case 1: // ctrl+a does home too in the emacs keybindings 5070 case KeyboardEvent.Key.Home: 5071 justHitTab = false; 5072 cursorPosition = 0; 5073 horizontalScrollPosition = 0; 5074 redraw(); 5075 break; 5076 case 5: // ctrl+e from emacs 5077 case KeyboardEvent.Key.End: 5078 justHitTab = false; 5079 cursorPosition = cast(int) line.length; 5080 scrollToEnd(); 5081 redraw(); 5082 break; 5083 case ('v' - 'a' + 1): 5084 if(rtti) 5085 rtti.requestPasteFromClipboard(); 5086 break; 5087 case KeyboardEvent.Key.Insert: 5088 justHitTab = false; 5089 if(ev.modifierState & ModifierState.shift) { 5090 // paste 5091 5092 // shift+insert = request paste 5093 // ctrl+insert = request copy. but that needs a selection 5094 5095 // those work on Windows!!!! and many linux TEs too. 5096 // but if it does make it here, we'll attempt it at this level 5097 if(rtti) 5098 rtti.requestPasteFromClipboard(); 5099 } else if(ev.modifierState & ModifierState.control) { 5100 // copy 5101 // FIXME 5102 } else { 5103 insertMode = !insertMode; 5104 5105 if(insertMode) 5106 terminal.cursor = TerminalCursor.insert; 5107 else 5108 terminal.cursor = TerminalCursor.block; 5109 } 5110 break; 5111 case KeyboardEvent.Key.Delete: 5112 justHitTab = false; 5113 if(ev.modifierState & ModifierState.control) 5114 deleteToEndOfLine(); 5115 else 5116 deleteChar(); 5117 redraw(); 5118 break; 5119 case 11: // ctrl+k is delete to end of line from emacs 5120 justHitTab = false; 5121 deleteToEndOfLine(); 5122 redraw(); 5123 break; 5124 default: 5125 justHitTab = false; 5126 if(e.keyboardEvent.isCharacter) 5127 addChar(ch); 5128 redraw(); 5129 } 5130 break; 5131 case InputEvent.Type.PasteEvent: 5132 justHitTab = false; 5133 if(pastePreprocessor) 5134 addString(pastePreprocessor(e.pasteEvent.pastedText)); 5135 else 5136 addString(defaultPastePreprocessor(e.pasteEvent.pastedText)); 5137 redraw(); 5138 break; 5139 case InputEvent.Type.MouseEvent: 5140 /* Clicking with the mouse to move the cursor is so much easier than arrowing 5141 or even emacs/vi style movements much of the time, so I'ma support it. */ 5142 5143 auto me = e.mouseEvent; 5144 if(me.eventType == MouseEvent.Type.Pressed) { 5145 if(me.buttons & MouseEvent.Button.Left) { 5146 if(me.y == startOfLineY) { 5147 int p = me.x - startOfLineX - promptLength + horizontalScrollPosition; 5148 if(p >= 0 && p < line.length) { 5149 justHitTab = false; 5150 cursorPosition = p; 5151 redraw(); 5152 } 5153 } 5154 } 5155 if(me.buttons & MouseEvent.Button.Middle) { 5156 if(rtti) 5157 rtti.requestPasteFromPrimary(); 5158 } 5159 } 5160 break; 5161 case InputEvent.Type.SizeChangedEvent: 5162 /* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent 5163 yourself and then don't pass it to this function. */ 5164 // FIXME 5165 initializeWithSize(); 5166 break; 5167 case InputEvent.Type.UserInterruptionEvent: 5168 /* I'll take this as canceling the line. */ 5169 throw new UserInterruptionException(); 5170 //break; 5171 case InputEvent.Type.HangupEvent: 5172 /* I'll take this as canceling the line. */ 5173 throw new HangupException(); 5174 //break; 5175 default: 5176 /* ignore. ideally it wouldn't be passed to us anyway! */ 5177 } 5178 5179 return true; 5180 } 5181 5182 string finishGettingLine() { 5183 import std.conv; 5184 auto f = to!string(line); 5185 auto history = historyFilter(f); 5186 if(history !is null) 5187 this.history ~= history; 5188 5189 // FIXME: we should hide the cursor if it was hidden in the call to startGettingLine 5190 return eof ? null : f.length ? f : ""; 5191 } 5192 } 5193 5194 /// Adds default constructors that just forward to the superclass 5195 mixin template LineGetterConstructors() { 5196 this(Terminal* tty, string historyFilename = null) { 5197 super(tty, historyFilename); 5198 } 5199 } 5200 5201 /// This is a line getter that customizes the tab completion to 5202 /// fill in file names separated by spaces, like a command line thing. 5203 class FileLineGetter : LineGetter { 5204 mixin LineGetterConstructors; 5205 5206 /// You can set this property to tell it where to search for the files 5207 /// to complete. 5208 string searchDirectory = "."; 5209 5210 override size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { 5211 import std.string; 5212 return candidate.lastIndexOf(" ") + 1; 5213 } 5214 5215 override protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { 5216 import std.file, std.conv, std.algorithm, std.string; 5217 5218 string[] list; 5219 foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) { 5220 // both with and without the (searchDirectory ~ "/") 5221 list ~= name[searchDirectory.length + 1 .. $]; 5222 list ~= name[0 .. $]; 5223 } 5224 5225 return list; 5226 } 5227 } 5228 5229 version(Windows) { 5230 // to get the directory for saving history in the line things 5231 enum CSIDL_APPDATA = 26; 5232 extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); 5233 } 5234 5235 5236 5237 5238 5239 /* Like getting a line, printing a lot of lines is kinda important too, so I'm including 5240 that widget here too. */ 5241 5242 5243 struct ScrollbackBuffer { 5244 5245 bool demandsAttention; 5246 5247 this(string name) { 5248 this.name = name; 5249 } 5250 5251 void write(T...)(T t) { 5252 import std.conv : text; 5253 addComponent(text(t), foreground_, background_, null); 5254 } 5255 5256 void writeln(T...)(T t) { 5257 write(t, "\n"); 5258 } 5259 5260 void writef(T...)(string fmt, T t) { 5261 import std.format: format; 5262 write(format(fmt, t)); 5263 } 5264 5265 void writefln(T...)(string fmt, T t) { 5266 writef(fmt, t, "\n"); 5267 } 5268 5269 void clear() { 5270 lines.clear(); 5271 clickRegions = null; 5272 scrollbackPosition = 0; 5273 } 5274 5275 int foreground_ = Color.DEFAULT, background_ = Color.DEFAULT; 5276 void color(int foreground, int background) { 5277 this.foreground_ = foreground; 5278 this.background_ = background; 5279 } 5280 5281 void addComponent(string text, int foreground, int background, bool delegate() onclick) { 5282 if(lines.length == 0) { 5283 addLine(); 5284 } 5285 bool first = true; 5286 import std.algorithm; 5287 foreach(t; splitter(text, "\n")) { 5288 if(!first) addLine(); 5289 first = false; 5290 lines[$-1].components ~= LineComponent(t, foreground, background, onclick); 5291 } 5292 } 5293 5294 void addLine() { 5295 lines ~= Line(); 5296 if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are 5297 scrollbackPosition++; 5298 } 5299 5300 void addLine(string line) { 5301 lines ~= Line([LineComponent(line)]); 5302 if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are 5303 scrollbackPosition++; 5304 } 5305 5306 void scrollUp(int lines = 1) { 5307 scrollbackPosition += lines; 5308 //if(scrollbackPosition >= this.lines.length) 5309 // scrollbackPosition = cast(int) this.lines.length - 1; 5310 } 5311 5312 void scrollDown(int lines = 1) { 5313 scrollbackPosition -= lines; 5314 if(scrollbackPosition < 0) 5315 scrollbackPosition = 0; 5316 } 5317 5318 void scrollToBottom() { 5319 scrollbackPosition = 0; 5320 } 5321 5322 // this needs width and height to know how to word wrap it 5323 void scrollToTop(int width, int height) { 5324 scrollbackPosition = scrollTopPosition(width, height); 5325 } 5326 5327 5328 5329 5330 struct LineComponent { 5331 string text; 5332 bool isRgb; 5333 union { 5334 int color; 5335 RGB colorRgb; 5336 } 5337 union { 5338 int background; 5339 RGB backgroundRgb; 5340 } 5341 bool delegate() onclick; // return true if you need to redraw 5342 5343 // 16 color ctor 5344 this(string text, int color = Color.DEFAULT, int background = Color.DEFAULT, bool delegate() onclick = null) { 5345 this.text = text; 5346 this.color = color; 5347 this.background = background; 5348 this.onclick = onclick; 5349 this.isRgb = false; 5350 } 5351 5352 // true color ctor 5353 this(string text, RGB colorRgb, RGB backgroundRgb = RGB(0, 0, 0), bool delegate() onclick = null) { 5354 this.text = text; 5355 this.colorRgb = colorRgb; 5356 this.backgroundRgb = backgroundRgb; 5357 this.onclick = onclick; 5358 this.isRgb = true; 5359 } 5360 } 5361 5362 struct Line { 5363 LineComponent[] components; 5364 int length() { 5365 int l = 0; 5366 foreach(c; components) 5367 l += c.text.length; 5368 return l; 5369 } 5370 } 5371 5372 static struct CircularBuffer(T) { 5373 T[] backing; 5374 5375 enum maxScrollback = 8192; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask... 5376 5377 int start; 5378 int length_; 5379 5380 void clear() { 5381 backing = null; 5382 start = 0; 5383 length_ = 0; 5384 } 5385 5386 size_t length() { 5387 return length_; 5388 } 5389 5390 void opOpAssign(string op : "~")(T line) { 5391 if(length_ < maxScrollback) { 5392 backing.assumeSafeAppend(); 5393 backing ~= line; 5394 length_++; 5395 } else { 5396 backing[start] = line; 5397 start++; 5398 if(start == maxScrollback) 5399 start = 0; 5400 } 5401 } 5402 5403 ref T opIndex(int idx) { 5404 return backing[(start + idx) % maxScrollback]; 5405 } 5406 ref T opIndex(Dollar idx) { 5407 return backing[(start + (length + idx.offsetFromEnd)) % maxScrollback]; 5408 } 5409 5410 CircularBufferRange opSlice(int startOfIteration, Dollar end) { 5411 return CircularBufferRange(&this, startOfIteration, cast(int) length - startOfIteration + end.offsetFromEnd); 5412 } 5413 CircularBufferRange opSlice(int startOfIteration, int end) { 5414 return CircularBufferRange(&this, startOfIteration, end - startOfIteration); 5415 } 5416 CircularBufferRange opSlice() { 5417 return CircularBufferRange(&this, 0, cast(int) length); 5418 } 5419 5420 static struct CircularBufferRange { 5421 CircularBuffer* item; 5422 int position; 5423 int remaining; 5424 this(CircularBuffer* item, int startOfIteration, int count) { 5425 this.item = item; 5426 position = startOfIteration; 5427 remaining = count; 5428 } 5429 5430 ref T front() { return (*item)[position]; } 5431 bool empty() { return remaining <= 0; } 5432 void popFront() { 5433 position++; 5434 remaining--; 5435 } 5436 5437 ref T back() { return (*item)[remaining - 1 - position]; } 5438 void popBack() { 5439 remaining--; 5440 } 5441 } 5442 5443 static struct Dollar { 5444 int offsetFromEnd; 5445 Dollar opBinary(string op : "-")(int rhs) { 5446 return Dollar(offsetFromEnd - rhs); 5447 } 5448 } 5449 Dollar opDollar() { return Dollar(0); } 5450 } 5451 5452 CircularBuffer!Line lines; 5453 string name; 5454 5455 int x, y, width, height; 5456 5457 int scrollbackPosition; 5458 5459 5460 int scrollTopPosition(int width, int height) { 5461 int lineCount; 5462 5463 foreach_reverse(line; lines) { 5464 int written = 0; 5465 comp_loop: foreach(cidx, component; line.components) { 5466 auto towrite = component.text; 5467 foreach(idx, dchar ch; towrite) { 5468 if(written >= width) { 5469 lineCount++; 5470 written = 0; 5471 } 5472 5473 if(ch == '\t') 5474 written += 8; // FIXME 5475 else 5476 written++; 5477 } 5478 } 5479 lineCount++; 5480 } 5481 5482 //if(lineCount > height) 5483 return lineCount - height; 5484 //return 0; 5485 } 5486 5487 void drawInto(Terminal* terminal, in int x = 0, in int y = 0, int width = 0, int height = 0) { 5488 if(lines.length == 0) 5489 return; 5490 5491 if(width == 0) 5492 width = terminal.width; 5493 if(height == 0) 5494 height = terminal.height; 5495 5496 this.x = x; 5497 this.y = y; 5498 this.width = width; 5499 this.height = height; 5500 5501 /* We need to figure out how much is going to fit 5502 in a first pass, so we can figure out where to 5503 start drawing */ 5504 5505 int remaining = height + scrollbackPosition; 5506 int start = cast(int) lines.length; 5507 int howMany = 0; 5508 5509 bool firstPartial = false; 5510 5511 static struct Idx { 5512 size_t cidx; 5513 size_t idx; 5514 } 5515 5516 Idx firstPartialStartIndex; 5517 5518 // this is private so I know we can safe append 5519 clickRegions.length = 0; 5520 clickRegions.assumeSafeAppend(); 5521 5522 // FIXME: should prolly handle \n and \r in here too. 5523 5524 // we'll work backwards to figure out how much will fit... 5525 // this will give accurate per-line things even with changing width and wrapping 5526 // while being generally efficient - we usually want to show the end of the list 5527 // anyway; actually using the scrollback is a bit of an exceptional case. 5528 5529 // It could probably do this instead of on each redraw, on each resize or insertion. 5530 // or at least cache between redraws until one of those invalidates it. 5531 foreach_reverse(line; lines) { 5532 int written = 0; 5533 int brokenLineCount; 5534 Idx[16] lineBreaksBuffer; 5535 Idx[] lineBreaks = lineBreaksBuffer[]; 5536 comp_loop: foreach(cidx, component; line.components) { 5537 auto towrite = component.text; 5538 foreach(idx, dchar ch; towrite) { 5539 if(written >= width) { 5540 if(brokenLineCount == lineBreaks.length) 5541 lineBreaks ~= Idx(cidx, idx); 5542 else 5543 lineBreaks[brokenLineCount] = Idx(cidx, idx); 5544 5545 brokenLineCount++; 5546 5547 written = 0; 5548 } 5549 5550 if(ch == '\t') 5551 written += 8; // FIXME 5552 else 5553 written++; 5554 } 5555 } 5556 5557 lineBreaks = lineBreaks[0 .. brokenLineCount]; 5558 5559 foreach_reverse(lineBreak; lineBreaks) { 5560 if(remaining == 1) { 5561 firstPartial = true; 5562 firstPartialStartIndex = lineBreak; 5563 break; 5564 } else { 5565 remaining--; 5566 } 5567 if(remaining <= 0) 5568 break; 5569 } 5570 5571 remaining--; 5572 5573 start--; 5574 howMany++; 5575 if(remaining <= 0) 5576 break; 5577 } 5578 5579 // second pass: actually draw it 5580 int linePos = remaining; 5581 5582 foreach(line; lines[start .. start + howMany]) { 5583 int written = 0; 5584 5585 if(linePos < 0) { 5586 linePos++; 5587 continue; 5588 } 5589 5590 terminal.moveTo(x, y + ((linePos >= 0) ? linePos : 0)); 5591 5592 auto todo = line.components; 5593 5594 if(firstPartial) { 5595 todo = todo[firstPartialStartIndex.cidx .. $]; 5596 } 5597 5598 foreach(ref component; todo) { 5599 if(component.isRgb) 5600 terminal.setTrueColor(component.colorRgb, component.backgroundRgb); 5601 else 5602 terminal.color(component.color, component.background); 5603 auto towrite = component.text; 5604 5605 again: 5606 5607 if(linePos >= height) 5608 break; 5609 5610 if(firstPartial) { 5611 towrite = towrite[firstPartialStartIndex.idx .. $]; 5612 firstPartial = false; 5613 } 5614 5615 foreach(idx, dchar ch; towrite) { 5616 if(written >= width) { 5617 clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 5618 terminal.write(towrite[0 .. idx]); 5619 towrite = towrite[idx .. $]; 5620 linePos++; 5621 written = 0; 5622 terminal.moveTo(x, y + linePos); 5623 goto again; 5624 } 5625 5626 if(ch == '\t') 5627 written += 8; // FIXME 5628 else 5629 written++; 5630 } 5631 5632 if(towrite.length) { 5633 clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 5634 terminal.write(towrite); 5635 } 5636 } 5637 5638 if(written < width) { 5639 terminal.color(Color.DEFAULT, Color.DEFAULT); 5640 foreach(i; written .. width) 5641 terminal.write(" "); 5642 } 5643 5644 linePos++; 5645 5646 if(linePos >= height) 5647 break; 5648 } 5649 5650 if(linePos < height) { 5651 terminal.color(Color.DEFAULT, Color.DEFAULT); 5652 foreach(i; linePos .. height) { 5653 if(i >= 0 && i < height) { 5654 terminal.moveTo(x, y + i); 5655 foreach(w; 0 .. width) 5656 terminal.write(" "); 5657 } 5658 } 5659 } 5660 } 5661 5662 private struct ClickRegion { 5663 LineComponent* component; 5664 int xStart; 5665 int yStart; 5666 int length; 5667 } 5668 private ClickRegion[] clickRegions; 5669 5670 /// Default event handling for this widget. Call this only after drawing it into a rectangle 5671 /// and only if the event ought to be dispatched to it (which you determine however you want; 5672 /// you could dispatch all events to it, or perhaps filter some out too) 5673 /// 5674 /// Returns true if it should be redrawn 5675 bool handleEvent(InputEvent e) { 5676 final switch(e.type) { 5677 case InputEvent.Type.LinkEvent: 5678 // meh 5679 break; 5680 case InputEvent.Type.KeyboardEvent: 5681 auto ev = e.keyboardEvent; 5682 5683 demandsAttention = false; 5684 5685 switch(ev.which) { 5686 case KeyboardEvent.Key.UpArrow: 5687 scrollUp(); 5688 return true; 5689 case KeyboardEvent.Key.DownArrow: 5690 scrollDown(); 5691 return true; 5692 case KeyboardEvent.Key.PageUp: 5693 scrollUp(height); 5694 return true; 5695 case KeyboardEvent.Key.PageDown: 5696 scrollDown(height); 5697 return true; 5698 default: 5699 // ignore 5700 } 5701 break; 5702 case InputEvent.Type.MouseEvent: 5703 auto ev = e.mouseEvent; 5704 if(ev.x >= x && ev.x < x + width && ev.y >= y && ev.y < y + height) { 5705 demandsAttention = false; 5706 // it is inside our box, so do something with it 5707 auto mx = ev.x - x; 5708 auto my = ev.y - y; 5709 5710 if(ev.eventType == MouseEvent.Type.Pressed) { 5711 if(ev.buttons & MouseEvent.Button.Left) { 5712 foreach(region; clickRegions) 5713 if(ev.x >= region.xStart && ev.x < region.xStart + region.length && ev.y == region.yStart) 5714 if(region.component.onclick !is null) 5715 return region.component.onclick(); 5716 } 5717 if(ev.buttons & MouseEvent.Button.ScrollUp) { 5718 scrollUp(); 5719 return true; 5720 } 5721 if(ev.buttons & MouseEvent.Button.ScrollDown) { 5722 scrollDown(); 5723 return true; 5724 } 5725 } 5726 } else { 5727 // outside our area, free to ignore 5728 } 5729 break; 5730 case InputEvent.Type.SizeChangedEvent: 5731 // (size changed might be but it needs to be handled at a higher level really anyway) 5732 // though it will return true because it probably needs redrawing anyway. 5733 return true; 5734 case InputEvent.Type.UserInterruptionEvent: 5735 throw new UserInterruptionException(); 5736 case InputEvent.Type.HangupEvent: 5737 throw new HangupException(); 5738 case InputEvent.Type.EndOfFileEvent: 5739 // ignore, not relevant to this 5740 break; 5741 case InputEvent.Type.CharacterEvent: 5742 case InputEvent.Type.NonCharacterKeyEvent: 5743 // obsolete, ignore them until they are removed 5744 break; 5745 case InputEvent.Type.CustomEvent: 5746 case InputEvent.Type.PasteEvent: 5747 // ignored, not relevant to us 5748 break; 5749 } 5750 5751 return false; 5752 } 5753 } 5754 5755 5756 class UserInterruptionException : Exception { 5757 this() { super("Ctrl+C"); } 5758 } 5759 class HangupException : Exception { 5760 this() { super("Hup"); } 5761 } 5762 5763 5764 5765 /* 5766 5767 // more efficient scrolling 5768 http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx 5769 // and the unix sequences 5770 5771 5772 rxvt documentation: 5773 use this to finish the input magic for that 5774 5775 5776 For the keypad, use Shift to temporarily override Application-Keypad 5777 setting use Num_Lock to toggle Application-Keypad setting if Num_Lock 5778 is off, toggle Application-Keypad setting. Also note that values of 5779 Home, End, Delete may have been compiled differently on your system. 5780 5781 Normal Shift Control Ctrl+Shift 5782 Tab ^I ESC [ Z ^I ESC [ Z 5783 BackSpace ^H ^? ^? ^? 5784 Find ESC [ 1 ~ ESC [ 1 $ ESC [ 1 ^ ESC [ 1 @ 5785 Insert ESC [ 2 ~ paste ESC [ 2 ^ ESC [ 2 @ 5786 Execute ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 5787 Select ESC [ 4 ~ ESC [ 4 $ ESC [ 4 ^ ESC [ 4 @ 5788 Prior ESC [ 5 ~ scroll-up ESC [ 5 ^ ESC [ 5 @ 5789 Next ESC [ 6 ~ scroll-down ESC [ 6 ^ ESC [ 6 @ 5790 Home ESC [ 7 ~ ESC [ 7 $ ESC [ 7 ^ ESC [ 7 @ 5791 End ESC [ 8 ~ ESC [ 8 $ ESC [ 8 ^ ESC [ 8 @ 5792 Delete ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 5793 F1 ESC [ 11 ~ ESC [ 23 ~ ESC [ 11 ^ ESC [ 23 ^ 5794 F2 ESC [ 12 ~ ESC [ 24 ~ ESC [ 12 ^ ESC [ 24 ^ 5795 F3 ESC [ 13 ~ ESC [ 25 ~ ESC [ 13 ^ ESC [ 25 ^ 5796 F4 ESC [ 14 ~ ESC [ 26 ~ ESC [ 14 ^ ESC [ 26 ^ 5797 F5 ESC [ 15 ~ ESC [ 28 ~ ESC [ 15 ^ ESC [ 28 ^ 5798 F6 ESC [ 17 ~ ESC [ 29 ~ ESC [ 17 ^ ESC [ 29 ^ 5799 F7 ESC [ 18 ~ ESC [ 31 ~ ESC [ 18 ^ ESC [ 31 ^ 5800 F8 ESC [ 19 ~ ESC [ 32 ~ ESC [ 19 ^ ESC [ 32 ^ 5801 F9 ESC [ 20 ~ ESC [ 33 ~ ESC [ 20 ^ ESC [ 33 ^ 5802 F10 ESC [ 21 ~ ESC [ 34 ~ ESC [ 21 ^ ESC [ 34 ^ 5803 F11 ESC [ 23 ~ ESC [ 23 $ ESC [ 23 ^ ESC [ 23 @ 5804 F12 ESC [ 24 ~ ESC [ 24 $ ESC [ 24 ^ ESC [ 24 @ 5805 F13 ESC [ 25 ~ ESC [ 25 $ ESC [ 25 ^ ESC [ 25 @ 5806 F14 ESC [ 26 ~ ESC [ 26 $ ESC [ 26 ^ ESC [ 26 @ 5807 F15 (Help) ESC [ 28 ~ ESC [ 28 $ ESC [ 28 ^ ESC [ 28 @ 5808 F16 (Menu) ESC [ 29 ~ ESC [ 29 $ ESC [ 29 ^ ESC [ 29 @ 5809 5810 F17 ESC [ 31 ~ ESC [ 31 $ ESC [ 31 ^ ESC [ 31 @ 5811 F18 ESC [ 32 ~ ESC [ 32 $ ESC [ 32 ^ ESC [ 32 @ 5812 F19 ESC [ 33 ~ ESC [ 33 $ ESC [ 33 ^ ESC [ 33 @ 5813 F20 ESC [ 34 ~ ESC [ 34 $ ESC [ 34 ^ ESC [ 34 @ 5814 Application 5815 Up ESC [ A ESC [ a ESC O a ESC O A 5816 Down ESC [ B ESC [ b ESC O b ESC O B 5817 Right ESC [ C ESC [ c ESC O c ESC O C 5818 Left ESC [ D ESC [ d ESC O d ESC O D 5819 KP_Enter ^M ESC O M 5820 KP_F1 ESC O P ESC O P 5821 KP_F2 ESC O Q ESC O Q 5822 KP_F3 ESC O R ESC O R 5823 KP_F4 ESC O S ESC O S 5824 XK_KP_Multiply * ESC O j 5825 XK_KP_Add + ESC O k 5826 XK_KP_Separator , ESC O l 5827 XK_KP_Subtract - ESC O m 5828 XK_KP_Decimal . ESC O n 5829 XK_KP_Divide / ESC O o 5830 XK_KP_0 0 ESC O p 5831 XK_KP_1 1 ESC O q 5832 XK_KP_2 2 ESC O r 5833 XK_KP_3 3 ESC O s 5834 XK_KP_4 4 ESC O t 5835 XK_KP_5 5 ESC O u 5836 XK_KP_6 6 ESC O v 5837 XK_KP_7 7 ESC O w 5838 XK_KP_8 8 ESC O x 5839 XK_KP_9 9 ESC O y 5840 */ 5841 5842 version(Demo_kbhit) 5843 void main() { 5844 auto terminal = Terminal(ConsoleOutputType.linear); 5845 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw); 5846 5847 int a; 5848 char ch = '.'; 5849 while(a < 1000) { 5850 a++; 5851 if(a % terminal.width == 0) { 5852 terminal.write("\r"); 5853 if(ch == '.') 5854 ch = ' '; 5855 else 5856 ch = '.'; 5857 } 5858 5859 if(input.kbhit()) 5860 terminal.write(input.getch()); 5861 else 5862 terminal.write(ch); 5863 5864 terminal.flush(); 5865 5866 import core.thread; 5867 Thread.sleep(50.msecs); 5868 } 5869 } 5870 5871 /* 5872 The Xterm palette progression is: 5873 [0, 95, 135, 175, 215, 255] 5874 5875 So if I take the color and subtract 55, then div 40, I get 5876 it into one of these areas. If I add 20, I get a reasonable 5877 rounding. 5878 */ 5879 5880 ubyte colorToXTermPaletteIndex(RGB color) { 5881 /* 5882 Here, I will round off to the color ramp or the 5883 greyscale. I will NOT use the bottom 16 colors because 5884 there's duplicates (or very close enough) to them in here 5885 */ 5886 5887 if(color.r == color.g && color.g == color.b) { 5888 // grey - find one of them: 5889 if(color.r == 0) return 0; 5890 // meh don't need those two, let's simplify branche 5891 //if(color.r == 0xc0) return 7; 5892 //if(color.r == 0x80) return 8; 5893 // it isn't == 255 because it wants to catch anything 5894 // that would wrap the simple algorithm below back to 0. 5895 if(color.r >= 248) return 15; 5896 5897 // there's greys in the color ramp too, but these 5898 // are all close enough as-is, no need to complicate 5899 // algorithm for approximation anyway 5900 5901 return cast(ubyte) (232 + ((color.r - 8) / 10)); 5902 } 5903 5904 // if it isn't grey, it is color 5905 5906 // the ramp goes blue, green, red, with 6 of each, 5907 // so just multiplying will give something good enough 5908 5909 // will give something between 0 and 5, with some rounding 5910 auto r = (cast(int) color.r - 35) / 40; 5911 auto g = (cast(int) color.g - 35) / 40; 5912 auto b = (cast(int) color.b - 35) / 40; 5913 5914 return cast(ubyte) (16 + b + g*6 + r*36); 5915 } 5916 5917 /++ 5918 Represents a 24-bit color. 5919 5920 5921 $(TIP You can convert these to and from [arsd.color.Color] using 5922 `.tupleof`: 5923 5924 --- 5925 RGB rgb; 5926 Color c = Color(rgb.tupleof); 5927 --- 5928 ) 5929 +/ 5930 struct RGB { 5931 ubyte r; /// 5932 ubyte g; /// 5933 ubyte b; /// 5934 // terminal can't actually use this but I want the value 5935 // there for assignment to an arsd.color.Color 5936 private ubyte a = 255; 5937 } 5938 5939 // This is an approximation too for a few entries, but a very close one. 5940 RGB xtermPaletteIndexToColor(int paletteIdx) { 5941 RGB color; 5942 5943 if(paletteIdx < 16) { 5944 if(paletteIdx == 7) 5945 return RGB(0xc0, 0xc0, 0xc0); 5946 else if(paletteIdx == 8) 5947 return RGB(0x80, 0x80, 0x80); 5948 5949 color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 5950 color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 5951 color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 5952 5953 } else if(paletteIdx < 232) { 5954 // color ramp, 6x6x6 cube 5955 color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); 5956 color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); 5957 color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); 5958 5959 if(color.r == 55) color.r = 0; 5960 if(color.g == 55) color.g = 0; 5961 if(color.b == 55) color.b = 0; 5962 } else { 5963 // greyscale ramp, from 0x8 to 0xee 5964 color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); 5965 color.g = color.r; 5966 color.b = color.g; 5967 } 5968 5969 return color; 5970 } 5971 5972 int approximate16Color(RGB color) { 5973 int c; 5974 c |= color.r > 64 ? RED_BIT : 0; 5975 c |= color.g > 64 ? GREEN_BIT : 0; 5976 c |= color.b > 64 ? BLUE_BIT : 0; 5977 5978 c |= (((color.r + color.g + color.b) / 3) > 80) ? Bright : 0; 5979 5980 return c; 5981 } 5982 5983 version(TerminalDirectToEmulator) { 5984 5985 /++ 5986 Indicates the TerminalDirectToEmulator features 5987 are present. You can check this with `static if`. 5988 5989 $(WARNING 5990 This will cause the [Terminal] constructor to spawn a GUI thread with [arsd.minigui]/[arsd.simpledisplay]. 5991 5992 This means you can NOT use those libraries in your 5993 own thing without using the [arsd.simpledisplay.runInGuiThread] helper since otherwise the main thread is inaccessible, since having two different threads creating event loops or windows is undefined behavior with those libraries. 5994 ) 5995 +/ 5996 enum IntegratedEmulator = true; 5997 5998 /++ 5999 Allows customization of the integrated emulator window. 6000 You may change the default colors, font, and other aspects 6001 of GUI integration. 6002 6003 Test for its presence before using with `static if(arsd.terminal.IntegratedEmulator)`. 6004 6005 All settings here must be set BEFORE you construct any [Terminal] instances. 6006 6007 History: 6008 Added March 7, 2020. 6009 +/ 6010 struct IntegratedTerminalEmulatorConfiguration { 6011 /// Note that all Colors in here are 24 bit colors. 6012 alias Color = arsd.color.Color; 6013 6014 /// Default foreground color of the terminal. 6015 Color defaultForeground = Color.black; 6016 /// Default background color of the terminal. 6017 Color defaultBackground = Color.white; 6018 6019 /++ 6020 Font to use in the window. It should be a monospace font, 6021 and your selection may not actually be used if not available on 6022 the user's system, in which case it will fallback to one. 6023 6024 History: 6025 Implemented March 26, 2020 6026 +/ 6027 string fontName; 6028 /// ditto 6029 int fontSize = 14; 6030 6031 /++ 6032 Requested initial terminal size in character cells. You may not actually get exactly this. 6033 +/ 6034 int initialWidth = 80; 6035 /// ditto 6036 int initialHeight = 40; 6037 6038 /++ 6039 If `true`, the window will close automatically when the main thread exits. 6040 Otherwise, the window will remain open so the user can work with output before 6041 it disappears. 6042 6043 History: 6044 Added April 10, 2020 (v7.2.0) 6045 +/ 6046 bool closeOnExit = false; 6047 6048 /++ 6049 Gives you a chance to modify the window as it is constructed. Intended 6050 to let you add custom menu options. 6051 6052 --- 6053 import arsd.terminal; 6054 integratedTerminalEmulatorConfiguration.menuExtensionsConstructor = (TerminalEmulatorWindow window) { 6055 import arsd.minigui; // for the menu related UDAs 6056 class Commands { 6057 @menu("Help") { 6058 void Topics() { 6059 auto window = new Window(); // make a help window of some sort 6060 window.show(); 6061 } 6062 6063 @separator 6064 6065 void About() { 6066 messageBox("My Application v 1.0"); 6067 } 6068 } 6069 } 6070 window.setMenuAndToolbarFromAnnotatedCode(new Commands()); 6071 }; 6072 --- 6073 6074 History: 6075 Added March 29, 2020. Included in release v7.1.0. 6076 +/ 6077 void delegate(TerminalEmulatorWindow) menuExtensionsConstructor; 6078 6079 /++ 6080 Set this to true if you want [Terminal] to fallback to the user's 6081 existing native terminal in the event that creating the custom terminal 6082 is impossible for whatever reason. 6083 6084 If your application must have all advanced features, set this to `false`. 6085 Otherwise, be sure you handle the absence of advanced features in your 6086 application by checking methods like [Terminal.inlineImagesSupported], 6087 etc., and only use things you can gracefully degrade without. 6088 6089 If this is set to false, `Terminal`'s constructor will throw if the gui fails 6090 instead of carrying on with the stdout terminal (if possible). 6091 6092 History: 6093 Added June 28, 2020. Included in release v8.1.0. 6094 6095 +/ 6096 bool fallbackToDegradedTerminal = true; 6097 } 6098 6099 /+ 6100 status bar should probably tell 6101 if scroll lock is on... 6102 +/ 6103 6104 /// You can set this in a static module constructor. (`shared static this() {}`) 6105 __gshared IntegratedTerminalEmulatorConfiguration integratedTerminalEmulatorConfiguration; 6106 6107 import arsd.terminalemulator; 6108 import arsd.minigui; 6109 6110 /++ 6111 Represents the window that the library pops up for you. 6112 +/ 6113 final class TerminalEmulatorWindow : MainWindow { 6114 6115 /++ 6116 Gives access to the underlying terminal emulation object. 6117 +/ 6118 TerminalEmulator terminalEmulator() { 6119 return tew.terminalEmulator; 6120 } 6121 6122 private TerminalEmulatorWindow parent; 6123 private TerminalEmulatorWindow[] children; 6124 private void childClosing(TerminalEmulatorWindow t) { 6125 foreach(idx, c; children) 6126 if(c is t) 6127 children = children[0 .. idx] ~ children[idx + 1 .. $]; 6128 } 6129 private void registerChild(TerminalEmulatorWindow t) { 6130 children ~= t; 6131 } 6132 6133 private this(Terminal* term, TerminalEmulatorWindow parent) { 6134 6135 this.parent = parent; 6136 scope(success) if(parent) parent.registerChild(this); 6137 6138 super("Terminal Application", integratedTerminalEmulatorConfiguration.initialWidth * integratedTerminalEmulatorConfiguration.fontSize / 2, integratedTerminalEmulatorConfiguration.initialHeight * integratedTerminalEmulatorConfiguration.fontSize); 6139 6140 smw = new ScrollMessageWidget(this); 6141 tew = new TerminalEmulatorWidget(term, smw); 6142 6143 smw.addEventListener("scroll", () { 6144 tew.terminalEmulator.scrollbackTo(smw.position.x, smw.position.y + tew.terminalEmulator.height); 6145 redraw(); 6146 }); 6147 6148 smw.setTotalArea(1, 1); 6149 6150 setMenuAndToolbarFromAnnotatedCode(this); 6151 if(integratedTerminalEmulatorConfiguration.menuExtensionsConstructor) 6152 integratedTerminalEmulatorConfiguration.menuExtensionsConstructor(this); 6153 } 6154 6155 TerminalEmulator.TerminalCell[] delegate(TerminalEmulator.TerminalCell[] i) parentFilter; 6156 6157 private void addScrollbackLineFromParent(TerminalEmulator.TerminalCell[] lineIn) { 6158 if(parentFilter is null) 6159 return; 6160 6161 auto line = parentFilter(lineIn); 6162 if(line is null) return; 6163 6164 if(tew && tew.terminalEmulator) { 6165 bool atBottom = smw.verticalScrollBar.atEnd && smw.horizontalScrollBar.atStart; 6166 tew.terminalEmulator.addScrollbackLine(line); 6167 tew.terminalEmulator.notifyScrollbackAdded(); 6168 if(atBottom) { 6169 tew.terminalEmulator.notifyScrollbarPosition(0, int.max); 6170 tew.terminalEmulator.scrollbackTo(0, int.max); 6171 tew.redraw(); 6172 } 6173 } 6174 } 6175 6176 private TerminalEmulatorWidget tew; 6177 private ScrollMessageWidget smw; 6178 6179 @menu("&History") { 6180 @tip("Saves the currently visible content to a file") 6181 void Save() { 6182 getSaveFileName((string name) { 6183 tew.terminalEmulator.writeScrollbackToFile(name); 6184 }); 6185 } 6186 6187 // FIXME 6188 version(FIXME) 6189 void Save_HTML() { 6190 6191 } 6192 6193 @separator 6194 /* 6195 void Find() { 6196 // FIXME 6197 // jump to the previous instance in the scrollback 6198 6199 } 6200 */ 6201 6202 void Filter() { 6203 // open a new window that just shows items that pass the filter 6204 6205 static struct FilterParams { 6206 string searchTerm; 6207 bool caseSensitive; 6208 } 6209 6210 dialog((FilterParams p) { 6211 auto nw = new TerminalEmulatorWindow(null, this); 6212 6213 nw.parentFilter = (TerminalEmulator.TerminalCell[] line) { 6214 import std.algorithm; 6215 import std.uni; 6216 // omg autodecoding being kinda useful for once LOL 6217 if(line.map!(c => c.hasNonCharacterData ? dchar(0) : (p.caseSensitive ? c.ch : c.ch.toLower)). 6218 canFind(p.searchTerm)) 6219 { 6220 // I might highlight the match too, but meh for now 6221 return line; 6222 } 6223 return null; 6224 }; 6225 6226 foreach(line; tew.terminalEmulator.sbb[0 .. $]) { 6227 if(auto l = nw.parentFilter(line)) 6228 nw.tew.terminalEmulator.addScrollbackLine(l); 6229 } 6230 nw.tew.terminalEmulator.toggleScrollLock(); 6231 nw.tew.terminalEmulator.drawScrollback(); 6232 nw.title = "Filter Display"; 6233 nw.show(); 6234 }); 6235 6236 } 6237 6238 @separator 6239 void Clear() { 6240 tew.terminalEmulator.clearScrollbackHistory(); 6241 tew.terminalEmulator.cls(); 6242 tew.terminalEmulator.moveCursor(0, 0); 6243 if(tew.term) { 6244 tew.term.windowSizeChanged = true; 6245 tew.terminalEmulator.outgoingSignal.notify(); 6246 } 6247 tew.redraw(); 6248 } 6249 6250 @separator 6251 void Exit() @accelerator("Alt+F4") @hotkey('x') { 6252 this.close(); 6253 } 6254 } 6255 6256 @menu("&Edit") { 6257 void Copy() { 6258 tew.terminalEmulator.copyToClipboard(tew.terminalEmulator.getSelectedText()); 6259 } 6260 6261 void Paste() { 6262 tew.terminalEmulator.pasteFromClipboard(&tew.terminalEmulator.sendPasteData); 6263 } 6264 } 6265 } 6266 6267 private class InputEventInternal { 6268 const(ubyte)[] data; 6269 this(in ubyte[] data) { 6270 this.data = data; 6271 } 6272 } 6273 6274 private class TerminalEmulatorWidget : Widget { 6275 6276 Menu ctx; 6277 6278 override Menu contextMenu(int x, int y) { 6279 if(ctx is null) { 6280 ctx = new Menu(""); 6281 ctx.addItem(new MenuItem(new Action("Copy", 0, { 6282 terminalEmulator.copyToClipboard(terminalEmulator.getSelectedText()); 6283 }))); 6284 ctx.addItem(new MenuItem(new Action("Paste", 0, { 6285 terminalEmulator.pasteFromClipboard(&terminalEmulator.sendPasteData); 6286 }))); 6287 ctx.addItem(new MenuItem(new Action("Toggle Scroll Lock", 0, { 6288 terminalEmulator.toggleScrollLock(); 6289 }))); 6290 } 6291 return ctx; 6292 } 6293 6294 this(Terminal* term, ScrollMessageWidget parent) { 6295 this.smw = parent; 6296 this.term = term; 6297 terminalEmulator = new TerminalEmulatorInsideWidget(this); 6298 super(parent); 6299 this.parentWindow.win.onClosing = { 6300 if(term) 6301 term.hangedUp = true; 6302 6303 if(auto wi = cast(TerminalEmulatorWindow) this.parentWindow) { 6304 if(wi.parent) 6305 wi.parent.childClosing(wi); 6306 } 6307 6308 // try to get it to terminate slightly more forcibly too, if possible 6309 if(sigIntExtension) 6310 sigIntExtension(); 6311 6312 terminalEmulator.outgoingSignal.notify(); 6313 terminalEmulator.incomingSignal.notify(); 6314 }; 6315 6316 this.parentWindow.win.addEventListener((InputEventInternal ie) { 6317 terminalEmulator.sendRawInput(ie.data); 6318 this.redraw(); 6319 terminalEmulator.incomingSignal.notify(); 6320 }); 6321 } 6322 6323 ScrollMessageWidget smw; 6324 Terminal* term; 6325 6326 void sendRawInput(const(ubyte)[] data) { 6327 if(this.parentWindow) { 6328 this.parentWindow.win.postEvent(new InputEventInternal(data)); 6329 terminalEmulator.incomingSignal.wait(); // blocking write basically, wait until the TE confirms the receipt of it 6330 } 6331 } 6332 6333 TerminalEmulatorInsideWidget terminalEmulator; 6334 6335 override void registerMovement() { 6336 super.registerMovement(); 6337 terminalEmulator.resized(width, height); 6338 } 6339 6340 override void focus() { 6341 super.focus(); 6342 terminalEmulator.attentionReceived(); 6343 } 6344 6345 override MouseCursor cursor() { return GenericCursor.Text; } 6346 6347 override void erase(WidgetPainter painter) { /* intentionally blank, paint does it better */ } 6348 6349 override void paint(WidgetPainter painter) { 6350 bool forceRedraw = false; 6351 if(terminalEmulator.invalidateAll || terminalEmulator.clearScreenRequested) { 6352 auto clearColor = terminalEmulator.defaultBackground; 6353 painter.outlineColor = clearColor; 6354 painter.fillColor = clearColor; 6355 painter.drawRectangle(Point(0, 0), this.width, this.height); 6356 terminalEmulator.clearScreenRequested = false; 6357 forceRedraw = true; 6358 } 6359 6360 terminalEmulator.redrawPainter(painter, forceRedraw); 6361 } 6362 } 6363 6364 private class TerminalEmulatorInsideWidget : TerminalEmulator { 6365 6366 private ScrollbackBuffer sbb() { return scrollbackBuffer; } 6367 6368 void resized(int w, int h) { 6369 this.resizeTerminal(w / fontWidth, h / fontHeight); 6370 if(widget && widget.smw) { 6371 widget.smw.setViewableArea(this.width, this.height); 6372 widget.smw.setPageSize(this.width / 2, this.height / 2); 6373 } 6374 clearScreenRequested = true; 6375 if(widget && widget.term) 6376 widget.term.windowSizeChanged = true; 6377 outgoingSignal.notify(); 6378 redraw(); 6379 } 6380 6381 override void addScrollbackLine(TerminalCell[] line) { 6382 super.addScrollbackLine(line); 6383 if(widget) 6384 if(auto p = cast(TerminalEmulatorWindow) widget.parentWindow) { 6385 foreach(child; p.children) 6386 child.addScrollbackLineFromParent(line); 6387 } 6388 } 6389 6390 override void notifyScrollbackAdded() { 6391 widget.smw.setTotalArea(this.scrollbackWidth > this.width ? this.scrollbackWidth : this.width, this.scrollbackLength > this.height ? this.scrollbackLength : this.height); 6392 } 6393 6394 override void notifyScrollbarPosition(int x, int y) { 6395 widget.smw.setPosition(x, y); 6396 widget.redraw(); 6397 } 6398 6399 override void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) { 6400 if(isRelevantVertically) 6401 notifyScrollbackAdded(); 6402 else 6403 widget.smw.setTotalArea(width, height); 6404 } 6405 6406 override @property public int cursorX() { return super.cursorX; } 6407 override @property public int cursorY() { return super.cursorY; } 6408 6409 protected override void changeCursorStyle(CursorStyle s) { } 6410 6411 string currentTitle; 6412 protected override void changeWindowTitle(string t) { 6413 if(widget && widget.parentWindow && t.length) { 6414 widget.parentWindow.win.title = t; 6415 currentTitle = t; 6416 } 6417 } 6418 protected override void changeWindowIcon(IndexedImage t) { 6419 if(widget && widget.parentWindow && t) 6420 widget.parentWindow.win.icon = t; 6421 } 6422 6423 protected override void changeIconTitle(string) {} 6424 protected override void changeTextAttributes(TextAttributes) {} 6425 protected override void soundBell() { 6426 static if(UsingSimpledisplayX11) 6427 XBell(XDisplayConnection.get(), 50); 6428 } 6429 6430 protected override void demandAttention() { 6431 if(widget && widget.parentWindow) 6432 widget.parentWindow.win.requestAttention(); 6433 } 6434 6435 protected override void copyToClipboard(string text) { 6436 setClipboardText(widget.parentWindow.win, text); 6437 } 6438 6439 override int maxScrollbackLength() const { 6440 return int.max; // no scrollback limit for custom programs 6441 } 6442 6443 protected override void pasteFromClipboard(void delegate(in char[]) dg) { 6444 static if(UsingSimpledisplayX11) 6445 getPrimarySelection(widget.parentWindow.win, dg); 6446 else 6447 getClipboardText(widget.parentWindow.win, (in char[] dataIn) { 6448 char[] data; 6449 // change Windows \r\n to plain \n 6450 foreach(char ch; dataIn) 6451 if(ch != 13) 6452 data ~= ch; 6453 dg(data); 6454 }); 6455 } 6456 6457 protected override void copyToPrimary(string text) { 6458 static if(UsingSimpledisplayX11) 6459 setPrimarySelection(widget.parentWindow.win, text); 6460 else 6461 {} 6462 } 6463 protected override void pasteFromPrimary(void delegate(in char[]) dg) { 6464 static if(UsingSimpledisplayX11) 6465 getPrimarySelection(widget.parentWindow.win, dg); 6466 } 6467 6468 override void requestExit() { 6469 widget.parentWindow.close(); 6470 } 6471 6472 bool echo = false; 6473 6474 override void sendRawInput(in ubyte[] data) { 6475 void send(in ubyte[] data) { 6476 if(data.length == 0) 6477 return; 6478 super.sendRawInput(data); 6479 if(echo) 6480 sendToApplication(data); 6481 } 6482 6483 // need to echo, translate 10 to 13/10 cr-lf 6484 size_t last = 0; 6485 const ubyte[2] crlf = [13, 10]; 6486 foreach(idx, ch; data) { 6487 if(ch == 10) { 6488 send(data[last .. idx]); 6489 send(crlf[]); 6490 last = idx + 1; 6491 } 6492 } 6493 6494 if(last < data.length) 6495 send(data[last .. $]); 6496 } 6497 6498 bool focused; 6499 6500 TerminalEmulatorWidget widget; 6501 6502 import arsd.simpledisplay; 6503 import arsd.color; 6504 import core.sync.semaphore; 6505 alias ModifierState = arsd.simpledisplay.ModifierState; 6506 alias Color = arsd.color.Color; 6507 alias fromHsl = arsd.color.fromHsl; 6508 6509 const(ubyte)[] pendingForApplication; 6510 Semaphore outgoingSignal; 6511 Semaphore incomingSignal; 6512 6513 override void sendToApplication(scope const(void)[] what) { 6514 synchronized(this) { 6515 pendingForApplication ~= cast(const(ubyte)[]) what; 6516 } 6517 outgoingSignal.notify(); 6518 } 6519 6520 @property int width() { return screenWidth; } 6521 @property int height() { return screenHeight; } 6522 6523 @property bool invalidateAll() { return super.invalidateAll; } 6524 6525 private this(TerminalEmulatorWidget widget) { 6526 6527 this.outgoingSignal = new Semaphore(); 6528 this.incomingSignal = new Semaphore(); 6529 6530 this.widget = widget; 6531 6532 if(integratedTerminalEmulatorConfiguration.fontName.length) { 6533 this.font = new OperatingSystemFont(integratedTerminalEmulatorConfiguration.fontName, integratedTerminalEmulatorConfiguration.fontSize, FontWeight.medium); 6534 this.fontWidth = font.averageWidth; 6535 this.fontHeight = font.height; 6536 } 6537 6538 6539 if(this.font is null || this.font.isNull) 6540 loadDefaultFont(integratedTerminalEmulatorConfiguration.fontSize); 6541 6542 super(integratedTerminalEmulatorConfiguration.initialWidth, integratedTerminalEmulatorConfiguration.initialHeight); 6543 6544 defaultForeground = integratedTerminalEmulatorConfiguration.defaultForeground; 6545 defaultBackground = integratedTerminalEmulatorConfiguration.defaultBackground; 6546 6547 bool skipNextChar = false; 6548 6549 widget.addEventListener("mousedown", (Event ev) { 6550 int termX = (ev.clientX - paddingLeft) / fontWidth; 6551 int termY = (ev.clientY - paddingTop) / fontHeight; 6552 6553 if((!mouseButtonTracking || (ev.state & ModifierState.shift)) && ev.button == MouseButton.right) 6554 widget.showContextMenu(ev.clientX, ev.clientY); 6555 else 6556 if(sendMouseInputToApplication(termX, termY, 6557 arsd.terminalemulator.MouseEventType.buttonPressed, 6558 cast(arsd.terminalemulator.MouseButton) ev.button, 6559 (ev.state & ModifierState.shift) ? true : false, 6560 (ev.state & ModifierState.ctrl) ? true : false, 6561 (ev.state & ModifierState.alt) ? true : false 6562 )) 6563 redraw(); 6564 }); 6565 6566 widget.addEventListener("mouseup", (Event ev) { 6567 int termX = (ev.clientX - paddingLeft) / fontWidth; 6568 int termY = (ev.clientY - paddingTop) / fontHeight; 6569 6570 if(sendMouseInputToApplication(termX, termY, 6571 arsd.terminalemulator.MouseEventType.buttonReleased, 6572 cast(arsd.terminalemulator.MouseButton) ev.button, 6573 (ev.state & ModifierState.shift) ? true : false, 6574 (ev.state & ModifierState.ctrl) ? true : false, 6575 (ev.state & ModifierState.alt) ? true : false 6576 )) 6577 redraw(); 6578 }); 6579 6580 widget.addEventListener("mousemove", (Event ev) { 6581 int termX = (ev.clientX - paddingLeft) / fontWidth; 6582 int termY = (ev.clientY - paddingTop) / fontHeight; 6583 6584 if(sendMouseInputToApplication(termX, termY, 6585 arsd.terminalemulator.MouseEventType.motion, 6586 cast(arsd.terminalemulator.MouseButton) ev.button, 6587 (ev.state & ModifierState.shift) ? true : false, 6588 (ev.state & ModifierState.ctrl) ? true : false, 6589 (ev.state & ModifierState.alt) ? true : false 6590 )) 6591 redraw(); 6592 }); 6593 6594 widget.addEventListener("keydown", (Event ev) { 6595 static string magic() { 6596 string code; 6597 foreach(member; __traits(allMembers, TerminalKey)) 6598 if(member != "Escape") 6599 code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ " 6600 , (ev.state & ModifierState.shift)?true:false 6601 , (ev.state & ModifierState.alt)?true:false 6602 , (ev.state & ModifierState.ctrl)?true:false 6603 , (ev.state & ModifierState.windows)?true:false 6604 )) redraw(); break;"; 6605 return code; 6606 } 6607 6608 6609 switch(ev.key) { 6610 mixin(magic()); 6611 default: 6612 // keep going, not special 6613 } 6614 6615 return; // the character event handler will do others 6616 }); 6617 6618 widget.addEventListener("char", (Event ev) { 6619 dchar c = ev.character; 6620 if(skipNextChar) { 6621 skipNextChar = false; 6622 return; 6623 } 6624 6625 endScrollback(); 6626 char[4] str; 6627 import std.utf; 6628 if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10 6629 auto data = str[0 .. encode(str, c)]; 6630 6631 6632 if(c == 0x1c) /* ctrl+\, force quit */ { 6633 version(Posix) { 6634 import core.sys.posix.signal; 6635 pthread_kill(widget.term.threadId, SIGQUIT); // or SIGKILL even? 6636 6637 assert(0); 6638 //import core.sys.posix.pthread; 6639 //pthread_cancel(widget.term.threadId); 6640 //widget.term = null; 6641 } else version(Windows) { 6642 import core.sys.windows.windows; 6643 auto hnd = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, TRUE, GetCurrentProcessId()); 6644 TerminateProcess(hnd, -1); 6645 assert(0); 6646 } 6647 } else if(c == 3) /* ctrl+c, interrupt */ { 6648 if(sigIntExtension) 6649 sigIntExtension(); 6650 6651 if(widget && widget.term) { 6652 widget.term.interrupted = true; 6653 outgoingSignal.notify(); 6654 } 6655 } else if(c != 127) { 6656 // on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler. 6657 sendToApplication(data); 6658 } 6659 }); 6660 } 6661 6662 bool clearScreenRequested = true; 6663 void redraw() { 6664 if(widget.parentWindow is null || widget.parentWindow.win is null || widget.parentWindow.win.closed) 6665 return; 6666 6667 widget.redraw(); 6668 } 6669 6670 mixin SdpyDraw; 6671 } 6672 } else { 6673 /// 6674 enum IntegratedEmulator = false; 6675 } 6676 6677 /* 6678 void main() { 6679 auto terminal = Terminal(ConsoleOutputType.linear); 6680 terminal.setTrueColor(RGB(255, 0, 255), RGB(255, 255, 255)); 6681 terminal.writeln("Hello, world!"); 6682 } 6683 */ 6684 6685 6686 /* 6687 ONLY SUPPORTED ON MY TERMINAL EMULATOR IN GENERAL 6688 6689 bracketed section can collapse and scroll independently in the TE. may also pop out into a window (possibly with a comparison window) 6690 6691 hyperlink can either just indicate something to the TE to handle externally 6692 OR 6693 indicate a certain input sequence be triggered when it is clicked (prolly wrapped up as a paste event). this MAY also be a custom event. 6694 6695 internally it can set two bits: one indicates it is a hyperlink, the other just flips each use to separate consecutive sequences. 6696 6697 it might require the content of the paste event to be the visible word but it would bne kinda cool if it could be some secret thing elsewhere. 6698 6699 6700 I could spread a unique id number across bits, one bit per char so the memory isn't too bad. 6701 so it would set a number and a word. this is sent back to the application to handle internally. 6702 6703 1) turn on special input 6704 2) turn off special input 6705 3) special input sends a paste event with a number and the text 6706 4) to make a link, you write out the begin sequence, the text, and the end sequence. including the magic number somewhere. 6707 magic number is allowed to have one bit per char. the terminal discards anything else. terminal.d api will enforce. 6708 6709 if magic number is zero, it is not sent in the paste event. maybe. 6710 6711 or if it is like 255, it is handled as a url and opened externally 6712 tho tbh a url could just be detected by regex pattern 6713 6714 6715 NOTE: if your program requests mouse input, the TE does not process it! Thus the user will have to shift+click for it. 6716 6717 mode 3004 for bracketed hyperlink 6718 6719 hyperlink sequence: \033[?220hnum;text\033[?220l~ 6720 6721 */