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