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