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