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