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