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