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