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