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