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