The OpenD Programming Language

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