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