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