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