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