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