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