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