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