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