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 	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].
6 
7 
8 	The main interface for this module is the Terminal struct, which
9 	encapsulates the output functions and line-buffered input of the terminal, and
10 	RealTimeConsoleInput, which gives real time input.
11 	
12 	Creating an instance of these structs will perform console initialization. When the struct
13 	goes out of scope, any changes in console settings will be automatically reverted.
14 
15 	Note: on Posix, it traps SIGINT and translates it into an input event. You should
16 	keep your event loop moving and keep an eye open for this to exit cleanly; simply break
17 	your event loop upon receiving a UserInterruptionEvent. (Without
18 	the signal handler, ctrl+c can leave your terminal in a bizarre state.)
19 
20 	As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\
21 
22 	On old Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work. Most functions basically
23 	work now with newer Mac OS versions though.
24 
25 	Future_Roadmap:
26 	$(LIST
27 		* The CharacterEvent and NonCharacterKeyEvent types will be removed. Instead, use KeyboardEvent
28 		  on new programs.
29 
30 		* The ScrollbackBuffer will be expanded to be easier to use to partition your screen. It might even
31 		  handle input events of some sort. Its API may change.
32 
33 		* getline I want to be really easy to use both for code and end users. It will need multi-line support
34 		  eventually.
35 
36 		* 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.)
37 
38 		* More advanced terminal features as functions, where available, like cursor changing and full-color functions.
39 
40 		* More documentation.
41 	)
42 
43 	WHAT I WON'T DO:
44 	$(LIST
45 		* support everything under the sun. If it isn't default-installed on an OS I or significant number of other people
46 		  might actually use, and isn't written by me, I don't really care about it. This means the only supported terminals are:
47 		  $(LIST
48 
49 		  * xterm (and decently xterm compatible emulators like Konsole)
50 		  * Windows console
51 		  * rxvt (to a lesser extent)
52 		  * Linux console
53 		  * My terminal emulator family of applications https://github.com/adamdruppe/terminal-emulator
54 		  )
55 
56 		  Anything else is cool if it does work, but I don't want to go out of my way for it.
57 
58 		* Use other libraries, unless strictly optional. terminal.d is a stand-alone module by default and
59 		  always will be.
60 
61 		* Do a full TUI widget set. I might do some basics and lay a little groundwork, but a full TUI
62 		  is outside the scope of this module (unless I can do it really small.)
63 	)
64 +/
65 module arsd.terminal;
66 
67 // FIXME: needs to support VT output on Windows too in certain situations
68 // 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.
69 
70 /++
71 	$(H3 Get Line)
72 
73 	This example will demonstrate the high-level getline interface.
74 
75 	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.
76 +/
77 version(demos) unittest {
78 	import arsd.terminal;
79 
80 	void main() {
81 		auto terminal = Terminal(ConsoleOutputType.linear);
82 		string line = terminal.getline();
83 		terminal.writeln("You wrote: ", line);
84 	}
85 
86 	main; // exclude from docs
87 }
88 
89 /++
90 	$(H3 Color)
91 
92 	This example demonstrates color output, using [Terminal.color]
93 	and the output functions like [Terminal.writeln].
94 +/
95 version(demos) unittest {
96 	import arsd.terminal;
97 
98 	void main() {
99 		auto terminal = Terminal(ConsoleOutputType.linear);
100 		terminal.color(Color.green, Color.black);
101 		terminal.writeln("Hello world, in green on black!");
102 		terminal.color(Color.DEFAULT, Color.DEFAULT);
103 		terminal.writeln("And back to normal.");
104 	}
105 
106 	main; // exclude from docs
107 }
108 
109 /++
110 	$(H3 Single Key)
111 
112 	This shows how to get one single character press using
113 	the [RealTimeConsoleInput] structure.
114 +/
115 version(demos) unittest {
116 	import arsd.terminal;
117 
118 	void main() {
119 		auto terminal = Terminal(ConsoleOutputType.linear);
120 		auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw);
121 
122 		terminal.writeln("Press any key to continue...");
123 		auto ch = input.getch();
124 		terminal.writeln("You pressed ", ch);
125 	}
126 
127 	main; // exclude from docs
128 }
129 
130 /*
131 	Widgets:
132 		tab widget
133 		scrollback buffer
134 		partitioned canvas
135 */
136 
137 // FIXME: ctrl+d eof on stdin
138 
139 // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx
140 
141 
142 /++
143 	A function the sigint handler will call (if overridden - which is the
144 	case when [RealTimeConsoleInput] is active on Posix or if you compile with
145 	`TerminalDirectToEmulator` version on any platform at this time) in addition
146 	to the library's default handling, which is to set a flag for the event loop
147 	to inform you.
148 
149 	Remember, this is called from a signal handler and/or from a separate thread,
150 	so you are not allowed to do much with it and need care when setting TLS variables.
151 
152 	I suggest you only set a `__gshared bool` flag as many other operations will risk
153 	undefined behavior.
154 
155 	$(WARNING
156 		This function is never called on the default Windows console
157 		configuration in the current implementation. You can use
158 		`-version=TerminalDirectToEmulator` to guarantee it is called there
159 		too by causing the library to pop up a gui window for your application.
160 	)
161 
162 	History:
163 		Added March 30, 2020. Included in release v7.1.0.
164 
165 +/
166 __gshared void delegate() nothrow @nogc sigIntExtension;
167 
168 
169 version(TerminalDirectToEmulator) {
170 	version=WithEncapsulatedSignals;
171 }
172 
173 version(Posix) {
174 	enum SIGWINCH = 28;
175 	__gshared bool windowSizeChanged = false;
176 	__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
177 	__gshared bool hangedUp = false; /// similar to interrupted.
178 	version=WithSignals;
179 
180 	version(with_eventloop)
181 		struct SignalFired {}
182 
183 	extern(C)
184 	void sizeSignalHandler(int sigNumber) nothrow {
185 		windowSizeChanged = true;
186 		version(with_eventloop) {
187 			import arsd.eventloop;
188 			try
189 				send(SignalFired());
190 			catch(Exception) {}
191 		}
192 	}
193 	extern(C)
194 	void interruptSignalHandler(int sigNumber) nothrow {
195 		interrupted = true;
196 		version(with_eventloop) {
197 			import arsd.eventloop;
198 			try
199 				send(SignalFired());
200 			catch(Exception) {}
201 		}
202 
203 		if(sigIntExtension)
204 			sigIntExtension();
205 	}
206 	extern(C)
207 	void hangupSignalHandler(int sigNumber) nothrow {
208 		hangedUp = true;
209 		version(with_eventloop) {
210 			import arsd.eventloop;
211 			try
212 				send(SignalFired());
213 			catch(Exception) {}
214 		}
215 	}
216 }
217 
218 // parts of this were taken from Robik's ConsoleD
219 // https://github.com/robik/ConsoleD/blob/master/consoled.d
220 
221 // Uncomment this line to get a main() to demonstrate this module's
222 // capabilities.
223 //version = Demo
224 
225 version(TerminalDirectToEmulator) {
226 	version=VtEscapeCodes;
227 } else version(Windows) {
228 	version(VtEscapeCodes) {} // cool
229 	version=Win32Console;
230 }
231 
232 version(Windows)
233 	import core.sys.windows.windows;
234 
235 version(Win32Console) {
236 	private {
237 		enum RED_BIT = 4;
238 		enum GREEN_BIT = 2;
239 		enum BLUE_BIT = 1;
240 	}
241 
242 	pragma(lib, "user32");
243 }
244 
245 version(Posix) {
246 
247 	version=VtEscapeCodes;
248 
249 	import core.sys.posix.termios;
250 	import core.sys.posix.unistd;
251 	import unix = core.sys.posix.unistd;
252 	import core.sys.posix.sys.types;
253 	import core.sys.posix.sys.time;
254 	import core.stdc.stdio;
255 
256 	import core.sys.posix.sys.ioctl;
257 }
258 
259 version(VtEscapeCodes) {
260 
261 	enum UseVtSequences = true;
262 
263 	version(TerminalDirectToEmulator) {
264 		private {
265 			enum RED_BIT = 1;
266 			enum GREEN_BIT = 2;
267 			enum BLUE_BIT = 4;
268 		}
269 	} else version(Windows) {} else
270 	private {
271 		enum RED_BIT = 1;
272 		enum GREEN_BIT = 2;
273 		enum BLUE_BIT = 4;
274 	}
275 
276 	struct winsize {
277 		ushort ws_row;
278 		ushort ws_col;
279 		ushort ws_xpixel;
280 		ushort ws_ypixel;
281 	}
282 
283 	// 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).
284 
285 	// this way we'll have some definitions for 99% of typical PC cases even without any help from the local operating system
286 
287 	enum string builtinTermcap = `
288 # Generic VT entry.
289 vg|vt-generic|Generic VT entries:\
290 	:bs:mi:ms:pt:xn:xo:it#8:\
291 	:RA=\E[?7l:SA=\E?7h:\
292 	:bl=^G:cr=^M:ta=^I:\
293 	:cm=\E[%i%d;%dH:\
294 	:le=^H:up=\E[A:do=\E[B:nd=\E[C:\
295 	:LE=\E[%dD:RI=\E[%dC:UP=\E[%dA:DO=\E[%dB:\
296 	:ho=\E[H:cl=\E[H\E[2J:ce=\E[K:cb=\E[1K:cd=\E[J:sf=\ED:sr=\EM:\
297 	:ct=\E[3g:st=\EH:\
298 	:cs=\E[%i%d;%dr:sc=\E7:rc=\E8:\
299 	:ei=\E[4l:ic=\E[@:IC=\E[%d@:al=\E[L:AL=\E[%dL:\
300 	:dc=\E[P:DC=\E[%dP:dl=\E[M:DL=\E[%dM:\
301 	:so=\E[7m:se=\E[m:us=\E[4m:ue=\E[m:\
302 	:mb=\E[5m:mh=\E[2m:md=\E[1m:mr=\E[7m:me=\E[m:\
303 	:sc=\E7:rc=\E8:kb=\177:\
304 	:ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D:
305 
306 
307 # Slackware 3.1 linux termcap entry (Sat Apr 27 23:03:58 CDT 1996):
308 lx|linux|console|con80x25|LINUX System Console:\
309         :do=^J:co#80:li#25:cl=\E[H\E[J:sf=\ED:sb=\EM:\
310         :le=^H:bs:am:cm=\E[%i%d;%dH:nd=\E[C:up=\E[A:\
311         :ce=\E[K:cd=\E[J:so=\E[7m:se=\E[27m:us=\E[36m:ue=\E[m:\
312         :md=\E[1m:mr=\E[7m:mb=\E[5m:me=\E[m:is=\E[1;25r\E[25;1H:\
313         :ll=\E[1;25r\E[25;1H:al=\E[L:dc=\E[P:dl=\E[M:\
314         :it#8:ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D:kb=^H:ti=\E[r\E[H:\
315         :ho=\E[H:kP=\E[5~:kN=\E[6~:kH=\E[4~:kh=\E[1~:kD=\E[3~:kI=\E[2~:\
316         :k1=\E[[A:k2=\E[[B:k3=\E[[C:k4=\E[[D:k5=\E[[E:k6=\E[17~:\
317 	:F1=\E[23~:F2=\E[24~:\
318         :k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:K1=\E[1~:K2=\E[5~:\
319         :K4=\E[4~:K5=\E[6~:\
320         :pt:sr=\EM:vt#3:xn:km:bl=^G:vi=\E[?25l:ve=\E[?25h:vs=\E[?25h:\
321         :sc=\E7:rc=\E8:cs=\E[%i%d;%dr:\
322         :r1=\Ec:r2=\Ec:r3=\Ec:
323 
324 # Some other, commonly used linux console entries.
325 lx|con80x28:co#80:li#28:tc=linux:
326 lx|con80x43:co#80:li#43:tc=linux:
327 lx|con80x50:co#80:li#50:tc=linux:
328 lx|con100x37:co#100:li#37:tc=linux:
329 lx|con100x40:co#100:li#40:tc=linux:
330 lx|con132x43:co#132:li#43:tc=linux:
331 
332 # vt102 - vt100 + insert line etc. VT102 does not have insert character.
333 v2|vt102|DEC vt102 compatible:\
334 	:co#80:li#24:\
335 	:ic@:IC@:\
336 	:is=\E[m\E[?1l\E>:\
337 	:rs=\E[m\E[?1l\E>:\
338 	:eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\
339 	:ks=:ke=:\
340 	:k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:\
341 	:tc=vt-generic:
342 
343 # vt100 - really vt102 without insert line, insert char etc.
344 vt|vt100|DEC vt100 compatible:\
345 	:im@:mi@:al@:dl@:ic@:dc@:AL@:DL@:IC@:DC@:\
346 	:tc=vt102:
347 
348 
349 # Entry for an xterm. Insert mode has been disabled.
350 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):\
351 	:am:bs:mi@:km:co#80:li#55:\
352 	:im@:ei@:\
353 	:cl=\E[H\E[J:\
354 	:ct=\E[3k:ue=\E[m:\
355 	:is=\E[m\E[?1l\E>:\
356 	:rs=\E[m\E[?1l\E>:\
357 	:vi=\E[?25l:ve=\E[?25h:\
358 	:eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\
359 	:kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\
360 	:k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:\
361 	:k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\
362 	:F1=\E[23~:F2=\E[24~:\
363 	:kh=\E[H:kH=\E[F:\
364 	:ks=:ke=:\
365 	:te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\
366 	:tc=vt-generic:
367 
368 
369 #rxvt, added by me
370 rxvt|rxvt-unicode|rxvt-unicode-256color:\
371 	:am:bs:mi@:km:co#80:li#55:\
372 	:im@:ei@:\
373 	:ct=\E[3k:ue=\E[m:\
374 	:is=\E[m\E[?1l\E>:\
375 	:rs=\E[m\E[?1l\E>:\
376 	:vi=\E[?25l:\
377 	:ve=\E[?25h:\
378 	:eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\
379 	:kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\
380 	:k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\
381 	:k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\
382 	:F1=\E[23~:F2=\E[24~:\
383 	:kh=\E[7~:kH=\E[8~:\
384 	:ks=:ke=:\
385 	:te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\
386 	:tc=vt-generic:
387 
388 
389 # Some other entries for the same xterm.
390 v2|xterms|vs100s|xterm small window:\
391 	:co#80:li#24:tc=xterm:
392 vb|xterm-bold|xterm with bold instead of underline:\
393 	:us=\E[1m:tc=xterm:
394 vi|xterm-ins|xterm with insert mode:\
395 	:mi:im=\E[4h:ei=\E[4l:tc=xterm:
396 
397 Eterm|Eterm Terminal Emulator (X11 Window System):\
398         :am:bw:eo:km:mi:ms:xn:xo:\
399         :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:\
400         :AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:IC=\E[%d@:\
401         :K1=\E[7~:K2=\EOu:K3=\E[5~:K4=\E[8~:K5=\E[6~:LE=\E[%dD:\
402         :RI=\E[%dC:UP=\E[%dA:ae=^O:al=\E[L:as=^N:bl=^G:cd=\E[J:\
403         :ce=\E[K:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=^M:\
404         :cs=\E[%i%d;%dr:ct=\E[3g:dc=\E[P:dl=\E[M:do=\E[B:\
405         :ec=\E[%dX:ei=\E[4l:ho=\E[H:i1=\E[?47l\E>\E[?1l:ic=\E[@:\
406         :im=\E[4h:is=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l:\
407         :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\
408         :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:kD=\E[3~:\
409         :kI=\E[2~:kN=\E[6~:kP=\E[5~:kb=^H:kd=\E[B:ke=:kh=\E[7~:\
410         :kl=\E[D:kr=\E[C:ks=:ku=\E[A:le=^H:mb=\E[5m:md=\E[1m:\
411         :me=\E[m\017:mr=\E[7m:nd=\E[C:rc=\E8:\
412         :sc=\E7:se=\E[27m:sf=^J:so=\E[7m:sr=\EM:st=\EH:ta=^I:\
413         :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:ue=\E[24m:up=\E[A:\
414         :us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?25h:vi=\E[?25l:\
415         :ac=aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~:
416 
417 # DOS terminal emulator such as Telix or TeleMate.
418 # This probably also works for the SCO console, though it's incomplete.
419 an|ansi|ansi-bbs|ANSI terminals (emulators):\
420 	:co#80:li#24:am:\
421 	:is=:rs=\Ec:kb=^H:\
422 	:as=\E[m:ae=:eA=:\
423 	:ac=0\333+\257,\256.\031-\030a\261f\370g\361j\331k\277l\332m\300n\305q\304t\264u\303v\301w\302x\263~\025:\
424 	:kD=\177:kH=\E[Y:kN=\E[U:kP=\E[V:kh=\E[H:\
425 	:k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\EOT:\
426 	:k6=\EOU:k7=\EOV:k8=\EOW:k9=\EOX:k0=\EOY:\
427 	:tc=vt-generic:
428 
429 	`;
430 } else {
431 	enum UseVtSequences = false;
432 }
433 
434 /// A modifier for [Color]
435 enum Bright = 0x08;
436 
437 /// Defines the list of standard colors understood by Terminal.
438 /// See also: [Bright]
439 enum Color : ushort {
440 	black = 0, /// .
441 	red = RED_BIT, /// .
442 	green = GREEN_BIT, /// .
443 	yellow = red | green, /// .
444 	blue = BLUE_BIT, /// .
445 	magenta = red | blue, /// .
446 	cyan = blue | green, /// .
447 	white = red | green | blue, /// .
448 	DEFAULT = 256,
449 }
450 
451 /// When capturing input, what events are you interested in?
452 ///
453 /// Note: these flags can be OR'd together to select more than one option at a time.
454 ///
455 /// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw.
456 /// 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.
457 enum ConsoleInputFlags {
458 	raw = 0, /// raw input returns keystrokes immediately, without line buffering
459 	echo = 1, /// do you want to automatically echo input back to the user?
460 	mouse = 2, /// capture mouse events
461 	paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes)
462 	size = 8, /// window resize events
463 
464 	releasedKeys = 64, /// key release events. Not reliable on Posix.
465 
466 	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.
467 	allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events.
468 
469 	noEolWrap = 128,
470 }
471 
472 /// Defines how terminal output should be handled.
473 enum ConsoleOutputType {
474 	linear = 0, /// do you want output to work one line at a time?
475 	cellular = 1, /// or do you want access to the terminal screen as a grid of characters?
476 	//truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges
477 
478 	minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here
479 }
480 
481 alias ConsoleOutputMode = ConsoleOutputType;
482 
483 /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present
484 enum ForceOption {
485 	automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right)
486 	neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution.
487 	alwaysSend = 1, /// always send the data, even if it doesn't seem necessary
488 }
489 
490 ///
491 enum TerminalCursor {
492 	DEFAULT = 0, ///
493 	insert = 1, ///
494 	block = 2 ///
495 }
496 
497 // we could do it with termcap too, getenv("TERMCAP") then split on : and replace \E with \033 and get the pieces
498 
499 /// Encapsulates the I/O capabilities of a terminal.
500 ///
501 /// Warning: do not write out escape sequences to the terminal. This won't work
502 /// on Windows and will confuse Terminal's internal state on Posix.
503 struct Terminal {
504 	///
505 	@disable this();
506 	@disable this(this);
507 	private ConsoleOutputType type;
508 
509 	version(TerminalDirectToEmulator) {
510 		private bool windowSizeChanged = false;
511 		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
512 		private bool hangedUp = false; /// similar to interrupted.
513 	}
514 
515 	private TerminalCursor currentCursor_;
516 	version(Windows) private CONSOLE_CURSOR_INFO originalCursorInfo;
517 
518 	/++
519 		Changes the current cursor.
520 	+/
521 	void cursor(TerminalCursor what, ForceOption force = ForceOption.automatic) {
522 		if(force == ForceOption.neverSend) {
523 			currentCursor_ = what;
524 			return;
525 		} else {
526 			if(what != currentCursor_ || force == ForceOption.alwaysSend) {
527 				currentCursor_ = what;
528 				version(Win32Console) {
529 					final switch(what) {
530 						case TerminalCursor.DEFAULT:
531 							SetConsoleCursorInfo(hConsole, &originalCursorInfo);
532 						break;
533 						case TerminalCursor.insert:
534 						case TerminalCursor.block:
535 							CONSOLE_CURSOR_INFO info;
536 							GetConsoleCursorInfo(hConsole, &info);
537 							info.dwSize = what == TerminalCursor.insert ? 1 : 100;
538 							SetConsoleCursorInfo(hConsole, &info);
539 						break;
540 					}
541 				} else {
542 					final switch(what) {
543 						case TerminalCursor.DEFAULT:
544 							if(terminalInFamily("linux"))
545 								writeStringRaw("\033[?0c");
546 							else
547 								writeStringRaw("\033[0 q");
548 						break;
549 						case TerminalCursor.insert:
550 							if(terminalInFamily("linux"))
551 								writeStringRaw("\033[?2c");
552 							else if(terminalInFamily("xterm"))
553 								writeStringRaw("\033[6 q");
554 							else
555 								writeStringRaw("\033[4 q");
556 						break;
557 						case TerminalCursor.block:
558 							if(terminalInFamily("linux"))
559 								writeStringRaw("\033[?6c");
560 							else
561 								writeStringRaw("\033[2 q");
562 						break;
563 					}
564 				}
565 			}
566 		}
567 	}
568 
569 	/++
570 		Terminal is only valid to use on an actual console device or terminal
571 		handle. You should not attempt to construct a Terminal instance if this
572 		returns false. Real time input is similarly impossible if `!stdinIsTerminal`.
573 	+/
574 	static bool stdoutIsTerminal() {
575 		version(TerminalDirectToEmulator) {
576 			version(Windows) {
577 				// if it is null, it was a gui subsystem exe. But otherwise, it
578 				// might be explicitly redirected and we should respect that for
579 				// compatibility with normal console expectations (even though like
580 				// we COULD pop up a gui and do both, really that isn't the normal
581 				// use of this library so don't wanna go too nuts)
582 				auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
583 				return hConsole is null || GetFileType(hConsole) == FILE_TYPE_CHAR;
584 			} else version(Posix) {
585 				// same as normal here since thee is no gui subsystem really
586 				import core.sys.posix.unistd;
587 				return cast(bool) isatty(1);
588 			} else static assert(0);
589 		} else version(Posix) {
590 			import core.sys.posix.unistd;
591 			return cast(bool) isatty(1);
592 		} else version(Win32Console) {
593 			auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
594 			return GetFileType(hConsole) == FILE_TYPE_CHAR;
595 			/+
596 			auto hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
597 			CONSOLE_SCREEN_BUFFER_INFO originalSbi;
598 			if(GetConsoleScreenBufferInfo(hConsole, &originalSbi) == 0)
599 				return false;
600 			else
601 				return true;
602 			+/
603 		} else static assert(0);
604 	}
605 
606 	///
607 	static bool stdinIsTerminal() {
608 		version(TerminalDirectToEmulator) {
609 			version(Windows) {
610 				auto hConsole = GetStdHandle(STD_INPUT_HANDLE);
611 				return hConsole is null || GetFileType(hConsole) == FILE_TYPE_CHAR;
612 			} else version(Posix) {
613 				// same as normal here since thee is no gui subsystem really
614 				import core.sys.posix.unistd;
615 				return cast(bool) isatty(0);
616 			} else static assert(0);
617 		} else version(Posix) {
618 			import core.sys.posix.unistd;
619 			return cast(bool) isatty(0);
620 		} else version(Win32Console) {
621 			auto hConsole = GetStdHandle(STD_INPUT_HANDLE);
622 			return GetFileType(hConsole) == FILE_TYPE_CHAR;
623 		} else static assert(0);
624 	}
625 
626 	version(Posix) {
627 		private int fdOut;
628 		private int fdIn;
629 		private int[] delegate() getSizeOverride;
630 		void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically
631 	}
632 
633 	bool terminalInFamily(string[] terms...) {
634 		import std.process;
635 		import std.string;
636 		version(TerminalDirectToEmulator)
637 			auto term = "xterm";
638 		else
639 			auto term = environment.get("TERM");
640 		foreach(t; terms)
641 			if(indexOf(term, t) != -1)
642 				return true;
643 
644 		return false;
645 	}
646 
647 	version(Posix) {
648 		// This is a filthy hack because Terminal.app and OS X are garbage who don't
649 		// work the way they're advertised. I just have to best-guess hack and hope it
650 		// doesn't break anything else. (If you know a better way, let me know!)
651 		bool isMacTerminal() {
652 			// it gives 1,2 in getTerminalCapabilities...
653 			// FIXME
654 			import std.process;
655 			import std.string;
656 			auto term = environment.get("TERM");
657 			return term == "xterm-256color";
658 		}
659 	} else
660 		bool isMacTerminal() { return false; }
661 
662 	static string[string] termcapDatabase;
663 	static void readTermcapFile(bool useBuiltinTermcap = false) {
664 		import std.file;
665 		import std.stdio;
666 		import std.string;
667 
668 		//if(!exists("/etc/termcap"))
669 			useBuiltinTermcap = true;
670 
671 		string current;
672 
673 		void commitCurrentEntry() {
674 			if(current is null)
675 				return;
676 
677 			string names = current;
678 			auto idx = indexOf(names, ":");
679 			if(idx != -1)
680 				names = names[0 .. idx];
681 
682 			foreach(name; split(names, "|"))
683 				termcapDatabase[name] = current;
684 
685 			current = null;
686 		}
687 
688 		void handleTermcapLine(in char[] line) {
689 			if(line.length == 0) { // blank
690 				commitCurrentEntry();
691 				return; // continue
692 			}
693 			if(line[0] == '#') // comment
694 				return; // continue
695 			size_t termination = line.length;
696 			if(line[$-1] == '\\')
697 				termination--; // cut off the \\
698 			current ~= strip(line[0 .. termination]);
699 			// termcap entries must be on one logical line, so if it isn't continued, we know we're done
700 			if(line[$-1] != '\\')
701 				commitCurrentEntry();
702 		}
703 
704 		if(useBuiltinTermcap) {
705 			version(VtEscapeCodes)
706 			foreach(line; splitLines(builtinTermcap)) {
707 				handleTermcapLine(line);
708 			}
709 		} else {
710 			foreach(line; File("/etc/termcap").byLine()) {
711 				handleTermcapLine(line);
712 			}
713 		}
714 	}
715 
716 	static string getTermcapDatabase(string terminal) {
717 		import std.string;
718 
719 		if(termcapDatabase is null)
720 			readTermcapFile();
721 
722 		auto data = terminal in termcapDatabase;
723 		if(data is null)
724 			return null;
725 
726 		auto tc = *data;
727 		auto more = indexOf(tc, ":tc=");
728 		if(more != -1) {
729 			auto tcKey = tc[more + ":tc=".length .. $];
730 			auto end = indexOf(tcKey, ":");
731 			if(end != -1)
732 				tcKey = tcKey[0 .. end];
733 			tc = getTermcapDatabase(tcKey) ~ tc;
734 		}
735 
736 		return tc;
737 	}
738 
739 	string[string] termcap;
740 	void readTermcap(string t = null) {
741 		version(TerminalDirectToEmulator)
742 		if(usingDirectEmulator)
743 			t = "xterm";
744 		import std.process;
745 		import std.string;
746 		import std.array;
747 
748 		string termcapData = environment.get("TERMCAP");
749 		if(termcapData.length == 0) {
750 			if(t is null) {
751 				t = environment.get("TERM");
752 			}
753 
754 			// loosen the check so any xterm variety gets
755 			// the same termcap. odds are this is right
756 			// almost always
757 			if(t.indexOf("xterm") != -1)
758 				t = "xterm";
759 			if(t.indexOf("putty") != -1)
760 				t = "xterm";
761 			if(t.indexOf("tmux") != -1)
762 				t = "tmux";
763 			if(t.indexOf("screen") != -1)
764 				t = "screen";
765 
766 			termcapData = getTermcapDatabase(t);
767 		}
768 
769 		auto e = replace(termcapData, "\\\n", "\n");
770 		termcap = null;
771 
772 		foreach(part; split(e, ":")) {
773 			// FIXME: handle numeric things too
774 
775 			auto things = split(part, "=");
776 			if(things.length)
777 				termcap[things[0]] =
778 					things.length > 1 ? things[1] : null;
779 		}
780 	}
781 
782 	string findSequenceInTermcap(in char[] sequenceIn) {
783 		char[10] sequenceBuffer;
784 		char[] sequence;
785 		if(sequenceIn.length > 0 && sequenceIn[0] == '\033') {
786 			if(!(sequenceIn.length < sequenceBuffer.length - 1))
787 				return null;
788 			sequenceBuffer[1 .. sequenceIn.length + 1] = sequenceIn[];
789 			sequenceBuffer[0] = '\\';
790 			sequenceBuffer[1] = 'E';
791 			sequence = sequenceBuffer[0 .. sequenceIn.length + 1];
792 		} else {
793 			sequence = sequenceBuffer[1 .. sequenceIn.length + 1];
794 		}
795 
796 		import std.array;
797 		foreach(k, v; termcap)
798 			if(v == sequence)
799 				return k;
800 		return null;
801 	}
802 
803 	string getTermcap(string key) {
804 		auto k = key in termcap;
805 		if(k !is null) return *k;
806 		return null;
807 	}
808 
809 	// Looks up a termcap item and tries to execute it. Returns false on failure
810 	bool doTermcap(T...)(string key, T t) {
811 		import std.conv;
812 		auto fs = getTermcap(key);
813 		if(fs is null)
814 			return false;
815 
816 		int swapNextTwo = 0;
817 
818 		R getArg(R)(int idx) {
819 			if(swapNextTwo == 2) {
820 				idx ++;
821 				swapNextTwo--;
822 			} else if(swapNextTwo == 1) {
823 				idx --;
824 				swapNextTwo--;
825 			}
826 
827 			foreach(i, arg; t) {
828 				if(i == idx)
829 					return to!R(arg);
830 			}
831 			assert(0, to!string(idx) ~ " is out of bounds working " ~ fs);
832 		}
833 
834 		char[256] buffer;
835 		int bufferPos = 0;
836 
837 		void addChar(char c) {
838 			import std.exception;
839 			enforce(bufferPos < buffer.length);
840 			buffer[bufferPos++] = c;
841 		}
842 
843 		void addString(in char[] c) {
844 			import std.exception;
845 			enforce(bufferPos + c.length < buffer.length);
846 			buffer[bufferPos .. bufferPos + c.length] = c[];
847 			bufferPos += c.length;
848 		}
849 
850 		void addInt(int c, int minSize) {
851 			import std.string;
852 			auto str = format("%0"~(minSize ? to!string(minSize) : "")~"d", c);
853 			addString(str);
854 		}
855 
856 		bool inPercent;
857 		int argPosition = 0;
858 		int incrementParams = 0;
859 		bool skipNext;
860 		bool nextIsChar;
861 		bool inBackslash;
862 
863 		foreach(char c; fs) {
864 			if(inBackslash) {
865 				if(c == 'E')
866 					addChar('\033');
867 				else
868 					addChar(c);
869 				inBackslash = false;
870 			} else if(nextIsChar) {
871 				if(skipNext)
872 					skipNext = false;
873 				else
874 					addChar(cast(char) (c + getArg!int(argPosition) + (incrementParams ? 1 : 0)));
875 				if(incrementParams) incrementParams--;
876 				argPosition++;
877 				inPercent = false;
878 			} else if(inPercent) {
879 				switch(c) {
880 					case '%':
881 						addChar('%');
882 						inPercent = false;
883 					break;
884 					case '2':
885 					case '3':
886 					case 'd':
887 						if(skipNext)
888 							skipNext = false;
889 						else
890 							addInt(getArg!int(argPosition) + (incrementParams ? 1 : 0),
891 								c == 'd' ? 0 : (c - '0')
892 							);
893 						if(incrementParams) incrementParams--;
894 						argPosition++;
895 						inPercent = false;
896 					break;
897 					case '.':
898 						if(skipNext)
899 							skipNext = false;
900 						else
901 							addChar(cast(char) (getArg!int(argPosition) + (incrementParams ? 1 : 0)));
902 						if(incrementParams) incrementParams--;
903 						argPosition++;
904 					break;
905 					case '+':
906 						nextIsChar = true;
907 						inPercent = false;
908 					break;
909 					case 'i':
910 						incrementParams = 2;
911 						inPercent = false;
912 					break;
913 					case 's':
914 						skipNext = true;
915 						inPercent = false;
916 					break;
917 					case 'b':
918 						argPosition--;
919 						inPercent = false;
920 					break;
921 					case 'r':
922 						swapNextTwo = 2;
923 						inPercent = false;
924 					break;
925 					// FIXME: there's more
926 					// http://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html
927 
928 					default:
929 						assert(0, "not supported " ~ c);
930 				}
931 			} else {
932 				if(c == '%')
933 					inPercent = true;
934 				else if(c == '\\')
935 					inBackslash = true;
936 				else
937 					addChar(c);
938 			}
939 		}
940 
941 		writeStringRaw(buffer[0 .. bufferPos]);
942 		return true;
943 	}
944 
945 	uint tcaps;
946 
947 	bool inlineImagesSupported() {
948 		return (tcaps & TerminalCapabilities.arsdImage) ? true : false;
949 	}
950 	bool clipboardSupported() {
951 		version(Win32Console) return true;
952 		else return (tcaps & TerminalCapabilities.arsdImage) ? true : false;
953 	}
954 
955 	// only supported on my custom terminal emulator. guarded behind if(inlineImagesSupported)
956 	// though that isn't even 100% accurate but meh
957 	void changeWindowIcon()(string filename) {
958 		if(inlineImagesSupported()) {
959 		        import arsd.png;
960 			auto image = readPng(filename);
961 			auto ii = cast(IndexedImage) image;
962 			assert(ii !is null);
963 
964 			// copy/pasted from my terminalemulator.d
965 			string encodeSmallTextImage(IndexedImage ii) {
966 				char encodeNumeric(int c) {
967 					if(c < 10)
968 						return cast(char)(c + '0');
969 					if(c < 10 + 26)
970 						return cast(char)(c - 10 + 'a');
971 					assert(0);
972 				}
973 
974 				string s;
975 				s ~= encodeNumeric(ii.width);
976 				s ~= encodeNumeric(ii.height);
977 
978 				foreach(entry; ii.palette)
979 					s ~= entry.toRgbaHexString();
980 				s ~= "Z";
981 
982 				ubyte rleByte;
983 				int rleCount;
984 
985 				void rleCommit() {
986 					if(rleByte >= 26)
987 						assert(0); // too many colors for us to handle
988 					if(rleCount == 0)
989 						goto finish;
990 					if(rleCount == 1) {
991 						s ~= rleByte + 'a';
992 						goto finish;
993 					}
994 
995 					import std.conv;
996 					s ~= to!string(rleCount);
997 					s ~= rleByte + 'a';
998 
999 					finish:
1000 						rleByte = 0;
1001 						rleCount = 0;
1002 				}
1003 
1004 				foreach(b; ii.data) {
1005 					if(b == rleByte)
1006 						rleCount++;
1007 					else {
1008 						rleCommit();
1009 						rleByte = b;
1010 						rleCount = 1;
1011 					}
1012 				}
1013 
1014 				rleCommit();
1015 
1016 				return s;
1017 			}
1018 
1019 			this.writeStringRaw("\033]5000;"~encodeSmallTextImage(ii)~"\007");
1020 		}
1021 	}
1022 
1023 	// dependent on tcaps...
1024 	void displayInlineImage()(ubyte[] imageData) {
1025 		if(inlineImagesSupported) {
1026 			import std.base64;
1027 
1028 			// I might change this protocol later!
1029 			enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:";
1030 
1031 			this.writeStringRaw("\000");
1032 			this.writeStringRaw(extensionMagicIdentifier);
1033 			this.writeStringRaw(Base64.encode(imageData));
1034 			this.writeStringRaw("\000");
1035 		}
1036 	}
1037 
1038 	void demandUserAttention() {
1039 		if(UseVtSequences) {
1040 			if(!terminalInFamily("linux"))
1041 				writeStringRaw("\033]5001;1\007");
1042 		}
1043 	}
1044 
1045 	void requestCopyToClipboard(string text) {
1046 		if(clipboardSupported) {
1047 			import std.base64;
1048 			writeStringRaw("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007");
1049 		}
1050 	}
1051 
1052 	void requestCopyToPrimary(string text) {
1053 		if(clipboardSupported) {
1054 			import std.base64;
1055 			writeStringRaw("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007");
1056 		}
1057 	}
1058 
1059 	bool hasDefaultDarkBackground() {
1060 		version(Win32Console) {
1061 			return !(defaultBackgroundColor & 0xf);
1062 		} else {
1063 			version(TerminalDirectToEmulator)
1064 			if(usingDirectEmulator)
1065 				return integratedTerminalEmulatorConfiguration.defaultBackground.g < 100;
1066 			// FIXME: there is probably a better way to do this
1067 			// but like idk how reliable it is.
1068 			if(terminalInFamily("linux"))
1069 				return true;
1070 			else
1071 				return false;
1072 		}
1073 	}
1074 
1075 	version(TerminalDirectToEmulator) {
1076 		TerminalEmulatorWidget tew;
1077 		private __gshared Window mainWindow;
1078 		import core.thread;
1079 		version(Posix)
1080 			ThreadID threadId;
1081 		else version(Windows)
1082 			HANDLE threadId;
1083 		private __gshared Thread guiThread;
1084 
1085 		private static class NewTerminalEvent {
1086 			Terminal* t;
1087 			this(Terminal* t) {
1088 				this.t = t;
1089 			}
1090 		}
1091 
1092 		bool usingDirectEmulator;
1093 	}
1094 
1095 	version(TerminalDirectToEmulator)
1096 	/++
1097 	+/
1098 	this(ConsoleOutputType type) {
1099 		this.type = type;
1100 
1101 		if(type == ConsoleOutputType.minimalProcessing) {
1102 			readTermcap("xterm");
1103 			_suppressDestruction = true;
1104 			return;
1105 		}
1106 
1107 		import arsd.simpledisplay;
1108 		static if(UsingSimpledisplayX11) {
1109 			try {
1110 				if(arsd.simpledisplay.librariesSuccessfullyLoaded) {
1111 					XDisplayConnection.get();
1112 					this.usingDirectEmulator = true;
1113 				} else if(!integratedTerminalEmulatorConfiguration.fallbackToDegradedTerminal) {
1114 					throw new Exception("Unable to load X libraries to create custom terminal.");
1115 				}
1116 			} catch(Exception e) {
1117 				if(!integratedTerminalEmulatorConfiguration.fallbackToDegradedTerminal)
1118 					throw e;
1119 
1120 			}
1121 		} else {
1122 			this.usingDirectEmulator = true;
1123 		}
1124 
1125 		if(!usingDirectEmulator) {
1126 			version(Posix)
1127 				posixInitialize(type, 0, 1, null);
1128 			else
1129 				throw new Exception("Total wtf - are you on a windows system without a gui?!?");
1130 			return;
1131 		}
1132 
1133 		tcaps = uint.max; // all capabilities
1134 		import core.thread;
1135 
1136 		version(Posix)
1137 			threadId = Thread.getThis.id;
1138 		else version(Windows)
1139 			threadId = GetCurrentThread();
1140 
1141 		if(guiThread is null) {
1142 			guiThread = new Thread( {
1143 				auto window = new TerminalEmulatorWindow(&this, null);
1144 				mainWindow = window;
1145 				mainWindow.win.addEventListener((NewTerminalEvent t) {
1146 					auto nw = new TerminalEmulatorWindow(t.t, null);
1147 					t.t.tew = nw.tew;
1148 					t.t = null;
1149 					nw.show();
1150 				});
1151 				tew = window.tew;
1152 				//try
1153 					window.loop();
1154 				/*
1155 				catch(Throwable t) {
1156 					import std.stdio;
1157 					stdout.writeln(t);
1158 					stdout.flush();
1159 				}
1160 				*/
1161 			});
1162 			guiThread.start();
1163 			guiThread.priority = Thread.PRIORITY_MAX; // gui thread needs responsiveness
1164 		} else {
1165 			// FIXME: 64 bit builds on linux segfault with multiple terminals
1166 			// so that isn't really supported as of yet.
1167 			while(cast(shared) mainWindow is null) {
1168 				import core.thread;
1169 				Thread.sleep(5.msecs);
1170 			}
1171 			mainWindow.win.postEvent(new NewTerminalEvent(&this));
1172 		}
1173 
1174 		// need to wait until it is properly initialized
1175 		while(cast(shared) tew is null) {
1176 			import core.thread;
1177 			Thread.sleep(5.msecs);
1178 		}
1179 
1180 		initializeVt();
1181 
1182 	}
1183 	else
1184 
1185 	version(Posix)
1186 	/**
1187 	 * Constructs an instance of Terminal representing the capabilities of
1188 	 * the current terminal.
1189 	 *
1190 	 * While it is possible to override the stdin+stdout file descriptors, remember
1191 	 * that is not portable across platforms and be sure you know what you're doing.
1192 	 *
1193 	 * ditto on getSizeOverride. That's there so you can do something instead of ioctl.
1194 	 */
1195 	this(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) {
1196 		posixInitialize(type, fdIn, fdOut, getSizeOverride);
1197 	}
1198 
1199 	version(Posix)
1200 	private void posixInitialize(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) {
1201 		this.fdIn = fdIn;
1202 		this.fdOut = fdOut;
1203 		this.getSizeOverride = getSizeOverride;
1204 		this.type = type;
1205 
1206 		if(type == ConsoleOutputType.minimalProcessing) {
1207 			readTermcap();
1208 			_suppressDestruction = true;
1209 			return;
1210 		}
1211 
1212 		tcaps = getTerminalCapabilities(fdIn, fdOut);
1213 		//writeln(tcaps);
1214 
1215 		initializeVt();
1216 	}
1217 
1218 	void initializeVt() {
1219 		readTermcap();
1220 
1221 		if(type == ConsoleOutputType.cellular) {
1222 			doTermcap("ti");
1223 			clear();
1224 			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
1225 		}
1226 
1227 		if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
1228 			writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it)
1229 		}
1230 
1231 	}
1232 
1233 	// EXPERIMENTAL do not use yet
1234 	Terminal alternateScreen() {
1235 		assert(this.type != ConsoleOutputType.cellular);
1236 
1237 		this.flush();
1238 		return Terminal(ConsoleOutputType.cellular);
1239 	}
1240 
1241 	version(Windows) {
1242 		HANDLE hConsole;
1243 		CONSOLE_SCREEN_BUFFER_INFO originalSbi;
1244 	}
1245 
1246 	version(Win32Console)
1247 	/// ditto
1248 	this(ConsoleOutputType type) {
1249 		if(UseVtSequences) {
1250 			hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
1251 			initializeVt();
1252 		} else {
1253 			if(type == ConsoleOutputType.cellular) {
1254 				hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, null, CONSOLE_TEXTMODE_BUFFER, null);
1255 				if(hConsole == INVALID_HANDLE_VALUE) {
1256 					import std.conv;
1257 					throw new Exception(to!string(GetLastError()));
1258 				}
1259 
1260 				SetConsoleActiveScreenBuffer(hConsole);
1261 				/*
1262 	http://msdn.microsoft.com/en-us/library/windows/desktop/ms686125%28v=vs.85%29.aspx
1263 	http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.aspx
1264 				*/
1265 				COORD size;
1266 				/*
1267 				CONSOLE_SCREEN_BUFFER_INFO sbi;
1268 				GetConsoleScreenBufferInfo(hConsole, &sbi);
1269 				size.X = cast(short) GetSystemMetrics(SM_CXMIN);
1270 				size.Y = cast(short) GetSystemMetrics(SM_CYMIN);
1271 				*/
1272 
1273 				// FIXME: this sucks, maybe i should just revert it. but there shouldn't be scrollbars in cellular mode
1274 				//size.X = 80;
1275 				//size.Y = 24;
1276 				//SetConsoleScreenBufferSize(hConsole, size);
1277 
1278 				GetConsoleCursorInfo(hConsole, &originalCursorInfo);
1279 
1280 				clear();
1281 			} else {
1282 				hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
1283 			}
1284 
1285 			if(GetConsoleScreenBufferInfo(hConsole, &originalSbi) == 0)
1286 				throw new Exception("not a user-interactive terminal");
1287 
1288 			defaultForegroundColor = cast(Color) (originalSbi.wAttributes & 0x0f);
1289 			defaultBackgroundColor = cast(Color) ((originalSbi.wAttributes >> 4) & 0x0f);
1290 
1291 			// this is unnecessary since I use the W versions of other functions
1292 			// and can cause weird font bugs, so I'm commenting unless some other
1293 			// need comes up.
1294 			/*
1295 			oldCp = GetConsoleOutputCP();
1296 			SetConsoleOutputCP(65001); // UTF-8
1297 
1298 			oldCpIn = GetConsoleCP();
1299 			SetConsoleCP(65001); // UTF-8
1300 			*/
1301 		}
1302 	}
1303 
1304 	version(Win32Console) {
1305 		private Color defaultBackgroundColor = Color.black;
1306 		private Color defaultForegroundColor = Color.white;
1307 		UINT oldCp;
1308 		UINT oldCpIn;
1309 	}
1310 
1311 	// 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...
1312 	bool _suppressDestruction;
1313 
1314 	~this() {
1315 		if(_suppressDestruction) {
1316 			flush();
1317 			return;
1318 		}
1319 
1320 		if(UseVtSequences) {
1321 			if(type == ConsoleOutputType.cellular) {
1322 				doTermcap("te");
1323 			}
1324 			version(TerminalDirectToEmulator) {
1325 				if(usingDirectEmulator) {
1326 					writeln("\n\n<exited>");
1327 					setTitle(tew.terminalEmulator.currentTitle ~ " <exited>");
1328 					tew.term = null;
1329 
1330 					if(integratedTerminalEmulatorConfiguration.closeOnExit)
1331 						tew.parentWindow.close();
1332 				} else {
1333 					if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
1334 						writeStringRaw("\033[23;0t"); // restore window title from the stack
1335 					}
1336 				}
1337 			} else
1338 			if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
1339 				writeStringRaw("\033[23;0t"); // restore window title from the stack
1340 			}
1341 			cursor = TerminalCursor.DEFAULT;
1342 			showCursor();
1343 			reset();
1344 			flush();
1345 
1346 			if(lineGetter !is null)
1347 				lineGetter.dispose();
1348 		} else version(Win32Console) {
1349 			flush(); // make sure user data is all flushed before resetting
1350 			reset();
1351 			showCursor();
1352 
1353 			if(lineGetter !is null)
1354 				lineGetter.dispose();
1355 
1356 
1357 			SetConsoleOutputCP(oldCp);
1358 			SetConsoleCP(oldCpIn);
1359 
1360 			auto stdo = GetStdHandle(STD_OUTPUT_HANDLE);
1361 			SetConsoleActiveScreenBuffer(stdo);
1362 			if(hConsole !is stdo)
1363 				CloseHandle(hConsole);
1364 		}
1365 	}
1366 
1367 	// lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit)
1368 	// and some history storage.
1369 	LineGetter lineGetter;
1370 
1371 	int _currentForeground = Color.DEFAULT;
1372 	int _currentBackground = Color.DEFAULT;
1373 	RGB _currentForegroundRGB;
1374 	RGB _currentBackgroundRGB;
1375 	bool reverseVideo = false;
1376 
1377 	/++
1378 		Attempts to set color according to a 24 bit value (r, g, b, each >= 0 and < 256).
1379 
1380 
1381 		This is not supported on all terminals. It will attempt to fall back to a 256-color
1382 		or 8-color palette in those cases automatically.
1383 
1384 		Returns: true if it believes it was successful (note that it cannot be completely sure),
1385 		false if it had to use a fallback.
1386 	+/
1387 	bool setTrueColor(RGB foreground, RGB background, ForceOption force = ForceOption.automatic) {
1388 		if(force == ForceOption.neverSend) {
1389 			_currentForeground = -1;
1390 			_currentBackground = -1;
1391 			_currentForegroundRGB = foreground;
1392 			_currentBackgroundRGB = background;
1393 			return true;
1394 		}
1395 
1396 		if(force == ForceOption.automatic && _currentForeground == -1 && _currentBackground == -1 && (_currentForegroundRGB == foreground && _currentBackgroundRGB == background))
1397 			return true;
1398 
1399 		_currentForeground = -1;
1400 		_currentBackground = -1;
1401 		_currentForegroundRGB = foreground;
1402 		_currentBackgroundRGB = background;
1403 
1404 		version(Win32Console) {
1405 			flush();
1406 			ushort setTob = cast(ushort) approximate16Color(background);
1407 			ushort setTof = cast(ushort) approximate16Color(foreground);
1408 			SetConsoleTextAttribute(
1409 				hConsole,
1410 				cast(ushort)((setTob << 4) | setTof));
1411 			return false;
1412 		} else {
1413 			// FIXME: if the terminal reliably does support 24 bit color, use it
1414 			// instead of the round off. But idk how to detect that yet...
1415 
1416 			// fallback to 16 color for term that i know don't take it well
1417 			import std.process;
1418 			import std.string;
1419 			version(TerminalDirectToEmulator)
1420 			if(usingDirectEmulator)
1421 				goto skip_approximation;
1422 
1423 			if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") {
1424 				// not likely supported, use 16 color fallback
1425 				auto setTof = approximate16Color(foreground);
1426 				auto setTob = approximate16Color(background);
1427 
1428 				writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm",
1429 					(setTof & Bright) ? 1 : 0,
1430 					cast(int) (setTof & ~Bright),
1431 					cast(int) (setTob & ~Bright)
1432 				));
1433 
1434 				return false;
1435 			}
1436 
1437 			skip_approximation:
1438 
1439 			// otherwise, assume it is probably supported and give it a try
1440 			writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm",
1441 				colorToXTermPaletteIndex(foreground),
1442 				colorToXTermPaletteIndex(background)
1443 			));
1444 
1445 			/+ // this is the full 24 bit color sequence
1446 			writeStringRaw(format("\033[38;2;%d;%d;%dm", foreground.r, foreground.g, foreground.b));
1447 			writeStringRaw(format("\033[48;2;%d;%d;%dm", background.r, background.g, background.b));
1448 			+/
1449 
1450 			return true;
1451 		}
1452 	}
1453 
1454 	/// Changes the current color. See enum Color for the values.
1455 	void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) {
1456 		if(force != ForceOption.neverSend) {
1457 			version(Win32Console) {
1458 				// assuming a dark background on windows, so LowContrast == dark which means the bit is NOT set on hardware
1459 				/*
1460 				foreground ^= LowContrast;
1461 				background ^= LowContrast;
1462 				*/
1463 
1464 				ushort setTof = cast(ushort) foreground;
1465 				ushort setTob = cast(ushort) background;
1466 
1467 				// this isn't necessarily right but meh
1468 				if(background == Color.DEFAULT)
1469 					setTob = defaultBackgroundColor;
1470 				if(foreground == Color.DEFAULT)
1471 					setTof = defaultForegroundColor;
1472 
1473 				if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) {
1474 					flush(); // if we don't do this now, the buffering can screw up the colors...
1475 					if(reverseVideo) {
1476 						if(background == Color.DEFAULT)
1477 							setTof = defaultBackgroundColor;
1478 						else
1479 							setTof = cast(ushort) background | (foreground & Bright);
1480 
1481 						if(background == Color.DEFAULT)
1482 							setTob = defaultForegroundColor;
1483 						else
1484 							setTob = cast(ushort) (foreground & ~Bright);
1485 					}
1486 					SetConsoleTextAttribute(
1487 						hConsole,
1488 						cast(ushort)((setTob << 4) | setTof));
1489 				}
1490 			} else {
1491 				import std.process;
1492 				// I started using this envvar for my text editor, but now use it elsewhere too
1493 				// if we aren't set to dark, assume light
1494 				/*
1495 				if(getenv("ELVISBG") == "dark") {
1496 					// LowContrast on dark bg menas
1497 				} else {
1498 					foreground ^= LowContrast;
1499 					background ^= LowContrast;
1500 				}
1501 				*/
1502 
1503 				ushort setTof = cast(ushort) foreground & ~Bright;
1504 				ushort setTob = cast(ushort) background & ~Bright;
1505 
1506 				if(foreground & Color.DEFAULT)
1507 					setTof = 9; // ansi sequence for reset
1508 				if(background == Color.DEFAULT)
1509 					setTob = 9;
1510 
1511 				import std.string;
1512 
1513 				if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) {
1514 					writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm\033[%dm",
1515 						(foreground != Color.DEFAULT && (foreground & Bright)) ? 1 : 0,
1516 						cast(int) setTof,
1517 						cast(int) setTob,
1518 						reverseVideo ? 7 : 27
1519 					));
1520 				}
1521 			}
1522 		}
1523 
1524 		_currentForeground = foreground;
1525 		_currentBackground = background;
1526 		this.reverseVideo = reverseVideo;
1527 	}
1528 
1529 	private bool _underlined = false;
1530 
1531 	/++
1532 		Outputs a hyperlink to my custom terminal (v0.0.7 or later) or to version
1533 		`TerminalDirectToEmulator`.  The way it works is a bit strange...
1534 
1535 
1536 		If using a terminal that supports it, it outputs the given text with the
1537 		given identifier attached (one bit of identifier per grapheme of text!). When
1538 		the user clicks on it, it will send a [LinkEvent] with the text and the identifier
1539 		for you to respond, if in real-time input mode, or a simple paste event with the
1540 		text if not (you will not be able to distinguish this from a user pasting the
1541 		same text).
1542 
1543 		If the user's terminal does not support my feature, it writes plain text instead.
1544 
1545 		It is important that you make sure your program still works even if the hyperlinks
1546 		never work - ideally, make them out of text the user can type manually or copy/paste
1547 		into your command line somehow too.
1548 
1549 		Hyperlinks may not work correctly after your program exits or if you are capturing
1550 		mouse input (the user will have to hold shift in that case). It is really designed
1551 		for linear mode with direct to emulator mode. If you are using cellular mode with
1552 		full input capturing, you should manage the clicks yourself.
1553 
1554 		Similarly, if it horizontally scrolls off the screen, it can be corrupted since it
1555 		packs your text and identifier into free bits in the screen buffer itself. I may be
1556 		able to fix that later.
1557 
1558 		Params:
1559 			text = text displayed in the terminal
1560 			identifier = an additional number attached to the text and returned to you in a [LinkEvent]
1561 			autoStyle = set to `false` to suppress the automatic color and underlining of the text.
1562 
1563 		Bugs:
1564 			there's no keyboard interaction with it at all right now. i might make the terminal
1565 			emulator offer the ids or something through a hold ctrl or something interface. idk.
1566 			or tap ctrl twice to turn that on.
1567 
1568 		History:
1569 			Added March 18, 2020
1570 	+/
1571 	void hyperlink(string text, ushort identifier = 0, bool autoStyle = true) {
1572 		if((tcaps & TerminalCapabilities.arsdHyperlinks)) {
1573 			bool previouslyUnderlined = _underlined;
1574 			int fg = _currentForeground, bg = _currentBackground;
1575 			if(autoStyle) {
1576 				color(Color.blue, Color.white);
1577 				underline = true;
1578 			}
1579 
1580 			import std.conv;
1581 			writeStringRaw("\033[?" ~ to!string(65536 + identifier) ~ "h");
1582 			write(text);
1583 			writeStringRaw("\033[?65536l");
1584 
1585 			if(autoStyle) {
1586 				underline = previouslyUnderlined;
1587 				color(fg, bg);
1588 			}
1589 		} else {
1590 			write(text); // graceful degrade  
1591 		}
1592 	}
1593 
1594 	/// Note: the Windows console does not support underlining
1595 	void underline(bool set, ForceOption force = ForceOption.automatic) {
1596 		if(set == _underlined && force != ForceOption.alwaysSend)
1597 			return;
1598 		if(UseVtSequences) {
1599 			if(set)
1600 				writeStringRaw("\033[4m");
1601 			else
1602 				writeStringRaw("\033[24m");
1603 		}
1604 		_underlined = set;
1605 	}
1606 	// FIXME: do I want to do bold and italic?
1607 
1608 	/// Returns the terminal to normal output colors
1609 	void reset() {
1610 		version(Win32Console)
1611 			SetConsoleTextAttribute(
1612 				hConsole,
1613 				originalSbi.wAttributes);
1614 		else
1615 			writeStringRaw("\033[0m");
1616 
1617 		_underlined = false;
1618 		_currentForeground = Color.DEFAULT;
1619 		_currentBackground = Color.DEFAULT;
1620 		reverseVideo = false;
1621 	}
1622 
1623 	// FIXME: add moveRelative
1624 
1625 	/// The current x position of the output cursor. 0 == leftmost column
1626 	@property int cursorX() {
1627 		return _cursorX;
1628 	}
1629 
1630 	/// The current y position of the output cursor. 0 == topmost row
1631 	@property int cursorY() {
1632 		return _cursorY;
1633 	}
1634 
1635 	private int _cursorX;
1636 	private int _cursorY;
1637 
1638 	/// 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
1639 	void moveTo(int x, int y, ForceOption force = ForceOption.automatic) {
1640 		if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) {
1641 			executeAutoHideCursor();
1642 			if(UseVtSequences) {
1643 				doTermcap("cm", y, x);
1644 			} else version(Win32Console) {
1645 
1646 				flush(); // if we don't do this now, the buffering can screw up the position
1647 				COORD coord = {cast(short) x, cast(short) y};
1648 				SetConsoleCursorPosition(hConsole, coord);
1649 			}
1650 		}
1651 
1652 		_cursorX = x;
1653 		_cursorY = y;
1654 	}
1655 
1656 	/// shows the cursor
1657 	void showCursor() {
1658 		if(UseVtSequences)
1659 			doTermcap("ve");
1660 		else version(Win32Console) {
1661 			CONSOLE_CURSOR_INFO info;
1662 			GetConsoleCursorInfo(hConsole, &info);
1663 			info.bVisible = true;
1664 			SetConsoleCursorInfo(hConsole, &info);
1665 		}
1666 	}
1667 
1668 	/// hides the cursor
1669 	void hideCursor() {
1670 		if(UseVtSequences) {
1671 			doTermcap("vi");
1672 		} else version(Win32Console) {
1673 			CONSOLE_CURSOR_INFO info;
1674 			GetConsoleCursorInfo(hConsole, &info);
1675 			info.bVisible = false;
1676 			SetConsoleCursorInfo(hConsole, &info);
1677 		}
1678 
1679 	}
1680 
1681 	private bool autoHidingCursor;
1682 	private bool autoHiddenCursor;
1683 	// explicitly not publicly documented
1684 	// Sets the cursor to automatically insert a hide command at the front of the output buffer iff it is moved.
1685 	// Call autoShowCursor when you are done with the batch update.
1686 	void autoHideCursor() {
1687 		autoHidingCursor = true;
1688 	}
1689 
1690 	private void executeAutoHideCursor() {
1691 		if(autoHidingCursor) {
1692 			version(Win32Console)
1693 				hideCursor();
1694 			else if(UseVtSequences) {
1695 				// prepend the hide cursor command so it is the first thing flushed
1696 				writeBuffer = "\033[?25l" ~ writeBuffer;
1697 			}
1698 
1699 			autoHiddenCursor = true;
1700 			autoHidingCursor = false; // already been done, don't insert the command again
1701 		}
1702 	}
1703 
1704 	// explicitly not publicly documented
1705 	// Shows the cursor if it was automatically hidden by autoHideCursor and resets the internal auto hide state.
1706 	void autoShowCursor() {
1707 		if(autoHiddenCursor)
1708 			showCursor();
1709 
1710 		autoHidingCursor = false;
1711 		autoHiddenCursor = false;
1712 	}
1713 
1714 	/*
1715 	// alas this doesn't work due to a bunch of delegate context pointer and postblit problems
1716 	// instead of using: auto input = terminal.captureInput(flags)
1717 	// use: auto input = RealTimeConsoleInput(&terminal, flags);
1718 	/// Gets real time input, disabling line buffering
1719 	RealTimeConsoleInput captureInput(ConsoleInputFlags flags) {
1720 		return RealTimeConsoleInput(&this, flags);
1721 	}
1722 	*/
1723 
1724 	/// Changes the terminal's title
1725 	void setTitle(string t) {
1726 		version(Win32Console) {
1727 			wchar[256] buffer;
1728 			size_t bufferLength;
1729 			foreach(wchar ch; t)
1730 				if(bufferLength < buffer.length)
1731 					buffer[bufferLength++] = ch;
1732 			if(bufferLength < buffer.length)
1733 				buffer[bufferLength++] = 0;
1734 			else
1735 				buffer[$-1] = 0;
1736 			SetConsoleTitleW(buffer.ptr);
1737 		} else {
1738 			import std.string;
1739 			if(terminalInFamily("xterm", "rxvt", "screen", "tmux"))
1740 				writeStringRaw(format("\033]0;%s\007", t));
1741 		}
1742 	}
1743 
1744 	/// Flushes your updates to the terminal.
1745 	/// It is important to call this when you are finished writing for now if you are using the version=with_eventloop
1746 	void flush() {
1747 		if(writeBuffer.length == 0)
1748 			return;
1749 
1750 		version(TerminalDirectToEmulator) {
1751 			if(usingDirectEmulator) {
1752 				tew.sendRawInput(cast(ubyte[]) writeBuffer);
1753 				writeBuffer = null;
1754 			} else {
1755 				interiorFlush();
1756 			}
1757 		} else {
1758 			interiorFlush();
1759 		}
1760 	}
1761 
1762 	private void interiorFlush() {
1763 		version(Posix) {
1764 			if(_writeDelegate !is null) {
1765 				_writeDelegate(writeBuffer);
1766 			} else {
1767 				ssize_t written;
1768 
1769 				while(writeBuffer.length) {
1770 					written = unix.write(this.fdOut, writeBuffer.ptr, writeBuffer.length);
1771 					if(written < 0)
1772 						throw new Exception("write failed for some reason");
1773 					writeBuffer = writeBuffer[written .. $];
1774 				}
1775 			}
1776 		} else version(Win32Console) {
1777 			import std.conv;
1778 			// FIXME: I'm not sure I'm actually happy with this allocation but
1779 			// it probably isn't a big deal. At least it has unicode support now.
1780 			wstring writeBufferw = to!wstring(writeBuffer);
1781 			while(writeBufferw.length) {
1782 				DWORD written;
1783 				WriteConsoleW(hConsole, writeBufferw.ptr, cast(DWORD)writeBufferw.length, &written, null);
1784 				writeBufferw = writeBufferw[written .. $];
1785 			}
1786 
1787 			writeBuffer = null;
1788 		}
1789 	}
1790 
1791 	int[] getSize() {
1792 		version(TerminalDirectToEmulator) {
1793 			if(usingDirectEmulator)
1794 				return [tew.terminalEmulator.width, tew.terminalEmulator.height];
1795 			else
1796 				return getSizeInternal();
1797 		} else {
1798 			return getSizeInternal();
1799 		}
1800 	}
1801 
1802 	private int[] getSizeInternal() {
1803 		version(Windows) {
1804 			CONSOLE_SCREEN_BUFFER_INFO info;
1805 			GetConsoleScreenBufferInfo( hConsole, &info );
1806         
1807 			int cols, rows;
1808         
1809 			cols = (info.srWindow.Right - info.srWindow.Left + 1);
1810 			rows = (info.srWindow.Bottom - info.srWindow.Top + 1);
1811 
1812 			return [cols, rows];
1813 		} else {
1814 			if(getSizeOverride is null) {
1815 				winsize w;
1816 				ioctl(0, TIOCGWINSZ, &w);
1817 				return [w.ws_col, w.ws_row];
1818 			} else return getSizeOverride();
1819 		}
1820 	}
1821 
1822 	void updateSize() {
1823 		auto size = getSize();
1824 		_width = size[0];
1825 		_height = size[1];
1826 	}
1827 
1828 	private int _width;
1829 	private int _height;
1830 
1831 	/// The current width of the terminal (the number of columns)
1832 	@property int width() {
1833 		if(_width == 0 || _height == 0)
1834 			updateSize();
1835 		return _width;
1836 	}
1837 
1838 	/// The current height of the terminal (the number of rows)
1839 	@property int height() {
1840 		if(_width == 0 || _height == 0)
1841 			updateSize();
1842 		return _height;
1843 	}
1844 
1845 	/*
1846 	void write(T...)(T t) {
1847 		foreach(arg; t) {
1848 			writeStringRaw(to!string(arg));
1849 		}
1850 	}
1851 	*/
1852 
1853 	/// Writes to the terminal at the current cursor position.
1854 	void writef(T...)(string f, T t) {
1855 		import std.string;
1856 		writePrintableString(format(f, t));
1857 	}
1858 
1859 	/// ditto
1860 	void writefln(T...)(string f, T t) {
1861 		writef(f ~ "\n", t);
1862 	}
1863 
1864 	/// ditto
1865 	void write(T...)(T t) {
1866 		import std.conv;
1867 		string data;
1868 		foreach(arg; t) {
1869 			data ~= to!string(arg);
1870 		}
1871 
1872 		writePrintableString(data);
1873 	}
1874 
1875 	/// ditto
1876 	void writeln(T...)(T t) {
1877 		write(t, "\n");
1878 	}
1879 
1880 	/+
1881 	/// A combined moveTo and writef that puts the cursor back where it was before when it finishes the write.
1882 	/// Only works in cellular mode. 
1883 	/// 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)
1884 	void writefAt(T...)(int x, int y, string f, T t) {
1885 		import std.string;
1886 		auto toWrite = format(f, t);
1887 
1888 		auto oldX = _cursorX;
1889 		auto oldY = _cursorY;
1890 
1891 		writeAtWithoutReturn(x, y, toWrite);
1892 
1893 		moveTo(oldX, oldY);
1894 	}
1895 
1896 	void writeAtWithoutReturn(int x, int y, in char[] data) {
1897 		moveTo(x, y);
1898 		writeStringRaw(toWrite, ForceOption.alwaysSend);
1899 	}
1900 	+/
1901 
1902 	void writePrintableString(const(char)[] s, ForceOption force = ForceOption.automatic) {
1903 		// an escape character is going to mess things up. Actually any non-printable character could, but meh
1904 		// assert(s.indexOf("\033") == -1);
1905 
1906 		if(s.length == 0)
1907 			return;
1908 
1909 		// tracking cursor position
1910 		// FIXME: by grapheme?
1911 		foreach(dchar ch; s) {
1912 			switch(ch) {
1913 				case '\n':
1914 					_cursorX = 0;
1915 					_cursorY++;
1916 				break;
1917 				case '\r':
1918 					_cursorX = 0;
1919 				break;
1920 				case '\t':
1921 					_cursorX ++;
1922 					_cursorX += _cursorX % 8; // FIXME: get the actual tabstop, if possible
1923 				break;
1924 				default:
1925 					_cursorX++;
1926 			}
1927 
1928 			if(_wrapAround && _cursorX > width) {
1929 				_cursorX = 0;
1930 				_cursorY++;
1931 			}
1932 
1933 			if(_cursorY == height)
1934 				_cursorY--;
1935 
1936 			/+
1937 			auto index = getIndex(_cursorX, _cursorY);
1938 			if(data[index] != ch) {
1939 				data[index] = ch;
1940 			}
1941 			+/
1942 		}
1943 
1944 		version(TerminalDirectToEmulator) {
1945 			// this breaks up extremely long output a little as an aid to the
1946 			// gui thread; by breaking it up, it helps to avoid monopolizing the
1947 			// event loop. Easier to do here than in the thread itself because
1948 			// this one doesn't have escape sequences to break up so it avoids work.
1949 			while(s.length) {
1950 				auto len = s.length;
1951 				if(len > 1024 * 32) {
1952 					len = 1024 * 32;
1953 					// get to the start of a utf-8 sequence. kidna sorta.
1954 					while(len && (s[len] & 0x1000_0000))
1955 						len--;
1956 				}
1957 				auto next = s[0 .. len];
1958 				s = s[len .. $];
1959 				writeStringRaw(next);
1960 			}
1961 		} else {
1962 			writeStringRaw(s);
1963 		}
1964 	}
1965 
1966 	/* private */ bool _wrapAround = true;
1967 
1968 	deprecated alias writePrintableString writeString; /// use write() or writePrintableString instead
1969 
1970 	private string writeBuffer;
1971 
1972 	// you really, really shouldn't use this unless you know what you are doing
1973 	/*private*/ void writeStringRaw(in char[] s) {
1974 		writeBuffer ~= s; // buffer it to do everything at once in flush() calls
1975 		if(writeBuffer.length >  1024 * 32)
1976 			flush();
1977 	}
1978 
1979 	/// Clears the screen.
1980 	void clear() {
1981 		if(UseVtSequences) {
1982 			doTermcap("cl");
1983 		} else version(Win32Console) {
1984 			// http://support.microsoft.com/kb/99261
1985 			flush();
1986 
1987 			DWORD c;
1988 			CONSOLE_SCREEN_BUFFER_INFO csbi;
1989 			DWORD conSize;
1990 			GetConsoleScreenBufferInfo(hConsole, &csbi);
1991 			conSize = csbi.dwSize.X * csbi.dwSize.Y;
1992 			COORD coordScreen;
1993 			FillConsoleOutputCharacterA(hConsole, ' ', conSize, coordScreen, &c);
1994 			FillConsoleOutputAttribute(hConsole, csbi.wAttributes, conSize, coordScreen, &c);
1995 			moveTo(0, 0, ForceOption.alwaysSend);
1996 		}
1997 
1998 		_cursorX = 0;
1999 		_cursorY = 0;
2000 	}
2001 
2002 	/// gets a line, including user editing. Convenience method around the LineGetter class and RealTimeConsoleInput facilities - use them if you need more control.
2003 	/// You really shouldn't call this if stdin isn't actually a user-interactive terminal! So if you expect people to pipe data to your app, check for that or use something else.
2004 	// FIXME: add a method to make it easy to check if stdin is actually a tty and use other methods there.
2005 	string getline(string prompt = null) {
2006 		if(lineGetter is null)
2007 			lineGetter = new LineGetter(&this);
2008 		// since the struct might move (it shouldn't, this should be unmovable!) but since
2009 		// it technically might, I'm updating the pointer before using it just in case.
2010 		lineGetter.terminal = &this;
2011 
2012 		if(prompt !is null)
2013 			lineGetter.prompt = prompt;
2014 
2015 		auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw);
2016 		auto line = lineGetter.getline(&input);
2017 
2018 		// lineGetter leaves us exactly where it was when the user hit enter, giving best
2019 		// flexibility to real-time input and cellular programs. The convenience function,
2020 		// however, wants to do what is right in most the simple cases, which is to actually
2021 		// print the line (echo would be enabled without RealTimeConsoleInput anyway and they
2022 		// did hit enter), so we'll do that here too.
2023 		writePrintableString("\n");
2024 
2025 		return line;
2026 	}
2027 
2028 }
2029 
2030 /++
2031 	Removes terminal color, bold, etc. sequences from a string,
2032 	making it plain text suitable for output to a normal .txt
2033 	file.
2034 +/
2035 inout(char)[] removeTerminalGraphicsSequences(inout(char)[] s) {
2036 	import std.string;
2037 
2038 	auto at = s.indexOf("\033[");
2039 	if(at == -1)
2040 		return s;
2041 
2042 	inout(char)[] ret;
2043 
2044 	do {
2045 		ret ~= s[0 .. at];
2046 		s = s[at + 2 .. $];
2047 		while(s.length && !((s[0] >= 'a' && s[0] <= 'z') || s[0] >= 'A' && s[0] <= 'Z')) {
2048 			s = s[1 .. $];
2049 		}
2050 		if(s.length)
2051 			s = s[1 .. $]; // skip the terminator
2052 		at = s.indexOf("\033[");
2053 	} while(at != -1);
2054 
2055 	ret ~= s;
2056 
2057 	return ret;
2058 }
2059 
2060 unittest {
2061 	assert("foo".removeTerminalGraphicsSequences == "foo");
2062 	assert("\033[34mfoo".removeTerminalGraphicsSequences == "foo");
2063 	assert("\033[34mfoo\033[39m".removeTerminalGraphicsSequences == "foo");
2064 	assert("\033[34m\033[45mfoo\033[39mbar\033[49m".removeTerminalGraphicsSequences == "foobar");
2065 }
2066 
2067 
2068 /+
2069 struct ConsoleBuffer {
2070 	int cursorX;
2071 	int cursorY;
2072 	int width;
2073 	int height;
2074 	dchar[] data;
2075 
2076 	void actualize(Terminal* t) {
2077 		auto writer = t.getBufferedWriter();
2078 
2079 		this.copyTo(&(t.onScreen));
2080 	}
2081 
2082 	void copyTo(ConsoleBuffer* buffer) {
2083 		buffer.cursorX = this.cursorX;
2084 		buffer.cursorY = this.cursorY;
2085 		buffer.width = this.width;
2086 		buffer.height = this.height;
2087 		buffer.data[] = this.data[];
2088 	}
2089 }
2090 +/
2091 
2092 /**
2093  * Encapsulates the stream of input events received from the terminal input.
2094  */
2095 struct RealTimeConsoleInput {
2096 	@disable this();
2097 	@disable this(this);
2098 
2099 	/++
2100 		Requests the system to send paste data as a [PasteEvent] to this stream, if possible.
2101 
2102 		See_Also:
2103 			[Terminal.requestCopyToPrimary]
2104 			[Terminal.requestCopyToClipboard]
2105 			[Terminal.clipboardSupported]
2106 
2107 		History:
2108 			Added February 17, 2020.
2109 
2110 			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.
2111 	+/
2112 	void requestPasteFromClipboard() {
2113 		version(Win32Console) {
2114 			HWND hwndOwner = null;
2115 			if(OpenClipboard(hwndOwner) == 0)
2116 				throw new Exception("OpenClipboard");
2117 			scope(exit)
2118 				CloseClipboard();
2119 			if(auto dataHandle = GetClipboardData(CF_UNICODETEXT)) {
2120 
2121 				if(auto data = cast(wchar*) GlobalLock(dataHandle)) {
2122 					scope(exit)
2123 						GlobalUnlock(dataHandle);
2124 
2125 					int len = 0;
2126 					auto d = data;
2127 					while(*d) {
2128 						d++;
2129 						len++;
2130 					}
2131 					string s;
2132 					s.reserve(len);
2133 					foreach(idx, dchar ch; data[0 .. len]) {
2134 						// CR/LF -> LF
2135 						if(ch == '\r' && idx + 1 < len && data[idx + 1] == '\n')
2136 							continue;
2137 						s ~= ch;
2138 					}
2139 
2140 					injectEvent(InputEvent(PasteEvent(s), terminal), InjectionPosition.tail);
2141 				}
2142 			}
2143 		} else
2144 		if(terminal.clipboardSupported) {
2145 			if(UseVtSequences)
2146 				terminal.writeStringRaw("\033]52;c;?\007");
2147 		}
2148 	}
2149 
2150 	/// ditto
2151 	void requestPasteFromPrimary() {
2152 		if(terminal.clipboardSupported) {
2153 			if(UseVtSequences)
2154 				terminal.writeStringRaw("\033]52;p;?\007");
2155 		}
2156 	}
2157 
2158 
2159 	version(Posix) {
2160 		private int fdOut;
2161 		private int fdIn;
2162 		private sigaction_t oldSigWinch;
2163 		private sigaction_t oldSigIntr;
2164 		private sigaction_t oldHupIntr;
2165 		private termios old;
2166 		ubyte[128] hack;
2167 		// apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes)....
2168 		// tcgetattr smashed other variables in here too that could create random problems
2169 		// so this hack is just to give some room for that to happen without destroying the rest of the world
2170 	}
2171 
2172 	version(Windows) {
2173 		private DWORD oldInput;
2174 		private DWORD oldOutput;
2175 		HANDLE inputHandle;
2176 	}
2177 
2178 	private ConsoleInputFlags flags;
2179 	private Terminal* terminal;
2180 	private void delegate()[] destructor;
2181 
2182 	/// To capture input, you need to provide a terminal and some flags.
2183 	public this(Terminal* terminal, ConsoleInputFlags flags) {
2184 		this.flags = flags;
2185 		this.terminal = terminal;
2186 
2187 		version(Windows) {
2188 			inputHandle = GetStdHandle(STD_INPUT_HANDLE);
2189 
2190 		}
2191 
2192 		version(Win32Console) {
2193 
2194 			GetConsoleMode(inputHandle, &oldInput);
2195 
2196 			DWORD mode = 0;
2197 			//mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C and automatic paste... which we probably want to be similar to linux
2198 			//if(flags & ConsoleInputFlags.size)
2199 			mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc
2200 			if(flags & ConsoleInputFlags.echo)
2201 				mode |= ENABLE_ECHO_INPUT; // 0x4
2202 			if(flags & ConsoleInputFlags.mouse)
2203 				mode |= ENABLE_MOUSE_INPUT; // 0x10
2204 			// if(flags & ConsoleInputFlags.raw) // FIXME: maybe that should be a separate flag for ENABLE_LINE_INPUT
2205 
2206 			SetConsoleMode(inputHandle, mode);
2207 			destructor ~= { SetConsoleMode(inputHandle, oldInput); };
2208 
2209 
2210 			GetConsoleMode(terminal.hConsole, &oldOutput);
2211 			mode = 0;
2212 			// we want this to match linux too
2213 			mode |= ENABLE_PROCESSED_OUTPUT; /* 0x01 */
2214 			if(!(flags & ConsoleInputFlags.noEolWrap))
2215 				mode |= ENABLE_WRAP_AT_EOL_OUTPUT; /* 0x02 */
2216 			SetConsoleMode(terminal.hConsole, mode);
2217 			destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); };
2218 		}
2219 
2220 		version(TerminalDirectToEmulator) {
2221 			if(terminal.usingDirectEmulator)
2222 				terminal.tew.terminalEmulator.echo = (flags & ConsoleInputFlags.echo) ? true : false;
2223 			else version(Posix)
2224 				posixInit();
2225 		} else version(Posix) {
2226 			posixInit();
2227 		}
2228 
2229 		if(UseVtSequences) {
2230 			if(flags & ConsoleInputFlags.mouse) {
2231 				// basic button press+release notification
2232 
2233 				// FIXME: try to get maximum capabilities from all terminals
2234 				// right now this works well on xterm but rxvt isn't sending movements...
2235 
2236 				terminal.writeStringRaw("\033[?1000h");
2237 				destructor ~= { terminal.writeStringRaw("\033[?1000l"); };
2238 				// the MOUSE_HACK env var is for the case where I run screen
2239 				// but set TERM=xterm (which I do from putty). The 1003 mouse mode
2240 				// doesn't work there, breaking mouse support entirely. So by setting
2241 				// MOUSE_HACK=1002 it tells us to use the other mode for a fallback.
2242 				import std.process : environment;
2243 				if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") {
2244 					// this is vt200 mouse with full motion tracking, supported by xterm
2245 					terminal.writeStringRaw("\033[?1003h");
2246 					destructor ~= { terminal.writeStringRaw("\033[?1003l"); };
2247 				} else if(terminal.terminalInFamily("rxvt", "screen", "tmux") || environment.get("MOUSE_HACK") == "1002") {
2248 					terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed
2249 					destructor ~= { terminal.writeStringRaw("\033[?1002l"); };
2250 				}
2251 			}
2252 			if(flags & ConsoleInputFlags.paste) {
2253 				if(terminal.terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
2254 					terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode
2255 					destructor ~= { terminal.writeStringRaw("\033[?2004l"); };
2256 				}
2257 			}
2258 
2259 			if(terminal.tcaps & TerminalCapabilities.arsdHyperlinks) {
2260 				terminal.writeStringRaw("\033[?3004h"); // bracketed link mode
2261 				destructor ~= { terminal.writeStringRaw("\033[?3004l"); };
2262 			}
2263 
2264 			// try to ensure the terminal is in UTF-8 mode
2265 			if(terminal.terminalInFamily("xterm", "screen", "linux", "tmux") && !terminal.isMacTerminal()) {
2266 				terminal.writeStringRaw("\033%G");
2267 			}
2268 
2269 			terminal.flush();
2270 		}
2271 
2272 
2273 		version(with_eventloop) {
2274 			import arsd.eventloop;
2275 			version(Win32Console)
2276 				auto listenTo = inputHandle;
2277 			else version(Posix)
2278 				auto listenTo = this.fdIn;
2279 			else static assert(0, "idk about this OS");
2280 
2281 			version(Posix)
2282 			addListener(&signalFired);
2283 
2284 			if(listenTo != -1) {
2285 				addFileEventListeners(listenTo, &eventListener, null, null);
2286 				destructor ~= { removeFileEventListeners(listenTo); };
2287 			}
2288 			addOnIdle(&terminal.flush);
2289 			destructor ~= { removeOnIdle(&terminal.flush); };
2290 		}
2291 	}
2292 
2293 	version(Posix)
2294 	private void posixInit() {
2295 		this.fdIn = terminal.fdIn;
2296 		this.fdOut = terminal.fdOut;
2297 
2298 		if(fdIn != -1) {
2299 			tcgetattr(fdIn, &old);
2300 			auto n = old;
2301 
2302 			auto f = ICANON;
2303 			if(!(flags & ConsoleInputFlags.echo))
2304 				f |= ECHO;
2305 
2306 			// \033Z or \033[c
2307 
2308 			n.c_lflag &= ~f;
2309 			tcsetattr(fdIn, TCSANOW, &n);
2310 		}
2311 
2312 		// some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3
2313 		//destructor ~= { tcsetattr(fdIn, TCSANOW, &old); };
2314 
2315 		if(flags & ConsoleInputFlags.size) {
2316 			import core.sys.posix.signal;
2317 			sigaction_t n;
2318 			n.sa_handler = &sizeSignalHandler;
2319 			n.sa_mask = cast(sigset_t) 0;
2320 			n.sa_flags = 0;
2321 			sigaction(SIGWINCH, &n, &oldSigWinch);
2322 		}
2323 
2324 		{
2325 			import core.sys.posix.signal;
2326 			sigaction_t n;
2327 			n.sa_handler = &interruptSignalHandler;
2328 			n.sa_mask = cast(sigset_t) 0;
2329 			n.sa_flags = 0;
2330 			sigaction(SIGINT, &n, &oldSigIntr);
2331 		}
2332 
2333 		{
2334 			import core.sys.posix.signal;
2335 			sigaction_t n;
2336 			n.sa_handler = &hangupSignalHandler;
2337 			n.sa_mask = cast(sigset_t) 0;
2338 			n.sa_flags = 0;
2339 			sigaction(SIGHUP, &n, &oldHupIntr);
2340 		}
2341 	}
2342 
2343 	void fdReadyReader() {
2344 		auto queue = readNextEvents();
2345 		foreach(event; queue)
2346 			userEventHandler(event);
2347 	}
2348 
2349 	void delegate(InputEvent) userEventHandler;
2350 
2351 	/++
2352 		If you are using [arsd.simpledisplay] and want terminal interop too, you can call
2353 		this function to add it to the sdpy event loop and get the callback called on new
2354 		input.
2355 
2356 		Note that you will probably need to call `terminal.flush()` when you are doing doing
2357 		output, as the sdpy event loop doesn't know to do that (yet). I will probably change
2358 		that in a future version, but it doesn't hurt to call it twice anyway, so I recommend
2359 		calling flush yourself in any code you write using this.
2360 	+/
2361 	void integrateWithSimpleDisplayEventLoop()(void delegate(InputEvent) userEventHandler) {
2362 		this.userEventHandler = userEventHandler;
2363 		import arsd.simpledisplay;
2364 		version(Win32Console)
2365 			auto listener = new WindowsHandleReader(&fdReadyReader, terminal.hConsole);
2366 		else version(linux)
2367 			auto listener = new PosixFdReader(&fdReadyReader, fdIn);
2368 		else static assert(0, "sdpy event loop integration not implemented on this platform");
2369 	}
2370 
2371 	version(with_eventloop) {
2372 		version(Posix)
2373 		void signalFired(SignalFired) {
2374 			if(interrupted) {
2375 				interrupted = false;
2376 				send(InputEvent(UserInterruptionEvent(), terminal));
2377 			}
2378 			if(windowSizeChanged)
2379 				send(checkWindowSizeChanged());
2380 			if(hangedUp) {
2381 				hangedUp = false;
2382 				send(InputEvent(HangupEvent(), terminal));
2383 			}
2384 		}
2385 
2386 		import arsd.eventloop;
2387 		void eventListener(OsFileHandle fd) {
2388 			auto queue = readNextEvents();
2389 			foreach(event; queue)
2390 				send(event);
2391 		}
2392 	}
2393 
2394 	bool _suppressDestruction;
2395 
2396 	~this() {
2397 		if(_suppressDestruction)
2398 			return;
2399 
2400 		// the delegate thing doesn't actually work for this... for some reason
2401 
2402 		version(TerminalDirectToEmulator) {
2403 			if(terminal && terminal.usingDirectEmulator)
2404 				goto skip_extra;
2405 		}
2406 
2407 		version(Posix) {
2408 			if(fdIn != -1)
2409 				tcsetattr(fdIn, TCSANOW, &old);
2410 
2411 			if(flags & ConsoleInputFlags.size) {
2412 				// restoration
2413 				sigaction(SIGWINCH, &oldSigWinch, null);
2414 			}
2415 			sigaction(SIGINT, &oldSigIntr, null);
2416 			sigaction(SIGHUP, &oldHupIntr, null);
2417 		}
2418 
2419 		skip_extra:
2420 
2421 		// we're just undoing everything the constructor did, in reverse order, same criteria
2422 		foreach_reverse(d; destructor)
2423 			d();
2424 	}
2425 
2426 	/**
2427 		Returns true if there iff getch() would not block.
2428 
2429 		WARNING: kbhit might consume input that would be ignored by getch. This
2430 		function is really only meant to be used in conjunction with getch. Typically,
2431 		you should use a full-fledged event loop if you want all kinds of input. kbhit+getch
2432 		are just for simple keyboard driven applications.
2433 	*/
2434 	bool kbhit() {
2435 		auto got = getch(true);
2436 
2437 		if(got == dchar.init)
2438 			return false;
2439 
2440 		getchBuffer = got;
2441 		return true;
2442 	}
2443 
2444 	/// Check for input, waiting no longer than the number of milliseconds
2445 	bool timedCheckForInput(int milliseconds) {
2446 		if(inputQueue.length || timedCheckForInput_bypassingBuffer(milliseconds))
2447 			return true;
2448 		version(WithEncapsulatedSignals)
2449 			if(terminal.interrupted || terminal.windowSizeChanged || terminal.hangedUp)
2450 				return true;
2451 		version(WithSignals)
2452 			if(interrupted || windowSizeChanged || hangedUp)
2453 				return true;
2454 		return false;
2455 	}
2456 
2457 	/* private */ bool anyInput_internal(int timeout = 0) {
2458 		return timedCheckForInput(timeout);
2459 	}
2460 
2461 	bool timedCheckForInput_bypassingBuffer(int milliseconds) {
2462 		version(TerminalDirectToEmulator) {
2463 			if(!terminal.usingDirectEmulator)
2464 				return timedCheckForInput_bypassingBuffer_impl(milliseconds);
2465 
2466 			import core.time;
2467 			if(terminal.tew.terminalEmulator.pendingForApplication.length)
2468 				return true;
2469 			if(terminal.tew.terminalEmulator.outgoingSignal.wait(milliseconds.msecs))
2470 				// it was notified, but it could be left over from stuff we
2471 				// already processed... so gonna check the blocking conditions here too
2472 				// (FIXME: this sucks and is surely a race condition of pain)
2473 				return terminal.tew.terminalEmulator.pendingForApplication.length || terminal.interrupted || terminal.windowSizeChanged || terminal.hangedUp;
2474 			else
2475 				return false;
2476 		} else
2477 			return timedCheckForInput_bypassingBuffer_impl(milliseconds);
2478 	}
2479 
2480 	private bool timedCheckForInput_bypassingBuffer_impl(int milliseconds) {
2481 		version(Windows) {
2482 			auto response = WaitForSingleObject(inputHandle, milliseconds);
2483 			if(response  == 0)
2484 				return true; // the object is ready
2485 			return false;
2486 		} else version(Posix) {
2487 			if(fdIn == -1)
2488 				return false;
2489 
2490 			timeval tv;
2491 			tv.tv_sec = 0;
2492 			tv.tv_usec = milliseconds * 1000;
2493 
2494 			fd_set fs;
2495 			FD_ZERO(&fs);
2496 
2497 			FD_SET(fdIn, &fs);
2498 			int tries = 0;
2499 			try_again:
2500 			auto ret = select(fdIn + 1, &fs, null, null, &tv);
2501 			if(ret == -1) {
2502 				import core.stdc.errno;
2503 				if(errno == EINTR) {
2504 					tries++;
2505 					if(tries < 3)
2506 						goto try_again;
2507 				}
2508 				return false;
2509 			}
2510 			if(ret == 0)
2511 				return false;
2512 
2513 			return FD_ISSET(fdIn, &fs);
2514 		}
2515 	}
2516 
2517 	private dchar getchBuffer;
2518 
2519 	/// Get one key press from the terminal, discarding other
2520 	/// events in the process. Returns dchar.init upon receiving end-of-file.
2521 	///
2522 	/// 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.
2523 	dchar getch(bool nonblocking = false) {
2524 		if(getchBuffer != dchar.init) {
2525 			auto a = getchBuffer;
2526 			getchBuffer = dchar.init;
2527 			return a;
2528 		}
2529 
2530 		if(nonblocking && !anyInput_internal())
2531 			return dchar.init;
2532 
2533 		auto event = nextEvent();
2534 		while(event.type != InputEvent.Type.KeyboardEvent || event.keyboardEvent.pressed == false) {
2535 			if(event.type == InputEvent.Type.UserInterruptionEvent)
2536 				throw new UserInterruptionException();
2537 			if(event.type == InputEvent.Type.HangupEvent)
2538 				throw new HangupException();
2539 			if(event.type == InputEvent.Type.EndOfFileEvent)
2540 				return dchar.init;
2541 
2542 			if(nonblocking && !anyInput_internal())
2543 				return dchar.init;
2544 
2545 			event = nextEvent();
2546 		}
2547 		return event.keyboardEvent.which;
2548 	}
2549 
2550 	//char[128] inputBuffer;
2551 	//int inputBufferPosition;
2552 	int nextRaw(bool interruptable = false) {
2553 		version(TerminalDirectToEmulator) {
2554 			if(!terminal.usingDirectEmulator)
2555 				return nextRaw_impl(interruptable);
2556 			moar:
2557 			//if(interruptable && inputQueue.length)
2558 				//return -1;
2559 			if(terminal.tew.terminalEmulator.pendingForApplication.length == 0)
2560 				terminal.tew.terminalEmulator.outgoingSignal.wait();
2561 			synchronized(terminal.tew.terminalEmulator) {
2562 				if(terminal.tew.terminalEmulator.pendingForApplication.length == 0) {
2563 					if(interruptable)
2564 						return -1;
2565 					else
2566 						goto moar;
2567 				}
2568 				auto a = terminal.tew.terminalEmulator.pendingForApplication[0];
2569 				terminal.tew.terminalEmulator.pendingForApplication = terminal.tew.terminalEmulator.pendingForApplication[1 .. $];
2570 				return a;
2571 			}
2572 		} else
2573 			return nextRaw_impl(interruptable);
2574 	}
2575 	private int nextRaw_impl(bool interruptable = false) {
2576 		version(Posix) {
2577 			if(fdIn == -1)
2578 				return 0;
2579 
2580 			char[1] buf;
2581 			try_again:
2582 			auto ret = read(fdIn, buf.ptr, buf.length);
2583 			if(ret == 0)
2584 				return 0; // input closed
2585 			if(ret == -1) {
2586 				import core.stdc.errno;
2587 				if(errno == EINTR)
2588 					// interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop
2589 					if(interruptable)
2590 						return -1;
2591 					else
2592 						goto try_again;
2593 				else
2594 					throw new Exception("read failed");
2595 			}
2596 
2597 			//terminal.writef("RAW READ: %d\n", buf[0]);
2598 
2599 			if(ret == 1)
2600 				return inputPrefilter ? inputPrefilter(buf[0]) : buf[0];
2601 			else
2602 				assert(0); // read too much, should be impossible
2603 		} else version(Windows) {
2604 			char[1] buf;
2605 			DWORD d;
2606 			import std.conv;
2607 			if(!ReadFile(inputHandle, buf.ptr, cast(int) buf.length, &d, null))
2608 				throw new Exception("ReadFile " ~ to!string(GetLastError()));
2609 			return buf[0];
2610 		}
2611 	}
2612 
2613 	version(Posix)
2614 		int delegate(char) inputPrefilter;
2615 
2616 	// for VT
2617 	dchar nextChar(int starting) {
2618 		if(starting <= 127)
2619 			return cast(dchar) starting;
2620 		char[6] buffer;
2621 		int pos = 0;
2622 		buffer[pos++] = cast(char) starting;
2623 
2624 		// see the utf-8 encoding for details
2625 		int remaining = 0;
2626 		ubyte magic = starting & 0xff;
2627 		while(magic & 0b1000_000) {
2628 			remaining++;
2629 			magic <<= 1;
2630 		}
2631 
2632 		while(remaining && pos < buffer.length) {
2633 			buffer[pos++] = cast(char) nextRaw();
2634 			remaining--;
2635 		}
2636 
2637 		import std.utf;
2638 		size_t throwAway; // it insists on the index but we don't care
2639 		return decode(buffer[], throwAway);
2640 	}
2641 
2642 	InputEvent checkWindowSizeChanged() {
2643 		auto oldWidth = terminal.width;
2644 		auto oldHeight = terminal.height;
2645 		terminal.updateSize();
2646 		version(WithSignals)
2647 			windowSizeChanged = false;
2648 		version(WithEncapsulatedSignals)
2649 			terminal.windowSizeChanged = false;
2650 		return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal);
2651 	}
2652 
2653 
2654 	// character event
2655 	// non-character key event
2656 	// paste event
2657 	// mouse event
2658 	// size event maybe, and if appropriate focus events
2659 
2660 	/// Returns the next event.
2661 	///
2662 	/// Experimental: It is also possible to integrate this into
2663 	/// a generic event loop, currently under -version=with_eventloop and it will
2664 	/// require the module arsd.eventloop (Linux only at this point)
2665 	InputEvent nextEvent() {
2666 		terminal.flush();
2667 
2668 		wait_for_more:
2669 		version(WithSignals) {
2670 			if(interrupted) {
2671 				interrupted = false;
2672 				return InputEvent(UserInterruptionEvent(), terminal);
2673 			}
2674 
2675 			if(hangedUp) {
2676 				hangedUp = false;
2677 				return InputEvent(HangupEvent(), terminal);
2678 			}
2679 
2680 			if(windowSizeChanged) {
2681 				return checkWindowSizeChanged();
2682 			}
2683 		}
2684 
2685 		version(WithEncapsulatedSignals) {
2686 			if(terminal.interrupted) {
2687 				terminal.interrupted = false;
2688 				return InputEvent(UserInterruptionEvent(), terminal);
2689 			}
2690 
2691 			if(terminal.hangedUp) {
2692 				terminal.hangedUp = false;
2693 				return InputEvent(HangupEvent(), terminal);
2694 			}
2695 
2696 			if(terminal.windowSizeChanged) {
2697 				return checkWindowSizeChanged();
2698 			}
2699 		}
2700 
2701 		if(inputQueue.length) {
2702 			auto e = inputQueue[0];
2703 			inputQueue = inputQueue[1 .. $];
2704 			return e;
2705 		}
2706 
2707 		auto more = readNextEvents();
2708 		if(!more.length)
2709 			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
2710 
2711 		assert(more.length);
2712 
2713 		auto e = more[0];
2714 		inputQueue = more[1 .. $];
2715 		return e;
2716 	}
2717 
2718 	InputEvent* peekNextEvent() {
2719 		if(inputQueue.length)
2720 			return &(inputQueue[0]);
2721 		return null;
2722 	}
2723 
2724 	enum InjectionPosition { head, tail }
2725 	void injectEvent(InputEvent ev, InjectionPosition where) {
2726 		final switch(where) {
2727 			case InjectionPosition.head:
2728 				inputQueue = ev ~ inputQueue;
2729 			break;
2730 			case InjectionPosition.tail:
2731 				inputQueue ~= ev;
2732 			break;
2733 		}
2734 	}
2735 
2736 	InputEvent[] inputQueue;
2737 
2738 	InputEvent[] readNextEvents() {
2739 		if(UseVtSequences)
2740 			return readNextEventsVt();
2741 		else version(Win32Console)
2742 			return readNextEventsWin32();
2743 		else
2744 			assert(0);
2745 	}
2746 
2747 	version(Win32Console)
2748 	InputEvent[] readNextEventsWin32() {
2749 		terminal.flush(); // make sure all output is sent out before waiting for anything
2750 
2751 		INPUT_RECORD[32] buffer;
2752 		DWORD actuallyRead;
2753 			// FIXME: ReadConsoleInputW
2754 		auto success = ReadConsoleInputW(inputHandle, buffer.ptr, buffer.length, &actuallyRead);
2755 		if(success == 0)
2756 			throw new Exception("ReadConsoleInput");
2757 
2758 		InputEvent[] newEvents;
2759 		input_loop: foreach(record; buffer[0 .. actuallyRead]) {
2760 			switch(record.EventType) {
2761 				case KEY_EVENT:
2762 					auto ev = record.KeyEvent;
2763 					KeyboardEvent ke;
2764 					CharacterEvent e;
2765 					NonCharacterKeyEvent ne;
2766 
2767 					ke.pressed = ev.bKeyDown ? true : false;
2768 
2769 					// only send released events when specifically requested
2770 					// terminal.writefln("got %s %s", ev.UnicodeChar, ev.bKeyDown);
2771 					if(ev.UnicodeChar && ev.wVirtualKeyCode == VK_MENU && ev.bKeyDown == 0) {
2772 						// this indicates Windows is actually sending us
2773 						// an alt+xxx key sequence, may also be a unicode paste.
2774 						// either way, it cool.
2775 						ke.pressed = true;
2776 					} else {
2777 						if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown)
2778 							break;
2779 					}
2780 
2781 					e.eventType = ke.pressed ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released;
2782 					ne.eventType = ke.pressed ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released;
2783 
2784 					e.modifierState = ev.dwControlKeyState;
2785 					ne.modifierState = ev.dwControlKeyState;
2786 					ke.modifierState = ev.dwControlKeyState;
2787 
2788 					if(ev.UnicodeChar) {
2789 						// new style event goes first
2790 
2791 						if(ev.UnicodeChar == 3) {
2792 							// handling this internally for linux compat too
2793 							newEvents ~= InputEvent(UserInterruptionEvent(), terminal);
2794 						} else if(ev.UnicodeChar == '\r') {
2795 							// translating \r to \n for same result as linux...
2796 							ke.which = cast(dchar) cast(wchar) '\n';
2797 							newEvents ~= InputEvent(ke, terminal);
2798 
2799 							// old style event then follows as the fallback
2800 							e.character = cast(dchar) cast(wchar) '\n';
2801 							newEvents ~= InputEvent(e, terminal);
2802 						} else if(ev.wVirtualKeyCode == 0x1b) {
2803 							ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000);
2804 							newEvents ~= InputEvent(ke, terminal);
2805 
2806 							ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode;
2807 							newEvents ~= InputEvent(ne, terminal);
2808 						} else {
2809 							ke.which = cast(dchar) cast(wchar) ev.UnicodeChar;
2810 							newEvents ~= InputEvent(ke, terminal);
2811 
2812 							// old style event then follows as the fallback
2813 							e.character = cast(dchar) cast(wchar) ev.UnicodeChar;
2814 							newEvents ~= InputEvent(e, terminal);
2815 						}
2816 					} else {
2817 						// old style event
2818 						ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode;
2819 
2820 						// new style event. See comment on KeyboardEvent.Key
2821 						ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000);
2822 
2823 						// FIXME: make this better. the goal is to make sure the key code is a valid enum member
2824 						// Windows sends more keys than Unix and we're doing lowest common denominator here
2825 						foreach(member; __traits(allMembers, NonCharacterKeyEvent.Key))
2826 							if(__traits(getMember, NonCharacterKeyEvent.Key, member) == ne.key) {
2827 								newEvents ~= InputEvent(ke, terminal);
2828 								newEvents ~= InputEvent(ne, terminal);
2829 								break;
2830 							}
2831 					}
2832 				break;
2833 				case MOUSE_EVENT:
2834 					auto ev = record.MouseEvent;
2835 					MouseEvent e;
2836 
2837 					e.modifierState = ev.dwControlKeyState;
2838 					e.x = ev.dwMousePosition.X;
2839 					e.y = ev.dwMousePosition.Y;
2840 
2841 					switch(ev.dwEventFlags) {
2842 						case 0:
2843 							//press or release
2844 							e.eventType = MouseEvent.Type.Pressed;
2845 							static DWORD lastButtonState;
2846 							auto lastButtonState2 = lastButtonState;
2847 							e.buttons = ev.dwButtonState;
2848 							lastButtonState = e.buttons;
2849 
2850 							// this is sent on state change. if fewer buttons are pressed, it must mean released
2851 							if(cast(DWORD) e.buttons < lastButtonState2) {
2852 								e.eventType = MouseEvent.Type.Released;
2853 								// if last was 101 and now it is 100, then button far right was released
2854 								// so we flip the bits, ~100 == 011, then and them: 101 & 011 == 001, the
2855 								// button that was released
2856 								e.buttons = lastButtonState2 & ~e.buttons;
2857 							}
2858 						break;
2859 						case MOUSE_MOVED:
2860 							e.eventType = MouseEvent.Type.Moved;
2861 							e.buttons = ev.dwButtonState;
2862 						break;
2863 						case 0x0004/*MOUSE_WHEELED*/:
2864 							e.eventType = MouseEvent.Type.Pressed;
2865 							if(ev.dwButtonState > 0)
2866 								e.buttons = MouseEvent.Button.ScrollDown;
2867 							else
2868 								e.buttons = MouseEvent.Button.ScrollUp;
2869 						break;
2870 						default:
2871 							continue input_loop;
2872 					}
2873 
2874 					newEvents ~= InputEvent(e, terminal);
2875 				break;
2876 				case WINDOW_BUFFER_SIZE_EVENT:
2877 					auto ev = record.WindowBufferSizeEvent;
2878 					auto oldWidth = terminal.width;
2879 					auto oldHeight = terminal.height;
2880 					terminal._width = ev.dwSize.X;
2881 					terminal._height = ev.dwSize.Y;
2882 					newEvents ~= InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal);
2883 				break;
2884 				// FIXME: can we catch ctrl+c here too?
2885 				default:
2886 					// ignore
2887 			}
2888 		}
2889 
2890 		return newEvents;
2891 	}
2892 
2893 	// for UseVtSequences....
2894 	InputEvent[] readNextEventsVt() {
2895 		terminal.flush(); // make sure all output is sent out before we try to get input
2896 
2897 		// we want to starve the read, especially if we're called from an edge-triggered
2898 		// epoll (which might happen in version=with_eventloop.. impl detail there subject
2899 		// to change).
2900 		auto initial = readNextEventsHelper();
2901 
2902 		// lol this calls select() inside a function prolly called from epoll but meh,
2903 		// it is the simplest thing that can possibly work. The alternative would be
2904 		// doing non-blocking reads and buffering in the nextRaw function (not a bad idea
2905 		// btw, just a bit more of a hassle).
2906 		while(timedCheckForInput_bypassingBuffer(0)) {
2907 			auto ne = readNextEventsHelper();
2908 			initial ~= ne;
2909 			foreach(n; ne)
2910 				if(n.type == InputEvent.Type.EndOfFileEvent)
2911 					return initial; // hit end of file, get out of here lest we infinite loop
2912 					// (select still returns info available even after we read end of file)
2913 		}
2914 		return initial;
2915 	}
2916 
2917 	// The helper reads just one actual event from the pipe...
2918 	// for UseVtSequences....
2919 	InputEvent[] readNextEventsHelper(int remainingFromLastTime = int.max) {
2920 		InputEvent[] charPressAndRelease(dchar character) {
2921 			if((flags & ConsoleInputFlags.releasedKeys))
2922 				return [
2923 					// new style event
2924 					InputEvent(KeyboardEvent(true, character, 0), terminal),
2925 					InputEvent(KeyboardEvent(false, character, 0), terminal),
2926 					// old style event
2927 					InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal),
2928 					InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0), terminal),
2929 				];
2930 			else return [
2931 				// new style event
2932 				InputEvent(KeyboardEvent(true, character, 0), terminal),
2933 				// old style event
2934 				InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal)
2935 			];
2936 		}
2937 		InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) {
2938 			if((flags & ConsoleInputFlags.releasedKeys))
2939 				return [
2940 					// new style event FIXME: when the old events are removed, kill the +0xF0000 from here!
2941 					InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal),
2942 					InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal),
2943 					// old style event
2944 					InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal),
2945 					InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal),
2946 				];
2947 			else return [
2948 				// new style event FIXME: when the old events are removed, kill the +0xF0000 from here!
2949 				InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal),
2950 				// old style event
2951 				InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal)
2952 			];
2953 		}
2954 
2955 		InputEvent[] keyPressAndRelease2(dchar c, uint modifiers = 0) {
2956 			if((flags & ConsoleInputFlags.releasedKeys))
2957 				return [
2958 					InputEvent(KeyboardEvent(true, c, modifiers), terminal),
2959 					InputEvent(KeyboardEvent(false, c, modifiers), terminal),
2960 					// old style event
2961 					InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, c, modifiers), terminal),
2962 					InputEvent(CharacterEvent(CharacterEvent.Type.Released, c, modifiers), terminal),
2963 				];
2964 			else return [
2965 				InputEvent(KeyboardEvent(true, c, modifiers), terminal),
2966 				// old style event
2967 				InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, c, modifiers), terminal)
2968 			];
2969 
2970 		}
2971 
2972 		char[30] sequenceBuffer;
2973 
2974 		// this assumes you just read "\033["
2975 		char[] readEscapeSequence(char[] sequence) {
2976 			int sequenceLength = 2;
2977 			sequence[0] = '\033';
2978 			sequence[1] = '[';
2979 
2980 			while(sequenceLength < sequence.length) {
2981 				auto n = nextRaw();
2982 				sequence[sequenceLength++] = cast(char) n;
2983 				// I think a [ is supposed to termiate a CSI sequence
2984 				// but the Linux console sends CSI[A for F1, so I'm
2985 				// hacking it to accept that too
2986 				if(n >= 0x40 && !(sequenceLength == 3 && n == '['))
2987 					break;
2988 			}
2989 
2990 			return sequence[0 .. sequenceLength];
2991 		}
2992 
2993 		InputEvent[] translateTermcapName(string cap) {
2994 			switch(cap) {
2995 				//case "k0":
2996 					//return keyPressAndRelease(NonCharacterKeyEvent.Key.F1);
2997 				case "k1":
2998 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F1);
2999 				case "k2":
3000 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F2);
3001 				case "k3":
3002 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F3);
3003 				case "k4":
3004 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F4);
3005 				case "k5":
3006 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F5);
3007 				case "k6":
3008 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F6);
3009 				case "k7":
3010 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F7);
3011 				case "k8":
3012 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F8);
3013 				case "k9":
3014 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F9);
3015 				case "k;":
3016 				case "k0":
3017 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F10);
3018 				case "F1":
3019 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F11);
3020 				case "F2":
3021 					return keyPressAndRelease(NonCharacterKeyEvent.Key.F12);
3022 
3023 
3024 				case "kb":
3025 					return charPressAndRelease('\b');
3026 				case "kD":
3027 					return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete);
3028 
3029 				case "kd":
3030 				case "do":
3031 					return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow);
3032 				case "ku":
3033 				case "up":
3034 					return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow);
3035 				case "kl":
3036 					return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow);
3037 				case "kr":
3038 				case "nd":
3039 					return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow);
3040 
3041 				case "kN":
3042 				case "K5":
3043 					return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown);
3044 				case "kP":
3045 				case "K2":
3046 					return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp);
3047 
3048 				case "ho": // this might not be a key but my thing sometimes returns it... weird...
3049 				case "kh":
3050 				case "K1":
3051 					return keyPressAndRelease(NonCharacterKeyEvent.Key.Home);
3052 				case "kH":
3053 					return keyPressAndRelease(NonCharacterKeyEvent.Key.End);
3054 				case "kI":
3055 					return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert);
3056 				default:
3057 					// don't know it, just ignore
3058 					//import std.stdio;
3059 					//terminal.writeln(cap);
3060 			}
3061 
3062 			return null;
3063 		}
3064 
3065 
3066 		InputEvent[] doEscapeSequence(in char[] sequence) {
3067 			switch(sequence) {
3068 				case "\033[200~":
3069 					// bracketed paste begin
3070 					// we want to keep reading until
3071 					// "\033[201~":
3072 					// and build a paste event out of it
3073 
3074 
3075 					string data;
3076 					for(;;) {
3077 						auto n = nextRaw();
3078 						if(n == '\033') {
3079 							n = nextRaw();
3080 							if(n == '[') {
3081 								auto esc = readEscapeSequence(sequenceBuffer);
3082 								if(esc == "\033[201~") {
3083 									// complete!
3084 									break;
3085 								} else {
3086 									// was something else apparently, but it is pasted, so keep it
3087 									data ~= esc;
3088 								}
3089 							} else {
3090 								data ~= '\033';
3091 								data ~= cast(char) n;
3092 							}
3093 						} else {
3094 							data ~= cast(char) n;
3095 						}
3096 					}
3097 					return [InputEvent(PasteEvent(data), terminal)];
3098 				case "\033[220~":
3099 					// bracketed hyperlink begin (arsd extension)
3100 
3101 					string data;
3102 					for(;;) {
3103 						auto n = nextRaw();
3104 						if(n == '\033') {
3105 							n = nextRaw();
3106 							if(n == '[') {
3107 								auto esc = readEscapeSequence(sequenceBuffer);
3108 								if(esc == "\033[221~") {
3109 									// complete!
3110 									break;
3111 								} else {
3112 									// was something else apparently, but it is pasted, so keep it
3113 									data ~= esc;
3114 								}
3115 							} else {
3116 								data ~= '\033';
3117 								data ~= cast(char) n;
3118 							}
3119 						} else {
3120 							data ~= cast(char) n;
3121 						}
3122 					}
3123 
3124 					import std.string, std.conv;
3125 					auto idx = data.indexOf(";");
3126 					auto id = data[0 .. idx].to!ushort;
3127 					data = data[idx + 1 .. $];
3128 					idx = data.indexOf(";");
3129 					auto cmd = data[0 .. idx].to!ushort;
3130 					data = data[idx + 1 .. $];
3131 
3132 					return [InputEvent(LinkEvent(data, id, cmd), terminal)];
3133 				case "\033[M":
3134 					// mouse event
3135 					auto buttonCode = nextRaw() - 32;
3136 						// nextChar is commented because i'm not using UTF-8 mouse mode
3137 						// cuz i don't think it is as widely supported
3138 					auto x = cast(int) (/*nextChar*/(nextRaw())) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */
3139 					auto y = cast(int) (/*nextChar*/(nextRaw())) - 33; /* ditto */
3140 
3141 
3142 					bool isRelease = (buttonCode & 0b11) == 3;
3143 					int buttonNumber;
3144 					if(!isRelease) {
3145 						buttonNumber = (buttonCode & 0b11);
3146 						if(buttonCode & 64)
3147 							buttonNumber += 3; // button 4 and 5 are sent as like button 1 and 2, but code | 64
3148 							// so button 1 == button 4 here
3149 
3150 						// note: buttonNumber == 0 means button 1 at this point
3151 						buttonNumber++; // hence this
3152 
3153 
3154 						// apparently this considers middle to be button 2. but i want middle to be button 3.
3155 						if(buttonNumber == 2)
3156 							buttonNumber = 3;
3157 						else if(buttonNumber == 3)
3158 							buttonNumber = 2;
3159 					}
3160 
3161 					auto modifiers = buttonCode & (0b0001_1100);
3162 						// 4 == shift
3163 						// 8 == meta
3164 						// 16 == control
3165 
3166 					MouseEvent m;
3167 
3168 					if(buttonCode & 32)
3169 						m.eventType = MouseEvent.Type.Moved;
3170 					else
3171 						m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed;
3172 
3173 					// ugh, if no buttons are pressed, released and moved are indistinguishable...
3174 					// so we'll count the buttons down, and if we get a release
3175 					static int buttonsDown = 0;
3176 					if(!isRelease && buttonNumber <= 3) // exclude wheel "presses"...
3177 						buttonsDown++;
3178 
3179 					if(isRelease && m.eventType != MouseEvent.Type.Moved) {
3180 						if(buttonsDown)
3181 							buttonsDown--;
3182 						else // no buttons down, so this should be a motion instead..
3183 							m.eventType = MouseEvent.Type.Moved;
3184 					}
3185 
3186 
3187 					if(buttonNumber == 0)
3188 						m.buttons = 0; // we don't actually know :(
3189 					else
3190 						m.buttons = 1 << (buttonNumber - 1); // I prefer flags so that's how we do it
3191 					m.x = x;
3192 					m.y = y;
3193 					m.modifierState = modifiers;
3194 
3195 					return [InputEvent(m, terminal)];
3196 				default:
3197 					// screen doesn't actually do the modifiers, but
3198 					// it uses the same format so this branch still works fine.
3199 					if(terminal.terminalInFamily("xterm", "screen", "tmux")) {
3200 						import std.conv, std.string;
3201 						auto terminator = sequence[$ - 1];
3202 						auto parts = sequence[2 .. $ - 1].split(";");
3203 						// parts[0] and terminator tells us the key
3204 						// parts[1] tells us the modifierState
3205 
3206 						uint modifierState;
3207 
3208 						int modGot;
3209 						if(parts.length > 1)
3210 							modGot = to!int(parts[1]);
3211 						mod_switch: switch(modGot) {
3212 							case 2: modifierState |= ModifierState.shift; break;
3213 							case 3: modifierState |= ModifierState.alt; break;
3214 							case 4: modifierState |= ModifierState.shift | ModifierState.alt; break;
3215 							case 5: modifierState |= ModifierState.control; break;
3216 							case 6: modifierState |= ModifierState.shift | ModifierState.control; break;
3217 							case 7: modifierState |= ModifierState.alt | ModifierState.control; break;
3218 							case 8: modifierState |= ModifierState.shift | ModifierState.alt | ModifierState.control; break;
3219 							case 9:
3220 							..
3221 							case 16:
3222 								modifierState |= ModifierState.meta;
3223 								if(modGot != 9) {
3224 									modGot -= 8;
3225 									goto mod_switch;
3226 								}
3227 							break;
3228 
3229 							// this is an extension in my own terminal emulator
3230 							case 20:
3231 							..
3232 							case 36:
3233 								modifierState |= ModifierState.windows;
3234 								modGot -= 20;
3235 								goto mod_switch;
3236 							default:
3237 						}
3238 
3239 						switch(terminator) {
3240 							case 'A': return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow, modifierState);
3241 							case 'B': return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow, modifierState);
3242 							case 'C': return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow, modifierState);
3243 							case 'D': return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow, modifierState);
3244 
3245 							case 'H': return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState);
3246 							case 'F': return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState);
3247 
3248 							case 'P': return keyPressAndRelease(NonCharacterKeyEvent.Key.F1, modifierState);
3249 							case 'Q': return keyPressAndRelease(NonCharacterKeyEvent.Key.F2, modifierState);
3250 							case 'R': return keyPressAndRelease(NonCharacterKeyEvent.Key.F3, modifierState);
3251 							case 'S': return keyPressAndRelease(NonCharacterKeyEvent.Key.F4, modifierState);
3252 
3253 							case '~': // others
3254 								switch(parts[0]) {
3255 									case "1": return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState);
3256 									case "4": return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState);
3257 									case "5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp, modifierState);
3258 									case "6": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown, modifierState);
3259 									case "2": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert, modifierState);
3260 									case "3": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete, modifierState);
3261 
3262 									case "15": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5, modifierState);
3263 									case "17": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6, modifierState);
3264 									case "18": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7, modifierState);
3265 									case "19": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8, modifierState);
3266 									case "20": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9, modifierState);
3267 									case "21": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10, modifierState);
3268 									case "23": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11, modifierState);
3269 									case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState);
3270 
3271 									// starting at 70 i do some magic for like shift+enter etc.
3272 									// this only happens on my own terminal emulator.
3273 									case "70": return keyPressAndRelease(NonCharacterKeyEvent.Key.ScrollLock, modifierState);
3274 									case "78": return keyPressAndRelease2('\b', modifierState);
3275 									case "79": return keyPressAndRelease2('\t', modifierState);
3276 									case "83": return keyPressAndRelease2('\n', modifierState);
3277 									default:
3278 								}
3279 							break;
3280 
3281 							default:
3282 						}
3283 					} else if(terminal.terminalInFamily("rxvt")) {
3284 						// look it up in the termcap key database
3285 						string cap = terminal.findSequenceInTermcap(sequence);
3286 						if(cap !is null) {
3287 						//terminal.writeln("found in termcap " ~ cap);
3288 							return translateTermcapName(cap);
3289 						}
3290 						// FIXME: figure these out. rxvt seems to just change the terminator while keeping the rest the same
3291 						// though it isn't consistent. ugh.
3292 					} else {
3293 						// 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
3294 						// so this space is semi-intentionally left blank
3295 						//terminal.writeln("wtf ", sequence[1..$]);
3296 
3297 						// look it up in the termcap key database
3298 						string cap = terminal.findSequenceInTermcap(sequence);
3299 						if(cap !is null) {
3300 						//terminal.writeln("found in termcap " ~ cap);
3301 							return translateTermcapName(cap);
3302 						}
3303 					}
3304 			}
3305 
3306 			return null;
3307 		}
3308 
3309 		auto c = remainingFromLastTime == int.max ? nextRaw(true) : remainingFromLastTime;
3310 		if(c == -1)
3311 			return null; // interrupted; give back nothing so the other level can recheck signal flags
3312 		if(c == 0)
3313 			return [InputEvent(EndOfFileEvent(), terminal)];
3314 		if(c == '\033') {
3315 			if(timedCheckForInput_bypassingBuffer(50)) {
3316 				// escape sequence
3317 				c = nextRaw();
3318 				if(c == '[') { // CSI, ends on anything >= 'A'
3319 					return doEscapeSequence(readEscapeSequence(sequenceBuffer));
3320 				} else if(c == 'O') {
3321 					// could be xterm function key
3322 					auto n = nextRaw();
3323 
3324 					char[3] thing;
3325 					thing[0] = '\033';
3326 					thing[1] = 'O';
3327 					thing[2] = cast(char) n;
3328 
3329 					auto cap = terminal.findSequenceInTermcap(thing);
3330 					if(cap is null) {
3331 						return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~
3332 							charPressAndRelease('O') ~
3333 							charPressAndRelease(thing[2]);
3334 					} else {
3335 						return translateTermcapName(cap);
3336 					}
3337 				} else if(c == '\033') {
3338 					// could be escape followed by an escape sequence!
3339 					return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ readNextEventsHelper(c);
3340 				} else {
3341 					// I don't know, probably unsupported terminal or just quick user input or something
3342 					return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ charPressAndRelease(nextChar(c));
3343 				}
3344 			} else {
3345 				// user hit escape (or super slow escape sequence, but meh)
3346 				return keyPressAndRelease(NonCharacterKeyEvent.Key.escape);
3347 			}
3348 		} else {
3349 			// FIXME: what if it is neither? we should check the termcap
3350 			auto next = nextChar(c);
3351 			if(next == 127) // some terminals send 127 on the backspace. Let's normalize that.
3352 				next = '\b';
3353 			return charPressAndRelease(next);
3354 		}
3355 	}
3356 }
3357 
3358 /// The new style of keyboard event
3359 struct KeyboardEvent {
3360 	bool pressed; ///
3361 	dchar which; ///
3362 	alias key = which; /// I often use this when porting old to new so i took it
3363 	alias character = which; /// I often use this when porting old to new so i took it
3364 	uint modifierState; ///
3365 
3366 	///
3367 	bool isCharacter() {
3368 		return !(which >= Key.min && which <= Key.max);
3369 	}
3370 
3371 	// these match Windows virtual key codes numerically for simplicity of translation there
3372 	// but are plus a unicode private use area offset so i can cram them in the dchar
3373 	// http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx
3374 	/// .
3375 	enum Key : dchar {
3376 		escape = 0x1b + 0xF0000, /// .
3377 		F1 = 0x70 + 0xF0000, /// .
3378 		F2 = 0x71 + 0xF0000, /// .
3379 		F3 = 0x72 + 0xF0000, /// .
3380 		F4 = 0x73 + 0xF0000, /// .
3381 		F5 = 0x74 + 0xF0000, /// .
3382 		F6 = 0x75 + 0xF0000, /// .
3383 		F7 = 0x76 + 0xF0000, /// .
3384 		F8 = 0x77 + 0xF0000, /// .
3385 		F9 = 0x78 + 0xF0000, /// .
3386 		F10 = 0x79 + 0xF0000, /// .
3387 		F11 = 0x7A + 0xF0000, /// .
3388 		F12 = 0x7B + 0xF0000, /// .
3389 		LeftArrow = 0x25 + 0xF0000, /// .
3390 		RightArrow = 0x27 + 0xF0000, /// .
3391 		UpArrow = 0x26 + 0xF0000, /// .
3392 		DownArrow = 0x28 + 0xF0000, /// .
3393 		Insert = 0x2d + 0xF0000, /// .
3394 		Delete = 0x2e + 0xF0000, /// .
3395 		Home = 0x24 + 0xF0000, /// .
3396 		End = 0x23 + 0xF0000, /// .
3397 		PageUp = 0x21 + 0xF0000, /// .
3398 		PageDown = 0x22 + 0xF0000, /// .
3399 		ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator
3400 	}
3401 
3402 
3403 }
3404 
3405 /// Deprecated: use KeyboardEvent instead in new programs
3406 /// Input event for characters
3407 struct CharacterEvent {
3408 	/// .
3409 	enum Type {
3410 		Released, /// .
3411 		Pressed /// .
3412 	}
3413 
3414 	Type eventType; /// .
3415 	dchar character; /// .
3416 	uint modifierState; /// Don't depend on this to be available for character events
3417 }
3418 
3419 /// Deprecated: use KeyboardEvent instead in new programs
3420 struct NonCharacterKeyEvent {
3421 	/// .
3422 	enum Type {
3423 		Released, /// .
3424 		Pressed /// .
3425 	}
3426 	Type eventType; /// .
3427 
3428 	// these match Windows virtual key codes numerically for simplicity of translation there
3429 	//http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx
3430 	/// .
3431 	enum Key : int {
3432 		escape = 0x1b, /// .
3433 		F1 = 0x70, /// .
3434 		F2 = 0x71, /// .
3435 		F3 = 0x72, /// .
3436 		F4 = 0x73, /// .
3437 		F5 = 0x74, /// .
3438 		F6 = 0x75, /// .
3439 		F7 = 0x76, /// .
3440 		F8 = 0x77, /// .
3441 		F9 = 0x78, /// .
3442 		F10 = 0x79, /// .
3443 		F11 = 0x7A, /// .
3444 		F12 = 0x7B, /// .
3445 		LeftArrow = 0x25, /// .
3446 		RightArrow = 0x27, /// .
3447 		UpArrow = 0x26, /// .
3448 		DownArrow = 0x28, /// .
3449 		Insert = 0x2d, /// .
3450 		Delete = 0x2e, /// .
3451 		Home = 0x24, /// .
3452 		End = 0x23, /// .
3453 		PageUp = 0x21, /// .
3454 		PageDown = 0x22, /// .
3455 		ScrollLock = 0x91, /// unlikely to work outside my terminal emulator
3456 		}
3457 	Key key; /// .
3458 
3459 	uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms
3460 
3461 }
3462 
3463 /// .
3464 struct PasteEvent {
3465 	string pastedText; /// .
3466 }
3467 
3468 /++
3469 	Indicates a hyperlink was clicked in my custom terminal emulator
3470 	or with version `TerminalDirectToEmulator`.
3471 
3472 	You can simply ignore this event in a `final switch` if you aren't
3473 	using the feature.
3474 
3475 	History:
3476 		Added March 18, 2020
3477 +/
3478 struct LinkEvent {
3479 	string text; ///
3480 	ushort identifier; ///
3481 	ushort command; /// set by the terminal to indicate how it was clicked. values tbd
3482 }
3483 
3484 /// .
3485 struct MouseEvent {
3486 	// these match simpledisplay.d numerically as well
3487 	/// .
3488 	enum Type {
3489 		Moved = 0, /// .
3490 		Pressed = 1, /// .
3491 		Released = 2, /// .
3492 		Clicked, /// .
3493 	}
3494 
3495 	Type eventType; /// .
3496 
3497 	// note: these should numerically match simpledisplay.d for maximum beauty in my other code
3498 	/// .
3499 	enum Button : uint {
3500 		None = 0, /// .
3501 		Left = 1, /// .
3502 		Middle = 4, /// .
3503 		Right = 2, /// .
3504 		ScrollUp = 8, /// .
3505 		ScrollDown = 16 /// .
3506 	}
3507 	uint buttons; /// A mask of Button
3508 	int x; /// 0 == left side
3509 	int y; /// 0 == top
3510 	uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something
3511 }
3512 
3513 /// When you get this, check terminal.width and terminal.height to see the new size and react accordingly.
3514 struct SizeChangedEvent {
3515 	int oldWidth;
3516 	int oldHeight;
3517 	int newWidth;
3518 	int newHeight;
3519 }
3520 
3521 /// the user hitting ctrl+c will send this
3522 /// You should drop what you're doing and perhaps exit when this happens.
3523 struct UserInterruptionEvent {}
3524 
3525 /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent.
3526 /// If you receive it, you should generally cleanly exit.
3527 struct HangupEvent {}
3528 
3529 /// Sent upon receiving end-of-file from stdin.
3530 struct EndOfFileEvent {}
3531 
3532 interface CustomEvent {}
3533 
3534 version(Win32Console)
3535 enum ModifierState : uint {
3536 	shift = 0x10,
3537 	control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl
3538 
3539 	// i'm not sure if the next two are available
3540 	alt = 2 | 1, //2 ==left alt, 1 == right alt
3541 
3542 	// FIXME: I don't think these are actually available
3543 	windows = 512,
3544 	meta = 4096, // FIXME sanity
3545 
3546 	// I don't think this is available on Linux....
3547 	scrollLock = 0x40,
3548 }
3549 else
3550 enum ModifierState : uint {
3551 	shift = 4,
3552 	alt = 2,
3553 	control = 16,
3554 	meta = 8,
3555 
3556 	windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones
3557 }
3558 
3559 version(DDoc)
3560 ///
3561 enum ModifierState : uint {
3562 	///
3563 	shift = 4,
3564 	///
3565 	alt = 2,
3566 	///
3567 	control = 16,
3568 
3569 }
3570 
3571 /++
3572 	[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.
3573 ++/
3574 struct InputEvent {
3575 	/// .
3576 	enum Type {
3577 		KeyboardEvent, /// Keyboard key pressed (or released, where supported)
3578 		CharacterEvent, /// Do not use this in new programs, use KeyboardEvent instead
3579 		NonCharacterKeyEvent, /// Do not use this in new programs, use KeyboardEvent instead
3580 		PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead.
3581 		LinkEvent, /// User clicked a hyperlink you created. Simply ignore if you are not using that feature.
3582 		MouseEvent, /// only sent if you subscribed to mouse events
3583 		SizeChangedEvent, /// only sent if you subscribed to size events
3584 		UserInterruptionEvent, /// the user hit ctrl+c
3585 		EndOfFileEvent, /// stdin has received an end of file
3586 		HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator
3587 		CustomEvent /// .
3588 	}
3589 
3590 	/// If this event is deprecated, you should filter it out in new programs
3591 	bool isDeprecated() {
3592 		return type == Type.CharacterEvent || type == Type.NonCharacterKeyEvent;
3593 	}
3594 
3595 	/// .
3596 	@property Type type() { return t; }
3597 
3598 	/// Returns a pointer to the terminal associated with this event.
3599 	/// (You can usually just ignore this as there's only one terminal typically.)
3600 	///
3601 	/// It may be null in the case of program-generated events;
3602 	@property Terminal* terminal() { return term; }
3603 
3604 	/++
3605 		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.
3606 
3607 		See_Also:
3608 
3609 		The event types:
3610 			[KeyboardEvent], [MouseEvent], [SizeChangedEvent],
3611 			[PasteEvent], [UserInterruptionEvent], 
3612 			[EndOfFileEvent], [HangupEvent], [CustomEvent]
3613 
3614 		And associated functions:
3615 			[RealTimeConsoleInput], [ConsoleInputFlags]
3616 	++/
3617 	@property auto get(Type T)() {
3618 		if(type != T)
3619 			throw new Exception("Wrong event type");
3620 		static if(T == Type.CharacterEvent)
3621 			return characterEvent;
3622 		else static if(T == Type.KeyboardEvent)
3623 			return keyboardEvent;
3624 		else static if(T == Type.NonCharacterKeyEvent)
3625 			return nonCharacterKeyEvent;
3626 		else static if(T == Type.PasteEvent)
3627 			return pasteEvent;
3628 		else static if(T == Type.LinkEvent)
3629 			return linkEvent;
3630 		else static if(T == Type.MouseEvent)
3631 			return mouseEvent;
3632 		else static if(T == Type.SizeChangedEvent)
3633 			return sizeChangedEvent;
3634 		else static if(T == Type.UserInterruptionEvent)
3635 			return userInterruptionEvent;
3636 		else static if(T == Type.EndOfFileEvent)
3637 			return endOfFileEvent;
3638 		else static if(T == Type.HangupEvent)
3639 			return hangupEvent;
3640 		else static if(T == Type.CustomEvent)
3641 			return customEvent;
3642 		else static assert(0, "Type " ~ T.stringof ~ " not added to the get function");
3643 	}
3644 
3645 	/// custom event is public because otherwise there's no point at all
3646 	this(CustomEvent c, Terminal* p = null) {
3647 		t = Type.CustomEvent;
3648 		customEvent = c;
3649 	}
3650 
3651 	private {
3652 		this(CharacterEvent c, Terminal* p) {
3653 			t = Type.CharacterEvent;
3654 			characterEvent = c;
3655 		}
3656 		this(KeyboardEvent c, Terminal* p) {
3657 			t = Type.KeyboardEvent;
3658 			keyboardEvent = c;
3659 		}
3660 		this(NonCharacterKeyEvent c, Terminal* p) {
3661 			t = Type.NonCharacterKeyEvent;
3662 			nonCharacterKeyEvent = c;
3663 		}
3664 		this(PasteEvent c, Terminal* p) {
3665 			t = Type.PasteEvent;
3666 			pasteEvent = c;
3667 		}
3668 		this(LinkEvent c, Terminal* p) {
3669 			t = Type.LinkEvent;
3670 			linkEvent = c;
3671 		}
3672 		this(MouseEvent c, Terminal* p) {
3673 			t = Type.MouseEvent;
3674 			mouseEvent = c;
3675 		}
3676 		this(SizeChangedEvent c, Terminal* p) {
3677 			t = Type.SizeChangedEvent;
3678 			sizeChangedEvent = c;
3679 		}
3680 		this(UserInterruptionEvent c, Terminal* p) {
3681 			t = Type.UserInterruptionEvent;
3682 			userInterruptionEvent = c;
3683 		}
3684 		this(HangupEvent c, Terminal* p) {
3685 			t = Type.HangupEvent;
3686 			hangupEvent = c;
3687 		}
3688 		this(EndOfFileEvent c, Terminal* p) {
3689 			t = Type.EndOfFileEvent;
3690 			endOfFileEvent = c;
3691 		}
3692 
3693 		Type t;
3694 		Terminal* term;
3695 
3696 		union {
3697 			KeyboardEvent keyboardEvent;
3698 			CharacterEvent characterEvent;
3699 			NonCharacterKeyEvent nonCharacterKeyEvent;
3700 			PasteEvent pasteEvent;
3701 			MouseEvent mouseEvent;
3702 			SizeChangedEvent sizeChangedEvent;
3703 			UserInterruptionEvent userInterruptionEvent;
3704 			HangupEvent hangupEvent;
3705 			EndOfFileEvent endOfFileEvent;
3706 			LinkEvent linkEvent;
3707 			CustomEvent customEvent;
3708 		}
3709 	}
3710 }
3711 
3712 version(Demo)
3713 /// View the source of this!
3714 void main() {
3715 	auto terminal = Terminal(ConsoleOutputType.cellular);
3716 
3717 	//terminal.color(Color.DEFAULT, Color.DEFAULT);
3718 
3719 	//
3720 	///*
3721 	auto getter = new FileLineGetter(&terminal, "test");
3722 	getter.prompt = "> ";
3723 	getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
3724 	terminal.writeln("\n" ~ getter.getline());
3725 	terminal.writeln("\n" ~ getter.getline());
3726 	terminal.writeln("\n" ~ getter.getline());
3727 	getter.dispose();
3728 	//*/
3729 
3730 	terminal.writeln(terminal.getline());
3731 	terminal.writeln(terminal.getline());
3732 	terminal.writeln(terminal.getline());
3733 
3734 	//input.getch();
3735 
3736 	// return;
3737 	//
3738 
3739 	terminal.setTitle("Basic I/O");
3740 	auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEventsWithRelease);
3741 	terminal.color(Color.green | Bright, Color.black);
3742 
3743 	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");
3744 	terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY);
3745 
3746 	terminal.color(Color.DEFAULT, Color.DEFAULT);
3747 
3748 	int centerX = terminal.width / 2;
3749 	int centerY = terminal.height / 2;
3750 
3751 	bool timeToBreak = false;
3752 
3753 	terminal.hyperlink("test", 4);
3754 	terminal.hyperlink("another", 7);
3755 
3756 	void handleEvent(InputEvent event) {
3757 		//terminal.writef("%s\n", event.type);
3758 		final switch(event.type) {
3759 			case InputEvent.Type.LinkEvent:
3760 				auto ev = event.get!(InputEvent.Type.LinkEvent);
3761 				terminal.writeln(ev);
3762 			break;
3763 			case InputEvent.Type.UserInterruptionEvent:
3764 			case InputEvent.Type.HangupEvent:
3765 			case InputEvent.Type.EndOfFileEvent:
3766 				timeToBreak = true;
3767 				version(with_eventloop) {
3768 					import arsd.eventloop;
3769 					exit();
3770 				}
3771 			break;
3772 			case InputEvent.Type.SizeChangedEvent:
3773 				auto ev = event.get!(InputEvent.Type.SizeChangedEvent);
3774 				terminal.writeln(ev);
3775 			break;
3776 			case InputEvent.Type.KeyboardEvent:
3777 				auto ev = event.get!(InputEvent.Type.KeyboardEvent);
3778 					terminal.writef("\t%s", ev);
3779 				terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which);
3780 				terminal.writeln();
3781 				if(ev.which == 'Q') {
3782 					timeToBreak = true;
3783 					version(with_eventloop) {
3784 						import arsd.eventloop;
3785 						exit();
3786 					}
3787 				}
3788 
3789 				if(ev.which == 'C')
3790 					terminal.clear();
3791 			break;
3792 			case InputEvent.Type.CharacterEvent: // obsolete
3793 				auto ev = event.get!(InputEvent.Type.CharacterEvent);
3794 				terminal.writef("\t%s\n", ev);
3795 			break;
3796 			case InputEvent.Type.NonCharacterKeyEvent: // obsolete
3797 				terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent));
3798 			break;
3799 			case InputEvent.Type.PasteEvent:
3800 				terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent));
3801 			break;
3802 			case InputEvent.Type.MouseEvent:
3803 				//terminal.writef("\t%s\n", event.get!(InputEvent.Type.MouseEvent));
3804 			break;
3805 			case InputEvent.Type.CustomEvent:
3806 			break;
3807 		}
3808 
3809 		//terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY);
3810 
3811 		/*
3812 		if(input.kbhit()) {
3813 			auto c = input.getch();
3814 			if(c == 'q' || c == 'Q')
3815 				break;
3816 			terminal.moveTo(centerX, centerY);
3817 			terminal.writef("%c", c);
3818 			terminal.flush();
3819 		}
3820 		usleep(10000);
3821 		*/
3822 	}
3823 
3824 	version(with_eventloop) {
3825 		import arsd.eventloop;
3826 		addListener(&handleEvent);
3827 		loop();
3828 	} else {
3829 		loop: while(true) {
3830 			auto event = input.nextEvent();
3831 			handleEvent(event);
3832 			if(timeToBreak)
3833 				break loop;
3834 		}
3835 	}
3836 }
3837 
3838 enum TerminalCapabilities : uint {
3839 	minimal = 0,
3840 	vt100 = 1 << 0,
3841 
3842 	// my special terminal emulator extensions
3843 	arsdClipboard = 1 << 15, // 90 in caps
3844 	arsdImage = 1 << 16, // 91 in caps
3845 	arsdHyperlinks = 1 << 17, // 92 in caps
3846 }
3847 
3848 version(Posix)
3849 private uint /* TerminalCapabilities bitmask */ getTerminalCapabilities(int fdIn, int fdOut) {
3850 	if(fdIn == -1 || fdOut == -1)
3851 		return TerminalCapabilities.minimal;
3852 
3853 	import std.conv;
3854 	import core.stdc.errno;
3855 	import core.sys.posix.unistd;
3856 
3857 	ubyte[128] hack2;
3858 	termios old;
3859 	ubyte[128] hack;
3860 	tcgetattr(fdIn, &old);
3861 	auto n = old;
3862 	n.c_lflag &= ~(ICANON | ECHO);
3863 	tcsetattr(fdIn, TCSANOW, &n);
3864 	scope(exit)
3865 		tcsetattr(fdIn, TCSANOW, &old);
3866 
3867 	// drain the buffer? meh
3868 
3869 	string cmd = "\033[c";
3870 	auto err = write(fdOut, cmd.ptr, cmd.length);
3871 	if(err != cmd.length) {
3872 		throw new Exception("couldn't ask terminal for ID");
3873 	}
3874 
3875 	// reading directly to bypass any buffering
3876 	int retries = 16;
3877 	int len;
3878 	ubyte[96] buffer;
3879 	try_again:
3880 
3881 
3882 	timeval tv;
3883 	tv.tv_sec = 0;
3884 	tv.tv_usec = 250 * 1000; // 250 ms
3885 
3886 	fd_set fs;
3887 	FD_ZERO(&fs);
3888 
3889 	FD_SET(fdIn, &fs);
3890 	if(select(fdIn + 1, &fs, null, null, &tv) == -1) {
3891 		goto try_again;
3892 	}
3893 
3894 	if(FD_ISSET(fdIn, &fs)) {
3895 		auto len2 = read(fdIn, &buffer[len], buffer.length - len);
3896 		if(len2 <= 0) {
3897 			retries--;
3898 			if(retries > 0)
3899 				goto try_again;
3900 			throw new Exception("can't get terminal id");
3901 		} else {
3902 			len += len2;
3903 		}
3904 	} else {
3905 		// no data... assume terminal doesn't support giving an answer
3906 		return TerminalCapabilities.minimal;
3907 	}
3908 
3909 	ubyte[] answer;
3910 	bool hasAnswer(ubyte[] data) {
3911 		if(data.length < 4)
3912 			return false;
3913 		answer = null;
3914 		size_t start;
3915 		int position = 0;
3916 		foreach(idx, ch; data) {
3917 			switch(position) {
3918 				case 0:
3919 					if(ch == '\033') {
3920 						start = idx;
3921 						position++;
3922 					}
3923 				break;
3924 				case 1:
3925 					if(ch == '[')
3926 						position++;
3927 					else
3928 						position = 0;
3929 				break;
3930 				case 2:
3931 					if(ch == '?')
3932 						position++;
3933 					else
3934 						position = 0;
3935 				break;
3936 				case 3:
3937 					// body
3938 					if(ch == 'c') {
3939 						answer = data[start .. idx + 1];
3940 						return true;
3941 					} else if(ch == ';' || (ch >= '0' && ch <= '9')) {
3942 						// good, keep going
3943 					} else {
3944 						// invalid, drop it
3945 						position = 0;
3946 					}
3947 				break;
3948 				default: assert(0);
3949 			}
3950 		}
3951 		return false;
3952 	}
3953 
3954 	auto got = buffer[0 .. len];
3955 	if(!hasAnswer(got)) {
3956 		goto try_again;
3957 	}
3958 	auto gots = cast(char[]) answer[3 .. $-1];
3959 
3960 	import std.string;
3961 
3962 	auto pieces = split(gots, ";");
3963 	uint ret = TerminalCapabilities.vt100;
3964 	foreach(p; pieces)
3965 		switch(p) {
3966 			case "90":
3967 				ret |= TerminalCapabilities.arsdClipboard;
3968 			break;
3969 			case "91":
3970 				ret |= TerminalCapabilities.arsdImage;
3971 			break;
3972 			case "92":
3973 				ret |= TerminalCapabilities.arsdHyperlinks;
3974 			break;
3975 			default:
3976 		}
3977 	return ret;
3978 }
3979 
3980 private extern(C) int mkstemp(char *templ);
3981 
3982 /**
3983 	FIXME: support lines that wrap
3984 	FIXME: better controls maybe
3985 
3986 	FIXME: support multi-line "lines" and some form of line continuation, both
3987 	       from the user (if permitted) and from the application, so like the user
3988 	       hits "class foo { \n" and the app says "that line needs continuation" automatically.
3989 
3990 	FIXME: fix lengths on prompt and suggestion
3991 
3992 	A note on history:
3993 
3994 	To save history, you must call LineGetter.dispose() when you're done with it.
3995 	History will not be automatically saved without that call!
3996 
3997 	The history saving and loading as a trivially encountered race condition: if you
3998 	open two programs that use the same one at the same time, the one that closes second
3999 	will overwrite any history changes the first closer saved.
4000 
4001 	GNU Getline does this too... and it actually kinda drives me nuts. But I don't know
4002 	what a good fix is except for doing a transactional commit straight to the file every
4003 	time and that seems like hitting the disk way too often.
4004 
4005 	We could also do like a history server like a database daemon that keeps the order
4006 	correct but I don't actually like that either because I kinda like different bashes
4007 	to have different history, I just don't like it all to get lost.
4008 
4009 	Regardless though, this isn't even used in bash anyway, so I don't think I care enough
4010 	to put that much effort into it. Just using separate files for separate tasks is good
4011 	enough I think.
4012 */
4013 class LineGetter {
4014 	/* A note on the assumeSafeAppends in here: since these buffers are private, we can be
4015 	   pretty sure that stomping isn't an issue, so I'm using this liberally to keep the
4016 	   append/realloc code simple and hopefully reasonably fast. */
4017 
4018 	// saved to file
4019 	string[] history;
4020 
4021 	// not saved
4022 	Terminal* terminal;
4023 	string historyFilename;
4024 
4025 	/// Make sure that the parent terminal struct remains in scope for the duration
4026 	/// of LineGetter's lifetime, as it does hold on to and use the passed pointer
4027 	/// throughout.
4028 	///
4029 	/// historyFilename will load and save an input history log to a particular folder.
4030 	/// Leaving it null will mean no file will be used and history will not be saved across sessions.
4031 	this(Terminal* tty, string historyFilename = null) {
4032 		this.terminal = tty;
4033 		this.historyFilename = historyFilename;
4034 
4035 		line.reserve(128);
4036 
4037 		if(historyFilename.length)
4038 			loadSettingsAndHistoryFromFile();
4039 
4040 		regularForeground = cast(Color) terminal._currentForeground;
4041 		background = cast(Color) terminal._currentBackground;
4042 		suggestionForeground = Color.blue;
4043 	}
4044 
4045 	/// Call this before letting LineGetter die so it can do any necessary
4046 	/// cleanup and save the updated history to a file.
4047 	void dispose() {
4048 		if(historyFilename.length)
4049 			saveSettingsAndHistoryToFile();
4050 	}
4051 
4052 	/// Override this to change the directory where history files are stored
4053 	///
4054 	/// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows.
4055 	/* virtual */ string historyFileDirectory() {
4056 		version(Windows) {
4057 			char[1024] path;
4058 			// FIXME: this doesn't link because the crappy dmd lib doesn't have it
4059 			if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) {
4060 				import core.stdc.string;
4061 				return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline";
4062 			} else {
4063 				import std.process;
4064 				return environment["APPDATA"] ~ "\\arsd-getline";
4065 			}
4066 		} else version(Posix) {
4067 			import std.process;
4068 			return environment["HOME"] ~ "/.arsd-getline";
4069 		}
4070 	}
4071 
4072 	/// You can customize the colors here. You should set these after construction, but before
4073 	/// calling startGettingLine or getline.
4074 	Color suggestionForeground = Color.blue;
4075 	Color regularForeground = Color.DEFAULT; /// ditto
4076 	Color background = Color.DEFAULT; /// ditto
4077 	Color promptColor = Color.DEFAULT; /// ditto
4078 	Color specialCharBackground = Color.green; /// ditto
4079 	//bool reverseVideo;
4080 
4081 	/// Set this if you want a prompt to be drawn with the line. It does NOT support color in string.
4082 	@property void prompt(string p) {
4083 		this.prompt_ = p;
4084 
4085 		promptLength = 0;
4086 		foreach(dchar c; p)
4087 			promptLength++;
4088 	}
4089 
4090 	/// ditto
4091 	@property string prompt() {
4092 		return this.prompt_;
4093 	}
4094 
4095 	private string prompt_;
4096 	private int promptLength;
4097 
4098 	/++
4099 		Turn on auto suggest if you want a greyed thing of what tab
4100 		would be able to fill in as you type.
4101 
4102 		You might want to turn it off if generating a completion list is slow.
4103 
4104 		Or if you know you want it, be sure to turn it on explicitly in your
4105 		code because I reserve the right to change the default without advance notice.
4106 
4107 		History:
4108 			On March 4, 2020, I changed the default to `false` because it
4109 			is kinda slow and not useful in all cases.
4110 	+/
4111 	bool autoSuggest = false;
4112 
4113 	/++
4114 		Returns true if there was any input in the buffer. Can be
4115 		checked in the case of a [UserInterruptionException].
4116 	+/
4117 	bool hadInput() {
4118 		return line.length > 0;
4119 	}
4120 
4121 	/// Override this if you don't want all lines added to the history.
4122 	/// You can return null to not add it at all, or you can transform it.
4123 	/* virtual */ string historyFilter(string candidate) {
4124 		return candidate;
4125 	}
4126 
4127 	/// You may override this to do nothing
4128 	/* virtual */ void saveSettingsAndHistoryToFile() {
4129 		import std.file;
4130 		if(!exists(historyFileDirectory))
4131 			mkdir(historyFileDirectory);
4132 		auto fn = historyPath();
4133 		import std.stdio;
4134 		auto file = File(fn, "wt");
4135 		foreach(item; history)
4136 			file.writeln(item);
4137 	}
4138 
4139 	/++
4140 		History:
4141 			Introduced on January 31, 2020
4142 	+/
4143 	/* virtual */ string historyFileExtension() {
4144 		return ".history";
4145 	}
4146 
4147 	private string historyPath() {
4148 		import std.path;
4149 		auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension();
4150 		return filename;
4151 	}
4152 
4153 	/// You may override this to do nothing
4154 	/* virtual */ void loadSettingsAndHistoryFromFile() {
4155 		import std.file;
4156 		history = null;
4157 		auto fn = historyPath();
4158 		if(exists(fn)) {
4159 			import std.stdio;
4160 			foreach(line; File(fn, "rt").byLine)
4161 				history ~= line.idup;
4162 
4163 		}
4164 	}
4165 
4166 	/++
4167 		Override this to provide tab completion. You may use the candidate
4168 		argument to filter the list, but you don't have to (LineGetter will
4169 		do it for you on the values you return). This means you can ignore
4170 		the arguments if you like.
4171 
4172 		Ideally, you wouldn't return more than about ten items since the list
4173 		gets difficult to use if it is too long.
4174 
4175 		Tab complete cannot modify text before or after the cursor at this time.
4176 		I *might* change that later to allow tab complete to fuzzy search and spell
4177 		check fix before. But right now it ONLY inserts.
4178 
4179 		Default is to provide recent command history as autocomplete.
4180 
4181 		Returns:
4182 			This function should return the full string to replace
4183 			`candidate[tabCompleteStartPoint(args) .. $]`.
4184 			For example, if your user wrote `wri<tab>` and you want to complete
4185 			it to `write` or `writeln`, you should return `["write", "writeln"]`.
4186 
4187 			If you offer different tab complete in different places, you still
4188 			need to return the whole string. For example, a file competition of
4189 			a second argument, when the user writes `terminal.d term<tab>` and you
4190 			want it to complete to an additional `terminal.d`, you should return
4191 			`["terminal.d terminal.d"]`; in other words, `candidate ~ completion`
4192 			for each completion.
4193 
4194 			It does this so you can simply return an array of words without having
4195 			to rebuild that array for each combination.
4196 
4197 			To choose the word separator, override [tabCompleteStartPoint].
4198 
4199 		Params:
4200 			candidate = the text of the line up to the text cursor, after
4201 			which the completed text would be inserted
4202 
4203 			afterCursor = the remaining text after the cursor. You can inspect
4204 			this, but cannot change it - this will be appended to the line
4205 			after completion, keeping the cursor in the same relative location.
4206 
4207 		History:
4208 			Prior to January 30, 2020, this method took only one argument,
4209 			`candidate`. It now takes `afterCursor` as well, to allow you to
4210 			make more intelligent completions with full context.
4211 	+/
4212 	/* virtual */ protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) {
4213 		return history.length > 20 ? history[0 .. 20] : history;
4214 	}
4215 
4216 	/++
4217 		Override this to provide a different tab competition starting point. The default
4218 		is `0`, always completing the complete line, but you may return the index of another
4219 		character of `candidate` to provide a new split.
4220 
4221 		Returns:
4222 			The index of `candidate` where we should start the slice to keep in [tabComplete].
4223 			It must be `>= 0 && <= candidate.length`.
4224 
4225 		History:
4226 			Added on February 1, 2020. Initial default is to return 0 to maintain
4227 			old behavior.
4228 	+/
4229 	/* virtual */ protected size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) {
4230 		return 0;
4231 	}
4232 
4233 	/++
4234 		This gives extra information for an item when displaying tab competition details.
4235 
4236 		History:
4237 			Added January 31, 2020.
4238 
4239 	+/
4240 	/* virtual */ protected string tabCompleteHelp(string candidate) {
4241 		return null;
4242 	}
4243 
4244 	private string[] filterTabCompleteList(string[] list, size_t start) {
4245 		if(list.length == 0)
4246 			return list;
4247 
4248 		string[] f;
4249 		f.reserve(list.length);
4250 
4251 		foreach(item; list) {
4252 			import std.algorithm;
4253 			if(startsWith(item, line[start .. cursorPosition]))
4254 				f ~= item;
4255 		}
4256 
4257 		/+
4258 		// if it is excessively long, let's trim it down by trying to
4259 		// group common sub-sequences together.
4260 		if(f.length > terminal.height * 3 / 4) {
4261 			import std.algorithm;
4262 			f.sort();
4263 
4264 			// see how many can be saved by just keeping going until there is
4265 			// no more common prefix. then commit that and keep on down the list.
4266 			// since it is sorted, if there is a commonality, it should appear quickly
4267 			string[] n;
4268 			string commonality = f[0];
4269 			size_t idx = 1;
4270 			while(idx < f.length) {
4271 				auto c = commonPrefix(commonality, f[idx]);
4272 				if(c.length > cursorPosition - start) {
4273 					commonality = c;
4274 				} else {
4275 					n ~= commonality;
4276 					commonality = f[idx];
4277 				}
4278 				idx++;
4279 			}
4280 			if(commonality.length)
4281 				n ~= commonality;
4282 
4283 			if(n.length)
4284 				f = n;
4285 		}
4286 		+/
4287 
4288 		return f;
4289 	}
4290 
4291 	/++
4292 		Override this to provide a custom display of the tab completion list.
4293 
4294 		History:
4295 			Prior to January 31, 2020, it only displayed the list. After
4296 			that, it would call [tabCompleteHelp] for each candidate and display
4297 			that string (if present) as well.
4298 	+/
4299 	protected void showTabCompleteList(string[] list) {
4300 		if(list.length) {
4301 			// FIXME: allow mouse clicking of an item, that would be cool
4302 
4303 			auto start = tabCompleteStartPoint(line[0 .. cursorPosition], line[cursorPosition .. $]);
4304 
4305 			// FIXME: scroll
4306 			//if(terminal.type == ConsoleOutputType.linear) {
4307 				terminal.writeln();
4308 				foreach(item; list) {
4309 					terminal.color(suggestionForeground, background);
4310 					import std.utf;
4311 					auto idx = codeLength!char(line[start .. cursorPosition]);
4312 					terminal.write("  ", item[0 .. idx]);
4313 					terminal.color(regularForeground, background);
4314 					terminal.write(item[idx .. $]);
4315 					auto help = tabCompleteHelp(item);
4316 					if(help !is null) {
4317 						import std.string;
4318 						help = help.replace("\t", " ").replace("\n", " ").replace("\r", " ");
4319 						terminal.write("\t\t");
4320 						int remaining;
4321 						if(terminal.cursorX + 2 < terminal.width) {
4322 							remaining = terminal.width - terminal.cursorX - 2;
4323 						}
4324 						if(remaining > 8)
4325 							terminal.write(remaining < help.length ? help[0 .. remaining] : help);
4326 					}
4327 					terminal.writeln();
4328 
4329 				}
4330 				updateCursorPosition();
4331 				redraw();
4332 			//}
4333 		}
4334 	}
4335 
4336 	/++
4337 		Called by the default event loop when the user presses F1. Override
4338 		`showHelp` to change the UI, override [helpMessage] if you just want
4339 		to change the message.
4340 
4341 		History:
4342 			Introduced on January 30, 2020
4343 	+/
4344 	protected void showHelp() {
4345 		terminal.writeln();
4346 		terminal.writeln(helpMessage);
4347 		updateCursorPosition();
4348 		redraw();
4349 	}
4350 
4351 	/++
4352 		History:
4353 			Introduced on January 30, 2020
4354 	+/
4355 	protected string helpMessage() {
4356 		return "Press F2 to edit current line in your editor. F3 searches. F9 runs current line while maintaining current edit state.";
4357 	}
4358 
4359 	/++
4360 		History:
4361 			Introduced on January 30, 2020
4362 	+/
4363 	protected dchar[] editLineInEditor(in dchar[] line, in size_t cursorPosition) {
4364 		import std.conv;
4365 		import std.process;
4366 		import std.file;
4367 
4368 		char[] tmpName;
4369 
4370 		version(Windows) {
4371 			import core.stdc.string;
4372 			char[280] path;
4373 			auto l = GetTempPathA(cast(DWORD) path.length, path.ptr);
4374 			if(l == 0) throw new Exception("GetTempPathA");
4375 			path[l] = 0;
4376 			char[280] name;
4377 			auto r = GetTempFileNameA(path.ptr, "adr", 0, name.ptr);
4378 			if(r == 0) throw new Exception("GetTempFileNameA");
4379 			tmpName = name[0 .. strlen(name.ptr)];
4380 			scope(exit)
4381 				std.file.remove(tmpName);
4382 			std.file.write(tmpName, to!string(line));
4383 
4384 			string editor = environment.get("EDITOR", "notepad.exe");
4385 		} else {
4386 			import core.stdc.stdlib;
4387 			import core.sys.posix.unistd;
4388 			char[120] name;
4389 			string p = "/tmp/adrXXXXXX";
4390 			name[0 .. p.length] = p[];
4391 			name[p.length] = 0;
4392 			auto fd = mkstemp(name.ptr);
4393 			tmpName = name[0 .. p.length];
4394 			if(fd == -1) throw new Exception("mkstemp");
4395 			scope(exit)
4396 				close(fd);
4397 			scope(exit)
4398 				std.file.remove(tmpName);
4399 
4400 			string s = to!string(line);
4401 			while(s.length) {
4402 				auto x = write(fd, s.ptr, s.length);
4403 				if(x == -1) throw new Exception("write");
4404 				s = s[x .. $];
4405 			}
4406 			string editor = environment.get("EDITOR", "vi");
4407 		}
4408 
4409 		// FIXME the spawned process changes terminal state!
4410 
4411 		spawnProcess([editor, tmpName]).wait;
4412 		import std.string;
4413 		return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp;
4414 	}
4415 
4416 	//private RealTimeConsoleInput* rtci;
4417 
4418 	/// One-call shop for the main workhorse
4419 	/// If you already have a RealTimeConsoleInput ready to go, you
4420 	/// should pass a pointer to yours here. Otherwise, LineGetter will
4421 	/// make its own.
4422 	public string getline(RealTimeConsoleInput* input = null) {
4423 		startGettingLine();
4424 		if(input is null) {
4425 			auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents | ConsoleInputFlags.noEolWrap);
4426 			//rtci = &i;
4427 			//scope(exit) rtci = null;
4428 			while(workOnLine(i.nextEvent(), &i)) {}
4429 		} else {
4430 			//rtci = input;
4431 			//scope(exit) rtci = null;
4432 			while(workOnLine(input.nextEvent(), input)) {}
4433 		}
4434 		return finishGettingLine();
4435 	}
4436 
4437 	private int currentHistoryViewPosition = 0;
4438 	private dchar[] uncommittedHistoryCandidate;
4439 	void loadFromHistory(int howFarBack) {
4440 		if(howFarBack < 0)
4441 			howFarBack = 0;
4442 		if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around.
4443 			howFarBack = cast(int) history.length;
4444 		if(howFarBack == currentHistoryViewPosition)
4445 			return;
4446 		if(currentHistoryViewPosition == 0) {
4447 			// save the current line so we can down arrow back to it later
4448 			if(uncommittedHistoryCandidate.length < line.length) {
4449 				uncommittedHistoryCandidate.length = line.length;
4450 			}
4451 
4452 			uncommittedHistoryCandidate[0 .. line.length] = line[];
4453 			uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length];
4454 			uncommittedHistoryCandidate.assumeSafeAppend();
4455 		}
4456 
4457 		currentHistoryViewPosition = howFarBack;
4458 
4459 		if(howFarBack == 0) {
4460 			line.length = uncommittedHistoryCandidate.length;
4461 			line.assumeSafeAppend();
4462 			line[] = uncommittedHistoryCandidate[];
4463 		} else {
4464 			line = line[0 .. 0];
4465 			line.assumeSafeAppend();
4466 			foreach(dchar ch; history[$ - howFarBack])
4467 				line ~= ch;
4468 		}
4469 
4470 		cursorPosition = cast(int) line.length;
4471 		scrollToEnd();
4472 	}
4473 
4474 	bool insertMode = true;
4475 	bool multiLineMode = false;
4476 
4477 	private dchar[] line;
4478 	private int cursorPosition = 0;
4479 	private int horizontalScrollPosition = 0;
4480 
4481 	private void scrollToEnd() {
4482 		horizontalScrollPosition = (cast(int) line.length);
4483 		horizontalScrollPosition -= availableLineLength();
4484 		if(horizontalScrollPosition < 0)
4485 			horizontalScrollPosition = 0;
4486 	}
4487 
4488 	// used for redrawing the line in the right place
4489 	// and detecting mouse events on our line.
4490 	private int startOfLineX;
4491 	private int startOfLineY;
4492 
4493 	// private string[] cachedCompletionList;
4494 
4495 	// FIXME
4496 	// /// Note that this assumes the tab complete list won't change between actual
4497 	// /// presses of tab by the user. If you pass it a list, it will use it, but
4498 	// /// otherwise it will keep track of the last one to avoid calls to tabComplete.
4499 	private string suggestion(string[] list = null) {
4500 		import std.algorithm, std.utf;
4501 		auto relevantLineSection = line[0 .. cursorPosition];
4502 		auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]);
4503 		relevantLineSection = relevantLineSection[start .. $];
4504 		// FIXME: see about caching the list if we easily can
4505 		if(list is null)
4506 			list = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start);
4507 
4508 		if(list.length) {
4509 			string commonality = list[0];
4510 			foreach(item; list[1 .. $]) {
4511 				commonality = commonPrefix(commonality, item);
4512 			}
4513 
4514 			if(commonality.length) {
4515 				return commonality[codeLength!char(relevantLineSection) .. $];
4516 			}
4517 		}
4518 
4519 		return null;
4520 	}
4521 
4522 	/// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something.
4523 	/// You'll probably want to call redraw() after adding chars.
4524 	void addChar(dchar ch) {
4525 		assert(cursorPosition >= 0 && cursorPosition <= line.length);
4526 		if(cursorPosition == line.length)
4527 			line ~= ch;
4528 		else {
4529 			assert(line.length);
4530 			if(insertMode) {
4531 				line ~= ' ';
4532 				for(int i = cast(int) line.length - 2; i >= cursorPosition; i --)
4533 					line[i + 1] = line[i];
4534 			}
4535 			line[cursorPosition] = ch;
4536 		}
4537 		cursorPosition++;
4538 
4539 		if(cursorPosition >= horizontalScrollPosition + availableLineLength())
4540 			horizontalScrollPosition++;
4541 	}
4542 
4543 	/// .
4544 	void addString(string s) {
4545 		// FIXME: this could be more efficient
4546 		// but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still)
4547 		foreach(dchar ch; s)
4548 			addChar(ch);
4549 	}
4550 
4551 	/// Deletes the character at the current position in the line.
4552 	/// You'll probably want to call redraw() after deleting chars.
4553 	void deleteChar() {
4554 		if(cursorPosition == line.length)
4555 			return;
4556 		for(int i = cursorPosition; i < line.length - 1; i++)
4557 			line[i] = line[i + 1];
4558 		line = line[0 .. $-1];
4559 		line.assumeSafeAppend();
4560 	}
4561 
4562 	///
4563 	void deleteToEndOfLine() {
4564 		line = line[0 .. cursorPosition];
4565 		line.assumeSafeAppend();
4566 		//while(cursorPosition < line.length)
4567 			//deleteChar();
4568 	}
4569 
4570 	int availableLineLength() {
4571 		return terminal.width - startOfLineX - promptLength - 1;
4572 	}
4573 
4574 	private int lastDrawLength = 0;
4575 	void redraw() {
4576 		terminal.hideCursor();
4577 		scope(exit) {
4578 			version(Win32Console) {
4579 				// on Windows, we want to make sure all
4580 				// is displayed before the cursor jumps around
4581 				terminal.flush();
4582 				terminal.showCursor();
4583 			} else {
4584 				// but elsewhere, the showCursor is itself buffered,
4585 				// so we can do it all at once for a slight speed boost
4586 				terminal.showCursor();
4587 				//import std.string; import std.stdio; writeln(terminal.writeBuffer.replace("\033", "\\e"));
4588 				terminal.flush();
4589 			}
4590 		}
4591 		terminal.moveTo(startOfLineX, startOfLineY);
4592 
4593 		auto lineLength = availableLineLength();
4594 		if(lineLength < 0)
4595 			throw new Exception("too narrow terminal to draw");
4596 
4597 		terminal.color(promptColor, background);
4598 		terminal.write(prompt);
4599 		terminal.color(regularForeground, background);
4600 
4601 		auto towrite = line[horizontalScrollPosition .. $];
4602 		auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition;
4603 		auto cursorPositionToDrawY = 0;
4604 
4605 		int written = promptLength;
4606 
4607 		void specialChar(char c) {
4608 			terminal.color(regularForeground, specialCharBackground);
4609 			terminal.write(c);
4610 			terminal.color(regularForeground, background);
4611 
4612 			written++;
4613 			lineLength--;
4614 		}
4615 
4616 		void regularChar(dchar ch) {
4617 			import std.utf;
4618 			char[4] buffer;
4619 			auto l = encode(buffer, ch);
4620 			// note the Terminal buffers it so meh
4621 			terminal.write(buffer[0 .. l]);
4622 
4623 			written++;
4624 			lineLength--;
4625 		}
4626 
4627 		// FIXME: if there is a color at the end of the line it messes up as you scroll
4628 		// FIXME: need a way to go to multi-line editing
4629 
4630 		foreach(dchar ch; towrite) {
4631 			if(lineLength == 0)
4632 				break;
4633 			switch(ch) {
4634 				case '\n': specialChar('n'); break;
4635 				case '\r': specialChar('r'); break;
4636 				case '\a': specialChar('a'); break;
4637 				case '\t': specialChar('t'); break;
4638 				case '\b': specialChar('b'); break;
4639 				case '\033': specialChar('e'); break;
4640 				default:
4641 					regularChar(ch);
4642 			}
4643 		}
4644 
4645 		string suggestion;
4646 
4647 		if(lineLength >= 0) {
4648 			suggestion = ((cursorPosition == towrite.length) && autoSuggest) ? this.suggestion() : null;
4649 			if(suggestion.length) {
4650 				terminal.color(suggestionForeground, background);
4651 				foreach(dchar ch; suggestion) {
4652 					if(lineLength == 0)
4653 						break;
4654 					regularChar(ch);
4655 				}
4656 				terminal.color(regularForeground, background);
4657 			}
4658 		}
4659 
4660 		// FIXME: graphemes
4661 
4662 		if(written < lastDrawLength)
4663 		foreach(i; written .. lastDrawLength)
4664 			terminal.write(" ");
4665 		lastDrawLength = written;
4666 
4667 		terminal.moveTo(startOfLineX + cursorPositionToDrawX + promptLength, startOfLineY + cursorPositionToDrawY);
4668 	}
4669 
4670 	/// Starts getting a new line. Call workOnLine and finishGettingLine afterward.
4671 	///
4672 	/// Make sure that you've flushed your input and output before calling this
4673 	/// function or else you might lose events or get exceptions from this.
4674 	void startGettingLine() {
4675 		// reset from any previous call first
4676 		if(!maintainBuffer) {
4677 			cursorPosition = 0;
4678 			horizontalScrollPosition = 0;
4679 			justHitTab = false;
4680 			currentHistoryViewPosition = 0;
4681 			if(line.length) {
4682 				line = line[0 .. 0];
4683 				line.assumeSafeAppend();
4684 			}
4685 		}
4686 
4687 		maintainBuffer = false;
4688 
4689 		initializeWithSize(true);
4690 
4691 		terminal.cursor = TerminalCursor.insert;
4692 		terminal.showCursor();
4693 	}
4694 
4695 	private void positionCursor() {
4696 		if(cursorPosition == 0)
4697 			horizontalScrollPosition = 0;
4698 		else if(cursorPosition == line.length)
4699 			scrollToEnd();
4700 		else {
4701 			// otherwise just try to center it in the screen
4702 			horizontalScrollPosition = cursorPosition;
4703 			horizontalScrollPosition -= terminal.width / 2;
4704 			// align on a code point boundary
4705 			aligned(horizontalScrollPosition, -1);
4706 			if(horizontalScrollPosition < 0)
4707 				horizontalScrollPosition = 0;
4708 		}
4709 	}
4710 
4711 	private void aligned(ref int what, int direction) {
4712 		// whereas line is right now dchar[] no need for this
4713 		// at least until we go by grapheme...
4714 		/*
4715 		while(what > 0 && what < line.length && ((line[what] & 0b1100_0000) == 0b1000_0000))
4716 			what += direction;
4717 		*/
4718 	}
4719 
4720 	private void initializeWithSize(bool firstEver = false) {
4721 		auto x = startOfLineX;
4722 
4723 		updateCursorPosition();
4724 
4725 		if(!firstEver) {
4726 			startOfLineX = x;
4727 			positionCursor();
4728 		}
4729 
4730 		lastDrawLength = terminal.width - terminal.cursorX;
4731 		version(Win32Console)
4732 			lastDrawLength -= 1; // I don't like this but Windows resizing is different anyway and it is liable to scroll if i go over..
4733 
4734 		redraw();
4735 	}
4736 
4737 	private void updateCursorPosition() {
4738 		terminal.flush();
4739 
4740 		// then get the current cursor position to start fresh
4741 		version(TerminalDirectToEmulator) {
4742 			if(!terminal.usingDirectEmulator)
4743 				return updateCursorPosition_impl();
4744 			startOfLineX = terminal.tew.terminalEmulator.cursorX;
4745 			startOfLineY = terminal.tew.terminalEmulator.cursorY;
4746 		} else
4747 			updateCursorPosition_impl();
4748 	}
4749 	private void updateCursorPosition_impl() {
4750 		version(Win32Console) {
4751 			CONSOLE_SCREEN_BUFFER_INFO info;
4752 			GetConsoleScreenBufferInfo(terminal.hConsole, &info);
4753 			startOfLineX = info.dwCursorPosition.X;
4754 			startOfLineY = info.dwCursorPosition.Y;
4755 		} else version(Posix) {
4756 			// request current cursor position
4757 
4758 			// we have to turn off cooked mode to get this answer, otherwise it will all
4759 			// be messed up. (I hate unix terminals, the Windows way is so much easer.)
4760 
4761 			// We also can't use RealTimeConsoleInput here because it also does event loop stuff
4762 			// which would be broken by the child destructor :( (maybe that should be a FIXME)
4763 
4764 			/+
4765 			if(rtci !is null) {
4766 				while(rtci.timedCheckForInput_bypassingBuffer(1000))
4767 					rtci.inputQueue ~= rtci.readNextEvents();
4768 			}
4769 			+/
4770 
4771 			ubyte[128] hack2;
4772 			termios old;
4773 			ubyte[128] hack;
4774 			tcgetattr(terminal.fdIn, &old);
4775 			auto n = old;
4776 			n.c_lflag &= ~(ICANON | ECHO);
4777 			tcsetattr(terminal.fdIn, TCSANOW, &n);
4778 			scope(exit)
4779 				tcsetattr(terminal.fdIn, TCSANOW, &old);
4780 
4781 
4782 			terminal.writeStringRaw("\033[6n");
4783 			terminal.flush();
4784 
4785 			import std.conv;
4786 			import core.stdc.errno;
4787 
4788 			import core.sys.posix.unistd;
4789 
4790 			ubyte readOne() {
4791 				ubyte[1] buffer;
4792 				int tries = 0;
4793 				try_again:
4794 				if(tries > 30)
4795 					throw new Exception("terminal reply timed out");
4796 				auto len = read(terminal.fdIn, buffer.ptr, buffer.length);
4797 				if(len == -1) {
4798 					if(errno == EINTR)
4799 						goto try_again;
4800 					if(errno == EAGAIN || errno == EWOULDBLOCK) {
4801 						import core.thread;
4802 						Thread.sleep(10.msecs);
4803 						tries++;
4804 						goto try_again;
4805 					}
4806 				} else if(len == 0) {
4807 					throw new Exception("Couldn't get cursor position to initialize get line " ~ to!string(len) ~ " " ~ to!string(errno));
4808 				}
4809 
4810 				return buffer[0];
4811 			}
4812 
4813 			nextEscape:
4814 			while(readOne() != '\033') {}
4815 			if(readOne() != '[')
4816 				goto nextEscape;
4817 
4818 			int x, y;
4819 
4820 			// now we should have some numbers being like yyy;xxxR
4821 			// but there may be a ? in there too; DEC private mode format
4822 			// of the very same data.
4823 
4824 			x = 0;
4825 			y = 0;
4826 
4827 			auto b = readOne();
4828 
4829 			if(b == '?')
4830 				b = readOne(); // no big deal, just ignore and continue
4831 
4832 			nextNumberY:
4833 			if(b >= '0' || b <= '9') {
4834 				y *= 10;
4835 				y += b - '0';
4836 			} else goto nextEscape;
4837 
4838 			b = readOne();
4839 			if(b != ';')
4840 				goto nextNumberY;
4841 
4842 			nextNumberX:
4843 			b = readOne();
4844 			if(b >= '0' || b <= '9') {
4845 				x *= 10;
4846 				x += b - '0';
4847 			} else goto nextEscape;
4848 
4849 			b = readOne();
4850 			if(b != 'R')
4851 				goto nextEscape; // it wasn't the right thing it after all
4852 
4853 			startOfLineX = x - 1;
4854 			startOfLineY = y - 1;
4855 		}
4856 
4857 		// updating these too because I can with the more accurate info from above
4858 		terminal._cursorX = startOfLineX;
4859 		terminal._cursorY = startOfLineY;
4860 	}
4861 
4862 	private bool justHitTab;
4863 	private bool eof;
4864 
4865 	///
4866 	string delegate(string s) pastePreprocessor;
4867 
4868 	string defaultPastePreprocessor(string s) {
4869 		return s;
4870 	}
4871 
4872 	void showIndividualHelp(string help) {
4873 		terminal.writeln();
4874 		terminal.writeln(help);
4875 	}
4876 
4877 	private bool maintainBuffer;
4878 
4879 	/++
4880 		for integrating into another event loop
4881 		you can pass individual events to this and
4882 		the line getter will work on it
4883 
4884 		returns false when there's nothing more to do
4885 
4886 		History:
4887 			On February 17, 2020, it was changed to take
4888 			a new argument which should be the input source
4889 			where the event came from.
4890 	+/
4891 	bool workOnLine(InputEvent e, RealTimeConsoleInput* rtti = null) {
4892 		switch(e.type) {
4893 			case InputEvent.Type.EndOfFileEvent:
4894 				justHitTab = false;
4895 				eof = true;
4896 				// FIXME: this should be distinct from an empty line when hit at the beginning
4897 				return false;
4898 			//break;
4899 			case InputEvent.Type.KeyboardEvent:
4900 				auto ev = e.keyboardEvent;
4901 				if(ev.pressed == false)
4902 					return true;
4903 				/* Insert the character (unless it is backspace, tab, or some other control char) */
4904 				auto ch = ev.which;
4905 				switch(ch) {
4906 					version(Windows) case 26: // and this is really for Windows
4907 						goto case;
4908 					case 4: // ctrl+d will also send a newline-equivalent 
4909 						if(line.length == 0)
4910 							eof = true;
4911 						goto case;
4912 					case '\r':
4913 					case '\n':
4914 						justHitTab = false;
4915 						return false;
4916 					case '\t':
4917 						auto relevantLineSection = line[0 .. cursorPosition];
4918 						auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]);
4919 						relevantLineSection = relevantLineSection[start .. $];
4920 						auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start);
4921 						import std.utf;
4922 
4923 						if(possibilities.length == 1) {
4924 							auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $];
4925 							if(toFill.length) {
4926 								addString(toFill);
4927 								redraw();
4928 							} else {
4929 								auto help = this.tabCompleteHelp(possibilities[0]);
4930 								if(help.length) {
4931 									showIndividualHelp(help);
4932 									updateCursorPosition();
4933 									redraw();
4934 								}
4935 							}
4936 							justHitTab = false;
4937 						} else {
4938 							if(justHitTab) {
4939 								justHitTab = false;
4940 								showTabCompleteList(possibilities);
4941 							} else {
4942 								justHitTab = true;
4943 								/* fill it in with as much commonality as there is amongst all the suggestions */
4944 								auto suggestion = this.suggestion(possibilities);
4945 								if(suggestion.length) {
4946 									addString(suggestion);
4947 									redraw();
4948 								}
4949 							}
4950 						}
4951 					break;
4952 					case '\b':
4953 						justHitTab = false;
4954 						if(cursorPosition) {
4955 							cursorPosition--;
4956 							for(int i = cursorPosition; i < line.length - 1; i++)
4957 								line[i] = line[i + 1];
4958 							line = line[0 .. $ - 1];
4959 							line.assumeSafeAppend();
4960 
4961 							if(!multiLineMode) {
4962 								if(horizontalScrollPosition > cursorPosition - 1)
4963 									horizontalScrollPosition = cursorPosition - 1 - availableLineLength();
4964 								if(horizontalScrollPosition < 0)
4965 									horizontalScrollPosition = 0;
4966 							}
4967 
4968 							redraw();
4969 						}
4970 					break;
4971 					case KeyboardEvent.Key.escape:
4972 						justHitTab = false;
4973 						cursorPosition = 0;
4974 						horizontalScrollPosition = 0;
4975 						line = line[0 .. 0];
4976 						line.assumeSafeAppend();
4977 						redraw();
4978 					break;
4979 					case KeyboardEvent.Key.F1:
4980 						justHitTab = false;
4981 						showHelp();
4982 					break;
4983 					case KeyboardEvent.Key.F2:
4984 						justHitTab = false;
4985 						line = editLineInEditor(line, cursorPosition);
4986 						if(cursorPosition > line.length)
4987 							cursorPosition = cast(int) line.length;
4988 						if(horizontalScrollPosition > line.length)
4989 							horizontalScrollPosition = cast(int) line.length;
4990 						positionCursor();
4991 						redraw();
4992 					break;
4993 					case KeyboardEvent.Key.F3:
4994 					// case 'r' - 'a' + 1: // ctrl+r
4995 						justHitTab = false;
4996 						// search in history
4997 						// FIXME: what about search in completion too?
4998 					break;
4999 					case KeyboardEvent.Key.F4:
5000 						justHitTab = false;
5001 						// FIXME: clear line
5002 					break;
5003 					case KeyboardEvent.Key.F9:
5004 						justHitTab = false;
5005 						// compile and run analog; return the current string
5006 						// but keep the buffer the same
5007 						maintainBuffer = true;
5008 						return false;
5009 					case 0x1d: // ctrl+5, because of vim % shortcut
5010 						justHitTab = false;
5011 						// FIXME: find matching delimiter
5012 					break;
5013 					case KeyboardEvent.Key.LeftArrow:
5014 						justHitTab = false;
5015 						if(cursorPosition)
5016 							cursorPosition--;
5017 						if(ev.modifierState & ModifierState.control) {
5018 							while(cursorPosition && line[cursorPosition - 1] != ' ')
5019 								cursorPosition--;
5020 						}
5021 						aligned(cursorPosition, -1);
5022 
5023 						if(cursorPosition < horizontalScrollPosition)
5024 							positionCursor();
5025 
5026 						redraw();
5027 					break;
5028 					case KeyboardEvent.Key.RightArrow:
5029 						justHitTab = false;
5030 						if(cursorPosition < line.length)
5031 							cursorPosition++;
5032 
5033 						if(ev.modifierState & ModifierState.control) {
5034 							while(cursorPosition + 1 < line.length && line[cursorPosition + 1] != ' ')
5035 								cursorPosition++;
5036 							cursorPosition += 2;
5037 							if(cursorPosition > line.length)
5038 								cursorPosition = cast(int) line.length;
5039 						}
5040 						aligned(cursorPosition, 1);
5041 
5042 						if(cursorPosition > horizontalScrollPosition + availableLineLength())
5043 							positionCursor();
5044 
5045 						redraw();
5046 					break;
5047 					case KeyboardEvent.Key.UpArrow:
5048 						justHitTab = false;
5049 						loadFromHistory(currentHistoryViewPosition + 1);
5050 						redraw();
5051 					break;
5052 					case KeyboardEvent.Key.DownArrow:
5053 						justHitTab = false;
5054 						loadFromHistory(currentHistoryViewPosition - 1);
5055 						redraw();
5056 					break;
5057 					case KeyboardEvent.Key.PageUp:
5058 						justHitTab = false;
5059 						loadFromHistory(cast(int) history.length);
5060 						redraw();
5061 					break;
5062 					case KeyboardEvent.Key.PageDown:
5063 						justHitTab = false;
5064 						loadFromHistory(0);
5065 						redraw();
5066 					break;
5067 					case 1: // ctrl+a does home too in the emacs keybindings
5068 					case KeyboardEvent.Key.Home:
5069 						justHitTab = false;
5070 						cursorPosition = 0;
5071 						horizontalScrollPosition = 0;
5072 						redraw();
5073 					break;
5074 					case 5: // ctrl+e from emacs
5075 					case KeyboardEvent.Key.End:
5076 						justHitTab = false;
5077 						cursorPosition = cast(int) line.length;
5078 						scrollToEnd();
5079 						redraw();
5080 					break;
5081 					case ('v' - 'a' + 1):
5082 						if(rtti)
5083 							rtti.requestPasteFromClipboard();
5084 					break;
5085 					case KeyboardEvent.Key.Insert:
5086 						justHitTab = false;
5087 						if(ev.modifierState & ModifierState.shift) {
5088 							// paste
5089 
5090 							// shift+insert = request paste
5091 							// ctrl+insert = request copy. but that needs a selection
5092 
5093 							// those work on Windows!!!! and many linux TEs too.
5094 							// but if it does make it here, we'll attempt it at this level
5095 							if(rtti)
5096 								rtti.requestPasteFromClipboard();
5097 						} else if(ev.modifierState & ModifierState.control) {
5098 							// copy
5099 							// FIXME
5100 						} else {
5101 							insertMode = !insertMode;
5102 
5103 							if(insertMode)
5104 								terminal.cursor = TerminalCursor.insert;
5105 							else
5106 								terminal.cursor = TerminalCursor.block;
5107 						}
5108 					break;
5109 					case KeyboardEvent.Key.Delete:
5110 						justHitTab = false;
5111 						if(ev.modifierState & ModifierState.control)
5112 							deleteToEndOfLine();
5113 						else
5114 							deleteChar();
5115 						redraw();
5116 					break;
5117 					case 11: // ctrl+k is delete to end of line from emacs
5118 						justHitTab = false;
5119 						deleteToEndOfLine();
5120 						redraw();
5121 					break;
5122 					default:
5123 						justHitTab = false;
5124 						if(e.keyboardEvent.isCharacter)
5125 							addChar(ch);
5126 						redraw();
5127 				}
5128 			break;
5129 			case InputEvent.Type.PasteEvent:
5130 				justHitTab = false;
5131 				if(pastePreprocessor)
5132 					addString(pastePreprocessor(e.pasteEvent.pastedText));
5133 				else
5134 					addString(defaultPastePreprocessor(e.pasteEvent.pastedText));
5135 				redraw();
5136 			break;
5137 			case InputEvent.Type.MouseEvent:
5138 				/* Clicking with the mouse to move the cursor is so much easier than arrowing
5139 				   or even emacs/vi style movements much of the time, so I'ma support it. */
5140 
5141 				auto me = e.mouseEvent;
5142 				if(me.eventType == MouseEvent.Type.Pressed) {
5143 					if(me.buttons & MouseEvent.Button.Left) {
5144 						if(me.y == startOfLineY) {
5145 							int p = me.x - startOfLineX - promptLength + horizontalScrollPosition;
5146 							if(p >= 0 && p < line.length) {
5147 								justHitTab = false;
5148 								cursorPosition = p;
5149 								redraw();
5150 							}
5151 						}
5152 					}
5153 					if(me.buttons & MouseEvent.Button.Middle) {
5154 						if(rtti)
5155 							rtti.requestPasteFromPrimary();
5156 					}
5157 				}
5158 			break;
5159 			case InputEvent.Type.SizeChangedEvent:
5160 				/* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent
5161 				   yourself and then don't pass it to this function. */
5162 				// FIXME
5163 				initializeWithSize();
5164 			break;
5165 			case InputEvent.Type.UserInterruptionEvent:
5166 				/* I'll take this as canceling the line. */
5167 				throw new UserInterruptionException();
5168 			//break;
5169 			case InputEvent.Type.HangupEvent:
5170 				/* I'll take this as canceling the line. */
5171 				throw new HangupException();
5172 			//break;
5173 			default:
5174 				/* ignore. ideally it wouldn't be passed to us anyway! */
5175 		}
5176 
5177 		return true;
5178 	}
5179 
5180 	string finishGettingLine() {
5181 		import std.conv;
5182 		auto f = to!string(line);
5183 		auto history = historyFilter(f);
5184 		if(history !is null)
5185 			this.history ~= history;
5186 
5187 		// FIXME: we should hide the cursor if it was hidden in the call to startGettingLine
5188 		return eof ? null : f.length ? f : "";
5189 	}
5190 }
5191 
5192 /// Adds default constructors that just forward to the superclass
5193 mixin template LineGetterConstructors() {
5194 	this(Terminal* tty, string historyFilename = null) {
5195 		super(tty, historyFilename);
5196 	}
5197 }
5198 
5199 /// This is a line getter that customizes the tab completion to
5200 /// fill in file names separated by spaces, like a command line thing.
5201 class FileLineGetter : LineGetter {
5202 	mixin LineGetterConstructors;
5203 
5204 	/// You can set this property to tell it where to search for the files
5205 	/// to complete.
5206 	string searchDirectory = ".";
5207 
5208 	override size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) {
5209 		import std.string;
5210 		return candidate.lastIndexOf(" ") + 1;
5211 	}
5212 
5213 	override protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) {
5214 		import std.file, std.conv, std.algorithm, std.string;
5215 
5216 		string[] list;
5217 		foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) {
5218 			// both with and without the (searchDirectory ~ "/")
5219 			list ~= name[searchDirectory.length + 1 .. $];
5220 			list ~= name[0 .. $];
5221 		}
5222 
5223 		return list;
5224 	}
5225 }
5226 
5227 version(Windows) {
5228 	// to get the directory for saving history in the line things
5229 	enum CSIDL_APPDATA = 26;
5230 	extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR);
5231 }
5232 
5233 
5234 
5235 
5236 
5237 /* Like getting a line, printing a lot of lines is kinda important too, so I'm including
5238    that widget here too. */
5239 
5240 
5241 struct ScrollbackBuffer {
5242 
5243 	bool demandsAttention;
5244 
5245 	this(string name) {
5246 		this.name = name;
5247 	}
5248 
5249 	void write(T...)(T t) {
5250 		import std.conv : text;
5251 		addComponent(text(t), foreground_, background_, null);
5252 	}
5253 
5254 	void writeln(T...)(T t) {
5255 		write(t, "\n");
5256 	}
5257 
5258 	void writef(T...)(string fmt, T t) {
5259 		import std.format: format;
5260 		write(format(fmt, t));
5261 	}
5262 
5263 	void writefln(T...)(string fmt, T t) {
5264 		writef(fmt, t, "\n");
5265 	}
5266 
5267 	void clear() {
5268 		lines.clear();
5269 		clickRegions = null;
5270 		scrollbackPosition = 0;
5271 	}
5272 
5273 	int foreground_ = Color.DEFAULT, background_ = Color.DEFAULT;
5274 	void color(int foreground, int background) {
5275 		this.foreground_ = foreground;
5276 		this.background_ = background;
5277 	}
5278 
5279 	void addComponent(string text, int foreground, int background, bool delegate() onclick) {
5280 		if(lines.length == 0) {
5281 			addLine();
5282 		}
5283 		bool first = true;
5284 		import std.algorithm;
5285 		foreach(t; splitter(text, "\n")) {
5286 			if(!first) addLine();
5287 			first = false;
5288 			lines[$-1].components ~= LineComponent(t, foreground, background, onclick);
5289 		}
5290 	}
5291 
5292 	void addLine() {
5293 		lines ~= Line();
5294 		if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are
5295 			scrollbackPosition++;
5296 	}
5297 
5298 	void addLine(string line) {
5299 		lines ~= Line([LineComponent(line)]);
5300 		if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are
5301 			scrollbackPosition++;
5302 	}
5303 
5304 	void scrollUp(int lines = 1) {
5305 		scrollbackPosition += lines;
5306 		//if(scrollbackPosition >= this.lines.length)
5307 		//	scrollbackPosition = cast(int) this.lines.length - 1;
5308 	}
5309 
5310 	void scrollDown(int lines = 1) {
5311 		scrollbackPosition -= lines;
5312 		if(scrollbackPosition < 0)
5313 			scrollbackPosition = 0;
5314 	}
5315 
5316 	void scrollToBottom() {
5317 		scrollbackPosition = 0;
5318 	}
5319 
5320 	// this needs width and height to know how to word wrap it
5321 	void scrollToTop(int width, int height) {
5322 		scrollbackPosition = scrollTopPosition(width, height);
5323 	}
5324 
5325 
5326 
5327 
5328 	struct LineComponent {
5329 		string text;
5330 		bool isRgb;
5331 		union {
5332 			int color;
5333 			RGB colorRgb;
5334 		}
5335 		union {
5336 			int background;
5337 			RGB backgroundRgb;
5338 		}
5339 		bool delegate() onclick; // return true if you need to redraw
5340 
5341 		// 16 color ctor
5342 		this(string text, int color = Color.DEFAULT, int background = Color.DEFAULT, bool delegate() onclick = null) {
5343 			this.text = text;
5344 			this.color = color;
5345 			this.background = background;
5346 			this.onclick = onclick;
5347 			this.isRgb = false;
5348 		}
5349 
5350 		// true color ctor
5351 		this(string text, RGB colorRgb, RGB backgroundRgb = RGB(0, 0, 0), bool delegate() onclick = null) {
5352 			this.text = text;
5353 			this.colorRgb = colorRgb;
5354 			this.backgroundRgb = backgroundRgb;
5355 			this.onclick = onclick;
5356 			this.isRgb = true;
5357 		}
5358 	}
5359 
5360 	struct Line {
5361 		LineComponent[] components;
5362 		int length() {
5363 			int l = 0;
5364 			foreach(c; components)
5365 				l += c.text.length;
5366 			return l;
5367 		}
5368 	}
5369 
5370 	static struct CircularBuffer(T) {
5371 		T[] backing;
5372 
5373 		enum maxScrollback = 8192; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask...
5374 
5375 		int start;
5376 		int length_;
5377 
5378 		void clear() {
5379 			backing = null;
5380 			start = 0;
5381 			length_ = 0;
5382 		}
5383 
5384 		size_t length() {
5385 			return length_;
5386 		}
5387 
5388 		void opOpAssign(string op : "~")(T line) {
5389 			if(length_ < maxScrollback) {
5390 				backing.assumeSafeAppend();
5391 				backing ~= line;
5392 				length_++;
5393 			} else {
5394 				backing[start] = line;
5395 				start++;
5396 				if(start == maxScrollback)
5397 					start = 0;
5398 			}
5399 		}
5400 
5401 		ref T opIndex(int idx) {
5402 			return backing[(start + idx) % maxScrollback];
5403 		}
5404 		ref T opIndex(Dollar idx) {
5405 			return backing[(start + (length + idx.offsetFromEnd)) % maxScrollback];
5406 		}
5407 
5408 		CircularBufferRange opSlice(int startOfIteration, Dollar end) {
5409 			return CircularBufferRange(&this, startOfIteration, cast(int) length - startOfIteration + end.offsetFromEnd);
5410 		}
5411 		CircularBufferRange opSlice(int startOfIteration, int end) {
5412 			return CircularBufferRange(&this, startOfIteration, end - startOfIteration);
5413 		}
5414 		CircularBufferRange opSlice() {
5415 			return CircularBufferRange(&this, 0, cast(int) length);
5416 		}
5417 
5418 		static struct CircularBufferRange {
5419 			CircularBuffer* item;
5420 			int position;
5421 			int remaining;
5422 			this(CircularBuffer* item, int startOfIteration, int count) {
5423 				this.item = item;
5424 				position = startOfIteration;
5425 				remaining = count;
5426 			}
5427 
5428 			ref T front() { return (*item)[position]; }
5429 			bool empty() { return remaining <= 0; }
5430 			void popFront() {
5431 				position++;
5432 				remaining--;
5433 			}
5434 
5435 			ref T back() { return (*item)[remaining - 1 - position]; }
5436 			void popBack() {
5437 				remaining--;
5438 			}
5439 		}
5440 
5441 		static struct Dollar {
5442 			int offsetFromEnd;
5443 			Dollar opBinary(string op : "-")(int rhs) {
5444 				return Dollar(offsetFromEnd - rhs);
5445 			}
5446 		}
5447 		Dollar opDollar() { return Dollar(0); }
5448 	}
5449 
5450 	CircularBuffer!Line lines;
5451 	string name;
5452 
5453 	int x, y, width, height;
5454 
5455 	int scrollbackPosition;
5456 
5457 
5458 	int scrollTopPosition(int width, int height) {
5459 		int lineCount;
5460 
5461 		foreach_reverse(line; lines) {
5462 			int written = 0;
5463 			comp_loop: foreach(cidx, component; line.components) {
5464 				auto towrite = component.text;
5465 				foreach(idx, dchar ch; towrite) {
5466 					if(written >= width) {
5467 						lineCount++;
5468 						written = 0;
5469 					}
5470 
5471 					if(ch == '\t')
5472 						written += 8; // FIXME
5473 					else
5474 						written++;
5475 				}
5476 			}
5477 			lineCount++;
5478 		}
5479 
5480 		//if(lineCount > height)
5481 			return lineCount - height;
5482 		//return 0;
5483 	}
5484 
5485 	void drawInto(Terminal* terminal, in int x = 0, in int y = 0, int width = 0, int height = 0) {
5486 		if(lines.length == 0)
5487 			return;
5488 
5489 		if(width == 0)
5490 			width = terminal.width;
5491 		if(height == 0)
5492 			height = terminal.height;
5493 
5494 		this.x = x;
5495 		this.y = y;
5496 		this.width = width;
5497 		this.height = height;
5498 
5499 		/* We need to figure out how much is going to fit
5500 		   in a first pass, so we can figure out where to
5501 		   start drawing */
5502 
5503 		int remaining = height + scrollbackPosition;
5504 		int start = cast(int) lines.length;
5505 		int howMany = 0;
5506 
5507 		bool firstPartial = false;
5508 
5509 		static struct Idx {
5510 			size_t cidx;
5511 			size_t idx;
5512 		}
5513 
5514 		Idx firstPartialStartIndex;
5515 
5516 		// this is private so I know we can safe append
5517 		clickRegions.length = 0;
5518 		clickRegions.assumeSafeAppend();
5519 
5520 		// FIXME: should prolly handle \n and \r in here too.
5521 
5522 		// we'll work backwards to figure out how much will fit...
5523 		// this will give accurate per-line things even with changing width and wrapping
5524 		// while being generally efficient - we usually want to show the end of the list
5525 		// anyway; actually using the scrollback is a bit of an exceptional case.
5526 
5527 		// It could probably do this instead of on each redraw, on each resize or insertion.
5528 		// or at least cache between redraws until one of those invalidates it.
5529 		foreach_reverse(line; lines) {
5530 			int written = 0;
5531 			int brokenLineCount;
5532 			Idx[16] lineBreaksBuffer;
5533 			Idx[] lineBreaks = lineBreaksBuffer[];
5534 			comp_loop: foreach(cidx, component; line.components) {
5535 				auto towrite = component.text;
5536 				foreach(idx, dchar ch; towrite) {
5537 					if(written >= width) {
5538 						if(brokenLineCount == lineBreaks.length)
5539 							lineBreaks ~= Idx(cidx, idx);
5540 						else
5541 							lineBreaks[brokenLineCount] = Idx(cidx, idx);
5542 
5543 						brokenLineCount++;
5544 
5545 						written = 0;
5546 					}
5547 
5548 					if(ch == '\t')
5549 						written += 8; // FIXME
5550 					else
5551 						written++;
5552 				}
5553 			}
5554 
5555 			lineBreaks = lineBreaks[0 .. brokenLineCount];
5556 
5557 			foreach_reverse(lineBreak; lineBreaks) {
5558 				if(remaining == 1) {
5559 					firstPartial = true;
5560 					firstPartialStartIndex = lineBreak;
5561 					break;
5562 				} else {
5563 					remaining--;
5564 				}
5565 				if(remaining <= 0)
5566 					break;
5567 			}
5568 
5569 			remaining--;
5570 
5571 			start--;
5572 			howMany++;
5573 			if(remaining <= 0)
5574 				break;
5575 		}
5576 
5577 		// second pass: actually draw it
5578 		int linePos = remaining;
5579 
5580 		foreach(line; lines[start .. start + howMany]) {
5581 			int written = 0;
5582 
5583 			if(linePos < 0) {
5584 				linePos++;
5585 				continue;
5586 			}
5587 		
5588 			terminal.moveTo(x, y + ((linePos >= 0) ? linePos : 0));
5589 
5590 			auto todo = line.components;
5591 
5592 			if(firstPartial) {
5593 				todo = todo[firstPartialStartIndex.cidx .. $];
5594 			}
5595 
5596 			foreach(ref component; todo) {
5597 				if(component.isRgb)
5598 					terminal.setTrueColor(component.colorRgb, component.backgroundRgb);
5599 				else
5600 					terminal.color(component.color, component.background);
5601 				auto towrite = component.text;
5602 
5603 				again:
5604 
5605 				if(linePos >= height)
5606 					break;
5607 
5608 				if(firstPartial) {
5609 					towrite = towrite[firstPartialStartIndex.idx .. $];
5610 					firstPartial = false;
5611 				}
5612 
5613 				foreach(idx, dchar ch; towrite) {
5614 					if(written >= width) {
5615 						clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written);
5616 						terminal.write(towrite[0 .. idx]);
5617 						towrite = towrite[idx .. $];
5618 						linePos++;
5619 						written = 0;
5620 						terminal.moveTo(x, y + linePos);
5621 						goto again;
5622 					}
5623 
5624 					if(ch == '\t')
5625 						written += 8; // FIXME
5626 					else
5627 						written++;
5628 				}
5629 
5630 				if(towrite.length) {
5631 					clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written);
5632 					terminal.write(towrite);
5633 				}
5634 			}
5635 
5636 			if(written < width) {
5637 				terminal.color(Color.DEFAULT, Color.DEFAULT);
5638 				foreach(i; written .. width)
5639 					terminal.write(" ");
5640 			}
5641 
5642 			linePos++;
5643 
5644 			if(linePos >= height)
5645 				break;
5646 		}
5647 
5648 		if(linePos < height) {
5649 			terminal.color(Color.DEFAULT, Color.DEFAULT);
5650 			foreach(i; linePos .. height) {
5651 				if(i >= 0 && i < height) {
5652 					terminal.moveTo(x, y + i);
5653 					foreach(w; 0 .. width)
5654 						terminal.write(" ");
5655 				}
5656 			}
5657 		}
5658 	}
5659 
5660 	private struct ClickRegion {
5661 		LineComponent* component;
5662 		int xStart;
5663 		int yStart;
5664 		int length;
5665 	}
5666 	private ClickRegion[] clickRegions;
5667 
5668 	/// Default event handling for this widget. Call this only after drawing it into a rectangle
5669 	/// and only if the event ought to be dispatched to it (which you determine however you want;
5670 	/// you could dispatch all events to it, or perhaps filter some out too)
5671 	///
5672 	/// Returns true if it should be redrawn
5673 	bool handleEvent(InputEvent e) {
5674 		final switch(e.type) {
5675 			case InputEvent.Type.LinkEvent:
5676 				// meh
5677 			break;
5678 			case InputEvent.Type.KeyboardEvent:
5679 				auto ev = e.keyboardEvent;
5680 
5681 				demandsAttention = false;
5682 
5683 				switch(ev.which) {
5684 					case KeyboardEvent.Key.UpArrow:
5685 						scrollUp();
5686 						return true;
5687 					case KeyboardEvent.Key.DownArrow:
5688 						scrollDown();
5689 						return true;
5690 					case KeyboardEvent.Key.PageUp:
5691 						scrollUp(height);
5692 						return true;
5693 					case KeyboardEvent.Key.PageDown:
5694 						scrollDown(height);
5695 						return true;
5696 					default:
5697 						// ignore
5698 				}
5699 			break;
5700 			case InputEvent.Type.MouseEvent:
5701 				auto ev = e.mouseEvent;
5702 				if(ev.x >= x && ev.x < x + width && ev.y >= y && ev.y < y + height) {
5703 					demandsAttention = false;
5704 					// it is inside our box, so do something with it
5705 					auto mx = ev.x - x;
5706 					auto my = ev.y - y;
5707 
5708 					if(ev.eventType == MouseEvent.Type.Pressed) {
5709 						if(ev.buttons & MouseEvent.Button.Left) {
5710 							foreach(region; clickRegions)
5711 								if(ev.x >= region.xStart && ev.x < region.xStart + region.length && ev.y == region.yStart)
5712 									if(region.component.onclick !is null)
5713 										return region.component.onclick();
5714 						}
5715 						if(ev.buttons & MouseEvent.Button.ScrollUp) {
5716 							scrollUp();
5717 							return true;
5718 						}
5719 						if(ev.buttons & MouseEvent.Button.ScrollDown) {
5720 							scrollDown();
5721 							return true;
5722 						}
5723 					}
5724 				} else {
5725 					// outside our area, free to ignore
5726 				}
5727 			break;
5728 			case InputEvent.Type.SizeChangedEvent:
5729 				// (size changed might be but it needs to be handled at a higher level really anyway)
5730 				// though it will return true because it probably needs redrawing anyway.
5731 				return true;
5732 			case InputEvent.Type.UserInterruptionEvent:
5733 				throw new UserInterruptionException();
5734 			case InputEvent.Type.HangupEvent:
5735 				throw new HangupException();
5736 			case InputEvent.Type.EndOfFileEvent:
5737 				// ignore, not relevant to this
5738 			break;
5739 			case InputEvent.Type.CharacterEvent:
5740 			case InputEvent.Type.NonCharacterKeyEvent:
5741 				// obsolete, ignore them until they are removed
5742 			break;
5743 			case InputEvent.Type.CustomEvent:
5744 			case InputEvent.Type.PasteEvent:
5745 				// ignored, not relevant to us
5746 			break;
5747 		}
5748 
5749 		return false;
5750 	}
5751 }
5752 
5753 
5754 class UserInterruptionException : Exception {
5755 	this() { super("Ctrl+C"); }
5756 }
5757 class HangupException : Exception {
5758 	this() { super("Hup"); }
5759 }
5760 
5761 
5762 
5763 /*
5764 
5765 	// more efficient scrolling
5766 	http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx
5767 	// and the unix sequences
5768 
5769 
5770 	rxvt documentation:
5771 	use this to finish the input magic for that
5772 
5773 
5774        For the keypad, use Shift to temporarily override Application-Keypad
5775        setting use Num_Lock to toggle Application-Keypad setting if Num_Lock
5776        is off, toggle Application-Keypad setting. Also note that values of
5777        Home, End, Delete may have been compiled differently on your system.
5778 
5779                          Normal       Shift         Control      Ctrl+Shift
5780        Tab               ^I           ESC [ Z       ^I           ESC [ Z
5781        BackSpace         ^H           ^?            ^?           ^?
5782        Find              ESC [ 1 ~    ESC [ 1 $     ESC [ 1 ^    ESC [ 1 @
5783        Insert            ESC [ 2 ~    paste         ESC [ 2 ^    ESC [ 2 @
5784        Execute           ESC [ 3 ~    ESC [ 3 $     ESC [ 3 ^    ESC [ 3 @
5785        Select            ESC [ 4 ~    ESC [ 4 $     ESC [ 4 ^    ESC [ 4 @
5786        Prior             ESC [ 5 ~    scroll-up     ESC [ 5 ^    ESC [ 5 @
5787        Next              ESC [ 6 ~    scroll-down   ESC [ 6 ^    ESC [ 6 @
5788        Home              ESC [ 7 ~    ESC [ 7 $     ESC [ 7 ^    ESC [ 7 @
5789        End               ESC [ 8 ~    ESC [ 8 $     ESC [ 8 ^    ESC [ 8 @
5790        Delete            ESC [ 3 ~    ESC [ 3 $     ESC [ 3 ^    ESC [ 3 @
5791        F1                ESC [ 11 ~   ESC [ 23 ~    ESC [ 11 ^   ESC [ 23 ^
5792        F2                ESC [ 12 ~   ESC [ 24 ~    ESC [ 12 ^   ESC [ 24 ^
5793        F3                ESC [ 13 ~   ESC [ 25 ~    ESC [ 13 ^   ESC [ 25 ^
5794        F4                ESC [ 14 ~   ESC [ 26 ~    ESC [ 14 ^   ESC [ 26 ^
5795        F5                ESC [ 15 ~   ESC [ 28 ~    ESC [ 15 ^   ESC [ 28 ^
5796        F6                ESC [ 17 ~   ESC [ 29 ~    ESC [ 17 ^   ESC [ 29 ^
5797        F7                ESC [ 18 ~   ESC [ 31 ~    ESC [ 18 ^   ESC [ 31 ^
5798        F8                ESC [ 19 ~   ESC [ 32 ~    ESC [ 19 ^   ESC [ 32 ^
5799        F9                ESC [ 20 ~   ESC [ 33 ~    ESC [ 20 ^   ESC [ 33 ^
5800        F10               ESC [ 21 ~   ESC [ 34 ~    ESC [ 21 ^   ESC [ 34 ^
5801        F11               ESC [ 23 ~   ESC [ 23 $    ESC [ 23 ^   ESC [ 23 @
5802        F12               ESC [ 24 ~   ESC [ 24 $    ESC [ 24 ^   ESC [ 24 @
5803        F13               ESC [ 25 ~   ESC [ 25 $    ESC [ 25 ^   ESC [ 25 @
5804        F14               ESC [ 26 ~   ESC [ 26 $    ESC [ 26 ^   ESC [ 26 @
5805        F15 (Help)        ESC [ 28 ~   ESC [ 28 $    ESC [ 28 ^   ESC [ 28 @
5806        F16 (Menu)        ESC [ 29 ~   ESC [ 29 $    ESC [ 29 ^   ESC [ 29 @
5807 
5808        F17               ESC [ 31 ~   ESC [ 31 $    ESC [ 31 ^   ESC [ 31 @
5809        F18               ESC [ 32 ~   ESC [ 32 $    ESC [ 32 ^   ESC [ 32 @
5810        F19               ESC [ 33 ~   ESC [ 33 $    ESC [ 33 ^   ESC [ 33 @
5811        F20               ESC [ 34 ~   ESC [ 34 $    ESC [ 34 ^   ESC [ 34 @
5812                                                                  Application
5813        Up                ESC [ A      ESC [ a       ESC O a      ESC O A
5814        Down              ESC [ B      ESC [ b       ESC O b      ESC O B
5815        Right             ESC [ C      ESC [ c       ESC O c      ESC O C
5816        Left              ESC [ D      ESC [ d       ESC O d      ESC O D
5817        KP_Enter          ^M                                      ESC O M
5818        KP_F1             ESC O P                                 ESC O P
5819        KP_F2             ESC O Q                                 ESC O Q
5820        KP_F3             ESC O R                                 ESC O R
5821        KP_F4             ESC O S                                 ESC O S
5822        XK_KP_Multiply    *                                       ESC O j
5823        XK_KP_Add         +                                       ESC O k
5824        XK_KP_Separator   ,                                       ESC O l
5825        XK_KP_Subtract    -                                       ESC O m
5826        XK_KP_Decimal     .                                       ESC O n
5827        XK_KP_Divide      /                                       ESC O o
5828        XK_KP_0           0                                       ESC O p
5829        XK_KP_1           1                                       ESC O q
5830        XK_KP_2           2                                       ESC O r
5831        XK_KP_3           3                                       ESC O s
5832        XK_KP_4           4                                       ESC O t
5833        XK_KP_5           5                                       ESC O u
5834        XK_KP_6           6                                       ESC O v
5835        XK_KP_7           7                                       ESC O w
5836        XK_KP_8           8                                       ESC O x
5837        XK_KP_9           9                                       ESC O y
5838 */
5839 
5840 version(Demo_kbhit)
5841 void main() {
5842 	auto terminal = Terminal(ConsoleOutputType.linear);
5843 	auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw);
5844 
5845 	int a;
5846 	char ch = '.';
5847 	while(a < 1000) {
5848 		a++;
5849 		if(a % terminal.width == 0) {
5850 			terminal.write("\r");
5851 			if(ch == '.')
5852 				ch = ' ';
5853 			else
5854 				ch = '.';
5855 		}
5856 
5857 		if(input.kbhit())
5858 			terminal.write(input.getch());
5859 		else
5860 			terminal.write(ch);
5861 
5862 		terminal.flush();
5863 
5864 		import core.thread;
5865 		Thread.sleep(50.msecs);
5866 	}
5867 }
5868 
5869 /*
5870 	The Xterm palette progression is:
5871 	[0, 95, 135, 175, 215, 255]
5872 
5873 	So if I take the color and subtract 55, then div 40, I get
5874 	it into one of these areas. If I add 20, I get a reasonable
5875 	rounding.
5876 */
5877 
5878 ubyte colorToXTermPaletteIndex(RGB color) {
5879 	/*
5880 		Here, I will round off to the color ramp or the
5881 		greyscale. I will NOT use the bottom 16 colors because
5882 		there's duplicates (or very close enough) to them in here
5883 	*/
5884 
5885 	if(color.r == color.g && color.g == color.b) {
5886 		// grey - find one of them:
5887 		if(color.r == 0) return 0;
5888 		// meh don't need those two, let's simplify branche
5889 		//if(color.r == 0xc0) return 7;
5890 		//if(color.r == 0x80) return 8;
5891 		// it isn't == 255 because it wants to catch anything
5892 		// that would wrap the simple algorithm below back to 0.
5893 		if(color.r >= 248) return 15;
5894 
5895 		// there's greys in the color ramp too, but these
5896 		// are all close enough as-is, no need to complicate
5897 		// algorithm for approximation anyway
5898 
5899 		return cast(ubyte) (232 + ((color.r - 8) / 10));
5900 	}
5901 
5902 	// if it isn't grey, it is color
5903 
5904 	// the ramp goes blue, green, red, with 6 of each,
5905 	// so just multiplying will give something good enough
5906 
5907 	// will give something between 0 and 5, with some rounding
5908 	auto r = (cast(int) color.r - 35) / 40;
5909 	auto g = (cast(int) color.g - 35) / 40;
5910 	auto b = (cast(int) color.b - 35) / 40;
5911 
5912 	return cast(ubyte) (16 + b + g*6 + r*36);
5913 }
5914 
5915 /++
5916 	Represents a 24-bit color.
5917 
5918 
5919 	$(TIP You can convert these to and from [arsd.color.Color] using
5920 	      `.tupleof`:
5921 
5922 		---
5923 	      	RGB rgb;
5924 		Color c = Color(rgb.tupleof);
5925 		---
5926 	)
5927 +/
5928 struct RGB {
5929 	ubyte r; ///
5930 	ubyte g; ///
5931 	ubyte b; ///
5932 	// terminal can't actually use this but I want the value
5933 	// there for assignment to an arsd.color.Color
5934 	private ubyte a = 255;
5935 }
5936 
5937 // This is an approximation too for a few entries, but a very close one.
5938 RGB xtermPaletteIndexToColor(int paletteIdx) {
5939 	RGB color;
5940 
5941 	if(paletteIdx < 16) {
5942 		if(paletteIdx == 7)
5943 			return RGB(0xc0, 0xc0, 0xc0);
5944 		else if(paletteIdx == 8)
5945 			return RGB(0x80, 0x80, 0x80);
5946 
5947 		color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00;
5948 		color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00;
5949 		color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00;
5950 
5951 	} else if(paletteIdx < 232) {
5952 		// color ramp, 6x6x6 cube
5953 		color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55);
5954 		color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55);
5955 		color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55);
5956 
5957 		if(color.r == 55) color.r = 0;
5958 		if(color.g == 55) color.g = 0;
5959 		if(color.b == 55) color.b = 0;
5960 	} else {
5961 		// greyscale ramp, from 0x8 to 0xee
5962 		color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10);
5963 		color.g = color.r;
5964 		color.b = color.g;
5965 	}
5966 
5967 	return color;
5968 }
5969 
5970 int approximate16Color(RGB color) {
5971 	int c;
5972 	c |= color.r > 64 ? RED_BIT : 0;
5973 	c |= color.g > 64 ? GREEN_BIT : 0;
5974 	c |= color.b > 64 ? BLUE_BIT : 0;
5975 
5976 	c |= (((color.r + color.g + color.b) / 3) > 80) ? Bright : 0;
5977 
5978 	return c;
5979 }
5980 
5981 version(TerminalDirectToEmulator) {
5982 
5983 	/++
5984 		Indicates the TerminalDirectToEmulator features
5985 		are present. You can check this with `static if`.
5986 
5987 		$(WARNING
5988 			This will cause the [Terminal] constructor to spawn a GUI thread with [arsd.minigui]/[arsd.simpledisplay].
5989 
5990 			This means you can NOT use those libraries in your
5991 			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.
5992 		)
5993 	+/
5994 	enum IntegratedEmulator = true;
5995 
5996 	/++
5997 		Allows customization of the integrated emulator window.
5998 		You may change the default colors, font, and other aspects
5999 		of GUI integration.
6000 
6001 		Test for its presence before using with `static if(arsd.terminal.IntegratedEmulator)`.
6002 
6003 		All settings here must be set BEFORE you construct any [Terminal] instances.
6004 
6005 		History:
6006 			Added March 7, 2020.
6007 	+/
6008 	struct IntegratedTerminalEmulatorConfiguration {
6009 		/// Note that all Colors in here are 24 bit colors.
6010 		alias Color = arsd.color.Color;
6011 
6012 		/// Default foreground color of the terminal.
6013 		Color defaultForeground = Color.black;
6014 		/// Default background color of the terminal.
6015 		Color defaultBackground = Color.white;
6016 
6017 		/++
6018 			Font to use in the window. It should be a monospace font,
6019 			and your selection may not actually be used if not available on
6020 			the user's system, in which case it will fallback to one.
6021 
6022 			History:
6023 				Implemented March 26, 2020
6024 		+/
6025 		string fontName;
6026 		/// ditto
6027 		int fontSize = 14;
6028 
6029 		/++
6030 			Requested initial terminal size in character cells. You may not actually get exactly this.
6031 		+/
6032 		int initialWidth = 80;
6033 		/// ditto
6034 		int initialHeight = 40;
6035 
6036 		/++
6037 			If `true`, the window will close automatically when the main thread exits.
6038 			Otherwise, the window will remain open so the user can work with output before
6039 			it disappears.
6040 
6041 			History:
6042 				Added April 10, 2020 (v7.2.0)
6043 		+/
6044 		bool closeOnExit = false;
6045 
6046 		/++
6047 			Gives you a chance to modify the window as it is constructed. Intended
6048 			to let you add custom menu options.
6049 
6050 			---
6051 			import arsd.terminal;
6052 			integratedTerminalEmulatorConfiguration.menuExtensionsConstructor = (TerminalEmulatorWindow window) {
6053 				import arsd.minigui; // for the menu related UDAs
6054 				class Commands {
6055 					@menu("Help") {
6056 						void Topics() {
6057 							auto window = new Window(); // make a help window of some sort
6058 							window.show();
6059 						}
6060 
6061 						@separator
6062 
6063 						void About() {
6064 							messageBox("My Application v 1.0");
6065 						}
6066 					}
6067 				}
6068 				window.setMenuAndToolbarFromAnnotatedCode(new Commands());
6069 			};
6070 			---
6071 
6072 			History:
6073 				Added March 29, 2020. Included in release v7.1.0.
6074 		+/
6075 		void delegate(TerminalEmulatorWindow) menuExtensionsConstructor;
6076 
6077 		/++
6078 			Set this to true if you want [Terminal] to fallback to the user's
6079 			existing native terminal in the event that creating the custom terminal
6080 			is impossible for whatever reason.
6081 
6082 			If your application must have all advanced features, set this to `false`.
6083 			Otherwise, be sure you handle the absence of advanced features in your
6084 			application by checking methods like [Terminal.inlineImagesSupported],
6085 			etc., and only use things you can gracefully degrade without.
6086 
6087 			If this is set to false, `Terminal`'s constructor will throw if the gui fails
6088 			instead of carrying on with the stdout terminal (if possible).
6089 
6090 			History:
6091 				Added June 28, 2020. Included in release v8.1.0.
6092 
6093 		+/
6094 		bool fallbackToDegradedTerminal = true;
6095 	}
6096 
6097 	/+
6098 		status bar should probably tell
6099 		if scroll lock is on...
6100 	+/
6101 
6102 	/// You can set this in a static module constructor. (`shared static this() {}`)
6103 	__gshared IntegratedTerminalEmulatorConfiguration integratedTerminalEmulatorConfiguration;
6104 
6105 	import arsd.terminalemulator;
6106 	import arsd.minigui;
6107 
6108 	/++
6109 		Represents the window that the library pops up for you.
6110 	+/
6111 	final class TerminalEmulatorWindow : MainWindow {
6112 
6113 		/++
6114 			Gives access to the underlying terminal emulation object.
6115 		+/
6116 		TerminalEmulator terminalEmulator() {
6117 			return tew.terminalEmulator;
6118 		}
6119 
6120 		private TerminalEmulatorWindow parent;
6121 		private TerminalEmulatorWindow[] children;
6122 		private void childClosing(TerminalEmulatorWindow t) {
6123 			foreach(idx, c; children)
6124 				if(c is t)
6125 					children = children[0 .. idx] ~ children[idx + 1 .. $];
6126 		}
6127 		private void registerChild(TerminalEmulatorWindow t) {
6128 			children ~= t;
6129 		}
6130 
6131 		private this(Terminal* term, TerminalEmulatorWindow parent) {
6132 
6133 			this.parent = parent;
6134 			scope(success) if(parent) parent.registerChild(this);
6135 
6136 			super("Terminal Application", integratedTerminalEmulatorConfiguration.initialWidth * integratedTerminalEmulatorConfiguration.fontSize / 2, integratedTerminalEmulatorConfiguration.initialHeight * integratedTerminalEmulatorConfiguration.fontSize);
6137 
6138 			smw = new ScrollMessageWidget(this);
6139 			tew = new TerminalEmulatorWidget(term, smw);
6140 
6141 			smw.addEventListener("scroll", () {
6142 				tew.terminalEmulator.scrollbackTo(smw.position.x, smw.position.y + tew.terminalEmulator.height);
6143 				redraw();
6144 			});
6145 
6146 			smw.setTotalArea(1, 1);
6147 
6148 			setMenuAndToolbarFromAnnotatedCode(this);
6149 			if(integratedTerminalEmulatorConfiguration.menuExtensionsConstructor)
6150 				integratedTerminalEmulatorConfiguration.menuExtensionsConstructor(this);
6151 		}
6152 
6153 		TerminalEmulator.TerminalCell[] delegate(TerminalEmulator.TerminalCell[] i) parentFilter;
6154 
6155 		private void addScrollbackLineFromParent(TerminalEmulator.TerminalCell[] lineIn) {
6156 			if(parentFilter is null)
6157 				return;
6158 
6159 			auto line = parentFilter(lineIn);
6160 			if(line is null) return;
6161 
6162 			if(tew && tew.terminalEmulator) {
6163 				bool atBottom = smw.verticalScrollBar.atEnd && smw.horizontalScrollBar.atStart;
6164 				tew.terminalEmulator.addScrollbackLine(line);
6165 				tew.terminalEmulator.notifyScrollbackAdded();
6166 				if(atBottom) {
6167 					tew.terminalEmulator.notifyScrollbarPosition(0, int.max);
6168 					tew.terminalEmulator.scrollbackTo(0, int.max);
6169 					tew.redraw();
6170 				}
6171 			}
6172 		}
6173 
6174 		private TerminalEmulatorWidget tew;
6175 		private ScrollMessageWidget smw;
6176 
6177 		@menu("&History") {
6178 			@tip("Saves the currently visible content to a file")
6179 			void Save() {
6180 				getSaveFileName((string name) {
6181 					tew.terminalEmulator.writeScrollbackToFile(name);
6182 				});
6183 			}
6184 
6185 			// FIXME
6186 			version(FIXME)
6187 			void Save_HTML() {
6188 
6189 			}
6190 
6191 			@separator
6192 			/*
6193 			void Find() {
6194 				// FIXME
6195 				// jump to the previous instance in the scrollback
6196 
6197 			}
6198 			*/
6199 
6200 			void Filter() {
6201 				// open a new window that just shows items that pass the filter
6202 
6203 				static struct FilterParams {
6204 					string searchTerm;
6205 					bool caseSensitive;
6206 				}
6207 
6208 				dialog((FilterParams p) {
6209 					auto nw = new TerminalEmulatorWindow(null, this);
6210 
6211 					nw.parentFilter = (TerminalEmulator.TerminalCell[] line) {
6212 						import std.algorithm;
6213 						import std.uni;
6214 						// omg autodecoding being kinda useful for once LOL
6215 						if(line.map!(c => c.hasNonCharacterData ? dchar(0) : (p.caseSensitive ? c.ch : c.ch.toLower)).
6216 							canFind(p.searchTerm))
6217 						{
6218 							// I might highlight the match too, but meh for now
6219 							return line;
6220 						}
6221 						return null;
6222 					};
6223 
6224 					foreach(line; tew.terminalEmulator.sbb[0 .. $]) {
6225 						if(auto l = nw.parentFilter(line))
6226 							nw.tew.terminalEmulator.addScrollbackLine(l);
6227 					}
6228 					nw.tew.terminalEmulator.toggleScrollLock();
6229 					nw.tew.terminalEmulator.drawScrollback();
6230 					nw.title = "Filter Display";
6231 					nw.show();
6232 				});
6233 
6234 			}
6235 
6236 			@separator
6237 			void Clear() {
6238 				tew.terminalEmulator.clearScrollbackHistory();
6239 				tew.terminalEmulator.cls();
6240 				tew.terminalEmulator.moveCursor(0, 0);
6241 				if(tew.term) {
6242 					tew.term.windowSizeChanged = true;
6243 					tew.terminalEmulator.outgoingSignal.notify();
6244 				}
6245 				tew.redraw();
6246 			}
6247 
6248 			@separator
6249 			void Exit() @accelerator("Alt+F4") @hotkey('x') {
6250 				this.close();
6251 			}
6252 		}
6253 
6254 		@menu("&Edit") {
6255 			void Copy() {
6256 				tew.terminalEmulator.copyToClipboard(tew.terminalEmulator.getSelectedText());
6257 			}
6258 
6259 			void Paste() {
6260 				tew.terminalEmulator.pasteFromClipboard(&tew.terminalEmulator.sendPasteData);
6261 			}
6262 		}
6263 	}
6264 
6265 	private class InputEventInternal {
6266 		const(ubyte)[] data;
6267 		this(in ubyte[] data) {
6268 			this.data = data;
6269 		}
6270 	}
6271 
6272 	private class TerminalEmulatorWidget : Widget {
6273 
6274 		Menu ctx;
6275 
6276 		override Menu contextMenu(int x, int y) {
6277 			if(ctx is null) {
6278 				ctx = new Menu("");
6279 				ctx.addItem(new MenuItem(new Action("Copy", 0, {
6280 					terminalEmulator.copyToClipboard(terminalEmulator.getSelectedText());
6281 				})));
6282 				 ctx.addItem(new MenuItem(new Action("Paste", 0, {
6283 					terminalEmulator.pasteFromClipboard(&terminalEmulator.sendPasteData);
6284 				})));
6285 				 ctx.addItem(new MenuItem(new Action("Toggle Scroll Lock", 0, {
6286 				 	terminalEmulator.toggleScrollLock();
6287 				})));
6288 			}
6289 			return ctx;
6290 		}
6291 
6292 		this(Terminal* term, ScrollMessageWidget parent) {
6293 			this.smw = parent;
6294 			this.term = term;
6295 			terminalEmulator = new TerminalEmulatorInsideWidget(this);
6296 			super(parent);
6297 			this.parentWindow.win.onClosing = {
6298 				if(term)
6299 					term.hangedUp = true;
6300 
6301 				if(auto wi = cast(TerminalEmulatorWindow) this.parentWindow) {
6302 					if(wi.parent)
6303 						wi.parent.childClosing(wi);
6304 				}
6305 
6306 				// try to get it to terminate slightly more forcibly too, if possible
6307 				if(sigIntExtension)
6308 					sigIntExtension();
6309 
6310 				terminalEmulator.outgoingSignal.notify();
6311 				terminalEmulator.incomingSignal.notify();
6312 			};
6313 
6314 			this.parentWindow.win.addEventListener((InputEventInternal ie) {
6315 				terminalEmulator.sendRawInput(ie.data);
6316 				this.redraw();
6317 				terminalEmulator.incomingSignal.notify();
6318 			});
6319 		}
6320 
6321 		ScrollMessageWidget smw;
6322 		Terminal* term;
6323 
6324 		void sendRawInput(const(ubyte)[] data) {
6325 			if(this.parentWindow) {
6326 				this.parentWindow.win.postEvent(new InputEventInternal(data));
6327 				terminalEmulator.incomingSignal.wait(); // blocking write basically, wait until the TE confirms the receipt of it
6328 			}
6329 		}
6330 
6331 		TerminalEmulatorInsideWidget terminalEmulator;
6332 
6333 		override void registerMovement() {
6334 			super.registerMovement();
6335 			terminalEmulator.resized(width, height);
6336 		}
6337 
6338 		override void focus() {
6339 			super.focus();
6340 			terminalEmulator.attentionReceived();
6341 		}
6342 
6343 		override MouseCursor cursor() { return GenericCursor.Text; }
6344 
6345 		override void erase(WidgetPainter painter) { /* intentionally blank, paint does it better */ }
6346 
6347 		override void paint(WidgetPainter painter) {
6348 			bool forceRedraw = false;
6349 			if(terminalEmulator.invalidateAll || terminalEmulator.clearScreenRequested) {
6350 				auto clearColor = terminalEmulator.defaultBackground;
6351 				painter.outlineColor = clearColor;
6352 				painter.fillColor = clearColor;
6353 				painter.drawRectangle(Point(0, 0), this.width, this.height);
6354 				terminalEmulator.clearScreenRequested = false;
6355 				forceRedraw = true;
6356 			}
6357 
6358 			terminalEmulator.redrawPainter(painter, forceRedraw);
6359 		}
6360 	}
6361 
6362 	private class TerminalEmulatorInsideWidget : TerminalEmulator {
6363 
6364 		private ScrollbackBuffer sbb() { return scrollbackBuffer; }
6365 
6366 		void resized(int w, int h) {
6367 			this.resizeTerminal(w / fontWidth, h / fontHeight);
6368 			if(widget && widget.smw) {
6369 				widget.smw.setViewableArea(this.width, this.height);
6370 				widget.smw.setPageSize(this.width / 2, this.height / 2);
6371 			}
6372 			clearScreenRequested = true;
6373 			if(widget && widget.term)
6374 				widget.term.windowSizeChanged = true;
6375 			outgoingSignal.notify();
6376 			redraw();
6377 		}
6378 
6379 		override void addScrollbackLine(TerminalCell[] line) {
6380 			super.addScrollbackLine(line);
6381 			if(widget)
6382 			if(auto p = cast(TerminalEmulatorWindow) widget.parentWindow) {
6383 				foreach(child; p.children)
6384 					child.addScrollbackLineFromParent(line);
6385 			}
6386 		}
6387 
6388 		override void notifyScrollbackAdded() {
6389 			widget.smw.setTotalArea(this.scrollbackWidth > this.width ? this.scrollbackWidth : this.width, this.scrollbackLength > this.height ? this.scrollbackLength : this.height);
6390 		}
6391 
6392 		override void notifyScrollbarPosition(int x, int y) {
6393 			widget.smw.setPosition(x, y);
6394 			widget.redraw();
6395 		}
6396 
6397 		override void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) {
6398 			if(isRelevantVertically)
6399 				notifyScrollbackAdded();
6400 			else
6401 				widget.smw.setTotalArea(width, height);
6402 		}
6403 
6404 		override @property public int cursorX() { return super.cursorX; }
6405 		override @property public int cursorY() { return super.cursorY; }
6406 
6407 		protected override void changeCursorStyle(CursorStyle s) { }
6408 
6409 		string currentTitle;
6410 		protected override void changeWindowTitle(string t) {
6411 			if(widget && widget.parentWindow && t.length) {
6412 				widget.parentWindow.win.title = t;
6413 				currentTitle = t;
6414 			}
6415 		}
6416 		protected override void changeWindowIcon(IndexedImage t) {
6417 			if(widget && widget.parentWindow && t)
6418 				widget.parentWindow.win.icon = t;
6419 		}
6420 
6421 		protected override void changeIconTitle(string) {}
6422 		protected override void changeTextAttributes(TextAttributes) {}
6423 		protected override void soundBell() {
6424 			static if(UsingSimpledisplayX11)
6425 				XBell(XDisplayConnection.get(), 50);
6426 		}
6427 
6428 		protected override void demandAttention() {
6429 			if(widget && widget.parentWindow)
6430 				widget.parentWindow.win.requestAttention();
6431 		}
6432 
6433 		protected override void copyToClipboard(string text) {
6434 			setClipboardText(widget.parentWindow.win, text);
6435 		}
6436 
6437 		override int maxScrollbackLength() const {
6438 			return int.max; // no scrollback limit for custom programs
6439 		}
6440 
6441 		protected override void pasteFromClipboard(void delegate(in char[]) dg) {
6442 			static if(UsingSimpledisplayX11)
6443 				getPrimarySelection(widget.parentWindow.win, dg);
6444 			else
6445 				getClipboardText(widget.parentWindow.win, (in char[] dataIn) {
6446 					char[] data;
6447 					// change Windows \r\n to plain \n
6448 					foreach(char ch; dataIn)
6449 						if(ch != 13)
6450 							data ~= ch;
6451 					dg(data);
6452 				});
6453 		}
6454 
6455 		protected override void copyToPrimary(string text) {
6456 			static if(UsingSimpledisplayX11)
6457 				setPrimarySelection(widget.parentWindow.win, text);
6458 			else
6459 				{}
6460 		}
6461 		protected override void pasteFromPrimary(void delegate(in char[]) dg) {
6462 			static if(UsingSimpledisplayX11)
6463 				getPrimarySelection(widget.parentWindow.win, dg);
6464 		}
6465 
6466 		override void requestExit() {
6467 			widget.parentWindow.close();
6468 		}
6469 
6470 		bool echo = false;
6471 
6472 		override void sendRawInput(in ubyte[] data) {
6473 			void send(in ubyte[] data) {
6474 				if(data.length == 0)
6475 					return;
6476 				super.sendRawInput(data);
6477 				if(echo)
6478 				sendToApplication(data);
6479 			}
6480 
6481 			// need to echo, translate 10 to 13/10 cr-lf
6482 			size_t last = 0;
6483 			const ubyte[2] crlf = [13, 10];
6484 			foreach(idx, ch; data) {
6485 				if(ch == 10) {
6486 					send(data[last .. idx]);
6487 					send(crlf[]);
6488 					last = idx + 1;
6489 				}
6490 			}
6491 
6492 			if(last < data.length)
6493 				send(data[last .. $]);
6494 		}
6495 
6496 		bool focused;
6497 
6498 		TerminalEmulatorWidget widget;
6499 
6500 		import arsd.simpledisplay;
6501 		import arsd.color;
6502 		import core.sync.semaphore;
6503 		alias ModifierState = arsd.simpledisplay.ModifierState;
6504 		alias Color = arsd.color.Color;
6505 		alias fromHsl = arsd.color.fromHsl;
6506 
6507 		const(ubyte)[] pendingForApplication;
6508 		Semaphore outgoingSignal;
6509 		Semaphore incomingSignal;
6510 
6511 		override void sendToApplication(scope const(void)[] what) {
6512 			synchronized(this) {
6513 				pendingForApplication ~= cast(const(ubyte)[]) what;
6514 			}
6515 			outgoingSignal.notify();
6516 		}
6517 
6518 		@property int width() { return screenWidth; }
6519 		@property int height() { return screenHeight; }
6520 
6521 		@property bool invalidateAll() { return super.invalidateAll; }
6522 
6523 		private this(TerminalEmulatorWidget widget) {
6524 
6525 			this.outgoingSignal = new Semaphore();
6526 			this.incomingSignal = new Semaphore();
6527 
6528 			this.widget = widget;
6529 
6530 			if(integratedTerminalEmulatorConfiguration.fontName.length) {
6531 				this.font = new OperatingSystemFont(integratedTerminalEmulatorConfiguration.fontName, integratedTerminalEmulatorConfiguration.fontSize, FontWeight.medium);
6532 				this.fontWidth = font.averageWidth;
6533 				this.fontHeight = font.height;
6534 			}
6535 
6536 
6537 			if(this.font is null || this.font.isNull)
6538 				loadDefaultFont(integratedTerminalEmulatorConfiguration.fontSize);
6539 
6540 			super(integratedTerminalEmulatorConfiguration.initialWidth, integratedTerminalEmulatorConfiguration.initialHeight);
6541 
6542 			defaultForeground = integratedTerminalEmulatorConfiguration.defaultForeground;
6543 			defaultBackground = integratedTerminalEmulatorConfiguration.defaultBackground;
6544 
6545 			bool skipNextChar = false;
6546 
6547 			widget.addEventListener("mousedown", (Event ev) {
6548 				int termX = (ev.clientX - paddingLeft) / fontWidth;
6549 				int termY = (ev.clientY - paddingTop) / fontHeight;
6550 
6551 				if((!mouseButtonTracking || (ev.state & ModifierState.shift)) && ev.button == MouseButton.right)
6552 					widget.showContextMenu(ev.clientX, ev.clientY);
6553 				else
6554 					if(sendMouseInputToApplication(termX, termY,
6555 						arsd.terminalemulator.MouseEventType.buttonPressed,
6556 						cast(arsd.terminalemulator.MouseButton) ev.button,
6557 						(ev.state & ModifierState.shift) ? true : false,
6558 						(ev.state & ModifierState.ctrl) ? true : false,
6559 						(ev.state & ModifierState.alt) ? true : false
6560 					))
6561 						redraw();
6562 			});
6563 
6564 			widget.addEventListener("mouseup", (Event ev) {
6565 				int termX = (ev.clientX - paddingLeft) / fontWidth;
6566 				int termY = (ev.clientY - paddingTop) / fontHeight;
6567 
6568 				if(sendMouseInputToApplication(termX, termY,
6569 					arsd.terminalemulator.MouseEventType.buttonReleased,
6570 					cast(arsd.terminalemulator.MouseButton) ev.button,
6571 					(ev.state & ModifierState.shift) ? true : false,
6572 					(ev.state & ModifierState.ctrl) ? true : false,
6573 					(ev.state & ModifierState.alt) ? true : false
6574 				))
6575 					redraw();
6576 			});
6577 
6578 			widget.addEventListener("mousemove", (Event ev) {
6579 				int termX = (ev.clientX - paddingLeft) / fontWidth;
6580 				int termY = (ev.clientY - paddingTop) / fontHeight;
6581 
6582 				if(sendMouseInputToApplication(termX, termY,
6583 					arsd.terminalemulator.MouseEventType.motion,
6584 					cast(arsd.terminalemulator.MouseButton) ev.button,
6585 					(ev.state & ModifierState.shift) ? true : false,
6586 					(ev.state & ModifierState.ctrl) ? true : false,
6587 					(ev.state & ModifierState.alt) ? true : false
6588 				))
6589 					redraw();
6590 			});
6591 
6592 			widget.addEventListener("keydown", (Event ev) {
6593 				static string magic() {
6594 					string code;
6595 					foreach(member; __traits(allMembers, TerminalKey))
6596 						if(member != "Escape")
6597 							code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
6598 								, (ev.state & ModifierState.shift)?true:false
6599 								, (ev.state & ModifierState.alt)?true:false
6600 								, (ev.state & ModifierState.ctrl)?true:false
6601 								, (ev.state & ModifierState.windows)?true:false
6602 							)) redraw(); break;";
6603 					return code;
6604 				}
6605 
6606 
6607 				switch(ev.key) {
6608 					mixin(magic());
6609 					default:
6610 						// keep going, not special
6611 				}
6612 
6613 				return; // the character event handler will do others
6614 			});
6615 
6616 			widget.addEventListener("char", (Event ev) {
6617 				dchar c = ev.character;
6618 				if(skipNextChar) {
6619 					skipNextChar = false;
6620 					return;
6621 				}
6622 
6623 				endScrollback();
6624 				char[4] str;
6625 				import std.utf;
6626 				if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
6627 				auto data = str[0 .. encode(str, c)];
6628 
6629 
6630 				if(c == 0x1c) /* ctrl+\, force quit */ {
6631 					version(Posix) {
6632 						import core.sys.posix.signal;
6633 						pthread_kill(widget.term.threadId, SIGQUIT); // or SIGKILL even?
6634 
6635 						assert(0);
6636 						//import core.sys.posix.pthread;
6637 						//pthread_cancel(widget.term.threadId);
6638 						//widget.term = null;
6639 					} else version(Windows) {
6640 						import core.sys.windows.windows;
6641 						auto hnd = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, TRUE, GetCurrentProcessId());
6642 						TerminateProcess(hnd, -1);
6643 						assert(0);
6644 					}
6645 				} else if(c == 3) /* ctrl+c, interrupt */ {
6646 					if(sigIntExtension)
6647 						sigIntExtension();
6648 
6649 					if(widget && widget.term) {
6650 						widget.term.interrupted = true;
6651 						outgoingSignal.notify();
6652 					}
6653 				} else if(c != 127) {
6654 					// on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler.
6655 					sendToApplication(data);
6656 				}
6657 			});
6658 		}
6659 
6660 		bool clearScreenRequested = true;
6661 		void redraw() {
6662 			if(widget.parentWindow is null || widget.parentWindow.win is null || widget.parentWindow.win.closed)
6663 				return;
6664 
6665 			widget.redraw();
6666 		}
6667 
6668 		mixin SdpyDraw;
6669 	}
6670 } else {
6671 	///
6672 	enum IntegratedEmulator = false;
6673 }
6674 
6675 /*
6676 void main() {
6677 	auto terminal = Terminal(ConsoleOutputType.linear);
6678 	terminal.setTrueColor(RGB(255, 0, 255), RGB(255, 255, 255));
6679 	terminal.writeln("Hello, world!");
6680 }
6681 */
6682 
6683 
6684 /*
6685 	ONLY SUPPORTED ON MY TERMINAL EMULATOR IN GENERAL
6686 
6687 	bracketed section can collapse and scroll independently in the TE. may also pop out into a window (possibly with a comparison window)
6688 
6689 	hyperlink can either just indicate something to the TE to handle externally
6690 	OR
6691 	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.
6692 
6693 	internally it can set two bits: one indicates it is a hyperlink, the other just flips each use to separate consecutive sequences.
6694 
6695 	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.
6696 
6697 
6698 	I could spread a unique id number across bits, one bit per char so the memory isn't too bad.
6699 	so it would set a number and a word. this is sent back to the application to handle internally.
6700 
6701 	1) turn on special input
6702 	2) turn off special input
6703 	3) special input sends a paste event with a number and the text
6704 	4) to make a link, you write out the begin sequence, the text, and the end sequence. including the magic number somewhere.
6705 		magic number is allowed to have one bit per char. the terminal discards anything else. terminal.d api will enforce.
6706 
6707 	if magic number is zero, it is not sent in the paste event. maybe.
6708 
6709 	or if it is like 255, it is handled as a url and opened externally
6710 		tho tbh a url could just be detected by regex pattern
6711 
6712 
6713 	NOTE: if your program requests mouse input, the TE does not process it! Thus the user will have to shift+click for it.
6714 
6715 	mode 3004 for bracketed hyperlink
6716 
6717 	hyperlink sequence: \033[?220hnum;text\033[?220l~
6718 
6719 */