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