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 if(t.indexOf("putty") != -1) 856 t = "xterm"; 857 if(t.indexOf("tmux") != -1) 858 t = "tmux"; 859 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 == '[') { // CSI, ends on anything >= 'A' 4161 return doEscapeSequence(readEscapeSequence(sequenceBuffer)); 4162 } else if(c == 'O') { 4163 // could be xterm function key 4164 auto n = nextRaw(); 4165 4166 char[3] thing; 4167 thing[0] = '\033'; 4168 thing[1] = 'O'; 4169 thing[2] = cast(char) n; 4170 4171 auto cap = terminal.findSequenceInTermcap(thing); 4172 if(cap is null) { 4173 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ 4174 charPressAndRelease('O') ~ 4175 charPressAndRelease(thing[2]); 4176 } else { 4177 return translateTermcapName(cap); 4178 } 4179 } else if(c == '\033') { 4180 // could be escape followed by an escape sequence! 4181 return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ readNextEventsHelper(c); 4182 } else { 4183 // exceedingly quick esc followed by char is also what many terminals do for alt 4184 return charPressAndRelease(nextChar(c), cast(uint)ModifierState.alt); 4185 } 4186 } else { 4187 // FIXME: what if it is neither? we should check the termcap 4188 auto next = nextChar(c); 4189 if(next == 127) // some terminals send 127 on the backspace. Let's normalize that. 4190 next = '\b'; 4191 return charPressAndRelease(next); 4192 } 4193 } 4194 } 4195 4196 /++ 4197 The new style of keyboard event 4198 4199 Worth noting some special cases terminals tend to do: 4200 4201 $(LIST 4202 * Ctrl+space bar sends char 0. 4203 * 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. 4204 * Some combinations like ctrl+i are indistinguishable from other keys like tab. 4205 * 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`. 4206 * 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! 4207 * Shift is sometimes applied to the character, sometimes set in modifierState, sometimes both, sometimes neither. 4208 * On some systems, the return key sends \r and some sends \n. 4209 ) 4210 +/ 4211 struct KeyboardEvent { 4212 bool pressed; /// 4213 dchar which; /// 4214 alias key = which; /// I often use this when porting old to new so i took it 4215 alias character = which; /// I often use this when porting old to new so i took it 4216 uint modifierState; /// 4217 4218 // filter irrelevant modifiers... 4219 uint modifierStateFiltered() const { 4220 uint ms = modifierState; 4221 if(which < 32 && which != 9 && which != 8 && which != '\n') 4222 ms &= ~ModifierState.control; 4223 return ms; 4224 } 4225 4226 /++ 4227 Returns true if the event was a normal typed character. 4228 4229 You may also want to check modifiers if you want to process things differently when alt, ctrl, or shift is pressed. 4230 [modifierStateFiltered] returns only modifiers that are special in some way for the typed character. You can bitwise 4231 and that against [ModifierState]'s members to test. 4232 4233 [isUnmodifiedCharacter] does such a check for you. 4234 4235 $(NOTE 4236 Please note that enter, tab, and backspace count as characters. 4237 ) 4238 +/ 4239 bool isCharacter() { 4240 return !isNonCharacterKey() && !isProprietary(); 4241 } 4242 4243 /++ 4244 Returns true if this keyboard event represents a normal character keystroke, with no extraordinary modifier keys depressed. 4245 4246 Shift is considered an ordinary modifier except in the cases of tab, backspace, enter, and the space bar, since it is a normal 4247 part of entering many other characters. 4248 4249 History: 4250 Added December 4, 2020. 4251 +/ 4252 bool isUnmodifiedCharacter() { 4253 uint modsInclude = ModifierState.control | ModifierState.alt | ModifierState.meta; 4254 if(which == '\b' || which == '\t' || which == '\n' || which == '\r' || which == ' ' || which == 0) 4255 modsInclude |= ModifierState.shift; 4256 return isCharacter() && (modifierStateFiltered() & modsInclude) == 0; 4257 } 4258 4259 /++ 4260 Returns true if the key represents one of the range named entries in the [Key] enum. 4261 This does not necessarily mean it IS one of the named entries, just that it is in the 4262 range. Checking more precisely would require a loop in here and you are better off doing 4263 that in your own `switch` statement, with a do-nothing `default`. 4264 4265 Remember that users can create synthetic input of any character value. 4266 4267 History: 4268 While this function was present before, it was undocumented until December 4, 2020. 4269 +/ 4270 bool isNonCharacterKey() { 4271 return which >= Key.min && which <= Key.max; 4272 } 4273 4274 /// 4275 bool isProprietary() { 4276 return which >= ProprietaryPseudoKeys.min && which <= ProprietaryPseudoKeys.max; 4277 } 4278 4279 // these match Windows virtual key codes numerically for simplicity of translation there 4280 // but are plus a unicode private use area offset so i can cram them in the dchar 4281 // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 4282 /++ 4283 Represents non-character keys. 4284 +/ 4285 enum Key : dchar { 4286 escape = 0x1b + 0xF0000, /// . 4287 F1 = 0x70 + 0xF0000, /// . 4288 F2 = 0x71 + 0xF0000, /// . 4289 F3 = 0x72 + 0xF0000, /// . 4290 F4 = 0x73 + 0xF0000, /// . 4291 F5 = 0x74 + 0xF0000, /// . 4292 F6 = 0x75 + 0xF0000, /// . 4293 F7 = 0x76 + 0xF0000, /// . 4294 F8 = 0x77 + 0xF0000, /// . 4295 F9 = 0x78 + 0xF0000, /// . 4296 F10 = 0x79 + 0xF0000, /// . 4297 F11 = 0x7A + 0xF0000, /// . 4298 F12 = 0x7B + 0xF0000, /// . 4299 LeftArrow = 0x25 + 0xF0000, /// . 4300 RightArrow = 0x27 + 0xF0000, /// . 4301 UpArrow = 0x26 + 0xF0000, /// . 4302 DownArrow = 0x28 + 0xF0000, /// . 4303 Insert = 0x2d + 0xF0000, /// . 4304 Delete = 0x2e + 0xF0000, /// . 4305 Home = 0x24 + 0xF0000, /// . 4306 End = 0x23 + 0xF0000, /// . 4307 PageUp = 0x21 + 0xF0000, /// . 4308 PageDown = 0x22 + 0xF0000, /// . 4309 ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator 4310 4311 /* 4312 Enter = '\n', 4313 Backspace = '\b', 4314 Tab = '\t', 4315 */ 4316 } 4317 4318 /++ 4319 These are extensions added for better interop with the embedded emulator. 4320 As characters inside the unicode private-use area, you shouldn't encounter 4321 them unless you opt in by using some other proprietary feature. 4322 4323 History: 4324 Added December 4, 2020. 4325 +/ 4326 enum ProprietaryPseudoKeys : dchar { 4327 /++ 4328 If you use [Terminal.requestSetTerminalSelection], you should also process 4329 this pseudo-key to clear the selection when the terminal tells you do to keep 4330 you UI in sync. 4331 4332 History: 4333 Added December 4, 2020. 4334 +/ 4335 SelectNone = 0x0 + 0xF1000, // 987136 4336 } 4337 } 4338 4339 /// Deprecated: use KeyboardEvent instead in new programs 4340 /// Input event for characters 4341 struct CharacterEvent { 4342 /// . 4343 enum Type { 4344 Released, /// . 4345 Pressed /// . 4346 } 4347 4348 Type eventType; /// . 4349 dchar character; /// . 4350 uint modifierState; /// Don't depend on this to be available for character events 4351 } 4352 4353 /// Deprecated: use KeyboardEvent instead in new programs 4354 struct NonCharacterKeyEvent { 4355 /// . 4356 enum Type { 4357 Released, /// . 4358 Pressed /// . 4359 } 4360 Type eventType; /// . 4361 4362 // these match Windows virtual key codes numerically for simplicity of translation there 4363 //http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 4364 /// . 4365 enum Key : int { 4366 escape = 0x1b, /// . 4367 F1 = 0x70, /// . 4368 F2 = 0x71, /// . 4369 F3 = 0x72, /// . 4370 F4 = 0x73, /// . 4371 F5 = 0x74, /// . 4372 F6 = 0x75, /// . 4373 F7 = 0x76, /// . 4374 F8 = 0x77, /// . 4375 F9 = 0x78, /// . 4376 F10 = 0x79, /// . 4377 F11 = 0x7A, /// . 4378 F12 = 0x7B, /// . 4379 LeftArrow = 0x25, /// . 4380 RightArrow = 0x27, /// . 4381 UpArrow = 0x26, /// . 4382 DownArrow = 0x28, /// . 4383 Insert = 0x2d, /// . 4384 Delete = 0x2e, /// . 4385 Home = 0x24, /// . 4386 End = 0x23, /// . 4387 PageUp = 0x21, /// . 4388 PageDown = 0x22, /// . 4389 ScrollLock = 0x91, /// unlikely to work outside my terminal emulator 4390 } 4391 Key key; /// . 4392 4393 uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms 4394 4395 } 4396 4397 /// . 4398 struct PasteEvent { 4399 string pastedText; /// . 4400 } 4401 4402 /++ 4403 Indicates a hyperlink was clicked in my custom terminal emulator 4404 or with version `TerminalDirectToEmulator`. 4405 4406 You can simply ignore this event in a `final switch` if you aren't 4407 using the feature. 4408 4409 History: 4410 Added March 18, 2020 4411 +/ 4412 struct LinkEvent { 4413 string text; /// the text visible to the user that they clicked on 4414 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. 4415 ushort command; /// set by the terminal to indicate how it was clicked. values tbd, currently always 0 4416 } 4417 4418 /// . 4419 struct MouseEvent { 4420 // these match simpledisplay.d numerically as well 4421 /// . 4422 enum Type { 4423 Moved = 0, /// . 4424 Pressed = 1, /// . 4425 Released = 2, /// . 4426 Clicked, /// . 4427 } 4428 4429 Type eventType; /// . 4430 4431 // note: these should numerically match simpledisplay.d for maximum beauty in my other code 4432 /// . 4433 enum Button : uint { 4434 None = 0, /// . 4435 Left = 1, /// . 4436 Middle = 4, /// . 4437 Right = 2, /// . 4438 ScrollUp = 8, /// . 4439 ScrollDown = 16 /// . 4440 } 4441 uint buttons; /// A mask of Button 4442 int x; /// 0 == left side 4443 int y; /// 0 == top 4444 uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something 4445 } 4446 4447 /// When you get this, check terminal.width and terminal.height to see the new size and react accordingly. 4448 struct SizeChangedEvent { 4449 int oldWidth; 4450 int oldHeight; 4451 int newWidth; 4452 int newHeight; 4453 } 4454 4455 /// the user hitting ctrl+c will send this 4456 /// You should drop what you're doing and perhaps exit when this happens. 4457 struct UserInterruptionEvent {} 4458 4459 /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. 4460 /// If you receive it, you should generally cleanly exit. 4461 struct HangupEvent {} 4462 4463 /// Sent upon receiving end-of-file from stdin. 4464 struct EndOfFileEvent {} 4465 4466 interface CustomEvent {} 4467 4468 class RunnableCustomEvent : CustomEvent { 4469 this(void delegate() dg) { 4470 this.dg = dg; 4471 } 4472 4473 void run() { 4474 if(dg) 4475 dg(); 4476 } 4477 4478 private void delegate() dg; 4479 } 4480 4481 version(Win32Console) 4482 enum ModifierState : uint { 4483 shift = 0x10, 4484 control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl 4485 4486 // i'm not sure if the next two are available 4487 alt = 2 | 1, //2 ==left alt, 1 == right alt 4488 4489 // FIXME: I don't think these are actually available 4490 windows = 512, 4491 meta = 4096, // FIXME sanity 4492 4493 // I don't think this is available on Linux.... 4494 scrollLock = 0x40, 4495 } 4496 else 4497 enum ModifierState : uint { 4498 shift = 4, 4499 alt = 2, 4500 control = 16, 4501 meta = 8, 4502 4503 windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones 4504 } 4505 4506 version(DDoc) 4507 /// 4508 enum ModifierState : uint { 4509 /// 4510 shift = 4, 4511 /// 4512 alt = 2, 4513 /// 4514 control = 16, 4515 4516 } 4517 4518 /++ 4519 [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. 4520 ++/ 4521 struct InputEvent { 4522 /// . 4523 enum Type { 4524 KeyboardEvent, /// Keyboard key pressed (or released, where supported) 4525 CharacterEvent, /// Do not use this in new programs, use KeyboardEvent instead 4526 NonCharacterKeyEvent, /// Do not use this in new programs, use KeyboardEvent instead 4527 PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. 4528 LinkEvent, /// User clicked a hyperlink you created. Simply ignore if you are not using that feature. 4529 MouseEvent, /// only sent if you subscribed to mouse events 4530 SizeChangedEvent, /// only sent if you subscribed to size events 4531 UserInterruptionEvent, /// the user hit ctrl+c 4532 EndOfFileEvent, /// stdin has received an end of file 4533 HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator 4534 CustomEvent /// . 4535 } 4536 4537 /// If this event is deprecated, you should filter it out in new programs 4538 bool isDeprecated() { 4539 return type == Type.CharacterEvent || type == Type.NonCharacterKeyEvent; 4540 } 4541 4542 /// . 4543 @property Type type() { return t; } 4544 4545 /// Returns a pointer to the terminal associated with this event. 4546 /// (You can usually just ignore this as there's only one terminal typically.) 4547 /// 4548 /// It may be null in the case of program-generated events; 4549 @property Terminal* terminal() { return term; } 4550 4551 /++ 4552 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. 4553 4554 See_Also: 4555 4556 The event types: 4557 [KeyboardEvent], [MouseEvent], [SizeChangedEvent], 4558 [PasteEvent], [UserInterruptionEvent], 4559 [EndOfFileEvent], [HangupEvent], [CustomEvent] 4560 4561 And associated functions: 4562 [RealTimeConsoleInput], [ConsoleInputFlags] 4563 ++/ 4564 @property auto get(Type T)() { 4565 if(type != T) 4566 throw new Exception("Wrong event type"); 4567 static if(T == Type.CharacterEvent) 4568 return characterEvent; 4569 else static if(T == Type.KeyboardEvent) 4570 return keyboardEvent; 4571 else static if(T == Type.NonCharacterKeyEvent) 4572 return nonCharacterKeyEvent; 4573 else static if(T == Type.PasteEvent) 4574 return pasteEvent; 4575 else static if(T == Type.LinkEvent) 4576 return linkEvent; 4577 else static if(T == Type.MouseEvent) 4578 return mouseEvent; 4579 else static if(T == Type.SizeChangedEvent) 4580 return sizeChangedEvent; 4581 else static if(T == Type.UserInterruptionEvent) 4582 return userInterruptionEvent; 4583 else static if(T == Type.EndOfFileEvent) 4584 return endOfFileEvent; 4585 else static if(T == Type.HangupEvent) 4586 return hangupEvent; 4587 else static if(T == Type.CustomEvent) 4588 return customEvent; 4589 else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); 4590 } 4591 4592 /// custom event is public because otherwise there's no point at all 4593 this(CustomEvent c, Terminal* p = null) { 4594 t = Type.CustomEvent; 4595 customEvent = c; 4596 } 4597 4598 private { 4599 this(CharacterEvent c, Terminal* p) { 4600 t = Type.CharacterEvent; 4601 characterEvent = c; 4602 } 4603 this(KeyboardEvent c, Terminal* p) { 4604 t = Type.KeyboardEvent; 4605 keyboardEvent = c; 4606 } 4607 this(NonCharacterKeyEvent c, Terminal* p) { 4608 t = Type.NonCharacterKeyEvent; 4609 nonCharacterKeyEvent = c; 4610 } 4611 this(PasteEvent c, Terminal* p) { 4612 t = Type.PasteEvent; 4613 pasteEvent = c; 4614 } 4615 this(LinkEvent c, Terminal* p) { 4616 t = Type.LinkEvent; 4617 linkEvent = c; 4618 } 4619 this(MouseEvent c, Terminal* p) { 4620 t = Type.MouseEvent; 4621 mouseEvent = c; 4622 } 4623 this(SizeChangedEvent c, Terminal* p) { 4624 t = Type.SizeChangedEvent; 4625 sizeChangedEvent = c; 4626 } 4627 this(UserInterruptionEvent c, Terminal* p) { 4628 t = Type.UserInterruptionEvent; 4629 userInterruptionEvent = c; 4630 } 4631 this(HangupEvent c, Terminal* p) { 4632 t = Type.HangupEvent; 4633 hangupEvent = c; 4634 } 4635 this(EndOfFileEvent c, Terminal* p) { 4636 t = Type.EndOfFileEvent; 4637 endOfFileEvent = c; 4638 } 4639 4640 Type t; 4641 Terminal* term; 4642 4643 union { 4644 KeyboardEvent keyboardEvent; 4645 CharacterEvent characterEvent; 4646 NonCharacterKeyEvent nonCharacterKeyEvent; 4647 PasteEvent pasteEvent; 4648 MouseEvent mouseEvent; 4649 SizeChangedEvent sizeChangedEvent; 4650 UserInterruptionEvent userInterruptionEvent; 4651 HangupEvent hangupEvent; 4652 EndOfFileEvent endOfFileEvent; 4653 LinkEvent linkEvent; 4654 CustomEvent customEvent; 4655 } 4656 } 4657 } 4658 4659 version(Demo) 4660 /// View the source of this! 4661 void main() { 4662 auto terminal = Terminal(ConsoleOutputType.cellular); 4663 4664 //terminal.color(Color.DEFAULT, Color.DEFAULT); 4665 4666 // 4667 ///* 4668 auto getter = new FileLineGetter(&terminal, "test"); 4669 getter.prompt = "> "; 4670 //getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; 4671 terminal.writeln("\n" ~ getter.getline()); 4672 terminal.writeln("\n" ~ getter.getline()); 4673 terminal.writeln("\n" ~ getter.getline()); 4674 getter.dispose(); 4675 //*/ 4676 4677 terminal.writeln(terminal.getline()); 4678 terminal.writeln(terminal.getline()); 4679 terminal.writeln(terminal.getline()); 4680 4681 //input.getch(); 4682 4683 // return; 4684 // 4685 4686 terminal.setTitle("Basic I/O"); 4687 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEventsWithRelease); 4688 terminal.color(Color.green | Bright, Color.black); 4689 4690 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"); 4691 terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 4692 4693 terminal.color(Color.DEFAULT, Color.DEFAULT); 4694 4695 int centerX = terminal.width / 2; 4696 int centerY = terminal.height / 2; 4697 4698 bool timeToBreak = false; 4699 4700 terminal.hyperlink("test", 4); 4701 terminal.hyperlink("another", 7); 4702 4703 void handleEvent(InputEvent event) { 4704 //terminal.writef("%s\n", event.type); 4705 final switch(event.type) { 4706 case InputEvent.Type.LinkEvent: 4707 auto ev = event.get!(InputEvent.Type.LinkEvent); 4708 terminal.writeln(ev); 4709 break; 4710 case InputEvent.Type.UserInterruptionEvent: 4711 case InputEvent.Type.HangupEvent: 4712 case InputEvent.Type.EndOfFileEvent: 4713 timeToBreak = true; 4714 version(with_eventloop) { 4715 import arsd.eventloop; 4716 exit(); 4717 } 4718 break; 4719 case InputEvent.Type.SizeChangedEvent: 4720 auto ev = event.get!(InputEvent.Type.SizeChangedEvent); 4721 terminal.writeln(ev); 4722 break; 4723 case InputEvent.Type.KeyboardEvent: 4724 auto ev = event.get!(InputEvent.Type.KeyboardEvent); 4725 if(!ev.pressed) break; 4726 terminal.writef("\t%s", ev); 4727 terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which); 4728 terminal.writeln(); 4729 if(ev.which == 'Q') { 4730 timeToBreak = true; 4731 version(with_eventloop) { 4732 import arsd.eventloop; 4733 exit(); 4734 } 4735 } 4736 4737 if(ev.which == 'C') 4738 terminal.clear(); 4739 break; 4740 case InputEvent.Type.CharacterEvent: // obsolete 4741 auto ev = event.get!(InputEvent.Type.CharacterEvent); 4742 //terminal.writef("\t%s\n", ev); 4743 break; 4744 case InputEvent.Type.NonCharacterKeyEvent: // obsolete 4745 //terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent)); 4746 break; 4747 case InputEvent.Type.PasteEvent: 4748 terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent)); 4749 break; 4750 case InputEvent.Type.MouseEvent: 4751 terminal.writef("\t%s\n", event.get!(InputEvent.Type.MouseEvent)); 4752 break; 4753 case InputEvent.Type.CustomEvent: 4754 break; 4755 } 4756 4757 //terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 4758 4759 /* 4760 if(input.kbhit()) { 4761 auto c = input.getch(); 4762 if(c == 'q' || c == 'Q') 4763 break; 4764 terminal.moveTo(centerX, centerY); 4765 terminal.writef("%c", c); 4766 terminal.flush(); 4767 } 4768 usleep(10000); 4769 */ 4770 } 4771 4772 version(with_eventloop) { 4773 import arsd.eventloop; 4774 addListener(&handleEvent); 4775 loop(); 4776 } else { 4777 loop: while(true) { 4778 auto event = input.nextEvent(); 4779 handleEvent(event); 4780 if(timeToBreak) 4781 break loop; 4782 } 4783 } 4784 } 4785 4786 enum TerminalCapabilities : uint { 4787 minimal = 0, 4788 vt100 = 1 << 0, 4789 4790 // my special terminal emulator extensions 4791 arsdClipboard = 1 << 15, // 90 in caps 4792 arsdImage = 1 << 16, // 91 in caps 4793 arsdHyperlinks = 1 << 17, // 92 in caps 4794 } 4795 4796 version(Posix) 4797 private uint /* TerminalCapabilities bitmask */ getTerminalCapabilities(int fdIn, int fdOut) { 4798 if(fdIn == -1 || fdOut == -1) 4799 return TerminalCapabilities.minimal; 4800 4801 import std.conv; 4802 import core.stdc.errno; 4803 import core.sys.posix.unistd; 4804 4805 ubyte[128] hack2; 4806 termios old; 4807 ubyte[128] hack; 4808 tcgetattr(fdIn, &old); 4809 auto n = old; 4810 n.c_lflag &= ~(ICANON | ECHO); 4811 tcsetattr(fdIn, TCSANOW, &n); 4812 scope(exit) 4813 tcsetattr(fdIn, TCSANOW, &old); 4814 4815 // drain the buffer? meh 4816 4817 string cmd = "\033[c"; 4818 auto err = write(fdOut, cmd.ptr, cmd.length); 4819 if(err != cmd.length) { 4820 throw new Exception("couldn't ask terminal for ID"); 4821 } 4822 4823 // reading directly to bypass any buffering 4824 int retries = 16; 4825 int len; 4826 ubyte[96] buffer; 4827 try_again: 4828 4829 4830 timeval tv; 4831 tv.tv_sec = 0; 4832 tv.tv_usec = 250 * 1000; // 250 ms 4833 4834 fd_set fs; 4835 FD_ZERO(&fs); 4836 4837 FD_SET(fdIn, &fs); 4838 if(select(fdIn + 1, &fs, null, null, &tv) == -1) { 4839 goto try_again; 4840 } 4841 4842 if(FD_ISSET(fdIn, &fs)) { 4843 auto len2 = read(fdIn, &buffer[len], buffer.length - len); 4844 if(len2 <= 0) { 4845 retries--; 4846 if(retries > 0) 4847 goto try_again; 4848 throw new Exception("can't get terminal id"); 4849 } else { 4850 len += len2; 4851 } 4852 } else { 4853 // no data... assume terminal doesn't support giving an answer 4854 return TerminalCapabilities.minimal; 4855 } 4856 4857 ubyte[] answer; 4858 bool hasAnswer(ubyte[] data) { 4859 if(data.length < 4) 4860 return false; 4861 answer = null; 4862 size_t start; 4863 int position = 0; 4864 foreach(idx, ch; data) { 4865 switch(position) { 4866 case 0: 4867 if(ch == '\033') { 4868 start = idx; 4869 position++; 4870 } 4871 break; 4872 case 1: 4873 if(ch == '[') 4874 position++; 4875 else 4876 position = 0; 4877 break; 4878 case 2: 4879 if(ch == '?') 4880 position++; 4881 else 4882 position = 0; 4883 break; 4884 case 3: 4885 // body 4886 if(ch == 'c') { 4887 answer = data[start .. idx + 1]; 4888 return true; 4889 } else if(ch == ';' || (ch >= '0' && ch <= '9')) { 4890 // good, keep going 4891 } else { 4892 // invalid, drop it 4893 position = 0; 4894 } 4895 break; 4896 default: assert(0); 4897 } 4898 } 4899 return false; 4900 } 4901 4902 auto got = buffer[0 .. len]; 4903 if(!hasAnswer(got)) { 4904 if(retries > 0) 4905 goto try_again; 4906 else 4907 return TerminalCapabilities.minimal; 4908 } 4909 auto gots = cast(char[]) answer[3 .. $-1]; 4910 4911 import std.string; 4912 4913 auto pieces = split(gots, ";"); 4914 uint ret = TerminalCapabilities.vt100; 4915 foreach(p; pieces) 4916 switch(p) { 4917 case "90": 4918 ret |= TerminalCapabilities.arsdClipboard; 4919 break; 4920 case "91": 4921 ret |= TerminalCapabilities.arsdImage; 4922 break; 4923 case "92": 4924 ret |= TerminalCapabilities.arsdHyperlinks; 4925 break; 4926 default: 4927 } 4928 return ret; 4929 } 4930 4931 private extern(C) int mkstemp(char *templ); 4932 4933 /* 4934 FIXME: support lines that wrap 4935 FIXME: better controls maybe 4936 4937 FIXME: support multi-line "lines" and some form of line continuation, both 4938 from the user (if permitted) and from the application, so like the user 4939 hits "class foo { \n" and the app says "that line needs continuation" automatically. 4940 4941 FIXME: fix lengths on prompt and suggestion 4942 */ 4943 /** 4944 A user-interactive line editor class, used by [Terminal.getline]. It is similar to 4945 GNU readline, offering comparable features like tab completion, history, and graceful 4946 degradation to adapt to the user's terminal. 4947 4948 4949 A note on history: 4950 4951 $(WARNING 4952 To save history, you must call LineGetter.dispose() when you're done with it. 4953 History will not be automatically saved without that call! 4954 ) 4955 4956 The history saving and loading as a trivially encountered race condition: if you 4957 open two programs that use the same one at the same time, the one that closes second 4958 will overwrite any history changes the first closer saved. 4959 4960 GNU Getline does this too... and it actually kinda drives me nuts. But I don't know 4961 what a good fix is except for doing a transactional commit straight to the file every 4962 time and that seems like hitting the disk way too often. 4963 4964 We could also do like a history server like a database daemon that keeps the order 4965 correct but I don't actually like that either because I kinda like different bashes 4966 to have different history, I just don't like it all to get lost. 4967 4968 Regardless though, this isn't even used in bash anyway, so I don't think I care enough 4969 to put that much effort into it. Just using separate files for separate tasks is good 4970 enough I think. 4971 */ 4972 class LineGetter { 4973 /* A note on the assumeSafeAppends in here: since these buffers are private, we can be 4974 pretty sure that stomping isn't an issue, so I'm using this liberally to keep the 4975 append/realloc code simple and hopefully reasonably fast. */ 4976 4977 // saved to file 4978 string[] history; 4979 4980 // not saved 4981 Terminal* terminal; 4982 string historyFilename; 4983 4984 /// Make sure that the parent terminal struct remains in scope for the duration 4985 /// of LineGetter's lifetime, as it does hold on to and use the passed pointer 4986 /// throughout. 4987 /// 4988 /// historyFilename will load and save an input history log to a particular folder. 4989 /// Leaving it null will mean no file will be used and history will not be saved across sessions. 4990 this(Terminal* tty, string historyFilename = null) { 4991 this.terminal = tty; 4992 this.historyFilename = historyFilename; 4993 4994 line.reserve(128); 4995 4996 if(historyFilename.length) 4997 loadSettingsAndHistoryFromFile(); 4998 4999 regularForeground = cast(Color) terminal._currentForeground; 5000 background = cast(Color) terminal._currentBackground; 5001 suggestionForeground = Color.blue; 5002 } 5003 5004 /// Call this before letting LineGetter die so it can do any necessary 5005 /// cleanup and save the updated history to a file. 5006 void dispose() { 5007 if(historyFilename.length && historyCommitMode == HistoryCommitMode.atTermination) 5008 saveSettingsAndHistoryToFile(); 5009 } 5010 5011 /// Override this to change the directory where history files are stored 5012 /// 5013 /// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows. 5014 /* virtual */ string historyFileDirectory() { 5015 version(Windows) { 5016 char[1024] path; 5017 // FIXME: this doesn't link because the crappy dmd lib doesn't have it 5018 if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) { 5019 import core.stdc.string; 5020 return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline"; 5021 } else { 5022 import std.process; 5023 return environment["APPDATA"] ~ "\\arsd-getline"; 5024 } 5025 } else version(Posix) { 5026 import std.process; 5027 return environment["HOME"] ~ "/.arsd-getline"; 5028 } 5029 } 5030 5031 /// You can customize the colors here. You should set these after construction, but before 5032 /// calling startGettingLine or getline. 5033 Color suggestionForeground = Color.blue; 5034 Color regularForeground = Color.DEFAULT; /// ditto 5035 Color background = Color.DEFAULT; /// ditto 5036 Color promptColor = Color.DEFAULT; /// ditto 5037 Color specialCharBackground = Color.green; /// ditto 5038 //bool reverseVideo; 5039 5040 /// Set this if you want a prompt to be drawn with the line. It does NOT support color in string. 5041 @property void prompt(string p) { 5042 this.prompt_ = p; 5043 5044 promptLength = 0; 5045 foreach(dchar c; p) 5046 promptLength++; 5047 } 5048 5049 /// ditto 5050 @property string prompt() { 5051 return this.prompt_; 5052 } 5053 5054 private string prompt_; 5055 private int promptLength; 5056 5057 /++ 5058 Turn on auto suggest if you want a greyed thing of what tab 5059 would be able to fill in as you type. 5060 5061 You might want to turn it off if generating a completion list is slow. 5062 5063 Or if you know you want it, be sure to turn it on explicitly in your 5064 code because I reserve the right to change the default without advance notice. 5065 5066 History: 5067 On March 4, 2020, I changed the default to `false` because it 5068 is kinda slow and not useful in all cases. 5069 +/ 5070 bool autoSuggest = false; 5071 5072 /++ 5073 Returns true if there was any input in the buffer. Can be 5074 checked in the case of a [UserInterruptionException]. 5075 +/ 5076 bool hadInput() { 5077 return line.length > 0; 5078 } 5079 5080 /++ 5081 Override this if you don't want all lines added to the history. 5082 You can return null to not add it at all, or you can transform it. 5083 5084 History: 5085 Prior to October 12, 2021, it always committed all candidates. 5086 After that, it no longer commits in F9/ctrl+enter "run and maintain buffer" 5087 operations. This is tested with the [lastLineWasRetained] method. 5088 5089 The idea is those are temporary experiments and need not clog history until 5090 it is complete. 5091 +/ 5092 /* virtual */ string historyFilter(string candidate) { 5093 if(lastLineWasRetained()) 5094 return null; 5095 return candidate; 5096 } 5097 5098 /++ 5099 History is normally only committed to the file when the program is 5100 terminating, but if you are losing data due to crashes, you might want 5101 to change this to `historyCommitMode = HistoryCommitMode.afterEachLine;`. 5102 5103 History: 5104 Added January 26, 2021 (version 9.2) 5105 +/ 5106 public enum HistoryCommitMode { 5107 /// The history file is written to disk only at disposal time by calling [saveSettingsAndHistoryToFile] 5108 atTermination, 5109 /// The history file is written to disk after each line of input by calling [appendHistoryToFile] 5110 afterEachLine 5111 } 5112 5113 /// ditto 5114 public HistoryCommitMode historyCommitMode; 5115 5116 /++ 5117 You may override this to do nothing. If so, you should 5118 also override [appendHistoryToFile] if you ever change 5119 [historyCommitMode]. 5120 5121 You should call [historyPath] to get the proper filename. 5122 +/ 5123 /* virtual */ void saveSettingsAndHistoryToFile() { 5124 import std.file; 5125 if(!exists(historyFileDirectory)) 5126 mkdirRecurse(historyFileDirectory); 5127 5128 auto fn = historyPath(); 5129 5130 import std.stdio; 5131 auto file = File(fn, "wb"); 5132 file.write("// getline history file\r\n"); 5133 foreach(item; history) 5134 file.writeln(item, "\r"); 5135 } 5136 5137 /++ 5138 If [historyCommitMode] is [HistoryCommitMode.afterEachLine], 5139 this line is called after each line to append to the file instead 5140 of [saveSettingsAndHistoryToFile]. 5141 5142 Use [historyPath] to get the proper full path. 5143 5144 History: 5145 Added January 26, 2021 (version 9.2) 5146 +/ 5147 /* virtual */ void appendHistoryToFile(string item) { 5148 import std.file; 5149 5150 if(!exists(historyFileDirectory)) 5151 mkdirRecurse(historyFileDirectory); 5152 // this isn't exactly atomic but meh tbh i don't care. 5153 auto fn = historyPath(); 5154 if(exists(fn)) { 5155 append(fn, item ~ "\r\n"); 5156 } else { 5157 std.file.write(fn, "// getline history file\r\n" ~ item ~ "\r\n"); 5158 } 5159 } 5160 5161 /// You may override this to do nothing 5162 /* virtual */ void loadSettingsAndHistoryFromFile() { 5163 import std.file; 5164 history = null; 5165 auto fn = historyPath(); 5166 if(exists(fn)) { 5167 import std.stdio, std.algorithm, std.string; 5168 string cur; 5169 5170 auto file = File(fn, "rb"); 5171 auto first = file.readln(); 5172 if(first.startsWith("// getline history file")) { 5173 foreach(chunk; file.byChunk(1024)) { 5174 auto idx = (cast(char[]) chunk).indexOf(cast(char) '\r'); 5175 while(idx != -1) { 5176 cur ~= cast(char[]) chunk[0 .. idx]; 5177 history ~= cur; 5178 cur = null; 5179 if(idx + 2 <= chunk.length) 5180 chunk = chunk[idx + 2 .. $]; // skipping \r\n 5181 else 5182 chunk = chunk[$ .. $]; 5183 idx = (cast(char[]) chunk).indexOf(cast(char) '\r'); 5184 } 5185 cur ~= cast(char[]) chunk; 5186 } 5187 if(cur.length) 5188 history ~= cur; 5189 } else { 5190 // old-style plain file 5191 history ~= first; 5192 foreach(line; file.byLine()) 5193 history ~= line.idup; 5194 } 5195 } 5196 } 5197 5198 /++ 5199 History: 5200 Introduced on January 31, 2020 5201 +/ 5202 /* virtual */ string historyFileExtension() { 5203 return ".history"; 5204 } 5205 5206 /// semi-private, do not rely upon yet 5207 final string historyPath() { 5208 import std.path; 5209 auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension(); 5210 return filename; 5211 } 5212 5213 /++ 5214 Override this to provide tab completion. You may use the candidate 5215 argument to filter the list, but you don't have to (LineGetter will 5216 do it for you on the values you return). This means you can ignore 5217 the arguments if you like. 5218 5219 Ideally, you wouldn't return more than about ten items since the list 5220 gets difficult to use if it is too long. 5221 5222 Tab complete cannot modify text before or after the cursor at this time. 5223 I *might* change that later to allow tab complete to fuzzy search and spell 5224 check fix before. But right now it ONLY inserts. 5225 5226 Default is to provide recent command history as autocomplete. 5227 5228 $(WARNING Both `candidate` and `afterCursor` may have private data packed into the dchar bits 5229 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5230 5231 Returns: 5232 This function should return the full string to replace 5233 `candidate[tabCompleteStartPoint(args) .. $]`. 5234 For example, if your user wrote `wri<tab>` and you want to complete 5235 it to `write` or `writeln`, you should return `["write", "writeln"]`. 5236 5237 If you offer different tab complete in different places, you still 5238 need to return the whole string. For example, a file completion of 5239 a second argument, when the user writes `terminal.d term<tab>` and you 5240 want it to complete to an additional `terminal.d`, you should return 5241 `["terminal.d terminal.d"]`; in other words, `candidate ~ completion` 5242 for each completion. 5243 5244 It does this so you can simply return an array of words without having 5245 to rebuild that array for each combination. 5246 5247 To choose the word separator, override [tabCompleteStartPoint]. 5248 5249 Params: 5250 candidate = the text of the line up to the text cursor, after 5251 which the completed text would be inserted 5252 5253 afterCursor = the remaining text after the cursor. You can inspect 5254 this, but cannot change it - this will be appended to the line 5255 after completion, keeping the cursor in the same relative location. 5256 5257 History: 5258 Prior to January 30, 2020, this method took only one argument, 5259 `candidate`. It now takes `afterCursor` as well, to allow you to 5260 make more intelligent completions with full context. 5261 +/ 5262 /* virtual */ protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { 5263 return history.length > 20 ? history[0 .. 20] : history; 5264 } 5265 5266 /++ 5267 Override this to provide a different tab competition starting point. The default 5268 is `0`, always completing the complete line, but you may return the index of another 5269 character of `candidate` to provide a new split. 5270 5271 $(WARNING Both `candidate` and `afterCursor` may have private data packed into the dchar bits 5272 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5273 5274 Returns: 5275 The index of `candidate` where we should start the slice to keep in [tabComplete]. 5276 It must be `>= 0 && <= candidate.length`. 5277 5278 History: 5279 Added on February 1, 2020. Initial default is to return 0 to maintain 5280 old behavior. 5281 +/ 5282 /* virtual */ protected size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { 5283 return 0; 5284 } 5285 5286 /++ 5287 This gives extra information for an item when displaying tab competition details. 5288 5289 History: 5290 Added January 31, 2020. 5291 5292 +/ 5293 /* virtual */ protected string tabCompleteHelp(string candidate) { 5294 return null; 5295 } 5296 5297 private string[] filterTabCompleteList(string[] list, size_t start) { 5298 if(list.length == 0) 5299 return list; 5300 5301 string[] f; 5302 f.reserve(list.length); 5303 5304 foreach(item; list) { 5305 import std.algorithm; 5306 if(startsWith(item, line[start .. cursorPosition].map!(x => x & ~PRIVATE_BITS_MASK))) 5307 f ~= item; 5308 } 5309 5310 /+ 5311 // if it is excessively long, let's trim it down by trying to 5312 // group common sub-sequences together. 5313 if(f.length > terminal.height * 3 / 4) { 5314 import std.algorithm; 5315 f.sort(); 5316 5317 // see how many can be saved by just keeping going until there is 5318 // no more common prefix. then commit that and keep on down the list. 5319 // since it is sorted, if there is a commonality, it should appear quickly 5320 string[] n; 5321 string commonality = f[0]; 5322 size_t idx = 1; 5323 while(idx < f.length) { 5324 auto c = commonPrefix(commonality, f[idx]); 5325 if(c.length > cursorPosition - start) { 5326 commonality = c; 5327 } else { 5328 n ~= commonality; 5329 commonality = f[idx]; 5330 } 5331 idx++; 5332 } 5333 if(commonality.length) 5334 n ~= commonality; 5335 5336 if(n.length) 5337 f = n; 5338 } 5339 +/ 5340 5341 return f; 5342 } 5343 5344 /++ 5345 Override this to provide a custom display of the tab completion list. 5346 5347 History: 5348 Prior to January 31, 2020, it only displayed the list. After 5349 that, it would call [tabCompleteHelp] for each candidate and display 5350 that string (if present) as well. 5351 +/ 5352 protected void showTabCompleteList(string[] list) { 5353 if(list.length) { 5354 // FIXME: allow mouse clicking of an item, that would be cool 5355 5356 auto start = tabCompleteStartPoint(line[0 .. cursorPosition], line[cursorPosition .. $]); 5357 5358 // FIXME: scroll 5359 //if(terminal.type == ConsoleOutputType.linear) { 5360 terminal.writeln(); 5361 foreach(item; list) { 5362 terminal.color(suggestionForeground, background); 5363 import std.utf; 5364 auto idx = codeLength!char(line[start .. cursorPosition]); 5365 terminal.write(" ", item[0 .. idx]); 5366 terminal.color(regularForeground, background); 5367 terminal.write(item[idx .. $]); 5368 auto help = tabCompleteHelp(item); 5369 if(help !is null) { 5370 import std.string; 5371 help = help.replace("\t", " ").replace("\n", " ").replace("\r", " "); 5372 terminal.write("\t\t"); 5373 int remaining; 5374 if(terminal.cursorX + 2 < terminal.width) { 5375 remaining = terminal.width - terminal.cursorX - 2; 5376 } 5377 if(remaining > 8) { 5378 string msg = help; 5379 foreach(idxh, dchar c; msg) { 5380 remaining--; 5381 if(remaining <= 0) { 5382 msg = msg[0 .. idxh]; 5383 break; 5384 } 5385 } 5386 5387 /+ 5388 size_t use = help.length < remaining ? help.length : remaining; 5389 5390 if(use < help.length) { 5391 if((help[use] & 0xc0) != 0x80) { 5392 import std.utf; 5393 use += stride(help[use .. $]); 5394 } else { 5395 // just get to the end of this code point 5396 while(use < help.length && (help[use] & 0xc0) == 0x80) 5397 use++; 5398 } 5399 } 5400 auto msg = help[0 .. use]; 5401 +/ 5402 if(msg.length) 5403 terminal.write(msg); 5404 } 5405 } 5406 terminal.writeln(); 5407 5408 } 5409 updateCursorPosition(); 5410 redraw(); 5411 //} 5412 } 5413 } 5414 5415 /++ 5416 Called by the default event loop when the user presses F1. Override 5417 `showHelp` to change the UI, override [helpMessage] if you just want 5418 to change the message. 5419 5420 History: 5421 Introduced on January 30, 2020 5422 +/ 5423 protected void showHelp() { 5424 terminal.writeln(); 5425 terminal.writeln(helpMessage); 5426 updateCursorPosition(); 5427 redraw(); 5428 } 5429 5430 /++ 5431 History: 5432 Introduced on January 30, 2020 5433 +/ 5434 protected string helpMessage() { 5435 return "Press F2 to edit current line in your external editor. F3 searches history. F9 runs current line while maintaining current edit state."; 5436 } 5437 5438 /++ 5439 $(WARNING `line` may have private data packed into the dchar bits 5440 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5441 5442 History: 5443 Introduced on January 30, 2020 5444 +/ 5445 protected dchar[] editLineInEditor(in dchar[] line, in size_t cursorPosition) { 5446 import std.conv; 5447 import std.process; 5448 import std.file; 5449 5450 char[] tmpName; 5451 5452 version(Windows) { 5453 import core.stdc.string; 5454 char[280] path; 5455 auto l = GetTempPathA(cast(DWORD) path.length, path.ptr); 5456 if(l == 0) throw new Exception("GetTempPathA"); 5457 path[l] = 0; 5458 char[280] name; 5459 auto r = GetTempFileNameA(path.ptr, "adr", 0, name.ptr); 5460 if(r == 0) throw new Exception("GetTempFileNameA"); 5461 tmpName = name[0 .. strlen(name.ptr)]; 5462 scope(exit) 5463 std.file.remove(tmpName); 5464 std.file.write(tmpName, to!string(line)); 5465 5466 string editor = environment.get("EDITOR", "notepad.exe"); 5467 } else { 5468 import core.stdc.stdlib; 5469 import core.sys.posix.unistd; 5470 char[120] name; 5471 string p = "/tmp/adrXXXXXX"; 5472 name[0 .. p.length] = p[]; 5473 name[p.length] = 0; 5474 auto fd = mkstemp(name.ptr); 5475 tmpName = name[0 .. p.length]; 5476 if(fd == -1) throw new Exception("mkstemp"); 5477 scope(exit) 5478 close(fd); 5479 scope(exit) 5480 std.file.remove(tmpName); 5481 5482 string s = to!string(line); 5483 while(s.length) { 5484 auto x = write(fd, s.ptr, s.length); 5485 if(x == -1) throw new Exception("write"); 5486 s = s[x .. $]; 5487 } 5488 string editor = environment.get("EDITOR", "vi"); 5489 } 5490 5491 // FIXME the spawned process changes even more terminal state than set up here! 5492 5493 try { 5494 version(none) 5495 if(UseVtSequences) { 5496 if(terminal.type == ConsoleOutputType.cellular) { 5497 terminal.doTermcap("te"); 5498 } 5499 } 5500 version(Posix) { 5501 import std.stdio; 5502 // need to go to the parent terminal jic we're in an embedded terminal with redirection 5503 terminal.write(" !! Editor may be in parent terminal !!"); 5504 terminal.flush(); 5505 spawnProcess([editor, tmpName], File("/dev/tty", "rb"), File("/dev/tty", "wb")).wait; 5506 } else { 5507 spawnProcess([editor, tmpName]).wait; 5508 } 5509 if(UseVtSequences) { 5510 if(terminal.type == ConsoleOutputType.cellular) 5511 terminal.doTermcap("ti"); 5512 } 5513 import std.string; 5514 return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp; 5515 } catch(Exception e) { 5516 // edit failed, we should prolly tell them but idk how.... 5517 return null; 5518 } 5519 } 5520 5521 //private RealTimeConsoleInput* rtci; 5522 5523 /// One-call shop for the main workhorse 5524 /// If you already have a RealTimeConsoleInput ready to go, you 5525 /// should pass a pointer to yours here. Otherwise, LineGetter will 5526 /// make its own. 5527 public string getline(RealTimeConsoleInput* input = null) { 5528 startGettingLine(); 5529 if(input is null) { 5530 auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents | ConsoleInputFlags.selectiveMouse | ConsoleInputFlags.noEolWrap); 5531 //rtci = &i; 5532 //scope(exit) rtci = null; 5533 while(workOnLine(i.nextEvent(), &i)) {} 5534 } else { 5535 //rtci = input; 5536 //scope(exit) rtci = null; 5537 while(workOnLine(input.nextEvent(), input)) {} 5538 } 5539 return finishGettingLine(); 5540 } 5541 5542 /++ 5543 Set in [historyRecallFilterMethod]. 5544 5545 History: 5546 Added November 27, 2020. 5547 +/ 5548 enum HistoryRecallFilterMethod { 5549 /++ 5550 Goes through history in simple chronological order. 5551 Your existing command entry is not considered as a filter. 5552 +/ 5553 chronological, 5554 /++ 5555 Goes through history filtered with only those that begin with your current command entry. 5556 5557 So, if you entered "animal", "and", "bad", "cat" previously, then enter 5558 "a" and pressed up, it would jump to "and", then up again would go to "animal". 5559 +/ 5560 prefixed, 5561 /++ 5562 Goes through history filtered with only those that $(B contain) your current command entry. 5563 5564 So, if you entered "animal", "and", "bad", "cat" previously, then enter 5565 "n" and pressed up, it would jump to "and", then up again would go to "animal". 5566 +/ 5567 containing, 5568 /++ 5569 Goes through history to fill in your command at the cursor. It filters to only entries 5570 that start with the text before your cursor and ends with text after your cursor. 5571 5572 So, if you entered "animal", "and", "bad", "cat" previously, then enter 5573 "ad" and pressed left to position the cursor between the a and d, then pressed up 5574 it would jump straight to "and". 5575 +/ 5576 sandwiched, 5577 } 5578 /++ 5579 Controls what happens when the user presses the up key, etc., to recall history entries. See [HistoryRecallMethod] for the options. 5580 5581 This has no effect on the history search user control (default key: F3 or ctrl+r), which always searches through a "containing" method. 5582 5583 History: 5584 Added November 27, 2020. 5585 +/ 5586 HistoryRecallFilterMethod historyRecallFilterMethod = HistoryRecallFilterMethod.chronological; 5587 5588 /++ 5589 Enables automatic closing of brackets like (, {, and [ when the user types. 5590 Specifically, you subclass and return a string of the completions you want to 5591 do, so for that set, return `"()[]{}"` 5592 5593 5594 $(WARNING 5595 If you subclass this and return anything other than `null`, your subclass must also 5596 realize that the `line` member and everything that slices it ([tabComplete] and more) 5597 need to mask away the extra bits to get the original content. See [PRIVATE_BITS_MASK]. 5598 `line[] &= cast(dchar) ~PRIVATE_BITS_MASK;` 5599 ) 5600 5601 Returns: 5602 A string with pairs of characters. When the user types the character in an even-numbered 5603 position, it automatically inserts the following character after the cursor (without moving 5604 the cursor). The inserted character will be automatically overstriken if the user types it 5605 again. 5606 5607 The default is `return null`, which disables the feature. 5608 5609 History: 5610 Added January 25, 2021 (version 9.2) 5611 +/ 5612 protected string enableAutoCloseBrackets() { 5613 return null; 5614 } 5615 5616 /++ 5617 If [enableAutoCloseBrackets] does not return null, you should ignore these bits in the line. 5618 +/ 5619 protected enum PRIVATE_BITS_MASK = 0x80_00_00_00; 5620 // note: several instances in the code of PRIVATE_BITS_MASK are kinda conservative; masking it away is destructive 5621 // but less so than crashing cuz of invalid unicode character popping up later. Besides the main intention is when 5622 // you are kinda immediately typing so it forgetting is probably fine. 5623 5624 /++ 5625 Subclasses that implement this function can enable syntax highlighting in the line as you edit it. 5626 5627 5628 The library will call this when it prepares to draw the line, giving you the full line as well as the 5629 current position in that array it is about to draw. You return a [SyntaxHighlightMatch] 5630 object with its `charsMatched` member set to how many characters the given colors should apply to. 5631 If it is set to zero, default behavior is retained for the next character, and [syntaxHighlightMatch] 5632 will be called again immediately. If it is set to -1 syntax highlighting is disabled for the rest of 5633 the line. If set to int.max, it will apply to the remainder of the line. 5634 5635 If it is set to another positive value, the given colors are applied for that number of characters and 5636 [syntaxHighlightMatch] will NOT be called again until those characters are consumed. 5637 5638 Note that the first call may have `currentDrawPosition` be greater than zero due to horizontal scrolling. 5639 After that though, it will be called based on your `charsMatched` in the return value. 5640 5641 `currentCursorPosition` is passed in case you want to do things like highlight a matching parenthesis over 5642 the cursor or similar. You can also simply ignore it. 5643 5644 $(WARNING `line` may have private data packed into the dchar bits 5645 if you enabled [enableAutoCloseBrackets]. Use `ch & ~PRIVATE_BITS_MASK` to get standard dchars.) 5646 5647 History: 5648 Added January 25, 2021 (version 9.2) 5649 +/ 5650 protected SyntaxHighlightMatch syntaxHighlightMatch(in dchar[] line, in size_t currentDrawPosition, in size_t currentCursorPosition) { 5651 return SyntaxHighlightMatch(-1); // -1 just means syntax highlighting is disabled and it shouldn't try again 5652 } 5653 5654 /// ditto 5655 static struct SyntaxHighlightMatch { 5656 int charsMatched = 0; 5657 Color foreground = Color.DEFAULT; 5658 Color background = Color.DEFAULT; 5659 } 5660 5661 5662 private int currentHistoryViewPosition = 0; 5663 private dchar[] uncommittedHistoryCandidate; 5664 private int uncommitedHistoryCursorPosition; 5665 void loadFromHistory(int howFarBack) { 5666 if(howFarBack < 0) 5667 howFarBack = 0; 5668 if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around. 5669 howFarBack = cast(int) history.length; 5670 if(howFarBack == currentHistoryViewPosition) 5671 return; 5672 if(currentHistoryViewPosition == 0) { 5673 // save the current line so we can down arrow back to it later 5674 if(uncommittedHistoryCandidate.length < line.length) { 5675 uncommittedHistoryCandidate.length = line.length; 5676 } 5677 5678 uncommittedHistoryCandidate[0 .. line.length] = line[]; 5679 uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length]; 5680 uncommittedHistoryCandidate.assumeSafeAppend(); 5681 uncommitedHistoryCursorPosition = cursorPosition; 5682 } 5683 5684 if(howFarBack == 0) { 5685 zero: 5686 line.length = uncommittedHistoryCandidate.length; 5687 line.assumeSafeAppend(); 5688 line[] = uncommittedHistoryCandidate[]; 5689 } else { 5690 line = line[0 .. 0]; 5691 line.assumeSafeAppend(); 5692 5693 string selection; 5694 5695 final switch(historyRecallFilterMethod) with(HistoryRecallFilterMethod) { 5696 case chronological: 5697 selection = history[$ - howFarBack]; 5698 break; 5699 case prefixed: 5700 case containing: 5701 import std.algorithm; 5702 int count; 5703 foreach_reverse(item; history) { 5704 if( 5705 (historyRecallFilterMethod == prefixed && item.startsWith(uncommittedHistoryCandidate)) 5706 || 5707 (historyRecallFilterMethod == containing && item.canFind(uncommittedHistoryCandidate)) 5708 ) 5709 { 5710 selection = item; 5711 count++; 5712 if(count == howFarBack) 5713 break; 5714 } 5715 } 5716 howFarBack = count; 5717 break; 5718 case sandwiched: 5719 import std.algorithm; 5720 int count; 5721 foreach_reverse(item; history) { 5722 if( 5723 (item.startsWith(uncommittedHistoryCandidate[0 .. uncommitedHistoryCursorPosition])) 5724 && 5725 (item.endsWith(uncommittedHistoryCandidate[uncommitedHistoryCursorPosition .. $])) 5726 ) 5727 { 5728 selection = item; 5729 count++; 5730 if(count == howFarBack) 5731 break; 5732 } 5733 } 5734 howFarBack = count; 5735 5736 break; 5737 } 5738 5739 if(howFarBack == 0) 5740 goto zero; 5741 5742 int i; 5743 line.length = selection.length; 5744 foreach(dchar ch; selection) 5745 line[i++] = ch; 5746 line = line[0 .. i]; 5747 line.assumeSafeAppend(); 5748 } 5749 5750 currentHistoryViewPosition = howFarBack; 5751 cursorPosition = cast(int) line.length; 5752 scrollToEnd(); 5753 } 5754 5755 bool insertMode = true; 5756 5757 private ConsoleOutputType original = cast(ConsoleOutputType) -1; 5758 private bool multiLineModeOn = false; 5759 private int startOfLineXOriginal; 5760 private int startOfLineYOriginal; 5761 void multiLineMode(bool on) { 5762 if(original == -1) { 5763 original = terminal.type; 5764 startOfLineXOriginal = startOfLineX; 5765 startOfLineYOriginal = startOfLineY; 5766 } 5767 5768 if(on) { 5769 terminal.enableAlternateScreen = true; 5770 startOfLineX = 0; 5771 startOfLineY = 0; 5772 } 5773 else if(original == ConsoleOutputType.linear) { 5774 terminal.enableAlternateScreen = false; 5775 } 5776 5777 if(!on) { 5778 startOfLineX = startOfLineXOriginal; 5779 startOfLineY = startOfLineYOriginal; 5780 } 5781 5782 multiLineModeOn = on; 5783 } 5784 bool multiLineMode() { return multiLineModeOn; } 5785 5786 void toggleMultiLineMode() { 5787 multiLineMode = !multiLineModeOn; 5788 redraw(); 5789 } 5790 5791 private dchar[] line; 5792 private int cursorPosition = 0; 5793 private int horizontalScrollPosition = 0; 5794 private int verticalScrollPosition = 0; 5795 5796 private void scrollToEnd() { 5797 if(multiLineMode) { 5798 // FIXME 5799 } else { 5800 horizontalScrollPosition = (cast(int) line.length); 5801 horizontalScrollPosition -= availableLineLength(); 5802 if(horizontalScrollPosition < 0) 5803 horizontalScrollPosition = 0; 5804 } 5805 } 5806 5807 // used for redrawing the line in the right place 5808 // and detecting mouse events on our line. 5809 private int startOfLineX; 5810 private int startOfLineY; 5811 5812 // private string[] cachedCompletionList; 5813 5814 // FIXME 5815 // /// Note that this assumes the tab complete list won't change between actual 5816 // /// presses of tab by the user. If you pass it a list, it will use it, but 5817 // /// otherwise it will keep track of the last one to avoid calls to tabComplete. 5818 private string suggestion(string[] list = null) { 5819 import std.algorithm, std.utf; 5820 auto relevantLineSection = line[0 .. cursorPosition]; 5821 auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); 5822 relevantLineSection = relevantLineSection[start .. $]; 5823 // FIXME: see about caching the list if we easily can 5824 if(list is null) 5825 list = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); 5826 5827 if(list.length) { 5828 string commonality = list[0]; 5829 foreach(item; list[1 .. $]) { 5830 commonality = commonPrefix(commonality, item); 5831 } 5832 5833 if(commonality.length) { 5834 return commonality[codeLength!char(relevantLineSection) .. $]; 5835 } 5836 } 5837 5838 return null; 5839 } 5840 5841 /// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something. 5842 /// You'll probably want to call redraw() after adding chars. 5843 void addChar(dchar ch) { 5844 assert(cursorPosition >= 0 && cursorPosition <= line.length); 5845 if(cursorPosition == line.length) 5846 line ~= ch; 5847 else { 5848 assert(line.length); 5849 if(insertMode) { 5850 line ~= ' '; 5851 for(int i = cast(int) line.length - 2; i >= cursorPosition; i --) 5852 line[i + 1] = line[i]; 5853 } 5854 line[cursorPosition] = ch; 5855 } 5856 cursorPosition++; 5857 5858 if(multiLineMode) { 5859 // FIXME 5860 } else { 5861 if(cursorPosition > horizontalScrollPosition + availableLineLength()) 5862 horizontalScrollPosition++; 5863 } 5864 5865 lineChanged = true; 5866 } 5867 5868 /// . 5869 void addString(string s) { 5870 // FIXME: this could be more efficient 5871 // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) 5872 5873 import std.utf; 5874 foreach(dchar ch; s.byDchar) // using this for the replacement dchar, normal foreach would throw on invalid utf 8 5875 addChar(ch); 5876 } 5877 5878 /// Deletes the character at the current position in the line. 5879 /// You'll probably want to call redraw() after deleting chars. 5880 void deleteChar() { 5881 if(cursorPosition == line.length) 5882 return; 5883 for(int i = cursorPosition; i < line.length - 1; i++) 5884 line[i] = line[i + 1]; 5885 line = line[0 .. $-1]; 5886 line.assumeSafeAppend(); 5887 lineChanged = true; 5888 } 5889 5890 protected bool lineChanged; 5891 5892 private void killText(dchar[] text) { 5893 if(!text.length) 5894 return; 5895 5896 if(justKilled) 5897 killBuffer = text ~ killBuffer; 5898 else 5899 killBuffer = text; 5900 } 5901 5902 /// 5903 void deleteToEndOfLine() { 5904 killText(line[cursorPosition .. $]); 5905 line = line[0 .. cursorPosition]; 5906 line.assumeSafeAppend(); 5907 //while(cursorPosition < line.length) 5908 //deleteChar(); 5909 } 5910 5911 /++ 5912 Used by the word movement keys (e.g. alt+backspace) to find a word break. 5913 5914 History: 5915 Added April 21, 2021 (dub v9.5) 5916 5917 Prior to that, [LineGetter] only used [std.uni.isWhite]. Now it uses this which 5918 uses if not alphanum and not underscore. 5919 5920 You can subclass this to customize its behavior. 5921 +/ 5922 bool isWordSeparatorCharacter(dchar d) { 5923 import std.uni : isAlphaNum; 5924 5925 return !(isAlphaNum(d) || d == '_'); 5926 } 5927 5928 private int wordForwardIdx() { 5929 int cursorPosition = this.cursorPosition; 5930 if(cursorPosition == line.length) 5931 return cursorPosition; 5932 while(cursorPosition + 1 < line.length && isWordSeparatorCharacter(line[cursorPosition])) 5933 cursorPosition++; 5934 while(cursorPosition + 1 < line.length && !isWordSeparatorCharacter(line[cursorPosition + 1])) 5935 cursorPosition++; 5936 cursorPosition += 2; 5937 if(cursorPosition > line.length) 5938 cursorPosition = cast(int) line.length; 5939 5940 return cursorPosition; 5941 } 5942 void wordForward() { 5943 cursorPosition = wordForwardIdx(); 5944 aligned(cursorPosition, 1); 5945 maybePositionCursor(); 5946 } 5947 void killWordForward() { 5948 int to = wordForwardIdx(), from = cursorPosition; 5949 killText(line[from .. to]); 5950 line = line[0 .. from] ~ line[to .. $]; 5951 cursorPosition = cast(int)from; 5952 maybePositionCursor(); 5953 } 5954 private int wordBackIdx() { 5955 if(!line.length || !cursorPosition) 5956 return cursorPosition; 5957 int ret = cursorPosition - 1; 5958 while(ret && isWordSeparatorCharacter(line[ret])) 5959 ret--; 5960 while(ret && !isWordSeparatorCharacter(line[ret - 1])) 5961 ret--; 5962 return ret; 5963 } 5964 void wordBack() { 5965 cursorPosition = wordBackIdx(); 5966 aligned(cursorPosition, -1); 5967 maybePositionCursor(); 5968 } 5969 void killWord() { 5970 int from = wordBackIdx(), to = cursorPosition; 5971 killText(line[from .. to]); 5972 line = line[0 .. from] ~ line[to .. $]; 5973 cursorPosition = cast(int)from; 5974 maybePositionCursor(); 5975 } 5976 5977 private void maybePositionCursor() { 5978 if(multiLineMode) { 5979 // omg this is so bad 5980 // and it more accurately sets scroll position 5981 int x, y; 5982 foreach(idx, ch; line) { 5983 if(idx == cursorPosition) 5984 break; 5985 if(ch == '\n') { 5986 x = 0; 5987 y++; 5988 } else { 5989 x++; 5990 } 5991 } 5992 5993 while(x - horizontalScrollPosition < 0) { 5994 horizontalScrollPosition -= terminal.width / 2; 5995 if(horizontalScrollPosition < 0) 5996 horizontalScrollPosition = 0; 5997 } 5998 while(y - verticalScrollPosition < 0) { 5999 verticalScrollPosition --; 6000 if(verticalScrollPosition < 0) 6001 verticalScrollPosition = 0; 6002 } 6003 6004 while((x - horizontalScrollPosition) >= terminal.width) { 6005 horizontalScrollPosition += terminal.width / 2; 6006 } 6007 while((y - verticalScrollPosition) + 2 >= terminal.height) { 6008 verticalScrollPosition ++; 6009 } 6010 6011 } else { 6012 if(cursorPosition < horizontalScrollPosition || cursorPosition > horizontalScrollPosition + availableLineLength()) { 6013 positionCursor(); 6014 } 6015 } 6016 } 6017 6018 private void charBack() { 6019 if(!cursorPosition) 6020 return; 6021 cursorPosition--; 6022 aligned(cursorPosition, -1); 6023 maybePositionCursor(); 6024 } 6025 private void charForward() { 6026 if(cursorPosition >= line.length) 6027 return; 6028 cursorPosition++; 6029 aligned(cursorPosition, 1); 6030 maybePositionCursor(); 6031 } 6032 6033 int availableLineLength() { 6034 return maximumDrawWidth - promptLength - 1; 6035 } 6036 6037 /++ 6038 Controls the input echo setting. 6039 6040 Possible values are: 6041 6042 `dchar.init` = normal; user can see their input. 6043 6044 `'\0'` = nothing; the cursor does not visually move as they edit. Similar to Unix style password prompts. 6045 6046 `'*'` (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. 6047 6048 History: 6049 Added October 11, 2021 (dub v10.4) 6050 +/ 6051 dchar echoChar = dchar.init; 6052 6053 protected static struct Drawer { 6054 LineGetter lg; 6055 6056 this(LineGetter lg) { 6057 this.lg = lg; 6058 linesRemaining = lg.terminal.height - 1; 6059 } 6060 6061 int written; 6062 int lineLength; 6063 6064 int linesRemaining; 6065 6066 6067 Color currentFg_ = Color.DEFAULT; 6068 Color currentBg_ = Color.DEFAULT; 6069 int colorChars = 0; 6070 6071 Color currentFg() { 6072 if(colorChars <= 0 || currentFg_ == Color.DEFAULT) 6073 return lg.regularForeground; 6074 return currentFg_; 6075 } 6076 6077 Color currentBg() { 6078 if(colorChars <= 0 || currentBg_ == Color.DEFAULT) 6079 return lg.background; 6080 return currentBg_; 6081 } 6082 6083 void specialChar(char c) { 6084 // maybe i should check echoChar here too but meh 6085 6086 lg.terminal.color(lg.regularForeground, lg.specialCharBackground); 6087 lg.terminal.write(c); 6088 lg.terminal.color(currentFg, currentBg); 6089 6090 written++; 6091 lineLength--; 6092 } 6093 6094 void regularChar(dchar ch) { 6095 import std.utf; 6096 char[4] buffer; 6097 6098 if(lg.echoChar == '\0') 6099 return; 6100 else if(lg.echoChar !is dchar.init) 6101 ch = lg.echoChar; 6102 6103 auto l = encode(buffer, ch); 6104 // note the Terminal buffers it so meh 6105 lg.terminal.write(buffer[0 .. l]); 6106 6107 written++; 6108 lineLength--; 6109 6110 if(lg.multiLineMode) { 6111 if(ch == '\n') { 6112 lineLength = lg.terminal.width; 6113 linesRemaining--; 6114 } 6115 } 6116 } 6117 6118 void drawContent(T)(T towrite, int highlightBegin = 0, int highlightEnd = 0, bool inverted = false, int lineidx = -1) { 6119 // FIXME: if there is a color at the end of the line it messes up as you scroll 6120 // FIXME: need a way to go to multi-line editing 6121 6122 bool highlightOn = false; 6123 void highlightOff() { 6124 lg.terminal.color(currentFg, currentBg, ForceOption.automatic, inverted); 6125 highlightOn = false; 6126 } 6127 6128 foreach(idx, dchar ch; towrite) { 6129 if(linesRemaining <= 0) 6130 break; 6131 if(lineLength <= 0) { 6132 if(lg.multiLineMode) { 6133 if(ch == '\n') { 6134 lineLength = lg.terminal.width; 6135 } 6136 continue; 6137 } else 6138 break; 6139 } 6140 6141 static if(is(T == dchar[])) { 6142 if(lineidx != -1 && colorChars == 0) { 6143 auto shm = lg.syntaxHighlightMatch(lg.line, lineidx + idx, lg.cursorPosition); 6144 if(shm.charsMatched > 0) { 6145 colorChars = shm.charsMatched; 6146 currentFg_ = shm.foreground; 6147 currentBg_ = shm.background; 6148 lg.terminal.color(currentFg, currentBg); 6149 } 6150 } 6151 } 6152 6153 switch(ch) { 6154 case '\n': lg.multiLineMode ? regularChar('\n') : specialChar('n'); break; 6155 case '\r': specialChar('r'); break; 6156 case '\a': specialChar('a'); break; 6157 case '\t': specialChar('t'); break; 6158 case '\b': specialChar('b'); break; 6159 case '\033': specialChar('e'); break; 6160 case '\ ': specialChar(' '); break; 6161 default: 6162 if(highlightEnd) { 6163 if(idx == highlightBegin) { 6164 lg.terminal.color(lg.regularForeground, Color.yellow, ForceOption.automatic, inverted); 6165 highlightOn = true; 6166 } 6167 if(idx == highlightEnd) { 6168 highlightOff(); 6169 } 6170 } 6171 6172 regularChar(ch & ~PRIVATE_BITS_MASK); 6173 } 6174 6175 if(colorChars > 0) { 6176 colorChars--; 6177 if(colorChars == 0) 6178 lg.terminal.color(currentFg, currentBg); 6179 } 6180 } 6181 if(highlightOn) 6182 highlightOff(); 6183 } 6184 6185 } 6186 6187 /++ 6188 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. 6189 6190 History: 6191 Added May 24, 2021 6192 +/ 6193 final public @property int maximumDrawWidth() { 6194 auto tw = terminal.width - startOfLineX; 6195 if(_drawWidthMax && _drawWidthMax <= tw) 6196 return _drawWidthMax; 6197 return tw; 6198 } 6199 6200 /++ 6201 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. 6202 6203 History: 6204 Added May 24, 2021 6205 +/ 6206 final public @property void maximumDrawWidth(int newMax) { 6207 _drawWidthMax = newMax; 6208 } 6209 6210 /++ 6211 Returns the maximum vertical space available to draw. 6212 6213 Currently, this is always 1. 6214 6215 History: 6216 Added May 24, 2021 6217 +/ 6218 @property int maximumDrawHeight() { 6219 return 1; 6220 } 6221 6222 private int _drawWidthMax = 0; 6223 6224 private int lastDrawLength = 0; 6225 void redraw() { 6226 finalizeRedraw(coreRedraw()); 6227 } 6228 6229 void finalizeRedraw(CoreRedrawInfo cdi) { 6230 if(!cdi.populated) 6231 return; 6232 6233 if(!multiLineMode) { 6234 terminal.clearToEndOfLine(); 6235 /* 6236 if(UseVtSequences && !_drawWidthMax) { 6237 terminal.writeStringRaw("\033[K"); 6238 } else { 6239 // FIXME: graphemes 6240 if(cdi.written + promptLength < lastDrawLength) 6241 foreach(i; cdi.written + promptLength .. lastDrawLength) 6242 terminal.write(" "); 6243 lastDrawLength = cdi.written; 6244 } 6245 */ 6246 // if echoChar is null then we don't want to reflect the position at all 6247 terminal.moveTo(startOfLineX + ((echoChar == 0) ? 0 : cdi.cursorPositionToDrawX) + promptLength, startOfLineY + cdi.cursorPositionToDrawY); 6248 } else { 6249 if(echoChar != 0) 6250 terminal.moveTo(cdi.cursorPositionToDrawX, cdi.cursorPositionToDrawY); 6251 } 6252 endRedraw(); // make sure the cursor is turned back on 6253 } 6254 6255 static struct CoreRedrawInfo { 6256 bool populated; 6257 int written; 6258 int cursorPositionToDrawX; 6259 int cursorPositionToDrawY; 6260 } 6261 6262 private void endRedraw() { 6263 version(Win32Console) { 6264 // on Windows, we want to make sure all 6265 // is displayed before the cursor jumps around 6266 terminal.flush(); 6267 terminal.showCursor(); 6268 } else { 6269 // but elsewhere, the showCursor is itself buffered, 6270 // so we can do it all at once for a slight speed boost 6271 terminal.showCursor(); 6272 //import std.string; import std.stdio; writeln(terminal.writeBuffer.replace("\033", "\\e")); 6273 terminal.flush(); 6274 } 6275 } 6276 6277 final CoreRedrawInfo coreRedraw() { 6278 if(supplementalGetter) 6279 return CoreRedrawInfo.init; // the supplementalGetter will be drawing instead... 6280 terminal.hideCursor(); 6281 scope(failure) { 6282 // don't want to leave the cursor hidden on the event of an exception 6283 // can't just scope(success) it here since the cursor will be seen bouncing when finalizeRedraw is run 6284 endRedraw(); 6285 } 6286 terminal.moveTo(startOfLineX, startOfLineY); 6287 6288 if(multiLineMode) 6289 terminal.clear(); 6290 6291 Drawer drawer = Drawer(this); 6292 6293 drawer.lineLength = availableLineLength(); 6294 if(drawer.lineLength < 0) 6295 throw new Exception("too narrow terminal to draw"); 6296 6297 if(!multiLineMode) { 6298 terminal.color(promptColor, background); 6299 terminal.write(prompt); 6300 terminal.color(regularForeground, background); 6301 } 6302 6303 dchar[] towrite; 6304 6305 if(multiLineMode) { 6306 towrite = line[]; 6307 if(verticalScrollPosition) { 6308 int remaining = verticalScrollPosition; 6309 while(towrite.length) { 6310 if(towrite[0] == '\n') { 6311 towrite = towrite[1 .. $]; 6312 remaining--; 6313 if(remaining == 0) 6314 break; 6315 continue; 6316 } 6317 towrite = towrite[1 .. $]; 6318 } 6319 } 6320 horizontalScrollPosition = 0; // FIXME 6321 } else { 6322 towrite = line[horizontalScrollPosition .. $]; 6323 } 6324 auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; 6325 auto cursorPositionToDrawY = 0; 6326 6327 if(selectionStart != selectionEnd) { 6328 dchar[] beforeSelection, selection, afterSelection; 6329 6330 beforeSelection = line[0 .. selectionStart]; 6331 selection = line[selectionStart .. selectionEnd]; 6332 afterSelection = line[selectionEnd .. $]; 6333 6334 drawer.drawContent(beforeSelection); 6335 terminal.color(regularForeground, background, ForceOption.automatic, true); 6336 drawer.drawContent(selection, 0, 0, true); 6337 terminal.color(regularForeground, background); 6338 drawer.drawContent(afterSelection); 6339 } else { 6340 drawer.drawContent(towrite, 0, 0, false, horizontalScrollPosition); 6341 } 6342 6343 string suggestion; 6344 6345 if(drawer.lineLength >= 0) { 6346 suggestion = ((cursorPosition == towrite.length) && autoSuggest) ? this.suggestion() : null; 6347 if(suggestion.length) { 6348 terminal.color(suggestionForeground, background); 6349 foreach(dchar ch; suggestion) { 6350 if(drawer.lineLength == 0) 6351 break; 6352 drawer.regularChar(ch); 6353 } 6354 terminal.color(regularForeground, background); 6355 } 6356 } 6357 6358 CoreRedrawInfo cri; 6359 cri.populated = true; 6360 cri.written = drawer.written; 6361 if(multiLineMode) { 6362 cursorPositionToDrawX = 0; 6363 cursorPositionToDrawY = 0; 6364 // would be better if it did this in the same drawing pass... 6365 foreach(idx, dchar ch; line) { 6366 if(idx == cursorPosition) 6367 break; 6368 if(ch == '\n') { 6369 cursorPositionToDrawX = 0; 6370 cursorPositionToDrawY++; 6371 } else { 6372 cursorPositionToDrawX++; 6373 } 6374 } 6375 6376 cri.cursorPositionToDrawX = cursorPositionToDrawX - horizontalScrollPosition; 6377 cri.cursorPositionToDrawY = cursorPositionToDrawY - verticalScrollPosition; 6378 } else { 6379 cri.cursorPositionToDrawX = cursorPositionToDrawX; 6380 cri.cursorPositionToDrawY = cursorPositionToDrawY; 6381 } 6382 6383 return cri; 6384 } 6385 6386 /// Starts getting a new line. Call workOnLine and finishGettingLine afterward. 6387 /// 6388 /// Make sure that you've flushed your input and output before calling this 6389 /// function or else you might lose events or get exceptions from this. 6390 void startGettingLine() { 6391 // reset from any previous call first 6392 if(!maintainBuffer) { 6393 cursorPosition = 0; 6394 horizontalScrollPosition = 0; 6395 verticalScrollPosition = 0; 6396 justHitTab = false; 6397 currentHistoryViewPosition = 0; 6398 if(line.length) { 6399 line = line[0 .. 0]; 6400 line.assumeSafeAppend(); 6401 } 6402 } 6403 6404 maintainBuffer = false; 6405 6406 initializeWithSize(true); 6407 6408 terminal.cursor = TerminalCursor.insert; 6409 terminal.showCursor(); 6410 } 6411 6412 private void positionCursor() { 6413 if(cursorPosition == 0) { 6414 horizontalScrollPosition = 0; 6415 verticalScrollPosition = 0; 6416 } else if(cursorPosition == line.length) { 6417 scrollToEnd(); 6418 } else { 6419 if(multiLineMode) { 6420 // FIXME 6421 maybePositionCursor(); 6422 } else { 6423 // otherwise just try to center it in the screen 6424 horizontalScrollPosition = cursorPosition; 6425 horizontalScrollPosition -= maximumDrawWidth / 2; 6426 // align on a code point boundary 6427 aligned(horizontalScrollPosition, -1); 6428 if(horizontalScrollPosition < 0) 6429 horizontalScrollPosition = 0; 6430 } 6431 } 6432 } 6433 6434 private void aligned(ref int what, int direction) { 6435 // whereas line is right now dchar[] no need for this 6436 // at least until we go by grapheme... 6437 /* 6438 while(what > 0 && what < line.length && ((line[what] & 0b1100_0000) == 0b1000_0000)) 6439 what += direction; 6440 */ 6441 } 6442 6443 protected void initializeWithSize(bool firstEver = false) { 6444 auto x = startOfLineX; 6445 6446 updateCursorPosition(); 6447 6448 if(!firstEver) { 6449 startOfLineX = x; 6450 positionCursor(); 6451 } 6452 6453 lastDrawLength = maximumDrawWidth; 6454 version(Win32Console) 6455 lastDrawLength -= 1; // I don't like this but Windows resizing is different anyway and it is liable to scroll if i go over.. 6456 6457 redraw(); 6458 } 6459 6460 protected void updateCursorPosition() { 6461 terminal.updateCursorPosition(); 6462 6463 startOfLineX = terminal.cursorX; 6464 startOfLineY = terminal.cursorY; 6465 } 6466 6467 // Text killed with C-w/C-u/C-k/C-backspace, to be restored by C-y 6468 private dchar[] killBuffer; 6469 6470 // Given 'a b c d|', C-w C-w C-y should kill c and d, and then restore both 6471 // But given 'a b c d|', C-w M-b C-w C-y should kill d, kill b, and then restore only b 6472 // So we need this extra bit of state to decide whether to append to or replace the kill buffer 6473 // when the user kills some text 6474 private bool justKilled; 6475 6476 private bool justHitTab; 6477 private bool eof; 6478 6479 /// 6480 string delegate(string s) pastePreprocessor; 6481 6482 string defaultPastePreprocessor(string s) { 6483 return s; 6484 } 6485 6486 void showIndividualHelp(string help) { 6487 terminal.writeln(); 6488 terminal.writeln(help); 6489 } 6490 6491 private bool maintainBuffer; 6492 6493 /++ 6494 Returns true if the last line was retained by the user via the F9 or ctrl+enter key 6495 which runs it but keeps it in the edit buffer. 6496 6497 This is only valid inside [finishGettingLine] or immediately after [finishGettingLine] 6498 returns, but before [startGettingLine] is called again. 6499 6500 History: 6501 Added October 12, 2021 6502 +/ 6503 final public bool lastLineWasRetained() const { 6504 return maintainBuffer; 6505 } 6506 6507 private LineGetter supplementalGetter; 6508 6509 /* selection helpers */ 6510 protected { 6511 // make sure you set the anchor first 6512 void extendSelectionToCursor() { 6513 if(cursorPosition < selectionStart) 6514 selectionStart = cursorPosition; 6515 else if(cursorPosition > selectionEnd) 6516 selectionEnd = cursorPosition; 6517 6518 terminal.requestSetTerminalSelection(getSelection()); 6519 } 6520 void setSelectionAnchorToCursor() { 6521 if(selectionStart == -1) 6522 selectionStart = selectionEnd = cursorPosition; 6523 } 6524 void sanitizeSelection() { 6525 if(selectionStart == selectionEnd) 6526 return; 6527 6528 if(selectionStart < 0 || selectionEnd < 0 || selectionStart > line.length || selectionEnd > line.length) 6529 selectNone(); 6530 } 6531 } 6532 public { 6533 // redraw after calling this 6534 void selectAll() { 6535 selectionStart = 0; 6536 selectionEnd = cast(int) line.length; 6537 } 6538 6539 // redraw after calling this 6540 void selectNone() { 6541 selectionStart = selectionEnd = -1; 6542 } 6543 6544 string getSelection() { 6545 sanitizeSelection(); 6546 if(selectionStart == selectionEnd) 6547 return null; 6548 import std.conv; 6549 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6550 return to!string(line[selectionStart .. selectionEnd]); 6551 } 6552 } 6553 private { 6554 int selectionStart = -1; 6555 int selectionEnd = -1; 6556 } 6557 6558 void backwardToNewline() { 6559 while(cursorPosition && line[cursorPosition - 1] != '\n') 6560 cursorPosition--; 6561 phantomCursorX = 0; 6562 } 6563 6564 void forwardToNewLine() { 6565 while(cursorPosition < line.length && line[cursorPosition] != '\n') 6566 cursorPosition++; 6567 } 6568 6569 private int phantomCursorX; 6570 6571 void lineBackward() { 6572 int count; 6573 while(cursorPosition && line[cursorPosition - 1] != '\n') { 6574 cursorPosition--; 6575 count++; 6576 } 6577 if(count > phantomCursorX) 6578 phantomCursorX = count; 6579 6580 if(cursorPosition == 0) 6581 return; 6582 cursorPosition--; 6583 6584 while(cursorPosition && line[cursorPosition - 1] != '\n') { 6585 cursorPosition--; 6586 } 6587 6588 count = phantomCursorX; 6589 while(count) { 6590 if(cursorPosition == line.length) 6591 break; 6592 if(line[cursorPosition] == '\n') 6593 break; 6594 cursorPosition++; 6595 count--; 6596 } 6597 } 6598 6599 void lineForward() { 6600 int count; 6601 6602 // see where we are in the current line 6603 auto beginPos = cursorPosition; 6604 while(beginPos && line[beginPos - 1] != '\n') { 6605 beginPos--; 6606 count++; 6607 } 6608 6609 if(count > phantomCursorX) 6610 phantomCursorX = count; 6611 6612 // get to the next line 6613 while(cursorPosition < line.length && line[cursorPosition] != '\n') { 6614 cursorPosition++; 6615 } 6616 if(cursorPosition == line.length) 6617 return; 6618 cursorPosition++; 6619 6620 // get to the same spot in this same line 6621 count = phantomCursorX; 6622 while(count) { 6623 if(cursorPosition == line.length) 6624 break; 6625 if(line[cursorPosition] == '\n') 6626 break; 6627 cursorPosition++; 6628 count--; 6629 } 6630 } 6631 6632 void pageBackward() { 6633 foreach(count; 0 .. terminal.height) 6634 lineBackward(); 6635 maybePositionCursor(); 6636 } 6637 6638 void pageForward() { 6639 foreach(count; 0 .. terminal.height) 6640 lineForward(); 6641 maybePositionCursor(); 6642 } 6643 6644 bool isSearchingHistory() { 6645 return supplementalGetter !is null; 6646 } 6647 6648 /++ 6649 Cancels an in-progress history search immediately, discarding the result, returning 6650 to the normal prompt. 6651 6652 If the user is not currently searching history (see [isSearchingHistory]), this 6653 function does nothing. 6654 +/ 6655 void cancelHistorySearch() { 6656 if(isSearchingHistory()) { 6657 lastDrawLength = maximumDrawWidth - 1; 6658 supplementalGetter = null; 6659 redraw(); 6660 } 6661 } 6662 6663 /++ 6664 for integrating into another event loop 6665 you can pass individual events to this and 6666 the line getter will work on it 6667 6668 returns false when there's nothing more to do 6669 6670 History: 6671 On February 17, 2020, it was changed to take 6672 a new argument which should be the input source 6673 where the event came from. 6674 +/ 6675 bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) { 6676 if(supplementalGetter) { 6677 if(!supplementalGetter.workOnLine(e, rtti)) { 6678 auto got = supplementalGetter.finishGettingLine(); 6679 // the supplementalGetter will poke our own state directly 6680 // so i can ignore the return value here... 6681 6682 // but i do need to ensure we clear any 6683 // stuff left on the screen from it. 6684 lastDrawLength = maximumDrawWidth - 1; 6685 supplementalGetter = null; 6686 redraw(); 6687 } 6688 return true; 6689 } 6690 6691 switch(e.type) { 6692 case InputEvent.Type.EndOfFileEvent: 6693 justHitTab = false; 6694 eof = true; 6695 // FIXME: this should be distinct from an empty line when hit at the beginning 6696 return false; 6697 //break; 6698 case InputEvent.Type.KeyboardEvent: 6699 auto ev = e.keyboardEvent; 6700 if(ev.pressed == false) 6701 return true; 6702 /* Insert the character (unless it is backspace, tab, or some other control char) */ 6703 auto ch = ev.which; 6704 switch(ch) { 6705 case KeyboardEvent.ProprietaryPseudoKeys.SelectNone: 6706 selectNone(); 6707 redraw(); 6708 break; 6709 version(Windows) case 'z', 26: { // and this is really for Windows 6710 if(!(ev.modifierState & ModifierState.control)) 6711 goto default; 6712 goto case; 6713 } 6714 case 'd', 4: // ctrl+d will also send a newline-equivalent 6715 if(ev.modifierState & ModifierState.alt) { 6716 // gnu alias for kill word (also on ctrl+backspace) 6717 justHitTab = false; 6718 lineChanged = true; 6719 killWordForward(); 6720 justKilled = true; 6721 redraw(); 6722 break; 6723 } 6724 if(!(ev.modifierState & ModifierState.control)) 6725 goto default; 6726 if(line.length == 0) 6727 eof = true; 6728 justHitTab = justKilled = false; 6729 return false; // indicate end of line so it doesn't maintain the buffer thinking it was ctrl+enter 6730 case '\r': 6731 case '\n': 6732 justHitTab = justKilled = false; 6733 if(ev.modifierState & ModifierState.control) { 6734 goto case KeyboardEvent.Key.F9; 6735 } 6736 if(ev.modifierState & ModifierState.shift) { 6737 addChar('\n'); 6738 redraw(); 6739 break; 6740 } 6741 return false; 6742 case '\t': 6743 justKilled = false; 6744 6745 if(ev.modifierState & ModifierState.shift) { 6746 justHitTab = false; 6747 addChar('\t'); 6748 redraw(); 6749 break; 6750 } 6751 6752 // I want to hide the private bits from the other functions, but retain them across completions, 6753 // which is why it does it on a copy here. Could probably be more efficient, but meh. 6754 auto line = this.line.dup; 6755 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6756 6757 auto relevantLineSection = line[0 .. cursorPosition]; 6758 auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); 6759 relevantLineSection = relevantLineSection[start .. $]; 6760 auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); 6761 import std.utf; 6762 6763 if(possibilities.length == 1) { 6764 auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $]; 6765 if(toFill.length) { 6766 addString(toFill); 6767 redraw(); 6768 } else { 6769 auto help = this.tabCompleteHelp(possibilities[0]); 6770 if(help.length) { 6771 showIndividualHelp(help); 6772 updateCursorPosition(); 6773 redraw(); 6774 } 6775 } 6776 justHitTab = false; 6777 } else { 6778 if(justHitTab) { 6779 justHitTab = false; 6780 showTabCompleteList(possibilities); 6781 } else { 6782 justHitTab = true; 6783 /* fill it in with as much commonality as there is amongst all the suggestions */ 6784 auto suggestion = this.suggestion(possibilities); 6785 if(suggestion.length) { 6786 addString(suggestion); 6787 redraw(); 6788 } 6789 } 6790 } 6791 break; 6792 case '\b': 6793 justHitTab = false; 6794 // i use control for delete word, but gnu uses alt. so this allows both 6795 if(ev.modifierState & (ModifierState.control | ModifierState.alt)) { 6796 lineChanged = true; 6797 killWord(); 6798 justKilled = true; 6799 redraw(); 6800 } else if(cursorPosition) { 6801 lineChanged = true; 6802 justKilled = false; 6803 cursorPosition--; 6804 for(int i = cursorPosition; i < line.length - 1; i++) 6805 line[i] = line[i + 1]; 6806 line = line[0 .. $ - 1]; 6807 line.assumeSafeAppend(); 6808 6809 if(multiLineMode) { 6810 // FIXME 6811 } else { 6812 if(horizontalScrollPosition > cursorPosition - 1) 6813 horizontalScrollPosition = cursorPosition - 1 - availableLineLength(); 6814 if(horizontalScrollPosition < 0) 6815 horizontalScrollPosition = 0; 6816 } 6817 6818 redraw(); 6819 } 6820 phantomCursorX = 0; 6821 break; 6822 case KeyboardEvent.Key.escape: 6823 justHitTab = justKilled = false; 6824 if(multiLineMode) 6825 multiLineMode = false; 6826 else { 6827 cursorPosition = 0; 6828 horizontalScrollPosition = 0; 6829 line = line[0 .. 0]; 6830 line.assumeSafeAppend(); 6831 } 6832 redraw(); 6833 break; 6834 case KeyboardEvent.Key.F1: 6835 justHitTab = justKilled = false; 6836 showHelp(); 6837 break; 6838 case KeyboardEvent.Key.F2: 6839 justHitTab = justKilled = false; 6840 6841 if(ev.modifierState & ModifierState.control) { 6842 toggleMultiLineMode(); 6843 break; 6844 } 6845 6846 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6847 auto got = editLineInEditor(line, cursorPosition); 6848 if(got !is null) { 6849 line = got; 6850 if(cursorPosition > line.length) 6851 cursorPosition = cast(int) line.length; 6852 if(horizontalScrollPosition > line.length) 6853 horizontalScrollPosition = cast(int) line.length; 6854 positionCursor(); 6855 redraw(); 6856 } 6857 break; 6858 case '(': 6859 if(!(ev.modifierState & ModifierState.alt)) 6860 goto default; 6861 justHitTab = justKilled = false; 6862 addChar('('); 6863 addChar(cast(dchar) (')' | PRIVATE_BITS_MASK)); 6864 charBack(); 6865 redraw(); 6866 break; 6867 case 'l', 12: 6868 if(!(ev.modifierState & ModifierState.control)) 6869 goto default; 6870 goto case; 6871 case KeyboardEvent.Key.F5: 6872 // FIXME: I might not want to do this on full screen programs, 6873 // but arguably the application should just hook the event then. 6874 terminal.clear(); 6875 updateCursorPosition(); 6876 redraw(); 6877 break; 6878 case 'r', 18: 6879 if(!(ev.modifierState & ModifierState.control)) 6880 goto default; 6881 goto case; 6882 case KeyboardEvent.Key.F3: 6883 justHitTab = justKilled = false; 6884 // search in history 6885 // FIXME: what about search in completion too? 6886 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 6887 supplementalGetter = new HistorySearchLineGetter(this); 6888 supplementalGetter.startGettingLine(); 6889 supplementalGetter.redraw(); 6890 break; 6891 case 'u', 21: 6892 if(!(ev.modifierState & ModifierState.control)) 6893 goto default; 6894 goto case; 6895 case KeyboardEvent.Key.F4: 6896 killText(line); 6897 line = []; 6898 cursorPosition = 0; 6899 justHitTab = false; 6900 justKilled = true; 6901 redraw(); 6902 break; 6903 // btw alt+enter could be alias for F9? 6904 case KeyboardEvent.Key.F9: 6905 justHitTab = justKilled = false; 6906 // compile and run analog; return the current string 6907 // but keep the buffer the same 6908 6909 maintainBuffer = true; 6910 return false; 6911 case '5', 0x1d: // ctrl+5, because of vim % shortcut 6912 if(!(ev.modifierState & ModifierState.control)) 6913 goto default; 6914 justHitTab = justKilled = false; 6915 // FIXME: would be cool if this worked with quotes and such too 6916 // FIXME: in insert mode prolly makes sense to look at the position before the cursor tbh 6917 if(cursorPosition >= 0 && cursorPosition < line.length) { 6918 dchar at = line[cursorPosition] & ~PRIVATE_BITS_MASK; 6919 int direction; 6920 dchar lookFor; 6921 switch(at) { 6922 case '(': direction = 1; lookFor = ')'; break; 6923 case '[': direction = 1; lookFor = ']'; break; 6924 case '{': direction = 1; lookFor = '}'; break; 6925 case ')': direction = -1; lookFor = '('; break; 6926 case ']': direction = -1; lookFor = '['; break; 6927 case '}': direction = -1; lookFor = '{'; break; 6928 default: 6929 } 6930 if(direction) { 6931 int pos = cursorPosition; 6932 int count; 6933 while(pos >= 0 && pos < line.length) { 6934 auto lp = line[pos] & ~PRIVATE_BITS_MASK; 6935 if(lp == at) 6936 count++; 6937 if(lp == lookFor) 6938 count--; 6939 if(count == 0) { 6940 cursorPosition = pos; 6941 redraw(); 6942 break; 6943 } 6944 pos += direction; 6945 } 6946 } 6947 } 6948 break; 6949 6950 // FIXME: should be able to update the selection with shift+arrows as well as mouse 6951 // if terminal emulator supports this, it can formally select it to the buffer for copy 6952 // and sending to primary on X11 (do NOT do it on Windows though!!!) 6953 case 'b', 2: 6954 if(ev.modifierState & ModifierState.alt) 6955 wordBack(); 6956 else if(ev.modifierState & ModifierState.control) 6957 charBack(); 6958 else 6959 goto default; 6960 justHitTab = justKilled = false; 6961 redraw(); 6962 break; 6963 case 'f', 6: 6964 if(ev.modifierState & ModifierState.alt) 6965 wordForward(); 6966 else if(ev.modifierState & ModifierState.control) 6967 charForward(); 6968 else 6969 goto default; 6970 justHitTab = justKilled = false; 6971 redraw(); 6972 break; 6973 case KeyboardEvent.Key.LeftArrow: 6974 justHitTab = justKilled = false; 6975 phantomCursorX = 0; 6976 6977 /* 6978 if(ev.modifierState & ModifierState.shift) 6979 setSelectionAnchorToCursor(); 6980 */ 6981 6982 if(ev.modifierState & ModifierState.control) 6983 wordBack(); 6984 else if(cursorPosition) 6985 charBack(); 6986 6987 /* 6988 if(ev.modifierState & ModifierState.shift) 6989 extendSelectionToCursor(); 6990 */ 6991 6992 redraw(); 6993 break; 6994 case KeyboardEvent.Key.RightArrow: 6995 justHitTab = justKilled = false; 6996 if(ev.modifierState & ModifierState.control) 6997 wordForward(); 6998 else 6999 charForward(); 7000 redraw(); 7001 break; 7002 case 'p', 16: 7003 if(ev.modifierState & ModifierState.control) 7004 goto case; 7005 goto default; 7006 case KeyboardEvent.Key.UpArrow: 7007 justHitTab = justKilled = false; 7008 if(multiLineMode) { 7009 lineBackward(); 7010 maybePositionCursor(); 7011 } else 7012 loadFromHistory(currentHistoryViewPosition + 1); 7013 redraw(); 7014 break; 7015 case 'n', 14: 7016 if(ev.modifierState & ModifierState.control) 7017 goto case; 7018 goto default; 7019 case KeyboardEvent.Key.DownArrow: 7020 justHitTab = justKilled = false; 7021 if(multiLineMode) { 7022 lineForward(); 7023 maybePositionCursor(); 7024 } else 7025 loadFromHistory(currentHistoryViewPosition - 1); 7026 redraw(); 7027 break; 7028 case KeyboardEvent.Key.PageUp: 7029 justHitTab = justKilled = false; 7030 if(multiLineMode) 7031 pageBackward(); 7032 else 7033 loadFromHistory(cast(int) history.length); 7034 redraw(); 7035 break; 7036 case KeyboardEvent.Key.PageDown: 7037 justHitTab = justKilled = false; 7038 if(multiLineMode) 7039 pageForward(); 7040 else 7041 loadFromHistory(0); 7042 redraw(); 7043 break; 7044 case 'a', 1: // this one conflicts with Windows-style select all... 7045 if(!(ev.modifierState & ModifierState.control)) 7046 goto default; 7047 if(ev.modifierState & ModifierState.shift) { 7048 // ctrl+shift+a will select all... 7049 // 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 7050 terminal.requestCopyToClipboard(lineAsString()); 7051 break; 7052 } 7053 goto case; 7054 case KeyboardEvent.Key.Home: 7055 justHitTab = justKilled = false; 7056 if(multiLineMode) { 7057 backwardToNewline(); 7058 } else { 7059 cursorPosition = 0; 7060 } 7061 horizontalScrollPosition = 0; 7062 redraw(); 7063 break; 7064 case 'e', 5: 7065 if(!(ev.modifierState & ModifierState.control)) 7066 goto default; 7067 goto case; 7068 case KeyboardEvent.Key.End: 7069 justHitTab = justKilled = false; 7070 if(multiLineMode) { 7071 forwardToNewLine(); 7072 } else { 7073 cursorPosition = cast(int) line.length; 7074 scrollToEnd(); 7075 } 7076 redraw(); 7077 break; 7078 case 'v', 22: 7079 if(!(ev.modifierState & ModifierState.control)) 7080 goto default; 7081 justKilled = false; 7082 if(rtti) 7083 rtti.requestPasteFromClipboard(); 7084 break; 7085 case KeyboardEvent.Key.Insert: 7086 justHitTab = justKilled = false; 7087 if(ev.modifierState & ModifierState.shift) { 7088 // paste 7089 7090 // shift+insert = request paste 7091 // ctrl+insert = request copy. but that needs a selection 7092 7093 // those work on Windows!!!! and many linux TEs too. 7094 // but if it does make it here, we'll attempt it at this level 7095 if(rtti) 7096 rtti.requestPasteFromClipboard(); 7097 } else if(ev.modifierState & ModifierState.control) { 7098 // copy 7099 // FIXME we could try requesting it though this control unlikely to even come 7100 } else { 7101 insertMode = !insertMode; 7102 7103 if(insertMode) 7104 terminal.cursor = TerminalCursor.insert; 7105 else 7106 terminal.cursor = TerminalCursor.block; 7107 } 7108 break; 7109 case KeyboardEvent.Key.Delete: 7110 justHitTab = false; 7111 if(ev.modifierState & ModifierState.control) { 7112 deleteToEndOfLine(); 7113 justKilled = true; 7114 } else { 7115 deleteChar(); 7116 justKilled = false; 7117 } 7118 redraw(); 7119 break; 7120 case 'k', 11: 7121 if(!(ev.modifierState & ModifierState.control)) 7122 goto default; 7123 deleteToEndOfLine(); 7124 justHitTab = false; 7125 justKilled = true; 7126 redraw(); 7127 break; 7128 case 'w', 23: 7129 if(!(ev.modifierState & ModifierState.control)) 7130 goto default; 7131 killWord(); 7132 justHitTab = false; 7133 justKilled = true; 7134 redraw(); 7135 break; 7136 case 'y', 25: 7137 if(!(ev.modifierState & ModifierState.control)) 7138 goto default; 7139 justHitTab = justKilled = false; 7140 foreach(c; killBuffer) 7141 addChar(c); 7142 redraw(); 7143 break; 7144 default: 7145 justHitTab = justKilled = false; 7146 if(e.keyboardEvent.isCharacter) { 7147 7148 // overstrike an auto-inserted thing if that's right there 7149 if(cursorPosition < line.length) 7150 if(line[cursorPosition] & PRIVATE_BITS_MASK) { 7151 if((line[cursorPosition] & ~PRIVATE_BITS_MASK) == ch) { 7152 line[cursorPosition] = ch; 7153 cursorPosition++; 7154 redraw(); 7155 break; 7156 } 7157 } 7158 7159 7160 7161 // the ordinary add, of course 7162 addChar(ch); 7163 7164 7165 // and auto-insert a closing pair if appropriate 7166 auto autoChars = enableAutoCloseBrackets(); 7167 bool found = false; 7168 foreach(idx, dchar ac; autoChars) { 7169 if(found) { 7170 addChar(ac | PRIVATE_BITS_MASK); 7171 charBack(); 7172 break; 7173 } 7174 if((idx&1) == 0 && ac == ch) 7175 found = true; 7176 } 7177 } 7178 redraw(); 7179 } 7180 break; 7181 case InputEvent.Type.PasteEvent: 7182 justHitTab = false; 7183 if(pastePreprocessor) 7184 addString(pastePreprocessor(e.pasteEvent.pastedText)); 7185 else 7186 addString(defaultPastePreprocessor(e.pasteEvent.pastedText)); 7187 redraw(); 7188 break; 7189 case InputEvent.Type.MouseEvent: 7190 /* Clicking with the mouse to move the cursor is so much easier than arrowing 7191 or even emacs/vi style movements much of the time, so I'ma support it. */ 7192 7193 auto me = e.mouseEvent; 7194 if(me.eventType == MouseEvent.Type.Pressed) { 7195 if(me.buttons & MouseEvent.Button.Left) { 7196 if(multiLineMode) { 7197 // FIXME 7198 } else if(me.y == startOfLineY) { // single line only processes on itself 7199 int p = me.x - startOfLineX - promptLength + horizontalScrollPosition; 7200 if(p >= 0 && p < line.length) { 7201 justHitTab = false; 7202 cursorPosition = p; 7203 redraw(); 7204 } 7205 } 7206 } 7207 if(me.buttons & MouseEvent.Button.Middle) { 7208 if(rtti) 7209 rtti.requestPasteFromPrimary(); 7210 } 7211 } 7212 break; 7213 case InputEvent.Type.LinkEvent: 7214 if(handleLinkEvent !is null) 7215 handleLinkEvent(e.linkEvent, this); 7216 break; 7217 case InputEvent.Type.SizeChangedEvent: 7218 /* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent 7219 yourself and then don't pass it to this function. */ 7220 // FIXME 7221 initializeWithSize(); 7222 break; 7223 case InputEvent.Type.CustomEvent: 7224 if(auto rce = cast(RunnableCustomEvent) e.customEvent) 7225 rce.run(); 7226 break; 7227 case InputEvent.Type.UserInterruptionEvent: 7228 /* I'll take this as canceling the line. */ 7229 throw new UserInterruptionException(); 7230 //break; 7231 case InputEvent.Type.HangupEvent: 7232 /* I'll take this as canceling the line. */ 7233 throw new HangupException(); 7234 //break; 7235 default: 7236 /* ignore. ideally it wouldn't be passed to us anyway! */ 7237 } 7238 7239 return true; 7240 } 7241 7242 /++ 7243 Gives a convenience hook for subclasses to handle my terminal's hyperlink extension. 7244 7245 7246 You can also handle these by filtering events before you pass them to [workOnLine]. 7247 That's still how I recommend handling any overrides or custom events, but making this 7248 a delegate is an easy way to inject handlers into an otherwise linear i/o application. 7249 7250 Does nothing if null. 7251 7252 It passes the event as well as the current line getter to the delegate. You may simply 7253 `lg.addString(ev.text); lg.redraw();` in some cases. 7254 7255 History: 7256 Added April 2, 2021. 7257 7258 See_Also: 7259 [Terminal.hyperlink] 7260 7261 [TerminalCapabilities.arsdHyperlinks] 7262 +/ 7263 void delegate(LinkEvent ev, LineGetter lg) handleLinkEvent; 7264 7265 /++ 7266 Replaces the line currently being edited with the given line and positions the cursor inside it. 7267 7268 History: 7269 Added November 27, 2020. 7270 +/ 7271 void replaceLine(const scope dchar[] line) { 7272 if(this.line.length < line.length) 7273 this.line.length = line.length; 7274 else 7275 this.line = this.line[0 .. line.length]; 7276 this.line.assumeSafeAppend(); 7277 this.line[] = line[]; 7278 if(cursorPosition > line.length) 7279 cursorPosition = cast(int) line.length; 7280 if(multiLineMode) { 7281 // FIXME? 7282 horizontalScrollPosition = 0; 7283 verticalScrollPosition = 0; 7284 } else { 7285 if(horizontalScrollPosition > line.length) 7286 horizontalScrollPosition = cast(int) line.length; 7287 } 7288 positionCursor(); 7289 } 7290 7291 /// ditto 7292 void replaceLine(const scope char[] line) { 7293 if(line.length >= 255) { 7294 import std.conv; 7295 replaceLine(to!dstring(line)); 7296 return; 7297 } 7298 dchar[255] tmp; 7299 size_t idx; 7300 foreach(dchar c; line) { 7301 tmp[idx++] = c; 7302 } 7303 7304 replaceLine(tmp[0 .. idx]); 7305 } 7306 7307 /++ 7308 Gets the current line buffer as a duplicated string. 7309 7310 History: 7311 Added January 25, 2021 7312 +/ 7313 string lineAsString() { 7314 import std.conv; 7315 7316 // FIXME: I should prolly not do this on the internal copy but it isn't a huge deal 7317 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 7318 7319 return to!string(line); 7320 } 7321 7322 /// 7323 string finishGettingLine() { 7324 import std.conv; 7325 7326 7327 if(multiLineMode) 7328 multiLineMode = false; 7329 7330 line[] &= cast(dchar) ~PRIVATE_BITS_MASK; 7331 7332 auto f = to!string(line); 7333 auto history = historyFilter(f); 7334 if(history !is null) { 7335 this.history ~= history; 7336 if(this.historyCommitMode == HistoryCommitMode.afterEachLine) 7337 appendHistoryToFile(history); 7338 } 7339 7340 // FIXME: we should hide the cursor if it was hidden in the call to startGettingLine 7341 7342 // also need to reset the color going forward 7343 terminal.color(Color.DEFAULT, Color.DEFAULT); 7344 7345 return eof ? null : f.length ? f : ""; 7346 } 7347 } 7348 7349 class HistorySearchLineGetter : LineGetter { 7350 LineGetter basedOn; 7351 string sideDisplay; 7352 this(LineGetter basedOn) { 7353 this.basedOn = basedOn; 7354 super(basedOn.terminal); 7355 } 7356 7357 override void updateCursorPosition() { 7358 super.updateCursorPosition(); 7359 startOfLineX = basedOn.startOfLineX; 7360 startOfLineY = basedOn.startOfLineY; 7361 } 7362 7363 override void initializeWithSize(bool firstEver = false) { 7364 if(maximumDrawWidth > 60) 7365 this.prompt = "(history search): \""; 7366 else 7367 this.prompt = "(hs): \""; 7368 super.initializeWithSize(firstEver); 7369 } 7370 7371 override int availableLineLength() { 7372 return maximumDrawWidth / 2 - promptLength - 1; 7373 } 7374 7375 override void loadFromHistory(int howFarBack) { 7376 currentHistoryViewPosition = howFarBack; 7377 reloadSideDisplay(); 7378 } 7379 7380 int highlightBegin; 7381 int highlightEnd; 7382 7383 void reloadSideDisplay() { 7384 import std.string; 7385 import std.range; 7386 int counter = currentHistoryViewPosition; 7387 7388 string lastHit; 7389 int hb, he; 7390 if(line.length) 7391 foreach_reverse(item; basedOn.history) { 7392 auto idx = item.indexOf(line); 7393 if(idx != -1) { 7394 hb = cast(int) idx; 7395 he = cast(int) (idx + line.walkLength); 7396 lastHit = item; 7397 if(counter) 7398 counter--; 7399 else 7400 break; 7401 } 7402 } 7403 sideDisplay = lastHit; 7404 highlightBegin = hb; 7405 highlightEnd = he; 7406 redraw(); 7407 } 7408 7409 7410 bool redrawQueued = false; 7411 override void redraw() { 7412 redrawQueued = true; 7413 } 7414 7415 void actualRedraw() { 7416 auto cri = coreRedraw(); 7417 terminal.write("\" "); 7418 7419 int available = maximumDrawWidth / 2 - 1; 7420 auto used = prompt.length + cri.written + 3 /* the write above plus a space */; 7421 if(used < available) 7422 available += available - used; 7423 7424 //terminal.moveTo(maximumDrawWidth / 2, startOfLineY); 7425 Drawer drawer = Drawer(this); 7426 drawer.lineLength = available; 7427 drawer.drawContent(sideDisplay, highlightBegin, highlightEnd); 7428 7429 cri.written += drawer.written; 7430 7431 finalizeRedraw(cri); 7432 } 7433 7434 override bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) { 7435 scope(exit) { 7436 if(redrawQueued) { 7437 actualRedraw(); 7438 redrawQueued = false; 7439 } 7440 } 7441 if(e.type == InputEvent.Type.KeyboardEvent) { 7442 auto ev = e.keyboardEvent; 7443 if(ev.pressed == false) 7444 return true; 7445 /* Insert the character (unless it is backspace, tab, or some other control char) */ 7446 auto ch = ev.which; 7447 switch(ch) { 7448 // modification being the search through history commands 7449 // should just keep searching, not endlessly nest. 7450 case 'r', 18: 7451 if(!(ev.modifierState & ModifierState.control)) 7452 goto default; 7453 goto case; 7454 case KeyboardEvent.Key.F3: 7455 e.keyboardEvent.which = KeyboardEvent.Key.UpArrow; 7456 break; 7457 case KeyboardEvent.Key.escape: 7458 sideDisplay = null; 7459 return false; // cancel 7460 default: 7461 } 7462 } 7463 if(super.workOnLine(e, rtti)) { 7464 if(lineChanged) { 7465 currentHistoryViewPosition = 0; 7466 reloadSideDisplay(); 7467 lineChanged = false; 7468 } 7469 return true; 7470 } 7471 return false; 7472 } 7473 7474 override void startGettingLine() { 7475 super.startGettingLine(); 7476 this.line = basedOn.line.dup; 7477 cursorPosition = cast(int) this.line.length; 7478 startOfLineX = basedOn.startOfLineX; 7479 startOfLineY = basedOn.startOfLineY; 7480 positionCursor(); 7481 reloadSideDisplay(); 7482 } 7483 7484 override string finishGettingLine() { 7485 auto got = super.finishGettingLine(); 7486 7487 if(sideDisplay.length) 7488 basedOn.replaceLine(sideDisplay); 7489 7490 return got; 7491 } 7492 } 7493 7494 /// Adds default constructors that just forward to the superclass 7495 mixin template LineGetterConstructors() { 7496 this(Terminal* tty, string historyFilename = null) { 7497 super(tty, historyFilename); 7498 } 7499 } 7500 7501 /// This is a line getter that customizes the tab completion to 7502 /// fill in file names separated by spaces, like a command line thing. 7503 class FileLineGetter : LineGetter { 7504 mixin LineGetterConstructors; 7505 7506 /// You can set this property to tell it where to search for the files 7507 /// to complete. 7508 string searchDirectory = "."; 7509 7510 override size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { 7511 import std.string; 7512 return candidate.lastIndexOf(" ") + 1; 7513 } 7514 7515 override protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { 7516 import std.file, std.conv, std.algorithm, std.string; 7517 7518 string[] list; 7519 foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) { 7520 // both with and without the (searchDirectory ~ "/") 7521 list ~= name[searchDirectory.length + 1 .. $]; 7522 list ~= name[0 .. $]; 7523 } 7524 7525 return list; 7526 } 7527 } 7528 7529 /+ 7530 class FullscreenEditor { 7531 7532 } 7533 +/ 7534 7535 7536 version(Windows) { 7537 // to get the directory for saving history in the line things 7538 enum CSIDL_APPDATA = 26; 7539 extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); 7540 } 7541 7542 7543 7544 7545 7546 /* Like getting a line, printing a lot of lines is kinda important too, so I'm including 7547 that widget here too. */ 7548 7549 7550 /++ 7551 The ScrollbackBuffer is a writable in-memory terminal that can be drawn to a real [Terminal] 7552 and maintain some internal position state by handling events. It is your responsibility to 7553 draw it (using the [drawInto] method) and dispatch events to its [handleEvent] method (if you 7554 want to, you can also just call the methods yourself). 7555 7556 7557 I originally wrote this to support my irc client and some of the features are geared toward 7558 helping with that (for example, [name] and [demandsAttention]), but the main thrust is to 7559 support either tabs or sub-sections of the terminal having their own output that can be displayed 7560 and scrolled back independently while integrating with some larger application. 7561 7562 History: 7563 Committed to git on August 4, 2015. 7564 7565 Cleaned up and documented on May 25, 2021. 7566 +/ 7567 struct ScrollbackBuffer { 7568 /++ 7569 A string you can set and process on your own. The library only sets it from the 7570 constructor, then leaves it alone. 7571 7572 In my irc client, I use this as the title of a tab I draw to indicate separate 7573 conversations. 7574 +/ 7575 public string name; 7576 /++ 7577 A flag you can set and process on your own. All the library does with it is 7578 set it to false when it handles an event, otherwise you can do whatever you 7579 want with it. 7580 7581 In my irc client, I use this to add a * to the tab to indicate new messages. 7582 +/ 7583 public bool demandsAttention; 7584 7585 /++ 7586 The coordinates of the last [drawInto] 7587 +/ 7588 int x, y, width, height; 7589 7590 private CircularBuffer!Line lines; 7591 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 7592 7593 /++ 7594 Property to control the current scrollback position. 0 = latest message 7595 at bottom of screen. 7596 7597 See_Also: [scrollToBottom], [scrollToTop], [scrollUp], [scrollDown], [scrollTopPosition] 7598 +/ 7599 @property int scrollbackPosition() const pure @nogc nothrow @safe { 7600 return scrollbackPosition_; 7601 } 7602 7603 /// ditto 7604 private @property void scrollbackPosition(int p) pure @nogc nothrow @safe { 7605 scrollbackPosition_ = p; 7606 } 7607 7608 private int scrollbackPosition_; 7609 7610 /++ 7611 This is the color it uses to clear the screen. 7612 7613 History: 7614 Added May 26, 2021 7615 +/ 7616 public Color defaultForeground = Color.DEFAULT; 7617 /// ditto 7618 public Color defaultBackground = Color.DEFAULT; 7619 7620 private int foreground_ = Color.DEFAULT, background_ = Color.DEFAULT; 7621 7622 /++ 7623 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. 7624 +/ 7625 this(string name) { 7626 this.name = name; 7627 } 7628 7629 /++ 7630 Writing into the scrollback buffer can be done with the same normal functions. 7631 7632 Note that you will have to call [redraw] yourself to make this actually appear on screen. 7633 +/ 7634 void write(T...)(T t) { 7635 import std.conv : text; 7636 addComponent(text(t), foreground_, background_, null); 7637 } 7638 7639 /// ditto 7640 void writeln(T...)(T t) { 7641 write(t, "\n"); 7642 } 7643 7644 /// ditto 7645 void writef(T...)(string fmt, T t) { 7646 import std.format: format; 7647 write(format(fmt, t)); 7648 } 7649 7650 /// ditto 7651 void writefln(T...)(string fmt, T t) { 7652 writef(fmt, t, "\n"); 7653 } 7654 7655 /// ditto 7656 void color(int foreground, int background) { 7657 this.foreground_ = foreground; 7658 this.background_ = background; 7659 } 7660 7661 /++ 7662 Clears the scrollback buffer. 7663 +/ 7664 void clear() { 7665 lines.clear(); 7666 clickRegions = null; 7667 scrollbackPosition_ = 0; 7668 } 7669 7670 /++ 7671 7672 +/ 7673 void addComponent(string text, int foreground, int background, bool delegate() onclick) { 7674 addComponent(LineComponent(text, foreground, background, onclick)); 7675 } 7676 7677 /++ 7678 7679 +/ 7680 void addComponent(LineComponent component) { 7681 if(lines.length == 0 || eol) { 7682 addLine(); 7683 eol = false; 7684 } 7685 bool first = true; 7686 import std.algorithm; 7687 7688 if(component.text.length && component.text[$-1] == '\n') { 7689 eol = true; 7690 component.text = component.text[0 .. $ - 1]; 7691 } 7692 7693 foreach(t; splitter(component.text, "\n")) { 7694 if(!first) addLine(); 7695 first = false; 7696 auto c = component; 7697 c.text = t; 7698 lines[$-1].components ~= c; 7699 } 7700 } 7701 7702 /++ 7703 Adds an empty line. 7704 +/ 7705 void addLine() { 7706 lines ~= Line(); 7707 if(scrollbackPosition_) // if the user is scrolling back, we want to keep them basically centered where they are 7708 scrollbackPosition_++; 7709 } 7710 7711 /++ 7712 This is what [writeln] actually calls. 7713 7714 Using this exclusively though can give you more control, especially over the trailing \n. 7715 +/ 7716 void addLine(string line) { 7717 lines ~= Line([LineComponent(line)]); 7718 if(scrollbackPosition_) // if the user is scrolling back, we want to keep them basically centered where they are 7719 scrollbackPosition_++; 7720 } 7721 7722 /++ 7723 Adds a line by components without affecting scrollback. 7724 7725 History: 7726 Added May 17, 2022 7727 +/ 7728 void addLine(LineComponent[] components...) { 7729 lines ~= Line(components.dup); 7730 } 7731 7732 /++ 7733 Scrolling controls. 7734 7735 Notice that `scrollToTop` needs width and height to know how to word wrap it to determine the number of lines present to scroll back. 7736 +/ 7737 void scrollUp(int lines = 1) { 7738 scrollbackPosition_ += lines; 7739 //if(scrollbackPosition >= this.lines.length) 7740 // scrollbackPosition = cast(int) this.lines.length - 1; 7741 } 7742 7743 /// ditto 7744 void scrollDown(int lines = 1) { 7745 scrollbackPosition_ -= lines; 7746 if(scrollbackPosition_ < 0) 7747 scrollbackPosition_ = 0; 7748 } 7749 7750 /// ditto 7751 void scrollToBottom() { 7752 scrollbackPosition_ = 0; 7753 } 7754 7755 /// ditto 7756 void scrollToTop(int width, int height) { 7757 scrollbackPosition_ = scrollTopPosition(width, height); 7758 } 7759 7760 7761 /++ 7762 You can construct these to get more control over specifics including 7763 setting RGB colors. 7764 7765 But generally just using [write] and friends is easier. 7766 +/ 7767 struct LineComponent { 7768 private string text; 7769 private bool isRgb; 7770 private union { 7771 int color; 7772 RGB colorRgb; 7773 } 7774 private union { 7775 int background; 7776 RGB backgroundRgb; 7777 } 7778 private bool delegate() onclick; // return true if you need to redraw 7779 7780 // 16 color ctor 7781 this(string text, int color = Color.DEFAULT, int background = Color.DEFAULT, bool delegate() onclick = null) { 7782 this.text = text; 7783 this.color = color; 7784 this.background = background; 7785 this.onclick = onclick; 7786 this.isRgb = false; 7787 } 7788 7789 // true color ctor 7790 this(string text, RGB colorRgb, RGB backgroundRgb = RGB(0, 0, 0), bool delegate() onclick = null) { 7791 this.text = text; 7792 this.colorRgb = colorRgb; 7793 this.backgroundRgb = backgroundRgb; 7794 this.onclick = onclick; 7795 this.isRgb = true; 7796 } 7797 } 7798 7799 private struct Line { 7800 LineComponent[] components; 7801 int length() { 7802 int l = 0; 7803 foreach(c; components) 7804 l += c.text.length; 7805 return l; 7806 } 7807 } 7808 7809 /++ 7810 This is an internal helper for its scrollback buffer. 7811 7812 It is fairly generic and I might move it somewhere else some day. 7813 7814 It has a compile-time specified limit of 8192 entries. 7815 +/ 7816 static struct CircularBuffer(T) { 7817 T[] backing; 7818 7819 enum maxScrollback = 8192; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask... 7820 7821 int start; 7822 int length_; 7823 7824 void clear() { 7825 backing = null; 7826 start = 0; 7827 length_ = 0; 7828 } 7829 7830 size_t length() { 7831 return length_; 7832 } 7833 7834 void opOpAssign(string op : "~")(T line) { 7835 if(length_ < maxScrollback) { 7836 backing.assumeSafeAppend(); 7837 backing ~= line; 7838 length_++; 7839 } else { 7840 backing[start] = line; 7841 start++; 7842 if(start == maxScrollback) 7843 start = 0; 7844 } 7845 } 7846 7847 ref T opIndex(int idx) { 7848 return backing[(start + idx) % maxScrollback]; 7849 } 7850 ref T opIndex(Dollar idx) { 7851 return backing[(start + (length + idx.offsetFromEnd)) % maxScrollback]; 7852 } 7853 7854 CircularBufferRange opSlice(int startOfIteration, Dollar end) { 7855 return CircularBufferRange(&this, startOfIteration, cast(int) length - startOfIteration + end.offsetFromEnd); 7856 } 7857 CircularBufferRange opSlice(int startOfIteration, int end) { 7858 return CircularBufferRange(&this, startOfIteration, end - startOfIteration); 7859 } 7860 CircularBufferRange opSlice() { 7861 return CircularBufferRange(&this, 0, cast(int) length); 7862 } 7863 7864 static struct CircularBufferRange { 7865 CircularBuffer* item; 7866 int position; 7867 int remaining; 7868 this(CircularBuffer* item, int startOfIteration, int count) { 7869 this.item = item; 7870 position = startOfIteration; 7871 remaining = count; 7872 } 7873 7874 ref T front() { return (*item)[position]; } 7875 bool empty() { return remaining <= 0; } 7876 void popFront() { 7877 position++; 7878 remaining--; 7879 } 7880 7881 ref T back() { return (*item)[remaining - 1 - position]; } 7882 void popBack() { 7883 remaining--; 7884 } 7885 } 7886 7887 static struct Dollar { 7888 int offsetFromEnd; 7889 Dollar opBinary(string op : "-")(int rhs) { 7890 return Dollar(offsetFromEnd - rhs); 7891 } 7892 } 7893 Dollar opDollar() { return Dollar(0); } 7894 } 7895 7896 /++ 7897 Given a size, how far would you have to scroll back to get to the top? 7898 7899 Please note that this is O(n) with the length of the scrollback buffer. 7900 +/ 7901 int scrollTopPosition(int width, int height) { 7902 int lineCount; 7903 7904 foreach_reverse(line; lines) { 7905 int written = 0; 7906 comp_loop: foreach(cidx, component; line.components) { 7907 auto towrite = component.text; 7908 foreach(idx, dchar ch; towrite) { 7909 if(written >= width) { 7910 lineCount++; 7911 written = 0; 7912 } 7913 7914 if(ch == '\t') 7915 written += 8; // FIXME 7916 else 7917 written++; 7918 } 7919 } 7920 lineCount++; 7921 } 7922 7923 //if(lineCount > height) 7924 return lineCount - height; 7925 //return 0; 7926 } 7927 7928 /++ 7929 Draws the current state into the given terminal inside the given bounding box. 7930 7931 Also updates its internal position and click region data which it uses for event filtering in [handleEvent]. 7932 +/ 7933 void drawInto(Terminal* terminal, in int x = 0, in int y = 0, int width = 0, int height = 0) { 7934 if(lines.length == 0) 7935 return; 7936 7937 if(width == 0) 7938 width = terminal.width; 7939 if(height == 0) 7940 height = terminal.height; 7941 7942 this.x = x; 7943 this.y = y; 7944 this.width = width; 7945 this.height = height; 7946 7947 /* We need to figure out how much is going to fit 7948 in a first pass, so we can figure out where to 7949 start drawing */ 7950 7951 int remaining = height + scrollbackPosition; 7952 int start = cast(int) lines.length; 7953 int howMany = 0; 7954 7955 bool firstPartial = false; 7956 7957 static struct Idx { 7958 size_t cidx; 7959 size_t idx; 7960 } 7961 7962 Idx firstPartialStartIndex; 7963 7964 // this is private so I know we can safe append 7965 clickRegions.length = 0; 7966 clickRegions.assumeSafeAppend(); 7967 7968 // FIXME: should prolly handle \n and \r in here too. 7969 7970 // we'll work backwards to figure out how much will fit... 7971 // this will give accurate per-line things even with changing width and wrapping 7972 // while being generally efficient - we usually want to show the end of the list 7973 // anyway; actually using the scrollback is a bit of an exceptional case. 7974 7975 // It could probably do this instead of on each redraw, on each resize or insertion. 7976 // or at least cache between redraws until one of those invalidates it. 7977 foreach_reverse(line; lines) { 7978 int written = 0; 7979 int brokenLineCount; 7980 Idx[16] lineBreaksBuffer; 7981 Idx[] lineBreaks = lineBreaksBuffer[]; 7982 comp_loop: foreach(cidx, component; line.components) { 7983 auto towrite = component.text; 7984 foreach(idx, dchar ch; towrite) { 7985 if(written >= width) { 7986 if(brokenLineCount == lineBreaks.length) 7987 lineBreaks ~= Idx(cidx, idx); 7988 else 7989 lineBreaks[brokenLineCount] = Idx(cidx, idx); 7990 7991 brokenLineCount++; 7992 7993 written = 0; 7994 } 7995 7996 if(ch == '\t') 7997 written += 8; // FIXME 7998 else 7999 written++; 8000 } 8001 } 8002 8003 lineBreaks = lineBreaks[0 .. brokenLineCount]; 8004 8005 foreach_reverse(lineBreak; lineBreaks) { 8006 if(remaining == 1) { 8007 firstPartial = true; 8008 firstPartialStartIndex = lineBreak; 8009 break; 8010 } else { 8011 remaining--; 8012 } 8013 if(remaining <= 0) 8014 break; 8015 } 8016 8017 remaining--; 8018 8019 start--; 8020 howMany++; 8021 if(remaining <= 0) 8022 break; 8023 } 8024 8025 // second pass: actually draw it 8026 int linePos = remaining; 8027 8028 foreach(line; lines[start .. start + howMany]) { 8029 int written = 0; 8030 8031 if(linePos < 0) { 8032 linePos++; 8033 continue; 8034 } 8035 8036 terminal.moveTo(x, y + ((linePos >= 0) ? linePos : 0)); 8037 8038 auto todo = line.components; 8039 8040 if(firstPartial) { 8041 todo = todo[firstPartialStartIndex.cidx .. $]; 8042 } 8043 8044 foreach(ref component; todo) { 8045 if(component.isRgb) 8046 terminal.setTrueColor(component.colorRgb, component.backgroundRgb); 8047 else 8048 terminal.color( 8049 component.color == Color.DEFAULT ? defaultForeground : component.color, 8050 component.background == Color.DEFAULT ? defaultBackground : component.background, 8051 ); 8052 auto towrite = component.text; 8053 8054 again: 8055 8056 if(linePos >= height) 8057 break; 8058 8059 if(firstPartial) { 8060 towrite = towrite[firstPartialStartIndex.idx .. $]; 8061 firstPartial = false; 8062 } 8063 8064 foreach(idx, dchar ch; towrite) { 8065 if(written >= width) { 8066 clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 8067 terminal.write(towrite[0 .. idx]); 8068 towrite = towrite[idx .. $]; 8069 linePos++; 8070 written = 0; 8071 terminal.moveTo(x, y + linePos); 8072 goto again; 8073 } 8074 8075 if(ch == '\t') 8076 written += 8; // FIXME 8077 else 8078 written++; 8079 } 8080 8081 if(towrite.length) { 8082 clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 8083 terminal.write(towrite); 8084 } 8085 } 8086 8087 if(written < width) { 8088 terminal.color(defaultForeground, defaultBackground); 8089 foreach(i; written .. width) 8090 terminal.write(" "); 8091 } 8092 8093 linePos++; 8094 8095 if(linePos >= height) 8096 break; 8097 } 8098 8099 if(linePos < height) { 8100 terminal.color(defaultForeground, defaultBackground); 8101 foreach(i; linePos .. height) { 8102 if(i >= 0 && i < height) { 8103 terminal.moveTo(x, y + i); 8104 foreach(w; 0 .. width) 8105 terminal.write(" "); 8106 } 8107 } 8108 } 8109 } 8110 8111 private struct ClickRegion { 8112 LineComponent* component; 8113 int xStart; 8114 int yStart; 8115 int length; 8116 } 8117 private ClickRegion[] clickRegions; 8118 8119 /++ 8120 Default event handling for this widget. Call this only after drawing it into a rectangle 8121 and only if the event ought to be dispatched to it (which you determine however you want; 8122 you could dispatch all events to it, or perhaps filter some out too) 8123 8124 Returns: true if it should be redrawn 8125 +/ 8126 bool handleEvent(InputEvent e) { 8127 final switch(e.type) { 8128 case InputEvent.Type.LinkEvent: 8129 // meh 8130 break; 8131 case InputEvent.Type.KeyboardEvent: 8132 auto ev = e.keyboardEvent; 8133 8134 demandsAttention = false; 8135 8136 switch(ev.which) { 8137 case KeyboardEvent.Key.UpArrow: 8138 scrollUp(); 8139 return true; 8140 case KeyboardEvent.Key.DownArrow: 8141 scrollDown(); 8142 return true; 8143 case KeyboardEvent.Key.PageUp: 8144 if(ev.modifierState & ModifierState.control) 8145 scrollToTop(width, height); 8146 else 8147 scrollUp(height); 8148 return true; 8149 case KeyboardEvent.Key.PageDown: 8150 if(ev.modifierState & ModifierState.control) 8151 scrollToBottom(); 8152 else 8153 scrollDown(height); 8154 return true; 8155 default: 8156 // ignore 8157 } 8158 break; 8159 case InputEvent.Type.MouseEvent: 8160 auto ev = e.mouseEvent; 8161 if(ev.x >= x && ev.x < x + width && ev.y >= y && ev.y < y + height) { 8162 demandsAttention = false; 8163 // it is inside our box, so do something with it 8164 auto mx = ev.x - x; 8165 auto my = ev.y - y; 8166 8167 if(ev.eventType == MouseEvent.Type.Pressed) { 8168 if(ev.buttons & MouseEvent.Button.Left) { 8169 foreach(region; clickRegions) 8170 if(ev.x >= region.xStart && ev.x < region.xStart + region.length && ev.y == region.yStart) 8171 if(region.component.onclick !is null) 8172 return region.component.onclick(); 8173 } 8174 if(ev.buttons & MouseEvent.Button.ScrollUp) { 8175 scrollUp(); 8176 return true; 8177 } 8178 if(ev.buttons & MouseEvent.Button.ScrollDown) { 8179 scrollDown(); 8180 return true; 8181 } 8182 } 8183 } else { 8184 // outside our area, free to ignore 8185 } 8186 break; 8187 case InputEvent.Type.SizeChangedEvent: 8188 // (size changed might be but it needs to be handled at a higher level really anyway) 8189 // though it will return true because it probably needs redrawing anyway. 8190 return true; 8191 case InputEvent.Type.UserInterruptionEvent: 8192 throw new UserInterruptionException(); 8193 case InputEvent.Type.HangupEvent: 8194 throw new HangupException(); 8195 case InputEvent.Type.EndOfFileEvent: 8196 // ignore, not relevant to this 8197 break; 8198 case InputEvent.Type.CharacterEvent: 8199 case InputEvent.Type.NonCharacterKeyEvent: 8200 // obsolete, ignore them until they are removed 8201 break; 8202 case InputEvent.Type.CustomEvent: 8203 case InputEvent.Type.PasteEvent: 8204 // ignored, not relevant to us 8205 break; 8206 } 8207 8208 return false; 8209 } 8210 } 8211 8212 8213 /++ 8214 Thrown by [LineGetter] if the user pressed ctrl+c while it is processing events. 8215 +/ 8216 class UserInterruptionException : Exception { 8217 this() { super("Ctrl+C"); } 8218 } 8219 /++ 8220 Thrown by [LineGetter] if the terminal closes while it is processing input. 8221 +/ 8222 class HangupException : Exception { 8223 this() { super("Terminal disconnected"); } 8224 } 8225 8226 8227 8228 /* 8229 8230 // more efficient scrolling 8231 http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx 8232 // and the unix sequences 8233 8234 8235 rxvt documentation: 8236 use this to finish the input magic for that 8237 8238 8239 For the keypad, use Shift to temporarily override Application-Keypad 8240 setting use Num_Lock to toggle Application-Keypad setting if Num_Lock 8241 is off, toggle Application-Keypad setting. Also note that values of 8242 Home, End, Delete may have been compiled differently on your system. 8243 8244 Normal Shift Control Ctrl+Shift 8245 Tab ^I ESC [ Z ^I ESC [ Z 8246 BackSpace ^H ^? ^? ^? 8247 Find ESC [ 1 ~ ESC [ 1 $ ESC [ 1 ^ ESC [ 1 @ 8248 Insert ESC [ 2 ~ paste ESC [ 2 ^ ESC [ 2 @ 8249 Execute ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 8250 Select ESC [ 4 ~ ESC [ 4 $ ESC [ 4 ^ ESC [ 4 @ 8251 Prior ESC [ 5 ~ scroll-up ESC [ 5 ^ ESC [ 5 @ 8252 Next ESC [ 6 ~ scroll-down ESC [ 6 ^ ESC [ 6 @ 8253 Home ESC [ 7 ~ ESC [ 7 $ ESC [ 7 ^ ESC [ 7 @ 8254 End ESC [ 8 ~ ESC [ 8 $ ESC [ 8 ^ ESC [ 8 @ 8255 Delete ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 8256 F1 ESC [ 11 ~ ESC [ 23 ~ ESC [ 11 ^ ESC [ 23 ^ 8257 F2 ESC [ 12 ~ ESC [ 24 ~ ESC [ 12 ^ ESC [ 24 ^ 8258 F3 ESC [ 13 ~ ESC [ 25 ~ ESC [ 13 ^ ESC [ 25 ^ 8259 F4 ESC [ 14 ~ ESC [ 26 ~ ESC [ 14 ^ ESC [ 26 ^ 8260 F5 ESC [ 15 ~ ESC [ 28 ~ ESC [ 15 ^ ESC [ 28 ^ 8261 F6 ESC [ 17 ~ ESC [ 29 ~ ESC [ 17 ^ ESC [ 29 ^ 8262 F7 ESC [ 18 ~ ESC [ 31 ~ ESC [ 18 ^ ESC [ 31 ^ 8263 F8 ESC [ 19 ~ ESC [ 32 ~ ESC [ 19 ^ ESC [ 32 ^ 8264 F9 ESC [ 20 ~ ESC [ 33 ~ ESC [ 20 ^ ESC [ 33 ^ 8265 F10 ESC [ 21 ~ ESC [ 34 ~ ESC [ 21 ^ ESC [ 34 ^ 8266 F11 ESC [ 23 ~ ESC [ 23 $ ESC [ 23 ^ ESC [ 23 @ 8267 F12 ESC [ 24 ~ ESC [ 24 $ ESC [ 24 ^ ESC [ 24 @ 8268 F13 ESC [ 25 ~ ESC [ 25 $ ESC [ 25 ^ ESC [ 25 @ 8269 F14 ESC [ 26 ~ ESC [ 26 $ ESC [ 26 ^ ESC [ 26 @ 8270 F15 (Help) ESC [ 28 ~ ESC [ 28 $ ESC [ 28 ^ ESC [ 28 @ 8271 F16 (Menu) ESC [ 29 ~ ESC [ 29 $ ESC [ 29 ^ ESC [ 29 @ 8272 8273 F17 ESC [ 31 ~ ESC [ 31 $ ESC [ 31 ^ ESC [ 31 @ 8274 F18 ESC [ 32 ~ ESC [ 32 $ ESC [ 32 ^ ESC [ 32 @ 8275 F19 ESC [ 33 ~ ESC [ 33 $ ESC [ 33 ^ ESC [ 33 @ 8276 F20 ESC [ 34 ~ ESC [ 34 $ ESC [ 34 ^ ESC [ 34 @ 8277 Application 8278 Up ESC [ A ESC [ a ESC O a ESC O A 8279 Down ESC [ B ESC [ b ESC O b ESC O B 8280 Right ESC [ C ESC [ c ESC O c ESC O C 8281 Left ESC [ D ESC [ d ESC O d ESC O D 8282 KP_Enter ^M ESC O M 8283 KP_F1 ESC O P ESC O P 8284 KP_F2 ESC O Q ESC O Q 8285 KP_F3 ESC O R ESC O R 8286 KP_F4 ESC O S ESC O S 8287 XK_KP_Multiply * ESC O j 8288 XK_KP_Add + ESC O k 8289 XK_KP_Separator , ESC O l 8290 XK_KP_Subtract - ESC O m 8291 XK_KP_Decimal . ESC O n 8292 XK_KP_Divide / ESC O o 8293 XK_KP_0 0 ESC O p 8294 XK_KP_1 1 ESC O q 8295 XK_KP_2 2 ESC O r 8296 XK_KP_3 3 ESC O s 8297 XK_KP_4 4 ESC O t 8298 XK_KP_5 5 ESC O u 8299 XK_KP_6 6 ESC O v 8300 XK_KP_7 7 ESC O w 8301 XK_KP_8 8 ESC O x 8302 XK_KP_9 9 ESC O y 8303 */ 8304 8305 version(Demo_kbhit) 8306 void main() { 8307 auto terminal = Terminal(ConsoleOutputType.linear); 8308 auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw); 8309 8310 int a; 8311 char ch = '.'; 8312 while(a < 1000) { 8313 a++; 8314 if(a % terminal.width == 0) { 8315 terminal.write("\r"); 8316 if(ch == '.') 8317 ch = ' '; 8318 else 8319 ch = '.'; 8320 } 8321 8322 if(input.kbhit()) 8323 terminal.write(input.getch()); 8324 else 8325 terminal.write(ch); 8326 8327 terminal.flush(); 8328 8329 import core.thread; 8330 Thread.sleep(50.msecs); 8331 } 8332 } 8333 8334 /* 8335 The Xterm palette progression is: 8336 [0, 95, 135, 175, 215, 255] 8337 8338 So if I take the color and subtract 55, then div 40, I get 8339 it into one of these areas. If I add 20, I get a reasonable 8340 rounding. 8341 */ 8342 8343 ubyte colorToXTermPaletteIndex(RGB color) { 8344 /* 8345 Here, I will round off to the color ramp or the 8346 greyscale. I will NOT use the bottom 16 colors because 8347 there's duplicates (or very close enough) to them in here 8348 */ 8349 8350 if(color.r == color.g && color.g == color.b) { 8351 // grey - find one of them: 8352 if(color.r == 0) return 0; 8353 // meh don't need those two, let's simplify branche 8354 //if(color.r == 0xc0) return 7; 8355 //if(color.r == 0x80) return 8; 8356 // it isn't == 255 because it wants to catch anything 8357 // that would wrap the simple algorithm below back to 0. 8358 if(color.r >= 248) return 15; 8359 8360 // there's greys in the color ramp too, but these 8361 // are all close enough as-is, no need to complicate 8362 // algorithm for approximation anyway 8363 8364 return cast(ubyte) (232 + ((color.r - 8) / 10)); 8365 } 8366 8367 // if it isn't grey, it is color 8368 8369 // the ramp goes blue, green, red, with 6 of each, 8370 // so just multiplying will give something good enough 8371 8372 // will give something between 0 and 5, with some rounding 8373 auto r = (cast(int) color.r - 35) / 40; 8374 auto g = (cast(int) color.g - 35) / 40; 8375 auto b = (cast(int) color.b - 35) / 40; 8376 8377 return cast(ubyte) (16 + b + g*6 + r*36); 8378 } 8379 8380 /++ 8381 Represents a 24-bit color. 8382 8383 8384 $(TIP You can convert these to and from [arsd.color.Color] using 8385 `.tupleof`: 8386 8387 --- 8388 RGB rgb; 8389 Color c = Color(rgb.tupleof); 8390 --- 8391 ) 8392 +/ 8393 struct RGB { 8394 ubyte r; /// 8395 ubyte g; /// 8396 ubyte b; /// 8397 // terminal can't actually use this but I want the value 8398 // there for assignment to an arsd.color.Color 8399 private ubyte a = 255; 8400 } 8401 8402 // This is an approximation too for a few entries, but a very close one. 8403 RGB xtermPaletteIndexToColor(int paletteIdx) { 8404 RGB color; 8405 8406 if(paletteIdx < 16) { 8407 if(paletteIdx == 7) 8408 return RGB(0xc0, 0xc0, 0xc0); 8409 else if(paletteIdx == 8) 8410 return RGB(0x80, 0x80, 0x80); 8411 8412 color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 8413 color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 8414 color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 8415 8416 } else if(paletteIdx < 232) { 8417 // color ramp, 6x6x6 cube 8418 color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); 8419 color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); 8420 color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); 8421 8422 if(color.r == 55) color.r = 0; 8423 if(color.g == 55) color.g = 0; 8424 if(color.b == 55) color.b = 0; 8425 } else { 8426 // greyscale ramp, from 0x8 to 0xee 8427 color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); 8428 color.g = color.r; 8429 color.b = color.g; 8430 } 8431 8432 return color; 8433 } 8434 8435 int approximate16Color(RGB color) { 8436 int c; 8437 c |= color.r > 64 ? RED_BIT : 0; 8438 c |= color.g > 64 ? GREEN_BIT : 0; 8439 c |= color.b > 64 ? BLUE_BIT : 0; 8440 8441 c |= (((color.r + color.g + color.b) / 3) > 80) ? Bright : 0; 8442 8443 return c; 8444 } 8445 8446 version(TerminalDirectToEmulator) { 8447 8448 void terminateTerminalProcess(T)(T threadId) { 8449 version(Posix) { 8450 pthread_kill(threadId, SIGQUIT); // or SIGKILL even? 8451 8452 assert(0); 8453 //import core.sys.posix.pthread; 8454 //pthread_cancel(widget.term.threadId); 8455 //widget.term = null; 8456 } else version(Windows) { 8457 import core.sys.windows.winbase; 8458 import core.sys.windows.winnt; 8459 8460 auto hnd = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, TRUE, GetCurrentProcessId()); 8461 TerminateProcess(hnd, -1); 8462 assert(0); 8463 } 8464 } 8465 8466 8467 8468 /++ 8469 Indicates the TerminalDirectToEmulator features 8470 are present. You can check this with `static if`. 8471 8472 $(WARNING 8473 This will cause the [Terminal] constructor to spawn a GUI thread with [arsd.minigui]/[arsd.simpledisplay]. 8474 8475 This means you can NOT use those libraries in your 8476 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. 8477 ) 8478 +/ 8479 enum IntegratedEmulator = true; 8480 8481 version(Windows) { 8482 private enum defaultFont = "Consolas"; 8483 private enum defaultSize = 14; 8484 } else { 8485 private enum defaultFont = "monospace"; 8486 private enum defaultSize = 12; // it is measured differently with fontconfig than core x and windows... 8487 } 8488 8489 /++ 8490 Allows customization of the integrated emulator window. 8491 You may change the default colors, font, and other aspects 8492 of GUI integration. 8493 8494 Test for its presence before using with `static if(arsd.terminal.IntegratedEmulator)`. 8495 8496 All settings here must be set BEFORE you construct any [Terminal] instances. 8497 8498 History: 8499 Added March 7, 2020. 8500 +/ 8501 struct IntegratedTerminalEmulatorConfiguration { 8502 /// Note that all Colors in here are 24 bit colors. 8503 alias Color = arsd.color.Color; 8504 8505 /// Default foreground color of the terminal. 8506 Color defaultForeground = Color.black; 8507 /// Default background color of the terminal. 8508 Color defaultBackground = Color.white; 8509 8510 /++ 8511 Font to use in the window. It should be a monospace font, 8512 and your selection may not actually be used if not available on 8513 the user's system, in which case it will fallback to one. 8514 8515 History: 8516 Implemented March 26, 2020 8517 8518 On January 16, 2021, I changed the default to be a fancier 8519 font than the underlying terminalemulator.d uses ("monospace" 8520 on Linux and "Consolas" on Windows, though I will note 8521 that I do *not* guarantee this won't change.) On January 18, 8522 I changed the default size. 8523 8524 If you want specific values for these things, you should set 8525 them in your own application. 8526 8527 On January 12, 2022, I changed the font size to be auto-scaled 8528 with detected dpi by default. You can undo this by setting 8529 `scaleFontSizeWithDpi` to false. On March 22, 2022, I tweaked 8530 this slightly to only scale if the font point size is not already 8531 scaled (e.g. by Xft.dpi settings) to avoid double scaling. 8532 +/ 8533 string fontName = defaultFont; 8534 /// ditto 8535 int fontSize = defaultSize; 8536 /// ditto 8537 bool scaleFontSizeWithDpi = true; 8538 8539 /++ 8540 Requested initial terminal size in character cells. You may not actually get exactly this. 8541 +/ 8542 int initialWidth = 80; 8543 /// ditto 8544 int initialHeight = 30; 8545 8546 /++ 8547 If `true`, the window will close automatically when the main thread exits. 8548 Otherwise, the window will remain open so the user can work with output before 8549 it disappears. 8550 8551 History: 8552 Added April 10, 2020 (v7.2.0) 8553 +/ 8554 bool closeOnExit = false; 8555 8556 /++ 8557 Gives you a chance to modify the window as it is constructed. Intended 8558 to let you add custom menu options. 8559 8560 --- 8561 import arsd.terminal; 8562 integratedTerminalEmulatorConfiguration.menuExtensionsConstructor = (TerminalEmulatorWindow window) { 8563 import arsd.minigui; // for the menu related UDAs 8564 class Commands { 8565 @menu("Help") { 8566 void Topics() { 8567 auto window = new Window(); // make a help window of some sort 8568 window.show(); 8569 } 8570 8571 @separator 8572 8573 void About() { 8574 messageBox("My Application v 1.0"); 8575 } 8576 } 8577 } 8578 window.setMenuAndToolbarFromAnnotatedCode(new Commands()); 8579 }; 8580 --- 8581 8582 History: 8583 Added March 29, 2020. Included in release v7.1.0. 8584 +/ 8585 void delegate(TerminalEmulatorWindow) menuExtensionsConstructor; 8586 8587 /++ 8588 Set this to true if you want [Terminal] to fallback to the user's 8589 existing native terminal in the event that creating the custom terminal 8590 is impossible for whatever reason. 8591 8592 If your application must have all advanced features, set this to `false`. 8593 Otherwise, be sure you handle the absence of advanced features in your 8594 application by checking methods like [Terminal.inlineImagesSupported], 8595 etc., and only use things you can gracefully degrade without. 8596 8597 If this is set to false, `Terminal`'s constructor will throw if the gui fails 8598 instead of carrying on with the stdout terminal (if possible). 8599 8600 History: 8601 Added June 28, 2020. Included in release v8.1.0. 8602 8603 +/ 8604 bool fallbackToDegradedTerminal = true; 8605 8606 /++ 8607 The default key control is ctrl+c sends an interrupt character and ctrl+shift+c 8608 does copy to clipboard. If you set this to `true`, it swaps those two bindings. 8609 8610 History: 8611 Added June 15, 2021. Included in release v10.1.0. 8612 +/ 8613 bool ctrlCCopies = false; // FIXME: i could make this context-sensitive too, so if text selected, copy, otherwise, cancel. prolly show in statu s bar 8614 8615 /++ 8616 When using the integrated terminal emulator, the default is to assume you want it. 8617 But some users may wish to force the in-terminal fallback anyway at start up time. 8618 8619 Seeing this to `true` will skip attempting to create the gui window where a fallback 8620 is available. It is ignored on systems where there is no fallback. Make sure that 8621 [fallbackToDegradedTerminal] is set to `true` if you use this. 8622 8623 History: 8624 Added October 4, 2022 (dub v10.10) 8625 +/ 8626 bool preferDegradedTerminal = false; 8627 } 8628 8629 /+ 8630 status bar should probably tell 8631 if scroll lock is on... 8632 +/ 8633 8634 /// You can set this in a static module constructor. (`shared static this() {}`) 8635 __gshared IntegratedTerminalEmulatorConfiguration integratedTerminalEmulatorConfiguration; 8636 8637 import arsd.terminalemulator; 8638 import arsd.minigui; 8639 8640 version(Posix) 8641 private extern(C) int openpty(int* master, int* slave, char*, const void*, const void*); 8642 8643 /++ 8644 Represents the window that the library pops up for you. 8645 +/ 8646 final class TerminalEmulatorWindow : MainWindow { 8647 /++ 8648 Returns the size of an individual character cell, in pixels. 8649 8650 History: 8651 Added April 2, 2021 8652 +/ 8653 Size characterCellSize() { 8654 if(tew && tew.terminalEmulator) 8655 return Size(tew.terminalEmulator.fontWidth, tew.terminalEmulator.fontHeight); 8656 else 8657 return Size(1, 1); 8658 } 8659 8660 /++ 8661 Gives access to the underlying terminal emulation object. 8662 +/ 8663 TerminalEmulator terminalEmulator() { 8664 return tew.terminalEmulator; 8665 } 8666 8667 private TerminalEmulatorWindow parent; 8668 private TerminalEmulatorWindow[] children; 8669 private void childClosing(TerminalEmulatorWindow t) { 8670 foreach(idx, c; children) 8671 if(c is t) 8672 children = children[0 .. idx] ~ children[idx + 1 .. $]; 8673 } 8674 private void registerChild(TerminalEmulatorWindow t) { 8675 children ~= t; 8676 } 8677 8678 private this(Terminal* term, TerminalEmulatorWindow parent) { 8679 8680 this.parent = parent; 8681 scope(success) if(parent) parent.registerChild(this); 8682 8683 super("Terminal Application"); 8684 //, integratedTerminalEmulatorConfiguration.initialWidth * integratedTerminalEmulatorConfiguration.fontSize / 2, integratedTerminalEmulatorConfiguration.initialHeight * integratedTerminalEmulatorConfiguration.fontSize); 8685 8686 smw = new ScrollMessageWidget(this); 8687 tew = new TerminalEmulatorWidget(term, smw); 8688 8689 if(integratedTerminalEmulatorConfiguration.initialWidth == 0 || integratedTerminalEmulatorConfiguration.initialHeight == 0) { 8690 win.show(); // if must be mapped before maximized... it does cause a flash but meh. 8691 win.maximize(); 8692 } else { 8693 win.resize(integratedTerminalEmulatorConfiguration.initialWidth * tew.terminalEmulator.fontWidth, integratedTerminalEmulatorConfiguration.initialHeight * tew.terminalEmulator.fontHeight); 8694 } 8695 8696 smw.addEventListener("scroll", () { 8697 tew.terminalEmulator.scrollbackTo(smw.position.x, smw.position.y + tew.terminalEmulator.height); 8698 redraw(); 8699 }); 8700 8701 smw.setTotalArea(1, 1); 8702 8703 setMenuAndToolbarFromAnnotatedCode(this); 8704 if(integratedTerminalEmulatorConfiguration.menuExtensionsConstructor) 8705 integratedTerminalEmulatorConfiguration.menuExtensionsConstructor(this); 8706 8707 8708 8709 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 8710 version(Posix) { 8711 import unix = core.sys.posix.unistd; 8712 import core.stdc.stdio; 8713 8714 auto fp = stdout; 8715 8716 // FIXME: openpty? child processes can get a lil borked. 8717 8718 int[2] fds; 8719 auto ret = pipe(fds); 8720 8721 auto fd = fileno(fp); 8722 8723 dup2(fds[1], fd); 8724 unix.close(fds[1]); 8725 if(isatty(2)) 8726 dup2(1, 2); 8727 auto listener = new PosixFdReader(() { 8728 ubyte[1024] buffer; 8729 auto ret = read(fds[0], buffer.ptr, buffer.length); 8730 if(ret <= 0) return; 8731 tew.terminalEmulator.sendRawInput(buffer[0 .. ret]); 8732 tew.terminalEmulator.redraw(); 8733 }, fds[0]); 8734 8735 readFd = fds[0]; 8736 } else version(CRuntime_Microsoft) { 8737 8738 CHAR[MAX_PATH] PipeNameBuffer; 8739 8740 static shared(int) PipeSerialNumber = 0; 8741 8742 import core.atomic; 8743 8744 import core.stdc.string; 8745 8746 // we need a unique name in the universal filesystem 8747 // so it can be freopen'd. When the process terminates, 8748 // this is auto-closed too, so the pid is good enough, just 8749 // with the shared number 8750 sprintf(PipeNameBuffer.ptr, 8751 `\\.\pipe\arsd.terminal.pipe.%08x.%08x`.ptr, 8752 GetCurrentProcessId(), 8753 atomicOp!"+="(PipeSerialNumber, 1) 8754 ); 8755 8756 readPipe = CreateNamedPipeA( 8757 PipeNameBuffer.ptr, 8758 1/*PIPE_ACCESS_INBOUND*/ | FILE_FLAG_OVERLAPPED, 8759 0 /*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/, 8760 1, // Number of pipes 8761 1024, // Out buffer size 8762 1024, // In buffer size 8763 0,//120 * 1000, // Timeout in ms 8764 null 8765 ); 8766 if (!readPipe) { 8767 throw new Exception("CreateNamedPipeA"); 8768 } 8769 8770 this.overlapped = new OVERLAPPED(); 8771 this.overlapped.hEvent = cast(void*) this; 8772 this.overlappedBuffer = new ubyte[](4096); 8773 8774 import std.conv; 8775 import core.stdc.errno; 8776 if(freopen(PipeNameBuffer.ptr, "wb", stdout) is null) 8777 //MessageBoxA(null, ("excep " ~ to!string(errno) ~ "\0").ptr, "asda", 0); 8778 throw new Exception("freopen"); 8779 8780 setvbuf(stdout, null, _IOLBF, 128); // I'd prefer to line buffer it, but that doesn't seem to work for some reason. 8781 8782 ConnectNamedPipe(readPipe, this.overlapped); 8783 8784 // also send stderr to stdout if it isn't already redirected somewhere else 8785 if(_fileno(stderr) < 0) { 8786 freopen("nul", "wb", stderr); 8787 8788 _dup2(_fileno(stdout), _fileno(stderr)); 8789 setvbuf(stderr, null, _IOLBF, 128); // if I don't unbuffer this it can really confuse things 8790 assert(0); 8791 } 8792 8793 WindowsRead(0, 0, this.overlapped); 8794 } else throw new Exception("pipeThroughStdOut not supported on this system currently. Use -m32mscoff instead."); 8795 } 8796 } 8797 8798 version(Windows) { 8799 HANDLE readPipe; 8800 private ubyte[] overlappedBuffer; 8801 private OVERLAPPED* overlapped; 8802 static final private extern(Windows) void WindowsRead(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) { 8803 TerminalEmulatorWindow w = cast(TerminalEmulatorWindow) overlapped.hEvent; 8804 if(numberOfBytes) { 8805 w.tew.terminalEmulator.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]); 8806 w.tew.terminalEmulator.redraw(); 8807 } 8808 import std.conv; 8809 if(!ReadFileEx(w.readPipe, w.overlappedBuffer.ptr, cast(DWORD) w.overlappedBuffer.length, overlapped, &WindowsRead)) 8810 if(GetLastError() == 997) {} 8811 //else throw new Exception("ReadFileEx " ~ to!string(GetLastError())); 8812 } 8813 } 8814 8815 version(Posix) { 8816 int readFd = -1; 8817 } 8818 8819 TerminalEmulator.TerminalCell[] delegate(TerminalEmulator.TerminalCell[] i) parentFilter; 8820 8821 private void addScrollbackLineFromParent(TerminalEmulator.TerminalCell[] lineIn) { 8822 if(parentFilter is null) 8823 return; 8824 8825 auto line = parentFilter(lineIn); 8826 if(line is null) return; 8827 8828 if(tew && tew.terminalEmulator) { 8829 bool atBottom = smw.verticalScrollBar.atEnd && smw.horizontalScrollBar.atStart; 8830 tew.terminalEmulator.addScrollbackLine(line); 8831 tew.terminalEmulator.notifyScrollbackAdded(); 8832 if(atBottom) { 8833 tew.terminalEmulator.notifyScrollbarPosition(0, int.max); 8834 tew.terminalEmulator.scrollbackTo(0, int.max); 8835 tew.terminalEmulator.drawScrollback(); 8836 tew.redraw(); 8837 } 8838 } 8839 } 8840 8841 private TerminalEmulatorWidget tew; 8842 private ScrollMessageWidget smw; 8843 8844 @menu("&History") { 8845 @tip("Saves the currently visible content to a file") 8846 void Save() { 8847 getSaveFileName((string name) { 8848 if(name.length) { 8849 try 8850 tew.terminalEmulator.writeScrollbackToFile(name); 8851 catch(Exception e) 8852 messageBox("Save failed: " ~ e.msg); 8853 } 8854 }); 8855 } 8856 8857 // FIXME 8858 version(FIXME) 8859 void Save_HTML() { 8860 8861 } 8862 8863 @separator 8864 /* 8865 void Find() { 8866 // FIXME 8867 // jump to the previous instance in the scrollback 8868 8869 } 8870 */ 8871 8872 void Filter() { 8873 // open a new window that just shows items that pass the filter 8874 8875 static struct FilterParams { 8876 string searchTerm; 8877 bool caseSensitive; 8878 } 8879 8880 dialog((FilterParams p) { 8881 auto nw = new TerminalEmulatorWindow(null, this); 8882 8883 nw.parentWindow.win.handleCharEvent = null; // kinda a hack... i just don't want it ever turning off scroll lock... 8884 8885 nw.parentFilter = (TerminalEmulator.TerminalCell[] line) { 8886 import std.algorithm; 8887 import std.uni; 8888 // omg autodecoding being kinda useful for once LOL 8889 if(line.map!(c => c.hasNonCharacterData ? dchar(0) : (p.caseSensitive ? c.ch : c.ch.toLower)). 8890 canFind(p.searchTerm)) 8891 { 8892 // I might highlight the match too, but meh for now 8893 return line; 8894 } 8895 return null; 8896 }; 8897 8898 foreach(line; tew.terminalEmulator.sbb[0 .. $]) { 8899 if(auto l = nw.parentFilter(line)) { 8900 nw.tew.terminalEmulator.addScrollbackLine(l); 8901 } 8902 } 8903 nw.tew.terminalEmulator.scrollLockLock(); 8904 nw.tew.terminalEmulator.drawScrollback(); 8905 nw.title = "Filter Display"; 8906 nw.show(); 8907 }); 8908 8909 } 8910 8911 @separator 8912 void Clear() { 8913 tew.terminalEmulator.clearScrollbackHistory(); 8914 tew.terminalEmulator.cls(); 8915 tew.terminalEmulator.moveCursor(0, 0); 8916 if(tew.term) { 8917 tew.term.windowSizeChanged = true; 8918 tew.terminalEmulator.outgoingSignal.notify(); 8919 } 8920 tew.redraw(); 8921 } 8922 8923 @separator 8924 void Exit() @accelerator("Alt+F4") @hotkey('x') { 8925 this.close(); 8926 } 8927 } 8928 8929 @menu("&Edit") { 8930 void Copy() { 8931 tew.terminalEmulator.copyToClipboard(tew.terminalEmulator.getSelectedText()); 8932 } 8933 8934 void Paste() { 8935 tew.terminalEmulator.pasteFromClipboard(&tew.terminalEmulator.sendPasteData); 8936 } 8937 } 8938 } 8939 8940 private class InputEventInternal { 8941 const(ubyte)[] data; 8942 this(in ubyte[] data) { 8943 this.data = data; 8944 } 8945 } 8946 8947 private class TerminalEmulatorWidget : Widget { 8948 8949 Menu ctx; 8950 8951 override Menu contextMenu(int x, int y) { 8952 if(ctx is null) { 8953 ctx = new Menu("", this); 8954 ctx.addItem(new MenuItem(new Action("Copy", 0, { 8955 terminalEmulator.copyToClipboard(terminalEmulator.getSelectedText()); 8956 }))); 8957 ctx.addItem(new MenuItem(new Action("Paste", 0, { 8958 terminalEmulator.pasteFromClipboard(&terminalEmulator.sendPasteData); 8959 }))); 8960 ctx.addItem(new MenuItem(new Action("Toggle Scroll Lock", 0, { 8961 terminalEmulator.toggleScrollLock(); 8962 }))); 8963 } 8964 return ctx; 8965 } 8966 8967 this(Terminal* term, ScrollMessageWidget parent) { 8968 this.smw = parent; 8969 this.term = term; 8970 super(parent); 8971 terminalEmulator = new TerminalEmulatorInsideWidget(this); 8972 this.parentWindow.addEventListener("closed", { 8973 if(term) { 8974 term.hangedUp = true; 8975 // should I just send an official SIGHUP?! 8976 } 8977 8978 if(auto wi = cast(TerminalEmulatorWindow) this.parentWindow) { 8979 if(wi.parent) 8980 wi.parent.childClosing(wi); 8981 8982 // if I don't close the redirected pipe, the other thread 8983 // will get stuck indefinitely as it tries to flush its stderr 8984 version(Windows) { 8985 CloseHandle(wi.readPipe); 8986 wi.readPipe = null; 8987 } version(Posix) { 8988 import unix = core.sys.posix.unistd; 8989 import unix2 = core.sys.posix.fcntl; 8990 unix.close(wi.readFd); 8991 8992 version(none) 8993 if(term && term.pipeThroughStdOut) { 8994 auto fd = unix2.open("/dev/null", unix2.O_RDWR); 8995 unix.close(0); 8996 unix.close(1); 8997 unix.close(2); 8998 8999 dup2(fd, 0); 9000 dup2(fd, 1); 9001 dup2(fd, 2); 9002 } 9003 } 9004 } 9005 9006 // try to get it to terminate slightly more forcibly too, if possible 9007 if(sigIntExtension) 9008 sigIntExtension(); 9009 9010 terminalEmulator.outgoingSignal.notify(); 9011 terminalEmulator.incomingSignal.notify(); 9012 terminalEmulator.syncSignal.notify(); 9013 9014 windowGone = true; 9015 }); 9016 9017 this.parentWindow.win.addEventListener((InputEventInternal ie) { 9018 terminalEmulator.sendRawInput(ie.data); 9019 this.redraw(); 9020 terminalEmulator.incomingSignal.notify(); 9021 }); 9022 } 9023 9024 ScrollMessageWidget smw; 9025 Terminal* term; 9026 9027 void sendRawInput(const(ubyte)[] data) { 9028 if(this.parentWindow) { 9029 this.parentWindow.win.postEvent(new InputEventInternal(data)); 9030 if(windowGone) forceTermination(); 9031 terminalEmulator.incomingSignal.wait(); // blocking write basically, wait until the TE confirms the receipt of it 9032 } 9033 } 9034 9035 override void dpiChanged() { 9036 if(terminalEmulator) { 9037 terminalEmulator.loadFont(); 9038 terminalEmulator.resized(width, height); 9039 } 9040 } 9041 9042 TerminalEmulatorInsideWidget terminalEmulator; 9043 9044 override void registerMovement() { 9045 super.registerMovement(); 9046 terminalEmulator.resized(width, height); 9047 } 9048 9049 override void focus() { 9050 super.focus(); 9051 terminalEmulator.attentionReceived(); 9052 } 9053 9054 static class Style : Widget.Style { 9055 override MouseCursor cursor() { 9056 return GenericCursor.Text; 9057 } 9058 } 9059 mixin OverrideStyle!Style; 9060 9061 override void erase(WidgetPainter painter) { /* intentionally blank, paint does it better */ } 9062 9063 override void paint(WidgetPainter painter) { 9064 bool forceRedraw = false; 9065 if(terminalEmulator.invalidateAll || terminalEmulator.clearScreenRequested) { 9066 auto clearColor = terminalEmulator.defaultBackground; 9067 painter.outlineColor = clearColor; 9068 painter.fillColor = clearColor; 9069 painter.drawRectangle(Point(0, 0), this.width, this.height); 9070 terminalEmulator.clearScreenRequested = false; 9071 forceRedraw = true; 9072 } 9073 9074 terminalEmulator.redrawPainter(painter, forceRedraw); 9075 } 9076 } 9077 9078 private class TerminalEmulatorInsideWidget : TerminalEmulator { 9079 9080 private ScrollbackBuffer sbb() { return scrollbackBuffer; } 9081 9082 void resized(int w, int h) { 9083 this.resizeTerminal(w / fontWidth, h / fontHeight); 9084 if(widget && widget.smw) { 9085 widget.smw.setViewableArea(this.width, this.height); 9086 widget.smw.setPageSize(this.width / 2, this.height / 2); 9087 } 9088 notifyScrollbarPosition(0, int.max); 9089 clearScreenRequested = true; 9090 if(widget && widget.term) 9091 widget.term.windowSizeChanged = true; 9092 outgoingSignal.notify(); 9093 redraw(); 9094 } 9095 9096 override void addScrollbackLine(TerminalCell[] line) { 9097 super.addScrollbackLine(line); 9098 if(widget) 9099 if(auto p = cast(TerminalEmulatorWindow) widget.parentWindow) { 9100 foreach(child; p.children) 9101 child.addScrollbackLineFromParent(line); 9102 } 9103 } 9104 9105 override void notifyScrollbackAdded() { 9106 widget.smw.setTotalArea(this.scrollbackWidth > this.width ? this.scrollbackWidth : this.width, this.scrollbackLength > this.height ? this.scrollbackLength : this.height); 9107 } 9108 9109 override void notifyScrollbarPosition(int x, int y) { 9110 widget.smw.setPosition(x, y); 9111 widget.redraw(); 9112 } 9113 9114 override void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) { 9115 if(isRelevantVertically) 9116 notifyScrollbackAdded(); 9117 else 9118 widget.smw.setTotalArea(width, height); 9119 } 9120 9121 override @property public int cursorX() { return super.cursorX; } 9122 override @property public int cursorY() { return super.cursorY; } 9123 9124 protected override void changeCursorStyle(CursorStyle s) { } 9125 9126 string currentTitle; 9127 protected override void changeWindowTitle(string t) { 9128 if(widget && widget.parentWindow && t.length) { 9129 widget.parentWindow.win.title = t; 9130 currentTitle = t; 9131 } 9132 } 9133 protected override void changeWindowIcon(IndexedImage t) { 9134 if(widget && widget.parentWindow && t) 9135 widget.parentWindow.win.icon = t; 9136 } 9137 9138 protected override void changeIconTitle(string) {} 9139 protected override void changeTextAttributes(TextAttributes) {} 9140 protected override void soundBell() { 9141 static if(UsingSimpledisplayX11) 9142 XBell(XDisplayConnection.get(), 50); 9143 } 9144 9145 protected override void demandAttention() { 9146 if(widget && widget.parentWindow) 9147 widget.parentWindow.win.requestAttention(); 9148 } 9149 9150 protected override void copyToClipboard(string text) { 9151 setClipboardText(widget.parentWindow.win, text); 9152 } 9153 9154 override int maxScrollbackLength() const { 9155 return int.max; // no scrollback limit for custom programs 9156 } 9157 9158 protected override void pasteFromClipboard(void delegate(in char[]) dg) { 9159 getClipboardText(widget.parentWindow.win, (in char[] dataIn) { 9160 char[] data; 9161 // change Windows \r\n to plain \n 9162 foreach(char ch; dataIn) 9163 if(ch != 13) 9164 data ~= ch; 9165 dg(data); 9166 }); 9167 } 9168 9169 protected override void copyToPrimary(string text) { 9170 static if(UsingSimpledisplayX11) 9171 setPrimarySelection(widget.parentWindow.win, text); 9172 else 9173 {} 9174 } 9175 protected override void pasteFromPrimary(void delegate(in char[]) dg) { 9176 static if(UsingSimpledisplayX11) 9177 getPrimarySelection(widget.parentWindow.win, dg); 9178 } 9179 9180 override void requestExit() { 9181 widget.parentWindow.close(); 9182 } 9183 9184 bool echo = false; 9185 9186 override void sendRawInput(in ubyte[] data) { 9187 void send(in ubyte[] data) { 9188 if(data.length == 0) 9189 return; 9190 super.sendRawInput(data); 9191 if(echo) 9192 sendToApplication(data); 9193 } 9194 9195 // need to echo, translate 10 to 13/10 cr-lf 9196 size_t last = 0; 9197 const ubyte[2] crlf = [13, 10]; 9198 foreach(idx, ch; data) { 9199 if(waitingForInboundSync && ch == 255) { 9200 send(data[last .. idx]); 9201 last = idx + 1; 9202 waitingForInboundSync = false; 9203 syncSignal.notify(); 9204 continue; 9205 } 9206 if(ch == 10) { 9207 send(data[last .. idx]); 9208 send(crlf[]); 9209 last = idx + 1; 9210 } 9211 } 9212 9213 if(last < data.length) 9214 send(data[last .. $]); 9215 } 9216 9217 bool focused; 9218 9219 TerminalEmulatorWidget widget; 9220 9221 import arsd.simpledisplay; 9222 import arsd.color; 9223 import core.sync.semaphore; 9224 alias ModifierState = arsd.simpledisplay.ModifierState; 9225 alias Color = arsd.color.Color; 9226 alias fromHsl = arsd.color.fromHsl; 9227 9228 const(ubyte)[] pendingForApplication; 9229 Semaphore syncSignal; 9230 Semaphore outgoingSignal; 9231 Semaphore incomingSignal; 9232 9233 private shared(bool) waitingForInboundSync; 9234 9235 override void sendToApplication(scope const(void)[] what) { 9236 synchronized(this) { 9237 pendingForApplication ~= cast(const(ubyte)[]) what; 9238 } 9239 outgoingSignal.notify(); 9240 } 9241 9242 @property int width() { return screenWidth; } 9243 @property int height() { return screenHeight; } 9244 9245 @property bool invalidateAll() { return super.invalidateAll; } 9246 9247 void loadFont() { 9248 if(this.font) { 9249 this.font.unload(); 9250 this.font = null; 9251 } 9252 auto fontSize = integratedTerminalEmulatorConfiguration.fontSize; 9253 if(integratedTerminalEmulatorConfiguration.scaleFontSizeWithDpi) { 9254 static if(UsingSimpledisplayX11) { 9255 // if it is an xft font and xft is already scaled, we should NOT double scale. 9256 import std.algorithm; 9257 if(integratedTerminalEmulatorConfiguration.fontName.startsWith("core:")) { 9258 // core font doesn't use xft anyway 9259 fontSize = widget.scaleWithDpi(fontSize); 9260 } else { 9261 auto xft = getXftDpi(); 9262 if(xft is float.init) 9263 xft = 96; 9264 // the xft passed as assumed means it will figure that's what the size 9265 // is based on (which it is, inside xft) preventing the double scale problem 9266 fontSize = widget.scaleWithDpi(fontSize, cast(int) xft); 9267 9268 } 9269 } else { 9270 fontSize = widget.scaleWithDpi(fontSize); 9271 } 9272 } 9273 9274 if(integratedTerminalEmulatorConfiguration.fontName.length) { 9275 this.font = new OperatingSystemFont(integratedTerminalEmulatorConfiguration.fontName, fontSize, FontWeight.medium); 9276 if(this.font.isNull) { 9277 // carry on, it will try a default later 9278 } else if(this.font.isMonospace) { 9279 this.fontWidth = font.averageWidth; 9280 this.fontHeight = font.height; 9281 } else { 9282 this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again 9283 } 9284 } 9285 9286 if(this.font is null || this.font.isNull) 9287 loadDefaultFont(fontSize); 9288 } 9289 9290 private this(TerminalEmulatorWidget widget) { 9291 9292 this.syncSignal = new Semaphore(); 9293 this.outgoingSignal = new Semaphore(); 9294 this.incomingSignal = new Semaphore(); 9295 9296 this.widget = widget; 9297 9298 loadFont(); 9299 9300 super(integratedTerminalEmulatorConfiguration.initialWidth ? integratedTerminalEmulatorConfiguration.initialWidth : 80, 9301 integratedTerminalEmulatorConfiguration.initialHeight ? integratedTerminalEmulatorConfiguration.initialHeight : 30); 9302 9303 defaultForeground = integratedTerminalEmulatorConfiguration.defaultForeground; 9304 defaultBackground = integratedTerminalEmulatorConfiguration.defaultBackground; 9305 9306 bool skipNextChar = false; 9307 9308 widget.addEventListener((MouseDownEvent ev) { 9309 int termX = (ev.clientX - paddingLeft) / fontWidth; 9310 int termY = (ev.clientY - paddingTop) / fontHeight; 9311 9312 if((!mouseButtonTracking || selectiveMouseTracking || (ev.state & ModifierState.shift)) && ev.button == MouseButton.right) 9313 widget.showContextMenu(ev.clientX, ev.clientY); 9314 else 9315 if(sendMouseInputToApplication(termX, termY, 9316 arsd.terminalemulator.MouseEventType.buttonPressed, 9317 cast(arsd.terminalemulator.MouseButton) ev.button, 9318 (ev.state & ModifierState.shift) ? true : false, 9319 (ev.state & ModifierState.ctrl) ? true : false, 9320 (ev.state & ModifierState.alt) ? true : false 9321 )) 9322 redraw(); 9323 }); 9324 9325 widget.addEventListener((MouseUpEvent ev) { 9326 int termX = (ev.clientX - paddingLeft) / fontWidth; 9327 int termY = (ev.clientY - paddingTop) / fontHeight; 9328 9329 if(sendMouseInputToApplication(termX, termY, 9330 arsd.terminalemulator.MouseEventType.buttonReleased, 9331 cast(arsd.terminalemulator.MouseButton) ev.button, 9332 (ev.state & ModifierState.shift) ? true : false, 9333 (ev.state & ModifierState.ctrl) ? true : false, 9334 (ev.state & ModifierState.alt) ? true : false 9335 )) 9336 redraw(); 9337 }); 9338 9339 widget.addEventListener((MouseMoveEvent ev) { 9340 int termX = (ev.clientX - paddingLeft) / fontWidth; 9341 int termY = (ev.clientY - paddingTop) / fontHeight; 9342 9343 if(sendMouseInputToApplication(termX, termY, 9344 arsd.terminalemulator.MouseEventType.motion, 9345 (ev.state & ModifierState.leftButtonDown) ? arsd.terminalemulator.MouseButton.left 9346 : (ev.state & ModifierState.rightButtonDown) ? arsd.terminalemulator.MouseButton.right 9347 : (ev.state & ModifierState.middleButtonDown) ? arsd.terminalemulator.MouseButton.middle 9348 : cast(arsd.terminalemulator.MouseButton) 0, 9349 (ev.state & ModifierState.shift) ? true : false, 9350 (ev.state & ModifierState.ctrl) ? true : false, 9351 (ev.state & ModifierState.alt) ? true : false 9352 )) 9353 redraw(); 9354 }); 9355 9356 widget.addEventListener((KeyDownEvent ev) { 9357 if(ev.key == Key.C && !(ev.state & ModifierState.shift) && (ev.state & ModifierState.ctrl)) { 9358 if(integratedTerminalEmulatorConfiguration.ctrlCCopies) { 9359 goto copy; 9360 } 9361 } 9362 if(ev.key == Key.C && (ev.state & ModifierState.shift) && (ev.state & ModifierState.ctrl)) { 9363 if(integratedTerminalEmulatorConfiguration.ctrlCCopies) { 9364 sendSigInt(); 9365 skipNextChar = true; 9366 return; 9367 } 9368 // ctrl+c is cancel so ctrl+shift+c ends up doing copy. 9369 copy: 9370 copyToClipboard(getSelectedText()); 9371 skipNextChar = true; 9372 return; 9373 } 9374 if(ev.key == Key.Insert && (ev.state & ModifierState.ctrl)) { 9375 copyToClipboard(getSelectedText()); 9376 return; 9377 } 9378 9379 auto keyToSend = ev.key; 9380 9381 static if(UsingSimpledisplayX11) { 9382 if((ev.state & ModifierState.alt) && ev.originalKeyEvent.charsPossible.length) { 9383 keyToSend = cast(Key) ev.originalKeyEvent.charsPossible[0]; 9384 } 9385 } 9386 9387 defaultKeyHandler!(typeof(ev.key))( 9388 keyToSend 9389 , (ev.state & ModifierState.shift)?true:false 9390 , (ev.state & ModifierState.alt)?true:false 9391 , (ev.state & ModifierState.ctrl)?true:false 9392 , (ev.state & ModifierState.windows)?true:false 9393 ); 9394 9395 return; // the character event handler will do others 9396 }); 9397 9398 widget.addEventListener((CharEvent ev) { 9399 if(skipNextChar) { 9400 skipNextChar = false; 9401 return; 9402 } 9403 dchar c = ev.character; 9404 9405 if(c == 0x1c) /* ctrl+\, force quit */ { 9406 version(Posix) { 9407 import core.sys.posix.signal; 9408 if(widget is null || widget.term is null) { 9409 // the other thread must already be dead, so we can just close 9410 widget.parentWindow.close(); // I'm gonna let it segfault if this is null cuz like that isn't supposed to happen 9411 return; 9412 } 9413 } 9414 9415 terminateTerminalProcess(widget.term.threadId); 9416 } 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 */ { 9417 sendSigInt(); 9418 } else { 9419 defaultCharHandler(c); 9420 } 9421 }); 9422 } 9423 9424 void sendSigInt() { 9425 if(sigIntExtension) 9426 sigIntExtension(); 9427 9428 if(widget && widget.term) { 9429 widget.term.interrupted = true; 9430 outgoingSignal.notify(); 9431 } 9432 } 9433 9434 bool clearScreenRequested = true; 9435 void redraw() { 9436 if(widget.parentWindow is null || widget.parentWindow.win is null || widget.parentWindow.win.closed) 9437 return; 9438 9439 widget.redraw(); 9440 } 9441 9442 mixin SdpyDraw; 9443 } 9444 } else { 9445 /// 9446 enum IntegratedEmulator = false; 9447 } 9448 9449 /* 9450 void main() { 9451 auto terminal = Terminal(ConsoleOutputType.linear); 9452 terminal.setTrueColor(RGB(255, 0, 255), RGB(255, 255, 255)); 9453 terminal.writeln("Hello, world!"); 9454 } 9455 */ 9456 9457 private version(Windows) { 9458 pragma(lib, "user32"); 9459 import core.sys.windows.winbase; 9460 import core.sys.windows.winnt; 9461 9462 extern(Windows) 9463 HANDLE CreateNamedPipeA( 9464 const(char)* lpName, 9465 DWORD dwOpenMode, 9466 DWORD dwPipeMode, 9467 DWORD nMaxInstances, 9468 DWORD nOutBufferSize, 9469 DWORD nInBufferSize, 9470 DWORD nDefaultTimeOut, 9471 LPSECURITY_ATTRIBUTES lpSecurityAttributes 9472 ); 9473 9474 version(CRuntime_Microsoft) { 9475 extern(C) int _dup2(int, int); 9476 extern(C) int _fileno(FILE*); 9477 } 9478 } 9479 9480 /++ 9481 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. 9482 9483 Please note that not all keys may be accurately forwarded. It is not meant to be 100% comprehensive; that's for the window. 9484 9485 History: 9486 Added December 29, 2020. 9487 +/ 9488 static if(__traits(compiles, mixin(`{ static foreach(i; 0 .. 1) {} }`))) 9489 mixin(q{ 9490 auto SdpyIntegratedKeys(SimpleWindow)(SimpleWindow window) { 9491 struct impl { 9492 static import sdpy = arsd.simpledisplay; 9493 Terminal* terminal; 9494 RealTimeConsoleInput* rtti; 9495 typeof(RealTimeConsoleInput.init.integrateWithSimpleDisplayEventLoop(null)) listener; 9496 this(sdpy.SimpleWindow window) { 9497 terminal = new Terminal(ConsoleOutputType.linear); 9498 rtti = new RealTimeConsoleInput(terminal, ConsoleInputFlags.releasedKeys); 9499 listener = rtti.integrateWithSimpleDisplayEventLoop(delegate(InputEvent ie) { 9500 if(ie.type != InputEvent.Type.KeyboardEvent) 9501 return; 9502 auto kbd = ie.get!(InputEvent.Type.KeyboardEvent); 9503 if(window.handleKeyEvent !is null) { 9504 sdpy.KeyEvent ke; 9505 ke.pressed = kbd.pressed; 9506 if(kbd.modifierState & ModifierState.control) 9507 ke.modifierState |= sdpy.ModifierState.ctrl; 9508 if(kbd.modifierState & ModifierState.alt) 9509 ke.modifierState |= sdpy.ModifierState.alt; 9510 if(kbd.modifierState & ModifierState.shift) 9511 ke.modifierState |= sdpy.ModifierState.shift; 9512 9513 sw: switch(kbd.which) { 9514 case KeyboardEvent.Key.escape: ke.key = sdpy.Key.Escape; break; 9515 case KeyboardEvent.Key.F1: ke.key = sdpy.Key.F1; break; 9516 case KeyboardEvent.Key.F2: ke.key = sdpy.Key.F2; break; 9517 case KeyboardEvent.Key.F3: ke.key = sdpy.Key.F3; break; 9518 case KeyboardEvent.Key.F4: ke.key = sdpy.Key.F4; break; 9519 case KeyboardEvent.Key.F5: ke.key = sdpy.Key.F5; break; 9520 case KeyboardEvent.Key.F6: ke.key = sdpy.Key.F6; break; 9521 case KeyboardEvent.Key.F7: ke.key = sdpy.Key.F7; break; 9522 case KeyboardEvent.Key.F8: ke.key = sdpy.Key.F8; break; 9523 case KeyboardEvent.Key.F9: ke.key = sdpy.Key.F9; break; 9524 case KeyboardEvent.Key.F10: ke.key = sdpy.Key.F10; break; 9525 case KeyboardEvent.Key.F11: ke.key = sdpy.Key.F11; break; 9526 case KeyboardEvent.Key.F12: ke.key = sdpy.Key.F12; break; 9527 case KeyboardEvent.Key.LeftArrow: ke.key = sdpy.Key.Left; break; 9528 case KeyboardEvent.Key.RightArrow: ke.key = sdpy.Key.Right; break; 9529 case KeyboardEvent.Key.UpArrow: ke.key = sdpy.Key.Up; break; 9530 case KeyboardEvent.Key.DownArrow: ke.key = sdpy.Key.Down; break; 9531 case KeyboardEvent.Key.Insert: ke.key = sdpy.Key.Insert; break; 9532 case KeyboardEvent.Key.Delete: ke.key = sdpy.Key.Delete; break; 9533 case KeyboardEvent.Key.Home: ke.key = sdpy.Key.Home; break; 9534 case KeyboardEvent.Key.End: ke.key = sdpy.Key.End; break; 9535 case KeyboardEvent.Key.PageUp: ke.key = sdpy.Key.PageUp; break; 9536 case KeyboardEvent.Key.PageDown: ke.key = sdpy.Key.PageDown; break; 9537 case KeyboardEvent.Key.ScrollLock: ke.key = sdpy.Key.ScrollLock; break; 9538 9539 case '\r', '\n': ke.key = sdpy.Key.Enter; break; 9540 case '\t': ke.key = sdpy.Key.Tab; break; 9541 case ' ': ke.key = sdpy.Key.Space; break; 9542 case '\b': ke.key = sdpy.Key.Backspace; break; 9543 9544 case '`': ke.key = sdpy.Key.Grave; break; 9545 case '-': ke.key = sdpy.Key.Dash; break; 9546 case '=': ke.key = sdpy.Key.Equals; break; 9547 case '[': ke.key = sdpy.Key.LeftBracket; break; 9548 case ']': ke.key = sdpy.Key.RightBracket; break; 9549 case '\\': ke.key = sdpy.Key.Backslash; break; 9550 case ';': ke.key = sdpy.Key.Semicolon; break; 9551 case '\'': ke.key = sdpy.Key.Apostrophe; break; 9552 case ',': ke.key = sdpy.Key.Comma; break; 9553 case '.': ke.key = sdpy.Key.Period; break; 9554 case '/': ke.key = sdpy.Key.Slash; break; 9555 9556 static foreach(ch; 'A' .. ('Z' + 1)) { 9557 case ch, ch + 32: 9558 version(Windows) 9559 ke.key = cast(sdpy.Key) ch; 9560 else 9561 ke.key = cast(sdpy.Key) (ch + 32); 9562 break sw; 9563 } 9564 static foreach(ch; '0' .. ('9' + 1)) { 9565 case ch: 9566 ke.key = cast(sdpy.Key) ch; 9567 break sw; 9568 } 9569 9570 default: 9571 } 9572 9573 // I'm tempted to leave the window null since it didn't originate from here 9574 // or maybe set a ModifierState.... 9575 //ke.window = window; 9576 9577 window.handleKeyEvent(ke); 9578 } 9579 if(window.handleCharEvent !is null) { 9580 if(kbd.isCharacter) 9581 window.handleCharEvent(kbd.which); 9582 } 9583 }); 9584 } 9585 ~this() { 9586 listener.dispose(); 9587 .destroy(*rtti); 9588 .destroy(*terminal); 9589 rtti = null; 9590 terminal = null; 9591 } 9592 } 9593 return impl(window); 9594 } 9595 }); 9596 9597 9598 /* 9599 ONLY SUPPORTED ON MY TERMINAL EMULATOR IN GENERAL 9600 9601 bracketed section can collapse and scroll independently in the TE. may also pop out into a window (possibly with a comparison window) 9602 9603 hyperlink can either just indicate something to the TE to handle externally 9604 OR 9605 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. 9606 9607 internally it can set two bits: one indicates it is a hyperlink, the other just flips each use to separate consecutive sequences. 9608 9609 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. 9610 9611 9612 I could spread a unique id number across bits, one bit per char so the memory isn't too bad. 9613 so it would set a number and a word. this is sent back to the application to handle internally. 9614 9615 1) turn on special input 9616 2) turn off special input 9617 3) special input sends a paste event with a number and the text 9618 4) to make a link, you write out the begin sequence, the text, and the end sequence. including the magic number somewhere. 9619 magic number is allowed to have one bit per char. the terminal discards anything else. terminal.d api will enforce. 9620 9621 if magic number is zero, it is not sent in the paste event. maybe. 9622 9623 or if it is like 255, it is handled as a url and opened externally 9624 tho tbh a url could just be detected by regex pattern 9625 9626 9627 NOTE: if your program requests mouse input, the TE does not process it! Thus the user will have to shift+click for it. 9628 9629 mode 3004 for bracketed hyperlink 9630 9631 hyperlink sequence: \033[?220hnum;text\033[?220l~ 9632 9633 */