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