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