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