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