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