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