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