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