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