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