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