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 // FIXME: the resume signal needs to be handled to set the terminal back in proper mode. 6 7 /++ 8 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]. 9 10 11 The main interface for this module is the Terminal struct, which 12 encapsulates the output functions and line-buffered input of the terminal, and 13 RealTimeConsoleInput, which gives real time input. 14 15 Creating an instance of these structs will perform console initialization. When the struct 16 goes out of scope, any changes in console settings will be automatically reverted and pending 17 output is flushed. Do not create a global Terminal, as this will skip the destructor. Also do 18 not create an instance inside a class or array, as again the destructor will be nondeterministic. 19 You should create the object as a local inside main (or wherever else will encapsulate its whole 20 usage lifetime), then pass borrowed pointers to it if needed somewhere else. This ensures the 21 construction and destruction is run in a timely manner. 22 23 $(PITFALL 24 Output is NOT flushed on \n! Output is buffered until: 25 26 $(LIST 27 * Terminal's destructor is run 28 * You request input from the terminal object 29 * You call `terminal.flush()` 30 ) 31 32 If you want to see output immediately, always call `terminal.flush()` 33 after writing. 34 ) 35 36 Note: on Posix, it traps SIGINT and translates it into an input event. You should 37 keep your event loop moving and keep an eye open for this to exit cleanly; simply break 38 your event loop upon receiving a UserInterruptionEvent. (Without 39 the signal handler, ctrl+c can leave your terminal in a bizarre state.) 40 41 As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\ 42 43 On old Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work on older versions. 44 Most functions work now with newer Mac OS versions though. 45 46 Future_Roadmap: 47 $(LIST 48 * The CharacterEvent and NonCharacterKeyEvent types will be removed. Instead, use KeyboardEvent 49 on new programs. 50 51 * The ScrollbackBuffer will be expanded to be easier to use to partition your screen. It might even 52 handle input events of some sort. Its API may change. 53 54 * getline I want to be really easy to use both for code and end users. It will need multi-line support 55 eventually. 56 57 * 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.) 58 59 * More advanced terminal features as functions, where available, like cursor changing and full-color functions. 60 61 * More documentation. 62 ) 63 64 WHAT I WON'T DO: 65 $(LIST 66 * support everything under the sun. If it isn't default-installed on an OS I or significant number of other people 67 might actually use, and isn't written by me, I don't really care about it. This means the only supported terminals are: 68 $(LIST 69 70 * xterm (and decently xterm compatible emulators like Konsole) 71 * Windows console 72 * rxvt (to a lesser extent) 73 * Linux console 74 * My terminal emulator family of applications https://github.com/adamdruppe/terminal-emulator 75 ) 76 77 Anything else is cool if it does work, but I don't want to go out of my way for it. 78 79 * Use other libraries, unless strictly optional. terminal.d is a stand-alone module by default and 80 always will be. 81 82 * Do a full TUI widget set. I might do some basics and lay a little groundwork, but a full TUI 83 is outside the scope of this module (unless I can do it really small.) 84 ) 85 86 History: 87 On December 29, 2020 the structs and their destructors got more protection against in-GC finalization errors and duplicate executions. 88 89 This should not affect your code. 90 +/ 91 module arsd.terminal; 92 93 // FIXME: needs to support VT output on Windows too in certain situations 94 // 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. 95 96 /++ 97 $(H3 Get Line) 98 99 This example will demonstrate the high-level [Terminal.getline] interface. 100 101 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. 102 +/ 103 unittest { 104 import arsd.terminal; 105 106 void main() { 107 auto terminal = Terminal(ConsoleOutputType.linear); 108 string line = terminal.getline(); 109 terminal.writeln("You wrote: ", line); 110 111 // new on October 11, 2021: you can change the echo char 112 // for password masking now. Also pass `0` there to get unix-style 113 // total silence. 114 string pwd = terminal.getline("Password: ", '*'); 115 terminal.writeln("Your password is: ", pwd); 116 } 117 118 version(demos) main; // exclude from docs 119 } 120 121 /++ 122 $(H3 Color) 123 124 This example demonstrates color output, using [Terminal.color] 125 and the output functions like [Terminal.writeln]. 126 +/ 127 unittest { 128 import arsd.terminal; 129 130 void main() { 131 auto terminal = Terminal(ConsoleOutputType.linear); 132 terminal.color(Color.green, Color.black); 133 terminal.writeln("Hello world, in green on black!"); 134 terminal.color(Color.DEFAULT, Color.DEFAULT); 135 terminal.writeln("And back to normal."); 136 } 137 138 version(demos) main; // exclude from docs 139 } 140 141 /++ 142 $(H3 Single Key) 143 144 This shows how to get one single character press using 145 the [RealTimeConsoleInput] structure. 146 +/ 147 unittest { 148 import arsd.terminal; 149 150 void main() { 151 auto terminal = Terminal(ConsoleOutputType.linear); 152 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw); 153 154 terminal.writeln("Press any key to continue..."); 155 auto ch = input.getch(); 156 terminal.writeln("You pressed ", ch); 157 } 158 159 version(demos) main; // exclude from docs 160 } 161 162 /++ 163 $(H3 Full screen) 164 165 This shows how to use the cellular (full screen) mode and pass terminal to functions. 166 +/ 167 unittest { 168 import arsd.terminal; 169 170 // passing terminals must be done by ref or by pointer 171 void helper(Terminal* terminal) { 172 terminal.moveTo(0, 1); 173 terminal.getline("Press enter to exit..."); 174 } 175 176 void main() { 177 // ask for cellular mode, it will go full screen 178 auto terminal = Terminal(ConsoleOutputType.cellular); 179 180 // it is automatically cleared upon entry 181 terminal.write("Hello upper left corner"); 182 183 // pass it by pointer to other functions 184 helper(&terminal); 185 186 // since at the end of main, Terminal's destructor 187 // resets the terminal to how it was before for the 188 // user 189 } 190 } 191 192 /* 193 Widgets: 194 tab widget 195 scrollback buffer 196 partitioned canvas 197 */ 198 199 // FIXME: ctrl+d eof on stdin 200 201 // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx 202 203 204 /++ 205 A function the sigint handler will call (if overridden - which is the 206 case when [RealTimeConsoleInput] is active on Posix or if you compile with 207 `TerminalDirectToEmulator` version on any platform at this time) in addition 208 to the library's default handling, which is to set a flag for the event loop 209 to inform you. 210 211 Remember, this is called from a signal handler and/or from a separate thread, 212 so you are not allowed to do much with it and need care when setting TLS variables. 213 214 I suggest you only set a `__gshared bool` flag as many other operations will risk 215 undefined behavior. 216 217 $(WARNING 218 This function is never called on the default Windows console 219 configuration in the current implementation. You can use 220 `-version=TerminalDirectToEmulator` to guarantee it is called there 221 too by causing the library to pop up a gui window for your application. 222 ) 223 224 History: 225 Added March 30, 2020. Included in release v7.1.0. 226 227 +/ 228 __gshared void delegate() nothrow @nogc sigIntExtension; 229 230 import core.stdc.stdio; 231 232 version(TerminalDirectToEmulator) { 233 version=WithEncapsulatedSignals; 234 private __gshared bool windowGone = false; 235 private bool forceTerminationTried = false; 236 private void forceTermination() { 237 if(forceTerminationTried) { 238 // why are we still here?! someone must be catching the exception and calling back. 239 // there's no recovery so time to kill this program. 240 import core.stdc.stdlib; 241 abort(); 242 } else { 243 // give them a chance to cleanly exit... 244 forceTerminationTried = true; 245 throw new HangupException(); 246 } 247 } 248 } 249 250 version(Posix) { 251 enum SIGWINCH = 28; 252 __gshared bool windowSizeChanged = false; 253 __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 254 __gshared bool hangedUp = false; /// similar to interrupted. 255 __gshared bool continuedFromSuspend = false; /// SIGCONT was just received, the terminal state may have changed. Added Feb 18, 2021. 256 version=WithSignals; 257 258 version(with_eventloop) 259 struct SignalFired {} 260 261 extern(C) 262 void sizeSignalHandler(int sigNumber) nothrow { 263 windowSizeChanged = true; 264 version(with_eventloop) { 265 import arsd.eventloop; 266 try 267 send(SignalFired()); 268 catch(Exception) {} 269 } 270 } 271 extern(C) 272 void interruptSignalHandler(int sigNumber) nothrow { 273 interrupted = true; 274 version(with_eventloop) { 275 import arsd.eventloop; 276 try 277 send(SignalFired()); 278 catch(Exception) {} 279 } 280 281 if(sigIntExtension) 282 sigIntExtension(); 283 } 284 extern(C) 285 void hangupSignalHandler(int sigNumber) nothrow { 286 hangedUp = true; 287 version(with_eventloop) { 288 import arsd.eventloop; 289 try 290 send(SignalFired()); 291 catch(Exception) {} 292 } 293 } 294 extern(C) 295 void continueSignalHandler(int sigNumber) nothrow { 296 continuedFromSuspend = true; 297 version(with_eventloop) { 298 import arsd.eventloop; 299 try 300 send(SignalFired()); 301 catch(Exception) {} 302 } 303 } 304 } 305 306 // parts of this were taken from Robik's ConsoleD 307 // https://github.com/robik/ConsoleD/blob/master/consoled.d 308 309 // Uncomment this line to get a main() to demonstrate this module's 310 // capabilities. 311 //version = Demo 312 313 version(TerminalDirectToEmulator) { 314 version=VtEscapeCodes; 315 } else version(Windows) { 316 version(VtEscapeCodes) {} // cool 317 version=Win32Console; 318 } 319 320 version(Windows) 321 import core.sys.windows.windows; 322 323 version(Win32Console) { 324 private { 325 enum RED_BIT = 4; 326 enum GREEN_BIT = 2; 327 enum BLUE_BIT = 1; 328 } 329 330 pragma(lib, "user32"); 331 } 332 333 version(Posix) { 334 335 version=VtEscapeCodes; 336 337 import core.sys.posix.termios; 338 import core.sys.posix.unistd; 339 import unix = core.sys.posix.unistd; 340 import core.sys.posix.sys.types; 341 import core.sys.posix.sys.time; 342 import core.stdc.stdio; 343 344 import core.sys.posix.sys.ioctl; 345 } 346 347 version(VtEscapeCodes) { 348 349 enum UseVtSequences = true; 350 351 version(TerminalDirectToEmulator) { 352 private { 353 enum RED_BIT = 1; 354 enum GREEN_BIT = 2; 355 enum BLUE_BIT = 4; 356 } 357 } else version(Windows) {} else 358 private { 359 enum RED_BIT = 1; 360 enum GREEN_BIT = 2; 361 enum BLUE_BIT = 4; 362 } 363 364 struct winsize { 365 ushort ws_row; 366 ushort ws_col; 367 ushort ws_xpixel; 368 ushort ws_ypixel; 369 } 370 371 // 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). 372 373 // this way we'll have some definitions for 99% of typical PC cases even without any help from the local operating system 374 375 enum string builtinTermcap = ` 376 # Generic VT entry. 377 vg|vt-generic|Generic VT entries:\ 378 :bs:mi:ms:pt:xn:xo:it#8:\ 379 :RA=\E[?7l:SA=\E?7h:\ 380 :bl=^G:cr=^M:ta=^I:\ 381 :cm=\E[%i%d;%dH:\ 382 :le=^H:up=\E[A:do=\E[B:nd=\E[C:\ 383 :LE=\E[%dD:RI=\E[%dC:UP=\E[%dA:DO=\E[%dB:\ 384 :ho=\E[H:cl=\E[H\E[2J:ce=\E[K:cb=\E[1K:cd=\E[J:sf=\ED:sr=\EM:\ 385 :ct=\E[3g:st=\EH:\ 386 :cs=\E[%i%d;%dr:sc=\E7:rc=\E8:\ 387 :ei=\E[4l:ic=\E[@:IC=\E[%d@:al=\E[L:AL=\E[%dL:\ 388 :dc=\E[P:DC=\E[%dP:dl=\E[M:DL=\E[%dM:\ 389 :so=\E[7m:se=\E[m:us=\E[4m:ue=\E[m:\ 390 :mb=\E[5m:mh=\E[2m:md=\E[1m:mr=\E[7m:me=\E[m:\ 391 :sc=\E7:rc=\E8:kb=\177:\ 392 :ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D: 393 394 395 # Slackware 3.1 linux termcap entry (Sat Apr 27 23:03:58 CDT 1996): 396 lx|linux|console|con80x25|LINUX System Console:\ 397 :do=^J:co#80:li#25:cl=\E[H\E[J:sf=\ED:sb=\EM:\ 398 :le=^H:bs:am:cm=\E[%i%d;%dH:nd=\E[C:up=\E[A:\ 399 :ce=\E[K:cd=\E[J:so=\E[7m:se=\E[27m:us=\E[36m:ue=\E[m:\ 400 :md=\E[1m:mr=\E[7m:mb=\E[5m:me=\E[m:is=\E[1;25r\E[25;1H:\ 401 :ll=\E[1;25r\E[25;1H:al=\E[L:dc=\E[P:dl=\E[M:\ 402 :it#8:ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D:kb=^H:ti=\E[r\E[H:\ 403 :ho=\E[H:kP=\E[5~:kN=\E[6~:kH=\E[4~:kh=\E[1~:kD=\E[3~:kI=\E[2~:\ 404 :k1=\E[[A:k2=\E[[B:k3=\E[[C:k4=\E[[D:k5=\E[[E:k6=\E[17~:\ 405 :F1=\E[23~:F2=\E[24~:\ 406 :k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:K1=\E[1~:K2=\E[5~:\ 407 :K4=\E[4~:K5=\E[6~:\ 408 :pt:sr=\EM:vt#3:xn:km:bl=^G:vi=\E[?25l:ve=\E[?25h:vs=\E[?25h:\ 409 :sc=\E7:rc=\E8:cs=\E[%i%d;%dr:\ 410 :r1=\Ec:r2=\Ec:r3=\Ec: 411 412 # Some other, commonly used linux console entries. 413 lx|con80x28:co#80:li#28:tc=linux: 414 lx|con80x43:co#80:li#43:tc=linux: 415 lx|con80x50:co#80:li#50:tc=linux: 416 lx|con100x37:co#100:li#37:tc=linux: 417 lx|con100x40:co#100:li#40:tc=linux: 418 lx|con132x43:co#132:li#43:tc=linux: 419 420 # vt102 - vt100 + insert line etc. VT102 does not have insert character. 421 v2|vt102|DEC vt102 compatible:\ 422 :co#80:li#24:\ 423 :ic@:IC@:\ 424 :is=\E[m\E[?1l\E>:\ 425 :rs=\E[m\E[?1l\E>:\ 426 :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 427 :ks=:ke=:\ 428 :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:\ 429 :tc=vt-generic: 430 431 # vt100 - really vt102 without insert line, insert char etc. 432 vt|vt100|DEC vt100 compatible:\ 433 :im@:mi@:al@:dl@:ic@:dc@:AL@:DL@:IC@:DC@:\ 434 :tc=vt102: 435 436 437 # Entry for an xterm. Insert mode has been disabled. 438 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):\ 439 :am:bs:mi@:km:co#80:li#55:\ 440 :im@:ei@:\ 441 :cl=\E[H\E[J:\ 442 :ct=\E[3k:ue=\E[m:\ 443 :is=\E[m\E[?1l\E>:\ 444 :rs=\E[m\E[?1l\E>:\ 445 :vi=\E[?25l:ve=\E[?25h:\ 446 :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 447 :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ 448 :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:\ 449 :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ 450 :F1=\E[23~:F2=\E[24~:\ 451 :kh=\E[H:kH=\E[F:\ 452 :ks=:ke=:\ 453 :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ 454 :tc=vt-generic: 455 456 457 #rxvt, added by me 458 rxvt|rxvt-unicode|rxvt-unicode-256color:\ 459 :am:bs:mi@:km:co#80:li#55:\ 460 :im@:ei@:\ 461 :ct=\E[3k:ue=\E[m:\ 462 :is=\E[m\E[?1l\E>:\ 463 :rs=\E[m\E[?1l\E>:\ 464 :vi=\E[?25l:\ 465 :ve=\E[?25h:\ 466 :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 467 :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ 468 :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ 469 :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ 470 :F1=\E[23~:F2=\E[24~:\ 471 :kh=\E[7~:kH=\E[8~:\ 472 :ks=:ke=:\ 473 :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ 474 :tc=vt-generic: 475 476 477 # Some other entries for the same xterm. 478 v2|xterms|vs100s|xterm small window:\ 479 :co#80:li#24:tc=xterm: 480 vb|xterm-bold|xterm with bold instead of underline:\ 481 :us=\E[1m:tc=xterm: 482 vi|xterm-ins|xterm with insert mode:\ 483 :mi:im=\E[4h:ei=\E[4l:tc=xterm: 484 485 Eterm|Eterm Terminal Emulator (X11 Window System):\ 486 :am:bw:eo:km:mi:ms:xn:xo:\ 487 :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:\ 488 :AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:IC=\E[%d@:\ 489 :K1=\E[7~:K2=\EOu:K3=\E[5~:K4=\E[8~:K5=\E[6~:LE=\E[%dD:\ 490 :RI=\E[%dC:UP=\E[%dA:ae=^O:al=\E[L:as=^N:bl=^G:cd=\E[J:\ 491 :ce=\E[K:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=^M:\ 492 :cs=\E[%i%d;%dr:ct=\E[3g:dc=\E[P:dl=\E[M:do=\E[B:\ 493 :ec=\E[%dX:ei=\E[4l:ho=\E[H:i1=\E[?47l\E>\E[?1l:ic=\E[@:\ 494 :im=\E[4h:is=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l:\ 495 :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ 496 :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:kD=\E[3~:\ 497 :kI=\E[2~:kN=\E[6~:kP=\E[5~:kb=^H:kd=\E[B:ke=:kh=\E[7~:\ 498 :kl=\E[D:kr=\E[C:ks=:ku=\E[A:le=^H:mb=\E[5m:md=\E[1m:\ 499 :me=\E[m\017:mr=\E[7m:nd=\E[C:rc=\E8:\ 500 :sc=\E7:se=\E[27m:sf=^J:so=\E[7m:sr=\EM:st=\EH:ta=^I:\ 501 :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:ue=\E[24m:up=\E[A:\ 502 :us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?25h:vi=\E[?25l:\ 503 :ac=aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~: 504 505 # DOS terminal emulator such as Telix or TeleMate. 506 # This probably also works for the SCO console, though it's incomplete. 507 an|ansi|ansi-bbs|ANSI terminals (emulators):\ 508 :co#80:li#24:am:\ 509 :is=:rs=\Ec:kb=^H:\ 510 :as=\E[m:ae=:eA=:\ 511 :ac=0\333+\257,\256.\031-\030a\261f\370g\361j\331k\277l\332m\300n\305q\304t\264u\303v\301w\302x\263~\025:\ 512 :kD=\177:kH=\E[Y:kN=\E[U:kP=\E[V:kh=\E[H:\ 513 :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\EOT:\ 514 :k6=\EOU:k7=\EOV:k8=\EOW:k9=\EOX:k0=\EOY:\ 515 :tc=vt-generic: 516 517 `; 518 } else { 519 enum UseVtSequences = false; 520 } 521 522 /// A modifier for [Color] 523 enum Bright = 0x08; 524 525 /// Defines the list of standard colors understood by Terminal. 526 /// See also: [Bright] 527 enum Color : ushort { 528 black = 0, /// . 529 red = RED_BIT, /// . 530 green = GREEN_BIT, /// . 531 yellow = red | green, /// . 532 blue = BLUE_BIT, /// . 533 magenta = red | blue, /// . 534 cyan = blue | green, /// . 535 white = red | green | blue, /// . 536 DEFAULT = 256, 537 } 538 539 /// When capturing input, what events are you interested in? 540 /// 541 /// Note: these flags can be OR'd together to select more than one option at a time. 542 /// 543 /// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw. 544 /// 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. 545 enum ConsoleInputFlags { 546 raw = 0, /// raw input returns keystrokes immediately, without line buffering 547 echo = 1, /// do you want to automatically echo input back to the user? 548 mouse = 2, /// capture mouse events 549 paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes) 550 size = 8, /// window resize events 551 552 releasedKeys = 64, /// key release events. Not reliable on Posix. 553 554 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. 555 allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events. 556 557 noEolWrap = 128, 558 selectiveMouse = 256, /// Uses arsd terminal emulator's proprietary extension to select mouse input only for special cases, intended to enhance getline while keeping default terminal mouse behavior in other places. If it is set, it overrides [mouse] event flag. If not using the arsd terminal emulator, this will disable application mouse input. 559 } 560 561 /// Defines how terminal output should be handled. 562 enum ConsoleOutputType { 563 linear = 0, /// do you want output to work one line at a time? 564 cellular = 1, /// or do you want access to the terminal screen as a grid of characters? 565 //truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges 566 567 minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here 568 } 569 570 alias ConsoleOutputMode = ConsoleOutputType; 571 572 /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present 573 enum ForceOption { 574 automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) 575 neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution. 576 alwaysSend = 1, /// always send the data, even if it doesn't seem necessary 577 } 578 579 /// 580 enum TerminalCursor { 581 DEFAULT = 0, /// 582 insert = 1, /// 583 block = 2 /// 584 } 585 586 // we could do it with termcap too, getenv("TERMCAP") then split on : and replace \E with \033 and get the pieces 587 588 /// Encapsulates the I/O capabilities of a terminal. 589 /// 590 /// Warning: do not write out escape sequences to the terminal. This won't work 591 /// on Windows and will confuse Terminal's internal state on Posix. 592 struct Terminal { 593 /// 594 @disable this(); 595 @disable this(this); 596 private ConsoleOutputType type; 597 598 version(TerminalDirectToEmulator) { 599 private bool windowSizeChanged = false; 600 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 601 private bool hangedUp = false; /// similar to interrupted. 602 } 603 604 private TerminalCursor currentCursor_; 605 version(Windows) private CONSOLE_CURSOR_INFO originalCursorInfo; 606 607 /++ 608 Changes the current cursor. 609 +/ 610 void cursor(TerminalCursor what, ForceOption force = ForceOption.automatic) { 611 if(force == ForceOption.neverSend) { 612 currentCursor_ = what; 613 return; 614 } else { 615 if(what != currentCursor_ || force == ForceOption.alwaysSend) { 616 currentCursor_ = what; 617 version(Win32Console) { 618 final switch(what) { 619 case TerminalCursor.DEFAULT: 620 SetConsoleCursorInfo(hConsole, &originalCursorInfo); 621 break; 622 case TerminalCursor.insert: 623 case TerminalCursor.block: 624 CONSOLE_CURSOR_INFO info; 625 GetConsoleCursorInfo(hConsole, &info); 626 info.dwSize = what == TerminalCursor.insert ? 1 : 100; 627 SetConsoleCursorInfo(hConsole, &info); 628 break; 629 } 630 } else { 631 final switch(what) { 632 case TerminalCursor.DEFAULT: 633 if(terminalInFamily("linux")) 634 writeStringRaw("\033[?0c"); 635 else 636 writeStringRaw("\033[2 q"); // assuming non-blinking block are the desired default 637 break; 638 case TerminalCursor.insert: 639 if(terminalInFamily("linux")) 640 writeStringRaw("\033[?2c"); 641 else if(terminalInFamily("xterm")) 642 writeStringRaw("\033[6 q"); 643 else 644 writeStringRaw("\033[4 q"); 645 break; 646 case TerminalCursor.block: 647 if(terminalInFamily("linux")) 648 writeStringRaw("\033[?6c"); 649 else 650 writeStringRaw("\033[2 q"); 651 break; 652 } 653 } 654 } 655 } 656 } 657 658 /++ 659 Terminal is only valid to use on an actual console device or terminal 660 handle. You should not attempt to construct a Terminal instance if this 661 returns false. Real time input is similarly impossible if `!stdinIsTerminal`. 662 +/ 663 static bool stdoutIsTerminal() { 664 version(TerminalDirectToEmulator) { 665 version(Windows) { 666 // if it is null, it was a gui subsystem exe. But otherwise, it 667 // might be explicitly redirected and we should respect that for 668 // compatibility with normal console expectations (even though like 669 // we COULD pop up a gui and do both, really that isn't the normal 670 // use of this library so don't wanna go too nuts) 671 auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 672 return hConsole is null || GetFileType(hConsole) == FILE_TYPE_CHAR; 673 } else version(Posix) { 674 // same as normal here since thee is no gui subsystem really 675 import core.sys.posix.unistd; 676 return cast(bool) isatty(1); 677 } else static assert(0); 678 } else version(Posix) { 679 import core.sys.posix.unistd; 680 return cast(bool) isatty(1); 681 } else version(Win32Console) { 682 auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 683 return GetFileType(hConsole) == FILE_TYPE_CHAR; 684 /+ 685 auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 686 CONSOLE_SCREEN_BUFFER_INFO originalSbi; 687 if(GetConsoleScreenBufferInfo(hConsole, &originalSbi) == 0) 688 return false; 689 else 690 return true; 691 +/ 692 } else static assert(0); 693 } 694 695 /// 696 static bool stdinIsTerminal() { 697 version(TerminalDirectToEmulator) { 698 version(Windows) { 699 auto hConsole = GetStdHandle(STD_INPUT_HANDLE); 700 return hConsole is null || GetFileType(hConsole) == FILE_TYPE_CHAR; 701 } else version(Posix) { 702 // same as normal here since thee is no gui subsystem really 703 import core.sys.posix.unistd; 704 return cast(bool) isatty(0); 705 } else static assert(0); 706 } else version(Posix) { 707 import core.sys.posix.unistd; 708 return cast(bool) isatty(0); 709 } else version(Win32Console) { 710 auto hConsole = GetStdHandle(STD_INPUT_HANDLE); 711 return GetFileType(hConsole) == FILE_TYPE_CHAR; 712 } else static assert(0); 713 } 714 715 version(Posix) { 716 private int fdOut; 717 private int fdIn; 718 private int[] delegate() getSizeOverride; 719 void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically 720 } 721 722 bool terminalInFamily(string[] terms...) { 723 import std.process; 724 import std.string; 725 version(TerminalDirectToEmulator) 726 auto term = "xterm"; 727 else 728 auto term = environment.get("TERM"); 729 foreach(t; terms) 730 if(indexOf(term, t) != -1) 731 return true; 732 733 return false; 734 } 735 736 version(Posix) { 737 // This is a filthy hack because Terminal.app and OS X are garbage who don't 738 // work the way they're advertised. I just have to best-guess hack and hope it 739 // doesn't break anything else. (If you know a better way, let me know!) 740 bool isMacTerminal() { 741 // it gives 1,2 in getTerminalCapabilities... 742 // FIXME 743 import std.process; 744 import std.string; 745 auto term = environment.get("TERM"); 746 return term == "xterm-256color"; 747 } 748 } else 749 bool isMacTerminal() { return false; } 750 751 static string[string] termcapDatabase; 752 static void readTermcapFile(bool useBuiltinTermcap = false) { 753 import std.file; 754 import std.stdio; 755 import std.string; 756 757 //if(!exists("/etc/termcap")) 758 useBuiltinTermcap = true; 759 760 string current; 761 762 void commitCurrentEntry() { 763 if(current is null) 764 return; 765 766 string names = current; 767 auto idx = indexOf(names, ":"); 768 if(idx != -1) 769 names = names[0 .. idx]; 770 771 foreach(name; split(names, "|")) 772 termcapDatabase[name] = current; 773 774 current = null; 775 } 776 777 void handleTermcapLine(in char[] line) { 778 if(line.length == 0) { // blank 779 commitCurrentEntry(); 780 return; // continue 781 } 782 if(line[0] == '#') // comment 783 return; // continue 784 size_t termination = line.length; 785 if(line[$-1] == '\\') 786 termination--; // cut off the \\ 787 current ~= strip(line[0 .. termination]); 788 // termcap entries must be on one logical line, so if it isn't continued, we know we're done 789 if(line[$-1] != '\\') 790 commitCurrentEntry(); 791 } 792 793 if(useBuiltinTermcap) { 794 version(VtEscapeCodes) 795 foreach(line; splitLines(builtinTermcap)) { 796 handleTermcapLine(line); 797 } 798 } else { 799 foreach(line; File("/etc/termcap").byLine()) { 800 handleTermcapLine(line); 801 } 802 } 803 } 804 805 static string getTermcapDatabase(string terminal) { 806 import std.string; 807 808 if(termcapDatabase is null) 809 readTermcapFile(); 810 811 auto data = terminal in termcapDatabase; 812 if(data is null) 813 return null; 814 815 auto tc = *data; 816 auto more = indexOf(tc, ":tc="); 817 if(more != -1) { 818 auto tcKey = tc[more + ":tc=".length .. $]; 819 auto end = indexOf(tcKey, ":"); 820 if(end != -1) 821 tcKey = tcKey[0 .. end]; 822 tc = getTermcapDatabase(tcKey) ~ tc; 823 } 824 825 return tc; 826 } 827 828 string[string] termcap; 829 void readTermcap(string t = null) { 830 version(TerminalDirectToEmulator) 831 if(usingDirectEmulator) 832 t = "xterm"; 833 import std.process; 834 import std.string; 835 import std.array; 836 837 string termcapData = environment.get("TERMCAP"); 838 if(termcapData.length == 0) { 839 if(t is null) { 840 t = environment.get("TERM"); 841 } 842 843 // loosen the check so any xterm variety gets 844 // the same termcap. odds are this is right 845 // almost always 846 if(t.indexOf("xterm") != -1) 847 t = "xterm"; 848 if(t.indexOf("putty") != -1) 849 t = "xterm"; 850 if(t.indexOf("tmux") != -1) 851 t = "tmux"; 852 if(t.indexOf("screen") != -1) 853 t = "screen"; 854 855 termcapData = getTermcapDatabase(t); 856 } 857 858 auto e = replace(termcapData, "\\\n", "\n"); 859 termcap = null; 860 861 foreach(part; split(e, ":")) { 862 // FIXME: handle numeric things too 863 864 auto things = split(part, "="); 865 if(things.length) 866 termcap[things[0]] = 867 things.length > 1 ? things[1] : null; 868 } 869 } 870 871 string findSequenceInTermcap(in char[] sequenceIn) { 872 char[10] sequenceBuffer; 873 char[] sequence; 874 if(sequenceIn.length > 0 && sequenceIn[0] == '\033') { 875 if(!(sequenceIn.length < sequenceBuffer.length - 1)) 876 return null; 877 sequenceBuffer[1 .. sequenceIn.length + 1] = sequenceIn[]; 878 sequenceBuffer[0] = '\\'; 879 sequenceBuffer[1] = 'E'; 880 sequence = sequenceBuffer[0 .. sequenceIn.length + 1]; 881 } else { 882 sequence = sequenceBuffer[1 .. sequenceIn.length + 1]; 883 } 884 885 import std.array; 886 foreach(k, v; termcap) 887 if(v == sequence) 888 return k; 889 return null; 890 } 891 892 string getTermcap(string key) { 893 auto k = key in termcap; 894 if(k !is null) return *k; 895 return null; 896 } 897 898 // Looks up a termcap item and tries to execute it. Returns false on failure 899 bool doTermcap(T...)(string key, T t) { 900 import std.conv; 901 auto fs = getTermcap(key); 902 if(fs is null) 903 return false; 904 905 int swapNextTwo = 0; 906 907 R getArg(R)(int idx) { 908 if(swapNextTwo == 2) { 909 idx ++; 910 swapNextTwo--; 911 } else if(swapNextTwo == 1) { 912 idx --; 913 swapNextTwo--; 914 } 915 916 foreach(i, arg; t) { 917 if(i == idx) 918 return to!R(arg); 919 } 920 assert(0, to!string(idx) ~ " is out of bounds working " ~ fs); 921 } 922 923 char[256] buffer; 924 int bufferPos = 0; 925 926 void addChar(char c) { 927 import std.exception; 928 enforce(bufferPos < buffer.length); 929 buffer[bufferPos++] = c; 930 } 931 932 void addString(in char[] c) { 933 import std.exception; 934 enforce(bufferPos + c.length < buffer.length); 935 buffer[bufferPos .. bufferPos + c.length] = c[]; 936 bufferPos += c.length; 937 } 938 939 void addInt(int c, int minSize) { 940 import std.string; 941 auto str = format("%0"~(minSize ? to!string(minSize) : "")~"d", c); 942 addString(str); 943 } 944 945 bool inPercent; 946 int argPosition = 0; 947 int incrementParams = 0; 948 bool skipNext; 949 bool nextIsChar; 950 bool inBackslash; 951 952 foreach(char c; fs) { 953 if(inBackslash) { 954 if(c == 'E') 955 addChar('\033'); 956 else 957 addChar(c); 958 inBackslash = false; 959 } else if(nextIsChar) { 960 if(skipNext) 961 skipNext = false; 962 else 963 addChar(cast(char) (c + getArg!int(argPosition) + (incrementParams ? 1 : 0))); 964 if(incrementParams) incrementParams--; 965 argPosition++; 966 inPercent = false; 967 } else if(inPercent) { 968 switch(c) { 969 case '%': 970 addChar('%'); 971 inPercent = false; 972 break; 973 case '2': 974 case '3': 975 case 'd': 976 if(skipNext) 977 skipNext = false; 978 else 979 addInt(getArg!int(argPosition) + (incrementParams ? 1 : 0), 980 c == 'd' ? 0 : (c - '0') 981 ); 982 if(incrementParams) incrementParams--; 983 argPosition++; 984 inPercent = false; 985 break; 986 case '.': 987 if(skipNext) 988 skipNext = false; 989 else 990 addChar(cast(char) (getArg!int(argPosition) + (incrementParams ? 1 : 0))); 991 if(incrementParams) incrementParams--; 992 argPosition++; 993 break; 994 case '+': 995 nextIsChar = true; 996 inPercent = false; 997 break; 998 case 'i': 999 incrementParams = 2; 1000 inPercent = false; 1001 break; 1002 case 's': 1003 skipNext = true; 1004 inPercent = false; 1005 break; 1006 case 'b': 1007 argPosition--; 1008 inPercent = false; 1009 break; 1010 case 'r': 1011 swapNextTwo = 2; 1012 inPercent = false; 1013 break; 1014 // FIXME: there's more 1015 // http://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html 1016 1017 default: 1018 assert(0, "not supported " ~ c); 1019 } 1020 } else { 1021 if(c == '%') 1022 inPercent = true; 1023 else if(c == '\\') 1024 inBackslash = true; 1025 else 1026 addChar(c); 1027 } 1028 } 1029 1030 writeStringRaw(buffer[0 .. bufferPos]); 1031 return true; 1032 } 1033 1034 uint tcaps; 1035 1036 bool inlineImagesSupported() const { 1037 return (tcaps & TerminalCapabilities.arsdImage) ? true : false; 1038 } 1039 bool clipboardSupported() const { 1040 version(Win32Console) return true; 1041 else return (tcaps & TerminalCapabilities.arsdClipboard) ? true : false; 1042 } 1043 1044 version (Win32Console) 1045 // Mimic sc & rc termcaps on Windows 1046 COORD[] cursorPositionStack; 1047 1048 /++ 1049 Saves/restores cursor position to a stack. 1050 1051 History: 1052 Added August 6, 2022 (dub v10.9) 1053 +/ 1054 bool saveCursorPosition() 1055 { 1056 version (Win32Console) 1057 { 1058 flush(); 1059 CONSOLE_SCREEN_BUFFER_INFO info; 1060 if (GetConsoleScreenBufferInfo(hConsole, &info)) 1061 { 1062 cursorPositionStack ~= info.dwCursorPosition; // push 1063 return true; 1064 } 1065 else 1066 { 1067 return false; 1068 } 1069 } 1070 else 1071 return doTermcap("sc"); 1072 } 1073 1074 /// ditto 1075 bool restoreCursorPosition() 1076 { 1077 version (Win32Console) 1078 { 1079 if (cursorPositionStack.length > 0) 1080 { 1081 auto p = cursorPositionStack[$ - 1]; 1082 moveTo(p.X, p.Y); 1083 cursorPositionStack = cursorPositionStack[0 .. $ - 1]; // pop 1084 return true; 1085 } 1086 else 1087 return false; 1088 } 1089 else 1090 { 1091 // FIXME: needs to update cursorX and cursorY 1092 return doTermcap("rc"); 1093 } 1094 } 1095 1096 // only supported on my custom terminal emulator. guarded behind if(inlineImagesSupported) 1097 // though that isn't even 100% accurate but meh 1098 void changeWindowIcon()(string filename) { 1099 if(inlineImagesSupported()) { 1100 import arsd.png; 1101 auto image = readPng(filename); 1102 auto ii = cast(IndexedImage) image; 1103 assert(ii !is null); 1104 1105 // copy/pasted from my terminalemulator.d 1106 string encodeSmallTextImage(IndexedImage ii) { 1107 char encodeNumeric(int c) { 1108 if(c < 10) 1109 return cast(char)(c + '0'); 1110 if(c < 10 + 26) 1111 return cast(char)(c - 10 + 'a'); 1112 assert(0); 1113 } 1114 1115 string s; 1116 s ~= encodeNumeric(ii.width); 1117 s ~= encodeNumeric(ii.height); 1118 1119 foreach(entry; ii.palette) 1120 s ~= entry.toRgbaHexString(); 1121 s ~= "Z"; 1122 1123 ubyte rleByte; 1124 int rleCount; 1125 1126 void rleCommit() { 1127 if(rleByte >= 26) 1128 assert(0); // too many colors for us to handle 1129 if(rleCount == 0) 1130 goto finish; 1131 if(rleCount == 1) { 1132 s ~= rleByte + 'a'; 1133 goto finish; 1134 } 1135 1136 import std.conv; 1137 s ~= to!string(rleCount); 1138 s ~= rleByte + 'a'; 1139 1140 finish: 1141 rleByte = 0; 1142 rleCount = 0; 1143 } 1144 1145 foreach(b; ii.data) { 1146 if(b == rleByte) 1147 rleCount++; 1148 else { 1149 rleCommit(); 1150 rleByte = b; 1151 rleCount = 1; 1152 } 1153 } 1154 1155 rleCommit(); 1156 1157 return s; 1158 } 1159 1160 this.writeStringRaw("\033]5000;"~encodeSmallTextImage(ii)~"\007"); 1161 } 1162 } 1163 1164 // dependent on tcaps... 1165 void displayInlineImage()(in ubyte[] imageData) { 1166 if(inlineImagesSupported) { 1167 import std.base64; 1168 1169 // I might change this protocol later! 1170 enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:"; 1171 1172 this.writeStringRaw("\000"); 1173 this.writeStringRaw(extensionMagicIdentifier); 1174 this.writeStringRaw(Base64.encode(imageData)); 1175 this.writeStringRaw("\000"); 1176 } 1177 } 1178 1179 void demandUserAttention() { 1180 if(UseVtSequences) { 1181 if(!terminalInFamily("linux")) 1182 writeStringRaw("\033]5001;1\007"); 1183 } 1184 } 1185 1186 void requestCopyToClipboard(in char[] text) { 1187 if(clipboardSupported) { 1188 import std.base64; 1189 writeStringRaw("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007"); 1190 } 1191 } 1192 1193 void requestCopyToPrimary(in char[] text) { 1194 if(clipboardSupported) { 1195 import std.base64; 1196 writeStringRaw("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007"); 1197 } 1198 } 1199 1200 // it sets the internal selection, you are still responsible for showing to users if need be 1201 // may not work though, check `clipboardSupported` or have some alternate way for the user to use the selection 1202 void requestSetTerminalSelection(string text) { 1203 if(clipboardSupported) { 1204 import std.base64; 1205 writeStringRaw("\033]52;s;"~Base64.encode(cast(ubyte[])text)~"\007"); 1206 } 1207 } 1208 1209 1210 bool hasDefaultDarkBackground() { 1211 version(Win32Console) { 1212 return !(defaultBackgroundColor & 0xf); 1213 } else { 1214 version(TerminalDirectToEmulator) 1215 if(usingDirectEmulator) 1216 return integratedTerminalEmulatorConfiguration.defaultBackground.g < 100; 1217 // FIXME: there is probably a better way to do this 1218 // but like idk how reliable it is. 1219 if(terminalInFamily("linux")) 1220 return true; 1221 else 1222 return false; 1223 } 1224 } 1225 1226 version(TerminalDirectToEmulator) { 1227 TerminalEmulatorWidget tew; 1228 private __gshared Window mainWindow; 1229 import core.thread; 1230 version(Posix) 1231 ThreadID threadId; 1232 else version(Windows) 1233 HANDLE threadId; 1234 private __gshared Thread guiThread; 1235 1236 private static class NewTerminalEvent { 1237 Terminal* t; 1238 this(Terminal* t) { 1239 this.t = t; 1240 } 1241 } 1242 1243 bool usingDirectEmulator; 1244 } 1245 1246 version(TerminalDirectToEmulator) 1247 /++ 1248 When using the embedded terminal emulator build, closing the terminal signals that the main thread should exit 1249 by sending it a hang up event. If the main thread responds, no problem. But if it doesn't, it can keep a thing 1250 running in the background with no visible window. This timeout gives it a chance to exit cleanly, but if it 1251 doesn't by the end of the time, the program will be forcibly closed automatically. 1252 1253 History: 1254 Added March 14, 2023 (dub v10.10) 1255 +/ 1256 static __gshared int terminateTimeoutMsecs = 3500; 1257 1258 version(TerminalDirectToEmulator) 1259 /++ 1260 +/ 1261 this(ConsoleOutputType type) { 1262 _initialized = true; 1263 this.type = type; 1264 1265 if(type == ConsoleOutputType.minimalProcessing) { 1266 readTermcap("xterm"); 1267 _suppressDestruction = true; 1268 return; 1269 } 1270 1271 import arsd.simpledisplay; 1272 static if(UsingSimpledisplayX11) { 1273 if(!integratedTerminalEmulatorConfiguration.preferDegradedTerminal) 1274 try { 1275 if(arsd.simpledisplay.librariesSuccessfullyLoaded) { 1276 XDisplayConnection.get(); 1277 this.usingDirectEmulator = true; 1278 } else if(!integratedTerminalEmulatorConfiguration.fallbackToDegradedTerminal) { 1279 throw new Exception("Unable to load X libraries to create custom terminal."); 1280 } 1281 } catch(Exception e) { 1282 if(!integratedTerminalEmulatorConfiguration.fallbackToDegradedTerminal) 1283 throw e; 1284 1285 } 1286 } else { 1287 this.usingDirectEmulator = true; 1288 } 1289 1290 if(!usingDirectEmulator) { 1291 version(Posix) { 1292 posixInitialize(type, 0, 1, null); 1293 return; 1294 } else { 1295 throw new Exception("Total wtf - are you on a windows system without a gui?!?"); 1296 } 1297 assert(0); 1298 } 1299 1300 tcaps = uint.max; // all capabilities 1301 import core.thread; 1302 1303 version(Posix) 1304 threadId = Thread.getThis.id; 1305 else version(Windows) 1306 threadId = GetCurrentThread(); 1307 1308 if(guiThread is null) { 1309 guiThread = new Thread( { 1310 try { 1311 auto window = new TerminalEmulatorWindow(&this, null); 1312 mainWindow = window; 1313 mainWindow.win.addEventListener((NewTerminalEvent t) { 1314 auto nw = new TerminalEmulatorWindow(t.t, null); 1315 t.t.tew = nw.tew; 1316 t.t = null; 1317 nw.show(); 1318 }); 1319 tew = window.tew; 1320 window.loop(); 1321 1322 // if the other thread doesn't terminate in a reasonable amount of time 1323 // after the window closes, we're gonna terminate it by force to avoid 1324 // leaving behind a background process with no obvious ui 1325 if(Terminal.terminateTimeoutMsecs >= 0) { 1326 auto murderThread = new Thread(() { 1327 Thread.sleep(terminateTimeoutMsecs.msecs); 1328 terminateTerminalProcess(threadId); 1329 }); 1330 murderThread.isDaemon = true; 1331 murderThread.start(); 1332 } 1333 } catch(Throwable t) { 1334 guiAbortProcess(t.toString()); 1335 } 1336 }); 1337 guiThread.start(); 1338 guiThread.priority = Thread.PRIORITY_MAX; // gui thread needs responsiveness 1339 } else { 1340 // FIXME: 64 bit builds on linux segfault with multiple terminals 1341 // so that isn't really supported as of yet. 1342 while(cast(shared) mainWindow is null) { 1343 import core.thread; 1344 Thread.sleep(5.msecs); 1345 } 1346 mainWindow.win.postEvent(new NewTerminalEvent(&this)); 1347 } 1348 1349 // need to wait until it is properly initialized 1350 while(cast(shared) tew is null) { 1351 import core.thread; 1352 Thread.sleep(5.msecs); 1353 } 1354 1355 initializeVt(); 1356 1357 } 1358 else 1359 1360 version(Posix) 1361 /** 1362 * Constructs an instance of Terminal representing the capabilities of 1363 * the current terminal. 1364 * 1365 * While it is possible to override the stdin+stdout file descriptors, remember 1366 * that is not portable across platforms and be sure you know what you're doing. 1367 * 1368 * ditto on getSizeOverride. That's there so you can do something instead of ioctl. 1369 */ 1370 this(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { 1371 _initialized = true; 1372 posixInitialize(type, fdIn, fdOut, getSizeOverride); 1373 } 1374 1375 version(Posix) 1376 private void posixInitialize(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { 1377 this.fdIn = fdIn; 1378 this.fdOut = fdOut; 1379 this.getSizeOverride = getSizeOverride; 1380 this.type = type; 1381 1382 if(type == ConsoleOutputType.minimalProcessing) { 1383 readTermcap(); 1384 _suppressDestruction = true; 1385 return; 1386 } 1387 1388 tcaps = getTerminalCapabilities(fdIn, fdOut); 1389 //writeln(tcaps); 1390 1391 initializeVt(); 1392 } 1393 1394 void initializeVt() { 1395 readTermcap(); 1396 1397 if(type == ConsoleOutputType.cellular) { 1398 goCellular(); 1399 } 1400 1401 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 1402 writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it) 1403 } 1404 1405 } 1406 1407 private void goCellular() { 1408 version(Win32Console) { 1409 hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, null, CONSOLE_TEXTMODE_BUFFER, null); 1410 if(hConsole == INVALID_HANDLE_VALUE) { 1411 import std.conv; 1412 throw new Exception(to!string(GetLastError())); 1413 } 1414 1415 SetConsoleActiveScreenBuffer(hConsole); 1416 /* 1417 http://msdn.microsoft.com/en-us/library/windows/desktop/ms686125%28v=vs.85%29.aspx 1418 http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.aspx 1419 */ 1420 COORD size; 1421 /* 1422 CONSOLE_SCREEN_BUFFER_INFO sbi; 1423 GetConsoleScreenBufferInfo(hConsole, &sbi); 1424 size.X = cast(short) GetSystemMetrics(SM_CXMIN); 1425 size.Y = cast(short) GetSystemMetrics(SM_CYMIN); 1426 */ 1427 1428 // FIXME: this sucks, maybe i should just revert it. but there shouldn't be scrollbars in cellular mode 1429 //size.X = 80; 1430 //size.Y = 24; 1431 //SetConsoleScreenBufferSize(hConsole, size); 1432 1433 GetConsoleCursorInfo(hConsole, &originalCursorInfo); 1434 1435 clear(); 1436 } else { 1437 doTermcap("ti"); 1438 clear(); 1439 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 1440 } 1441 } 1442 1443 private void goLinear() { 1444 version(Win32Console) { 1445 auto stdo = GetStdHandle(STD_OUTPUT_HANDLE); 1446 SetConsoleActiveScreenBuffer(stdo); 1447 if(hConsole !is stdo) 1448 CloseHandle(hConsole); 1449 1450 hConsole = stdo; 1451 } else { 1452 doTermcap("te"); 1453 } 1454 } 1455 1456 private ConsoleOutputType originalType; 1457 private bool typeChanged; 1458 1459 // EXPERIMENTAL do not use yet 1460 /++ 1461 It is not valid to call this if you constructed with minimalProcessing. 1462 +/ 1463 void enableAlternateScreen(bool active) { 1464 assert(type != ConsoleOutputType.minimalProcessing); 1465 1466 if(active) { 1467 if(type == ConsoleOutputType.cellular) 1468 return; // already set 1469 1470 flush(); 1471 goCellular(); 1472 type = ConsoleOutputType.cellular; 1473 } else { 1474 if(type == ConsoleOutputType.linear) 1475 return; // already set 1476 1477 flush(); 1478 goLinear(); 1479 type = ConsoleOutputType.linear; 1480 } 1481 } 1482 1483 version(Windows) { 1484 HANDLE hConsole; 1485 CONSOLE_SCREEN_BUFFER_INFO originalSbi; 1486 } 1487 1488 version(Win32Console) 1489 /// ditto 1490 this(ConsoleOutputType type) { 1491 _initialized = true; 1492 if(UseVtSequences) { 1493 hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 1494 initializeVt(); 1495 } else { 1496 if(type == ConsoleOutputType.cellular) { 1497 goCellular(); 1498 } else { 1499 hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 1500 } 1501 1502 if(GetConsoleScreenBufferInfo(hConsole, &originalSbi) == 0) 1503 throw new Exception("not a user-interactive terminal"); 1504 1505 defaultForegroundColor = cast(Color) (originalSbi.wAttributes & 0x0f); 1506 defaultBackgroundColor = cast(Color) ((originalSbi.wAttributes >> 4) & 0x0f); 1507 1508 // this is unnecessary since I use the W versions of other functions 1509 // and can cause weird font bugs, so I'm commenting unless some other 1510 // need comes up. 1511 /* 1512 oldCp = GetConsoleOutputCP(); 1513 SetConsoleOutputCP(65001); // UTF-8 1514 1515 oldCpIn = GetConsoleCP(); 1516 SetConsoleCP(65001); // UTF-8 1517 */ 1518 } 1519 } 1520 1521 version(Win32Console) { 1522 private Color defaultBackgroundColor = Color.black; 1523 private Color defaultForegroundColor = Color.white; 1524 UINT oldCp; 1525 UINT oldCpIn; 1526 } 1527 1528 // 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... 1529 bool _suppressDestruction = false; 1530 1531 bool _initialized = false; // set to true for Terminal.init purposes, but ctors will set it to false initially, then might reset to true if needed 1532 1533 ~this() { 1534 if(!_initialized) 1535 return; 1536 1537 import core.memory; 1538 static if(is(typeof(GC.inFinalizer))) 1539 if(GC.inFinalizer) 1540 return; 1541 1542 if(_suppressDestruction) { 1543 flush(); 1544 return; 1545 } 1546 1547 if(UseVtSequences) { 1548 if(type == ConsoleOutputType.cellular) { 1549 goLinear(); 1550 } 1551 version(TerminalDirectToEmulator) { 1552 if(usingDirectEmulator) { 1553 1554 if(integratedTerminalEmulatorConfiguration.closeOnExit) { 1555 tew.parentWindow.close(); 1556 } else { 1557 writeln("\n\n<exited>"); 1558 setTitle(tew.terminalEmulator.currentTitle ~ " <exited>"); 1559 } 1560 1561 tew.term = null; 1562 } else { 1563 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 1564 writeStringRaw("\033[23;0t"); // restore window title from the stack 1565 } 1566 } 1567 } else 1568 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 1569 writeStringRaw("\033[23;0t"); // restore window title from the stack 1570 } 1571 cursor = TerminalCursor.DEFAULT; 1572 showCursor(); 1573 reset(); 1574 flush(); 1575 1576 if(lineGetter !is null) 1577 lineGetter.dispose(); 1578 } else version(Win32Console) { 1579 flush(); // make sure user data is all flushed before resetting 1580 reset(); 1581 showCursor(); 1582 1583 if(lineGetter !is null) 1584 lineGetter.dispose(); 1585 1586 1587 SetConsoleOutputCP(oldCp); 1588 SetConsoleCP(oldCpIn); 1589 1590 goLinear(); 1591 } 1592 1593 version(TerminalDirectToEmulator) 1594 if(usingDirectEmulator && guiThread !is null) { 1595 guiThread.join(); 1596 guiThread = null; 1597 } 1598 } 1599 1600 // lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit) 1601 // and some history storage. 1602 /++ 1603 The cached object used by [getline]. You can set it yourself if you like. 1604 1605 History: 1606 Documented `public` on December 25, 2020. 1607 +/ 1608 public LineGetter lineGetter; 1609 1610 int _currentForeground = Color.DEFAULT; 1611 int _currentBackground = Color.DEFAULT; 1612 RGB _currentForegroundRGB; 1613 RGB _currentBackgroundRGB; 1614 bool reverseVideo = false; 1615 1616 /++ 1617 Attempts to set color according to a 24 bit value (r, g, b, each >= 0 and < 256). 1618 1619 1620 This is not supported on all terminals. It will attempt to fall back to a 256-color 1621 or 8-color palette in those cases automatically. 1622 1623 Returns: true if it believes it was successful (note that it cannot be completely sure), 1624 false if it had to use a fallback. 1625 +/ 1626 bool setTrueColor(RGB foreground, RGB background, ForceOption force = ForceOption.automatic) { 1627 if(force == ForceOption.neverSend) { 1628 _currentForeground = -1; 1629 _currentBackground = -1; 1630 _currentForegroundRGB = foreground; 1631 _currentBackgroundRGB = background; 1632 return true; 1633 } 1634 1635 if(force == ForceOption.automatic && _currentForeground == -1 && _currentBackground == -1 && (_currentForegroundRGB == foreground && _currentBackgroundRGB == background)) 1636 return true; 1637 1638 _currentForeground = -1; 1639 _currentBackground = -1; 1640 _currentForegroundRGB = foreground; 1641 _currentBackgroundRGB = background; 1642 1643 version(Win32Console) { 1644 flush(); 1645 ushort setTob = cast(ushort) approximate16Color(background); 1646 ushort setTof = cast(ushort) approximate16Color(foreground); 1647 SetConsoleTextAttribute( 1648 hConsole, 1649 cast(ushort)((setTob << 4) | setTof)); 1650 return false; 1651 } else { 1652 // FIXME: if the terminal reliably does support 24 bit color, use it 1653 // instead of the round off. But idk how to detect that yet... 1654 1655 // fallback to 16 color for term that i know don't take it well 1656 import std.process; 1657 import std.string; 1658 version(TerminalDirectToEmulator) 1659 if(usingDirectEmulator) 1660 goto skip_approximation; 1661 1662 if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") { 1663 // not likely supported, use 16 color fallback 1664 auto setTof = approximate16Color(foreground); 1665 auto setTob = approximate16Color(background); 1666 1667 writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm", 1668 (setTof & Bright) ? 1 : 0, 1669 cast(int) (setTof & ~Bright), 1670 cast(int) (setTob & ~Bright) 1671 )); 1672 1673 return false; 1674 } 1675 1676 skip_approximation: 1677 1678 // otherwise, assume it is probably supported and give it a try 1679 writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm", 1680 colorToXTermPaletteIndex(foreground), 1681 colorToXTermPaletteIndex(background) 1682 )); 1683 1684 /+ // this is the full 24 bit color sequence 1685 writeStringRaw(format("\033[38;2;%d;%d;%dm", foreground.r, foreground.g, foreground.b)); 1686 writeStringRaw(format("\033[48;2;%d;%d;%dm", background.r, background.g, background.b)); 1687 +/ 1688 1689 return true; 1690 } 1691 } 1692 1693 /// Changes the current color. See enum [Color] for the values and note colors can be [arsd.docs.general_concepts#bitmasks|bitwise-or] combined with [Bright]. 1694 void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { 1695 if(force != ForceOption.neverSend) { 1696 version(Win32Console) { 1697 // assuming a dark background on windows, so LowContrast == dark which means the bit is NOT set on hardware 1698 /* 1699 foreground ^= LowContrast; 1700 background ^= LowContrast; 1701 */ 1702 1703 ushort setTof = cast(ushort) foreground; 1704 ushort setTob = cast(ushort) background; 1705 1706 // this isn't necessarily right but meh 1707 if(background == Color.DEFAULT) 1708 setTob = defaultBackgroundColor; 1709 if(foreground == Color.DEFAULT) 1710 setTof = defaultForegroundColor; 1711 1712 if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 1713 flush(); // if we don't do this now, the buffering can screw up the colors... 1714 if(reverseVideo) { 1715 if(background == Color.DEFAULT) 1716 setTof = defaultBackgroundColor; 1717 else 1718 setTof = cast(ushort) background | (foreground & Bright); 1719 1720 if(background == Color.DEFAULT) 1721 setTob = defaultForegroundColor; 1722 else 1723 setTob = cast(ushort) (foreground & ~Bright); 1724 } 1725 SetConsoleTextAttribute( 1726 hConsole, 1727 cast(ushort)((setTob << 4) | setTof)); 1728 } 1729 } else { 1730 import std.process; 1731 // I started using this envvar for my text editor, but now use it elsewhere too 1732 // if we aren't set to dark, assume light 1733 /* 1734 if(getenv("ELVISBG") == "dark") { 1735 // LowContrast on dark bg menas 1736 } else { 1737 foreground ^= LowContrast; 1738 background ^= LowContrast; 1739 } 1740 */ 1741 1742 ushort setTof = cast(ushort) foreground & ~Bright; 1743 ushort setTob = cast(ushort) background & ~Bright; 1744 1745 if(foreground & Color.DEFAULT) 1746 setTof = 9; // ansi sequence for reset 1747 if(background == Color.DEFAULT) 1748 setTob = 9; 1749 1750 import std.string; 1751 1752 if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 1753 writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm\033[%dm", 1754 (foreground != Color.DEFAULT && (foreground & Bright)) ? 1 : 0, 1755 cast(int) setTof, 1756 cast(int) setTob, 1757 reverseVideo ? 7 : 27 1758 )); 1759 } 1760 } 1761 } 1762 1763 _currentForeground = foreground; 1764 _currentBackground = background; 1765 this.reverseVideo = reverseVideo; 1766 } 1767 1768 private bool _underlined = false; 1769 private bool _bolded = false; 1770 private bool _italics = false; 1771 1772 /++ 1773 Outputs a hyperlink to my custom terminal (v0.0.7 or later) or to version 1774 `TerminalDirectToEmulator`. The way it works is a bit strange... 1775 1776 1777 If using a terminal that supports it, it outputs the given text with the 1778 given identifier attached (one bit of identifier per grapheme of text!). When 1779 the user clicks on it, it will send a [LinkEvent] with the text and the identifier 1780 for you to respond, if in real-time input mode, or a simple paste event with the 1781 text if not (you will not be able to distinguish this from a user pasting the 1782 same text). 1783 1784 If the user's terminal does not support my feature, it writes plain text instead. 1785 1786 It is important that you make sure your program still works even if the hyperlinks 1787 never work - ideally, make them out of text the user can type manually or copy/paste 1788 into your command line somehow too. 1789 1790 Hyperlinks may not work correctly after your program exits or if you are capturing 1791 mouse input (the user will have to hold shift in that case). It is really designed 1792 for linear mode with direct to emulator mode. If you are using cellular mode with 1793 full input capturing, you should manage the clicks yourself. 1794 1795 Similarly, if it horizontally scrolls off the screen, it can be corrupted since it 1796 packs your text and identifier into free bits in the screen buffer itself. I may be 1797 able to fix that later. 1798 1799 Params: 1800 text = text displayed in the terminal 1801 1802 identifier = an additional number attached to the text and returned to you in a [LinkEvent]. 1803 Possible uses of this are to have a small number of "link classes" that are handled based on 1804 the text. For example, maybe identifier == 0 means paste text into the line. identifier == 1 1805 could mean open a browser. identifier == 2 might open details for it. Just be sure to encode 1806 the bulk of the information into the text so the user can copy/paste it out too. 1807 1808 You may also create a mapping of (identifier,text) back to some other activity, but if you do 1809 that, be sure to check [hyperlinkSupported] and fallback in your own code so it still makes 1810 sense to users on other terminals. 1811 1812 autoStyle = set to `false` to suppress the automatic color and underlining of the text. 1813 1814 Bugs: 1815 there's no keyboard interaction with it at all right now. i might make the terminal 1816 emulator offer the ids or something through a hold ctrl or something interface. idk. 1817 or tap ctrl twice to turn that on. 1818 1819 History: 1820 Added March 18, 2020 1821 +/ 1822 void hyperlink(string text, ushort identifier = 0, bool autoStyle = true) { 1823 if((tcaps & TerminalCapabilities.arsdHyperlinks)) { 1824 bool previouslyUnderlined = _underlined; 1825 int fg = _currentForeground, bg = _currentBackground; 1826 if(autoStyle) { 1827 color(Color.blue, Color.white); 1828 underline = true; 1829 } 1830 1831 import std.conv; 1832 writeStringRaw("\033[?" ~ to!string(65536 + identifier) ~ "h"); 1833 write(text); 1834 writeStringRaw("\033[?65536l"); 1835 1836 if(autoStyle) { 1837 underline = previouslyUnderlined; 1838 color(fg, bg); 1839 } 1840 } else { 1841 write(text); // graceful degrade 1842 } 1843 } 1844 1845 /++ 1846 Returns true if the terminal advertised compatibility with the [hyperlink] function's 1847 implementation. 1848 1849 History: 1850 Added April 2, 2021 1851 +/ 1852 bool hyperlinkSupported() { 1853 if((tcaps & TerminalCapabilities.arsdHyperlinks)) { 1854 return true; 1855 } else { 1856 return false; 1857 } 1858 } 1859 1860 /++ 1861 Sets or resets the terminal's text rendering options. 1862 1863 Note: the Windows console does not support these and many Unix terminals don't either. 1864 Many will treat italic as blink and bold as brighter color. There is no way to know 1865 what will happen. So I don't recommend you use these in general. They don't even work 1866 with `-version=TerminalDirectToEmulator`. 1867 1868 History: 1869 underline was added in March 2020. italic and bold were added November 1, 2022 1870 1871 since they are unreliable, i didnt want to add these but did for some special requests. 1872 +/ 1873 void underline(bool set, ForceOption force = ForceOption.automatic) { 1874 if(set == _underlined && force != ForceOption.alwaysSend) 1875 return; 1876 if(UseVtSequences) { 1877 if(set) 1878 writeStringRaw("\033[4m"); 1879 else 1880 writeStringRaw("\033[24m"); 1881 } 1882 _underlined = set; 1883 } 1884 /// ditto 1885 void italic(bool set, ForceOption force = ForceOption.automatic) { 1886 if(set == _italics && force != ForceOption.alwaysSend) 1887 return; 1888 if(UseVtSequences) { 1889 if(set) 1890 writeStringRaw("\033[3m"); 1891 else 1892 writeStringRaw("\033[23m"); 1893 } 1894 _italics = set; 1895 } 1896 /// ditto 1897 void bold(bool set, ForceOption force = ForceOption.automatic) { 1898 if(set == _bolded && force != ForceOption.alwaysSend) 1899 return; 1900 if(UseVtSequences) { 1901 if(set) 1902 writeStringRaw("\033[1m"); 1903 else 1904 writeStringRaw("\033[22m"); 1905 } 1906 _bolded = set; 1907 } 1908 1909 // FIXME: implement this in arsd terminalemulator too 1910 // and make my vim use it. these are extensions in the iterm, etc 1911 /+ 1912 void setUnderlineColor(Color colorIndex) {} // 58;5;n 1913 void setUnderlineColor(int r, int g, int b) {} // 58;2;r;g;b 1914 void setDefaultUnderlineColor() {} // 59 1915 +/ 1916 1917 1918 1919 1920 1921 /// Returns the terminal to normal output colors 1922 void reset() { 1923 version(Win32Console) 1924 SetConsoleTextAttribute( 1925 hConsole, 1926 originalSbi.wAttributes); 1927 else 1928 writeStringRaw("\033[0m"); 1929 1930 _underlined = false; 1931 _italics = false; 1932 _bolded = false; 1933 _currentForeground = Color.DEFAULT; 1934 _currentBackground = Color.DEFAULT; 1935 reverseVideo = false; 1936 } 1937 1938 // FIXME: add moveRelative 1939 1940 /++ 1941 The current cached x and y positions of the output cursor. 0 == leftmost column for x and topmost row for y. 1942 1943 Please note that the cached position is not necessarily accurate. You may consider calling [updateCursorPosition] 1944 first to ask the terminal for its authoritative answer. 1945 +/ 1946 @property int cursorX() { 1947 return _cursorX; 1948 } 1949 1950 /// ditto 1951 @property int cursorY() { 1952 return _cursorY; 1953 } 1954 1955 private int _cursorX; 1956 private int _cursorY; 1957 1958 /// 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 1959 void moveTo(int x, int y, ForceOption force = ForceOption.automatic) { 1960 if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) { 1961 executeAutoHideCursor(); 1962 if(UseVtSequences) { 1963 doTermcap("cm", y, x); 1964 } else version(Win32Console) { 1965 1966 flush(); // if we don't do this now, the buffering can screw up the position 1967 COORD coord = {cast(short) x, cast(short) y}; 1968 SetConsoleCursorPosition(hConsole, coord); 1969 } 1970 } 1971 1972 _cursorX = x; 1973 _cursorY = y; 1974 } 1975 1976 /// shows the cursor 1977 void showCursor() { 1978 if(UseVtSequences) 1979 doTermcap("ve"); 1980 else version(Win32Console) { 1981 CONSOLE_CURSOR_INFO info; 1982 GetConsoleCursorInfo(hConsole, &info); 1983 info.bVisible = true; 1984 SetConsoleCursorInfo(hConsole, &info); 1985 } 1986 } 1987 1988 /// hides the cursor 1989 void hideCursor() { 1990 if(UseVtSequences) { 1991 doTermcap("vi"); 1992 } else version(Win32Console) { 1993 CONSOLE_CURSOR_INFO info; 1994 GetConsoleCursorInfo(hConsole, &info); 1995 info.bVisible = false; 1996 SetConsoleCursorInfo(hConsole, &info); 1997 } 1998 1999 } 2000 2001 private bool autoHidingCursor; 2002 private bool autoHiddenCursor; 2003 // explicitly not publicly documented 2004 // Sets the cursor to automatically insert a hide command at the front of the output buffer iff it is moved. 2005 // Call autoShowCursor when you are done with the batch update. 2006 void autoHideCursor() { 2007 autoHidingCursor = true; 2008 } 2009 2010 private void executeAutoHideCursor() { 2011 if(autoHidingCursor) { 2012 version(Win32Console) 2013 hideCursor(); 2014 else if(UseVtSequences) { 2015 // prepend the hide cursor command so it is the first thing flushed 2016 writeBuffer = "\033[?25l" ~ writeBuffer; 2017 } 2018 2019 autoHiddenCursor = true; 2020 autoHidingCursor = false; // already been done, don't insert the command again 2021 } 2022 } 2023 2024 // explicitly not publicly documented 2025 // Shows the cursor if it was automatically hidden by autoHideCursor and resets the internal auto hide state. 2026 void autoShowCursor() { 2027 if(autoHiddenCursor) 2028 showCursor(); 2029 2030 autoHidingCursor = false; 2031 autoHiddenCursor = false; 2032 } 2033 2034 /* 2035 // alas this doesn't work due to a bunch of delegate context pointer and postblit problems 2036 // instead of using: auto input = terminal.captureInput(flags) 2037 // use: auto input = RealTimeConsoleInput(&terminal, flags); 2038 /// Gets real time input, disabling line buffering 2039 RealTimeConsoleInput captureInput(ConsoleInputFlags flags) { 2040 return RealTimeConsoleInput(&this, flags); 2041 } 2042 */ 2043 2044 /// Changes the terminal's title 2045 void setTitle(string t) { 2046 version(Win32Console) { 2047 wchar[256] buffer; 2048 size_t bufferLength; 2049 foreach(wchar ch; t) 2050 if(bufferLength < buffer.length) 2051 buffer[bufferLength++] = ch; 2052 if(bufferLength < buffer.length) 2053 buffer[bufferLength++] = 0; 2054 else 2055 buffer[$-1] = 0; 2056 SetConsoleTitleW(buffer.ptr); 2057 } else { 2058 import std.string; 2059 if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) 2060 writeStringRaw(format("\033]0;%s\007", t)); 2061 } 2062 } 2063 2064 /// Flushes your updates to the terminal. 2065 /// It is important to call this when you are finished writing for now if you are using the version=with_eventloop 2066 void flush() { 2067 version(TerminalDirectToEmulator) 2068 if(windowGone) 2069 return; 2070 version(TerminalDirectToEmulator) 2071 if(pipeThroughStdOut) { 2072 fflush(stdout); 2073 fflush(stderr); 2074 return; 2075 } 2076 2077 if(writeBuffer.length == 0) 2078 return; 2079 2080 version(TerminalDirectToEmulator) { 2081 if(usingDirectEmulator) { 2082 tew.sendRawInput(cast(ubyte[]) writeBuffer); 2083 writeBuffer = null; 2084 } else { 2085 interiorFlush(); 2086 } 2087 } else { 2088 interiorFlush(); 2089 } 2090 } 2091 2092 private void interiorFlush() { 2093 version(Posix) { 2094 if(_writeDelegate !is null) { 2095 _writeDelegate(writeBuffer); 2096 } else { 2097 ssize_t written; 2098 2099 while(writeBuffer.length) { 2100 written = unix.write(this.fdOut, writeBuffer.ptr, writeBuffer.length); 2101 if(written < 0) { 2102 import core.stdc.errno; 2103 auto err = errno(); 2104 if(err == EAGAIN || err == EWOULDBLOCK) { 2105 import core.thread; 2106 Thread.sleep(1.msecs); 2107 continue; 2108 } 2109 throw new Exception("write failed for some reason"); 2110 } 2111 writeBuffer = writeBuffer[written .. $]; 2112 } 2113 } 2114 } else version(Win32Console) { 2115 import std.conv; 2116 // FIXME: I'm not sure I'm actually happy with this allocation but 2117 // it probably isn't a big deal. At least it has unicode support now. 2118 wstring writeBufferw = to!wstring(writeBuffer); 2119 while(writeBufferw.length) { 2120 DWORD written; 2121 WriteConsoleW(hConsole, writeBufferw.ptr, cast(DWORD)writeBufferw.length, &written, null); 2122 writeBufferw = writeBufferw[written .. $]; 2123 } 2124 2125 writeBuffer = null; 2126 } 2127 } 2128 2129 int[] getSize() { 2130 version(TerminalDirectToEmulator) { 2131 if(usingDirectEmulator) 2132 return [tew.terminalEmulator.width, tew.terminalEmulator.height]; 2133 else 2134 return getSizeInternal(); 2135 } else { 2136 return getSizeInternal(); 2137 } 2138 } 2139 2140 private int[] getSizeInternal() { 2141 version(Windows) { 2142 CONSOLE_SCREEN_BUFFER_INFO info; 2143 GetConsoleScreenBufferInfo( hConsole, &info ); 2144 2145 int cols, rows; 2146 2147 cols = (info.srWindow.Right - info.srWindow.Left + 1); 2148 rows = (info.srWindow.Bottom - info.srWindow.Top + 1); 2149 2150 return [cols, rows]; 2151 } else { 2152 if(getSizeOverride is null) { 2153 winsize w; 2154 ioctl(0, TIOCGWINSZ, &w); 2155 return [w.ws_col, w.ws_row]; 2156 } else return getSizeOverride(); 2157 } 2158 } 2159 2160 void updateSize() { 2161 auto size = getSize(); 2162 _width = size[0]; 2163 _height = size[1]; 2164 } 2165 2166 private int _width; 2167 private int _height; 2168 2169 /// The current width of the terminal (the number of columns) 2170 @property int width() { 2171 if(_width == 0 || _height == 0) 2172 updateSize(); 2173 return _width; 2174 } 2175 2176 /// The current height of the terminal (the number of rows) 2177 @property int height() { 2178 if(_width == 0 || _height == 0) 2179 updateSize(); 2180 return _height; 2181 } 2182 2183 /* 2184 void write(T...)(T t) { 2185 foreach(arg; t) { 2186 writeStringRaw(to!string(arg)); 2187 } 2188 } 2189 */ 2190 2191 /// Writes to the terminal at the current cursor position. 2192 void writef(T...)(string f, T t) { 2193 import std.string; 2194 writePrintableString(format(f, t)); 2195 } 2196 2197 /// ditto 2198 void writefln(T...)(string f, T t) { 2199 writef(f ~ "\n", t); 2200 } 2201 2202 /// ditto 2203 void write(T...)(T t) { 2204 import std.conv; 2205 string data; 2206 foreach(arg; t) { 2207 data ~= to!string(arg); 2208 } 2209 2210 writePrintableString(data); 2211 } 2212 2213 /// ditto 2214 void writeln(T...)(T t) { 2215 write(t, "\n"); 2216 } 2217 import std.uni; 2218 int[Grapheme] graphemeWidth; 2219 bool willInsertFollowingLine = false; 2220 bool uncertainIfAtEndOfLine = false; 2221 /+ 2222 /// A combined moveTo and writef that puts the cursor back where it was before when it finishes the write. 2223 /// Only works in cellular mode. 2224 /// 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) 2225 void writefAt(T...)(int x, int y, string f, T t) { 2226 import std.string; 2227 auto toWrite = format(f, t); 2228 2229 auto oldX = _cursorX; 2230 auto oldY = _cursorY; 2231 2232 writeAtWithoutReturn(x, y, toWrite); 2233 2234 moveTo(oldX, oldY); 2235 } 2236 2237 void writeAtWithoutReturn(int x, int y, in char[] data) { 2238 moveTo(x, y); 2239 writeStringRaw(toWrite, ForceOption.alwaysSend); 2240 } 2241 +/ 2242 void writePrintableString(const(char)[] s, ForceOption force = ForceOption.automatic) { 2243 while(s.length > 0) { 2244 size_t index = 0; 2245 import std.utf; 2246 import std.uni; 2247 import std.range; 2248 bool written=false; 2249 loop: 2250 foreach(g; s.byDchar.byGrapheme) { 2251 index += g[].byChar.walkLength; 2252 if(uncertainIfAtEndOfLine) { 2253 uncertainIfAtEndOfLine = false; 2254 writePrintableString_(s[0..index],force); 2255 written = true; 2256 updateCursorPosition(); 2257 break; 2258 } 2259 if(willInsertFollowingLine) { 2260 willInsertFollowingLine = false; 2261 _cursorX = 0; 2262 _cursorY++; 2263 if(_cursorY >= height) { _cursorY--; } 2264 } 2265 switch(g[0]) { 2266 case '\n': 2267 _cursorX = 0; 2268 _cursorY++; 2269 break; 2270 case '\r': 2271 _cursorX = 0; 2272 break; 2273 case '\t': 2274 // FIXME: get the actual tabstop, if possible 2275 int diff = 8 - (_cursorX % 8); 2276 if(diff == 0) 2277 diff = 8; 2278 _cursorX += diff; 2279 break; 2280 default: 2281 if(auto ptr = g in graphemeWidth) { 2282 _cursorX += *ptr; 2283 2284 if(_wrapAround) { 2285 if(_cursorX == width) { 2286 _cursorX--; 2287 willInsertFollowingLine = true; 2288 } 2289 else if(cursorX > width) { 2290 _cursorX = *ptr; 2291 _cursorY++; 2292 if(_cursorY == height) 2293 _cursorY--; 2294 } 2295 } 2296 2297 } 2298 else { 2299 int x = _cursorX; 2300 int y = _cursorY; 2301 writePrintableString_(s[0..index],force); 2302 written = true; 2303 updateCursorPosition(); 2304 //FIXME: At some point it might be worthwhile to check if the next wrtie will wrap on those terminals that support it 2305 //Note that: tmux (and screen?) seem to signal that the next write will wrap by reporting cursorX==width 2306 if (_cursorX+1< width || _cursorX == width) { 2307 graphemeWidth[g] = _cursorY > y 2308 ? _cursorX 2309 : _cursorX - x; 2310 if(_cursorX == width) { willInsertFollowingLine = true; } 2311 } 2312 else { uncertainIfAtEndOfLine = true; } 2313 break loop; 2314 } 2315 } 2316 } 2317 if(!written) { writePrintableString_(s[0..index],force); } 2318 s = s[index..$]; 2319 } 2320 } 2321 2322 void writePrintableString_(const(char)[] s, ForceOption force = ForceOption.automatic) { 2323 // an escape character is going to mess things up. Actually any non-printable character could, but meh 2324 // assert(s.indexOf("\033") == -1); 2325 2326 if(s.length == 0) 2327 return; 2328 2329 2330 version(TerminalDirectToEmulator) { 2331 // this breaks up extremely long output a little as an aid to the 2332 // gui thread; by breaking it up, it helps to avoid monopolizing the 2333 // event loop. Easier to do here than in the thread itself because 2334 // this one doesn't have escape sequences to break up so it avoids work. 2335 while(s.length) { 2336 auto len = s.length; 2337 if(len > 1024 * 32) { 2338 len = 1024 * 32; 2339 // get to the start of a utf-8 sequence. kidna sorta. 2340 while(len && (s[len] & 0x1000_0000)) 2341 len--; 2342 } 2343 auto next = s[0 .. len]; 2344 s = s[len .. $]; 2345 writeStringRaw(next); 2346 } 2347 } else { 2348 writeStringRaw(s); 2349 } 2350 } 2351 2352 /* private */ bool _wrapAround = true; 2353 2354 deprecated alias writePrintableString writeString; /// use write() or writePrintableString instead 2355 2356 private string writeBuffer; 2357 /++ 2358 Set this before you create any `Terminal`s if you want it to merge the C 2359 stdout and stderr streams into the GUI terminal window. It will always 2360 redirect stdout if this is set (you may want to check for existing redirections 2361 first before setting this, see [Terminal.stdoutIsTerminal]), and will redirect 2362 stderr as well if it is invalid or points to the parent terminal. 2363 2364 You must opt into this since it is globally invasive (changing the C handle 2365 can affect things across the program) and possibly buggy. It also will likely 2366 hurt the efficiency of embedded terminal output. 2367 2368 Please note that this is currently only available in with `TerminalDirectToEmulator` 2369 version enabled. 2370 2371 History: 2372 Added October 2, 2020. 2373 +/ 2374 version(TerminalDirectToEmulator) 2375 static shared(bool) pipeThroughStdOut = false; 2376 2377 /++ 2378 Options for [stderrBehavior]. Only applied if [pipeThroughStdOut] is set to `true` and its redirection actually is performed. 2379 +/ 2380 version(TerminalDirectToEmulator) 2381 enum StderrBehavior { 2382 sendToWindowIfNotAlreadyRedirected, /// If stderr does not exist or is pointing at a parent terminal, change it to point at the window alongside stdout (if stdout is changed by [pipeThroughStdOut]). 2383 neverSendToWindow, /// Tell this library to never redirect stderr. It will leave it alone. 2384 alwaysSendToWindow /// Always redirect stderr to the window through stdout if [pipeThroughStdOut] is set, even if it has already been redirected by the shell or code previously in your program. 2385 } 2386 2387 /++ 2388 If [pipeThroughStdOut] is set, this decides what happens to stderr. 2389 See: [StderrBehavior]. 2390 2391 History: 2392 Added October 3, 2020. 2393 +/ 2394 version(TerminalDirectToEmulator) 2395 static shared(StderrBehavior) stderrBehavior = StderrBehavior.sendToWindowIfNotAlreadyRedirected; 2396 2397 // you really, really shouldn't use this unless you know what you are doing 2398 /*private*/ void writeStringRaw(in char[] s) { 2399 version(TerminalDirectToEmulator) 2400 if(pipeThroughStdOut) { 2401 fwrite(s.ptr, 1, s.length, stdout); 2402 return; 2403 } 2404 2405 writeBuffer ~= s; // buffer it to do everything at once in flush() calls 2406 if(writeBuffer.length > 1024 * 32) 2407 flush(); 2408 } 2409 2410 2411 /// Clears the screen. 2412 void clear() { 2413 if(UseVtSequences) { 2414 doTermcap("cl"); 2415 } else version(Win32Console) { 2416 // http://support.microsoft.com/kb/99261 2417 flush(); 2418 2419 DWORD c; 2420 CONSOLE_SCREEN_BUFFER_INFO csbi; 2421 DWORD conSize; 2422 GetConsoleScreenBufferInfo(hConsole, &csbi); 2423 conSize = csbi.dwSize.X * csbi.dwSize.Y; 2424 COORD coordScreen; 2425 FillConsoleOutputCharacterA(hConsole, ' ', conSize, coordScreen, &c); 2426 FillConsoleOutputAttribute(hConsole, csbi.wAttributes, conSize, coordScreen, &c); 2427 moveTo(0, 0, ForceOption.alwaysSend); 2428 } 2429 2430 _cursorX = 0; 2431 _cursorY = 0; 2432 } 2433 2434 /++ 2435 Clears the current line from the cursor onwards. 2436 2437 History: 2438 Added January 25, 2023 (dub v11.0) 2439 +/ 2440 void clearToEndOfLine() { 2441 if(UseVtSequences) { 2442 writeStringRaw("\033[0K"); 2443 } 2444 else version(Windows) { 2445 updateCursorPosition(); 2446 auto x = _cursorX; 2447 auto y = _cursorY; 2448 DWORD c; 2449 CONSOLE_SCREEN_BUFFER_INFO csbi; 2450 DWORD conSize = width-x; 2451 GetConsoleScreenBufferInfo(hConsole, &csbi); 2452 auto coordScreen = COORD(cast(short) x, cast(short) y); 2453 FillConsoleOutputCharacterA(hConsole, ' ', conSize, coordScreen, &c); 2454 FillConsoleOutputAttribute(hConsole, csbi.wAttributes, conSize, coordScreen, &c); 2455 moveTo(x, y, ForceOption.alwaysSend); 2456 } 2457 } 2458 /++ 2459 Gets a line, including user editing. Convenience method around the [LineGetter] class and [RealTimeConsoleInput] facilities - use them if you need more control. 2460 2461 2462 $(TIP 2463 You can set the [lineGetter] member directly if you want things like stored history. 2464 2465 --- 2466 Terminal terminal = Terminal(ConsoleOutputType.linear); 2467 terminal.lineGetter = new LineGetter(&terminal, "my_history"); 2468 2469 auto line = terminal.getline("$ "); 2470 terminal.writeln(line); 2471 --- 2472 ) 2473 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. See [stdinIsTerminal]. 2474 2475 Params: 2476 prompt = the prompt to give the user. For example, `"Your name: "`. 2477 echoChar = the character to show back to the user as they type. The default value of `dchar.init` shows the user their own input back normally. Passing `0` here will disable echo entirely, like a Unix password prompt. Or you might also try `'*'` to do a password prompt that shows the number of characters input to the user. 2478 prefilledData = the initial data to populate the edit buffer 2479 2480 History: 2481 The `echoChar` parameter was added on October 11, 2021 (dub v10.4). 2482 2483 The `prompt` would not take effect if it was `null` prior to November 12, 2021. Before then, a `null` prompt would just leave the previous prompt string in place on the object. After that, the prompt is always set to the argument, including turning it off if you pass `null` (which is the default). 2484 2485 Always pass a string if you want it to display a string. 2486 2487 The `prefilledData` (and overload with it as second param) was added on January 1, 2023 (dub v10.10 / v11.0). 2488 +/ 2489 string getline(string prompt = null, dchar echoChar = dchar.init, string prefilledData = null) { 2490 if(lineGetter is null) 2491 lineGetter = new LineGetter(&this); 2492 // since the struct might move (it shouldn't, this should be unmovable!) but since 2493 // it technically might, I'm updating the pointer before using it just in case. 2494 lineGetter.terminal = &this; 2495 2496 auto ec = lineGetter.echoChar; 2497 auto p = lineGetter.prompt; 2498 scope(exit) { 2499 lineGetter.echoChar = ec; 2500 lineGetter.prompt = p; 2501 } 2502 lineGetter.echoChar = echoChar; 2503 2504 2505 lineGetter.prompt = prompt; 2506 if(prefilledData) { 2507 lineGetter.addString(prefilledData); 2508 lineGetter.maintainBuffer = true; 2509 } 2510 2511 auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw | ConsoleInputFlags.selectiveMouse | ConsoleInputFlags.paste | ConsoleInputFlags.size | ConsoleInputFlags.noEolWrap); 2512 auto line = lineGetter.getline(&input); 2513 2514 // lineGetter leaves us exactly where it was when the user hit enter, giving best 2515 // flexibility to real-time input and cellular programs. The convenience function, 2516 // however, wants to do what is right in most the simple cases, which is to actually 2517 // print the line (echo would be enabled without RealTimeConsoleInput anyway and they 2518 // did hit enter), so we'll do that here too. 2519 writePrintableString("\n"); 2520 2521 return line; 2522 } 2523 2524 /// ditto 2525 string getline(string prompt, string prefilledData, dchar echoChar = dchar.init) { 2526 return getline(prompt, echoChar, prefilledData); 2527 } 2528 2529 2530 /++ 2531 Forces [cursorX] and [cursorY] to resync from the terminal. 2532 2533 History: 2534 Added January 8, 2023 2535 +/ 2536 void updateCursorPosition() { 2537 auto terminal = &this; 2538 2539 terminal.flush(); 2540 2541 // then get the current cursor position to start fresh 2542 version(TerminalDirectToEmulator) { 2543 if(!terminal.usingDirectEmulator) 2544 return updateCursorPosition_impl(); 2545 2546 if(terminal.pipeThroughStdOut) { 2547 terminal.tew.terminalEmulator.waitingForInboundSync = true; 2548 terminal.writeStringRaw("\xff"); 2549 terminal.flush(); 2550 if(windowGone) forceTermination(); 2551 terminal.tew.terminalEmulator.syncSignal.wait(); 2552 } 2553 2554 terminal._cursorX = terminal.tew.terminalEmulator.cursorX; 2555 terminal._cursorY = terminal.tew.terminalEmulator.cursorY; 2556 } else 2557 updateCursorPosition_impl(); 2558 if(_cursorX == width) { 2559 willInsertFollowingLine = true; 2560 _cursorX--; 2561 } 2562 } 2563 private void updateCursorPosition_impl() { 2564 auto terminal = &this; 2565 version(Win32Console) { 2566 CONSOLE_SCREEN_BUFFER_INFO info; 2567 GetConsoleScreenBufferInfo(terminal.hConsole, &info); 2568 _cursorX = info.dwCursorPosition.X; 2569 _cursorY = info.dwCursorPosition.Y; 2570 } else version(Posix) { 2571 // request current cursor position 2572 2573 // we have to turn off cooked mode to get this answer, otherwise it will all 2574 // be messed up. (I hate unix terminals, the Windows way is so much easer.) 2575 2576 // We also can't use RealTimeConsoleInput here because it also does event loop stuff 2577 // which would be broken by the child destructor :( (maybe that should be a FIXME) 2578 2579 /+ 2580 if(rtci !is null) { 2581 while(rtci.timedCheckForInput_bypassingBuffer(1000)) 2582 rtci.inputQueue ~= rtci.readNextEvents(); 2583 } 2584 +/ 2585 2586 ubyte[128] hack2; 2587 termios old; 2588 ubyte[128] hack; 2589 tcgetattr(terminal.fdIn, &old); 2590 auto n = old; 2591 n.c_lflag &= ~(ICANON | ECHO); 2592 tcsetattr(terminal.fdIn, TCSANOW, &n); 2593 scope(exit) 2594 tcsetattr(terminal.fdIn, TCSANOW, &old); 2595 2596 2597 terminal.writeStringRaw("\033[6n"); 2598 terminal.flush(); 2599 2600 import std.conv; 2601 import core.stdc.errno; 2602 2603 import core.sys.posix.unistd; 2604 2605 ubyte readOne() { 2606 ubyte[1] buffer; 2607 int tries = 0; 2608 try_again: 2609 if(tries > 30) 2610 throw new Exception("terminal reply timed out"); 2611 auto len = read(terminal.fdIn, buffer.ptr, buffer.length); 2612 if(len == -1) { 2613 if(errno == EINTR) 2614 goto try_again; 2615 if(errno == EAGAIN || errno == EWOULDBLOCK) { 2616 import core.thread; 2617 Thread.sleep(10.msecs); 2618 tries++; 2619 goto try_again; 2620 } 2621 } else if(len == 0) { 2622 throw new Exception("Couldn't get cursor position to initialize get line " ~ to!string(len) ~ " " ~ to!string(errno)); 2623 } 2624 2625 return buffer[0]; 2626 } 2627 2628 nextEscape: 2629 while(readOne() != '\033') {} 2630 if(readOne() != '[') 2631 goto nextEscape; 2632 2633 int x, y; 2634 2635 // now we should have some numbers being like yyy;xxxR 2636 // but there may be a ? in there too; DEC private mode format 2637 // of the very same data. 2638 2639 x = 0; 2640 y = 0; 2641 2642 auto b = readOne(); 2643 2644 if(b == '?') 2645 b = readOne(); // no big deal, just ignore and continue 2646 2647 nextNumberY: 2648 if(b >= '0' && b <= '9') { 2649 y *= 10; 2650 y += b - '0'; 2651 } else goto nextEscape; 2652 2653 b = readOne(); 2654 if(b != ';') 2655 goto nextNumberY; 2656 2657 b = readOne(); 2658 nextNumberX: 2659 if(b >= '0' && b <= '9') { 2660 x *= 10; 2661 x += b - '0'; 2662 } else goto nextEscape; 2663 2664 b = readOne(); 2665 // another digit 2666 if(b >= '0' && b <= '9') 2667 goto nextNumberX; 2668 2669 if(b != 'R') 2670 goto nextEscape; // it wasn't the right thing it after all 2671 2672 _cursorX = x - 1; 2673 _cursorY = y - 1; 2674 } 2675 } 2676 } 2677 2678 /++ 2679 Removes terminal color, bold, etc. sequences from a string, 2680 making it plain text suitable for output to a normal .txt 2681 file. 2682 +/ 2683 inout(char)[] removeTerminalGraphicsSequences(inout(char)[] s) { 2684 import std.string; 2685 2686 // on old compilers, inout index of fails, but const works, so i'll just 2687 // cast it, this is ok since inout and const work the same regardless 2688 auto at = (cast(const(char)[])s).indexOf("\033["); 2689 if(at == -1) 2690 return s; 2691 2692 inout(char)[] ret; 2693 2694 do { 2695 ret ~= s[0 .. at]; 2696 s = s[at + 2 .. $]; 2697 while(s.length && !((s[0] >= 'a' && s[0] <= 'z') || s[0] >= 'A' && s[0] <= 'Z')) { 2698 s = s[1 .. $]; 2699 } 2700 if(s.length) 2701 s = s[1 .. $]; // skip the terminator 2702 at = (cast(const(char)[])s).indexOf("\033["); 2703 } while(at != -1); 2704 2705 ret ~= s; 2706 2707 return ret; 2708 } 2709 2710 unittest { 2711 assert("foo".removeTerminalGraphicsSequences == "foo"); 2712 assert("\033[34mfoo".removeTerminalGraphicsSequences == "foo"); 2713 assert("\033[34mfoo\033[39m".removeTerminalGraphicsSequences == "foo"); 2714 assert("\033[34m\033[45mfoo\033[39mbar\033[49m".removeTerminalGraphicsSequences == "foobar"); 2715 } 2716 2717 2718 /+ 2719 struct ConsoleBuffer { 2720 int cursorX; 2721 int cursorY; 2722 int width; 2723 int height; 2724 dchar[] data; 2725 2726 void actualize(Terminal* t) { 2727 auto writer = t.getBufferedWriter(); 2728 2729 this.copyTo(&(t.onScreen)); 2730 } 2731 2732 void copyTo(ConsoleBuffer* buffer) { 2733 buffer.cursorX = this.cursorX; 2734 buffer.cursorY = this.cursorY; 2735 buffer.width = this.width; 2736 buffer.height = this.height; 2737 buffer.data[] = this.data[]; 2738 } 2739 } 2740 +/ 2741 2742 /** 2743 * Encapsulates the stream of input events received from the terminal input. 2744 */ 2745 struct RealTimeConsoleInput { 2746 @disable this(); 2747 @disable this(this); 2748 2749 /++ 2750 Requests the system to send paste data as a [PasteEvent] to this stream, if possible. 2751 2752 See_Also: 2753 [Terminal.requestCopyToPrimary] 2754 [Terminal.requestCopyToClipboard] 2755 [Terminal.clipboardSupported] 2756 2757 History: 2758 Added February 17, 2020. 2759 2760 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. 2761 +/ 2762 void requestPasteFromClipboard() { 2763 version(Win32Console) { 2764 HWND hwndOwner = null; 2765 if(OpenClipboard(hwndOwner) == 0) 2766 throw new Exception("OpenClipboard"); 2767 scope(exit) 2768 CloseClipboard(); 2769 if(auto dataHandle = GetClipboardData(CF_UNICODETEXT)) { 2770 2771 if(auto data = cast(wchar*) GlobalLock(dataHandle)) { 2772 scope(exit) 2773 GlobalUnlock(dataHandle); 2774 2775 int len = 0; 2776 auto d = data; 2777 while(*d) { 2778 d++; 2779 len++; 2780 } 2781 string s; 2782 s.reserve(len); 2783 foreach(idx, dchar ch; data[0 .. len]) { 2784 // CR/LF -> LF 2785 if(ch == '\r' && idx + 1 < len && data[idx + 1] == '\n') 2786 continue; 2787 s ~= ch; 2788 } 2789 2790 injectEvent(InputEvent(PasteEvent(s), terminal), InjectionPosition.tail); 2791 } 2792 } 2793 } else 2794 if(terminal.clipboardSupported) { 2795 if(UseVtSequences) 2796 terminal.writeStringRaw("\033]52;c;?\007"); 2797 } 2798 } 2799 2800 /// ditto 2801 void requestPasteFromPrimary() { 2802 if(terminal.clipboardSupported) { 2803 if(UseVtSequences) 2804 terminal.writeStringRaw("\033]52;p;?\007"); 2805 } 2806 } 2807 2808 private bool utf8MouseMode; 2809 2810 version(Posix) { 2811 private int fdOut; 2812 private int fdIn; 2813 private sigaction_t oldSigWinch; 2814 private sigaction_t oldSigIntr; 2815 private sigaction_t oldHupIntr; 2816 private sigaction_t oldContIntr; 2817 private termios old; 2818 ubyte[128] hack; 2819 // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... 2820 // tcgetattr smashed other variables in here too that could create random problems 2821 // so this hack is just to give some room for that to happen without destroying the rest of the world 2822 } 2823 2824 version(Windows) { 2825 private DWORD oldInput; 2826 private DWORD oldOutput; 2827 HANDLE inputHandle; 2828 } 2829 2830 private ConsoleInputFlags flags; 2831 private Terminal* terminal; 2832 private void function(RealTimeConsoleInput*)[] destructor; 2833 2834 version(Posix) 2835 private bool reinitializeAfterSuspend() { 2836 version(TerminalDirectToEmulator) { 2837 if(terminal.usingDirectEmulator) 2838 return false; 2839 } 2840 2841 // copy/paste from posixInit but with private old 2842 if(fdIn != -1) { 2843 termios old; 2844 ubyte[128] hack; 2845 2846 tcgetattr(fdIn, &old); 2847 auto n = old; 2848 2849 auto f = ICANON; 2850 if(!(flags & ConsoleInputFlags.echo)) 2851 f |= ECHO; 2852 2853 n.c_lflag &= ~f; 2854 tcsetattr(fdIn, TCSANOW, &n); 2855 2856 // ensure these are still blocking after the resumption 2857 import core.sys.posix.fcntl; 2858 if(fdIn != -1) { 2859 auto ctl = fcntl(fdIn, F_GETFL); 2860 ctl &= ~O_NONBLOCK; 2861 fcntl(fdIn, F_SETFL, ctl); 2862 } 2863 if(fdOut != -1) { 2864 auto ctl = fcntl(fdOut, F_GETFL); 2865 ctl &= ~O_NONBLOCK; 2866 fcntl(fdOut, F_SETFL, ctl); 2867 } 2868 } 2869 2870 // copy paste from constructor, but not setting the destructor teardown since that's already done 2871 if(flags & ConsoleInputFlags.selectiveMouse) { 2872 terminal.writeStringRaw("\033[?1014h"); 2873 } else if(flags & ConsoleInputFlags.mouse) { 2874 terminal.writeStringRaw("\033[?1000h"); 2875 import std.process : environment; 2876 2877 if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { 2878 terminal.writeStringRaw("\033[?1003h\033[?1005h"); // full mouse tracking (1003) with utf-8 mode (1005) for exceedingly large terminals 2879 utf8MouseMode = true; 2880 } else if(terminal.terminalInFamily("rxvt", "screen", "tmux") || environment.get("MOUSE_HACK") == "1002") { 2881 terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed 2882 } 2883 } 2884 if(flags & ConsoleInputFlags.paste) { 2885 if(terminal.terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 2886 terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode 2887 } 2888 } 2889 2890 if(terminal.tcaps & TerminalCapabilities.arsdHyperlinks) { 2891 terminal.writeStringRaw("\033[?3004h"); // bracketed link mode 2892 } 2893 2894 // try to ensure the terminal is in UTF-8 mode 2895 if(terminal.terminalInFamily("xterm", "screen", "linux", "tmux") && !terminal.isMacTerminal()) { 2896 terminal.writeStringRaw("\033%G"); 2897 } 2898 2899 terminal.flush(); 2900 2901 // returning true will send a resize event as well, which does the rest of the catch up and redraw as necessary 2902 return true; 2903 } 2904 2905 /// To capture input, you need to provide a terminal and some flags. 2906 public this(Terminal* terminal, ConsoleInputFlags flags) { 2907 createLock(); 2908 _initialized = true; 2909 this.flags = flags; 2910 this.terminal = terminal; 2911 2912 version(Windows) { 2913 inputHandle = GetStdHandle(STD_INPUT_HANDLE); 2914 2915 } 2916 2917 version(Win32Console) { 2918 2919 GetConsoleMode(inputHandle, &oldInput); 2920 2921 DWORD mode = 0; 2922 //mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C and automatic paste... which we probably want to be similar to linux 2923 //if(flags & ConsoleInputFlags.size) 2924 mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc 2925 if(flags & ConsoleInputFlags.echo) 2926 mode |= ENABLE_ECHO_INPUT; // 0x4 2927 if(flags & ConsoleInputFlags.mouse) 2928 mode |= ENABLE_MOUSE_INPUT; // 0x10 2929 // if(flags & ConsoleInputFlags.raw) // FIXME: maybe that should be a separate flag for ENABLE_LINE_INPUT 2930 2931 SetConsoleMode(inputHandle, mode); 2932 destructor ~= (this_) { SetConsoleMode(this_.inputHandle, this_.oldInput); }; 2933 2934 2935 GetConsoleMode(terminal.hConsole, &oldOutput); 2936 mode = 0; 2937 // we want this to match linux too 2938 mode |= ENABLE_PROCESSED_OUTPUT; /* 0x01 */ 2939 if(!(flags & ConsoleInputFlags.noEolWrap)) 2940 mode |= ENABLE_WRAP_AT_EOL_OUTPUT; /* 0x02 */ 2941 SetConsoleMode(terminal.hConsole, mode); 2942 destructor ~= (this_) { SetConsoleMode(this_.terminal.hConsole, this_.oldOutput); }; 2943 } 2944 2945 version(TerminalDirectToEmulator) { 2946 if(terminal.usingDirectEmulator) 2947 terminal.tew.terminalEmulator.echo = (flags & ConsoleInputFlags.echo) ? true : false; 2948 else version(Posix) 2949 posixInit(); 2950 } else version(Posix) { 2951 posixInit(); 2952 } 2953 2954 if(UseVtSequences) { 2955 2956 2957 if(flags & ConsoleInputFlags.selectiveMouse) { 2958 // arsd terminal extension, but harmless on most other terminals 2959 terminal.writeStringRaw("\033[?1014h"); 2960 destructor ~= (this_) { this_.terminal.writeStringRaw("\033[?1014l"); }; 2961 } else if(flags & ConsoleInputFlags.mouse) { 2962 // basic button press+release notification 2963 2964 // FIXME: try to get maximum capabilities from all terminals 2965 // right now this works well on xterm but rxvt isn't sending movements... 2966 2967 terminal.writeStringRaw("\033[?1000h"); 2968 destructor ~= (this_) { this_.terminal.writeStringRaw("\033[?1000l"); }; 2969 // the MOUSE_HACK env var is for the case where I run screen 2970 // but set TERM=xterm (which I do from putty). The 1003 mouse mode 2971 // doesn't work there, breaking mouse support entirely. So by setting 2972 // MOUSE_HACK=1002 it tells us to use the other mode for a fallback. 2973 import std.process : environment; 2974 2975 if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { 2976 // this is vt200 mouse with full motion tracking, supported by xterm 2977 terminal.writeStringRaw("\033[?1003h\033[?1005h"); 2978 utf8MouseMode = true; 2979 destructor ~= (this_) { this_.terminal.writeStringRaw("\033[?1005l\033[?1003l"); }; 2980 } else if(terminal.terminalInFamily("rxvt", "screen", "tmux") || environment.get("MOUSE_HACK") == "1002") { 2981 terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed 2982 destructor ~= (this_) { this_.terminal.writeStringRaw("\033[?1002l"); }; 2983 } 2984 } 2985 if(flags & ConsoleInputFlags.paste) { 2986 if(terminal.terminalInFamily("xterm", "rxvt", "screen", "tmux")) { 2987 terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode 2988 destructor ~= (this_) { this_.terminal.writeStringRaw("\033[?2004l"); }; 2989 } 2990 } 2991 2992 if(terminal.tcaps & TerminalCapabilities.arsdHyperlinks) { 2993 terminal.writeStringRaw("\033[?3004h"); // bracketed link mode 2994 destructor ~= (this_) { this_.terminal.writeStringRaw("\033[?3004l"); }; 2995 } 2996 2997 // try to ensure the terminal is in UTF-8 mode 2998 if(terminal.terminalInFamily("xterm", "screen", "linux", "tmux") && !terminal.isMacTerminal()) { 2999 terminal.writeStringRaw("\033%G"); 3000 } 3001 3002 terminal.flush(); 3003 } 3004 3005 3006 version(with_eventloop) { 3007 import arsd.eventloop; 3008 version(Win32Console) { 3009 static HANDLE listenTo; 3010 listenTo = inputHandle; 3011 } else version(Posix) { 3012 // total hack but meh i only ever use this myself 3013 static int listenTo; 3014 listenTo = this.fdIn; 3015 } else static assert(0, "idk about this OS"); 3016 3017 version(Posix) 3018 addListener(&signalFired); 3019 3020 if(listenTo != -1) { 3021 addFileEventListeners(listenTo, &eventListener, null, null); 3022 destructor ~= (this_) { removeFileEventListeners(listenTo); }; 3023 } 3024 addOnIdle(&terminal.flush); 3025 destructor ~= (this_) { removeOnIdle(&this_.terminal.flush); }; 3026 } 3027 } 3028 3029 version(Posix) 3030 private void posixInit() { 3031 this.fdIn = terminal.fdIn; 3032 this.fdOut = terminal.fdOut; 3033 3034 // if a naughty program changes the mode on these to nonblocking 3035 // and doesn't change them back, it can cause trouble to us here. 3036 // so i explicitly set the blocking flag since EAGAIN is not as nice 3037 // for my purposes (it isn't consistently handled well in here) 3038 import core.sys.posix.fcntl; 3039 { 3040 auto ctl = fcntl(fdIn, F_GETFL); 3041 ctl &= ~O_NONBLOCK; 3042 fcntl(fdIn, F_SETFL, ctl); 3043 } 3044 { 3045 auto ctl = fcntl(fdOut, F_GETFL); 3046 ctl &= ~O_NONBLOCK; 3047 fcntl(fdOut, F_SETFL, ctl); 3048 } 3049 3050 if(fdIn != -1) { 3051 tcgetattr(fdIn, &old); 3052 auto n = old; 3053 3054 auto f = ICANON; 3055 if(!(flags & ConsoleInputFlags.echo)) 3056 f |= ECHO; 3057 3058 // \033Z or \033[c 3059 3060 n.c_lflag &= ~f; 3061 tcsetattr(fdIn, TCSANOW, &n); 3062 } 3063 3064 // some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3 3065 //destructor ~= { tcsetattr(fdIn, TCSANOW, &old); }; 3066 3067 if(flags & ConsoleInputFlags.size) { 3068 import core.sys.posix.signal; 3069 sigaction_t n; 3070 n.sa_handler = &sizeSignalHandler; 3071 n.sa_mask = cast(sigset_t) 0; 3072 n.sa_flags = 0; 3073 sigaction(SIGWINCH, &n, &oldSigWinch); 3074 } 3075 3076 { 3077 import core.sys.posix.signal; 3078 sigaction_t n; 3079 n.sa_handler = &interruptSignalHandler; 3080 n.sa_mask = cast(sigset_t) 0; 3081 n.sa_flags = 0; 3082 sigaction(SIGINT, &n, &oldSigIntr); 3083 } 3084 3085 { 3086 import core.sys.posix.signal; 3087 sigaction_t n; 3088 n.sa_handler = &hangupSignalHandler; 3089 n.sa_mask = cast(sigset_t) 0; 3090 n.sa_flags = 0; 3091 sigaction(SIGHUP, &n, &oldHupIntr); 3092 } 3093 3094 { 3095 import core.sys.posix.signal; 3096 sigaction_t n; 3097 n.sa_handler = &continueSignalHandler; 3098 n.sa_mask = cast(sigset_t) 0; 3099 n.sa_flags = 0; 3100 sigaction(SIGCONT, &n, &oldContIntr); 3101 } 3102 3103 } 3104 3105 void fdReadyReader() { 3106 auto queue = readNextEvents(); 3107 foreach(event; queue) 3108 userEventHandler(event); 3109 } 3110 3111 void delegate(InputEvent) userEventHandler; 3112 3113 /++ 3114 If you are using [arsd.simpledisplay] and want terminal interop too, you can call 3115 this function to add it to the sdpy event loop and get the callback called on new 3116 input. 3117 3118 Note that you will probably need to call `terminal.flush()` when you are doing doing 3119 output, as the sdpy event loop doesn't know to do that (yet). I will probably change 3120 that in a future version, but it doesn't hurt to call it twice anyway, so I recommend 3121 calling flush yourself in any code you write using this. 3122 +/ 3123 auto integrateWithSimpleDisplayEventLoop()(void delegate(InputEvent) userEventHandler) { 3124 this.userEventHandler = userEventHandler; 3125 import arsd.simpledisplay; 3126 version(Win32Console) 3127 auto listener = new WindowsHandleReader(&fdReadyReader, terminal.hConsole); 3128 else version(linux) 3129 auto listener = new PosixFdReader(&fdReadyReader, fdIn); 3130 else static assert(0, "sdpy event loop integration not implemented on this platform"); 3131 3132 return listener; 3133 } 3134 3135 version(with_eventloop) { 3136 version(Posix) 3137 void signalFired(SignalFired) { 3138 if(interrupted) { 3139 interrupted = false; 3140 send(InputEvent(UserInterruptionEvent(), terminal)); 3141 } 3142 if(windowSizeChanged) 3143 send(checkWindowSizeChanged()); 3144 if(hangedUp) { 3145 hangedUp = false; 3146 send(InputEvent(HangupEvent(), terminal)); 3147 } 3148 } 3149 3150 import arsd.eventloop; 3151 void eventListener(OsFileHandle fd) { 3152 auto queue = readNextEvents(); 3153 foreach(event; queue) 3154 send(event); 3155 } 3156 } 3157 3158 bool _suppressDestruction; 3159 bool _initialized = false; 3160 3161 ~this() { 3162 if(!_initialized) 3163 return; 3164 import core.memory; 3165 static if(is(typeof(GC.inFinalizer))) 3166 if(GC.inFinalizer) 3167 return; 3168 3169 if(_suppressDestruction) 3170 return; 3171 3172 // the delegate thing doesn't actually work for this... for some reason 3173 3174 version(TerminalDirectToEmulator) { 3175 if(terminal && terminal.usingDirectEmulator) 3176 goto skip_extra; 3177 } 3178 3179 version(Posix) { 3180 if(fdIn != -1) 3181 tcsetattr(fdIn, TCSANOW, &old); 3182 3183 if(flags & ConsoleInputFlags.size) { 3184 // restoration 3185 sigaction(SIGWINCH, &oldSigWinch, null); 3186 } 3187 sigaction(SIGINT, &oldSigIntr, null); 3188 sigaction(SIGHUP, &oldHupIntr, null); 3189 sigaction(SIGCONT, &oldContIntr, null); 3190 } 3191 3192 skip_extra: 3193 3194 // we're just undoing everything the constructor did, in reverse order, same criteria 3195 foreach_reverse(d; destructor) 3196 d(&this); 3197 } 3198 3199 /** 3200 Returns true if there iff getch() would not block. 3201 3202 WARNING: kbhit might consume input that would be ignored by getch. This 3203 function is really only meant to be used in conjunction with getch. Typically, 3204 you should use a full-fledged event loop if you want all kinds of input. kbhit+getch 3205 are just for simple keyboard driven applications. 3206 */ 3207 bool kbhit() { 3208 auto got = getch(true); 3209 3210 if(got == dchar.init) 3211 return false; 3212 3213 getchBuffer = got; 3214 return true; 3215 } 3216 3217 /// Check for input, waiting no longer than the number of milliseconds. Note that this doesn't necessarily mean [getch] will not block, use this AND [kbhit] for that case. 3218 bool timedCheckForInput(int milliseconds) { 3219 if(inputQueue.length || timedCheckForInput_bypassingBuffer(milliseconds)) 3220 return true; 3221 version(WithEncapsulatedSignals) 3222 if(terminal.interrupted || terminal.windowSizeChanged || terminal.hangedUp) 3223 return true; 3224 version(WithSignals) 3225 if(interrupted || windowSizeChanged || hangedUp) 3226 return true; 3227 return false; 3228 } 3229 3230 /* private */ bool anyInput_internal(int timeout = 0) { 3231 return timedCheckForInput(timeout); 3232 } 3233 3234 bool timedCheckForInput_bypassingBuffer(int milliseconds) { 3235 version(TerminalDirectToEmulator) { 3236 if(!terminal.usingDirectEmulator) 3237 return timedCheckForInput_bypassingBuffer_impl(milliseconds); 3238 3239 import core.time; 3240 if(terminal.tew.terminalEmulator.pendingForApplication.length) 3241 return true; 3242 if(windowGone) forceTermination(); 3243 if(terminal.tew.terminalEmulator.outgoingSignal.wait(milliseconds.msecs)) 3244 // it was notified, but it could be left over from stuff we 3245 // already processed... so gonna check the blocking conditions here too 3246 // (FIXME: this sucks and is surely a race condition of pain) 3247 return terminal.tew.terminalEmulator.pendingForApplication.length || terminal.interrupted || terminal.windowSizeChanged || terminal.hangedUp; 3248 else 3249 return false; 3250 } else 3251 return timedCheckForInput_bypassingBuffer_impl(milliseconds); 3252 } 3253 3254 private bool timedCheckForInput_bypassingBuffer_impl(int milliseconds) { 3255 version(Windows) { 3256 auto response = WaitForSingleObject(inputHandle, milliseconds); 3257 if(response == 0) 3258 return true; // the object is ready 3259 return false; 3260 } else version(Posix) { 3261 if(fdIn == -1) 3262 return false; 3263 3264 timeval tv; 3265 tv.tv_sec = 0; 3266 tv.tv_usec = milliseconds * 1000; 3267 3268 fd_set fs; 3269 FD_ZERO(&fs); 3270 3271 FD_SET(fdIn, &fs); 3272 int tries = 0; 3273 try_again: 3274 auto ret = select(fdIn + 1, &fs, null, null, &tv); 3275 if(ret == -1) { 3276 import core.stdc.errno; 3277 if(errno == EINTR) { 3278 tries++; 3279 if(tries < 3) 3280 goto try_again; 3281 } 3282 return false; 3283 } 3284 if(ret == 0) 3285 return false; 3286 3287 return FD_ISSET(fdIn, &fs); 3288 } 3289 } 3290 3291 private dchar getchBuffer; 3292 3293 /// Get one key press from the terminal, discarding other 3294 /// events in the process. Returns dchar.init upon receiving end-of-file. 3295 /// 3296 /// 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. 3297 dchar getch(bool nonblocking = false) { 3298 if(getchBuffer != dchar.init) { 3299 auto a = getchBuffer; 3300 getchBuffer = dchar.init; 3301 return a; 3302 } 3303 3304 if(nonblocking && !anyInput_internal()) 3305 return dchar.init; 3306 3307 auto event = nextEvent(); 3308 while(event.type != InputEvent.Type.KeyboardEvent || event.keyboardEvent.pressed == false) { 3309 if(event.type == InputEvent.Type.UserInterruptionEvent) 3310 throw new UserInterruptionException(); 3311 if(event.type == InputEvent.Type.HangupEvent) 3312 throw new HangupException(); 3313 if(event.type == InputEvent.Type.EndOfFileEvent) 3314 return dchar.init; 3315 3316 if(nonblocking && !anyInput_internal()) 3317 return dchar.init; 3318 3319 event = nextEvent(); 3320 } 3321 return event.keyboardEvent.which; 3322 } 3323 3324 //char[128] inputBuffer; 3325 //int inputBufferPosition; 3326 int nextRaw(bool interruptable = false) { 3327 version(TerminalDirectToEmulator) { 3328 if(!terminal.usingDirectEmulator) 3329 return nextRaw_impl(interruptable); 3330 moar: 3331 //if(interruptable && inputQueue.length) 3332 //return -1; 3333 if(terminal.tew.terminalEmulator.pendingForApplication.length == 0) { 3334 if(windowGone) forceTermination(); 3335 terminal.tew.terminalEmulator.outgoingSignal.wait(); 3336 } 3337 synchronized(terminal.tew.terminalEmulator) { 3338 if(terminal.tew.terminalEmulator.pendingForApplication.length == 0) { 3339 if(interruptable) 3340 return -1; 3341 else 3342 goto moar; 3343 } 3344 auto a = terminal.tew.terminalEmulator.pendingForApplication[0]; 3345 terminal.tew.terminalEmulator.pendingForApplication = terminal.tew.terminalEmulator.pendingForApplication[1 .. $]; 3346 return a; 3347 } 3348 } else { 3349 auto got = nextRaw_impl(interruptable); 3350 if(got == int.min && !interruptable) 3351 throw new Exception("eof found in non-interruptable context"); 3352 // import std.stdio; writeln(cast(int) got); 3353 return got; 3354 } 3355 } 3356 private int nextRaw_impl(bool interruptable = false) { 3357 version(Posix) { 3358 if(fdIn == -1) 3359 return 0; 3360 3361 char[1] buf; 3362 try_again: 3363 auto ret = read(fdIn, buf.ptr, buf.length); 3364 if(ret == 0) 3365 return int.min; // input closed 3366 if(ret == -1) { 3367 import core.stdc.errno; 3368 if(errno == EINTR) { 3369 // interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop 3370 if(interruptable) 3371 return -1; 3372 else 3373 goto try_again; 3374 } else if(errno == EAGAIN || errno == EWOULDBLOCK) { 3375 // I turn off O_NONBLOCK explicitly in setup, but 3376 // still just in case, let's keep this working too 3377 import core.thread; 3378 Thread.sleep(1.msecs); 3379 goto try_again; 3380 } else { 3381 import std.conv; 3382 throw new Exception("read failed " ~ to!string(errno)); 3383 } 3384 } 3385 3386 //terminal.writef("RAW READ: %d\n", buf[0]); 3387 3388 if(ret == 1) 3389 return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; 3390 else 3391 assert(0); // read too much, should be impossible 3392 } else version(Windows) { 3393 char[1] buf; 3394 DWORD d; 3395 import std.conv; 3396 if(!ReadFile(inputHandle, buf.ptr, cast(int) buf.length, &d, null)) 3397 throw new Exception("ReadFile " ~ to!string(GetLastError())); 3398 if(d == 0) 3399 return int.min; 3400 return buf[0]; 3401 } 3402 } 3403 3404 version(Posix) 3405 int delegate(char) inputPrefilter; 3406 3407 // for VT 3408 dchar nextChar(int starting) { 3409 if(starting <= 127) 3410 return cast(dchar) starting; 3411 char[6] buffer; 3412 int pos = 0; 3413 buffer[pos++] = cast(char) starting; 3414 3415 // see the utf-8 encoding for details 3416 int remaining = 0; 3417 ubyte magic = starting & 0xff; 3418 while(magic & 0b1000_000) { 3419 remaining++; 3420 magic <<= 1; 3421 } 3422 3423 while(remaining && pos < buffer.length) { 3424 buffer[pos++] = cast(char) nextRaw(); 3425 remaining--; 3426 } 3427 3428 import std.utf; 3429 size_t throwAway; // it insists on the index but we don't care 3430 return decode(buffer[], throwAway); 3431 } 3432 3433 InputEvent checkWindowSizeChanged() { 3434 auto oldWidth = terminal.width; 3435 auto oldHeight = terminal.height; 3436 terminal.updateSize(); 3437 version(WithSignals) 3438 windowSizeChanged = false; 3439 version(WithEncapsulatedSignals) 3440 terminal.windowSizeChanged = false; 3441 return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); 3442 } 3443 3444 3445 // character event 3446 // non-character key event 3447 // paste event 3448 // mouse event 3449 // size event maybe, and if appropriate focus events 3450 3451 /// Returns the next event. 3452 /// 3453 /// Experimental: It is also possible to integrate this into 3454 /// a generic event loop, currently under -version=with_eventloop and it will 3455 /// require the module arsd.eventloop (Linux only at this point) 3456 InputEvent nextEvent() { 3457 terminal.flush(); 3458 3459 wait_for_more: 3460 version(WithSignals) { 3461 if(interrupted) { 3462 interrupted = false; 3463 return InputEvent(UserInterruptionEvent(), terminal); 3464 } 3465 3466 if(hangedUp) { 3467 hangedUp = false; 3468 return InputEvent(HangupEvent(), terminal); 3469 } 3470 3471 if(windowSizeChanged) { 3472 return checkWindowSizeChanged(); 3473 } 3474 3475 if(continuedFromSuspend) { 3476 continuedFromSuspend = false; 3477 if(reinitializeAfterSuspend()) 3478 return checkWindowSizeChanged(); // while it was suspended it is possible the window got resized, so we'll check that, and sending this event also triggers a redraw on most programs too which is also convenient for getting them caught back up to the screen 3479 else 3480 goto wait_for_more; 3481 } 3482 } 3483 3484 version(WithEncapsulatedSignals) { 3485 if(terminal.interrupted) { 3486 terminal.interrupted = false; 3487 return InputEvent(UserInterruptionEvent(), terminal); 3488 } 3489 3490 if(terminal.hangedUp) { 3491 terminal.hangedUp = false; 3492 return InputEvent(HangupEvent(), terminal); 3493 } 3494 3495 if(terminal.windowSizeChanged) { 3496 return checkWindowSizeChanged(); 3497 } 3498 } 3499 3500 mutex.lock(); 3501 if(inputQueue.length) { 3502 auto e = inputQueue[0]; 3503 inputQueue = inputQueue[1 .. $]; 3504 mutex.unlock(); 3505 return e; 3506 } 3507 mutex.unlock(); 3508 3509 auto more = readNextEvents(); 3510 if(!more.length) 3511 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 3512 3513 assert(more.length); 3514 3515 auto e = more[0]; 3516 mutex.lock(); scope(exit) mutex.unlock(); 3517 inputQueue = more[1 .. $]; 3518 return e; 3519 } 3520 3521 InputEvent* peekNextEvent() { 3522 mutex.lock(); scope(exit) mutex.unlock(); 3523 if(inputQueue.length) 3524 return &(inputQueue[0]); 3525 return null; 3526 } 3527 3528 3529 import core.sync.mutex; 3530 private shared(Mutex) mutex; 3531 3532 private void createLock() { 3533 if(mutex is null) 3534 mutex = new shared Mutex; 3535 } 3536 enum InjectionPosition { head, tail } 3537 3538 /++ 3539 Injects a custom event into the terminal input queue. 3540 3541 History: 3542 `shared` overload added November 24, 2021 (dub v10.4) 3543 Bugs: 3544 Unless using `TerminalDirectToEmulator`, this will not wake up the 3545 event loop if it is already blocking until normal terminal input 3546 arrives anyway, then the event will be processed before the new event. 3547 3548 I might change this later. 3549 +/ 3550 void injectEvent(CustomEvent ce) shared { 3551 (cast() this).injectEvent(InputEvent(ce, cast(Terminal*) terminal), InjectionPosition.tail); 3552 3553 version(TerminalDirectToEmulator) { 3554 if(terminal.usingDirectEmulator) { 3555 (cast(Terminal*) terminal).tew.terminalEmulator.outgoingSignal.notify(); 3556 return; 3557 } 3558 } 3559 // FIXME: for the others, i might need to wake up the WaitForSingleObject or select calls. 3560 } 3561 3562 void injectEvent(InputEvent ev, InjectionPosition where) { 3563 mutex.lock(); scope(exit) mutex.unlock(); 3564 final switch(where) { 3565 case InjectionPosition.head: 3566 inputQueue = ev ~ inputQueue; 3567 break; 3568 case InjectionPosition.tail: 3569 inputQueue ~= ev; 3570 break; 3571 } 3572 } 3573 3574 InputEvent[] inputQueue; 3575 3576 InputEvent[] readNextEvents() { 3577 if(UseVtSequences) 3578 return readNextEventsVt(); 3579 else version(Win32Console) 3580 return readNextEventsWin32(); 3581 else 3582 assert(0); 3583 } 3584 3585 version(Win32Console) 3586 InputEvent[] readNextEventsWin32() { 3587 terminal.flush(); // make sure all output is sent out before waiting for anything 3588 3589 INPUT_RECORD[32] buffer; 3590 DWORD actuallyRead; 3591 auto success = ReadConsoleInputW(inputHandle, buffer.ptr, buffer.length, &actuallyRead); 3592 //import std.stdio; writeln(buffer[0 .. actuallyRead][0].KeyEvent, cast(int) buffer[0].KeyEvent.UnicodeChar); 3593 if(success == 0) 3594 throw new Exception("ReadConsoleInput"); 3595 3596 InputEvent[] newEvents; 3597 input_loop: foreach(record; buffer[0 .. actuallyRead]) { 3598 switch(record.EventType) { 3599 case KEY_EVENT: 3600 auto ev = record.KeyEvent; 3601 KeyboardEvent ke; 3602 CharacterEvent e; 3603 NonCharacterKeyEvent ne; 3604 3605 ke.pressed = ev.bKeyDown ? true : false; 3606 3607 // only send released events when specifically requested 3608 // terminal.writefln("got %s %s", ev.UnicodeChar, ev.bKeyDown); 3609 if(ev.UnicodeChar && ev.wVirtualKeyCode == VK_MENU && ev.bKeyDown == 0) { 3610 // this indicates Windows is actually sending us 3611 // an alt+xxx key sequence, may also be a unicode paste. 3612 // either way, it cool. 3613 ke.pressed = true; 3614 } else { 3615 if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) 3616 break; 3617 } 3618 3619 if(ev.UnicodeChar == 0 && ev.wVirtualKeyCode == VK_SPACE && ev.bKeyDown == 1) { 3620 ke.which = 0; 3621 ke.modifierState = ev.dwControlKeyState; 3622 newEvents ~= InputEvent(ke, terminal); 3623 continue; 3624 } 3625 3626 e.eventType = ke.pressed ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; 3627 ne.eventType = ke.pressed ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released; 3628 3629 e.modifierState = ev.dwControlKeyState; 3630 ne.modifierState = ev.dwControlKeyState; 3631 ke.modifierState = ev.dwControlKeyState; 3632 3633 if(ev.UnicodeChar) { 3634 // new style event goes first 3635 3636 if(ev.UnicodeChar == 3) { 3637 // handling this internally for linux compat too 3638 newEvents ~= InputEvent(UserInterruptionEvent(), terminal); 3639 } else if(ev.UnicodeChar == '\r') { 3640 // translating \r to \n for same result as linux... 3641 ke.which = cast(dchar) cast(wchar) '\n'; 3642 newEvents ~= InputEvent(ke, terminal); 3643 3644 // old style event then follows as the fallback 3645 e.character = cast(dchar) cast(wchar) '\n'; 3646 newEvents ~= InputEvent(e, terminal); 3647 } else if(ev.wVirtualKeyCode == 0x1b) { 3648 ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 3649 newEvents ~= InputEvent(ke, terminal); 3650 3651 ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; 3652 newEvents ~= InputEvent(ne, terminal); 3653 } else { 3654 ke.which = cast(dchar) cast(wchar) ev.UnicodeChar; 3655 newEvents ~= InputEvent(ke, terminal); 3656 3657 // old style event then follows as the fallback 3658 e.character = cast(dchar) cast(wchar) ev.UnicodeChar; 3659 newEvents ~= InputEvent(e, terminal); 3660 } 3661 } else { 3662 // old style event 3663 ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; 3664 3665 // new style event. See comment on KeyboardEvent.Key 3666 ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 3667 3668 // FIXME: make this better. the goal is to make sure the key code is a valid enum member 3669 // Windows sends more keys than Unix and we're doing lowest common denominator here 3670 foreach(member; __traits(allMembers, NonCharacterKeyEvent.Key)) 3671 if(__traits(getMember, NonCharacterKeyEvent.Key, member) == ne.key) { 3672 newEvents ~= InputEvent(ke, terminal); 3673 newEvents ~= InputEvent(ne, terminal); 3674 break; 3675 } 3676 } 3677 break; 3678 case MOUSE_EVENT: 3679 auto ev = record.MouseEvent; 3680 MouseEvent e; 3681 3682 e.modifierState = ev.dwControlKeyState; 3683 e.x = ev.dwMousePosition.X; 3684 e.y = ev.dwMousePosition.Y; 3685 3686 switch(ev.dwEventFlags) { 3687 case 0: 3688 //press or release 3689 e.eventType = MouseEvent.Type.Pressed; 3690 static DWORD lastButtonState; 3691 auto lastButtonState2 = lastButtonState; 3692 e.buttons = ev.dwButtonState; 3693 lastButtonState = e.buttons; 3694 3695 // this is sent on state change. if fewer buttons are pressed, it must mean released 3696 if(cast(DWORD) e.buttons < lastButtonState2) { 3697 e.eventType = MouseEvent.Type.Released; 3698 // if last was 101 and now it is 100, then button far right was released 3699 // so we flip the bits, ~100 == 011, then and them: 101 & 011 == 001, the 3700 // button that was released 3701 e.buttons = lastButtonState2 & ~e.buttons; 3702 } 3703 break; 3704 case MOUSE_MOVED: 3705 e.eventType = MouseEvent.Type.Moved; 3706 e.buttons = ev.dwButtonState; 3707 break; 3708 case 0x0004/*MOUSE_WHEELED*/: 3709 e.eventType = MouseEvent.Type.Pressed; 3710 if(ev.dwButtonState > 0) 3711 e.buttons = MouseEvent.Button.ScrollDown; 3712 else 3713 e.buttons = MouseEvent.Button.ScrollUp; 3714 break; 3715 default: 3716 continue input_loop; 3717 } 3718 3719 newEvents ~= InputEvent(e, terminal); 3720 break; 3721 case WINDOW_BUFFER_SIZE_EVENT: 3722 auto ev = record.WindowBufferSizeEvent; 3723 auto oldWidth = terminal.width; 3724 auto oldHeight = terminal.height; 3725 terminal._width = ev.dwSize.X; 3726 terminal._height = ev.dwSize.Y; 3727 newEvents ~= InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); 3728 break; 3729 // FIXME: can we catch ctrl+c here too? 3730 default: 3731 // ignore 3732 } 3733 } 3734 3735 return newEvents; 3736 } 3737 3738 // for UseVtSequences.... 3739 InputEvent[] readNextEventsVt() { 3740 terminal.flush(); // make sure all output is sent out before we try to get input 3741 3742 // we want to starve the read, especially if we're called from an edge-triggered 3743 // epoll (which might happen in version=with_eventloop.. impl detail there subject 3744 // to change). 3745 auto initial = readNextEventsHelper(); 3746 3747 // lol this calls select() inside a function prolly called from epoll but meh, 3748 // it is the simplest thing that can possibly work. The alternative would be 3749 // doing non-blocking reads and buffering in the nextRaw function (not a bad idea 3750 // btw, just a bit more of a hassle). 3751 while(timedCheckForInput_bypassingBuffer(0)) { 3752 auto ne = readNextEventsHelper(); 3753 initial ~= ne; 3754 foreach(n; ne) 3755 if(n.type == InputEvent.Type.EndOfFileEvent || n.type == InputEvent.Type.HangupEvent) 3756 return initial; // hit end of file, get out of here lest we infinite loop 3757 // (select still returns info available even after we read end of file) 3758 } 3759 return initial; 3760 } 3761 3762 // The helper reads just one actual event from the pipe... 3763 // for UseVtSequences.... 3764 InputEvent[] readNextEventsHelper(int remainingFromLastTime = int.max) { 3765 bool maybeTranslateCtrl(ref dchar c) { 3766 import std.algorithm : canFind; 3767 // map anything in the range of [1, 31] to C-lowercase character 3768 // except backspace (^h), tab (^i), linefeed (^j), carriage return (^m), and esc (^[) 3769 // \a, \v (lol), and \f are also 'special', but not worthwhile to special-case here 3770 if(1 <= c && c <= 31 3771 && !"\b\t\n\r\x1b"d.canFind(c)) 3772 { 3773 // I'm versioning this out because it is a breaking change. Maybe can come back to it later. 3774 version(terminal_translate_ctl) { 3775 c += 'a' - 1; 3776 } 3777 return true; 3778 } 3779 return false; 3780 } 3781 InputEvent[] charPressAndRelease(dchar character, uint modifiers = 0) { 3782 if(maybeTranslateCtrl(character)) 3783 modifiers |= ModifierState.control; 3784 if((flags & ConsoleInputFlags.releasedKeys)) 3785 return [ 3786 // new style event 3787 InputEvent(KeyboardEvent(true, character, modifiers), terminal), 3788 InputEvent(KeyboardEvent(false, character, modifiers), terminal), 3789 // old style event 3790 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, modifiers), terminal), 3791 InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, modifiers), terminal), 3792 ]; 3793 else return [ 3794 // new style event 3795 InputEvent(KeyboardEvent(true, character, modifiers), terminal), 3796 // old style event 3797 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, modifiers), terminal) 3798 ]; 3799 } 3800 InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { 3801 if((flags & ConsoleInputFlags.releasedKeys)) 3802 return [ 3803 // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! 3804 InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), 3805 InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal), 3806 // old style event 3807 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal), 3808 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal), 3809 ]; 3810 else return [ 3811 // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! 3812 InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), 3813 // old style event 3814 InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal) 3815 ]; 3816 } 3817 3818 InputEvent[] keyPressAndRelease2(dchar c, uint modifiers = 0) { 3819 if((flags & ConsoleInputFlags.releasedKeys)) 3820 return [ 3821 InputEvent(KeyboardEvent(true, c, modifiers), terminal), 3822 InputEvent(KeyboardEvent(false, c, modifiers), terminal), 3823 // old style event 3824 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, c, modifiers), terminal), 3825 InputEvent(CharacterEvent(CharacterEvent.Type.Released, c, modifiers), terminal), 3826 ]; 3827 else return [ 3828 InputEvent(KeyboardEvent(true, c, modifiers), terminal), 3829 // old style event 3830 InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, c, modifiers), terminal) 3831 ]; 3832 3833 } 3834 3835 char[30] sequenceBuffer; 3836 3837 // this assumes you just read "\033[" 3838 char[] readEscapeSequence(char[] sequence) { 3839 int sequenceLength = 2; 3840 sequence[0] = '\033'; 3841 sequence[1] = '['; 3842 3843 while(sequenceLength < sequence.length) { 3844 auto n = nextRaw(); 3845 sequence[sequenceLength++] = cast(char) n; 3846 // I think a [ is supposed to termiate a CSI sequence 3847 // but the Linux console sends CSI[A for F1, so I'm 3848 // hacking it to accept that too 3849 if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) 3850 break; 3851 } 3852 3853 return sequence[0 .. sequenceLength]; 3854 } 3855 3856 InputEvent[] translateTermcapName(string cap) { 3857 switch(cap) { 3858 //case "k0": 3859 //return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 3860 case "k1": 3861 return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 3862 case "k2": 3863 return keyPressAndRelease(NonCharacterKeyEvent.Key.F2); 3864 case "k3": 3865 return keyPressAndRelease(NonCharacterKeyEvent.Key.F3); 3866 case "k4": 3867 return keyPressAndRelease(NonCharacterKeyEvent.Key.F4); 3868 case "k5": 3869 return keyPressAndRelease(NonCharacterKeyEvent.Key.F5); 3870 case "k6": 3871 return keyPressAndRelease(NonCharacterKeyEvent.Key.F6); 3872 case "k7": 3873 return keyPressAndRelease(NonCharacterKeyEvent.Key.F7); 3874 case "k8": 3875 return keyPressAndRelease(NonCharacterKeyEvent.Key.F8); 3876 case "k9": 3877 return keyPressAndRelease(NonCharacterKeyEvent.Key.F9); 3878 case "k;": 3879 case "k0": 3880 return keyPressAndRelease(NonCharacterKeyEvent.Key.F10); 3881 case "F1": 3882 return keyPressAndRelease(NonCharacterKeyEvent.Key.F11); 3883 case "F2": 3884 return keyPressAndRelease(NonCharacterKeyEvent.Key.F12); 3885 3886 3887 case "kb": 3888 return charPressAndRelease('\b'); 3889 case "kD": 3890 return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete); 3891 3892 case "kd": 3893 case "do": 3894 return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow); 3895 case "ku": 3896 case "up": 3897 return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow); 3898 case "kl": 3899 return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow); 3900 case "kr": 3901 case "nd": 3902 return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow); 3903 3904 case "kN": 3905 case "K5": 3906 return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown); 3907 case "kP": 3908 case "K2": 3909 return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp); 3910 3911 case "ho": // this might not be a key but my thing sometimes returns it... weird... 3912 case "kh": 3913 case "K1": 3914 return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); 3915 case "kH": 3916 return keyPressAndRelease(NonCharacterKeyEvent.Key.End); 3917 case "kI": 3918 return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); 3919 default: 3920 // don't know it, just ignore 3921 //import std.stdio; 3922 //terminal.writeln(cap); 3923 } 3924 3925 return null; 3926 } 3927 3928 3929 InputEvent[] doEscapeSequence(in char[] sequence) { 3930 switch(sequence) { 3931 case "\033[200~": 3932 // bracketed paste begin 3933 // we want to keep reading until 3934 // "\033[201~": 3935 // and build a paste event out of it 3936 3937 3938 string data; 3939 for(;;) { 3940 auto n = nextRaw(); 3941 if(n == '\033') { 3942 n = nextRaw(); 3943 if(n == '[') { 3944 auto esc = readEscapeSequence(sequenceBuffer); 3945 if(esc == "\033[201~") { 3946 // complete! 3947 break; 3948 } else { 3949 // was something else apparently, but it is pasted, so keep it 3950 data ~= esc; 3951 } 3952 } else { 3953 data ~= '\033'; 3954 data ~= cast(char) n; 3955 } 3956 } else { 3957 data ~= cast(char) n; 3958 } 3959 } 3960 return [InputEvent(PasteEvent(data), terminal)]; 3961 case "\033[220~": 3962 // bracketed hyperlink begin (arsd extension) 3963 3964 string data; 3965 for(;;) { 3966 auto n = nextRaw(); 3967 if(n == '\033') { 3968 n = nextRaw(); 3969 if(n == '[') { 3970 auto esc = readEscapeSequence(sequenceBuffer); 3971 if(esc == "\033[221~") { 3972 // complete! 3973 break; 3974 } else { 3975 // was something else apparently, but it is pasted, so keep it 3976 data ~= esc; 3977 } 3978 } else { 3979 data ~= '\033'; 3980 data ~= cast(char) n; 3981 } 3982 } else { 3983 data ~= cast(char) n; 3984 } 3985 } 3986 3987 import std.string, std.conv; 3988 auto idx = data.indexOf(";"); 3989 auto id = data[0 .. idx].to!ushort; 3990 data = data[idx + 1 .. $]; 3991 idx = data.indexOf(";"); 3992 auto cmd = data[0 .. idx].to!ushort; 3993 data = data[idx + 1 .. $]; 3994 3995 return [InputEvent(LinkEvent(data, id, cmd), terminal)]; 3996 case "\033[M": 3997 // mouse event 3998 auto buttonCode = nextRaw() - 32; 3999 // nextChar is commented because i'm not using UTF-8 mouse mode 4000 // cuz i don't think it is as widely supported 4001 int x; 4002 int y; 4003 4004 if(utf8MouseMode) { 4005 x = cast(int) nextChar(nextRaw()) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */ 4006 y = cast(int) nextChar(nextRaw()) - 33; /* ditto */ 4007 } else { 4008 x = cast(int) (/*nextChar*/(nextRaw())) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */ 4009 y = cast(int) (/*nextChar*/(nextRaw())) - 33; /* ditto */ 4010 } 4011 4012 4013 bool isRelease = (buttonCode & 0b11) == 3; 4014 int buttonNumber; 4015 if(!isRelease) { 4016 buttonNumber = (buttonCode & 0b11); 4017 if(buttonCode & 64) 4018 buttonNumber += 3; // button 4 and 5 are sent as like button 1 and 2, but code | 64 4019 // so button 1 == button 4 here 4020 4021 // note: buttonNumber == 0 means button 1 at this point 4022 buttonNumber++; // hence this 4023 4024 4025 // apparently this considers middle to be button 2. but i want middle to be button 3. 4026 if(buttonNumber == 2) 4027 buttonNumber = 3; 4028 else if(buttonNumber == 3) 4029 buttonNumber = 2; 4030 } 4031 4032 auto modifiers = buttonCode & (0b0001_1100); 4033 // 4 == shift 4034 // 8 == meta 4035 // 16 == control 4036 4037 MouseEvent m; 4038 4039 if(buttonCode & 32) 4040 m.eventType = MouseEvent.Type.Moved; 4041 else 4042 m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; 4043 4044 // ugh, if no buttons are pressed, released and moved are indistinguishable... 4045 // so we'll count the buttons down, and if we get a release 4046 static int buttonsDown = 0; 4047 if(!isRelease && buttonNumber <= 3) // exclude wheel "presses"... 4048 buttonsDown++; 4049 4050 if(isRelease && m.eventType != MouseEvent.Type.Moved) { 4051 if(buttonsDown) 4052 buttonsDown--; 4053 else // no buttons down, so this should be a motion instead.. 4054 m.eventType = MouseEvent.Type.Moved; 4055 } 4056 4057 4058 if(buttonNumber == 0) 4059 m.buttons = 0; // we don't actually know :( 4060 else 4061 m.buttons = 1 << (buttonNumber - 1); // I prefer flags so that's how we do it 4062 m.x = x; 4063 m.y = y; 4064 m.modifierState = modifiers; 4065 4066 return [InputEvent(m, terminal)]; 4067 default: 4068 // screen doesn't actually do the modifiers, but 4069 // it uses the same format so this branch still works fine. 4070 if(terminal.terminalInFamily("xterm", "screen", "tmux")) { 4071 import std.conv, std.string; 4072 auto terminator = sequence[$ - 1]; 4073 auto parts = sequence[2 .. $ - 1].split(";"); 4074 // parts[0] and terminator tells us the key 4075 // parts[1] tells us the modifierState 4076 4077 uint modifierState; 4078 4079 int keyGot; 4080 4081 int modGot; 4082 if(parts.length > 1) 4083 modGot = to!int(parts[1]); 4084 if(parts.length > 2) 4085 keyGot = to!int(parts[2]); 4086 mod_switch: switch(modGot) { 4087 case 2: modifierState |= ModifierState.shift; break; 4088 case 3: modifierState |= ModifierState.alt; break; 4089 case 4: modifierState |= ModifierState.shift | ModifierState.alt; break; 4090 case 5: modifierState |= ModifierState.control; break; 4091 case 6: modifierState |= ModifierState.shift | ModifierState.control; break; 4092 case 7: modifierState |= ModifierState.alt | ModifierState.control; break; 4093 case 8: modifierState |= ModifierState.shift | ModifierState.alt | ModifierState.control; break; 4094 case 9: 4095 .. 4096 case 16: 4097 modifierState |= ModifierState.meta; 4098 if(modGot != 9) { 4099 modGot -= 8; 4100 goto mod_switch; 4101 } 4102 break; 4103 4104 // this is an extension in my own terminal emulator 4105 case 20: 4106 .. 4107 case 36: 4108 modifierState |= ModifierState.windows; 4109 modGot -= 20; 4110 goto mod_switch; 4111 default: 4112 } 4113 4114 switch(terminator) { 4115 case 'A': return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow, modifierState); 4116 case 'B': return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow, modifierState); 4117 case 'C': return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow, modifierState); 4118 case 'D': return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow, modifierState); 4119 4120 case 'H': return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); 4121 case 'F': return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); 4122 4123 case 'P': return keyPressAndRelease(NonCharacterKeyEvent.Key.F1, modifierState); 4124 case 'Q': return keyPressAndRelease(NonCharacterKeyEvent.Key.F2, modifierState); 4125 case 'R': return keyPressAndRelease(NonCharacterKeyEvent.Key.F3, modifierState); 4126 case 'S': return keyPressAndRelease(NonCharacterKeyEvent.Key.F4, modifierState); 4127 4128 case '~': // others 4129 switch(parts[0]) { 4130 case "1": return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); 4131 case "4": return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); 4132 case "5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp, modifierState); 4133 case "6": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown, modifierState); 4134 case "2": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert, modifierState); 4135 case "3": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete, modifierState); 4136 4137 case "15": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5, modifierState); 4138 case "17": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6, modifierState); 4139 case "18": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7, modifierState); 4140 case "19": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8, modifierState); 4141 case "20": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9, modifierState); 4142 case "21": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10, modifierState); 4143 case "23": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11, modifierState); 4144 case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); 4145 4146 // xterm extension for arbitrary keys with arbitrary modifiers 4147 case "27": return keyPressAndRelease2(keyGot == '\x1b' ? KeyboardEvent.Key.escape : keyGot, modifierState); 4148 4149 // starting at 70 im free to do my own but i rolled all but ScrollLock into 27 as of Dec 3, 2020 4150 case "70": return keyPressAndRelease(NonCharacterKeyEvent.Key.ScrollLock, modifierState); 4151 default: 4152 } 4153 break; 4154 4155 default: 4156 } 4157 } else if(terminal.terminalInFamily("rxvt")) { 4158 // look it up in the termcap key database 4159 string cap = terminal.findSequenceInTermcap(sequence); 4160 if(cap !is null) { 4161 //terminal.writeln("found in termcap " ~ cap); 4162 return translateTermcapName(cap); 4163 } 4164 // FIXME: figure these out. rxvt seems to just change the terminator while keeping the rest the same 4165 // though it isn't consistent. ugh. 4166 } else { 4167 // 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 4168 // so this space is semi-intentionally left blank 4169 //terminal.writeln("wtf ", sequence[1..$]); 4170 4171 // look it up in the termcap key database 4172 string cap = terminal.findSequenceInTermcap(sequence); 4173 if(cap !is null) { 4174 //terminal.writeln("found in termcap " ~ cap); 4175 return translateTermcapName(cap); 4176 } 4177 } 4178 } 4179 4180 return null; 4181 } 4182 4183 auto c = remainingFromLastTime == int.max ? nextRaw(true) : remainingFromLastTime; 4184 if(c == -1) 4185 return null; // interrupted; give back nothing so the other level can recheck signal flags 4186 // 0 conflicted with ctrl+space, so I have to use int.min to indicate eof 4187 if(c == int.min) 4188 return [InputEvent(EndOfFileEvent(), terminal)]; 4189 if(c == '\033') { 4190 if(!timedCheckForInput_bypassingBuffer(50)) { 4191 // user hit escape (or super slow escape sequence, but meh) 4192 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); 4193 } 4194 // escape sequence 4195 c = nextRaw(); 4196 if(c == '[') { // CSI, ends on anything >= 'A' 4197 return doEscapeSequence(readEscapeSequence(sequenceBuffer)); 4198 } else if(c == 'O') { 4199 // could be xterm function key 4200 auto n = nextRaw(); 4201 4202 char[3] thing; 4203 thing[0] = '\033'; 4204 thing[1] = 'O'; 4205 thing[2] = cast(char) n; 4206 4207 auto cap = terminal.findSequenceInTermcap(thing); 4208 if(cap is null) { 4209 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ 4210 charPressAndRelease('O') ~ 4211 charPressAndRelease(thing[2]); 4212 } else { 4213 return translateTermcapName(cap); 4214 } 4215 } else if(c == '\033') { 4216 // could be escape followed by an escape sequence! 4217 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ readNextEventsHelper(c); 4218 } else { 4219 // exceedingly quick esc followed by char is also what many terminals do for alt 4220 return charPressAndRelease(nextChar(c), cast(uint)ModifierState.alt); 4221 } 4222 } else { 4223 // FIXME: what if it is neither? we should check the termcap 4224 auto next = nextChar(c); 4225 if(next == 127) // some terminals send 127 on the backspace. Let's normalize that. 4226 next = '\b'; 4227 return charPressAndRelease(next); 4228 } 4229 } 4230 } 4231 4232 /++ 4233 The new style of keyboard event 4234 4235 Worth noting some special cases terminals tend to do: 4236 4237 $(LIST 4238 * Ctrl+space bar sends char 0. 4239 * Ctrl+ascii characters send char 1 - 26 as chars on all systems. Ctrl+shift+ascii is generally not recognizable on Linux, but works on Windows and with my terminal emulator on all systems. Alt+ctrl+ascii, for example Alt+Ctrl+F, is sometimes sent as modifierState = alt|ctrl, key = 'f'. Sometimes modifierState = alt|ctrl, key = 'F'. Sometimes modifierState = ctrl|alt, key = 6. Which one you get depends on the system/terminal and the user's caps lock state. You're probably best off checking all three and being aware it might not work at all. 4240 * Some combinations like ctrl+i are indistinguishable from other keys like tab. 4241 * Other modifier+key combinations may send random other things or not be detected as it is configuration-specific with no way to detect. It is reasonably reliable for the non-character keys (arrows, F1-F12, Home/End, etc.) but not perfectly so. Some systems just don't send them. If they do though, terminal will try to set `modifierState`. 4242 * Alt+key combinations do not generally work on Windows since the operating system uses that combination for something else. The events may come to you, but it may also go to the window menu or some other operation too. In fact, it might do both! 4243 * Shift is sometimes applied to the character, sometimes set in modifierState, sometimes both, sometimes neither. 4244 * On some systems, the return key sends \r and some sends \n. 4245 ) 4246 +/ 4247 struct KeyboardEvent { 4248 bool pressed; /// 4249 dchar which; /// 4250 alias key = which; /// I often use this when porting old to new so i took it 4251 alias character = which; /// I often use this when porting old to new so i took it 4252 uint modifierState; /// 4253 4254 // filter irrelevant modifiers... 4255 uint modifierStateFiltered() const { 4256 uint ms = modifierState; 4257 if(which < 32 && which != 9 && which != 8 && which != '\n') 4258 ms &= ~ModifierState.control; 4259 return ms; 4260 } 4261 4262 /++ 4263 Returns true if the event was a normal typed character. 4264 4265 You may also want to check modifiers if you want to process things differently when alt, ctrl, or shift is pressed. 4266 [modifierStateFiltered] returns only modifiers that are special in some way for the typed character. You can bitwise 4267 and that against [ModifierState]'s members to test. 4268 4269 [isUnmodifiedCharacter] does such a check for you. 4270 4271 $(NOTE 4272 Please note that enter, tab, and backspace count as characters. 4273 ) 4274 +/ 4275 bool isCharacter() { 4276 return !isNonCharacterKey() && !isProprietary(); 4277 } 4278 4279 /++ 4280 Returns true if this keyboard event represents a normal character keystroke, with no extraordinary modifier keys depressed. 4281 4282 Shift is considered an ordinary modifier except in the cases of tab, backspace, enter, and the space bar, since it is a normal 4283 part of entering many other characters. 4284 4285 History: 4286 Added December 4, 2020. 4287 +/ 4288 bool isUnmodifiedCharacter() { 4289 uint modsInclude = ModifierState.control | ModifierState.alt | ModifierState.meta; 4290 if(which == '\b' || which == '\t' || which == '\n' || which == '\r' || which == ' ' || which == 0) 4291 modsInclude |= ModifierState.shift; 4292 return isCharacter() && (modifierStateFiltered() & modsInclude) == 0; 4293 } 4294 4295 /++ 4296 Returns true if the key represents one of the range named entries in the [Key] enum. 4297 This does not necessarily mean it IS one of the named entries, just that it is in the 4298 range. Checking more precisely would require a loop in here and you are better off doing 4299 that in your own `switch` statement, with a do-nothing `default`. 4300 4301 Remember that users can create synthetic input of any character value. 4302 4303 History: 4304 While this function was present before, it was undocumented until December 4, 2020. 4305 +/ 4306 bool isNonCharacterKey() { 4307 return which >= Key.min && which <= Key.max; 4308 } 4309 4310 /// 4311 bool isProprietary() { 4312 return which >= ProprietaryPseudoKeys.min && which <= ProprietaryPseudoKeys.max; 4313 } 4314 4315 // these match Windows virtual key codes numerically for simplicity of translation there 4316 // but are plus a unicode private use area offset so i can cram them in the dchar 4317 // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 4318 /++ 4319 Represents non-character keys. 4320 +/ 4321 enum Key : dchar { 4322 escape = 0x1b + 0xF0000, /// . 4323 F1 = 0x70 + 0xF0000, /// . 4324 F2 = 0x71 + 0xF0000, /// . 4325 F3 = 0x72 + 0xF0000, /// . 4326 F4 = 0x73 + 0xF0000, /// . 4327 F5 = 0x74 + 0xF0000, /// . 4328 F6 = 0x75 + 0xF0000, /// . 4329 F7 = 0x76 + 0xF0000, /// . 4330 F8 = 0x77 + 0xF0000, /// . 4331 F9 = 0x78 + 0xF0000, /// . 4332 F10 = 0x79 + 0xF0000, /// . 4333 F11 = 0x7A + 0xF0000, /// . 4334 F12 = 0x7B + 0xF0000, /// . 4335 LeftArrow = 0x25 + 0xF0000, /// . 4336 RightArrow = 0x27 + 0xF0000, /// . 4337 UpArrow = 0x26 + 0xF0000, /// . 4338 DownArrow = 0x28 + 0xF0000, /// . 4339 Insert = 0x2d + 0xF0000, /// . 4340 Delete = 0x2e + 0xF0000, /// . 4341 Home = 0x24 + 0xF0000, /// . 4342 End = 0x23 + 0xF0000, /// . 4343 PageUp = 0x21 + 0xF0000, /// . 4344 PageDown = 0x22 + 0xF0000, /// . 4345 ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator 4346 4347 /* 4348 Enter = '\n', 4349 Backspace = '\b', 4350 Tab = '\t', 4351 */ 4352 } 4353 4354 /++ 4355 These are extensions added for better interop with the embedded emulator. 4356 As characters inside the unicode private-use area, you shouldn't encounter 4357 them unless you opt in by using some other proprietary feature. 4358 4359 History: 4360 Added December 4, 2020. 4361 +/ 4362 enum ProprietaryPseudoKeys : dchar { 4363 /++ 4364 If you use [Terminal.requestSetTerminalSelection], you should also process 4365 this pseudo-key to clear the selection when the terminal tells you do to keep 4366 you UI in sync. 4367 4368 History: 4369 Added December 4, 2020. 4370 +/ 4371 SelectNone = 0x0 + 0xF1000, // 987136 4372 } 4373 } 4374 4375 /// Deprecated: use KeyboardEvent instead in new programs 4376 /// Input event for characters 4377 struct CharacterEvent { 4378 /// . 4379 enum Type { 4380 Released, /// . 4381 Pressed /// . 4382 } 4383 4384 Type eventType; /// . 4385 dchar character; /// . 4386 uint modifierState; /// Don't depend on this to be available for character events 4387 } 4388 4389 /// Deprecated: use KeyboardEvent instead in new programs 4390 struct NonCharacterKeyEvent { 4391 /// . 4392 enum Type { 4393 Released, /// . 4394 Pressed /// . 4395 } 4396 Type eventType; /// . 4397 4398 // these match Windows virtual key codes numerically for simplicity of translation there 4399 //http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 4400 /// . 4401 enum Key : int { 4402 escape = 0x1b, /// . 4403 F1 = 0x70, /// . 4404 F2 = 0x71, /// . 4405 F3 = 0x72, /// . 4406 F4 = 0x73, /// . 4407 F5 = 0x74, /// . 4408 F6 = 0x75, /// . 4409 F7 = 0x76, /// . 4410 F8 = 0x77, /// . 4411 F9 = 0x78, /// . 4412 F10 = 0x79, /// . 4413 F11 = 0x7A, /// . 4414 F12 = 0x7B, /// . 4415 LeftArrow = 0x25, /// . 4416 RightArrow = 0x27, /// . 4417 UpArrow = 0x26, /// . 4418 DownArrow = 0x28, /// . 4419 Insert = 0x2d, /// . 4420 Delete = 0x2e, /// . 4421 Home = 0x24, /// . 4422 End = 0x23, /// . 4423 PageUp = 0x21, /// . 4424 PageDown = 0x22, /// . 4425 ScrollLock = 0x91, /// unlikely to work outside my terminal emulator 4426 } 4427 Key key; /// . 4428 4429 uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms 4430 4431 } 4432 4433 /// . 4434 struct PasteEvent { 4435 string pastedText; /// . 4436 } 4437 4438 /++ 4439 Indicates a hyperlink was clicked in my custom terminal emulator 4440 or with version `TerminalDirectToEmulator`. 4441 4442 You can simply ignore this event in a `final switch` if you aren't 4443 using the feature. 4444 4445 History: 4446 Added March 18, 2020 4447 +/ 4448 struct LinkEvent { 4449 string text; /// the text visible to the user that they clicked on 4450 ushort identifier; /// the identifier set when you output the link. This is small because it is packed into extra bits on the text, one bit per character. 4451 ushort command; /// set by the terminal to indicate how it was clicked. values tbd, currently always 0 4452 } 4453 4454 /// . 4455 struct MouseEvent { 4456 // these match simpledisplay.d numerically as well 4457 /// . 4458 enum Type { 4459 Moved = 0, /// . 4460 Pressed = 1, /// . 4461 Released = 2, /// . 4462 Clicked, /// . 4463 } 4464 4465 Type eventType; /// . 4466 4467 // note: these should numerically match simpledisplay.d for maximum beauty in my other code 4468 /// . 4469 enum Button : uint { 4470 None = 0, /// . 4471 Left = 1, /// . 4472 Middle = 4, /// . 4473 Right = 2, /// . 4474 ScrollUp = 8, /// . 4475 ScrollDown = 16 /// . 4476 } 4477 uint buttons; /// A mask of Button 4478 int x; /// 0 == left side 4479 int y; /// 0 == top 4480 uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something 4481 } 4482 4483 /// When you get this, check terminal.width and terminal.height to see the new size and react accordingly. 4484 struct SizeChangedEvent { 4485 int oldWidth; 4486 int oldHeight; 4487 int newWidth; 4488 int newHeight; 4489 } 4490 4491 /// the user hitting ctrl+c will send this 4492 /// You should drop what you're doing and perhaps exit when this happens. 4493 struct UserInterruptionEvent {} 4494 4495 /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. 4496 /// If you receive it, you should generally cleanly exit. 4497 struct HangupEvent {} 4498 4499 /// Sent upon receiving end-of-file from stdin. 4500 struct EndOfFileEvent {} 4501 4502 interface CustomEvent {} 4503 4504 class RunnableCustomEvent : CustomEvent { 4505 this(void delegate() dg) { 4506 this.dg = dg; 4507 } 4508 4509 void run() { 4510 if(dg) 4511 dg(); 4512 } 4513 4514 private void delegate() dg; 4515 } 4516 4517 version(Win32Console) 4518 enum ModifierState : uint { 4519 shift = 0x10, 4520 control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl 4521 4522 // i'm not sure if the next two are available 4523 alt = 2 | 1, //2 ==left alt, 1 == right alt 4524 4525 // FIXME: I don't think these are actually available 4526 windows = 512, 4527 meta = 4096, // FIXME sanity 4528 4529 // I don't think this is available on Linux.... 4530 scrollLock = 0x40, 4531 } 4532 else 4533 enum ModifierState : uint { 4534 shift = 4, 4535 alt = 2, 4536 control = 16, 4537 meta = 8, 4538 4539 windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones 4540 } 4541 4542 version(DDoc) 4543 /// 4544 enum ModifierState : uint { 4545 /// 4546 shift = 4, 4547 /// 4548 alt = 2, 4549 /// 4550 control = 16, 4551 4552 } 4553 4554 /++ 4555 [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. 4556 ++/ 4557 struct InputEvent { 4558 /// . 4559 enum Type { 4560 KeyboardEvent, /// Keyboard key pressed (or released, where supported) 4561 CharacterEvent, /// Do not use this in new programs, use KeyboardEvent instead 4562 NonCharacterKeyEvent, /// Do not use this in new programs, use KeyboardEvent instead 4563 PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. 4564 LinkEvent, /// User clicked a hyperlink you created. Simply ignore if you are not using that feature. 4565 MouseEvent, /// only sent if you subscribed to mouse events 4566 SizeChangedEvent, /// only sent if you subscribed to size events 4567 UserInterruptionEvent, /// the user hit ctrl+c 4568 EndOfFileEvent, /// stdin has received an end of file 4569 HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator 4570 CustomEvent /// . 4571 } 4572 4573 /// If this event is deprecated, you should filter it out in new programs 4574 bool isDeprecated() { 4575 return type == Type.CharacterEvent || type == Type.NonCharacterKeyEvent; 4576 } 4577 4578 /// . 4579 @property Type type() { return t; } 4580 4581 /// Returns a pointer to the terminal associated with this event. 4582 /// (You can usually just ignore this as there's only one terminal typically.) 4583 /// 4584 /// It may be null in the case of program-generated events; 4585 @property Terminal* terminal() { return term; } 4586 4587 /++ 4588 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. 4589 4590 See_Also: 4591 4592 The event types: 4593 [KeyboardEvent], [MouseEvent], [SizeChangedEvent], 4594 [PasteEvent], [UserInterruptionEvent], 4595 [EndOfFileEvent], [HangupEvent], [CustomEvent] 4596 4597 And associated functions: 4598 [RealTimeConsoleInput], [ConsoleInputFlags] 4599 ++/ 4600 @property auto get(Type T)() { 4601 if(type != T) 4602 throw new Exception("Wrong event type"); 4603 static if(T == Type.CharacterEvent) 4604 return characterEvent; 4605 else static if(T == Type.KeyboardEvent) 4606 return keyboardEvent; 4607 else static if(T == Type.NonCharacterKeyEvent) 4608 return nonCharacterKeyEvent; 4609 else static if(T == Type.PasteEvent) 4610 return pasteEvent; 4611 else static if(T == Type.LinkEvent) 4612 return linkEvent; 4613 else static if(T == Type.MouseEvent) 4614 return mouseEvent; 4615 else static if(T == Type.SizeChangedEvent) 4616 return sizeChangedEvent; 4617 else static if(T == Type.UserInterruptionEvent) 4618 return userInterruptionEvent; 4619 else static if(T == Type.EndOfFileEvent) 4620 return endOfFileEvent; 4621 else static if(T == Type.HangupEvent) 4622 return hangupEvent; 4623 else static if(T == Type.CustomEvent) 4624 return customEvent; 4625 else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); 4626 } 4627 4628 /// custom event is public because otherwise there's no point at all 4629 this(CustomEvent c, Terminal* p = null) { 4630 t = Type.CustomEvent; 4631 customEvent = c; 4632 } 4633 4634 private { 4635 this(CharacterEvent c, Terminal* p) { 4636 t = Type.CharacterEvent; 4637 characterEvent = c; 4638 } 4639 this(KeyboardEvent c, Terminal* p) { 4640 t = Type.KeyboardEvent; 4641 keyboardEvent = c; 4642 } 4643 this(NonCharacterKeyEvent c, Terminal* p) { 4644 t = Type.NonCharacterKeyEvent; 4645 nonCharacterKeyEvent = c; 4646 } 4647 this(PasteEvent c, Terminal* p) { 4648 t = Type.PasteEvent; 4649 pasteEvent = c; 4650 } 4651 this(LinkEvent c, Terminal* p) { 4652 t = Type.LinkEvent; 4653 linkEvent = c; 4654 } 4655 this(MouseEvent c, Terminal* p) { 4656 t = Type.MouseEvent; 4657 mouseEvent = c; 4658 } 4659 this(SizeChangedEvent c, Terminal* p) { 4660 t = Type.SizeChangedEvent; 4661 sizeChangedEvent = c; 4662 } 4663 this(UserInterruptionEvent c, Terminal* p) { 4664 t = Type.UserInterruptionEvent; 4665 userInterruptionEvent = c; 4666 } 4667 this(HangupEvent c, Terminal* p) { 4668 t = Type.HangupEvent; 4669 hangupEvent = c; 4670 } 4671 this(EndOfFileEvent c, Terminal* p) { 4672 t = Type.EndOfFileEvent; 4673 endOfFileEvent = c; 4674 } 4675 4676 Type t; 4677 Terminal* term; 4678 4679 union { 4680 KeyboardEvent keyboardEvent; 4681 CharacterEvent characterEvent; 4682 NonCharacterKeyEvent nonCharacterKeyEvent; 4683 PasteEvent pasteEvent; 4684 MouseEvent mouseEvent; 4685 SizeChangedEvent sizeChangedEvent; 4686 UserInterruptionEvent userInterruptionEvent; 4687 HangupEvent hangupEvent; 4688 EndOfFileEvent endOfFileEvent; 4689 LinkEvent linkEvent; 4690 CustomEvent customEvent; 4691 } 4692 } 4693 } 4694 4695 version(Demo) 4696 /// View the source of this! 4697 void main() { 4698 auto terminal = Terminal(ConsoleOutputType.cellular); 4699 4700 //terminal.color(Color.DEFAULT, Color.DEFAULT); 4701 4702 // 4703 ///* 4704 auto getter = new FileLineGetter(&terminal, "test"); 4705 getter.prompt = "> "; 4706 //getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; 4707 terminal.writeln("\n" ~ getter.getline()); 4708 terminal.writeln("\n" ~ getter.getline()); 4709 terminal.writeln("\n" ~ getter.getline()); 4710 getter.dispose(); 4711 //*/ 4712 4713 terminal.writeln(terminal.getline()); 4714 terminal.writeln(terminal.getline()); 4715 terminal.writeln(terminal.getline()); 4716 4717 //input.getch(); 4718 4719 // return; 4720 // 4721 4722 terminal.setTitle("Basic I/O"); 4723 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEventsWithRelease); 4724 terminal.color(Color.green | Bright, Color.black); 4725 4726 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"); 4727 terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 4728 4729 terminal.color(Color.DEFAULT, Color.DEFAULT); 4730 4731 int centerX = terminal.width / 2; 4732 int centerY = terminal.height / 2; 4733 4734 bool timeToBreak = false; 4735 4736 terminal.hyperlink("test", 4); 4737 terminal.hyperlink("another", 7); 4738 4739 void handleEvent(InputEvent event) { 4740 //terminal.writef("%s\n", event.type); 4741 final switch(event.type) { 4742 case InputEvent.Type.LinkEvent: 4743 auto ev = event.get!(InputEvent.Type.LinkEvent); 4744 terminal.writeln(ev); 4745 break; 4746 case InputEvent.Type.UserInterruptionEvent: 4747 case InputEvent.Type.HangupEvent: 4748 case InputEvent.Type.EndOfFileEvent: 4749 timeToBreak = true; 4750 version(with_eventloop) { 4751 import arsd.eventloop; 4752 exit(); 4753 } 4754 break; 4755 case InputEvent.Type.SizeChangedEvent: 4756 auto ev = event.get!(InputEvent.Type.SizeChangedEvent); 4757 terminal.writeln(ev); 4758 break; 4759 case InputEvent.Type.KeyboardEvent: 4760 auto ev = event.get!(InputEvent.Type.KeyboardEvent); 4761 if(!ev.pressed) break; 4762 terminal.writef("\t%s", ev); 4763 terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which); 4764 terminal.writeln(); 4765 if(ev.which == 'Q') { 4766 timeToBreak = true; 4767 version(with_eventloop) { 4768 import arsd.eventloop; 4769 exit(); 4770 } 4771 } 4772 4773 if(ev.which == 'C') 4774 terminal.clear(); 4775 break; 4776 case InputEvent.Type.CharacterEvent: // obsolete 4777 auto ev = event.get!(InputEvent.Type.CharacterEvent); 4778 //terminal.writef("\t%s\n", ev); 4779 break; 4780 case InputEvent.Type.NonCharacterKeyEvent: // obsolete 4781 //terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent)); 4782 break; 4783 case InputEvent.Type.PasteEvent: 4784 terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent)); 4785 break; 4786 case InputEvent.Type.MouseEvent: 4787 terminal.writef("\t%s\n", event.get!(InputEvent.Type.MouseEvent)); 4788 break; 4789 case InputEvent.Type.CustomEvent: 4790 break; 4791 } 4792 4793 //terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 4794 4795 /* 4796 if(input.kbhit()) { 4797 auto c = input.getch(); 4798 if(c == 'q' || c == 'Q') 4799 break; 4800 terminal.moveTo(centerX, centerY); 4801 terminal.writef("%c", c); 4802 terminal.flush(); 4803 } 4804 usleep(10000); 4805 */ 4806 } 4807 4808 version(with_eventloop) { 4809 import arsd.eventloop; 4810 addListener(&handleEvent); 4811 loop(); 4812 } else { 4813 loop: while(true) { 4814 auto event = input.nextEvent(); 4815 handleEvent(event); 4816 if(timeToBreak) 4817 break loop; 4818 } 4819 } 4820 } 4821 4822 enum TerminalCapabilities : uint { 4823 minimal = 0, 4824 vt100 = 1 << 0, 4825 4826 // my special terminal emulator extensions 4827 arsdClipboard = 1 << 15, // 90 in caps 4828 arsdImage = 1 << 16, // 91 in caps 4829 arsdHyperlinks = 1 << 17, // 92 in caps 4830 } 4831 4832 version(Posix) 4833 private uint /* TerminalCapabilities bitmask */ getTerminalCapabilities(int fdIn, int fdOut) { 4834 if(fdIn == -1 || fdOut == -1) 4835 return TerminalCapabilities.minimal; 4836 4837 import std.conv; 4838 import core.stdc.errno; 4839 import core.sys.posix.unistd; 4840 4841 ubyte[128] hack2; 4842 termios old; 4843 ubyte[128] hack; 4844 tcgetattr(fdIn, &old); 4845 auto n = old; 4846 n.c_lflag &= ~(ICANON | ECHO); 4847 tcsetattr(fdIn, TCSANOW, &n); 4848 scope(exit) 4849 tcsetattr(fdIn, TCSANOW, &old); 4850 4851 // drain the buffer? meh 4852 4853 string cmd = "\033[c"; 4854 auto err = write(fdOut, cmd.ptr, cmd.length); 4855 if(err != cmd.length) { 4856 throw new Exception("couldn't ask terminal for ID"); 4857 } 4858 4859 // reading directly to bypass any buffering 4860 int retries = 16; 4861 int len; 4862 ubyte[96] buffer; 4863 try_again: 4864 4865 4866 timeval tv; 4867 tv.tv_sec = 0; 4868 tv.tv_usec = 250 * 1000; // 250 ms 4869 4870 fd_set fs; 4871 FD_ZERO(&fs); 4872 4873 FD_SET(fdIn, &fs); 4874 if(select(fdIn + 1, &fs, null, null, &tv) == -1) { 4875 goto try_again; 4876 } 4877 4878 if(FD_ISSET(fdIn, &fs)) { 4879 auto len2 = read(fdIn, &buffer[len], buffer.length - len); 4880 if(len2 <= 0) { 4881 retries--; 4882 if(retries > 0) 4883 goto try_again; 4884 throw new Exception("can't get terminal id"); 4885 } else { 4886 len += len2; 4887 } 4888 } else { 4889 // no data... assume terminal doesn't support giving an answer 4890 return TerminalCapabilities.minimal; 4891 } 4892 4893 ubyte[] answer; 4894 bool hasAnswer(ubyte[] data) { 4895 if(data.length < 4) 4896 return false; 4897 answer = null; 4898 size_t start; 4899 int position = 0; 4900 foreach(idx, ch; data) { 4901 switch(position) { 4902 case 0: 4903 if(ch == '\033') { 4904 start = idx; 4905 position++; 4906 } 4907 break; 4908 case 1: 4909 if(ch == '[') 4910 position++; 4911 else 4912 position = 0; 4913 break; 4914 case 2: 4915 if(ch == '?') 4916 position++; 4917 else 4918 position = 0; 4919 break; 4920 case 3: 4921 // body 4922 if(ch == 'c') { 4923 answer = data[start .. idx + 1]; 4924 return true; 4925 } else if(ch == ';' || (ch >= '0' && ch <= '9')) { 4926 // good, keep going 4927 } else { 4928 // invalid, drop it 4929 position = 0; 4930 } 4931 break; 4932 default: assert(0); 4933 } 4934 } 4935 return false; 4936 } 4937 4938 auto got = buffer[0 .. len]; 4939 if(!hasAnswer(got)) { 4940 if(retries > 0) 4941 goto try_again; 4942 else 4943 return TerminalCapabilities.minimal; 4944 } 4945 auto gots = cast(char[]) answer[3 .. $-1]; 4946 4947 import std.string; 4948 4949 auto pieces = split(gots, ";"); 4950 uint ret = TerminalCapabilities.vt100; 4951 foreach(p; pieces) 4952 switch(p) { 4953 case "90": 4954 ret |= TerminalCapabilities.arsdClipboard; 4955 break; 4956 case "91": 4957 ret |= TerminalCapabilities.arsdImage; 4958 break; 4959 case "92": 4960 ret |= TerminalCapabilities.arsdHyperlinks; 4961 break; 4962 default: 4963 } 4964 return ret; 4965 } 4966 4967 private extern(C) int mkstemp(char *templ); 4968 4969 /* 4970 FIXME: support lines that wrap 4971 FIXME: better controls maybe 4972 4973 FIXME: support multi-line "lines" and some form of line continuation, both 4974 from the user (if permitted) and from the application, so like the user 4975 hits "class foo { \n" and the app says "that line needs continuation" automatically. 4976 4977 FIXME: fix lengths on prompt and suggestion 4978 */ 4979 /** 4980 A user-interactive line editor class, used by [Terminal.getline]. It is similar to 4981 GNU readline, offering comparable features like tab completion, history, and graceful 4982 degradation to adapt to the user's terminal. 4983 4984 4985 A note on history: 4986 4987 $(WARNING 4988 To save history, you must call LineGetter.dispose() when you're done with it. 4989 History will not be automatically saved without that call! 4990 ) 4991 4992 The history saving and loading as a trivially encountered race condition: if you 4993 open two programs that use the same one at the same time, the one that closes second 4994 will overwrite any history changes the first closer saved. 4995 4996 GNU Getline does this too... and it actually kinda drives me nuts. But I don't know 4997 what a good fix is except for doing a transactional commit straight to the file every 4998 time and that seems like hitting the disk way too often. 4999 5000 We could also do like a history server like a database daemon that keeps the order 5001 correct but I don't actually like that either because I kinda like different bashes 5002 to have different history, I just don't like it all to get lost. 5003 5004 Regardless though, this isn't even used in bash anyway, so I don't think I care enough 5005 to put that much effort into it. Just using separate files for separate tasks is good 5006 enough I think. 5007 */ 5008 class LineGetter { 5009 /* A note on the assumeSafeAppends in here: since these buffers are private, we can be 5010 pretty sure that stomping isn't an issue, so I'm using this liberally to keep the 5011 append/realloc code simple and hopefully reasonably fast. */ 5012 5013 // saved to file 5014 string[] history; 5015 5016 // not saved 5017 Terminal* terminal; 5018 string historyFilename; 5019 5020 /// Make sure that the parent terminal struct remains in scope for the duration 5021 /// of LineGetter's lifetime, as it does hold on to and use the passed pointer 5022 /// throughout. 5023 /// 5024 /// historyFilename will load and save an input history log to a particular folder. 5025 /// Leaving it null will mean no file will be used and history will not be saved across sessions. 5026 this(Terminal* tty, string historyFilename = null) { 5027 this.terminal = tty; 5028 this.historyFilename = historyFilename; 5029 5030 line.reserve(128); 5031 5032 if(historyFilename.length) 5033 loadSettingsAndHistoryFromFile(); 5034 5035 regularForeground = cast(Color) terminal._currentForeground; 5036 background = cast(Color) terminal._currentBackground; 5037 suggestionForeground = Color.blue; 5038 } 5039 5040 /// Call this before letting LineGetter die so it can do any necessary 5041 /// cleanup and save the updated history to a file. 5042 void dispose() { 5043 if(historyFilename.length && historyCommitMode == HistoryCommitMode.atTermination) 5044 saveSettingsAndHistoryToFile(); 5045 } 5046 5047 /// Override this to change the directory where history files are stored 5048 /// 5049 /// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows. 5050 /* virtual */ string historyFileDirectory() { 5051 version(Windows) { 5052 char[1024] path; 5053 // FIXME: this doesn't link because the crappy dmd lib doesn't have it 5054 if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) { 5055 import core.stdc.string; 5056 return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline"; 5057 } else { 5058 import std.process; 5059 return environment["APPDATA"] ~ "\\arsd-getline"; 5060 } 5061 } else version(Posix) { 5062 import std.process; 5063 return environment["HOME"] ~ "/.arsd-getline"; 5064 } 5065 } 5066 5067 /// You can customize the colors here. You should set these after construction, but before 5068 /// calling startGettingLine or getline. 5069 Color suggestionForeground = Color.blue; 5070 Color regularForeground = Color.DEFAULT; /// ditto 5071 Color background = Color.DEFAULT; /// ditto 5072 Color promptColor = Color.DEFAULT; /// ditto 5073 Color specialCharBackground = Color.green; /// ditto 5074 //bool reverseVideo; 5075 5076 /// Set this if you want a prompt to be drawn with the line. It does NOT support color in string. 5077 @property void prompt(string p) { 5078 this.prompt_ = p; 5079 5080 promptLength = 0; 5081 foreach(dchar c; p) 5082 promptLength++; 5083 } 5084 5085 /// ditto 5086 @property string prompt() { 5087 return this.prompt_; 5088 } 5089 5090 private string prompt_; 5091 private int promptLength; 5092 5093 /++ 5094 Turn on auto suggest if you want a greyed thing of what tab 5095 would be able to fill in as you type. 5096 5097 You might want to turn it off if generating a completion list is slow. 5098 5099 Or if you know you want it, be sure to turn it on explicitly in your 5100 code because I reserve the right to change the default without advance notice. 5101 5102 History: 5103 On March 4, 2020, I changed the default to `false` because it 5104 is kinda slow and not useful in all cases. 5105 +/ 5106 bool autoSuggest = false; 5107 5108 /++ 5109 Returns true if there was any input in the buffer. Can be 5110 checked in the case of a [UserInterruptionException]. 5111 +/ 5112 bool hadInput() { 5113 return line.length > 0; 5114 } 5115 5116 /++ 5117 Override this if you don't want all lines added to the history. 5118 You can return null to not add it at all, or you can transform it. 5119 5120 History: 5121 Prior to October 12, 2021, it always committed all candidates. 5122 After that, it no longer commits in F9/ctrl+enter "run and maintain buffer" 5123 operations. This is tested with the [lastLineWasRetained] method. 5124 5125 The idea is those are temporary experiments and need not clog history until 5126 it is complete. 5127 +/ 5128 /* virtual */ string historyFilter(string candidate) { 5129 if(lastLineWasRetained()) 5130 return null; 5131 return candidate; 5132 } 5133 5134 /++ 5135 History is normally only committed to the file when the program is 5136 terminating, but if you are losing data due to crashes, you might want 5137 to change this to `historyCommitMode = HistoryCommitMode.afterEachLine;`. 5138 5139 History: 5140 Added January 26, 2021 (version 9.2) 5141 +/ 5142 public enum HistoryCommitMode { 5143 /// The history file is written to disk only at disposal time by calling [saveSettingsAndHistoryToFile] 5144 atTermination, 5145 /// The history file is written to disk after each line of input by calling [appendHistoryToFile] 5146 afterEachLine 5147 } 5148 5149 /// ditto 5150 public HistoryCommitMode historyCommitMode; 5151 5152 /++ 5153 You may override this to do nothing. If so, you should 5154 also override [appendHistoryToFile] if you ever change 5155 [historyCommitMode]. 5156 5157 You should call [historyPath] to get the proper filename. 5158 +/ 5159 /* virtual */ void saveSettingsAndHistoryToFile() { 5160 import std.file; 5161 if(!exists(historyFileDirectory)) 5162 mkdirRecurse(historyFileDirectory); 5163 5164 auto fn = historyPath(); 5165 5166 import std.stdio; 5167 auto file = File(fn, "wb"); 5168 file.write("// getline history file\r\n"); 5169 foreach(item; history) 5170 file.writeln(item, "\r"); 5171 } 5172 5173 /++ 5174 If [historyCommitMode] is [HistoryCommitMode.afterEachLine], 5175 this line is called after each line to append to the file instead 5176 of [saveSettingsAndHistoryToFile]. 5177 5178 Use [historyPath] to get the proper full path. 5179 5180 History: 5181 Added January 26, 2021 (version 9.2) 5182 +/ 5183 /* virtual */ void appendHistoryToFile(string item) { 5184 import std.file; 5185 5186 if(!exists(historyFileDirectory)) 5187 mkdirRecurse(historyFileDirectory); 5188 // this isn't exactly atomic but meh tbh i don't care. 5189 auto fn = historyPath(); 5190 if(exists(fn)) { 5191 append(fn, item ~ "\r\n"); 5192 } else { 5193 std.file.write(fn, "// getline history file\r\n" ~ item ~ "\r\n"); 5194 } 5195 } 5196 5197 /// You may override this to do nothing 5198 /* virtual */ void loadSettingsAndHistoryFromFile() { 5199 import std.file; 5200 history = null; 5201 auto fn = historyPath(); 5202 if(exists(fn)) { 5203 import std.stdio, std.algorithm, std.string; 5204 string cur; 5205 5206 auto file = File(fn, "rb"); 5207 auto first = file.readln(); 5208 if(first.startsWith("// getline history file")) { 5209 foreach(chunk; file.byChunk(1024)) { 5210 auto idx = (cast(char[]) chunk).indexOf(cast(char) '\r'); 5211 while(idx != -1) { 5212 cur ~= cast(char[]) chunk[0 .. idx]; 5213 history ~= cur; 5214 cur = null; 5215 if(idx + 2 <= chunk.length) 5216 chunk = chunk[idx + 2 .. $]; // skipping \r\n 5217 else 5218 chunk = chunk[$ .. $]; 5219 idx = (cast(char[]) chunk).indexOf(cast(char) '\r'); 5220 } 5221 cur ~= cast(char[]) chunk; 5222 } 5223 if(cur.length) 5224 history ~= cur; 5225 } else { 5226 // old-style plain file 5227 history ~= first; 5228 foreach(line; file.byLine()) 5229 history ~= line.idup; 5230 } 5231 } 5232 } 5233 5234 /++ 5235 History: 5236 Introduced on January 31, 2020 5237 +/ 5238 /* virtual */ string historyFileExtension() { 5239 return ".history"; 5240 } 5241 5242 /// semi-private, do not rely upon yet 5243 final string historyPath() { 5244 import std.path; 5245 auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension(); 5246 return filename; 5247 } 5248 5249 /++ 5250 Override this to provide tab completion. You may use the candidate 5251 argument to filter the list, but you don't have to (LineGetter will 5252 do it for you on the values you return). This means you can ignore 5253 the arguments if you like. 5254 5255 Ideally, you wouldn't return more than about ten items since the list 5256 gets difficult to use if it is too long. 5257 5258 Tab complete cannot modify text before or after the cursor at this time. 5259 I *might* change that later to allow tab complete to fuzzy search and spell 5260 check fix before. But right now it ONLY inserts. 5261 5262 Default is to provide recent command history as autocomplete. 5263 5264 $(WARNING Both `candidate` and `afterCursor` may have private data packed into the dchar bits 5265 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5266 5267 Returns: 5268 This function should return the full string to replace 5269 `candidate[tabCompleteStartPoint(args) .. $]`. 5270 For example, if your user wrote `wri<tab>` and you want to complete 5271 it to `write` or `writeln`, you should return `["write", "writeln"]`. 5272 5273 If you offer different tab complete in different places, you still 5274 need to return the whole string. For example, a file completion of 5275 a second argument, when the user writes `terminal.d term<tab>` and you 5276 want it to complete to an additional `terminal.d`, you should return 5277 `["terminal.d terminal.d"]`; in other words, `candidate ~ completion` 5278 for each completion. 5279 5280 It does this so you can simply return an array of words without having 5281 to rebuild that array for each combination. 5282 5283 To choose the word separator, override [tabCompleteStartPoint]. 5284 5285 Params: 5286 candidate = the text of the line up to the text cursor, after 5287 which the completed text would be inserted 5288 5289 afterCursor = the remaining text after the cursor. You can inspect 5290 this, but cannot change it - this will be appended to the line 5291 after completion, keeping the cursor in the same relative location. 5292 5293 History: 5294 Prior to January 30, 2020, this method took only one argument, 5295 `candidate`. It now takes `afterCursor` as well, to allow you to 5296 make more intelligent completions with full context. 5297 +/ 5298 /* virtual */ protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { 5299 return history.length > 20 ? history[0 .. 20] : history; 5300 } 5301 5302 /++ 5303 Override this to provide a different tab competition starting point. The default 5304 is `0`, always completing the complete line, but you may return the index of another 5305 character of `candidate` to provide a new split. 5306 5307 $(WARNING Both `candidate` and `afterCursor` may have private data packed into the dchar bits 5308 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5309 5310 Returns: 5311 The index of `candidate` where we should start the slice to keep in [tabComplete]. 5312 It must be `>= 0 && <= candidate.length`. 5313 5314 History: 5315 Added on February 1, 2020. Initial default is to return 0 to maintain 5316 old behavior. 5317 +/ 5318 /* virtual */ protected size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { 5319 return 0; 5320 } 5321 5322 /++ 5323 This gives extra information for an item when displaying tab competition details. 5324 5325 History: 5326 Added January 31, 2020. 5327 5328 +/ 5329 /* virtual */ protected string tabCompleteHelp(string candidate) { 5330 return null; 5331 } 5332 5333 private string[] filterTabCompleteList(string[] list, size_t start) { 5334 if(list.length == 0) 5335 return list; 5336 5337 string[] f; 5338 f.reserve(list.length); 5339 5340 foreach(item; list) { 5341 import std.algorithm; 5342 if(startsWith(item, line[start .. cursorPosition].map!(x => x & ~PRIVATE_BITS_MASK))) 5343 f ~= item; 5344 } 5345 5346 /+ 5347 // if it is excessively long, let's trim it down by trying to 5348 // group common sub-sequences together. 5349 if(f.length > terminal.height * 3 / 4) { 5350 import std.algorithm; 5351 f.sort(); 5352 5353 // see how many can be saved by just keeping going until there is 5354 // no more common prefix. then commit that and keep on down the list. 5355 // since it is sorted, if there is a commonality, it should appear quickly 5356 string[] n; 5357 string commonality = f[0]; 5358 size_t idx = 1; 5359 while(idx < f.length) { 5360 auto c = commonPrefix(commonality, f[idx]); 5361 if(c.length > cursorPosition - start) { 5362 commonality = c; 5363 } else { 5364 n ~= commonality; 5365 commonality = f[idx]; 5366 } 5367 idx++; 5368 } 5369 if(commonality.length) 5370 n ~= commonality; 5371 5372 if(n.length) 5373 f = n; 5374 } 5375 +/ 5376 5377 return f; 5378 } 5379 5380 /++ 5381 Override this to provide a custom display of the tab completion list. 5382 5383 History: 5384 Prior to January 31, 2020, it only displayed the list. After 5385 that, it would call [tabCompleteHelp] for each candidate and display 5386 that string (if present) as well. 5387 +/ 5388 protected void showTabCompleteList(string[] list) { 5389 if(list.length) { 5390 // FIXME: allow mouse clicking of an item, that would be cool 5391 5392 auto start = tabCompleteStartPoint(line[0 .. cursorPosition], line[cursorPosition .. $]); 5393 5394 // FIXME: scroll 5395 //if(terminal.type == ConsoleOutputType.linear) { 5396 terminal.writeln(); 5397 foreach(item; list) { 5398 terminal.color(suggestionForeground, background); 5399 import std.utf; 5400 auto idx = codeLength!char(line[start .. cursorPosition]); 5401 terminal.write(" ", item[0 .. idx]); 5402 terminal.color(regularForeground, background); 5403 terminal.write(item[idx .. $]); 5404 auto help = tabCompleteHelp(item); 5405 if(help !is null) { 5406 import std.string; 5407 help = help.replace("\t", " ").replace("\n", " ").replace("\r", " "); 5408 terminal.write("\t\t"); 5409 int remaining; 5410 if(terminal.cursorX + 2 < terminal.width) { 5411 remaining = terminal.width - terminal.cursorX - 2; 5412 } 5413 if(remaining > 8) { 5414 string msg = help; 5415 foreach(idxh, dchar c; msg) { 5416 remaining--; 5417 if(remaining <= 0) { 5418 msg = msg[0 .. idxh]; 5419 break; 5420 } 5421 } 5422 5423 /+ 5424 size_t use = help.length < remaining ? help.length : remaining; 5425 5426 if(use < help.length) { 5427 if((help[use] & 0xc0) != 0x80) { 5428 import std.utf; 5429 use += stride(help[use .. $]); 5430 } else { 5431 // just get to the end of this code point 5432 while(use < help.length && (help[use] & 0xc0) == 0x80) 5433 use++; 5434 } 5435 } 5436 auto msg = help[0 .. use]; 5437 +/ 5438 if(msg.length) 5439 terminal.write(msg); 5440 } 5441 } 5442 terminal.writeln(); 5443 5444 } 5445 updateCursorPosition(); 5446 redraw(); 5447 //} 5448 } 5449 } 5450 5451 /++ 5452 Called by the default event loop when the user presses F1. Override 5453 `showHelp` to change the UI, override [helpMessage] if you just want 5454 to change the message. 5455 5456 History: 5457 Introduced on January 30, 2020 5458 +/ 5459 protected void showHelp() { 5460 terminal.writeln(); 5461 terminal.writeln(helpMessage); 5462 updateCursorPosition(); 5463 redraw(); 5464 } 5465 5466 /++ 5467 History: 5468 Introduced on January 30, 2020 5469 +/ 5470 protected string helpMessage() { 5471 return "Press F2 to edit current line in your external editor. F3 searches history. F9 runs current line while maintaining current edit state."; 5472 } 5473 5474 /++ 5475 $(WARNING `line` may have private data packed into the dchar bits 5476 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5477 5478 History: 5479 Introduced on January 30, 2020 5480 +/ 5481 protected dchar[] editLineInEditor(in dchar[] line, in size_t cursorPosition) { 5482 import std.conv; 5483 import std.process; 5484 import std.file; 5485 5486 char[] tmpName; 5487 5488 version(Windows) { 5489 import core.stdc.string; 5490 char[280] path; 5491 auto l = GetTempPathA(cast(DWORD) path.length, path.ptr); 5492 if(l == 0) throw new Exception("GetTempPathA"); 5493 path[l] = 0; 5494 char[280] name; 5495 auto r = GetTempFileNameA(path.ptr, "adr", 0, name.ptr); 5496 if(r == 0) throw new Exception("GetTempFileNameA"); 5497 tmpName = name[0 .. strlen(name.ptr)]; 5498 scope(exit) 5499 std.file.remove(tmpName); 5500 std.file.write(tmpName, to!string(line)); 5501 5502 string editor = environment.get("EDITOR", "notepad.exe"); 5503 } else { 5504 import core.stdc.stdlib; 5505 import core.sys.posix.unistd; 5506 char[120] name; 5507 string p = "/tmp/adrXXXXXX"; 5508 name[0 .. p.length] = p[]; 5509 name[p.length] = 0; 5510 auto fd = mkstemp(name.ptr); 5511 tmpName = name[0 .. p.length]; 5512 if(fd == -1) throw new Exception("mkstemp"); 5513 scope(exit) 5514 close(fd); 5515 scope(exit) 5516 std.file.remove(tmpName); 5517 5518 string s = to!string(line); 5519 while(s.length) { 5520 auto x = write(fd, s.ptr, s.length); 5521 if(x == -1) throw new Exception("write"); 5522 s = s[x .. $]; 5523 } 5524 string editor = environment.get("EDITOR", "vi"); 5525 } 5526 5527 // FIXME the spawned process changes even more terminal state than set up here! 5528 5529 try { 5530 version(none) 5531 if(UseVtSequences) { 5532 if(terminal.type == ConsoleOutputType.cellular) { 5533 terminal.doTermcap("te"); 5534 } 5535 } 5536 version(Posix) { 5537 import std.stdio; 5538 // need to go to the parent terminal jic we're in an embedded terminal with redirection 5539 terminal.write(" !! Editor may be in parent terminal !!"); 5540 terminal.flush(); 5541 spawnProcess([editor, tmpName], File("/dev/tty", "rb"), File("/dev/tty", "wb")).wait; 5542 } else { 5543 spawnProcess([editor, tmpName]).wait; 5544 } 5545 if(UseVtSequences) { 5546 if(terminal.type == ConsoleOutputType.cellular) 5547 terminal.doTermcap("ti"); 5548 } 5549 import std.string; 5550 return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp; 5551 } catch(Exception e) { 5552 // edit failed, we should prolly tell them but idk how.... 5553 return null; 5554 } 5555 } 5556 5557 //private RealTimeConsoleInput* rtci; 5558 5559 /// One-call shop for the main workhorse 5560 /// If you already have a RealTimeConsoleInput ready to go, you 5561 /// should pass a pointer to yours here. Otherwise, LineGetter will 5562 /// make its own. 5563 public string getline(RealTimeConsoleInput* input = null) { 5564 startGettingLine(); 5565 if(input is null) { 5566 auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents | ConsoleInputFlags.selectiveMouse | ConsoleInputFlags.noEolWrap); 5567 //rtci = &i; 5568 //scope(exit) rtci = null; 5569 while(workOnLine(i.nextEvent(), &i)) {} 5570 } else { 5571 //rtci = input; 5572 //scope(exit) rtci = null; 5573 while(workOnLine(input.nextEvent(), input)) {} 5574 } 5575 return finishGettingLine(); 5576 } 5577 5578 /++ 5579 Set in [historyRecallFilterMethod]. 5580 5581 History: 5582 Added November 27, 2020. 5583 +/ 5584 enum HistoryRecallFilterMethod { 5585 /++ 5586 Goes through history in simple chronological order. 5587 Your existing command entry is not considered as a filter. 5588 +/ 5589 chronological, 5590 /++ 5591 Goes through history filtered with only those that begin with your current command entry. 5592 5593 So, if you entered "animal", "and", "bad", "cat" previously, then enter 5594 "a" and pressed up, it would jump to "and", then up again would go to "animal". 5595 +/ 5596 prefixed, 5597 /++ 5598 Goes through history filtered with only those that $(B contain) your current command entry. 5599 5600 So, if you entered "animal", "and", "bad", "cat" previously, then enter 5601 "n" and pressed up, it would jump to "and", then up again would go to "animal". 5602 +/ 5603 containing, 5604 /++ 5605 Goes through history to fill in your command at the cursor. It filters to only entries 5606 that start with the text before your cursor and ends with text after your cursor. 5607 5608 So, if you entered "animal", "and", "bad", "cat" previously, then enter 5609 "ad" and pressed left to position the cursor between the a and d, then pressed up 5610 it would jump straight to "and". 5611 +/ 5612 sandwiched, 5613 } 5614 /++ 5615 Controls what happens when the user presses the up key, etc., to recall history entries. See [HistoryRecallMethod] for the options. 5616 5617 This has no effect on the history search user control (default key: F3 or ctrl+r), which always searches through a "containing" method. 5618 5619 History: 5620 Added November 27, 2020. 5621 +/ 5622 HistoryRecallFilterMethod historyRecallFilterMethod = HistoryRecallFilterMethod.chronological; 5623 5624 /++ 5625 Enables automatic closing of brackets like (, {, and [ when the user types. 5626 Specifically, you subclass and return a string of the completions you want to 5627 do, so for that set, return `"()[]{}"` 5628 5629 5630 $(WARNING 5631 If you subclass this and return anything other than `null`, your subclass must also 5632 realize that the `line` member and everything that slices it ([tabComplete] and more) 5633 need to mask away the extra bits to get the original content. See [PRIVATE_BITS_MASK]. 5634 `line[] &= cast(dchar) ~PRIVATE_BITS_MASK;` 5635 ) 5636 5637 Returns: 5638 A string with pairs of characters. When the user types the character in an even-numbered 5639 position, it automatically inserts the following character after the cursor (without moving 5640 the cursor). The inserted character will be automatically overstriken if the user types it 5641 again. 5642 5643 The default is `return null`, which disables the feature. 5644 5645 History: 5646 Added January 25, 2021 (version 9.2) 5647 +/ 5648 protected string enableAutoCloseBrackets() { 5649 return null; 5650 } 5651 5652 /++ 5653 If [enableAutoCloseBrackets] does not return null, you should ignore these bits in the line. 5654 +/ 5655 protected enum PRIVATE_BITS_MASK = 0x80_00_00_00; 5656 // note: several instances in the code of PRIVATE_BITS_MASK are kinda conservative; masking it away is destructive 5657 // but less so than crashing cuz of invalid unicode character popping up later. Besides the main intention is when 5658 // you are kinda immediately typing so it forgetting is probably fine. 5659 5660 /++ 5661 Subclasses that implement this function can enable syntax highlighting in the line as you edit it. 5662 5663 5664 The library will call this when it prepares to draw the line, giving you the full line as well as the 5665 current position in that array it is about to draw. You return a [SyntaxHighlightMatch] 5666 object with its `charsMatched` member set to how many characters the given colors should apply to. 5667 If it is set to zero, default behavior is retained for the next character, and [syntaxHighlightMatch] 5668 will be called again immediately. If it is set to -1 syntax highlighting is disabled for the rest of 5669 the line. If set to int.max, it will apply to the remainder of the line. 5670 5671 If it is set to another positive value, the given colors are applied for that number of characters and 5672 [syntaxHighlightMatch] will NOT be called again until those characters are consumed. 5673 5674 Note that the first call may have `currentDrawPosition` be greater than zero due to horizontal scrolling. 5675 After that though, it will be called based on your `charsMatched` in the return value. 5676 5677 `currentCursorPosition` is passed in case you want to do things like highlight a matching parenthesis over 5678 the cursor or similar. You can also simply ignore it. 5679 5680 $(WARNING `line` may have private data packed into the dchar bits 5681 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5682 5683 History: 5684 Added January 25, 2021 (version 9.2) 5685 +/ 5686 protected SyntaxHighlightMatch syntaxHighlightMatch(in dchar[] line, in size_t currentDrawPosition, in size_t currentCursorPosition) { 5687 return SyntaxHighlightMatch(-1); // -1 just means syntax highlighting is disabled and it shouldn't try again 5688 } 5689 5690 /// ditto 5691 static struct SyntaxHighlightMatch { 5692 int charsMatched = 0; 5693 Color foreground = Color.DEFAULT; 5694 Color background = Color.DEFAULT; 5695 } 5696 5697 5698 private int currentHistoryViewPosition = 0; 5699 private dchar[] uncommittedHistoryCandidate; 5700 private int uncommitedHistoryCursorPosition; 5701 void loadFromHistory(int howFarBack) { 5702 if(howFarBack < 0) 5703 howFarBack = 0; 5704 if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around. 5705 howFarBack = cast(int) history.length; 5706 if(howFarBack == currentHistoryViewPosition) 5707 return; 5708 if(currentHistoryViewPosition == 0) { 5709 // save the current line so we can down arrow back to it later 5710 if(uncommittedHistoryCandidate.length < line.length) { 5711 uncommittedHistoryCandidate.length = line.length; 5712 } 5713 5714 uncommittedHistoryCandidate[0 .. line.length] = line[]; 5715 uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length]; 5716 uncommittedHistoryCandidate.assumeSafeAppend(); 5717 uncommitedHistoryCursorPosition = cursorPosition; 5718 } 5719 5720 if(howFarBack == 0) { 5721 zero: 5722 line.length = uncommittedHistoryCandidate.length; 5723 line.assumeSafeAppend(); 5724 line[] = uncommittedHistoryCandidate[]; 5725 } else { 5726 line = line[0 .. 0]; 5727 line.assumeSafeAppend(); 5728 5729 string selection; 5730 5731 final switch(historyRecallFilterMethod) with(HistoryRecallFilterMethod) { 5732 case chronological: 5733 selection = history[$ - howFarBack]; 5734 break; 5735 case prefixed: 5736 case containing: 5737 import std.algorithm; 5738 int count; 5739 foreach_reverse(item; history) { 5740 if( 5741 (historyRecallFilterMethod == prefixed && item.startsWith(uncommittedHistoryCandidate)) 5742 || 5743 (historyRecallFilterMethod == containing && item.canFind(uncommittedHistoryCandidate)) 5744 ) 5745 { 5746 selection = item; 5747 count++; 5748 if(count == howFarBack) 5749 break; 5750 } 5751 } 5752 howFarBack = count; 5753 break; 5754 case sandwiched: 5755 import std.algorithm; 5756 int count; 5757 foreach_reverse(item; history) { 5758 if( 5759 (item.startsWith(uncommittedHistoryCandidate[0 .. uncommitedHistoryCursorPosition])) 5760 && 5761 (item.endsWith(uncommittedHistoryCandidate[uncommitedHistoryCursorPosition .. $])) 5762 ) 5763 { 5764 selection = item; 5765 count++; 5766 if(count == howFarBack) 5767 break; 5768 } 5769 } 5770 howFarBack = count; 5771 5772 break; 5773 } 5774 5775 if(howFarBack == 0) 5776 goto zero; 5777 5778 int i; 5779 line.length = selection.length; 5780 foreach(dchar ch; selection) 5781 line[i++] = ch; 5782 line = line[0 .. i]; 5783 line.assumeSafeAppend(); 5784 } 5785 5786 currentHistoryViewPosition = howFarBack; 5787 cursorPosition = cast(int) line.length; 5788 scrollToEnd(); 5789 } 5790 5791 bool insertMode = true; 5792 5793 private ConsoleOutputType original = cast(ConsoleOutputType) -1; 5794 private bool multiLineModeOn = false; 5795 private int startOfLineXOriginal; 5796 private int startOfLineYOriginal; 5797 void multiLineMode(bool on) { 5798 if(original == -1) { 5799 original = terminal.type; 5800 startOfLineXOriginal = startOfLineX; 5801 startOfLineYOriginal = startOfLineY; 5802 } 5803 5804 if(on) { 5805 terminal.enableAlternateScreen = true; 5806 startOfLineX = 0; 5807 startOfLineY = 0; 5808 } 5809 else if(original == ConsoleOutputType.linear) { 5810 terminal.enableAlternateScreen = false; 5811 } 5812 5813 if(!on) { 5814 startOfLineX = startOfLineXOriginal; 5815 startOfLineY = startOfLineYOriginal; 5816 } 5817 5818 multiLineModeOn = on; 5819 } 5820 bool multiLineMode() { return multiLineModeOn; } 5821 5822 void toggleMultiLineMode() { 5823 multiLineMode = !multiLineModeOn; 5824 redraw(); 5825 } 5826 5827 private dchar[] line; 5828 private int cursorPosition = 0; 5829 private int horizontalScrollPosition = 0; 5830 private int verticalScrollPosition = 0; 5831 5832 private void scrollToEnd() { 5833 if(multiLineMode) { 5834 // FIXME 5835 } else { 5836 horizontalScrollPosition = (cast(int) line.length); 5837 horizontalScrollPosition -= availableLineLength(); 5838 if(horizontalScrollPosition < 0) 5839 horizontalScrollPosition = 0; 5840 } 5841 } 5842 5843 // used for redrawing the line in the right place 5844 // and detecting mouse events on our line. 5845 private int startOfLineX; 5846 private int startOfLineY; 5847 5848 // private string[] cachedCompletionList; 5849 5850 // FIXME 5851 // /// Note that this assumes the tab complete list won't change between actual 5852 // /// presses of tab by the user. If you pass it a list, it will use it, but 5853 // /// otherwise it will keep track of the last one to avoid calls to tabComplete. 5854 private string suggestion(string[] list = null) { 5855 import std.algorithm, std.utf; 5856 auto relevantLineSection = line[0 .. cursorPosition]; 5857 auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); 5858 relevantLineSection = relevantLineSection[start .. $]; 5859 // FIXME: see about caching the list if we easily can 5860 if(list is null) 5861 list = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); 5862 5863 if(list.length) { 5864 string commonality = list[0]; 5865 foreach(item; list[1 .. $]) { 5866 commonality = commonPrefix(commonality, item); 5867 } 5868 5869 if(commonality.length) { 5870 return commonality[codeLength!char(relevantLineSection) .. $]; 5871 } 5872 } 5873 5874 return null; 5875 } 5876 5877 /// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something. 5878 /// You'll probably want to call redraw() after adding chars. 5879 void addChar(dchar ch) { 5880 assert(cursorPosition >= 0 && cursorPosition <= line.length); 5881 if(cursorPosition == line.length) 5882 line ~= ch; 5883 else { 5884 assert(line.length); 5885 if(insertMode) { 5886 line ~= ' '; 5887 for(int i = cast(int) line.length - 2; i >= cursorPosition; i --) 5888 line[i + 1] = line[i]; 5889 } 5890 line[cursorPosition] = ch; 5891 } 5892 cursorPosition++; 5893 5894 if(multiLineMode) { 5895 // FIXME 5896 } else { 5897 if(cursorPosition > horizontalScrollPosition + availableLineLength()) 5898 horizontalScrollPosition++; 5899 } 5900 5901 lineChanged = true; 5902 } 5903 5904 /// . 5905 void addString(string s) { 5906 // FIXME: this could be more efficient 5907 // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) 5908 5909 import std.utf; 5910 foreach(dchar ch; s.byDchar) // using this for the replacement dchar, normal foreach would throw on invalid utf 8 5911 addChar(ch); 5912 } 5913 5914 /// Deletes the character at the current position in the line. 5915 /// You'll probably want to call redraw() after deleting chars. 5916 void deleteChar() { 5917 if(cursorPosition == line.length) 5918 return; 5919 for(int i = cursorPosition; i < line.length - 1; i++) 5920 line[i] = line[i + 1]; 5921 line = line[0 .. $-1]; 5922 line.assumeSafeAppend(); 5923 lineChanged = true; 5924 } 5925 5926 protected bool lineChanged; 5927 5928 private void killText(dchar[] text) { 5929 if(!text.length) 5930 return; 5931 5932 if(justKilled) 5933 killBuffer = text ~ killBuffer; 5934 else 5935 killBuffer = text; 5936 } 5937 5938 /// 5939 void deleteToEndOfLine() { 5940 killText(line[cursorPosition .. $]); 5941 line = line[0 .. cursorPosition]; 5942 line.assumeSafeAppend(); 5943 //while(cursorPosition < line.length) 5944 //deleteChar(); 5945 } 5946 5947 /++ 5948 Used by the word movement keys (e.g. alt+backspace) to find a word break. 5949 5950 History: 5951 Added April 21, 2021 (dub v9.5) 5952 5953 Prior to that, [LineGetter] only used [std.uni.isWhite]. Now it uses this which 5954 uses if not alphanum and not underscore. 5955 5956 You can subclass this to customize its behavior. 5957 +/ 5958 bool isWordSeparatorCharacter(dchar d) { 5959 import std.uni : isAlphaNum; 5960 5961 return !(isAlphaNum(d) || d == '_'); 5962 } 5963 5964 private int wordForwardIdx() { 5965 int cursorPosition = this.cursorPosition; 5966 if(cursorPosition == line.length) 5967 return cursorPosition; 5968 while(cursorPosition + 1 < line.length && isWordSeparatorCharacter(line[cursorPosition])) 5969 cursorPosition++; 5970 while(cursorPosition + 1 < line.length && !isWordSeparatorCharacter(line[cursorPosition + 1])) 5971 cursorPosition++; 5972 cursorPosition += 2; 5973 if(cursorPosition > line.length) 5974 cursorPosition = cast(int) line.length; 5975 5976 return cursorPosition; 5977 } 5978 void wordForward() { 5979 cursorPosition = wordForwardIdx(); 5980 aligned(cursorPosition, 1); 5981 maybePositionCursor(); 5982 } 5983 void killWordForward() { 5984 int to = wordForwardIdx(), from = cursorPosition; 5985 killText(line[from .. to]); 5986 line = line[0 .. from] ~ line[to .. $]; 5987 cursorPosition = cast(int)from; 5988 maybePositionCursor(); 5989 } 5990 private int wordBackIdx() { 5991 if(!line.length || !cursorPosition) 5992 return cursorPosition; 5993 int ret = cursorPosition - 1; 5994 while(ret && isWordSeparatorCharacter(line[ret])) 5995 ret--; 5996 while(ret && !isWordSeparatorCharacter(line[ret - 1])) 5997 ret--; 5998 return ret; 5999 } 6000 void wordBack() { 6001 cursorPosition = wordBackIdx(); 6002 aligned(cursorPosition, -1); 6003 maybePositionCursor(); 6004 } 6005 void killWord() { 6006 int from = wordBackIdx(), to = cursorPosition; 6007 killText(line[from .. to]); 6008 line = line[0 .. from] ~ line[to .. $]; 6009 cursorPosition = cast(int)from; 6010 maybePositionCursor(); 6011 } 6012 6013 private void maybePositionCursor() { 6014 if(multiLineMode) { 6015 // omg this is so bad 6016 // and it more accurately sets scroll position 6017 int x, y; 6018 foreach(idx, ch; line) { 6019 if(idx == cursorPosition) 6020 break; 6021 if(ch == '\n') { 6022 x = 0; 6023 y++; 6024 } else { 6025 x++; 6026 } 6027 } 6028 6029 while(x - horizontalScrollPosition < 0) { 6030 horizontalScrollPosition -= terminal.width / 2; 6031 if(horizontalScrollPosition < 0) 6032 horizontalScrollPosition = 0; 6033 } 6034 while(y - verticalScrollPosition < 0) { 6035 verticalScrollPosition --; 6036 if(verticalScrollPosition < 0) 6037 verticalScrollPosition = 0; 6038 } 6039 6040 while((x - horizontalScrollPosition) >= terminal.width) { 6041 horizontalScrollPosition += terminal.width / 2; 6042 } 6043 while((y - verticalScrollPosition) + 2 >= terminal.height) { 6044 verticalScrollPosition ++; 6045 } 6046 6047 } else { 6048 if(cursorPosition < horizontalScrollPosition || cursorPosition > horizontalScrollPosition + availableLineLength()) { 6049 positionCursor(); 6050 } 6051 } 6052 } 6053 6054 private void charBack() { 6055 if(!cursorPosition) 6056 return; 6057 cursorPosition--; 6058 aligned(cursorPosition, -1); 6059 maybePositionCursor(); 6060 } 6061 private void charForward() { 6062 if(cursorPosition >= line.length) 6063 return; 6064 cursorPosition++; 6065 aligned(cursorPosition, 1); 6066 maybePositionCursor(); 6067 } 6068 6069 int availableLineLength() { 6070 return maximumDrawWidth - promptLength - 1; 6071 } 6072 6073 /++ 6074 Controls the input echo setting. 6075 6076 Possible values are: 6077 6078 `dchar.init` = normal; user can see their input. 6079 6080 `'\0'` = nothing; the cursor does not visually move as they edit. Similar to Unix style password prompts. 6081 6082 `'*'` (or anything else really) = will replace all input characters with stars when displaying, obscure the specific characters, but still showing the number of characters and position of the cursor to the user. 6083 6084 History: 6085 Added October 11, 2021 (dub v10.4) 6086 +/ 6087 dchar echoChar = dchar.init; 6088 6089 protected static struct Drawer { 6090 LineGetter lg; 6091 6092 this(LineGetter lg) { 6093 this.lg = lg; 6094 linesRemaining = lg.terminal.height - 1; 6095 } 6096 6097 int written; 6098 int lineLength; 6099 6100 int linesRemaining; 6101 6102 6103 Color currentFg_ = Color.DEFAULT; 6104 Color currentBg_ = Color.DEFAULT; 6105 int colorChars = 0; 6106 6107 Color currentFg() { 6108 if(colorChars <= 0 || currentFg_ == Color.DEFAULT) 6109 return lg.regularForeground; 6110 return currentFg_; 6111 } 6112 6113 Color currentBg() { 6114 if(colorChars <= 0 || currentBg_ == Color.DEFAULT) 6115 return lg.background; 6116 return currentBg_; 6117 } 6118 6119 void specialChar(char c) { 6120 // maybe i should check echoChar here too but meh 6121 6122 lg.terminal.color(lg.regularForeground, lg.specialCharBackground); 6123 lg.terminal.write(c); 6124 lg.terminal.color(currentFg, currentBg); 6125 6126 written++; 6127 lineLength--; 6128 } 6129 6130 void regularChar(dchar ch) { 6131 import std.utf; 6132 char[4] buffer; 6133 6134 if(lg.echoChar == '\0') 6135 return; 6136 else if(lg.echoChar !is dchar.init) 6137 ch = lg.echoChar; 6138 6139 auto l = encode(buffer, ch); 6140 // note the Terminal buffers it so meh 6141 lg.terminal.write(buffer[0 .. l]); 6142 6143 written++; 6144 lineLength--; 6145 6146 if(lg.multiLineMode) { 6147 if(ch == '\n') { 6148 lineLength = lg.terminal.width; 6149 linesRemaining--; 6150 } 6151 } 6152 } 6153 6154 void drawContent(T)(T towrite, int highlightBegin = 0, int highlightEnd = 0, bool inverted = false, int lineidx = -1) { 6155 // FIXME: if there is a color at the end of the line it messes up as you scroll 6156 // FIXME: need a way to go to multi-line editing 6157 6158 bool highlightOn = false; 6159 void highlightOff() { 6160 lg.terminal.color(currentFg, currentBg, ForceOption.automatic, inverted); 6161 highlightOn = false; 6162 } 6163 6164 foreach(idx, dchar ch; towrite) { 6165 if(linesRemaining <= 0) 6166 break; 6167 if(lineLength <= 0) { 6168 if(lg.multiLineMode) { 6169 if(ch == '\n') { 6170 lineLength = lg.terminal.width; 6171 } 6172 continue; 6173 } else 6174 break; 6175 } 6176 6177 static if(is(T == dchar[])) { 6178 if(lineidx != -1 && colorChars == 0) { 6179 auto shm = lg.syntaxHighlightMatch(lg.line, lineidx + idx, lg.cursorPosition); 6180 if(shm.charsMatched > 0) { 6181 colorChars = shm.charsMatched; 6182 currentFg_ = shm.foreground; 6183 currentBg_ = shm.background; 6184 lg.terminal.color(currentFg, currentBg); 6185 } 6186 } 6187 } 6188 6189 switch(ch) { 6190 case '\n': lg.multiLineMode ? regularChar('\n') : specialChar('n'); break; 6191 case '\r': specialChar('r'); break; 6192 case '\a': specialChar('a'); break; 6193 case '\t': specialChar('t'); break; 6194 case '\b': specialChar('b'); break; 6195 case '\033': specialChar('e'); break; 6196 case '\ ': specialChar(' '); break; 6197 default: 6198 if(highlightEnd) { 6199 if(idx == highlightBegin) { 6200 lg.terminal.color(lg.regularForeground, Color.yellow, ForceOption.automatic, inverted); 6201 highlightOn = true; 6202 } 6203 if(idx == highlightEnd) { 6204 highlightOff(); 6205 } 6206 } 6207 6208 regularChar(ch & ~PRIVATE_BITS_MASK); 6209 } 6210 6211 if(colorChars > 0) { 6212 colorChars--; 6213 if(colorChars == 0) 6214 lg.terminal.color(currentFg, currentBg); 6215 } 6216 } 6217 if(highlightOn) 6218 highlightOff(); 6219 } 6220 6221 } 6222 6223 /++ 6224 If you are implementing a subclass, use this instead of `terminal.width` to see how far you can draw. Use care to remember this is a width, not a right coordinate. 6225 6226 History: 6227 Added May 24, 2021 6228 +/ 6229 final public @property int maximumDrawWidth() { 6230 auto tw = terminal.width - startOfLineX; 6231 if(_drawWidthMax && _drawWidthMax <= tw) 6232 return _drawWidthMax; 6233 return tw; 6234 } 6235 6236 /++ 6237 Sets the maximum width the line getter will use. Set to 0 to disable, in which case it will use the entire width of the terminal. 6238 6239 History: 6240 Added May 24, 2021 6241 +/ 6242 final public @property void maximumDrawWidth(int newMax) { 6243 _drawWidthMax = newMax; 6244 } 6245 6246 /++ 6247 Returns the maximum vertical space available to draw. 6248 6249 Currently, this is always 1. 6250 6251 History: 6252 Added May 24, 2021 6253 +/ 6254 @property int maximumDrawHeight() { 6255 return 1; 6256 } 6257 6258 private int _drawWidthMax = 0; 6259 6260 private int lastDrawLength = 0; 6261 void redraw() { 6262 finalizeRedraw(coreRedraw()); 6263 } 6264 6265 void finalizeRedraw(CoreRedrawInfo cdi) { 6266 if(!cdi.populated) 6267 return; 6268 6269 if(!multiLineMode) { 6270 terminal.clearToEndOfLine(); 6271 /* 6272 if(UseVtSequences && !_drawWidthMax) { 6273 terminal.writeStringRaw("\033[K"); 6274 } else { 6275 // FIXME: graphemes 6276 if(cdi.written + promptLength < lastDrawLength) 6277 foreach(i; cdi.written + promptLength .. lastDrawLength) 6278 terminal.write(" "); 6279 lastDrawLength = cdi.written; 6280 } 6281 */ 6282 // if echoChar is null then we don't want to reflect the position at all 6283 terminal.moveTo(startOfLineX + ((echoChar == 0) ? 0 : cdi.cursorPositionToDrawX) + promptLength, startOfLineY + cdi.cursorPositionToDrawY); 6284 } else { 6285 if(echoChar != 0) 6286 terminal.moveTo(cdi.cursorPositionToDrawX, cdi.cursorPositionToDrawY); 6287 } 6288 endRedraw(); // make sure the cursor is turned back on 6289 } 6290 6291 static struct CoreRedrawInfo { 6292 bool populated; 6293 int written; 6294 int cursorPositionToDrawX; 6295 int cursorPositionToDrawY; 6296 } 6297 6298 private void endRedraw() { 6299 version(Win32Console) { 6300 // on Windows, we want to make sure all 6301 // is displayed before the cursor jumps around 6302 terminal.flush(); 6303 terminal.showCursor(); 6304 } else { 6305 // but elsewhere, the showCursor is itself buffered, 6306 // so we can do it all at once for a slight speed boost 6307 terminal.showCursor(); 6308 //import std.string; import std.stdio; writeln(terminal.writeBuffer.replace("\033", "\\e")); 6309 terminal.flush(); 6310 } 6311 } 6312 6313 final CoreRedrawInfo coreRedraw() { 6314 if(supplementalGetter) 6315 return CoreRedrawInfo.init; // the supplementalGetter will be drawing instead... 6316 terminal.hideCursor(); 6317 scope(failure) { 6318 // don't want to leave the cursor hidden on the event of an exception 6319 // can't just scope(success) it here since the cursor will be seen bouncing when finalizeRedraw is run 6320 endRedraw(); 6321 } 6322 terminal.moveTo(startOfLineX, startOfLineY); 6323 6324 if(multiLineMode) 6325 terminal.clear(); 6326 6327 Drawer drawer = Drawer(this); 6328 6329 drawer.lineLength = availableLineLength(); 6330 if(drawer.lineLength < 0) 6331 throw new Exception("too narrow terminal to draw"); 6332 6333 if(!multiLineMode) { 6334 terminal.color(promptColor, background); 6335 terminal.write(prompt); 6336 terminal.color(regularForeground, background); 6337 } 6338 6339 dchar[] towrite; 6340 6341 if(multiLineMode) { 6342 towrite = line[]; 6343 if(verticalScrollPosition) { 6344 int remaining = verticalScrollPosition; 6345 while(towrite.length) { 6346 if(towrite[0] == '\n') { 6347 towrite = towrite[1 .. $]; 6348 remaining--; 6349 if(remaining == 0) 6350 break; 6351 continue; 6352 } 6353 towrite = towrite[1 .. $]; 6354 } 6355 } 6356 horizontalScrollPosition = 0; // FIXME 6357 } else { 6358 towrite = line[horizontalScrollPosition .. $]; 6359 } 6360 auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; 6361 auto cursorPositionToDrawY = 0; 6362 6363 if(selectionStart != selectionEnd) { 6364 dchar[] beforeSelection, selection, afterSelection; 6365 6366 beforeSelection = line[0 .. selectionStart]; 6367 selection = line[selectionStart .. selectionEnd]; 6368 afterSelection = line[selectionEnd .. $]; 6369 6370 drawer.drawContent(beforeSelection); 6371 terminal.color(regularForeground, background, ForceOption.automatic, true); 6372 drawer.drawContent(selection, 0, 0, true); 6373 terminal.color(regularForeground, background); 6374 drawer.drawContent(afterSelection); 6375 } else { 6376 drawer.drawContent(towrite, 0, 0, false, horizontalScrollPosition); 6377 } 6378 6379 string suggestion; 6380 6381 if(drawer.lineLength >= 0) { 6382 suggestion = ((cursorPosition == towrite.length) && autoSuggest) ? this.suggestion() : null; 6383 if(suggestion.length) { 6384 terminal.color(suggestionForeground, background); 6385 foreach(dchar ch; suggestion) { 6386 if(drawer.lineLength == 0) 6387 break; 6388 drawer.regularChar(ch); 6389 } 6390 terminal.color(regularForeground, background); 6391 } 6392 } 6393 6394 CoreRedrawInfo cri; 6395 cri.populated = true; 6396 cri.written = drawer.written; 6397 if(multiLineMode) { 6398 cursorPositionToDrawX = 0; 6399 cursorPositionToDrawY = 0; 6400 // would be better if it did this in the same drawing pass... 6401 foreach(idx, dchar ch; line) { 6402 if(idx == cursorPosition) 6403 break; 6404 if(ch == '\n') { 6405 cursorPositionToDrawX = 0; 6406 cursorPositionToDrawY++; 6407 } else { 6408 cursorPositionToDrawX++; 6409 } 6410 } 6411 6412 cri.cursorPositionToDrawX = cursorPositionToDrawX - horizontalScrollPosition; 6413 cri.cursorPositionToDrawY = cursorPositionToDrawY - verticalScrollPosition; 6414 } else { 6415 cri.cursorPositionToDrawX = cursorPositionToDrawX; 6416 cri.cursorPositionToDrawY = cursorPositionToDrawY; 6417 } 6418 6419 return cri; 6420 } 6421 6422 /// Starts getting a new line. Call workOnLine and finishGettingLine afterward. 6423 /// 6424 /// Make sure that you've flushed your input and output before calling this 6425 /// function or else you might lose events or get exceptions from this. 6426 void startGettingLine() { 6427 // reset from any previous call first 6428 if(!maintainBuffer) { 6429 cursorPosition = 0; 6430 horizontalScrollPosition = 0; 6431 verticalScrollPosition = 0; 6432 justHitTab = false; 6433 currentHistoryViewPosition = 0; 6434 if(line.length) { 6435 line = line[0 .. 0]; 6436 line.assumeSafeAppend(); 6437 } 6438 } 6439 6440 maintainBuffer = false; 6441 6442 initializeWithSize(true); 6443 6444 terminal.cursor = TerminalCursor.insert; 6445 terminal.showCursor(); 6446 } 6447 6448 private void positionCursor() { 6449 if(cursorPosition == 0) { 6450 horizontalScrollPosition = 0; 6451 verticalScrollPosition = 0; 6452 } else if(cursorPosition == line.length) { 6453 scrollToEnd(); 6454 } else { 6455 if(multiLineMode) { 6456 // FIXME 6457 maybePositionCursor(); 6458 } else { 6459 // otherwise just try to center it in the screen 6460 horizontalScrollPosition = cursorPosition; 6461 horizontalScrollPosition -= maximumDrawWidth / 2; 6462 // align on a code point boundary 6463 aligned(horizontalScrollPosition, -1); 6464 if(horizontalScrollPosition < 0) 6465 horizontalScrollPosition = 0; 6466 } 6467 } 6468 } 6469 6470 private void aligned(ref int what, int direction) { 6471 // whereas line is right now dchar[] no need for this 6472 // at least until we go by grapheme... 6473 /* 6474 while(what > 0 && what < line.length && ((line[what] & 0b1100_0000) == 0b1000_0000)) 6475 what += direction; 6476 */ 6477 } 6478 6479 protected void initializeWithSize(bool firstEver = false) { 6480 auto x = startOfLineX; 6481 6482 updateCursorPosition(); 6483 6484 if(!firstEver) { 6485 startOfLineX = x; 6486 positionCursor(); 6487 } 6488 6489 lastDrawLength = maximumDrawWidth; 6490 version(Win32Console) 6491 lastDrawLength -= 1; // I don't like this but Windows resizing is different anyway and it is liable to scroll if i go over.. 6492 6493 redraw(); 6494 } 6495 6496 protected void updateCursorPosition() { 6497 terminal.updateCursorPosition(); 6498 6499 startOfLineX = terminal.cursorX; 6500 startOfLineY = terminal.cursorY; 6501 } 6502 6503 // Text killed with C-w/C-u/C-k/C-backspace, to be restored by C-y 6504 private dchar[] killBuffer; 6505 6506 // Given 'a b c d|', C-w C-w C-y should kill c and d, and then restore both 6507 // But given 'a b c d|', C-w M-b C-w C-y should kill d, kill b, and then restore only b 6508 // So we need this extra bit of state to decide whether to append to or replace the kill buffer 6509 // when the user kills some text 6510 private bool justKilled; 6511 6512 private bool justHitTab; 6513 private bool eof; 6514 6515 /// 6516 string delegate(string s) pastePreprocessor; 6517 6518 string defaultPastePreprocessor(string s) { 6519 return s; 6520 } 6521 6522 void showIndividualHelp(string help) { 6523 terminal.writeln(); 6524 terminal.writeln(help); 6525 } 6526 6527 private bool maintainBuffer; 6528 6529 /++ 6530 Returns true if the last line was retained by the user via the F9 or ctrl+enter key 6531 which runs it but keeps it in the edit buffer. 6532 6533 This is only valid inside [finishGettingLine] or immediately after [finishGettingLine] 6534 returns, but before [startGettingLine] is called again. 6535 6536 History: 6537 Added October 12, 2021 6538 +/ 6539 final public bool lastLineWasRetained() const { 6540 return maintainBuffer; 6541 } 6542 6543 private LineGetter supplementalGetter; 6544 6545 /* selection helpers */ 6546 protected { 6547 // make sure you set the anchor first 6548 void extendSelectionToCursor() { 6549 if(cursorPosition < selectionStart) 6550 selectionStart = cursorPosition; 6551 else if(cursorPosition > selectionEnd) 6552 selectionEnd = cursorPosition; 6553 6554 terminal.requestSetTerminalSelection(getSelection()); 6555 } 6556 void setSelectionAnchorToCursor() { 6557 if(selectionStart == -1) 6558 selectionStart = selectionEnd = cursorPosition; 6559 } 6560 void sanitizeSelection() { 6561 if(selectionStart == selectionEnd) 6562 return; 6563 6564 if(selectionStart < 0 || selectionEnd < 0 || selectionStart > line.length || selectionEnd > line.length) 6565 selectNone(); 6566 } 6567 } 6568 public { 6569 // redraw after calling this 6570 void selectAll() { 6571 selectionStart = 0; 6572 selectionEnd = cast(int) line.length; 6573 } 6574 6575 // redraw after calling this 6576 void selectNone() { 6577 selectionStart = selectionEnd = -1; 6578 } 6579 6580 string getSelection() { 6581 sanitizeSelection(); 6582 if(selectionStart == selectionEnd) 6583 return null; 6584 import std.conv; 6585 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6586 return to!string(line[selectionStart .. selectionEnd]); 6587 } 6588 } 6589 private { 6590 int selectionStart = -1; 6591 int selectionEnd = -1; 6592 } 6593 6594 void backwardToNewline() { 6595 while(cursorPosition && line[cursorPosition - 1] != '\n') 6596 cursorPosition--; 6597 phantomCursorX = 0; 6598 } 6599 6600 void forwardToNewLine() { 6601 while(cursorPosition < line.length && line[cursorPosition] != '\n') 6602 cursorPosition++; 6603 } 6604 6605 private int phantomCursorX; 6606 6607 void lineBackward() { 6608 int count; 6609 while(cursorPosition && line[cursorPosition - 1] != '\n') { 6610 cursorPosition--; 6611 count++; 6612 } 6613 if(count > phantomCursorX) 6614 phantomCursorX = count; 6615 6616 if(cursorPosition == 0) 6617 return; 6618 cursorPosition--; 6619 6620 while(cursorPosition && line[cursorPosition - 1] != '\n') { 6621 cursorPosition--; 6622 } 6623 6624 count = phantomCursorX; 6625 while(count) { 6626 if(cursorPosition == line.length) 6627 break; 6628 if(line[cursorPosition] == '\n') 6629 break; 6630 cursorPosition++; 6631 count--; 6632 } 6633 } 6634 6635 void lineForward() { 6636 int count; 6637 6638 // see where we are in the current line 6639 auto beginPos = cursorPosition; 6640 while(beginPos && line[beginPos - 1] != '\n') { 6641 beginPos--; 6642 count++; 6643 } 6644 6645 if(count > phantomCursorX) 6646 phantomCursorX = count; 6647 6648 // get to the next line 6649 while(cursorPosition < line.length && line[cursorPosition] != '\n') { 6650 cursorPosition++; 6651 } 6652 if(cursorPosition == line.length) 6653 return; 6654 cursorPosition++; 6655 6656 // get to the same spot in this same line 6657 count = phantomCursorX; 6658 while(count) { 6659 if(cursorPosition == line.length) 6660 break; 6661 if(line[cursorPosition] == '\n') 6662 break; 6663 cursorPosition++; 6664 count--; 6665 } 6666 } 6667 6668 void pageBackward() { 6669 foreach(count; 0 .. terminal.height) 6670 lineBackward(); 6671 maybePositionCursor(); 6672 } 6673 6674 void pageForward() { 6675 foreach(count; 0 .. terminal.height) 6676 lineForward(); 6677 maybePositionCursor(); 6678 } 6679 6680 bool isSearchingHistory() { 6681 return supplementalGetter !is null; 6682 } 6683 6684 /++ 6685 Cancels an in-progress history search immediately, discarding the result, returning 6686 to the normal prompt. 6687 6688 If the user is not currently searching history (see [isSearchingHistory]), this 6689 function does nothing. 6690 +/ 6691 void cancelHistorySearch() { 6692 if(isSearchingHistory()) { 6693 lastDrawLength = maximumDrawWidth - 1; 6694 supplementalGetter = null; 6695 redraw(); 6696 } 6697 } 6698 6699 /++ 6700 for integrating into another event loop 6701 you can pass individual events to this and 6702 the line getter will work on it 6703 6704 returns false when there's nothing more to do 6705 6706 History: 6707 On February 17, 2020, it was changed to take 6708 a new argument which should be the input source 6709 where the event came from. 6710 +/ 6711 bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) { 6712 if(supplementalGetter) { 6713 if(!supplementalGetter.workOnLine(e, rtti)) { 6714 auto got = supplementalGetter.finishGettingLine(); 6715 // the supplementalGetter will poke our own state directly 6716 // so i can ignore the return value here... 6717 6718 // but i do need to ensure we clear any 6719 // stuff left on the screen from it. 6720 lastDrawLength = maximumDrawWidth - 1; 6721 supplementalGetter = null; 6722 redraw(); 6723 } 6724 return true; 6725 } 6726 6727 switch(e.type) { 6728 case InputEvent.Type.EndOfFileEvent: 6729 justHitTab = false; 6730 eof = true; 6731 // FIXME: this should be distinct from an empty line when hit at the beginning 6732 return false; 6733 //break; 6734 case InputEvent.Type.KeyboardEvent: 6735 auto ev = e.keyboardEvent; 6736 if(ev.pressed == false) 6737 return true; 6738 /* Insert the character (unless it is backspace, tab, or some other control char) */ 6739 auto ch = ev.which; 6740 switch(ch) { 6741 case KeyboardEvent.ProprietaryPseudoKeys.SelectNone: 6742 selectNone(); 6743 redraw(); 6744 break; 6745 version(Windows) case 'z', 26: { // and this is really for Windows 6746 if(!(ev.modifierState & ModifierState.control)) 6747 goto default; 6748 goto case; 6749 } 6750 case 'd', 4: // ctrl+d will also send a newline-equivalent 6751 if(ev.modifierState & ModifierState.alt) { 6752 // gnu alias for kill word (also on ctrl+backspace) 6753 justHitTab = false; 6754 lineChanged = true; 6755 killWordForward(); 6756 justKilled = true; 6757 redraw(); 6758 break; 6759 } 6760 if(!(ev.modifierState & ModifierState.control)) 6761 goto default; 6762 if(line.length == 0) 6763 eof = true; 6764 justHitTab = justKilled = false; 6765 return false; // indicate end of line so it doesn't maintain the buffer thinking it was ctrl+enter 6766 case '\r': 6767 case '\n': 6768 justHitTab = justKilled = false; 6769 if(ev.modifierState & ModifierState.control) { 6770 goto case KeyboardEvent.Key.F9; 6771 } 6772 if(ev.modifierState & ModifierState.shift) { 6773 addChar('\n'); 6774 redraw(); 6775 break; 6776 } 6777 return false; 6778 case '\t': 6779 justKilled = false; 6780 6781 if(ev.modifierState & ModifierState.shift) { 6782 justHitTab = false; 6783 addChar('\t'); 6784 redraw(); 6785 break; 6786 } 6787 6788 // I want to hide the private bits from the other functions, but retain them across completions, 6789 // which is why it does it on a copy here. Could probably be more efficient, but meh. 6790 auto line = this.line.dup; 6791 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6792 6793 auto relevantLineSection = line[0 .. cursorPosition]; 6794 auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); 6795 relevantLineSection = relevantLineSection[start .. $]; 6796 auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); 6797 import std.utf; 6798 6799 if(possibilities.length == 1) { 6800 auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $]; 6801 if(toFill.length) { 6802 addString(toFill); 6803 redraw(); 6804 } else { 6805 auto help = this.tabCompleteHelp(possibilities[0]); 6806 if(help.length) { 6807 showIndividualHelp(help); 6808 updateCursorPosition(); 6809 redraw(); 6810 } 6811 } 6812 justHitTab = false; 6813 } else { 6814 if(justHitTab) { 6815 justHitTab = false; 6816 showTabCompleteList(possibilities); 6817 } else { 6818 justHitTab = true; 6819 /* fill it in with as much commonality as there is amongst all the suggestions */ 6820 auto suggestion = this.suggestion(possibilities); 6821 if(suggestion.length) { 6822 addString(suggestion); 6823 redraw(); 6824 } 6825 } 6826 } 6827 break; 6828 case '\b': 6829 justHitTab = false; 6830 // i use control for delete word, but gnu uses alt. so this allows both 6831 if(ev.modifierState & (ModifierState.control | ModifierState.alt)) { 6832 lineChanged = true; 6833 killWord(); 6834 justKilled = true; 6835 redraw(); 6836 } else if(cursorPosition) { 6837 lineChanged = true; 6838 justKilled = false; 6839 cursorPosition--; 6840 for(int i = cursorPosition; i < line.length - 1; i++) 6841 line[i] = line[i + 1]; 6842 line = line[0 .. $ - 1]; 6843 line.assumeSafeAppend(); 6844 6845 if(multiLineMode) { 6846 // FIXME 6847 } else { 6848 if(horizontalScrollPosition > cursorPosition - 1) 6849 horizontalScrollPosition = cursorPosition - 1 - availableLineLength(); 6850 if(horizontalScrollPosition < 0) 6851 horizontalScrollPosition = 0; 6852 } 6853 6854 redraw(); 6855 } 6856 phantomCursorX = 0; 6857 break; 6858 case KeyboardEvent.Key.escape: 6859 justHitTab = justKilled = false; 6860 if(multiLineMode) 6861 multiLineMode = false; 6862 else { 6863 cursorPosition = 0; 6864 horizontalScrollPosition = 0; 6865 line = line[0 .. 0]; 6866 line.assumeSafeAppend(); 6867 } 6868 redraw(); 6869 break; 6870 case KeyboardEvent.Key.F1: 6871 justHitTab = justKilled = false; 6872 showHelp(); 6873 break; 6874 case KeyboardEvent.Key.F2: 6875 justHitTab = justKilled = false; 6876 6877 if(ev.modifierState & ModifierState.control) { 6878 toggleMultiLineMode(); 6879 break; 6880 } 6881 6882 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6883 auto got = editLineInEditor(line, cursorPosition); 6884 if(got !is null) { 6885 line = got; 6886 if(cursorPosition > line.length) 6887 cursorPosition = cast(int) line.length; 6888 if(horizontalScrollPosition > line.length) 6889 horizontalScrollPosition = cast(int) line.length; 6890 positionCursor(); 6891 redraw(); 6892 } 6893 break; 6894 case '(': 6895 if(!(ev.modifierState & ModifierState.alt)) 6896 goto default; 6897 justHitTab = justKilled = false; 6898 addChar('('); 6899 addChar(cast(dchar) (')' | PRIVATE_BITS_MASK)); 6900 charBack(); 6901 redraw(); 6902 break; 6903 case 'l', 12: 6904 if(!(ev.modifierState & ModifierState.control)) 6905 goto default; 6906 goto case; 6907 case KeyboardEvent.Key.F5: 6908 // FIXME: I might not want to do this on full screen programs, 6909 // but arguably the application should just hook the event then. 6910 terminal.clear(); 6911 updateCursorPosition(); 6912 redraw(); 6913 break; 6914 case 'r', 18: 6915 if(!(ev.modifierState & ModifierState.control)) 6916 goto default; 6917 goto case; 6918 case KeyboardEvent.Key.F3: 6919 justHitTab = justKilled = false; 6920 // search in history 6921 // FIXME: what about search in completion too? 6922 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6923 supplementalGetter = new HistorySearchLineGetter(this); 6924 supplementalGetter.startGettingLine(); 6925 supplementalGetter.redraw(); 6926 break; 6927 case 'u', 21: 6928 if(!(ev.modifierState & ModifierState.control)) 6929 goto default; 6930 goto case; 6931 case KeyboardEvent.Key.F4: 6932 killText(line); 6933 line = []; 6934 cursorPosition = 0; 6935 justHitTab = false; 6936 justKilled = true; 6937 redraw(); 6938 break; 6939 // btw alt+enter could be alias for F9? 6940 case KeyboardEvent.Key.F9: 6941 justHitTab = justKilled = false; 6942 // compile and run analog; return the current string 6943 // but keep the buffer the same 6944 6945 maintainBuffer = true; 6946 return false; 6947 case '5', 0x1d: // ctrl+5, because of vim % shortcut 6948 if(!(ev.modifierState & ModifierState.control)) 6949 goto default; 6950 justHitTab = justKilled = false; 6951 // FIXME: would be cool if this worked with quotes and such too 6952 // FIXME: in insert mode prolly makes sense to look at the position before the cursor tbh 6953 if(cursorPosition >= 0 && cursorPosition < line.length) { 6954 dchar at = line[cursorPosition] & ~PRIVATE_BITS_MASK; 6955 int direction; 6956 dchar lookFor; 6957 switch(at) { 6958 case '(': direction = 1; lookFor = ')'; break; 6959 case '[': direction = 1; lookFor = ']'; break; 6960 case '{': direction = 1; lookFor = '}'; break; 6961 case ')': direction = -1; lookFor = '('; break; 6962 case ']': direction = -1; lookFor = '['; break; 6963 case '}': direction = -1; lookFor = '{'; break; 6964 default: 6965 } 6966 if(direction) { 6967 int pos = cursorPosition; 6968 int count; 6969 while(pos >= 0 && pos < line.length) { 6970 auto lp = line[pos] & ~PRIVATE_BITS_MASK; 6971 if(lp == at) 6972 count++; 6973 if(lp == lookFor) 6974 count--; 6975 if(count == 0) { 6976 cursorPosition = pos; 6977 redraw(); 6978 break; 6979 } 6980 pos += direction; 6981 } 6982 } 6983 } 6984 break; 6985 6986 // FIXME: should be able to update the selection with shift+arrows as well as mouse 6987 // if terminal emulator supports this, it can formally select it to the buffer for copy 6988 // and sending to primary on X11 (do NOT do it on Windows though!!!) 6989 case 'b', 2: 6990 if(ev.modifierState & ModifierState.alt) 6991 wordBack(); 6992 else if(ev.modifierState & ModifierState.control) 6993 charBack(); 6994 else 6995 goto default; 6996 justHitTab = justKilled = false; 6997 redraw(); 6998 break; 6999 case 'f', 6: 7000 if(ev.modifierState & ModifierState.alt) 7001 wordForward(); 7002 else if(ev.modifierState & ModifierState.control) 7003 charForward(); 7004 else 7005 goto default; 7006 justHitTab = justKilled = false; 7007 redraw(); 7008 break; 7009 case KeyboardEvent.Key.LeftArrow: 7010 justHitTab = justKilled = false; 7011 phantomCursorX = 0; 7012 7013 /* 7014 if(ev.modifierState & ModifierState.shift) 7015 setSelectionAnchorToCursor(); 7016 */ 7017 7018 if(ev.modifierState & ModifierState.control) 7019 wordBack(); 7020 else if(cursorPosition) 7021 charBack(); 7022 7023 /* 7024 if(ev.modifierState & ModifierState.shift) 7025 extendSelectionToCursor(); 7026 */ 7027 7028 redraw(); 7029 break; 7030 case KeyboardEvent.Key.RightArrow: 7031 justHitTab = justKilled = false; 7032 if(ev.modifierState & ModifierState.control) 7033 wordForward(); 7034 else 7035 charForward(); 7036 redraw(); 7037 break; 7038 case 'p', 16: 7039 if(ev.modifierState & ModifierState.control) 7040 goto case; 7041 goto default; 7042 case KeyboardEvent.Key.UpArrow: 7043 justHitTab = justKilled = false; 7044 if(multiLineMode) { 7045 lineBackward(); 7046 maybePositionCursor(); 7047 } else 7048 loadFromHistory(currentHistoryViewPosition + 1); 7049 redraw(); 7050 break; 7051 case 'n', 14: 7052 if(ev.modifierState & ModifierState.control) 7053 goto case; 7054 goto default; 7055 case KeyboardEvent.Key.DownArrow: 7056 justHitTab = justKilled = false; 7057 if(multiLineMode) { 7058 lineForward(); 7059 maybePositionCursor(); 7060 } else 7061 loadFromHistory(currentHistoryViewPosition - 1); 7062 redraw(); 7063 break; 7064 case KeyboardEvent.Key.PageUp: 7065 justHitTab = justKilled = false; 7066 if(multiLineMode) 7067 pageBackward(); 7068 else 7069 loadFromHistory(cast(int) history.length); 7070 redraw(); 7071 break; 7072 case KeyboardEvent.Key.PageDown: 7073 justHitTab = justKilled = false; 7074 if(multiLineMode) 7075 pageForward(); 7076 else 7077 loadFromHistory(0); 7078 redraw(); 7079 break; 7080 case 'a', 1: // this one conflicts with Windows-style select all... 7081 if(!(ev.modifierState & ModifierState.control)) 7082 goto default; 7083 if(ev.modifierState & ModifierState.shift) { 7084 // ctrl+shift+a will select all... 7085 // for now I will have it just copy to clipboard but later once I get the time to implement full selection handling, I'll change it 7086 terminal.requestCopyToClipboard(lineAsString()); 7087 break; 7088 } 7089 goto case; 7090 case KeyboardEvent.Key.Home: 7091 justHitTab = justKilled = false; 7092 if(multiLineMode) { 7093 backwardToNewline(); 7094 } else { 7095 cursorPosition = 0; 7096 } 7097 horizontalScrollPosition = 0; 7098 redraw(); 7099 break; 7100 case 'e', 5: 7101 if(!(ev.modifierState & ModifierState.control)) 7102 goto default; 7103 goto case; 7104 case KeyboardEvent.Key.End: 7105 justHitTab = justKilled = false; 7106 if(multiLineMode) { 7107 forwardToNewLine(); 7108 } else { 7109 cursorPosition = cast(int) line.length; 7110 scrollToEnd(); 7111 } 7112 redraw(); 7113 break; 7114 case 'v', 22: 7115 if(!(ev.modifierState & ModifierState.control)) 7116 goto default; 7117 justKilled = false; 7118 if(rtti) 7119 rtti.requestPasteFromClipboard(); 7120 break; 7121 case KeyboardEvent.Key.Insert: 7122 justHitTab = justKilled = false; 7123 if(ev.modifierState & ModifierState.shift) { 7124 // paste 7125 7126 // shift+insert = request paste 7127 // ctrl+insert = request copy. but that needs a selection 7128 7129 // those work on Windows!!!! and many linux TEs too. 7130 // but if it does make it here, we'll attempt it at this level 7131 if(rtti) 7132 rtti.requestPasteFromClipboard(); 7133 } else if(ev.modifierState & ModifierState.control) { 7134 // copy 7135 // FIXME we could try requesting it though this control unlikely to even come 7136 } else { 7137 insertMode = !insertMode; 7138 7139 if(insertMode) 7140 terminal.cursor = TerminalCursor.insert; 7141 else 7142 terminal.cursor = TerminalCursor.block; 7143 } 7144 break; 7145 case KeyboardEvent.Key.Delete: 7146 justHitTab = false; 7147 if(ev.modifierState & ModifierState.control) { 7148 deleteToEndOfLine(); 7149 justKilled = true; 7150 } else { 7151 deleteChar(); 7152 justKilled = false; 7153 } 7154 redraw(); 7155 break; 7156 case 'k', 11: 7157 if(!(ev.modifierState & ModifierState.control)) 7158 goto default; 7159 deleteToEndOfLine(); 7160 justHitTab = false; 7161 justKilled = true; 7162 redraw(); 7163 break; 7164 case 'w', 23: 7165 if(!(ev.modifierState & ModifierState.control)) 7166 goto default; 7167 killWord(); 7168 justHitTab = false; 7169 justKilled = true; 7170 redraw(); 7171 break; 7172 case 'y', 25: 7173 if(!(ev.modifierState & ModifierState.control)) 7174 goto default; 7175 justHitTab = justKilled = false; 7176 foreach(c; killBuffer) 7177 addChar(c); 7178 redraw(); 7179 break; 7180 default: 7181 justHitTab = justKilled = false; 7182 if(e.keyboardEvent.isCharacter) { 7183 7184 // overstrike an auto-inserted thing if that's right there 7185 if(cursorPosition < line.length) 7186 if(line[cursorPosition] & PRIVATE_BITS_MASK) { 7187 if((line[cursorPosition] & ~PRIVATE_BITS_MASK) == ch) { 7188 line[cursorPosition] = ch; 7189 cursorPosition++; 7190 redraw(); 7191 break; 7192 } 7193 } 7194 7195 7196 7197 // the ordinary add, of course 7198 addChar(ch); 7199 7200 7201 // and auto-insert a closing pair if appropriate 7202 auto autoChars = enableAutoCloseBrackets(); 7203 bool found = false; 7204 foreach(idx, dchar ac; autoChars) { 7205 if(found) { 7206 addChar(ac | PRIVATE_BITS_MASK); 7207 charBack(); 7208 break; 7209 } 7210 if((idx&1) == 0 && ac == ch) 7211 found = true; 7212 } 7213 } 7214 redraw(); 7215 } 7216 break; 7217 case InputEvent.Type.PasteEvent: 7218 justHitTab = false; 7219 if(pastePreprocessor) 7220 addString(pastePreprocessor(e.pasteEvent.pastedText)); 7221 else 7222 addString(defaultPastePreprocessor(e.pasteEvent.pastedText)); 7223 redraw(); 7224 break; 7225 case InputEvent.Type.MouseEvent: 7226 /* Clicking with the mouse to move the cursor is so much easier than arrowing 7227 or even emacs/vi style movements much of the time, so I'ma support it. */ 7228 7229 auto me = e.mouseEvent; 7230 if(me.eventType == MouseEvent.Type.Pressed) { 7231 if(me.buttons & MouseEvent.Button.Left) { 7232 if(multiLineMode) { 7233 // FIXME 7234 } else if(me.y == startOfLineY) { // single line only processes on itself 7235 int p = me.x - startOfLineX - promptLength + horizontalScrollPosition; 7236 if(p >= 0 && p < line.length) { 7237 justHitTab = false; 7238 cursorPosition = p; 7239 redraw(); 7240 } 7241 } 7242 } 7243 if(me.buttons & MouseEvent.Button.Middle) { 7244 if(rtti) 7245 rtti.requestPasteFromPrimary(); 7246 } 7247 } 7248 break; 7249 case InputEvent.Type.LinkEvent: 7250 if(handleLinkEvent !is null) 7251 handleLinkEvent(e.linkEvent, this); 7252 break; 7253 case InputEvent.Type.SizeChangedEvent: 7254 /* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent 7255 yourself and then don't pass it to this function. */ 7256 // FIXME 7257 initializeWithSize(); 7258 break; 7259 case InputEvent.Type.CustomEvent: 7260 if(auto rce = cast(RunnableCustomEvent) e.customEvent) 7261 rce.run(); 7262 break; 7263 case InputEvent.Type.UserInterruptionEvent: 7264 /* I'll take this as canceling the line. */ 7265 throw new UserInterruptionException(); 7266 //break; 7267 case InputEvent.Type.HangupEvent: 7268 /* I'll take this as canceling the line. */ 7269 throw new HangupException(); 7270 //break; 7271 default: 7272 /* ignore. ideally it wouldn't be passed to us anyway! */ 7273 } 7274 7275 return true; 7276 } 7277 7278 /++ 7279 Gives a convenience hook for subclasses to handle my terminal's hyperlink extension. 7280 7281 7282 You can also handle these by filtering events before you pass them to [workOnLine]. 7283 That's still how I recommend handling any overrides or custom events, but making this 7284 a delegate is an easy way to inject handlers into an otherwise linear i/o application. 7285 7286 Does nothing if null. 7287 7288 It passes the event as well as the current line getter to the delegate. You may simply 7289 `lg.addString(ev.text); lg.redraw();` in some cases. 7290 7291 History: 7292 Added April 2, 2021. 7293 7294 See_Also: 7295 [Terminal.hyperlink] 7296 7297 [TerminalCapabilities.arsdHyperlinks] 7298 +/ 7299 void delegate(LinkEvent ev, LineGetter lg) handleLinkEvent; 7300 7301 /++ 7302 Replaces the line currently being edited with the given line and positions the cursor inside it. 7303 7304 History: 7305 Added November 27, 2020. 7306 +/ 7307 void replaceLine(const scope dchar[] line) { 7308 if(this.line.length < line.length) 7309 this.line.length = line.length; 7310 else 7311 this.line = this.line[0 .. line.length]; 7312 this.line.assumeSafeAppend(); 7313 this.line[] = line[]; 7314 if(cursorPosition > line.length) 7315 cursorPosition = cast(int) line.length; 7316 if(multiLineMode) { 7317 // FIXME? 7318 horizontalScrollPosition = 0; 7319 verticalScrollPosition = 0; 7320 } else { 7321 if(horizontalScrollPosition > line.length) 7322 horizontalScrollPosition = cast(int) line.length; 7323 } 7324 positionCursor(); 7325 } 7326 7327 /// ditto 7328 void replaceLine(const scope char[] line) { 7329 if(line.length >= 255) { 7330 import std.conv; 7331 replaceLine(to!dstring(line)); 7332 return; 7333 } 7334 dchar[255] tmp; 7335 size_t idx; 7336 foreach(dchar c; line) { 7337 tmp[idx++] = c; 7338 } 7339 7340 replaceLine(tmp[0 .. idx]); 7341 } 7342 7343 /++ 7344 Gets the current line buffer as a duplicated string. 7345 7346 History: 7347 Added January 25, 2021 7348 +/ 7349 string lineAsString() { 7350 import std.conv; 7351 7352 // FIXME: I should prolly not do this on the internal copy but it isn't a huge deal 7353 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 7354 7355 return to!string(line); 7356 } 7357 7358 /// 7359 string finishGettingLine() { 7360 import std.conv; 7361 7362 7363 if(multiLineMode) 7364 multiLineMode = false; 7365 7366 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 7367 7368 auto f = to!string(line); 7369 auto history = historyFilter(f); 7370 if(history !is null) { 7371 this.history ~= history; 7372 if(this.historyCommitMode == HistoryCommitMode.afterEachLine) 7373 appendHistoryToFile(history); 7374 } 7375 7376 // FIXME: we should hide the cursor if it was hidden in the call to startGettingLine 7377 7378 // also need to reset the color going forward 7379 terminal.color(Color.DEFAULT, Color.DEFAULT); 7380 7381 return eof ? null : f.length ? f : ""; 7382 } 7383 } 7384 7385 class HistorySearchLineGetter : LineGetter { 7386 LineGetter basedOn; 7387 string sideDisplay; 7388 this(LineGetter basedOn) { 7389 this.basedOn = basedOn; 7390 super(basedOn.terminal); 7391 } 7392 7393 override void updateCursorPosition() { 7394 super.updateCursorPosition(); 7395 startOfLineX = basedOn.startOfLineX; 7396 startOfLineY = basedOn.startOfLineY; 7397 } 7398 7399 override void initializeWithSize(bool firstEver = false) { 7400 if(maximumDrawWidth > 60) 7401 this.prompt = "(history search): \""; 7402 else 7403 this.prompt = "(hs): \""; 7404 super.initializeWithSize(firstEver); 7405 } 7406 7407 override int availableLineLength() { 7408 return maximumDrawWidth / 2 - promptLength - 1; 7409 } 7410 7411 override void loadFromHistory(int howFarBack) { 7412 currentHistoryViewPosition = howFarBack; 7413 reloadSideDisplay(); 7414 } 7415 7416 int highlightBegin; 7417 int highlightEnd; 7418 7419 void reloadSideDisplay() { 7420 import std.string; 7421 import std.range; 7422 int counter = currentHistoryViewPosition; 7423 7424 string lastHit; 7425 int hb, he; 7426 if(line.length) 7427 foreach_reverse(item; basedOn.history) { 7428 auto idx = item.indexOf(line); 7429 if(idx != -1) { 7430 hb = cast(int) idx; 7431 he = cast(int) (idx + line.walkLength); 7432 lastHit = item; 7433 if(counter) 7434 counter--; 7435 else 7436 break; 7437 } 7438 } 7439 sideDisplay = lastHit; 7440 highlightBegin = hb; 7441 highlightEnd = he; 7442 redraw(); 7443 } 7444 7445 7446 bool redrawQueued = false; 7447 override void redraw() { 7448 redrawQueued = true; 7449 } 7450 7451 void actualRedraw() { 7452 auto cri = coreRedraw(); 7453 terminal.write("\" "); 7454 7455 int available = maximumDrawWidth / 2 - 1; 7456 auto used = prompt.length + cri.written + 3 /* the write above plus a space */; 7457 if(used < available) 7458 available += available - used; 7459 7460 //terminal.moveTo(maximumDrawWidth / 2, startOfLineY); 7461 Drawer drawer = Drawer(this); 7462 drawer.lineLength = available; 7463 drawer.drawContent(sideDisplay, highlightBegin, highlightEnd); 7464 7465 cri.written += drawer.written; 7466 7467 finalizeRedraw(cri); 7468 } 7469 7470 override bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) { 7471 scope(exit) { 7472 if(redrawQueued) { 7473 actualRedraw(); 7474 redrawQueued = false; 7475 } 7476 } 7477 if(e.type == InputEvent.Type.KeyboardEvent) { 7478 auto ev = e.keyboardEvent; 7479 if(ev.pressed == false) 7480 return true; 7481 /* Insert the character (unless it is backspace, tab, or some other control char) */ 7482 auto ch = ev.which; 7483 switch(ch) { 7484 // modification being the search through history commands 7485 // should just keep searching, not endlessly nest. 7486 case 'r', 18: 7487 if(!(ev.modifierState & ModifierState.control)) 7488 goto default; 7489 goto case; 7490 case KeyboardEvent.Key.F3: 7491 e.keyboardEvent.which = KeyboardEvent.Key.UpArrow; 7492 break; 7493 case KeyboardEvent.Key.escape: 7494 sideDisplay = null; 7495 return false; // cancel 7496 default: 7497 } 7498 } 7499 if(super.workOnLine(e, rtti)) { 7500 if(lineChanged) { 7501 currentHistoryViewPosition = 0; 7502 reloadSideDisplay(); 7503 lineChanged = false; 7504 } 7505 return true; 7506 } 7507 return false; 7508 } 7509 7510 override void startGettingLine() { 7511 super.startGettingLine(); 7512 this.line = basedOn.line.dup; 7513 cursorPosition = cast(int) this.line.length; 7514 startOfLineX = basedOn.startOfLineX; 7515 startOfLineY = basedOn.startOfLineY; 7516 positionCursor(); 7517 reloadSideDisplay(); 7518 } 7519 7520 override string finishGettingLine() { 7521 auto got = super.finishGettingLine(); 7522 7523 if(sideDisplay.length) 7524 basedOn.replaceLine(sideDisplay); 7525 7526 return got; 7527 } 7528 } 7529 7530 /// Adds default constructors that just forward to the superclass 7531 mixin template LineGetterConstructors() { 7532 this(Terminal* tty, string historyFilename = null) { 7533 super(tty, historyFilename); 7534 } 7535 } 7536 7537 /// This is a line getter that customizes the tab completion to 7538 /// fill in file names separated by spaces, like a command line thing. 7539 class FileLineGetter : LineGetter { 7540 mixin LineGetterConstructors; 7541 7542 /// You can set this property to tell it where to search for the files 7543 /// to complete. 7544 string searchDirectory = "."; 7545 7546 override size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { 7547 import std.string; 7548 return candidate.lastIndexOf(" ") + 1; 7549 } 7550 7551 override protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { 7552 import std.file, std.conv, std.algorithm, std.string; 7553 7554 string[] list; 7555 foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) { 7556 // both with and without the (searchDirectory ~ "/") 7557 list ~= name[searchDirectory.length + 1 .. $]; 7558 list ~= name[0 .. $]; 7559 } 7560 7561 return list; 7562 } 7563 } 7564 7565 /+ 7566 class FullscreenEditor { 7567 7568 } 7569 +/ 7570 7571 7572 version(Windows) { 7573 // to get the directory for saving history in the line things 7574 enum CSIDL_APPDATA = 26; 7575 extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); 7576 } 7577 7578 7579 7580 7581 7582 /* Like getting a line, printing a lot of lines is kinda important too, so I'm including 7583 that widget here too. */ 7584 7585 7586 /++ 7587 The ScrollbackBuffer is a writable in-memory terminal that can be drawn to a real [Terminal] 7588 and maintain some internal position state by handling events. It is your responsibility to 7589 draw it (using the [drawInto] method) and dispatch events to its [handleEvent] method (if you 7590 want to, you can also just call the methods yourself). 7591 7592 7593 I originally wrote this to support my irc client and some of the features are geared toward 7594 helping with that (for example, [name] and [demandsAttention]), but the main thrust is to 7595 support either tabs or sub-sections of the terminal having their own output that can be displayed 7596 and scrolled back independently while integrating with some larger application. 7597 7598 History: 7599 Committed to git on August 4, 2015. 7600 7601 Cleaned up and documented on May 25, 2021. 7602 +/ 7603 struct ScrollbackBuffer { 7604 /++ 7605 A string you can set and process on your own. The library only sets it from the 7606 constructor, then leaves it alone. 7607 7608 In my irc client, I use this as the title of a tab I draw to indicate separate 7609 conversations. 7610 +/ 7611 public string name; 7612 /++ 7613 A flag you can set and process on your own. All the library does with it is 7614 set it to false when it handles an event, otherwise you can do whatever you 7615 want with it. 7616 7617 In my irc client, I use this to add a * to the tab to indicate new messages. 7618 +/ 7619 public bool demandsAttention; 7620 7621 /++ 7622 The coordinates of the last [drawInto] 7623 +/ 7624 int x, y, width, height; 7625 7626 private CircularBuffer!Line lines; 7627 private bool eol; // if the last line had an eol, next append needs a new line. doing this means we won't have a spurious blank line at the end of the draw-in 7628 7629 /++ 7630 Property to control the current scrollback position. 0 = latest message 7631 at bottom of screen. 7632 7633 See_Also: [scrollToBottom], [scrollToTop], [scrollUp], [scrollDown], [scrollTopPosition] 7634 +/ 7635 @property int scrollbackPosition() const pure @nogc nothrow @safe { 7636 return scrollbackPosition_; 7637 } 7638 7639 /// ditto 7640 private @property void scrollbackPosition(int p) pure @nogc nothrow @safe { 7641 scrollbackPosition_ = p; 7642 } 7643 7644 private int scrollbackPosition_; 7645 7646 /++ 7647 This is the color it uses to clear the screen. 7648 7649 History: 7650 Added May 26, 2021 7651 +/ 7652 public Color defaultForeground = Color.DEFAULT; 7653 /// ditto 7654 public Color defaultBackground = Color.DEFAULT; 7655 7656 private int foreground_ = Color.DEFAULT, background_ = Color.DEFAULT; 7657 7658 /++ 7659 The name is for your own use only. I use the name as a tab title but you could ignore it and just pass `null` too. 7660 +/ 7661 this(string name) { 7662 this.name = name; 7663 } 7664 7665 /++ 7666 Writing into the scrollback buffer can be done with the same normal functions. 7667 7668 Note that you will have to call [redraw] yourself to make this actually appear on screen. 7669 +/ 7670 void write(T...)(T t) { 7671 import std.conv : text; 7672 addComponent(text(t), foreground_, background_, null); 7673 } 7674 7675 /// ditto 7676 void writeln(T...)(T t) { 7677 write(t, "\n"); 7678 } 7679 7680 /// ditto 7681 void writef(T...)(string fmt, T t) { 7682 import std.format: format; 7683 write(format(fmt, t)); 7684 } 7685 7686 /// ditto 7687 void writefln(T...)(string fmt, T t) { 7688 writef(fmt, t, "\n"); 7689 } 7690 7691 /// ditto 7692 void color(int foreground, int background) { 7693 this.foreground_ = foreground; 7694 this.background_ = background; 7695 } 7696 7697 /++ 7698 Clears the scrollback buffer. 7699 +/ 7700 void clear() { 7701 lines.clear(); 7702 clickRegions = null; 7703 scrollbackPosition_ = 0; 7704 } 7705 7706 /++ 7707 7708 +/ 7709 void addComponent(string text, int foreground, int background, bool delegate() onclick) { 7710 addComponent(LineComponent(text, foreground, background, onclick)); 7711 } 7712 7713 /++ 7714 7715 +/ 7716 void addComponent(LineComponent component) { 7717 if(lines.length == 0 || eol) { 7718 addLine(); 7719 eol = false; 7720 } 7721 bool first = true; 7722 import std.algorithm; 7723 7724 if(component.text.length && component.text[$-1] == '\n') { 7725 eol = true; 7726 component.text = component.text[0 .. $ - 1]; 7727 } 7728 7729 foreach(t; splitter(component.text, "\n")) { 7730 if(!first) addLine(); 7731 first = false; 7732 auto c = component; 7733 c.text = t; 7734 lines[$-1].components ~= c; 7735 } 7736 } 7737 7738 /++ 7739 Adds an empty line. 7740 +/ 7741 void addLine() { 7742 lines ~= Line(); 7743 if(scrollbackPosition_) // if the user is scrolling back, we want to keep them basically centered where they are 7744 scrollbackPosition_++; 7745 } 7746 7747 /++ 7748 This is what [writeln] actually calls. 7749 7750 Using this exclusively though can give you more control, especially over the trailing \n. 7751 +/ 7752 void addLine(string line) { 7753 lines ~= Line([LineComponent(line)]); 7754 if(scrollbackPosition_) // if the user is scrolling back, we want to keep them basically centered where they are 7755 scrollbackPosition_++; 7756 } 7757 7758 /++ 7759 Adds a line by components without affecting scrollback. 7760 7761 History: 7762 Added May 17, 2022 7763 +/ 7764 void addLine(LineComponent[] components...) { 7765 lines ~= Line(components.dup); 7766 } 7767 7768 /++ 7769 Scrolling controls. 7770 7771 Notice that `scrollToTop` needs width and height to know how to word wrap it to determine the number of lines present to scroll back. 7772 +/ 7773 void scrollUp(int lines = 1) { 7774 scrollbackPosition_ += lines; 7775 //if(scrollbackPosition >= this.lines.length) 7776 // scrollbackPosition = cast(int) this.lines.length - 1; 7777 } 7778 7779 /// ditto 7780 void scrollDown(int lines = 1) { 7781 scrollbackPosition_ -= lines; 7782 if(scrollbackPosition_ < 0) 7783 scrollbackPosition_ = 0; 7784 } 7785 7786 /// ditto 7787 void scrollToBottom() { 7788 scrollbackPosition_ = 0; 7789 } 7790 7791 /// ditto 7792 void scrollToTop(int width, int height) { 7793 scrollbackPosition_ = scrollTopPosition(width, height); 7794 } 7795 7796 7797 /++ 7798 You can construct these to get more control over specifics including 7799 setting RGB colors. 7800 7801 But generally just using [write] and friends is easier. 7802 +/ 7803 struct LineComponent { 7804 private string text; 7805 private bool isRgb; 7806 private union { 7807 int color; 7808 RGB colorRgb; 7809 } 7810 private union { 7811 int background; 7812 RGB backgroundRgb; 7813 } 7814 private bool delegate() onclick; // return true if you need to redraw 7815 7816 // 16 color ctor 7817 this(string text, int color = Color.DEFAULT, int background = Color.DEFAULT, bool delegate() onclick = null) { 7818 this.text = text; 7819 this.color = color; 7820 this.background = background; 7821 this.onclick = onclick; 7822 this.isRgb = false; 7823 } 7824 7825 // true color ctor 7826 this(string text, RGB colorRgb, RGB backgroundRgb = RGB(0, 0, 0), bool delegate() onclick = null) { 7827 this.text = text; 7828 this.colorRgb = colorRgb; 7829 this.backgroundRgb = backgroundRgb; 7830 this.onclick = onclick; 7831 this.isRgb = true; 7832 } 7833 } 7834 7835 private struct Line { 7836 LineComponent[] components; 7837 int length() { 7838 int l = 0; 7839 foreach(c; components) 7840 l += c.text.length; 7841 return l; 7842 } 7843 } 7844 7845 /++ 7846 This is an internal helper for its scrollback buffer. 7847 7848 It is fairly generic and I might move it somewhere else some day. 7849 7850 It has a compile-time specified limit of 8192 entries. 7851 +/ 7852 static struct CircularBuffer(T) { 7853 T[] backing; 7854 7855 enum maxScrollback = 8192; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask... 7856 7857 int start; 7858 int length_; 7859 7860 void clear() { 7861 backing = null; 7862 start = 0; 7863 length_ = 0; 7864 } 7865 7866 size_t length() { 7867 return length_; 7868 } 7869 7870 void opOpAssign(string op : "~")(T line) { 7871 if(length_ < maxScrollback) { 7872 backing.assumeSafeAppend(); 7873 backing ~= line; 7874 length_++; 7875 } else { 7876 backing[start] = line; 7877 start++; 7878 if(start == maxScrollback) 7879 start = 0; 7880 } 7881 } 7882 7883 ref T opIndex(int idx) { 7884 return backing[(start + idx) % maxScrollback]; 7885 } 7886 ref T opIndex(Dollar idx) { 7887 return backing[(start + (length + idx.offsetFromEnd)) % maxScrollback]; 7888 } 7889 7890 CircularBufferRange opSlice(int startOfIteration, Dollar end) { 7891 return CircularBufferRange(&this, startOfIteration, cast(int) length - startOfIteration + end.offsetFromEnd); 7892 } 7893 CircularBufferRange opSlice(int startOfIteration, int end) { 7894 return CircularBufferRange(&this, startOfIteration, end - startOfIteration); 7895 } 7896 CircularBufferRange opSlice() { 7897 return CircularBufferRange(&this, 0, cast(int) length); 7898 } 7899 7900 static struct CircularBufferRange { 7901 CircularBuffer* item; 7902 int position; 7903 int remaining; 7904 this(CircularBuffer* item, int startOfIteration, int count) { 7905 this.item = item; 7906 position = startOfIteration; 7907 remaining = count; 7908 } 7909 7910 ref T front() { return (*item)[position]; } 7911 bool empty() { return remaining <= 0; } 7912 void popFront() { 7913 position++; 7914 remaining--; 7915 } 7916 7917 ref T back() { return (*item)[remaining - 1 - position]; } 7918 void popBack() { 7919 remaining--; 7920 } 7921 } 7922 7923 static struct Dollar { 7924 int offsetFromEnd; 7925 Dollar opBinary(string op : "-")(int rhs) { 7926 return Dollar(offsetFromEnd - rhs); 7927 } 7928 } 7929 Dollar opDollar() { return Dollar(0); } 7930 } 7931 7932 /++ 7933 Given a size, how far would you have to scroll back to get to the top? 7934 7935 Please note that this is O(n) with the length of the scrollback buffer. 7936 +/ 7937 int scrollTopPosition(int width, int height) { 7938 int lineCount; 7939 7940 foreach_reverse(line; lines) { 7941 int written = 0; 7942 comp_loop: foreach(cidx, component; line.components) { 7943 auto towrite = component.text; 7944 foreach(idx, dchar ch; towrite) { 7945 if(written >= width) { 7946 lineCount++; 7947 written = 0; 7948 } 7949 7950 if(ch == '\t') 7951 written += 8; // FIXME 7952 else 7953 written++; 7954 } 7955 } 7956 lineCount++; 7957 } 7958 7959 //if(lineCount > height) 7960 return lineCount - height; 7961 //return 0; 7962 } 7963 7964 /++ 7965 Draws the current state into the given terminal inside the given bounding box. 7966 7967 Also updates its internal position and click region data which it uses for event filtering in [handleEvent]. 7968 +/ 7969 void drawInto(Terminal* terminal, in int x = 0, in int y = 0, int width = 0, int height = 0) { 7970 if(lines.length == 0) 7971 return; 7972 7973 if(width == 0) 7974 width = terminal.width; 7975 if(height == 0) 7976 height = terminal.height; 7977 7978 this.x = x; 7979 this.y = y; 7980 this.width = width; 7981 this.height = height; 7982 7983 /* We need to figure out how much is going to fit 7984 in a first pass, so we can figure out where to 7985 start drawing */ 7986 7987 int remaining = height + scrollbackPosition; 7988 int start = cast(int) lines.length; 7989 int howMany = 0; 7990 7991 bool firstPartial = false; 7992 7993 static struct Idx { 7994 size_t cidx; 7995 size_t idx; 7996 } 7997 7998 Idx firstPartialStartIndex; 7999 8000 // this is private so I know we can safe append 8001 clickRegions.length = 0; 8002 clickRegions.assumeSafeAppend(); 8003 8004 // FIXME: should prolly handle \n and \r in here too. 8005 8006 // we'll work backwards to figure out how much will fit... 8007 // this will give accurate per-line things even with changing width and wrapping 8008 // while being generally efficient - we usually want to show the end of the list 8009 // anyway; actually using the scrollback is a bit of an exceptional case. 8010 8011 // It could probably do this instead of on each redraw, on each resize or insertion. 8012 // or at least cache between redraws until one of those invalidates it. 8013 foreach_reverse(line; lines) { 8014 int written = 0; 8015 int brokenLineCount; 8016 Idx[16] lineBreaksBuffer; 8017 Idx[] lineBreaks = lineBreaksBuffer[]; 8018 comp_loop: foreach(cidx, component; line.components) { 8019 auto towrite = component.text; 8020 foreach(idx, dchar ch; towrite) { 8021 if(written >= width) { 8022 if(brokenLineCount == lineBreaks.length) 8023 lineBreaks ~= Idx(cidx, idx); 8024 else 8025 lineBreaks[brokenLineCount] = Idx(cidx, idx); 8026 8027 brokenLineCount++; 8028 8029 written = 0; 8030 } 8031 8032 if(ch == '\t') 8033 written += 8; // FIXME 8034 else 8035 written++; 8036 } 8037 } 8038 8039 lineBreaks = lineBreaks[0 .. brokenLineCount]; 8040 8041 foreach_reverse(lineBreak; lineBreaks) { 8042 if(remaining == 1) { 8043 firstPartial = true; 8044 firstPartialStartIndex = lineBreak; 8045 break; 8046 } else { 8047 remaining--; 8048 } 8049 if(remaining <= 0) 8050 break; 8051 } 8052 8053 remaining--; 8054 8055 start--; 8056 howMany++; 8057 if(remaining <= 0) 8058 break; 8059 } 8060 8061 // second pass: actually draw it 8062 int linePos = remaining; 8063 8064 foreach(line; lines[start .. start + howMany]) { 8065 int written = 0; 8066 8067 if(linePos < 0) { 8068 linePos++; 8069 continue; 8070 } 8071 8072 terminal.moveTo(x, y + ((linePos >= 0) ? linePos : 0)); 8073 8074 auto todo = line.components; 8075 8076 if(firstPartial) { 8077 todo = todo[firstPartialStartIndex.cidx .. $]; 8078 } 8079 8080 foreach(ref component; todo) { 8081 if(component.isRgb) 8082 terminal.setTrueColor(component.colorRgb, component.backgroundRgb); 8083 else 8084 terminal.color( 8085 component.color == Color.DEFAULT ? defaultForeground : component.color, 8086 component.background == Color.DEFAULT ? defaultBackground : component.background, 8087 ); 8088 auto towrite = component.text; 8089 8090 again: 8091 8092 if(linePos >= height) 8093 break; 8094 8095 if(firstPartial) { 8096 towrite = towrite[firstPartialStartIndex.idx .. $]; 8097 firstPartial = false; 8098 } 8099 8100 foreach(idx, dchar ch; towrite) { 8101 if(written >= width) { 8102 clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 8103 terminal.write(towrite[0 .. idx]); 8104 towrite = towrite[idx .. $]; 8105 linePos++; 8106 written = 0; 8107 terminal.moveTo(x, y + linePos); 8108 goto again; 8109 } 8110 8111 if(ch == '\t') 8112 written += 8; // FIXME 8113 else 8114 written++; 8115 } 8116 8117 if(towrite.length) { 8118 clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 8119 terminal.write(towrite); 8120 } 8121 } 8122 8123 if(written < width) { 8124 terminal.color(defaultForeground, defaultBackground); 8125 foreach(i; written .. width) 8126 terminal.write(" "); 8127 } 8128 8129 linePos++; 8130 8131 if(linePos >= height) 8132 break; 8133 } 8134 8135 if(linePos < height) { 8136 terminal.color(defaultForeground, defaultBackground); 8137 foreach(i; linePos .. height) { 8138 if(i >= 0 && i < height) { 8139 terminal.moveTo(x, y + i); 8140 foreach(w; 0 .. width) 8141 terminal.write(" "); 8142 } 8143 } 8144 } 8145 } 8146 8147 private struct ClickRegion { 8148 LineComponent* component; 8149 int xStart; 8150 int yStart; 8151 int length; 8152 } 8153 private ClickRegion[] clickRegions; 8154 8155 /++ 8156 Default event handling for this widget. Call this only after drawing it into a rectangle 8157 and only if the event ought to be dispatched to it (which you determine however you want; 8158 you could dispatch all events to it, or perhaps filter some out too) 8159 8160 Returns: true if it should be redrawn 8161 +/ 8162 bool handleEvent(InputEvent e) { 8163 final switch(e.type) { 8164 case InputEvent.Type.LinkEvent: 8165 // meh 8166 break; 8167 case InputEvent.Type.KeyboardEvent: 8168 auto ev = e.keyboardEvent; 8169 8170 demandsAttention = false; 8171 8172 switch(ev.which) { 8173 case KeyboardEvent.Key.UpArrow: 8174 scrollUp(); 8175 return true; 8176 case KeyboardEvent.Key.DownArrow: 8177 scrollDown(); 8178 return true; 8179 case KeyboardEvent.Key.PageUp: 8180 if(ev.modifierState & ModifierState.control) 8181 scrollToTop(width, height); 8182 else 8183 scrollUp(height); 8184 return true; 8185 case KeyboardEvent.Key.PageDown: 8186 if(ev.modifierState & ModifierState.control) 8187 scrollToBottom(); 8188 else 8189 scrollDown(height); 8190 return true; 8191 default: 8192 // ignore 8193 } 8194 break; 8195 case InputEvent.Type.MouseEvent: 8196 auto ev = e.mouseEvent; 8197 if(ev.x >= x && ev.x < x + width && ev.y >= y && ev.y < y + height) { 8198 demandsAttention = false; 8199 // it is inside our box, so do something with it 8200 auto mx = ev.x - x; 8201 auto my = ev.y - y; 8202 8203 if(ev.eventType == MouseEvent.Type.Pressed) { 8204 if(ev.buttons & MouseEvent.Button.Left) { 8205 foreach(region; clickRegions) 8206 if(ev.x >= region.xStart && ev.x < region.xStart + region.length && ev.y == region.yStart) 8207 if(region.component.onclick !is null) 8208 return region.component.onclick(); 8209 } 8210 if(ev.buttons & MouseEvent.Button.ScrollUp) { 8211 scrollUp(); 8212 return true; 8213 } 8214 if(ev.buttons & MouseEvent.Button.ScrollDown) { 8215 scrollDown(); 8216 return true; 8217 } 8218 } 8219 } else { 8220 // outside our area, free to ignore 8221 } 8222 break; 8223 case InputEvent.Type.SizeChangedEvent: 8224 // (size changed might be but it needs to be handled at a higher level really anyway) 8225 // though it will return true because it probably needs redrawing anyway. 8226 return true; 8227 case InputEvent.Type.UserInterruptionEvent: 8228 throw new UserInterruptionException(); 8229 case InputEvent.Type.HangupEvent: 8230 throw new HangupException(); 8231 case InputEvent.Type.EndOfFileEvent: 8232 // ignore, not relevant to this 8233 break; 8234 case InputEvent.Type.CharacterEvent: 8235 case InputEvent.Type.NonCharacterKeyEvent: 8236 // obsolete, ignore them until they are removed 8237 break; 8238 case InputEvent.Type.CustomEvent: 8239 case InputEvent.Type.PasteEvent: 8240 // ignored, not relevant to us 8241 break; 8242 } 8243 8244 return false; 8245 } 8246 } 8247 8248 8249 /++ 8250 Thrown by [LineGetter] if the user pressed ctrl+c while it is processing events. 8251 +/ 8252 class UserInterruptionException : Exception { 8253 this() { super("Ctrl+C"); } 8254 } 8255 /++ 8256 Thrown by [LineGetter] if the terminal closes while it is processing input. 8257 +/ 8258 class HangupException : Exception { 8259 this() { super("Terminal disconnected"); } 8260 } 8261 8262 8263 8264 /* 8265 8266 // more efficient scrolling 8267 http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx 8268 // and the unix sequences 8269 8270 8271 rxvt documentation: 8272 use this to finish the input magic for that 8273 8274 8275 For the keypad, use Shift to temporarily override Application-Keypad 8276 setting use Num_Lock to toggle Application-Keypad setting if Num_Lock 8277 is off, toggle Application-Keypad setting. Also note that values of 8278 Home, End, Delete may have been compiled differently on your system. 8279 8280 Normal Shift Control Ctrl+Shift 8281 Tab ^I ESC [ Z ^I ESC [ Z 8282 BackSpace ^H ^? ^? ^? 8283 Find ESC [ 1 ~ ESC [ 1 $ ESC [ 1 ^ ESC [ 1 @ 8284 Insert ESC [ 2 ~ paste ESC [ 2 ^ ESC [ 2 @ 8285 Execute ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 8286 Select ESC [ 4 ~ ESC [ 4 $ ESC [ 4 ^ ESC [ 4 @ 8287 Prior ESC [ 5 ~ scroll-up ESC [ 5 ^ ESC [ 5 @ 8288 Next ESC [ 6 ~ scroll-down ESC [ 6 ^ ESC [ 6 @ 8289 Home ESC [ 7 ~ ESC [ 7 $ ESC [ 7 ^ ESC [ 7 @ 8290 End ESC [ 8 ~ ESC [ 8 $ ESC [ 8 ^ ESC [ 8 @ 8291 Delete ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 8292 F1 ESC [ 11 ~ ESC [ 23 ~ ESC [ 11 ^ ESC [ 23 ^ 8293 F2 ESC [ 12 ~ ESC [ 24 ~ ESC [ 12 ^ ESC [ 24 ^ 8294 F3 ESC [ 13 ~ ESC [ 25 ~ ESC [ 13 ^ ESC [ 25 ^ 8295 F4 ESC [ 14 ~ ESC [ 26 ~ ESC [ 14 ^ ESC [ 26 ^ 8296 F5 ESC [ 15 ~ ESC [ 28 ~ ESC [ 15 ^ ESC [ 28 ^ 8297 F6 ESC [ 17 ~ ESC [ 29 ~ ESC [ 17 ^ ESC [ 29 ^ 8298 F7 ESC [ 18 ~ ESC [ 31 ~ ESC [ 18 ^ ESC [ 31 ^ 8299 F8 ESC [ 19 ~ ESC [ 32 ~ ESC [ 19 ^ ESC [ 32 ^ 8300 F9 ESC [ 20 ~ ESC [ 33 ~ ESC [ 20 ^ ESC [ 33 ^ 8301 F10 ESC [ 21 ~ ESC [ 34 ~ ESC [ 21 ^ ESC [ 34 ^ 8302 F11 ESC [ 23 ~ ESC [ 23 $ ESC [ 23 ^ ESC [ 23 @ 8303 F12 ESC [ 24 ~ ESC [ 24 $ ESC [ 24 ^ ESC [ 24 @ 8304 F13 ESC [ 25 ~ ESC [ 25 $ ESC [ 25 ^ ESC [ 25 @ 8305 F14 ESC [ 26 ~ ESC [ 26 $ ESC [ 26 ^ ESC [ 26 @ 8306 F15 (Help) ESC [ 28 ~ ESC [ 28 $ ESC [ 28 ^ ESC [ 28 @ 8307 F16 (Menu) ESC [ 29 ~ ESC [ 29 $ ESC [ 29 ^ ESC [ 29 @ 8308 8309 F17 ESC [ 31 ~ ESC [ 31 $ ESC [ 31 ^ ESC [ 31 @ 8310 F18 ESC [ 32 ~ ESC [ 32 $ ESC [ 32 ^ ESC [ 32 @ 8311 F19 ESC [ 33 ~ ESC [ 33 $ ESC [ 33 ^ ESC [ 33 @ 8312 F20 ESC [ 34 ~ ESC [ 34 $ ESC [ 34 ^ ESC [ 34 @ 8313 Application 8314 Up ESC [ A ESC [ a ESC O a ESC O A 8315 Down ESC [ B ESC [ b ESC O b ESC O B 8316 Right ESC [ C ESC [ c ESC O c ESC O C 8317 Left ESC [ D ESC [ d ESC O d ESC O D 8318 KP_Enter ^M ESC O M 8319 KP_F1 ESC O P ESC O P 8320 KP_F2 ESC O Q ESC O Q 8321 KP_F3 ESC O R ESC O R 8322 KP_F4 ESC O S ESC O S 8323 XK_KP_Multiply * ESC O j 8324 XK_KP_Add + ESC O k 8325 XK_KP_Separator , ESC O l 8326 XK_KP_Subtract - ESC O m 8327 XK_KP_Decimal . ESC O n 8328 XK_KP_Divide / ESC O o 8329 XK_KP_0 0 ESC O p 8330 XK_KP_1 1 ESC O q 8331 XK_KP_2 2 ESC O r 8332 XK_KP_3 3 ESC O s 8333 XK_KP_4 4 ESC O t 8334 XK_KP_5 5 ESC O u 8335 XK_KP_6 6 ESC O v 8336 XK_KP_7 7 ESC O w 8337 XK_KP_8 8 ESC O x 8338 XK_KP_9 9 ESC O y 8339 */ 8340 8341 version(Demo_kbhit) 8342 void main() { 8343 auto terminal = Terminal(ConsoleOutputType.linear); 8344 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw); 8345 8346 int a; 8347 char ch = '.'; 8348 while(a < 1000) { 8349 a++; 8350 if(a % terminal.width == 0) { 8351 terminal.write("\r"); 8352 if(ch == '.') 8353 ch = ' '; 8354 else 8355 ch = '.'; 8356 } 8357 8358 if(input.kbhit()) 8359 terminal.write(input.getch()); 8360 else 8361 terminal.write(ch); 8362 8363 terminal.flush(); 8364 8365 import core.thread; 8366 Thread.sleep(50.msecs); 8367 } 8368 } 8369 8370 /* 8371 The Xterm palette progression is: 8372 [0, 95, 135, 175, 215, 255] 8373 8374 So if I take the color and subtract 55, then div 40, I get 8375 it into one of these areas. If I add 20, I get a reasonable 8376 rounding. 8377 */ 8378 8379 ubyte colorToXTermPaletteIndex(RGB color) { 8380 /* 8381 Here, I will round off to the color ramp or the 8382 greyscale. I will NOT use the bottom 16 colors because 8383 there's duplicates (or very close enough) to them in here 8384 */ 8385 8386 if(color.r == color.g && color.g == color.b) { 8387 // grey - find one of them: 8388 if(color.r == 0) return 0; 8389 // meh don't need those two, let's simplify branche 8390 //if(color.r == 0xc0) return 7; 8391 //if(color.r == 0x80) return 8; 8392 // it isn't == 255 because it wants to catch anything 8393 // that would wrap the simple algorithm below back to 0. 8394 if(color.r >= 248) return 15; 8395 8396 // there's greys in the color ramp too, but these 8397 // are all close enough as-is, no need to complicate 8398 // algorithm for approximation anyway 8399 8400 return cast(ubyte) (232 + ((color.r - 8) / 10)); 8401 } 8402 8403 // if it isn't grey, it is color 8404 8405 // the ramp goes blue, green, red, with 6 of each, 8406 // so just multiplying will give something good enough 8407 8408 // will give something between 0 and 5, with some rounding 8409 auto r = (cast(int) color.r - 35) / 40; 8410 auto g = (cast(int) color.g - 35) / 40; 8411 auto b = (cast(int) color.b - 35) / 40; 8412 8413 return cast(ubyte) (16 + b + g*6 + r*36); 8414 } 8415 8416 /++ 8417 Represents a 24-bit color. 8418 8419 8420 $(TIP You can convert these to and from [arsd.color.Color] using 8421 `.tupleof`: 8422 8423 --- 8424 RGB rgb; 8425 Color c = Color(rgb.tupleof); 8426 --- 8427 ) 8428 +/ 8429 struct RGB { 8430 ubyte r; /// 8431 ubyte g; /// 8432 ubyte b; /// 8433 // terminal can't actually use this but I want the value 8434 // there for assignment to an arsd.color.Color 8435 private ubyte a = 255; 8436 } 8437 8438 // This is an approximation too for a few entries, but a very close one. 8439 RGB xtermPaletteIndexToColor(int paletteIdx) { 8440 RGB color; 8441 8442 if(paletteIdx < 16) { 8443 if(paletteIdx == 7) 8444 return RGB(0xc0, 0xc0, 0xc0); 8445 else if(paletteIdx == 8) 8446 return RGB(0x80, 0x80, 0x80); 8447 8448 color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 8449 color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 8450 color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 8451 8452 } else if(paletteIdx < 232) { 8453 // color ramp, 6x6x6 cube 8454 color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); 8455 color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); 8456 color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); 8457 8458 if(color.r == 55) color.r = 0; 8459 if(color.g == 55) color.g = 0; 8460 if(color.b == 55) color.b = 0; 8461 } else { 8462 // greyscale ramp, from 0x8 to 0xee 8463 color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); 8464 color.g = color.r; 8465 color.b = color.g; 8466 } 8467 8468 return color; 8469 } 8470 8471 int approximate16Color(RGB color) { 8472 int c; 8473 c |= color.r > 64 ? RED_BIT : 0; 8474 c |= color.g > 64 ? GREEN_BIT : 0; 8475 c |= color.b > 64 ? BLUE_BIT : 0; 8476 8477 c |= (((color.r + color.g + color.b) / 3) > 80) ? Bright : 0; 8478 8479 return c; 8480 } 8481 8482 version(TerminalDirectToEmulator) { 8483 8484 void terminateTerminalProcess(T)(T threadId) { 8485 version(Posix) { 8486 pthread_kill(threadId, SIGQUIT); // or SIGKILL even? 8487 8488 assert(0); 8489 //import core.sys.posix.pthread; 8490 //pthread_cancel(widget.term.threadId); 8491 //widget.term = null; 8492 } else version(Windows) { 8493 import core.sys.windows.windows; 8494 auto hnd = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, TRUE, GetCurrentProcessId()); 8495 TerminateProcess(hnd, -1); 8496 assert(0); 8497 } 8498 } 8499 8500 8501 8502 /++ 8503 Indicates the TerminalDirectToEmulator features 8504 are present. You can check this with `static if`. 8505 8506 $(WARNING 8507 This will cause the [Terminal] constructor to spawn a GUI thread with [arsd.minigui]/[arsd.simpledisplay]. 8508 8509 This means you can NOT use those libraries in your 8510 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. 8511 ) 8512 +/ 8513 enum IntegratedEmulator = true; 8514 8515 version(Windows) { 8516 private enum defaultFont = "Consolas"; 8517 private enum defaultSize = 14; 8518 } else { 8519 private enum defaultFont = "monospace"; 8520 private enum defaultSize = 12; // it is measured differently with fontconfig than core x and windows... 8521 } 8522 8523 /++ 8524 Allows customization of the integrated emulator window. 8525 You may change the default colors, font, and other aspects 8526 of GUI integration. 8527 8528 Test for its presence before using with `static if(arsd.terminal.IntegratedEmulator)`. 8529 8530 All settings here must be set BEFORE you construct any [Terminal] instances. 8531 8532 History: 8533 Added March 7, 2020. 8534 +/ 8535 struct IntegratedTerminalEmulatorConfiguration { 8536 /// Note that all Colors in here are 24 bit colors. 8537 alias Color = arsd.color.Color; 8538 8539 /// Default foreground color of the terminal. 8540 Color defaultForeground = Color.black; 8541 /// Default background color of the terminal. 8542 Color defaultBackground = Color.white; 8543 8544 /++ 8545 Font to use in the window. It should be a monospace font, 8546 and your selection may not actually be used if not available on 8547 the user's system, in which case it will fallback to one. 8548 8549 History: 8550 Implemented March 26, 2020 8551 8552 On January 16, 2021, I changed the default to be a fancier 8553 font than the underlying terminalemulator.d uses ("monospace" 8554 on Linux and "Consolas" on Windows, though I will note 8555 that I do *not* guarantee this won't change.) On January 18, 8556 I changed the default size. 8557 8558 If you want specific values for these things, you should set 8559 them in your own application. 8560 8561 On January 12, 2022, I changed the font size to be auto-scaled 8562 with detected dpi by default. You can undo this by setting 8563 `scaleFontSizeWithDpi` to false. On March 22, 2022, I tweaked 8564 this slightly to only scale if the font point size is not already 8565 scaled (e.g. by Xft.dpi settings) to avoid double scaling. 8566 +/ 8567 string fontName = defaultFont; 8568 /// ditto 8569 int fontSize = defaultSize; 8570 /// ditto 8571 bool scaleFontSizeWithDpi = true; 8572 8573 /++ 8574 Requested initial terminal size in character cells. You may not actually get exactly this. 8575 +/ 8576 int initialWidth = 80; 8577 /// ditto 8578 int initialHeight = 30; 8579 8580 /++ 8581 If `true`, the window will close automatically when the main thread exits. 8582 Otherwise, the window will remain open so the user can work with output before 8583 it disappears. 8584 8585 History: 8586 Added April 10, 2020 (v7.2.0) 8587 +/ 8588 bool closeOnExit = false; 8589 8590 /++ 8591 Gives you a chance to modify the window as it is constructed. Intended 8592 to let you add custom menu options. 8593 8594 --- 8595 import arsd.terminal; 8596 integratedTerminalEmulatorConfiguration.menuExtensionsConstructor = (TerminalEmulatorWindow window) { 8597 import arsd.minigui; // for the menu related UDAs 8598 class Commands { 8599 @menu("Help") { 8600 void Topics() { 8601 auto window = new Window(); // make a help window of some sort 8602 window.show(); 8603 } 8604 8605 @separator 8606 8607 void About() { 8608 messageBox("My Application v 1.0"); 8609 } 8610 } 8611 } 8612 window.setMenuAndToolbarFromAnnotatedCode(new Commands()); 8613 }; 8614 --- 8615 8616 History: 8617 Added March 29, 2020. Included in release v7.1.0. 8618 +/ 8619 void delegate(TerminalEmulatorWindow) menuExtensionsConstructor; 8620 8621 /++ 8622 Set this to true if you want [Terminal] to fallback to the user's 8623 existing native terminal in the event that creating the custom terminal 8624 is impossible for whatever reason. 8625 8626 If your application must have all advanced features, set this to `false`. 8627 Otherwise, be sure you handle the absence of advanced features in your 8628 application by checking methods like [Terminal.inlineImagesSupported], 8629 etc., and only use things you can gracefully degrade without. 8630 8631 If this is set to false, `Terminal`'s constructor will throw if the gui fails 8632 instead of carrying on with the stdout terminal (if possible). 8633 8634 History: 8635 Added June 28, 2020. Included in release v8.1.0. 8636 8637 +/ 8638 bool fallbackToDegradedTerminal = true; 8639 8640 /++ 8641 The default key control is ctrl+c sends an interrupt character and ctrl+shift+c 8642 does copy to clipboard. If you set this to `true`, it swaps those two bindings. 8643 8644 History: 8645 Added June 15, 2021. Included in release v10.1.0. 8646 +/ 8647 bool ctrlCCopies = false; // FIXME: i could make this context-sensitive too, so if text selected, copy, otherwise, cancel. prolly show in statu s bar 8648 8649 /++ 8650 When using the integrated terminal emulator, the default is to assume you want it. 8651 But some users may wish to force the in-terminal fallback anyway at start up time. 8652 8653 Seeing this to `true` will skip attempting to create the gui window where a fallback 8654 is available. It is ignored on systems where there is no fallback. Make sure that 8655 [fallbackToDegradedTerminal] is set to `true` if you use this. 8656 8657 History: 8658 Added October 4, 2022 (dub v10.10) 8659 +/ 8660 bool preferDegradedTerminal = false; 8661 } 8662 8663 /+ 8664 status bar should probably tell 8665 if scroll lock is on... 8666 +/ 8667 8668 /// You can set this in a static module constructor. (`shared static this() {}`) 8669 __gshared IntegratedTerminalEmulatorConfiguration integratedTerminalEmulatorConfiguration; 8670 8671 import arsd.terminalemulator; 8672 import arsd.minigui; 8673 8674 version(Posix) 8675 private extern(C) int openpty(int* master, int* slave, char*, const void*, const void*); 8676 8677 /++ 8678 Represents the window that the library pops up for you. 8679 +/ 8680 final class TerminalEmulatorWindow : MainWindow { 8681 /++ 8682 Returns the size of an individual character cell, in pixels. 8683 8684 History: 8685 Added April 2, 2021 8686 +/ 8687 Size characterCellSize() { 8688 if(tew && tew.terminalEmulator) 8689 return Size(tew.terminalEmulator.fontWidth, tew.terminalEmulator.fontHeight); 8690 else 8691 return Size(1, 1); 8692 } 8693 8694 /++ 8695 Gives access to the underlying terminal emulation object. 8696 +/ 8697 TerminalEmulator terminalEmulator() { 8698 return tew.terminalEmulator; 8699 } 8700 8701 private TerminalEmulatorWindow parent; 8702 private TerminalEmulatorWindow[] children; 8703 private void childClosing(TerminalEmulatorWindow t) { 8704 foreach(idx, c; children) 8705 if(c is t) 8706 children = children[0 .. idx] ~ children[idx + 1 .. $]; 8707 } 8708 private void registerChild(TerminalEmulatorWindow t) { 8709 children ~= t; 8710 } 8711 8712 private this(Terminal* term, TerminalEmulatorWindow parent) { 8713 8714 this.parent = parent; 8715 scope(success) if(parent) parent.registerChild(this); 8716 8717 super("Terminal Application"); 8718 //, integratedTerminalEmulatorConfiguration.initialWidth * integratedTerminalEmulatorConfiguration.fontSize / 2, integratedTerminalEmulatorConfiguration.initialHeight * integratedTerminalEmulatorConfiguration.fontSize); 8719 8720 smw = new ScrollMessageWidget(this); 8721 tew = new TerminalEmulatorWidget(term, smw); 8722 8723 if(integratedTerminalEmulatorConfiguration.initialWidth == 0 || integratedTerminalEmulatorConfiguration.initialHeight == 0) { 8724 win.show(); // if must be mapped before maximized... it does cause a flash but meh. 8725 win.maximize(); 8726 } else { 8727 win.resize(integratedTerminalEmulatorConfiguration.initialWidth * tew.terminalEmulator.fontWidth, integratedTerminalEmulatorConfiguration.initialHeight * tew.terminalEmulator.fontHeight); 8728 } 8729 8730 smw.addEventListener("scroll", () { 8731 tew.terminalEmulator.scrollbackTo(smw.position.x, smw.position.y + tew.terminalEmulator.height); 8732 redraw(); 8733 }); 8734 8735 smw.setTotalArea(1, 1); 8736 8737 setMenuAndToolbarFromAnnotatedCode(this); 8738 if(integratedTerminalEmulatorConfiguration.menuExtensionsConstructor) 8739 integratedTerminalEmulatorConfiguration.menuExtensionsConstructor(this); 8740 8741 8742 8743 if(term.pipeThroughStdOut && parent is null) { // if we have a parent, it already did this and stealing it is going to b0rk the output entirely 8744 version(Posix) { 8745 import unix = core.sys.posix.unistd; 8746 import core.stdc.stdio; 8747 8748 auto fp = stdout; 8749 8750 // FIXME: openpty? child processes can get a lil borked. 8751 8752 int[2] fds; 8753 auto ret = pipe(fds); 8754 8755 auto fd = fileno(fp); 8756 8757 dup2(fds[1], fd); 8758 unix.close(fds[1]); 8759 if(isatty(2)) 8760 dup2(1, 2); 8761 auto listener = new PosixFdReader(() { 8762 ubyte[1024] buffer; 8763 auto ret = read(fds[0], buffer.ptr, buffer.length); 8764 if(ret <= 0) return; 8765 tew.terminalEmulator.sendRawInput(buffer[0 .. ret]); 8766 tew.terminalEmulator.redraw(); 8767 }, fds[0]); 8768 8769 readFd = fds[0]; 8770 } else version(CRuntime_Microsoft) { 8771 8772 CHAR[MAX_PATH] PipeNameBuffer; 8773 8774 static shared(int) PipeSerialNumber = 0; 8775 8776 import core.atomic; 8777 8778 import core.stdc.string; 8779 8780 // we need a unique name in the universal filesystem 8781 // so it can be freopen'd. When the process terminates, 8782 // this is auto-closed too, so the pid is good enough, just 8783 // with the shared number 8784 sprintf(PipeNameBuffer.ptr, 8785 `\\.\pipe\arsd.terminal.pipe.%08x.%08x`.ptr, 8786 GetCurrentProcessId(), 8787 atomicOp!"+="(PipeSerialNumber, 1) 8788 ); 8789 8790 readPipe = CreateNamedPipeA( 8791 PipeNameBuffer.ptr, 8792 1/*PIPE_ACCESS_INBOUND*/ | FILE_FLAG_OVERLAPPED, 8793 0 /*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/, 8794 1, // Number of pipes 8795 1024, // Out buffer size 8796 1024, // In buffer size 8797 0,//120 * 1000, // Timeout in ms 8798 null 8799 ); 8800 if (!readPipe) { 8801 throw new Exception("CreateNamedPipeA"); 8802 } 8803 8804 this.overlapped = new OVERLAPPED(); 8805 this.overlapped.hEvent = cast(void*) this; 8806 this.overlappedBuffer = new ubyte[](4096); 8807 8808 import std.conv; 8809 import core.stdc.errno; 8810 if(freopen(PipeNameBuffer.ptr, "wb", stdout) is null) 8811 //MessageBoxA(null, ("excep " ~ to!string(errno) ~ "\0").ptr, "asda", 0); 8812 throw new Exception("freopen"); 8813 8814 setvbuf(stdout, null, _IOLBF, 128); // I'd prefer to line buffer it, but that doesn't seem to work for some reason. 8815 8816 ConnectNamedPipe(readPipe, this.overlapped); 8817 8818 // also send stderr to stdout if it isn't already redirected somewhere else 8819 if(_fileno(stderr) < 0) { 8820 freopen("nul", "wb", stderr); 8821 8822 _dup2(_fileno(stdout), _fileno(stderr)); 8823 setvbuf(stderr, null, _IOLBF, 128); // if I don't unbuffer this it can really confuse things 8824 assert(0); 8825 } 8826 8827 WindowsRead(0, 0, this.overlapped); 8828 } else throw new Exception("pipeThroughStdOut not supported on this system currently. Use -m32mscoff instead."); 8829 } 8830 } 8831 8832 version(Windows) { 8833 HANDLE readPipe; 8834 private ubyte[] overlappedBuffer; 8835 private OVERLAPPED* overlapped; 8836 static final private extern(Windows) void WindowsRead(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) { 8837 TerminalEmulatorWindow w = cast(TerminalEmulatorWindow) overlapped.hEvent; 8838 if(numberOfBytes) { 8839 w.tew.terminalEmulator.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]); 8840 w.tew.terminalEmulator.redraw(); 8841 } 8842 import std.conv; 8843 if(!ReadFileEx(w.readPipe, w.overlappedBuffer.ptr, cast(DWORD) w.overlappedBuffer.length, overlapped, &WindowsRead)) 8844 if(GetLastError() == 997) {} 8845 //else throw new Exception("ReadFileEx " ~ to!string(GetLastError())); 8846 } 8847 } 8848 8849 version(Posix) { 8850 int readFd = -1; 8851 } 8852 8853 TerminalEmulator.TerminalCell[] delegate(TerminalEmulator.TerminalCell[] i) parentFilter; 8854 8855 private void addScrollbackLineFromParent(TerminalEmulator.TerminalCell[] lineIn) { 8856 if(parentFilter is null) 8857 return; 8858 8859 auto line = parentFilter(lineIn); 8860 if(line is null) return; 8861 8862 if(tew && tew.terminalEmulator) { 8863 bool atBottom = smw.verticalScrollBar.atEnd && smw.horizontalScrollBar.atStart; 8864 tew.terminalEmulator.addScrollbackLine(line); 8865 tew.terminalEmulator.notifyScrollbackAdded(); 8866 if(atBottom) { 8867 tew.terminalEmulator.notifyScrollbarPosition(0, int.max); 8868 tew.terminalEmulator.scrollbackTo(0, int.max); 8869 tew.terminalEmulator.drawScrollback(); 8870 tew.redraw(); 8871 } 8872 } 8873 } 8874 8875 private TerminalEmulatorWidget tew; 8876 private ScrollMessageWidget smw; 8877 8878 @menu("&History") { 8879 @tip("Saves the currently visible content to a file") 8880 void Save() { 8881 getSaveFileName((string name) { 8882 if(name.length) { 8883 try 8884 tew.terminalEmulator.writeScrollbackToFile(name); 8885 catch(Exception e) 8886 messageBox("Save failed: " ~ e.msg); 8887 } 8888 }); 8889 } 8890 8891 // FIXME 8892 version(FIXME) 8893 void Save_HTML() { 8894 8895 } 8896 8897 @separator 8898 /* 8899 void Find() { 8900 // FIXME 8901 // jump to the previous instance in the scrollback 8902 8903 } 8904 */ 8905 8906 void Filter() { 8907 // open a new window that just shows items that pass the filter 8908 8909 static struct FilterParams { 8910 string searchTerm; 8911 bool caseSensitive; 8912 } 8913 8914 dialog((FilterParams p) { 8915 auto nw = new TerminalEmulatorWindow(null, this); 8916 8917 nw.parentWindow.win.handleCharEvent = null; // kinda a hack... i just don't want it ever turning off scroll lock... 8918 8919 nw.parentFilter = (TerminalEmulator.TerminalCell[] line) { 8920 import std.algorithm; 8921 import std.uni; 8922 // omg autodecoding being kinda useful for once LOL 8923 if(line.map!(c => c.hasNonCharacterData ? dchar(0) : (p.caseSensitive ? c.ch : c.ch.toLower)). 8924 canFind(p.searchTerm)) 8925 { 8926 // I might highlight the match too, but meh for now 8927 return line; 8928 } 8929 return null; 8930 }; 8931 8932 foreach(line; tew.terminalEmulator.sbb[0 .. $]) { 8933 if(auto l = nw.parentFilter(line)) { 8934 nw.tew.terminalEmulator.addScrollbackLine(l); 8935 } 8936 } 8937 nw.tew.terminalEmulator.scrollLockLock(); 8938 nw.tew.terminalEmulator.drawScrollback(); 8939 nw.title = "Filter Display"; 8940 nw.show(); 8941 }); 8942 8943 } 8944 8945 @separator 8946 void Clear() { 8947 tew.terminalEmulator.clearScrollbackHistory(); 8948 tew.terminalEmulator.cls(); 8949 tew.terminalEmulator.moveCursor(0, 0); 8950 if(tew.term) { 8951 tew.term.windowSizeChanged = true; 8952 tew.terminalEmulator.outgoingSignal.notify(); 8953 } 8954 tew.redraw(); 8955 } 8956 8957 @separator 8958 void Exit() @accelerator("Alt+F4") @hotkey('x') { 8959 this.close(); 8960 } 8961 } 8962 8963 @menu("&Edit") { 8964 void Copy() { 8965 tew.terminalEmulator.copyToClipboard(tew.terminalEmulator.getSelectedText()); 8966 } 8967 8968 void Paste() { 8969 tew.terminalEmulator.pasteFromClipboard(&tew.terminalEmulator.sendPasteData); 8970 } 8971 } 8972 } 8973 8974 private class InputEventInternal { 8975 const(ubyte)[] data; 8976 this(in ubyte[] data) { 8977 this.data = data; 8978 } 8979 } 8980 8981 private class TerminalEmulatorWidget : Widget { 8982 8983 Menu ctx; 8984 8985 override Menu contextMenu(int x, int y) { 8986 if(ctx is null) { 8987 ctx = new Menu("", this); 8988 ctx.addItem(new MenuItem(new Action("Copy", 0, { 8989 terminalEmulator.copyToClipboard(terminalEmulator.getSelectedText()); 8990 }))); 8991 ctx.addItem(new MenuItem(new Action("Paste", 0, { 8992 terminalEmulator.pasteFromClipboard(&terminalEmulator.sendPasteData); 8993 }))); 8994 ctx.addItem(new MenuItem(new Action("Toggle Scroll Lock", 0, { 8995 terminalEmulator.toggleScrollLock(); 8996 }))); 8997 } 8998 return ctx; 8999 } 9000 9001 this(Terminal* term, ScrollMessageWidget parent) { 9002 this.smw = parent; 9003 this.term = term; 9004 super(parent); 9005 terminalEmulator = new TerminalEmulatorInsideWidget(this); 9006 this.parentWindow.addEventListener("closed", { 9007 if(term) { 9008 term.hangedUp = true; 9009 // should I just send an official SIGHUP?! 9010 } 9011 9012 if(auto wi = cast(TerminalEmulatorWindow) this.parentWindow) { 9013 if(wi.parent) 9014 wi.parent.childClosing(wi); 9015 9016 // if I don't close the redirected pipe, the other thread 9017 // will get stuck indefinitely as it tries to flush its stderr 9018 version(Windows) { 9019 CloseHandle(wi.readPipe); 9020 wi.readPipe = null; 9021 } version(Posix) { 9022 import unix = core.sys.posix.unistd; 9023 import unix2 = core.sys.posix.fcntl; 9024 unix.close(wi.readFd); 9025 9026 version(none) 9027 if(term && term.pipeThroughStdOut) { 9028 auto fd = unix2.open("/dev/null", unix2.O_RDWR); 9029 unix.close(0); 9030 unix.close(1); 9031 unix.close(2); 9032 9033 dup2(fd, 0); 9034 dup2(fd, 1); 9035 dup2(fd, 2); 9036 } 9037 } 9038 } 9039 9040 // try to get it to terminate slightly more forcibly too, if possible 9041 if(sigIntExtension) 9042 sigIntExtension(); 9043 9044 terminalEmulator.outgoingSignal.notify(); 9045 terminalEmulator.incomingSignal.notify(); 9046 terminalEmulator.syncSignal.notify(); 9047 9048 windowGone = true; 9049 }); 9050 9051 this.parentWindow.win.addEventListener((InputEventInternal ie) { 9052 terminalEmulator.sendRawInput(ie.data); 9053 this.redraw(); 9054 terminalEmulator.incomingSignal.notify(); 9055 }); 9056 } 9057 9058 ScrollMessageWidget smw; 9059 Terminal* term; 9060 9061 void sendRawInput(const(ubyte)[] data) { 9062 if(this.parentWindow) { 9063 this.parentWindow.win.postEvent(new InputEventInternal(data)); 9064 if(windowGone) forceTermination(); 9065 terminalEmulator.incomingSignal.wait(); // blocking write basically, wait until the TE confirms the receipt of it 9066 } 9067 } 9068 9069 override void dpiChanged() { 9070 if(terminalEmulator) { 9071 terminalEmulator.loadFont(); 9072 terminalEmulator.resized(width, height); 9073 } 9074 } 9075 9076 TerminalEmulatorInsideWidget terminalEmulator; 9077 9078 override void registerMovement() { 9079 super.registerMovement(); 9080 terminalEmulator.resized(width, height); 9081 } 9082 9083 override void focus() { 9084 super.focus(); 9085 terminalEmulator.attentionReceived(); 9086 } 9087 9088 static class Style : Widget.Style { 9089 override MouseCursor cursor() { 9090 return GenericCursor.Text; 9091 } 9092 } 9093 mixin OverrideStyle!Style; 9094 9095 override void erase(WidgetPainter painter) { /* intentionally blank, paint does it better */ } 9096 9097 override void paint(WidgetPainter painter) { 9098 bool forceRedraw = false; 9099 if(terminalEmulator.invalidateAll || terminalEmulator.clearScreenRequested) { 9100 auto clearColor = terminalEmulator.defaultBackground; 9101 painter.outlineColor = clearColor; 9102 painter.fillColor = clearColor; 9103 painter.drawRectangle(Point(0, 0), this.width, this.height); 9104 terminalEmulator.clearScreenRequested = false; 9105 forceRedraw = true; 9106 } 9107 9108 terminalEmulator.redrawPainter(painter, forceRedraw); 9109 } 9110 } 9111 9112 private class TerminalEmulatorInsideWidget : TerminalEmulator { 9113 9114 private ScrollbackBuffer sbb() { return scrollbackBuffer; } 9115 9116 void resized(int w, int h) { 9117 this.resizeTerminal(w / fontWidth, h / fontHeight); 9118 if(widget && widget.smw) { 9119 widget.smw.setViewableArea(this.width, this.height); 9120 widget.smw.setPageSize(this.width / 2, this.height / 2); 9121 } 9122 notifyScrollbarPosition(0, int.max); 9123 clearScreenRequested = true; 9124 if(widget && widget.term) 9125 widget.term.windowSizeChanged = true; 9126 outgoingSignal.notify(); 9127 redraw(); 9128 } 9129 9130 override void addScrollbackLine(TerminalCell[] line) { 9131 super.addScrollbackLine(line); 9132 if(widget) 9133 if(auto p = cast(TerminalEmulatorWindow) widget.parentWindow) { 9134 foreach(child; p.children) 9135 child.addScrollbackLineFromParent(line); 9136 } 9137 } 9138 9139 override void notifyScrollbackAdded() { 9140 widget.smw.setTotalArea(this.scrollbackWidth > this.width ? this.scrollbackWidth : this.width, this.scrollbackLength > this.height ? this.scrollbackLength : this.height); 9141 } 9142 9143 override void notifyScrollbarPosition(int x, int y) { 9144 widget.smw.setPosition(x, y); 9145 widget.redraw(); 9146 } 9147 9148 override void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) { 9149 if(isRelevantVertically) 9150 notifyScrollbackAdded(); 9151 else 9152 widget.smw.setTotalArea(width, height); 9153 } 9154 9155 override @property public int cursorX() { return super.cursorX; } 9156 override @property public int cursorY() { return super.cursorY; } 9157 9158 protected override void changeCursorStyle(CursorStyle s) { } 9159 9160 string currentTitle; 9161 protected override void changeWindowTitle(string t) { 9162 if(widget && widget.parentWindow && t.length) { 9163 widget.parentWindow.win.title = t; 9164 currentTitle = t; 9165 } 9166 } 9167 protected override void changeWindowIcon(IndexedImage t) { 9168 if(widget && widget.parentWindow && t) 9169 widget.parentWindow.win.icon = t; 9170 } 9171 9172 protected override void changeIconTitle(string) {} 9173 protected override void changeTextAttributes(TextAttributes) {} 9174 protected override void soundBell() { 9175 static if(UsingSimpledisplayX11) 9176 XBell(XDisplayConnection.get(), 50); 9177 } 9178 9179 protected override void demandAttention() { 9180 if(widget && widget.parentWindow) 9181 widget.parentWindow.win.requestAttention(); 9182 } 9183 9184 protected override void copyToClipboard(string text) { 9185 setClipboardText(widget.parentWindow.win, text); 9186 } 9187 9188 override int maxScrollbackLength() const { 9189 return int.max; // no scrollback limit for custom programs 9190 } 9191 9192 protected override void pasteFromClipboard(void delegate(in char[]) dg) { 9193 getClipboardText(widget.parentWindow.win, (in char[] dataIn) { 9194 char[] data; 9195 // change Windows \r\n to plain \n 9196 foreach(char ch; dataIn) 9197 if(ch != 13) 9198 data ~= ch; 9199 dg(data); 9200 }); 9201 } 9202 9203 protected override void copyToPrimary(string text) { 9204 static if(UsingSimpledisplayX11) 9205 setPrimarySelection(widget.parentWindow.win, text); 9206 else 9207 {} 9208 } 9209 protected override void pasteFromPrimary(void delegate(in char[]) dg) { 9210 static if(UsingSimpledisplayX11) 9211 getPrimarySelection(widget.parentWindow.win, dg); 9212 } 9213 9214 override void requestExit() { 9215 widget.parentWindow.close(); 9216 } 9217 9218 bool echo = false; 9219 9220 override void sendRawInput(in ubyte[] data) { 9221 void send(in ubyte[] data) { 9222 if(data.length == 0) 9223 return; 9224 super.sendRawInput(data); 9225 if(echo) 9226 sendToApplication(data); 9227 } 9228 9229 // need to echo, translate 10 to 13/10 cr-lf 9230 size_t last = 0; 9231 const ubyte[2] crlf = [13, 10]; 9232 foreach(idx, ch; data) { 9233 if(waitingForInboundSync && ch == 255) { 9234 send(data[last .. idx]); 9235 last = idx + 1; 9236 waitingForInboundSync = false; 9237 syncSignal.notify(); 9238 continue; 9239 } 9240 if(ch == 10) { 9241 send(data[last .. idx]); 9242 send(crlf[]); 9243 last = idx + 1; 9244 } 9245 } 9246 9247 if(last < data.length) 9248 send(data[last .. $]); 9249 } 9250 9251 bool focused; 9252 9253 TerminalEmulatorWidget widget; 9254 9255 import arsd.simpledisplay; 9256 import arsd.color; 9257 import core.sync.semaphore; 9258 alias ModifierState = arsd.simpledisplay.ModifierState; 9259 alias Color = arsd.color.Color; 9260 alias fromHsl = arsd.color.fromHsl; 9261 9262 const(ubyte)[] pendingForApplication; 9263 Semaphore syncSignal; 9264 Semaphore outgoingSignal; 9265 Semaphore incomingSignal; 9266 9267 private shared(bool) waitingForInboundSync; 9268 9269 override void sendToApplication(scope const(void)[] what) { 9270 synchronized(this) { 9271 pendingForApplication ~= cast(const(ubyte)[]) what; 9272 } 9273 outgoingSignal.notify(); 9274 } 9275 9276 @property int width() { return screenWidth; } 9277 @property int height() { return screenHeight; } 9278 9279 @property bool invalidateAll() { return super.invalidateAll; } 9280 9281 void loadFont() { 9282 if(this.font) { 9283 this.font.unload(); 9284 this.font = null; 9285 } 9286 auto fontSize = integratedTerminalEmulatorConfiguration.fontSize; 9287 if(integratedTerminalEmulatorConfiguration.scaleFontSizeWithDpi) { 9288 static if(UsingSimpledisplayX11) { 9289 // if it is an xft font and xft is already scaled, we should NOT double scale. 9290 import std.algorithm; 9291 if(integratedTerminalEmulatorConfiguration.fontName.startsWith("core:")) { 9292 // core font doesn't use xft anyway 9293 fontSize = widget.scaleWithDpi(fontSize); 9294 } else { 9295 auto xft = getXftDpi(); 9296 if(xft is float.init) 9297 xft = 96; 9298 // the xft passed as assumed means it will figure that's what the size 9299 // is based on (which it is, inside xft) preventing the double scale problem 9300 fontSize = widget.scaleWithDpi(fontSize, cast(int) xft); 9301 9302 } 9303 } else { 9304 fontSize = widget.scaleWithDpi(fontSize); 9305 } 9306 } 9307 9308 if(integratedTerminalEmulatorConfiguration.fontName.length) { 9309 this.font = new OperatingSystemFont(integratedTerminalEmulatorConfiguration.fontName, fontSize, FontWeight.medium); 9310 if(this.font.isNull) { 9311 // carry on, it will try a default later 9312 } else if(this.font.isMonospace) { 9313 this.fontWidth = font.averageWidth; 9314 this.fontHeight = font.height; 9315 } else { 9316 this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again 9317 } 9318 } 9319 9320 if(this.font is null || this.font.isNull) 9321 loadDefaultFont(fontSize); 9322 } 9323 9324 private this(TerminalEmulatorWidget widget) { 9325 9326 this.syncSignal = new Semaphore(); 9327 this.outgoingSignal = new Semaphore(); 9328 this.incomingSignal = new Semaphore(); 9329 9330 this.widget = widget; 9331 9332 loadFont(); 9333 9334 super(integratedTerminalEmulatorConfiguration.initialWidth ? integratedTerminalEmulatorConfiguration.initialWidth : 80, 9335 integratedTerminalEmulatorConfiguration.initialHeight ? integratedTerminalEmulatorConfiguration.initialHeight : 30); 9336 9337 defaultForeground = integratedTerminalEmulatorConfiguration.defaultForeground; 9338 defaultBackground = integratedTerminalEmulatorConfiguration.defaultBackground; 9339 9340 bool skipNextChar = false; 9341 9342 widget.addEventListener((MouseDownEvent ev) { 9343 int termX = (ev.clientX - paddingLeft) / fontWidth; 9344 int termY = (ev.clientY - paddingTop) / fontHeight; 9345 9346 if((!mouseButtonTracking || selectiveMouseTracking || (ev.state & ModifierState.shift)) && ev.button == MouseButton.right) 9347 widget.showContextMenu(ev.clientX, ev.clientY); 9348 else 9349 if(sendMouseInputToApplication(termX, termY, 9350 arsd.terminalemulator.MouseEventType.buttonPressed, 9351 cast(arsd.terminalemulator.MouseButton) ev.button, 9352 (ev.state & ModifierState.shift) ? true : false, 9353 (ev.state & ModifierState.ctrl) ? true : false, 9354 (ev.state & ModifierState.alt) ? true : false 9355 )) 9356 redraw(); 9357 }); 9358 9359 widget.addEventListener((MouseUpEvent ev) { 9360 int termX = (ev.clientX - paddingLeft) / fontWidth; 9361 int termY = (ev.clientY - paddingTop) / fontHeight; 9362 9363 if(sendMouseInputToApplication(termX, termY, 9364 arsd.terminalemulator.MouseEventType.buttonReleased, 9365 cast(arsd.terminalemulator.MouseButton) ev.button, 9366 (ev.state & ModifierState.shift) ? true : false, 9367 (ev.state & ModifierState.ctrl) ? true : false, 9368 (ev.state & ModifierState.alt) ? true : false 9369 )) 9370 redraw(); 9371 }); 9372 9373 widget.addEventListener((MouseMoveEvent ev) { 9374 int termX = (ev.clientX - paddingLeft) / fontWidth; 9375 int termY = (ev.clientY - paddingTop) / fontHeight; 9376 9377 if(sendMouseInputToApplication(termX, termY, 9378 arsd.terminalemulator.MouseEventType.motion, 9379 (ev.state & ModifierState.leftButtonDown) ? arsd.terminalemulator.MouseButton.left 9380 : (ev.state & ModifierState.rightButtonDown) ? arsd.terminalemulator.MouseButton.right 9381 : (ev.state & ModifierState.middleButtonDown) ? arsd.terminalemulator.MouseButton.middle 9382 : cast(arsd.terminalemulator.MouseButton) 0, 9383 (ev.state & ModifierState.shift) ? true : false, 9384 (ev.state & ModifierState.ctrl) ? true : false, 9385 (ev.state & ModifierState.alt) ? true : false 9386 )) 9387 redraw(); 9388 }); 9389 9390 widget.addEventListener((KeyDownEvent ev) { 9391 if(ev.key == Key.C && !(ev.state & ModifierState.shift) && (ev.state & ModifierState.ctrl)) { 9392 if(integratedTerminalEmulatorConfiguration.ctrlCCopies) { 9393 goto copy; 9394 } 9395 } 9396 if(ev.key == Key.C && (ev.state & ModifierState.shift) && (ev.state & ModifierState.ctrl)) { 9397 if(integratedTerminalEmulatorConfiguration.ctrlCCopies) { 9398 sendSigInt(); 9399 skipNextChar = true; 9400 return; 9401 } 9402 // ctrl+c is cancel so ctrl+shift+c ends up doing copy. 9403 copy: 9404 copyToClipboard(getSelectedText()); 9405 skipNextChar = true; 9406 return; 9407 } 9408 if(ev.key == Key.Insert && (ev.state & ModifierState.ctrl)) { 9409 copyToClipboard(getSelectedText()); 9410 return; 9411 } 9412 9413 auto keyToSend = ev.key; 9414 9415 static if(UsingSimpledisplayX11) { 9416 if((ev.state & ModifierState.alt) && ev.originalKeyEvent.charsPossible.length) { 9417 keyToSend = cast(Key) ev.originalKeyEvent.charsPossible[0]; 9418 } 9419 } 9420 9421 defaultKeyHandler!(typeof(ev.key))( 9422 keyToSend 9423 , (ev.state & ModifierState.shift)?true:false 9424 , (ev.state & ModifierState.alt)?true:false 9425 , (ev.state & ModifierState.ctrl)?true:false 9426 , (ev.state & ModifierState.windows)?true:false 9427 ); 9428 9429 return; // the character event handler will do others 9430 }); 9431 9432 widget.addEventListener((CharEvent ev) { 9433 if(skipNextChar) { 9434 skipNextChar = false; 9435 return; 9436 } 9437 dchar c = ev.character; 9438 9439 if(c == 0x1c) /* ctrl+\, force quit */ { 9440 version(Posix) { 9441 import core.sys.posix.signal; 9442 if(widget is null || widget.term is null) { 9443 // the other thread must already be dead, so we can just close 9444 widget.parentWindow.close(); // I'm gonna let it segfault if this is null cuz like that isn't supposed to happen 9445 return; 9446 } 9447 } 9448 9449 terminateTerminalProcess(widget.term.threadId); 9450 } else if(c == 3) {// && !ev.shiftKey) /* ctrl+c, interrupt. But NOT ctrl+shift+c as that's a user-defined keystroke and/or "copy", but ctrl+shift+c never gets sent here.... thanks to the skipNextChar above */ { 9451 sendSigInt(); 9452 } else { 9453 defaultCharHandler(c); 9454 } 9455 }); 9456 } 9457 9458 void sendSigInt() { 9459 if(sigIntExtension) 9460 sigIntExtension(); 9461 9462 if(widget && widget.term) { 9463 widget.term.interrupted = true; 9464 outgoingSignal.notify(); 9465 } 9466 } 9467 9468 bool clearScreenRequested = true; 9469 void redraw() { 9470 if(widget.parentWindow is null || widget.parentWindow.win is null || widget.parentWindow.win.closed) 9471 return; 9472 9473 widget.redraw(); 9474 } 9475 9476 mixin SdpyDraw; 9477 } 9478 } else { 9479 /// 9480 enum IntegratedEmulator = false; 9481 } 9482 9483 /* 9484 void main() { 9485 auto terminal = Terminal(ConsoleOutputType.linear); 9486 terminal.setTrueColor(RGB(255, 0, 255), RGB(255, 255, 255)); 9487 terminal.writeln("Hello, world!"); 9488 } 9489 */ 9490 9491 private version(Windows) { 9492 pragma(lib, "user32"); 9493 import core.sys.windows.windows; 9494 9495 extern(Windows) 9496 HANDLE CreateNamedPipeA( 9497 const(char)* lpName, 9498 DWORD dwOpenMode, 9499 DWORD dwPipeMode, 9500 DWORD nMaxInstances, 9501 DWORD nOutBufferSize, 9502 DWORD nInBufferSize, 9503 DWORD nDefaultTimeOut, 9504 LPSECURITY_ATTRIBUTES lpSecurityAttributes 9505 ); 9506 9507 version(CRuntime_Microsoft) { 9508 extern(C) int _dup2(int, int); 9509 extern(C) int _fileno(FILE*); 9510 } 9511 } 9512 9513 /++ 9514 Convenience object to forward terminal keys to a [arsd.simpledisplay.SimpleWindow]. Meant for cases when you have a gui window as the primary mode of interaction, but also want keys to the parent terminal to be usable too by the window. 9515 9516 Please note that not all keys may be accurately forwarded. It is not meant to be 100% comprehensive; that's for the window. 9517 9518 History: 9519 Added December 29, 2020. 9520 +/ 9521 static if(__traits(compiles, mixin(`{ static foreach(i; 0 .. 1) {} }`))) 9522 mixin(q{ 9523 auto SdpyIntegratedKeys(SimpleWindow)(SimpleWindow window) { 9524 struct impl { 9525 static import sdpy = arsd.simpledisplay; 9526 Terminal* terminal; 9527 RealTimeConsoleInput* rtti; 9528 typeof(RealTimeConsoleInput.init.integrateWithSimpleDisplayEventLoop(null)) listener; 9529 this(sdpy.SimpleWindow window) { 9530 terminal = new Terminal(ConsoleOutputType.linear); 9531 rtti = new RealTimeConsoleInput(terminal, ConsoleInputFlags.releasedKeys); 9532 listener = rtti.integrateWithSimpleDisplayEventLoop(delegate(InputEvent ie) { 9533 if(ie.type != InputEvent.Type.KeyboardEvent) 9534 return; 9535 auto kbd = ie.get!(InputEvent.Type.KeyboardEvent); 9536 if(window.handleKeyEvent !is null) { 9537 sdpy.KeyEvent ke; 9538 ke.pressed = kbd.pressed; 9539 if(kbd.modifierState & ModifierState.control) 9540 ke.modifierState |= sdpy.ModifierState.ctrl; 9541 if(kbd.modifierState & ModifierState.alt) 9542 ke.modifierState |= sdpy.ModifierState.alt; 9543 if(kbd.modifierState & ModifierState.shift) 9544 ke.modifierState |= sdpy.ModifierState.shift; 9545 9546 sw: switch(kbd.which) { 9547 case KeyboardEvent.Key.escape: ke.key = sdpy.Key.Escape; break; 9548 case KeyboardEvent.Key.F1: ke.key = sdpy.Key.F1; break; 9549 case KeyboardEvent.Key.F2: ke.key = sdpy.Key.F2; break; 9550 case KeyboardEvent.Key.F3: ke.key = sdpy.Key.F3; break; 9551 case KeyboardEvent.Key.F4: ke.key = sdpy.Key.F4; break; 9552 case KeyboardEvent.Key.F5: ke.key = sdpy.Key.F5; break; 9553 case KeyboardEvent.Key.F6: ke.key = sdpy.Key.F6; break; 9554 case KeyboardEvent.Key.F7: ke.key = sdpy.Key.F7; break; 9555 case KeyboardEvent.Key.F8: ke.key = sdpy.Key.F8; break; 9556 case KeyboardEvent.Key.F9: ke.key = sdpy.Key.F9; break; 9557 case KeyboardEvent.Key.F10: ke.key = sdpy.Key.F10; break; 9558 case KeyboardEvent.Key.F11: ke.key = sdpy.Key.F11; break; 9559 case KeyboardEvent.Key.F12: ke.key = sdpy.Key.F12; break; 9560 case KeyboardEvent.Key.LeftArrow: ke.key = sdpy.Key.Left; break; 9561 case KeyboardEvent.Key.RightArrow: ke.key = sdpy.Key.Right; break; 9562 case KeyboardEvent.Key.UpArrow: ke.key = sdpy.Key.Up; break; 9563 case KeyboardEvent.Key.DownArrow: ke.key = sdpy.Key.Down; break; 9564 case KeyboardEvent.Key.Insert: ke.key = sdpy.Key.Insert; break; 9565 case KeyboardEvent.Key.Delete: ke.key = sdpy.Key.Delete; break; 9566 case KeyboardEvent.Key.Home: ke.key = sdpy.Key.Home; break; 9567 case KeyboardEvent.Key.End: ke.key = sdpy.Key.End; break; 9568 case KeyboardEvent.Key.PageUp: ke.key = sdpy.Key.PageUp; break; 9569 case KeyboardEvent.Key.PageDown: ke.key = sdpy.Key.PageDown; break; 9570 case KeyboardEvent.Key.ScrollLock: ke.key = sdpy.Key.ScrollLock; break; 9571 9572 case '\r', '\n': ke.key = sdpy.Key.Enter; break; 9573 case '\t': ke.key = sdpy.Key.Tab; break; 9574 case ' ': ke.key = sdpy.Key.Space; break; 9575 case '\b': ke.key = sdpy.Key.Backspace; break; 9576 9577 case '`': ke.key = sdpy.Key.Grave; break; 9578 case '-': ke.key = sdpy.Key.Dash; break; 9579 case '=': ke.key = sdpy.Key.Equals; break; 9580 case '[': ke.key = sdpy.Key.LeftBracket; break; 9581 case ']': ke.key = sdpy.Key.RightBracket; break; 9582 case '\\': ke.key = sdpy.Key.Backslash; break; 9583 case ';': ke.key = sdpy.Key.Semicolon; break; 9584 case '\'': ke.key = sdpy.Key.Apostrophe; break; 9585 case ',': ke.key = sdpy.Key.Comma; break; 9586 case '.': ke.key = sdpy.Key.Period; break; 9587 case '/': ke.key = sdpy.Key.Slash; break; 9588 9589 static foreach(ch; 'A' .. ('Z' + 1)) { 9590 case ch, ch + 32: 9591 version(Windows) 9592 ke.key = cast(sdpy.Key) ch; 9593 else 9594 ke.key = cast(sdpy.Key) (ch + 32); 9595 break sw; 9596 } 9597 static foreach(ch; '0' .. ('9' + 1)) { 9598 case ch: 9599 ke.key = cast(sdpy.Key) ch; 9600 break sw; 9601 } 9602 9603 default: 9604 } 9605 9606 // I'm tempted to leave the window null since it didn't originate from here 9607 // or maybe set a ModifierState.... 9608 //ke.window = window; 9609 9610 window.handleKeyEvent(ke); 9611 } 9612 if(window.handleCharEvent !is null) { 9613 if(kbd.isCharacter) 9614 window.handleCharEvent(kbd.which); 9615 } 9616 }); 9617 } 9618 ~this() { 9619 listener.dispose(); 9620 .destroy(*rtti); 9621 .destroy(*terminal); 9622 rtti = null; 9623 terminal = null; 9624 } 9625 } 9626 return impl(window); 9627 } 9628 }); 9629 9630 9631 /* 9632 ONLY SUPPORTED ON MY TERMINAL EMULATOR IN GENERAL 9633 9634 bracketed section can collapse and scroll independently in the TE. may also pop out into a window (possibly with a comparison window) 9635 9636 hyperlink can either just indicate something to the TE to handle externally 9637 OR 9638 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. 9639 9640 internally it can set two bits: one indicates it is a hyperlink, the other just flips each use to separate consecutive sequences. 9641 9642 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. 9643 9644 9645 I could spread a unique id number across bits, one bit per char so the memory isn't too bad. 9646 so it would set a number and a word. this is sent back to the application to handle internally. 9647 9648 1) turn on special input 9649 2) turn off special input 9650 3) special input sends a paste event with a number and the text 9651 4) to make a link, you write out the begin sequence, the text, and the end sequence. including the magic number somewhere. 9652 magic number is allowed to have one bit per char. the terminal discards anything else. terminal.d api will enforce. 9653 9654 if magic number is zero, it is not sent in the paste event. maybe. 9655 9656 or if it is like 255, it is handled as a url and opened externally 9657 tho tbh a url could just be detected by regex pattern 9658 9659 9660 NOTE: if your program requests mouse input, the TE does not process it! Thus the user will have to shift+click for it. 9661 9662 mode 3004 for bracketed hyperlink 9663 9664 hyperlink sequence: \033[?220hnum;text\033[?220l~ 9665 9666 */