1 /**
2 	FIXME: writing a line in color then a line in ordinary does something
3 	wrong.
4 
5 	 # huh if i do underline then change color it undoes the underline
6 
7 	FIXME: make shift+enter send something special to the application
8 		and shift+space, etc.
9 		identify itself somehow too for client extensions
10 		ctrl+space is supposed to send char 0.
11 
12 	ctrl+click on url pattern could open in browser perhaps
13 
14 	FIXME: scroll stuff should be higher level  in the implementation.
15 	so like scroll Rect, DirectionAndAmount
16 
17 	There should be a redraw thing that is given batches of instructions
18 	in here that the other thing just implements.
19 
20 	FIXME: the save stack stuff should do cursor style too
21 
22 	This is an extendible unix terminal emulator and some helper functions to help actually implement one.
23 
24 	You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
25 
26 	See nestedterminalemulator.d or main.d for how I did it.
27 */
28 module arsd.terminalemulator;
29 
30 import arsd.color;
31 import std.algorithm : max;
32 
33 enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:";
34 
35 /+
36 	The ;90 ones are my extensions.
37 
38 	90 - clipboard extensions
39 	91 - image extensions
40 	92 - hyperlink extensions
41 +/
42 enum terminalIdCode = "\033[?64;1;2;6;9;15;16;17;18;21;22;28;90;91;92c";
43 
44 interface NonCharacterData {
45 	//const(ubyte)[] serialize();
46 }
47 
48 struct BinaryDataTerminalRepresentation {
49 	int width;
50 	int height;
51 	TerminalEmulator.TerminalCell[] representation;
52 }
53 
54 // old name, don't use in new programs anymore.
55 deprecated alias BrokenUpImage = BinaryDataTerminalRepresentation;
56 
57 struct CustomGlyph {
58 	TrueColorImage image;
59 	dchar substitute;
60 }
61 
62 void unknownEscapeSequence(in char[] esc) {
63 	import std.file;
64 	version(Posix) {
65 		debug append("/tmp/arsd-te-bad-esc-sequences.txt", esc ~ "\n");
66 	} else {
67 		debug append("arsd-te-bad-esc-sequences.txt", esc ~ "\n");
68 	}
69 }
70 
71 // This is used for the double-click word selection
72 bool isWordSeparator(dchar ch) {
73 	return ch == ' ' || ch == '"' || ch == '<' || ch == '>' || ch == '(' || ch == ')' || ch == ',';
74 }
75 
76 TerminalEmulator.TerminalCell[] sliceTrailingWhitespace(TerminalEmulator.TerminalCell[] t) {
77 	size_t end = t.length;
78 	while(end >= 1) {
79 		if(t[end-1].hasNonCharacterData || t[end-1].ch != ' ')
80 			break;
81 		end--;
82 	}
83 
84 	t = t[0 .. end];
85 
86 	/*
87 	import std.stdio;
88 	foreach(ch; t)
89 		write(ch.ch);
90 	writeln("*");
91 	*/
92 
93 	return t;
94 }
95 
96 struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
97 	T[maxSize] bufferInternal;
98 	T[] buffer;
99 	size_t length;
100 	bool isNull = true;
101 	T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
102 	void opOpAssign(string op : "~")(in T rhs) {
103 		if(buffer is null) buffer = bufferInternal[];
104 		isNull = false;
105 		static if(allowGrowth) {
106 			if(this.length == buffer.length)
107 				buffer.length = buffer.length * 2;
108 
109 			buffer[this.length++] = rhs;
110 		} else {
111 			if(this.length < buffer.length) // i am silently discarding more crap
112 				buffer[this.length++] = rhs;
113 		}
114 	}
115 	void opOpAssign(string op : "~")(in T[] rhs) {
116 		if(buffer is null) buffer = bufferInternal[];
117 		isNull = false;
118 		buffer[this.length .. this.length + rhs.length] = rhs[];
119 		this.length += rhs.length;
120 	}
121 	void opAssign(in T[] rhs) {
122 		isNull = rhs is null;
123 		if(buffer is null) buffer = bufferInternal[];
124 		buffer[0 .. rhs.length] = rhs[];
125 		this.length = rhs.length;
126 	}
127 	void opAssign(typeof(null)) {
128 		isNull = true;
129 		length = 0;
130 	}
131 	T opIndex(size_t idx) {
132 		assert(!isNull);
133 		assert(idx < length);
134 		return buffer[idx];
135 	}
136 	void clear() {
137 		isNull = true;
138 		length = 0; 
139 	}
140 }
141 
142 /**
143 	An abstract class that does terminal emulation. You'll have to subclass it to make it work.
144 
145 	The terminal implements a subset of what xterm does and then, optionally, some special features.
146 
147 	Its linear mode (normal) screen buffer is infinitely long and infinitely wide. It is the responsibility
148 	of your subclass to do line wrapping, etc., for display. This i think is actually incompatible with xterm but meh.
149 
150 	actually maybe it *should* automatically wrap them. idk. I think GNU screen does both. FIXME decide.
151 
152 	Its cellular mode (alternate) screen buffer can be any size you want.
153 */
154 class TerminalEmulator {
155 	/* override these to do stuff on the interface.
156 	You might be able to stub them out if there's no state maintained on the target, since TerminalEmulator maintains its own internal state */
157 	protected abstract void changeWindowTitle(string); /// the title of the window
158 	protected abstract void changeIconTitle(string); /// the shorter window/iconified window
159 
160 	protected abstract void changeWindowIcon(IndexedImage); /// change the window icon. note this may be null
161 
162 	protected abstract void changeCursorStyle(CursorStyle); /// cursor style
163 
164 	protected abstract void changeTextAttributes(TextAttributes); /// current text output attributes
165 	protected abstract void soundBell(); /// sounds the bell
166 	protected abstract void sendToApplication(scope const(void)[]); /// send some data to the program running in the terminal, so keypresses etc.
167 
168 	protected abstract void copyToClipboard(string); /// copy the given data to the clipboard (or you can do nothing if you can't)
169 	protected abstract void pasteFromClipboard(void delegate(in char[])); /// requests a paste. we pass it a delegate that should accept the data
170 
171 	protected abstract void copyToPrimary(string); /// copy the given data to the PRIMARY X selection (or you can do nothing if you can't)
172 	protected abstract void pasteFromPrimary(void delegate(in char[])); /// requests a paste from PRIMARY. we pass it a delegate that should accept the data
173 
174 	abstract protected void requestExit(); /// the program is finished and the terminal emulator is requesting you to exit
175 
176 	/// Signal the UI that some attention should be given, e.g. blink the taskbar or sound the bell.
177 	/// The default is to ignore the demand by instantly acknowledging it - if you override this, do NOT call super().
178 	protected void demandAttention() {
179 		attentionReceived();
180 	}
181 
182 	/// After it demands attention, call this when the attention has been received
183 	/// you may call it immediately to ignore the demand (the default)
184 	public void attentionReceived() {
185 		attentionDemanded = false;
186 	}
187 
188 	// I believe \033[50buffer[] and up are available for extensions everywhere.
189 	// when keys are shifted, xterm sends them as \033[1;2F for example with end. but is this even sane? how would we do it with say, F5?
190 	// apparently shifted F5 is ^[[15;2~
191 	// alt + f5 is ^[[15;3~
192 	// alt+shift+f5 is ^[[15;4~
193 
194 	private string pasteDataPending = null;
195 
196 	protected void justRead() {
197 		if(pasteDataPending.length) {
198 			sendPasteData(pasteDataPending);
199 			import core.thread; Thread.sleep(50.msecs); // hack to keep it from closing, broken pipe i think
200 		}
201 	}
202 
203 	// my custom extension.... the data is the text content of the link, the identifier is some bits attached to the unit
204 	public void sendHyperlinkData(scope const(dchar)[] data, uint identifier) {
205 		if(bracketedHyperlinkMode) {
206 			sendToApplication("\033[220~");
207 
208 			import std.conv;
209 			// FIXME: that second 0 is a "command", like which menu option, which mouse button, etc.
210 			sendToApplication(to!string(identifier) ~ ";0;" ~ to!string(data));
211 
212 			sendToApplication("\033[221~");
213 		} else {
214 			// without bracketed hyperlink, it simulates a paste
215 			import std.conv;
216 			sendPasteData(to!string(data));
217 		}
218 	}
219 
220 	public void sendPasteData(scope const(char)[] data) {
221 		//if(pasteDataPending.length)
222 			//throw new Exception("paste data being discarded, wtf, shouldnt happen");
223 
224 		// FIXME: i should put it all together so the brackets don't get separated by threads
225 
226 		if(bracketedPasteMode)
227 			sendToApplication("\033[200~");
228 
229 		version(use_libssh2)
230 			enum MAX_PASTE_CHUNK = 4000;
231 		else
232 			enum MAX_PASTE_CHUNK = 1024 * 1024 * 10;
233 
234 		if(data.length > MAX_PASTE_CHUNK) {
235 			// need to chunk it in order to receive echos, etc,
236 			// to avoid deadlocks
237 			pasteDataPending = data[MAX_PASTE_CHUNK .. $].idup;
238 			data = data[0 .. MAX_PASTE_CHUNK];
239 		} else {
240 			pasteDataPending = null;
241 		}
242 
243 		if(data.length)
244 			sendToApplication(data);
245 
246 		if(bracketedPasteMode)
247 			sendToApplication("\033[201~");
248 	}
249 
250 	private string overriddenSelection;
251 	protected void cancelOverriddenSelection() {
252 		if(overriddenSelection.length == 0)
253 			return;
254 		overriddenSelection = null;
255 		sendToApplication("\033[27;0;987136~"); // fake "select none" key, see terminal.d's ProprietaryPseudoKeys for values.
256 
257 		// The reason that proprietary thing is ok is setting the selection is itself a proprietary extension
258 		// so if it was ever set, it implies the user code is familiar with our magic.
259 	}
260 
261 	public string getSelectedText() {
262 		if(overriddenSelection.length)
263 			return overriddenSelection;
264 		return getPlainText(selectionStart, selectionEnd);
265 	}
266 
267 	bool dragging;
268 	int lastDragX, lastDragY;
269 	public bool sendMouseInputToApplication(int termX, int termY, MouseEventType type, MouseButton button, bool shift, bool ctrl, bool alt) {
270 		if(termX < 0)
271 			termX = 0;
272 		if(termX >= screenWidth)
273 			termX = screenWidth - 1;
274 		if(termY < 0)
275 			termY = 0;
276 		if(termY >= screenHeight)
277 			termY = screenHeight - 1;
278 
279 		version(Windows) {
280 			// I'm swapping these because my laptop doesn't have a middle button,
281 			// and putty swaps them too by default so whatevs.
282 			if(button == MouseButton.right)
283 				button = MouseButton.middle;
284 			else if(button == MouseButton.middle)
285 				button = MouseButton.right;
286 		}
287 
288 		int baseEventCode() {
289 			int b;
290 			// lol the xterm mouse thing sucks like javascript! unbelievable
291 			// it doesn't support two buttons at once...
292 			if(button == MouseButton.left)
293 				b = 0;
294 			else if(button == MouseButton.right)
295 				b = 2;
296 			else if(button == MouseButton.middle)
297 				b = 1;
298 			else if(button == MouseButton.wheelUp)
299 				b = 64 | 0;
300 			else if(button == MouseButton.wheelDown)
301 				b = 64 | 1;
302 			else
303 				b = 3; // none pressed or button released
304 
305 			if(shift)
306 				b |= 4;
307 			if(ctrl)
308 				b |= 16;
309 			if(alt) // sending alt as meta
310 				b |= 8;
311 
312 			return b;
313 		}
314 
315 
316 		if(type == MouseEventType.buttonReleased) {
317 			// X sends press and release on wheel events, but we certainly don't care about those
318 			if(button == MouseButton.wheelUp || button == MouseButton.wheelDown)
319 				return false;
320 
321 			if(dragging) {
322 				auto text = getSelectedText();
323 				if(text.length) {
324 					copyToPrimary(text);
325 				} else if(!mouseButtonReleaseTracking || shift || (selectiveMouseTracking && ((!alternateScreenActive || scrollingBack) || termY != 0) && termY != cursorY)) {
326 					// hyperlink check
327 					int idx = termY * screenWidth + termX;
328 					auto screen = (alternateScreenActive ? alternateScreen : normalScreen);
329 
330 					if(screen[idx].hyperlinkStatus & 0x01) {
331 						// it is a link! need to find the beginning and the end
332 						auto start = idx;
333 						auto end = idx;
334 						auto value = screen[idx].hyperlinkStatus;
335 						while(start > 0 && screen[start].hyperlinkStatus == value)
336 							start--;
337 						if(screen[start].hyperlinkStatus != value)
338 							start++;
339 						while(end < screen.length && screen[end].hyperlinkStatus == value)
340 							end++;
341 
342 						uint number;
343 						dchar[64] buffer;
344 						foreach(i, ch; screen[start .. end]) {
345 							if(i >= buffer.length)
346 								break;
347 							if(!ch.hasNonCharacterData)
348 								buffer[i] = ch.ch;
349 							if(i < 16) {
350 								number |= (ch.hyperlinkBit ? 1 : 0) << i;
351 							}
352 						}
353 
354 						sendHyperlinkData(buffer[0 .. end - start], number);
355 					}
356 				}
357 			}
358 
359 			dragging = false;
360 			if(mouseButtonReleaseTracking) {
361 				int b = baseEventCode;
362 				b |= 3; // always send none / button released
363 				ScopeBuffer!(char, 16) buffer;
364 				buffer ~= "\033[M";
365 				buffer ~= cast(char) (b | 32);
366 				buffer ~= cast(char) (termX+1 + 32);
367 				buffer ~= cast(char) (termY+1 + 32);
368 				sendToApplication(buffer[]);
369 			}
370 		}
371 
372 		if(type == MouseEventType.motion) {
373 			if(termX != lastDragX || termY != lastDragY) {
374 				lastDragY = termY;
375 				lastDragX = termX;
376 				if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
377 					int b = baseEventCode;
378 					ScopeBuffer!(char, 16) buffer;
379 					buffer ~= "\033[M";
380 					buffer ~= cast(char) ((b | 32) + 32);
381 					buffer ~= cast(char) (termX+1 + 32);
382 					buffer ~= cast(char) (termY+1 + 32);
383 					sendToApplication(buffer[]);
384 				}
385 
386 				if(dragging) {
387 					auto idx = termY * screenWidth + termX;
388 
389 					// the no-longer-selected portion needs to be invalidated
390 					int start, end;
391 					if(idx > selectionEnd) {
392 						start = selectionEnd;
393 						end = idx;
394 					} else {
395 						start = idx;
396 						end = selectionEnd;
397 					}
398 					if(start < 0 || end >= ((alternateScreenActive ? alternateScreen.length : normalScreen.length)))
399 						return false;
400 
401 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
402 						cell.invalidated = true;
403 						cell.selected = false;
404 					}
405 
406 					cancelOverriddenSelection();
407 					selectionEnd = idx;
408 
409 					// and the freshly selected portion needs to be invalidated
410 					if(selectionStart > selectionEnd) {
411 						start = selectionEnd;
412 						end = selectionStart;
413 					} else {
414 						start = selectionStart;
415 						end = selectionEnd;
416 					}
417 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
418 						cell.invalidated = true;
419 						cell.selected = true;
420 					}
421 
422 					return true;
423 				}
424 			}
425 		}
426 
427 		if(type == MouseEventType.buttonPressed) {
428 			// double click detection
429 			import std.datetime;
430 			static SysTime lastClickTime;
431 			static int consecutiveClicks = 1;
432 
433 			if(button != MouseButton.wheelUp && button != MouseButton.wheelDown) {
434 				if(Clock.currTime() - lastClickTime < dur!"msecs"(350))
435 					consecutiveClicks++;
436 				else
437 					consecutiveClicks = 1;
438 
439 				lastClickTime = Clock.currTime();
440 			}
441 			// end dbl click
442 
443 			if(!(shift) && mouseButtonTracking) {
444 				if(selectiveMouseTracking && termY != 0 && termY != cursorY) {
445 					if(button == MouseButton.left || button == MouseButton.right)
446 						goto do_default_behavior;
447 					if((!alternateScreenActive || scrollingBack) && (button == MouseButton.wheelUp || button.MouseButton.wheelDown))
448 						goto do_default_behavior;
449 				}
450 				// top line only gets special cased on full screen apps
451 				if(selectiveMouseTracking && (!alternateScreenActive || scrollingBack) && termY == 0 && cursorY != 0)
452 					goto do_default_behavior;
453 
454 				int b = baseEventCode;
455 
456 				int x = termX;
457 				int y = termY;
458 				x++; y++; // applications expect it to be one-based
459 
460 				ScopeBuffer!(char, 16) buffer;
461 				buffer ~= "\033[M";
462 				buffer ~= cast(char) (b | 32);
463 				buffer ~= cast(char) (x + 32);
464 				buffer ~= cast(char) (y + 32);
465 
466 				sendToApplication(buffer[]);
467 			} else {
468 				do_default_behavior:
469 				if(button == MouseButton.middle) {
470 					pasteFromPrimary(&sendPasteData);
471 				}
472 
473 				if(button == MouseButton.wheelUp) {
474 					scrollback(alt ? 0 : (ctrl ? 10 : 1), alt ? -(ctrl ? 10 : 1) : 0);
475 					return true;
476 				}
477 				if(button == MouseButton.wheelDown) {
478 					scrollback(alt ? 0 : -(ctrl ? 10 : 1), alt ? (ctrl ? 10 : 1) : 0);
479 					return true;
480 				}
481 
482 				if(button == MouseButton.left) {
483 					// we invalidate the old selection since it should no longer be highlighted...
484 					makeSelectionOffsetsSane(selectionStart, selectionEnd);
485 
486 					cancelOverriddenSelection();
487 
488 					auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen);
489 					foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) {
490 						cell.invalidated = true;
491 						cell.selected = false;
492 					}
493 
494 					if(consecutiveClicks == 1) {
495 						selectionStart = termY * screenWidth + termX;
496 						selectionEnd = selectionStart;
497 					} else if(consecutiveClicks == 2) {
498 						selectionStart = termY * screenWidth + termX;
499 						selectionEnd = selectionStart;
500 						while(selectionStart > 0 && !isWordSeparator((*activeScreen)[selectionStart-1].ch)) {
501 							selectionStart--;
502 						}
503 
504 						while(selectionEnd < (*activeScreen).length && !isWordSeparator((*activeScreen)[selectionEnd].ch)) {
505 							selectionEnd++;
506 						}
507 
508 					} else if(consecutiveClicks == 3) {
509 						selectionStart = termY * screenWidth;
510 						selectionEnd = selectionStart + screenWidth;
511 					}
512 					dragging = true;
513 					lastDragX = termX;
514 					lastDragY = termY;
515 
516 					// then invalidate the new selection as well since it should be highlighted
517 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[selectionStart .. selectionEnd]) {
518 						cell.invalidated = true;
519 						cell.selected = true;
520 					}
521 
522 					return true;
523 				}
524 				if(button == MouseButton.right) {
525 
526 					int changed1;
527 					int changed2;
528 
529 					cancelOverriddenSelection();
530 
531 					auto click = termY * screenWidth + termX;
532 					if(click < selectionStart) {
533 						auto oldSelectionStart = selectionStart;
534 						selectionStart = click;
535 						changed1 = selectionStart;
536 						changed2 = oldSelectionStart;
537 					} else if(click > selectionEnd) {
538 						auto oldSelectionEnd = selectionEnd;
539 						selectionEnd = click;
540 
541 						changed1 = oldSelectionEnd;
542 						changed2 = selectionEnd;
543 					}
544 
545 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[changed1 .. changed2]) {
546 						cell.invalidated = true;
547 						cell.selected = true;
548 					}
549 
550 					auto text = getPlainText(selectionStart, selectionEnd);
551 					if(text.length) {
552 						copyToPrimary(text);
553 					}
554 					return true;
555 				}
556 			}
557 		}
558 
559 		return false;
560 	}
561 
562 	protected void returnToNormalScreen() {
563 		alternateScreenActive = false;
564 
565 		if(cueScrollback) {
566 			showScrollbackOnScreen(normalScreen, 0, true, 0);
567 			newLine(false);
568 			cueScrollback = false;
569 		}
570 
571 		notifyScrollbarRelevant(true, true);
572 	}
573 
574 	protected void outputOccurred() { }
575 
576 	private int selectionStart; // an offset into the screen buffer
577 	private int selectionEnd; // ditto
578 
579 	void requestRedraw() {}
580 
581 
582 	private bool skipNextChar;
583 	// assuming Key is an enum with members just like the one in simpledisplay.d
584 	// returns true if it was handled here
585 	protected bool defaultKeyHandler(Key)(Key key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
586 		enum bool KeyHasNamedAscii = is(typeof(Key.A));
587 
588 		static string magic() {
589 			string code;
590 			foreach(member; __traits(allMembers, TerminalKey))
591 				if(member != "Escape")
592 					code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
593 						, shift ?true:false
594 						, alt ?true:false
595 						, ctrl ?true:false
596 						, windows ?true:false
597 					)) requestRedraw(); return true;";
598 			return code;
599 		}
600 
601 		void specialAscii(dchar what) {
602 			if(!alt)
603 				skipNextChar = true;
604 			if(sendKeyToApplication(
605 				cast(TerminalKey) what
606 				, shift ? true:false
607 				, alt ? true:false
608 				, ctrl ? true:false
609 				, windows ? true:false
610 			)) requestRedraw();
611 		}
612 
613 		static if(KeyHasNamedAscii) {
614 			enum Space = Key.Space;
615 			enum Enter = Key.Enter;
616 			enum Backspace = Key.Backspace;
617 			enum Tab = Key.Tab;
618 			enum Escape = Key.Escape;
619 		} else {
620 			enum Space = ' ';
621 			enum Enter = '\n';
622 			enum Backspace = '\b';
623 			enum Tab = '\t';
624 			enum Escape = '\033';
625 		}
626 
627 
628 		switch(key) {
629 			//// I want the escape key to send twice to differentiate it from
630 			//// other escape sequences easily.
631 			//case Key.Escape: sendToApplication("\033"); break;
632 
633 			/*
634 			case Key.V:
635 			case Key.C:
636 				if(shift && ctrl) {
637 					skipNextChar = true;
638 					if(key == Key.V)
639 						pasteFromClipboard(&sendPasteData);
640 					else if(key == Key.C)
641 						copyToClipboard(getSelectedText());
642 				}
643 			break;
644 			*/
645 
646 			// expansion of my own for like shift+enter to terminal.d users
647 			case Enter, Backspace, Tab, Escape:
648 				if(shift || alt || ctrl) {
649 					static if(KeyHasNamedAscii) {
650 						specialAscii(
651 							cast(TerminalKey) (
652 								key == Key.Enter ? '\n' :
653 								key == Key.Tab ? '\t' :
654 								key == Key.Backspace ? '\b' :
655 								key == Key.Escape ? '\033' :
656 									0 /* assert(0) */
657 							)
658 						);
659 					} else {
660 						specialAscii(key);
661 					}
662 					return true;
663 				}
664 			break;
665 			case Space:
666 				if(alt) { // it used to be shift || alt here, but like shift+space is more trouble than it is worth in actual usage experience. too easily to accidentally type it in the middle of something else to be unambiguously useful. I wouldn't even set a hotkey on it so gonna just send it as plain space always.
667 					// ctrl+space sends 0 per normal translation char rules
668 					specialAscii(' ');
669 					return true;
670 				}
671 			break;
672 
673 			mixin(magic());
674 
675 			static if(is(typeof(Key.Shift))) {
676 				// modifiers are not ascii, ignore them here
677 				case Key.Shift, Key.Ctrl, Key.Alt, Key.Windows, Key.Alt_r, Key.Shift_r, Key.Ctrl_r, Key.CapsLock, Key.NumLock:
678 				// nor are these special keys that don't return characters
679 				case Key.Menu, Key.Pause, Key.PrintScreen:
680 					return false;
681 			}
682 
683 			default:
684 				// alt basically always get special treatment, since it doesn't
685 				// generate anything from the char handler. but shift and ctrl
686 				// do, so we'll just use that unless both are pressed, in which
687 				// case I want to go custom to differentiate like ctrl+c from ctrl+shift+c and such.
688 
689 				// FIXME: xterm offers some control on this, see: https://invisible-island.net/xterm/xterm.faq.html#xterm_modother
690 				if(alt || (shift && ctrl)) {
691 					if(key >= 'A' && key <= 'Z')
692 						key += 32; // always use lowercase for as much consistency as we can since the shift modifier need not apply here. Windows' keysyms are uppercase while X's are lowercase too
693 					specialAscii(key);
694 					if(!alt)
695 						skipNextChar = true;
696 					return true;
697 				}
698 		}
699 
700 		return true;
701 	}
702 	protected bool defaultCharHandler(dchar c) {
703 		if(skipNextChar) {
704 			skipNextChar = false;
705 			return true;
706 		}
707 
708 		endScrollback();
709 		char[4] str;
710 		char[5] send;
711 
712 		import std.utf;
713 		//if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
714 		auto data = str[0 .. encode(str, c)];
715 
716 		// 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.
717 		if(c != 127)
718 			sendToApplication(data);
719 
720 		return true;
721 	}
722 
723 	/// Send a non-character key sequence
724 	public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
725 		bool redrawRequired = false;
726 
727 		if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) {
728 			toggleScrollLock();
729 			return true;
730 		}
731 
732 		/*
733 			So ctrl + A-Z, [, \, ], ^, and _ are all chars 1-31
734 			ctrl+5 send ^]
735 
736 			FIXME: for alt+keys and the other ctrl+them, send the xterm ascii magc thing terminal.d knows how to use
737 		*/
738 
739 		// scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen
740 		// doesn't have scrollback anyway. Thus the key will be forwarded to the application.
741 		if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageUp && (shift || scrollLock)) {
742 			scrollback(10);
743 			return true;
744 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageDown && (shift || scrollLock)) {
745 			scrollback(-10);
746 			return true;
747 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Left && (shift || scrollLock)) {
748 			scrollback(0, ctrl ? -10 : -1);
749 			return true;
750 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Right && (shift || scrollLock)) {
751 			scrollback(0, ctrl ? 10 : 1);
752 			return true;
753 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Up && (shift || scrollLock)) {
754 			scrollback(ctrl ? 10 : 1);
755 			return true;
756 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Down && (shift || scrollLock)) {
757 			scrollback(ctrl ? -10 : -1);
758 			return true;
759 		} else if((!alternateScreenActive || scrollingBack)) { // && ev.key != Key.Shift && ev.key != Key.Shift_r) {
760 			if(endScrollback())
761 				redrawRequired = true;
762 		}
763 
764 
765 
766 		void sendToApplicationModified(string s, int key = 0) {
767 			bool anyModifier = shift || alt || ctrl || windows;
768 			if(!anyModifier || applicationCursorKeys)
769 				sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh
770 			else {
771 				ScopeBuffer!(char, 16) modifierNumber;
772 				char otherModifier = 0;
773 				if(shift && alt && ctrl) modifierNumber = "8";
774 				if(alt && ctrl && !shift) modifierNumber = "7";
775 				if(shift && ctrl && !alt) modifierNumber = "6";
776 				if(ctrl && !shift && !alt) modifierNumber = "5";
777 				if(shift && alt && !ctrl) modifierNumber = "4";
778 				if(alt && !shift && !ctrl) modifierNumber = "3";
779 				if(shift && !alt && !ctrl) modifierNumber = "2";
780 				// FIXME: meta and windows
781 				// windows is an extension
782 				if(windows) {
783 					if(modifierNumber.length)
784 						otherModifier = '2';
785 					else
786 						modifierNumber = "20";
787 					/* // the below is what we're really doing
788 					int mn = 0;
789 					if(modifierNumber.length)
790 						mn = modifierNumber[0] + '0';
791 					mn += 20;
792 					*/
793 				}
794 
795 				string keyNumber;
796 				char terminator;
797 
798 				if(s[$-1] == '~') {
799 					keyNumber = s[2 .. $-1];
800 					terminator = '~';
801 				} else {
802 					keyNumber = "1";
803 					terminator = s[$ - 1];
804 				}
805 
806 				ScopeBuffer!(char, 32) buffer;
807 				buffer ~= "\033[";
808 				buffer ~= keyNumber;
809 				buffer ~= ";";
810 				if(otherModifier)
811 					buffer ~= otherModifier;
812 				buffer ~= modifierNumber[];
813 				if(key) {
814 					buffer ~= ";";
815 					import std.conv;
816 					buffer ~= to!string(key);
817 				}
818 				buffer ~= terminator;
819 				// the xterm style is last bit tell us what it is
820 				sendToApplication(buffer[]);
821 			}
822 		}
823 
824 		alias TerminalKey Key;
825 		import std.stdio;
826 		// writefln("Key: %x", cast(int) key);
827 		switch(key) {
828 			case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break;
829 			case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break;
830 			case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break;
831 			case Key.Right: sendToApplicationModified(applicationCursorKeys ? "\033OC" : "\033[C"); break;
832 
833 			case Key.Home: sendToApplicationModified(applicationCursorKeys ? "\033OH" : (1 ? "\033[H" : "\033[1~")); break;
834 			case Key.Insert: sendToApplicationModified("\033[2~"); break;
835 			case Key.Delete: sendToApplicationModified("\033[3~"); break;
836 
837 			// the 1? is xterm vs gnu screen. but i really want xterm compatibility.
838 			case Key.End: sendToApplicationModified(applicationCursorKeys ? "\033OF" : (1 ? "\033[F" : "\033[4~")); break;
839 			case Key.PageUp: sendToApplicationModified("\033[5~"); break;
840 			case Key.PageDown: sendToApplicationModified("\033[6~"); break;
841 
842 			// the first one here is preferred, the second option is what xterm does if you turn on the "old function keys" option, which most apps don't actually expect
843 			case Key.F1: sendToApplicationModified(1 ? "\033OP" : "\033[11~"); break;
844 			case Key.F2: sendToApplicationModified(1 ? "\033OQ" : "\033[12~"); break;
845 			case Key.F3: sendToApplicationModified(1 ? "\033OR" : "\033[13~"); break;
846 			case Key.F4: sendToApplicationModified(1 ? "\033OS" : "\033[14~"); break;
847 			case Key.F5: sendToApplicationModified("\033[15~"); break;
848 			case Key.F6: sendToApplicationModified("\033[17~"); break;
849 			case Key.F7: sendToApplicationModified("\033[18~"); break;
850 			case Key.F8: sendToApplicationModified("\033[19~"); break;
851 			case Key.F9: sendToApplicationModified("\033[20~"); break;
852 			case Key.F10: sendToApplicationModified("\033[21~"); break;
853 			case Key.F11: sendToApplicationModified("\033[23~"); break;
854 			case Key.F12: sendToApplicationModified("\033[24~"); break;
855 
856 			case Key.Escape: sendToApplicationModified("\033"); break;
857 
858 			// my extensions, see terminator.d for the other side of it
859 			case Key.ScrollLock: sendToApplicationModified("\033[70~"); break;
860 
861 			// xterm extension for arbitrary modified unicode chars
862 			default:
863 				sendToApplicationModified("\033[27~", key);
864 		}
865 
866 		return redrawRequired;
867 	}
868 
869 	/// if a binary extension is triggered, the implementing class is responsible for figuring out how it should be made to fit into the screen buffer
870 	protected /*abstract*/ BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[]) {
871 		return BinaryDataTerminalRepresentation();
872 	}
873 
874 	/// If you subclass this and return true, you can scroll on command without needing to redraw the entire screen;
875 	/// returning true here suppresses the automatic invalidation of scrolled lines (except the new one).
876 	protected bool scrollLines(int howMany, bool scrollUp) {
877 		return false;
878 	}
879 
880 	// might be worth doing the redraw magic in here too.
881 	// FIXME: not implemented
882 	@disable protected void drawTextSection(int x, int y, TextAttributes attributes, in dchar[] text, bool isAllSpaces) {
883 		// if you implement this it will always give you a continuous block on a single line. note that text may be a bunch of spaces, in that case you can just draw the bg color to clear the area
884 		// or you can redraw based on the invalidated flag on the buffer
885 	}
886 	// FIXME: what about image sections? maybe it is still necessary to loop through them
887 
888 	/// Style of the cursor
889 	enum CursorStyle {
890 		block, /// a solid block over the position (like default xterm or many gui replace modes)
891 		underline, /// underlining the position (like the vga text mode default)
892 		bar, /// a bar on the left side of the cursor position (like gui insert modes)
893 	}
894 
895 	// these can be overridden, but don't have to be
896 	TextAttributes defaultTextAttributes() {
897 		TextAttributes ta;
898 
899 		ta.foregroundIndex = 256; // terminal.d uses this as Color.DEFAULT
900 		ta.backgroundIndex = 256;
901 
902 		import std.process;
903 		// I'm using the environment for this because my programs and scripts
904 		// already know this variable and then it gets nicely inherited. It is
905 		// also easy to set without buggering with other arguments. So works for me.
906 		version(with_24_bit_color) {
907 			if(environment.get("ELVISBG") == "dark") {
908 				ta.foreground = Color.white;
909 				ta.background = Color.black;
910 			} else {
911 				ta.foreground = Color.black;
912 				ta.background = Color.white;
913 			}
914 		}
915 
916 		return ta;
917 	}
918 
919 	Color defaultForeground;
920 	Color defaultBackground;
921 
922 	Color[256] palette;
923 
924 	/// .
925 	static struct TextAttributes {
926 		align(1):
927 		bool bold() { return (attrStore & 1) ? true : false; } ///
928 		void bold(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } ///
929 
930 		bool blink() { return (attrStore & 2) ? true : false; } ///
931 		void blink(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } ///
932 
933 		bool invisible() { return (attrStore & 4) ? true : false; } ///
934 		void invisible(bool t) { attrStore &= ~4; if(t) attrStore |= 4; } ///
935 
936 		bool inverse() { return (attrStore & 8) ? true : false; } ///
937 		void inverse(bool t) { attrStore &= ~8; if(t) attrStore |= 8; } ///
938 
939 		bool underlined() { return (attrStore & 16) ? true : false; } ///
940 		void underlined(bool t) { attrStore &= ~16; if(t) attrStore |= 16; } ///
941 
942 		bool italic() { return (attrStore & 32) ? true : false; } ///
943 		void italic(bool t) { attrStore &= ~32; if(t) attrStore |= 32; } ///
944 
945 		bool strikeout() { return (attrStore & 64) ? true : false; } ///
946 		void strikeout(bool t) { attrStore &= ~64; if(t) attrStore |= 64; } ///
947 
948 		bool faint() { return (attrStore & 128) ? true : false; } ///
949 		void faint(bool t) { attrStore &= ~128; if(t) attrStore |= 128; } ///
950 
951 		// if the high bit here is set, you should use the full Color values if possible, and the value here sans the high bit if not
952 
953 		bool foregroundIsDefault() { return (attrStore & 256) ? true : false; } ///
954 		void foregroundIsDefault(bool t) { attrStore &= ~256; if(t) attrStore |= 256; } ///
955 
956 		bool backgroundIsDefault() { return (attrStore & 512) ? true : false; } ///
957 		void backgroundIsDefault(bool t) { attrStore &= ~512; if(t) attrStore |= 512; } ///
958 
959 		// I am doing all this to  get the store a bit smaller but
960 		// I could go back to just plain `ushort foregroundIndex` etc.
961 
962 		///
963 		@property ushort foregroundIndex() {
964 			if(foregroundIsDefault)
965 				return 256;
966 			else
967 				return foregroundIndexStore;
968 		}
969 		///
970 		@property ushort backgroundIndex() {
971 			if(backgroundIsDefault)
972 				return 256;
973 			else
974 				return backgroundIndexStore;
975 		}
976 		///
977 		@property void foregroundIndex(ushort v) {
978 			if(v == 256)
979 				foregroundIsDefault = true;
980 			else
981 				foregroundIsDefault = false;
982 			foregroundIndexStore = cast(ubyte) v;
983 		}
984 		///
985 		@property void backgroundIndex(ushort v) {
986 			if(v == 256)
987 				backgroundIsDefault = true;
988 			else
989 				backgroundIsDefault = false;
990 			backgroundIndexStore = cast(ubyte) v;
991 		}
992 
993 		ubyte foregroundIndexStore; /// the internal storage
994 		ubyte backgroundIndexStore; /// ditto
995 		ushort attrStore = 0; /// ditto
996 
997 		version(with_24_bit_color) {
998 			Color foreground; /// ditto
999 			Color background; /// ditto
1000 		}
1001 	}
1002 
1003 		//pragma(msg, TerminalCell.sizeof);
1004 	/// represents one terminal cell
1005 	align((void*).sizeof)
1006 	static struct TerminalCell {
1007 	align(1):
1008 		private union {
1009 			// OMG the top 11 bits of a dchar are always 0
1010 			// and i can reuse them!!!
1011 			struct {
1012 				dchar chStore = ' '; /// the character
1013 				TextAttributes attributesStore; /// color, etc.
1014 			}
1015 			// 64 bit pointer also has unused 16 bits but meh.
1016 			NonCharacterData nonCharacterDataStore; /// iff hasNonCharacterData
1017 		}
1018 
1019 		dchar ch() {
1020 			assert(!hasNonCharacterData);
1021 			return chStore;
1022 		}
1023 		void ch(dchar c) { 
1024 			hasNonCharacterData = false;
1025 			chStore = c;
1026 		}
1027 		ref TextAttributes attributes() return {
1028 			assert(!hasNonCharacterData);
1029 			return attributesStore;
1030 		}
1031 		NonCharacterData nonCharacterData() {
1032 			assert(hasNonCharacterData);
1033 			return nonCharacterDataStore;
1034 		}
1035 		void nonCharacterData(NonCharacterData c) {
1036 			hasNonCharacterData = true;
1037 			nonCharacterDataStore = c;
1038 		}
1039 
1040 		// bits: RRHLLNSI
1041 		// R = reserved, H = hyperlink ID bit, L = link, N = non-character data, S = selected, I = invalidated
1042 		ubyte attrStore = 1;  // just invalidated to start
1043 
1044 		bool invalidated() { return (attrStore & 1) ? true : false; } /// if it needs to be redrawn
1045 		void invalidated(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } /// ditto
1046 
1047 		bool selected() { return (attrStore & 2) ? true : false; } /// if it is currently selected by the user (for being copied to the clipboard)
1048 		void selected(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } /// ditto
1049 
1050 		bool hasNonCharacterData() { return (attrStore & 4) ? true : false; } ///
1051 		void hasNonCharacterData(bool t) { attrStore &= ~4; if(t) attrStore |= 4; }
1052 
1053 		// 0 means it is not a hyperlink. Otherwise, it just alternates between 1 and 3 to tell adjacent links apart.
1054 		// value of 2 is reserved for future use.
1055 		ubyte hyperlinkStatus() { return (attrStore & 0b11000) >> 3; }
1056 		void hyperlinkStatus(ubyte t) { assert(t < 4); attrStore &= ~0b11000; attrStore |= t << 3; }
1057 
1058 		bool hyperlinkBit() { return (attrStore & 0b100000) >> 5; }
1059 		void hyperlinkBit(bool t) { (attrStore &= ~0b100000); if(t) attrStore |= 0b100000; }
1060 	}
1061 
1062 	bool hyperlinkFlipper;
1063 	bool hyperlinkActive;
1064 	int hyperlinkNumber;
1065 
1066 	/// Cursor position, zero based. (0,0) == upper left. (0, 1) == second row, first column.
1067 	static struct CursorPosition {
1068 		int x; /// .
1069 		int y; /// .
1070 		alias y row;
1071 		alias x column;
1072 	}
1073 
1074 	// these public functions can be used to manipulate the terminal
1075 
1076 	/// clear the screen
1077 	void cls() {
1078 		TerminalCell plain;
1079 		plain.ch = ' ';
1080 		plain.attributes = currentAttributes;
1081 		plain.invalidated = true;
1082 		foreach(i, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
1083 			cell = plain;
1084 		}
1085 	}
1086 
1087 	void makeSelectionOffsetsSane(ref int offsetStart, ref int offsetEnd) {
1088 		auto buffer = &alternateScreen;
1089 
1090 		if(offsetStart < 0)
1091 			offsetStart = 0;
1092 		if(offsetEnd < 0)
1093 			offsetEnd = 0;
1094 		if(offsetStart > (*buffer).length)
1095 			offsetStart = cast(int) (*buffer).length;
1096 		if(offsetEnd > (*buffer).length)
1097 			offsetEnd = cast(int) (*buffer).length;
1098 
1099 		// if it is backwards, we can flip it
1100 		if(offsetEnd < offsetStart) {
1101 			auto tmp = offsetStart;
1102 			offsetStart = offsetEnd;
1103 			offsetEnd = tmp;
1104 		}
1105 	}
1106 
1107 	public string getPlainText(int offsetStart, int offsetEnd) {
1108 		auto buffer = alternateScreenActive ? &alternateScreen : &normalScreen;
1109 
1110 		makeSelectionOffsetsSane(offsetStart, offsetEnd);
1111 
1112 		if(offsetStart == offsetEnd)
1113 			return null;
1114 
1115 		int x = offsetStart % screenWidth;
1116 		int firstSpace = -1;
1117 		string ret;
1118 		foreach(cell; (*buffer)[offsetStart .. offsetEnd]) {
1119 			if(cell.hasNonCharacterData)
1120 				break;
1121 			ret ~= cell.ch;
1122 
1123 			x++;
1124 			if(x == screenWidth) {
1125 				x = 0;
1126 				if(firstSpace != -1) {
1127 					// we ended with a bunch of spaces, let's replace them with a single newline so the next is more natural
1128 					ret = ret[0 .. firstSpace];
1129 					ret ~= "\n";
1130 					firstSpace = -1;
1131 				}
1132 			} else {
1133 				if(cell.ch == ' ' && firstSpace == -1)
1134 					firstSpace = cast(int) ret.length - 1;
1135 				else if(cell.ch != ' ')
1136 					firstSpace = -1;
1137 			}
1138 		}
1139 		if(firstSpace != -1) {
1140 			bool allSpaces = true;
1141 			foreach(item; ret[firstSpace .. $]) {
1142 				if(item != ' ') {
1143 					allSpaces = false;
1144 					break;
1145 				}
1146 			}
1147 
1148 			if(allSpaces)
1149 				ret = ret[0 .. firstSpace];
1150 		}
1151 
1152 		return ret;
1153 	}
1154 
1155 	void scrollDown(int count = 1) {
1156 		if(cursorY + 1 < screenHeight) {
1157 			TerminalCell plain;
1158 			plain.ch = ' ';
1159 			plain.attributes = defaultTextAttributes();
1160 			plain.invalidated = true;
1161 			foreach(i; 0 .. count) {
1162 				// FIXME: should that be cursorY or scrollZoneTop?
1163 				for(int y = scrollZoneBottom; y > cursorY; y--)
1164 				foreach(x; 0 .. screenWidth) {
1165 					ASS[y][x] = ASS[y - 1][x];
1166 					ASS[y][x].invalidated = true;
1167 				}
1168 
1169 				foreach(x; 0 .. screenWidth)
1170 					ASS[cursorY][x] = plain;
1171 			}
1172 		}
1173 	}
1174 
1175 	void scrollUp(int count = 1) {
1176 		if(cursorY + 1 < screenHeight) {
1177 			TerminalCell plain;
1178 			plain.ch = ' ';
1179 			plain.attributes = defaultTextAttributes();
1180 			plain.invalidated = true;
1181 			foreach(i; 0 .. count) {
1182 				// FIXME: should that be cursorY or scrollZoneBottom?
1183 				for(int y = scrollZoneTop; y < cursorY; y++)
1184 				foreach(x; 0 .. screenWidth) {
1185 					ASS[y][x] = ASS[y + 1][x];
1186 					ASS[y][x].invalidated = true;
1187 				}
1188 
1189 				foreach(x; 0 .. screenWidth)
1190 					ASS[cursorY][x] = plain;
1191 			}
1192 		}
1193 	}
1194 
1195 
1196 	int readingExtensionData = -1;
1197 	string extensionData;
1198 
1199 	immutable(dchar[dchar])* characterSet = null; // null means use regular UTF-8
1200 
1201 	bool readingEsc = false;
1202 	ScopeBuffer!(ubyte, 1024, true) esc;
1203 	/// sends raw input data to the terminal as if the application printf()'d it or it echoed or whatever
1204 	void sendRawInput(in ubyte[] datain) {
1205 		const(ubyte)[] data = datain;
1206 	//import std.array;
1207 	//assert(!readingEsc, replace(cast(string) esc, "\033", "\\"));
1208 		again:
1209 		foreach(didx, b; data) {
1210 			if(readingExtensionData >= 0) {
1211 				if(readingExtensionData == extensionMagicIdentifier.length) {
1212 					if(b) {
1213 						switch(b) {
1214 							case 13, 10:
1215 								// ignore
1216 							break;
1217 							case 'A': .. case 'Z':
1218 							case 'a': .. case 'z':
1219 							case '0': .. case '9':
1220 							case '=':
1221 							case '+', '/':
1222 							case '_', '-':
1223 								// base64 ok
1224 								extensionData ~= b;
1225 							break;
1226 							default:
1227 								// others should abort the read
1228 								readingExtensionData = -1;
1229 						}
1230 					} else {
1231 						readingExtensionData = -1;
1232 						import std.base64;
1233 						auto got = handleBinaryExtensionData(Base64.decode(extensionData));
1234 
1235 						auto rep = got.representation;
1236 						foreach(y; 0 .. got.height) {
1237 							foreach(x; 0 .. got.width) {
1238 								addOutput(rep[0]);
1239 								rep = rep[1 .. $];
1240 							}
1241 							newLine(true);
1242 						}
1243 					}
1244 				} else {
1245 					if(b == extensionMagicIdentifier[readingExtensionData])
1246 						readingExtensionData++;
1247 					else {
1248 						// put the data back into the buffer, if possible
1249 						// (if the data was split across two packets, this may
1250 						//  not be possible. but in that case, meh.)
1251 						if(cast(int) didx - cast(int) readingExtensionData >= 0)
1252 							data = data[didx - readingExtensionData .. $];
1253 						readingExtensionData = -1;
1254 						goto again;
1255 					}
1256 				}
1257 
1258 				continue;
1259 			}
1260 
1261 			if(b == 0) {
1262 				readingExtensionData = 0;
1263 				extensionData = null;
1264 				continue;
1265 			}
1266 
1267 			if(readingEsc) {
1268 				if(b == 27) {
1269 					// an esc in the middle of a sequence will
1270 					// cancel the first one
1271 					esc = null;
1272 					continue;
1273 				}
1274 
1275 				if(b == 10) {
1276 					readingEsc = false;
1277 				}
1278 				esc ~= b;
1279 
1280 				if(esc.length == 1 && esc[0] == '7') {
1281 					pushSavedCursor(cursorPosition);
1282 					esc = null;
1283 					readingEsc = false;
1284 				} else if(esc.length == 1 && esc[0] == 'M') {
1285 					// reverse index
1286 					esc = null;
1287 					readingEsc = false;
1288 					if(cursorY <= scrollZoneTop)
1289 						scrollDown();
1290 					else
1291 						cursorY = cursorY - 1;
1292 				} else if(esc.length == 1 && esc[0] == '=') {
1293 					// application keypad
1294 					esc = null;
1295 					readingEsc = false;
1296 				} else if(esc.length == 2 && esc[0] == '%' && esc[1] == 'G') {
1297 					// UTF-8 mode
1298 					esc = null;
1299 					readingEsc = false;
1300 				} else if(esc.length == 1 && esc[0] == '8') {
1301 					cursorPosition = popSavedCursor;
1302 					esc = null;
1303 					readingEsc = false;
1304 				} else if(esc.length == 1 && esc[0] == 'c') {
1305 					// reset
1306 					// FIXME
1307 					esc = null;
1308 					readingEsc = false;
1309 				} else if(esc.length == 1 && esc[0] == '>') {
1310 					// normal keypad
1311 					esc = null;
1312 					readingEsc = false;
1313 				} else if(esc.length > 1 && (
1314 					(esc[0] == '[' && (b >= 64 && b <= 126)) ||
1315 					(esc[0] == ']' && b == '\007')))
1316 				{
1317 					try {
1318 						tryEsc(esc[]);
1319 					} catch(Exception e) {
1320 						unknownEscapeSequence(e.msg ~ " :: " ~ cast(char[]) esc[]);
1321 					}
1322 					esc = null;
1323 					readingEsc = false;
1324 				} else if(esc.length == 3 && esc[0] == '%' && esc[1] == 'G') {
1325 					// UTF-8 mode. ignored because we're always in utf-8 mode (though should we be?)
1326 					esc = null;
1327 					readingEsc = false;
1328 				} else if(esc.length == 2 && esc[0] == ')') {
1329 					// more character set selection. idk exactly how this works
1330 					esc = null;
1331 					readingEsc = false;
1332 				} else if(esc.length == 2 && esc[0] == '(') {
1333 					// xterm command for character set
1334 					// FIXME: handling esc[1] == '0' would be pretty boss
1335 					// and esc[1] == 'B' == united states
1336 					if(esc[1] == '0')
1337 						characterSet = &lineDrawingCharacterSet;
1338 					else
1339 						characterSet = null; // our default is UTF-8 and i don't care much about others anyway.
1340 
1341 					esc = null;
1342 					readingEsc = false;
1343 				} else if(esc.length == 1 && esc[0] == 'Z') {
1344 					// identify terminal
1345 					sendToApplication(terminalIdCode);
1346 				}
1347 				continue;
1348 			}
1349 
1350 			if(b == 27) {
1351 				readingEsc = true;
1352 				debug if(esc.isNull && esc.length) {
1353 					import std.stdio; writeln("discarding esc ", cast(string) esc[]);
1354 				}
1355 				esc = null;
1356 				continue;
1357 			}
1358 
1359 			if(b == 13) {
1360 				cursorX = 0;
1361 				setTentativeScrollback(0);
1362 				continue;
1363 			}
1364 
1365 			if(b == 7) {
1366 				soundBell();
1367 				continue;
1368 			}
1369 
1370 			if(b == 8) {
1371 				cursorX = cursorX - 1;
1372 				setTentativeScrollback(cursorX);
1373 				continue;
1374 			}
1375 
1376 			if(b == 9) {
1377 				int howMany = 8 - (cursorX % 8);
1378 				// so apparently it is just supposed to move the cursor.
1379 				// it breaks mutt to output spaces
1380 				cursorX = cursorX + howMany;
1381 
1382 				if(!alternateScreenActive)
1383 					foreach(i; 0 .. howMany)
1384 						addScrollbackOutput(' '); // FIXME: it would be nice to actually put a tab character there for copy/paste accuracy (ditto with newlines actually)
1385 				continue;
1386 			}
1387 
1388 //			std.stdio.writeln("READ ", data[w]);
1389 			addOutput(b);
1390 		}
1391 	}
1392 
1393 
1394 	/// construct
1395 	this(int width, int height) {
1396 		// initialization
1397 
1398 		import std.process;
1399 		if(environment.get("ELVISBG") == "dark") {
1400 			defaultForeground = Color.white;
1401 			defaultBackground = Color.black;
1402 		} else {
1403 			defaultForeground = Color.black;
1404 			defaultBackground = Color.white;
1405 		}
1406 
1407 		currentAttributes = defaultTextAttributes();
1408 		cursorColor = Color.white;
1409 
1410 		palette[] = xtermPalette[];
1411 
1412 		resizeTerminal(width, height);
1413 
1414 		// update the other thing
1415 		if(windowTitle.length == 0)
1416 			windowTitle = "Terminal Emulator";
1417 		changeWindowTitle(windowTitle);
1418 		changeIconTitle(iconTitle);
1419 		changeTextAttributes(currentAttributes);
1420 	}
1421 
1422 
1423 	private {
1424 		TerminalCell[] scrollbackMainScreen;
1425 		bool scrollbackCursorShowing;
1426 		int scrollbackCursorX;
1427 		int scrollbackCursorY;
1428 	}
1429 
1430 	protected {
1431 		bool scrollingBack;
1432 
1433 		int currentScrollback;
1434 		int currentScrollbackX;
1435 	}
1436 
1437 	// FIXME: if it is resized while scrolling back, stuff can get messed up
1438 
1439 	private int scrollbackLength_;
1440 	private void scrollbackLength(int i) {
1441 		scrollbackLength_ = i;
1442 	}
1443 
1444 	int scrollbackLength() {
1445 		return scrollbackLength_;
1446 	}
1447 
1448 	private int scrollbackWidth_;
1449 	int scrollbackWidth() {
1450 		return scrollbackWidth_ > screenWidth ? scrollbackWidth_ : screenWidth;
1451 	}
1452 
1453 	/* virtual */ void notifyScrollbackAdded() {}
1454 	/* virtual */ void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) {}
1455 	/* virtual */ void notifyScrollbarPosition(int x, int y) {}
1456 
1457 	// coordinates are for a scroll bar, where 0,0 is the beginning of history
1458 	void scrollbackTo(int x, int y) {
1459 		if(alternateScreenActive && !scrollingBack)
1460 			return;
1461 
1462 		if(!scrollingBack) {
1463 			startScrollback();
1464 		}
1465 
1466 		if(y < 0)
1467 			y = 0;
1468 		if(x < 0)
1469 			x = 0;
1470 
1471 		currentScrollbackX = x;
1472 		currentScrollback = scrollbackLength - y;
1473 
1474 		if(currentScrollback < 0)
1475 			currentScrollback = 0;
1476 		if(currentScrollbackX < 0)
1477 			currentScrollbackX = 0;
1478 
1479 		if(!scrollLock && currentScrollback == 0 && currentScrollbackX == 0) {
1480 			endScrollback();
1481 		} else {
1482 			cls();
1483 			showScrollbackOnScreen(alternateScreen, currentScrollback, false, currentScrollbackX);
1484 		}
1485 	}
1486 
1487 	void scrollback(int delta, int deltaX = 0) {
1488 		if(alternateScreenActive && !scrollingBack)
1489 			return;
1490 
1491 		if(!scrollingBack) {
1492 			if(delta <= 0 && deltaX == 0)
1493 				return; // it does nothing to scroll down when not scrolling back
1494 			startScrollback();
1495 		}
1496 		currentScrollback += delta;
1497 		if(!scrollbackReflow && deltaX) {
1498 			currentScrollbackX += deltaX;
1499 			int max = scrollbackWidth - screenWidth;
1500 			if(max < 0)
1501 				max = 0;
1502 			if(currentScrollbackX > max)
1503 				currentScrollbackX = max;
1504 			if(currentScrollbackX < 0)
1505 				currentScrollbackX = 0;
1506 		}
1507 
1508 		int max = cast(int) scrollbackBuffer.length - screenHeight;
1509 		if(scrollbackReflow && max < 0) {
1510 			foreach(line; scrollbackBuffer[]) {
1511 				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
1512 					max += 0;
1513 				else
1514 					max += cast(int) line.length / screenWidth;
1515 			}
1516 		}
1517 
1518 		if(max < 0)
1519 			max = 0;
1520 
1521 		if(scrollbackReflow && currentScrollback > max) {
1522 			foreach(line; scrollbackBuffer[]) {
1523 				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
1524 					max += 0;
1525 				else
1526 					max += cast(int) line.length / screenWidth;
1527 			}
1528 		}
1529 
1530 		if(currentScrollback > max)
1531 			currentScrollback = max;
1532 		if(currentScrollback < 0)
1533 			currentScrollback = 0;
1534 
1535 		if(!scrollLock && currentScrollback <= 0 && currentScrollbackX <= 0)
1536 			endScrollback();
1537 		else {
1538 			cls();
1539 			showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
1540 			notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
1541 		}
1542 	}
1543 
1544 	private void startScrollback() {
1545 		if(scrollingBack)
1546 			return;
1547 		currentScrollback = 0;
1548 		currentScrollbackX = 0;
1549 		scrollingBack = true;
1550 		scrollbackCursorX = cursorX;
1551 		scrollbackCursorY = cursorY;
1552 		scrollbackCursorShowing = cursorShowing;
1553 		scrollbackMainScreen = alternateScreen.dup;
1554 		alternateScreenActive = true;
1555 
1556 		cursorShowing = false;
1557 	}
1558 
1559 	bool endScrollback() {
1560 		//if(scrollLock)
1561 		//	return false;
1562 		if(!scrollingBack)
1563 			return false;
1564 		scrollingBack = false;
1565 		cursorX = scrollbackCursorX;
1566 		cursorY = scrollbackCursorY;
1567 		cursorShowing = scrollbackCursorShowing;
1568 		alternateScreen = scrollbackMainScreen;
1569 		alternateScreenActive = false;
1570 
1571 		currentScrollback = 0;
1572 		currentScrollbackX = 0;
1573 
1574 		if(!scrollLock) {
1575 			scrollbackReflow = true;
1576 			recalculateScrollbackLength();
1577 		}
1578 
1579 		notifyScrollbarPosition(0, int.max);
1580 
1581 		return true;
1582 	}
1583 
1584 	private bool scrollbackReflow = true;
1585 	/* deprecated? */
1586 	public void toggleScrollbackWrap() {
1587 		scrollbackReflow = !scrollbackReflow;
1588 		recalculateScrollbackLength();
1589 	}
1590 
1591 	private bool scrollLockLockEnabled = false;
1592 	package void scrollLockLock() {
1593 		scrollLockLockEnabled = true;
1594 		if(!scrollLock)
1595 			toggleScrollLock();
1596 	}
1597 
1598 	private bool scrollLock = false;
1599 	public void toggleScrollLock() {
1600 		if(scrollLockLockEnabled && scrollLock)
1601 			goto nochange;
1602 		scrollLock = !scrollLock;
1603 		scrollbackReflow = !scrollLock;
1604 
1605 		nochange:
1606 		recalculateScrollbackLength();
1607 
1608 		if(scrollLock) {
1609 			startScrollback();
1610 
1611 			cls();
1612 			currentScrollback = 0;
1613 			currentScrollbackX = 0;
1614 			showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
1615 			notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
1616 		} else {
1617 			endScrollback();
1618 		}
1619 
1620 		//cls();
1621 		//drawScrollback();
1622 	}
1623 
1624 	private void recalculateScrollbackLength() {
1625 		int count = cast(int) scrollbackBuffer.length;
1626 		int max;
1627 		if(scrollbackReflow) {
1628 			foreach(line; scrollbackBuffer[]) {
1629 				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
1630 					{} // intentionally blank, the count is fine since this line isn't reflowed anyway
1631 				else
1632 					count += cast(int) line.length / screenWidth;
1633 			}
1634 		} else {
1635 			foreach(line; scrollbackBuffer[]) {
1636 				if(line.length > max)
1637 					max = cast(int) line.length;
1638 			}
1639 		}
1640 		scrollbackWidth_ = max;
1641 		scrollbackLength = count;
1642 		notifyScrollbackAdded();
1643 		notifyScrollbarPosition(currentScrollbackX, currentScrollback ? scrollbackLength - currentScrollback : int.max);
1644 	}
1645 
1646 	/++
1647 		Writes the text in the scrollback buffer to the given file.
1648 
1649 		Discards formatting information and embedded images.
1650 
1651 		See_Also:
1652 			[writeScrollbackToDelegate]
1653 	+/
1654 	public void writeScrollbackToFile(string filename) {
1655 		import std.stdio;
1656 		auto file = File(filename, "wt");
1657 		foreach(line; scrollbackBuffer[]) {
1658 			foreach(c; line)
1659 				if(!c.hasNonCharacterData)
1660 					file.write(c.ch); // I hope this is buffered
1661 			file.writeln();
1662 		}
1663 	}
1664 
1665 	/++
1666 		Writes the text in the scrollback buffer to the given delegate, one character at a time.
1667 
1668 		Discards formatting information and embedded images.
1669 
1670 		See_Also:
1671 			[writeScrollbackToFile]
1672 		History:
1673 			Added March 14, 2021 (dub version 9.4)
1674 	+/
1675 	public void writeScrollbackToDelegate(scope void delegate(dchar c) dg) {
1676 		foreach(line; scrollbackBuffer[]) {
1677 			foreach(c; line)
1678 				if(!c.hasNonCharacterData)
1679 					dg(c.ch);
1680 			dg('\n');
1681 		}
1682 	}
1683 
1684 	public void drawScrollback(bool useAltScreen = false) {
1685 		showScrollbackOnScreen(useAltScreen ? alternateScreen : normalScreen, 0, true, 0);
1686 	}
1687 
1688 	private void showScrollbackOnScreen(ref TerminalCell[] screen, int howFar, bool reflow, int howFarX) {
1689 		int start;
1690 
1691 		cursorX = 0;
1692 		cursorY = 0;
1693 
1694 		int excess = 0;
1695 
1696 		if(scrollbackReflow) {
1697 			int numLines;
1698 			int idx = cast(int) scrollbackBuffer.length - 1;
1699 			foreach_reverse(line; scrollbackBuffer[]) {
1700 				auto lineCount = 1 + line.length / screenWidth;
1701 
1702 				// if the line has an image in it, it cannot be reflowed. this hack to check just the first and last thing is the cheapest way rn
1703 				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
1704 					lineCount = 1;
1705 
1706 				numLines += lineCount;
1707 				if(numLines >= (screenHeight + howFar)) {
1708 					start = cast(int) idx;
1709 					excess = numLines - (screenHeight + howFar);
1710 					break;
1711 				}
1712 				idx--;
1713 			}
1714 		} else {
1715 			auto termination = cast(int) scrollbackBuffer.length - howFar;
1716 			if(termination < 0)
1717 				termination = cast(int) scrollbackBuffer.length;
1718 
1719 			start = termination - screenHeight;
1720 			if(start < 0)
1721 				start = 0;
1722 		}
1723 
1724 		TerminalCell overflowCell;
1725 		overflowCell.ch = '\&raquo;';
1726 		overflowCell.attributes.backgroundIndex = 3;
1727 		overflowCell.attributes.foregroundIndex = 0;
1728 		version(with_24_bit_color) {
1729 			overflowCell.attributes.foreground = Color(40, 40, 40);
1730 			overflowCell.attributes.background = Color.yellow;
1731 		}
1732 
1733 		outer: foreach(line; scrollbackBuffer[start .. $]) {
1734 			if(excess) {
1735 				line = line[excess * screenWidth .. $];
1736 				excess = 0;
1737 			}
1738 
1739 			if(howFarX) {
1740 				if(howFarX <= line.length)
1741 					line = line[howFarX .. $];
1742 				else
1743 					line = null;
1744 			}
1745 
1746 			bool overflowed;
1747 			foreach(cell; line) {
1748 				cell.invalidated = true;
1749 				if(overflowed) {
1750 					screen[cursorY * screenWidth + cursorX] = overflowCell;
1751 					break;
1752 				} else {
1753 					screen[cursorY * screenWidth + cursorX] = cell;
1754 				}
1755 
1756 				if(cursorX == screenWidth-1) {
1757 					if(scrollbackReflow) {
1758 						// don't attempt to reflow images
1759 						if(cell.hasNonCharacterData)
1760 							break;
1761 						cursorX = 0;
1762 						if(cursorY + 1 == screenHeight)
1763 							break outer;
1764 						cursorY = cursorY + 1;
1765 					} else {
1766 						overflowed = true;
1767 					}
1768 				} else
1769 					cursorX = cursorX + 1;
1770 			}
1771 			if(cursorY + 1 == screenHeight)
1772 				break;
1773 			cursorY = cursorY + 1;
1774 			cursorX = 0;
1775 		}
1776 
1777 		cursorX = 0;
1778 	}
1779 
1780 	protected bool cueScrollback;
1781 
1782 	public void resizeTerminal(int w, int h) {
1783 		if(w == screenWidth && h == screenHeight)
1784 			return; // we're already good, do nothing to avoid wasting time and possibly losing a line (bash doesn't seem to like being told it "resized" to the same size)
1785 
1786 		// do i like this?
1787 		if(scrollLock)
1788 			toggleScrollLock();
1789 
1790 		// FIXME: hack
1791 		endScrollback();
1792 
1793 		screenWidth = w;
1794 		screenHeight = h;
1795 
1796 		normalScreen.length = screenWidth * screenHeight;
1797 		alternateScreen.length = screenWidth * screenHeight;
1798 		scrollZoneBottom = screenHeight - 1;
1799 		if(scrollZoneTop < 0 || scrollZoneTop >= scrollZoneBottom)
1800 			scrollZoneTop = 0;
1801 
1802 		// we need to make sure the state is sane all across the board, so first we'll clear everything...
1803 		TerminalCell plain;
1804 		plain.ch = ' ';
1805 		plain.attributes = defaultTextAttributes;
1806 		plain.invalidated = true;
1807 		normalScreen[] = plain;
1808 		alternateScreen[] = plain;
1809 
1810 		// then, in normal mode, we'll redraw using the scrollback buffer
1811 		//
1812 		// if we're in the alternate screen though, keep it blank because
1813 		// while redrawing makes sense in theory, odds are the program in
1814 		// charge of the normal screen didn't get the resize signal.
1815 		if(!alternateScreenActive)
1816 			showScrollbackOnScreen(normalScreen, 0, true, 0);
1817 		else
1818 			cueScrollback = true;
1819 		// but in alternate mode, it is the application's responsibility
1820 
1821 		// the property ensures these are within bounds so this set just forces that
1822 		cursorY = cursorY;
1823 		cursorX = cursorX;
1824 
1825 		recalculateScrollbackLength();
1826 	}
1827 
1828 	private CursorPosition popSavedCursor() {
1829 		CursorPosition pos;
1830 		//import std.stdio; writeln("popped");
1831 		if(savedCursors.length) {
1832 			pos = savedCursors[$-1];
1833 			savedCursors = savedCursors[0 .. $-1];
1834 			savedCursors.assumeSafeAppend(); // we never keep references elsewhere so might as well reuse the memory as much as we can
1835 		}
1836 
1837 		// If the screen resized after this was saved, it might be restored to a bad amount, so we need to sanity test.
1838 		if(pos.x < 0)
1839 			pos.x = 0;
1840 		if(pos.y < 0)
1841 			pos.y = 0;
1842 		if(pos.x > screenWidth)
1843 			pos.x = screenWidth - 1;
1844 		if(pos.y > screenHeight)
1845 			pos.y = screenHeight - 1;
1846 
1847 		return pos;
1848 	}
1849 
1850 	private void pushSavedCursor(CursorPosition pos) {
1851 		//import std.stdio; writeln("pushed");
1852 		savedCursors ~= pos;
1853 	}
1854 
1855 	public void clearScrollbackHistory() {
1856 		if(scrollingBack)
1857 			endScrollback();
1858 		scrollbackBuffer.clear();
1859 		scrollbackLength_ = 0;
1860 		scrollbackWidth_ = 0;
1861 
1862 		notifyScrollbackAdded();
1863 	}
1864 
1865 	public void moveCursor(int x, int y) {
1866 		cursorX = x;
1867 		cursorY = y;
1868 	}
1869 
1870 	/* FIXME: i want these to be private */
1871 	protected {
1872 		TextAttributes currentAttributes;
1873 		CursorPosition cursorPosition;
1874 		CursorPosition[] savedCursors; // a stack
1875 		CursorStyle cursorStyle;
1876 		Color cursorColor;
1877 		string windowTitle;
1878 		string iconTitle;
1879 
1880 		bool attentionDemanded;
1881 
1882 		IndexedImage windowIcon;
1883 		IndexedImage[] iconStack;
1884 
1885 		string[] titleStack;
1886 
1887 		bool bracketedPasteMode;
1888 		bool bracketedHyperlinkMode;
1889 		bool mouseButtonTracking;
1890 		private bool _mouseMotionTracking;
1891 		bool mouseButtonReleaseTracking;
1892 		bool mouseButtonMotionTracking;
1893 		bool selectiveMouseTracking;
1894 		/+
1895 			When set, it causes xterm to send CSI I when the terminal gains focus, and CSI O  when it loses focus.
1896 			this is turned on by mode 1004 with mouse events.
1897 
1898 			FIXME: not implemented.
1899 		+/
1900 		bool sendFocusEvents;
1901 
1902 		bool mouseMotionTracking() {
1903 			return _mouseMotionTracking;
1904 		}
1905 
1906 		void mouseMotionTracking(bool b) {
1907 			_mouseMotionTracking = b;
1908 		}
1909 
1910 		void allMouseTrackingOff() {
1911 			selectiveMouseTracking = false;
1912 			mouseMotionTracking = false;
1913 			mouseButtonTracking = false;
1914 			mouseButtonReleaseTracking = false;
1915 			mouseButtonMotionTracking = false;
1916 			sendFocusEvents = false;
1917 		}
1918 
1919 		bool wraparoundMode = true;
1920 
1921 		bool alternateScreenActive;
1922 		bool cursorShowing = true;
1923 
1924 		bool reverseVideo;
1925 		bool applicationCursorKeys;
1926 
1927 		bool scrollingEnabled = true;
1928 		int scrollZoneTop;
1929 		int scrollZoneBottom;
1930 
1931 		int screenWidth;
1932 		int screenHeight;
1933 		// assert(alternateScreen.length = screenWidth * screenHeight);
1934 		TerminalCell[] alternateScreen;
1935 		TerminalCell[] normalScreen;
1936 
1937 		// the lengths can be whatever
1938 		ScrollbackBuffer scrollbackBuffer;
1939 
1940 		static struct ScrollbackBuffer {
1941 			TerminalCell[][] backing;
1942 
1943 			enum maxScrollback = 8192 / 2; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask...
1944 
1945 			int start;
1946 			int length_;
1947 
1948 			size_t length() {
1949 				return length_;
1950 			}
1951 
1952 			void clear() {
1953 				start = 0;
1954 				length_ = 0;
1955 				backing = null;
1956 			}
1957 
1958 			// FIXME: if scrollback hits limits the scroll bar needs
1959 			// to understand the circular buffer
1960 
1961 			void opOpAssign(string op : "~")(TerminalCell[] line) {
1962 				if(length_ < maxScrollback) {
1963 					backing.assumeSafeAppend();
1964 					backing ~= line;
1965 					length_++;
1966 				} else {
1967 					backing[start] = line;
1968 					start++;
1969 					if(start == maxScrollback)
1970 						start = 0;
1971 				}
1972 			}
1973 
1974 			/*
1975 			int opApply(scope int delegate(ref TerminalCell[]) dg) {
1976 				foreach(ref l; backing)
1977 					if(auto res = dg(l))
1978 						return res;
1979 				return 0;
1980 			}
1981 
1982 			int opApplyReverse(scope int delegate(size_t, ref TerminalCell[]) dg) {
1983 				foreach_reverse(idx, ref l; backing)
1984 					if(auto res = dg(idx, l))
1985 						return res;
1986 				return 0;
1987 			}
1988 			*/
1989 
1990 			TerminalCell[] opIndex(int idx) {
1991 				return backing[(start + idx) % maxScrollback];
1992 			}
1993 
1994 			ScrollbackBufferRange opSlice(int startOfIteration, Dollar end) {
1995 				return ScrollbackBufferRange(&this, startOfIteration);
1996 			}
1997 			ScrollbackBufferRange opSlice() {
1998 				return ScrollbackBufferRange(&this, 0);
1999 			}
2000 
2001 			static struct ScrollbackBufferRange {
2002 				ScrollbackBuffer* item;
2003 				int position;
2004 				int remaining;
2005 				this(ScrollbackBuffer* item, int startOfIteration) {
2006 					this.item = item;
2007 					position = startOfIteration;
2008 					remaining = cast(int) item.length - startOfIteration;
2009 
2010 				}
2011 
2012 				TerminalCell[] front() { return (*item)[position]; }
2013 				bool empty() { return remaining <= 0; }
2014 				void popFront() {
2015 					position++;
2016 					remaining--;
2017 				}
2018 
2019 				TerminalCell[] back() { return (*item)[remaining - 1 - position]; }
2020 				void popBack() {
2021 					remaining--;
2022 				}
2023 			}
2024 
2025 			static struct Dollar {};
2026 			Dollar opDollar() { return Dollar(); }
2027 
2028 		}
2029 
2030 		struct Helper2 {
2031 			size_t row;
2032 			TerminalEmulator t;
2033 			this(TerminalEmulator t, size_t row) {
2034 				this.t = t;
2035 				this.row = row;
2036 			}
2037 
2038 			ref TerminalCell opIndex(size_t cell) {
2039 				auto thing = t.alternateScreenActive ? &(t.alternateScreen) : &(t.normalScreen);
2040 				return (*thing)[row * t.screenWidth + cell];
2041 			}
2042 		}
2043 
2044 		struct Helper {
2045 			TerminalEmulator t;
2046 			this(TerminalEmulator t) {
2047 				this.t = t;
2048 			}
2049 
2050 			Helper2 opIndex(size_t row) {
2051 				return Helper2(t, row);
2052 			}
2053 		}
2054 
2055 		@property Helper ASS() {
2056 			return Helper(this);
2057 		}
2058 
2059 		@property int cursorX() { return cursorPosition.x; }
2060 		@property int cursorY() { return cursorPosition.y; }
2061 		@property void cursorX(int x) {
2062 			if(x < 0)
2063 				x = 0;
2064 			if(x >= screenWidth)
2065 				x = screenWidth - 1;
2066 			cursorPosition.x = x;
2067 		}
2068 		@property void cursorY(int y) {
2069 			if(y < 0)
2070 				y = 0;
2071 			if(y >= screenHeight)
2072 				y = screenHeight - 1;
2073 			cursorPosition.y = y;
2074 		}
2075 
2076 		void addOutput(string b) {
2077 			foreach(c; b)
2078 				addOutput(c);
2079 		}
2080 
2081 		TerminalCell[] currentScrollbackLine;
2082 		ubyte[6] utf8SequenceBuffer;
2083 		int utf8SequenceBufferPosition;
2084 		// int scrollbackWrappingAt = 0;
2085 		dchar utf8Sequence;
2086 		int utf8BytesRemaining;
2087 		int currentUtf8Shift;
2088 		bool newLineOnNext;
2089 		void addOutput(ubyte b) {
2090 
2091 			void addChar(dchar c) {
2092 				if(newLineOnNext) {
2093 					newLineOnNext = false;
2094 					// only if we're still on the right side...
2095 					if(cursorX == screenWidth - 1)
2096 						newLine(false);
2097 				}
2098 				TerminalCell tc;
2099 
2100 				if(characterSet !is null) {
2101 					if(auto replacement = utf8Sequence in *characterSet)
2102 						utf8Sequence = *replacement;
2103 				}
2104 				tc.ch = utf8Sequence;
2105 				tc.attributes = currentAttributes;
2106 				tc.invalidated = true;
2107 
2108 				if(hyperlinkActive) {
2109 					tc.hyperlinkStatus = hyperlinkFlipper ? 3 : 1;
2110 					tc.hyperlinkBit = hyperlinkNumber & 0x01;
2111 					hyperlinkNumber >>= 1;
2112 				}
2113 
2114 				addOutput(tc);
2115 			}
2116 
2117 
2118 			// this takes in bytes at a time, but since the input encoding is assumed to be UTF-8, we need to gather the bytes
2119 			if(utf8BytesRemaining == 0) {
2120 				// we're at the beginning of a sequence
2121 				utf8Sequence = 0;
2122 				if(b < 128) {
2123 					utf8Sequence = cast(dchar) b;
2124 					// one byte thing, do nothing more...
2125 				} else {
2126 					// the number of bytes in the sequence is the number of set bits in the first byte...
2127 					ubyte checkingBit = 7;
2128 					while(b & (1 << checkingBit)) {
2129 						utf8BytesRemaining++;
2130 						checkingBit--;
2131 					}
2132 					uint shifted = b & ((1 << checkingBit) - 1);
2133 					utf8BytesRemaining--; // since this current byte counts too
2134 					currentUtf8Shift = utf8BytesRemaining * 6;
2135 
2136 
2137 					shifted <<= currentUtf8Shift;
2138 					utf8Sequence = cast(dchar) shifted;
2139 
2140 					utf8SequenceBufferPosition = 0;
2141 					utf8SequenceBuffer[utf8SequenceBufferPosition++] = b;
2142 				}
2143 			} else {
2144 				// add this to the byte we're doing right now...
2145 				utf8BytesRemaining--;
2146 				currentUtf8Shift -= 6;
2147 				if((b & 0b11000000) != 0b10000000) {
2148 					// invalid utf-8 sequence,
2149 					// discard it and try to continue
2150 					utf8BytesRemaining = 0;
2151 					utf8Sequence = 0xfffd;
2152 					foreach(i; 0 .. utf8SequenceBufferPosition)
2153 						addChar(utf8Sequence); // put out replacement char for everything in there so far
2154 					utf8SequenceBufferPosition = 0;
2155 					addOutput(b); // retry sending this byte as a new sequence after abandoning the old crap
2156 					return;
2157 				}
2158 				uint shifted = b;
2159 				shifted &= 0b00111111;
2160 				shifted <<= currentUtf8Shift;
2161 				utf8Sequence |= shifted;
2162 
2163 				if(utf8SequenceBufferPosition < utf8SequenceBuffer.length)
2164 					utf8SequenceBuffer[utf8SequenceBufferPosition++] = b;
2165 			}
2166 
2167 			if(utf8BytesRemaining)
2168 				return; // not enough data yet, wait for more before displaying anything
2169 
2170 			if(utf8Sequence == 10) {
2171 				newLineOnNext = false;
2172 				auto cx = cursorX; // FIXME: this cx thing is a hack, newLine should prolly just do the right thing
2173 
2174 				/*
2175 				TerminalCell tc;
2176 				tc.ch = utf8Sequence;
2177 				tc.attributes = currentAttributes;
2178 				tc.invalidated = true;
2179 				addOutput(tc);
2180 				*/
2181 
2182 				newLine(true);
2183 				cursorX = cx;
2184 			} else {
2185 				addChar(utf8Sequence);
2186 			}
2187 		}
2188 
2189 		private int recalculationThreshold = 0;
2190 		public void addScrollbackLine(TerminalCell[] line) {
2191 			scrollbackBuffer ~= line;
2192 
2193 			if(scrollbackBuffer.length_ == ScrollbackBuffer.maxScrollback) {
2194 				recalculationThreshold++;
2195 				if(recalculationThreshold > 100) {
2196 					recalculateScrollbackLength();
2197 					notifyScrollbackAdded();
2198 					recalculationThreshold = 0;
2199 				}
2200 			} else {
2201 				if(!scrollbackReflow && line.length > scrollbackWidth_)
2202 					scrollbackWidth_ = cast(int) line.length;
2203 
2204 				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
2205 					scrollbackLength = scrollbackLength + 1;
2206 				else
2207 					scrollbackLength = cast(int) (scrollbackLength + 1 + (scrollbackBuffer[cast(int) scrollbackBuffer.length - 1].length) / screenWidth);
2208 				notifyScrollbackAdded();
2209 			}
2210 
2211 			if(!alternateScreenActive)
2212 				notifyScrollbarPosition(0, int.max);
2213 		}
2214 
2215 		protected int maxScrollbackLength() pure const @nogc nothrow {
2216 			return 1024;
2217 		}
2218 
2219 		bool insertMode = false;
2220 		void newLine(bool commitScrollback) {
2221 			if(!alternateScreenActive && commitScrollback) {
2222 				// I am limiting this because obscenely long lines are kinda useless anyway and
2223 				// i don't want it to eat excessive memory when i spam some thing accidentally
2224 				if(currentScrollbackLine.length < maxScrollbackLength())
2225 					addScrollbackLine(currentScrollbackLine.sliceTrailingWhitespace);
2226 				else
2227 					addScrollbackLine(currentScrollbackLine[0 .. maxScrollbackLength()].sliceTrailingWhitespace);
2228 
2229 				currentScrollbackLine = null;
2230 				currentScrollbackLine.reserve(64);
2231 				// scrollbackWrappingAt = 0;
2232 			}
2233 
2234 			cursorX = 0;
2235 			if(scrollingEnabled && cursorY >= scrollZoneBottom) {
2236 				size_t idx = scrollZoneTop * screenWidth;
2237 
2238 				// When we scroll up, we need to update the selection position too
2239 				if(selectionStart != selectionEnd) {
2240 					selectionStart -= screenWidth;
2241 					selectionEnd -= screenWidth;
2242 				}
2243 				foreach(l; scrollZoneTop .. scrollZoneBottom) {
2244 					if(alternateScreenActive) {
2245 						if(idx + screenWidth * 2 > alternateScreen.length)
2246 							break;
2247 						alternateScreen[idx .. idx + screenWidth] = alternateScreen[idx + screenWidth .. idx + screenWidth * 2];
2248 					} else {
2249 						if(screenWidth <= 0)
2250 							break;
2251 						if(idx + screenWidth * 2 > normalScreen.length)
2252 							break;
2253 						normalScreen[idx .. idx + screenWidth] = normalScreen[idx + screenWidth .. idx + screenWidth * 2];
2254 					}
2255 					idx += screenWidth;
2256 				}
2257 				/*
2258 				foreach(i; 0 .. screenWidth) {
2259 					if(alternateScreenActive) {
2260 						alternateScreen[idx] = alternateScreen[idx + screenWidth];
2261 						alternateScreen[idx].invalidated = true;
2262 					} else {
2263 						normalScreen[idx] = normalScreen[idx + screenWidth];
2264 						normalScreen[idx].invalidated = true;
2265 					}
2266 					idx++;
2267 				}
2268 				*/
2269 				/*
2270 				foreach(i; 0 .. screenWidth) {
2271 					if(alternateScreenActive) {
2272 						alternateScreen[idx].ch = ' ';
2273 						alternateScreen[idx].attributes = currentAttributes;
2274 						alternateScreen[idx].invalidated = true;
2275 					} else {
2276 						normalScreen[idx].ch = ' ';
2277 						normalScreen[idx].attributes = currentAttributes;
2278 						normalScreen[idx].invalidated = true;
2279 					}
2280 					idx++;
2281 				}
2282 				*/
2283 
2284 				TerminalCell plain;
2285 				plain.ch = ' ';
2286 				plain.attributes = currentAttributes;
2287 				if(alternateScreenActive) {
2288 					alternateScreen[idx .. idx + screenWidth] = plain;
2289 				} else {
2290 					normalScreen[idx .. idx + screenWidth] = plain;
2291 				}
2292 			} else {
2293 				if(insertMode) {
2294 					scrollDown();
2295 				} else
2296 					cursorY = cursorY + 1;
2297 			}
2298 
2299 			invalidateAll = true;
2300 		}
2301 
2302 		protected bool invalidateAll;
2303 
2304 		void clearSelection() {
2305 			clearSelectionInternal();
2306 			cancelOverriddenSelection();
2307 		}
2308 
2309 		private void clearSelectionInternal() {
2310 			foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen)
2311 				if(tc.selected) {
2312 					tc.selected = false;
2313 					tc.invalidated = true;
2314 				}
2315 			selectionStart = 0;
2316 			selectionEnd = 0;
2317 		}
2318 
2319 		private int tentativeScrollback = int.max;
2320 		private void setTentativeScrollback(int a) {
2321 			tentativeScrollback = a;
2322 		}
2323 
2324 		void addScrollbackOutput(dchar ch) {
2325 			TerminalCell plain;
2326 			plain.ch = ch;
2327 			plain.attributes = currentAttributes;
2328 			addScrollbackOutput(plain);
2329 		}
2330 
2331 		void addScrollbackOutput(TerminalCell tc) {
2332 			if(tentativeScrollback != int.max) {
2333 				if(tentativeScrollback >= 0 && tentativeScrollback < currentScrollbackLine.length) {
2334 					currentScrollbackLine = currentScrollbackLine[0 .. tentativeScrollback];
2335 					currentScrollbackLine.assumeSafeAppend();
2336 				}
2337 				tentativeScrollback = int.max;
2338 			}
2339 
2340 			/*
2341 			TerminalCell plain;
2342 			plain.ch = ' ';
2343 			plain.attributes = currentAttributes;
2344 			int lol = cursorX + scrollbackWrappingAt;
2345 			while(lol >= currentScrollbackLine.length)
2346 				currentScrollbackLine ~= plain;
2347 			currentScrollbackLine[lol] = tc;
2348 			*/
2349 
2350 			currentScrollbackLine ~= tc;
2351 
2352 		}
2353 
2354 		void addOutput(TerminalCell tc) {
2355 			if(alternateScreenActive) {
2356 				if(alternateScreen[cursorY * screenWidth + cursorX].selected) {
2357 					clearSelection();
2358 				}
2359 				alternateScreen[cursorY * screenWidth + cursorX] = tc;
2360 			} else {
2361 				if(normalScreen[cursorY * screenWidth + cursorX].selected) {
2362 					clearSelection();
2363 				}
2364 				// FIXME: make this more efficient if it is writing the same thing,
2365 				// then it need not be invalidated. Same with above for the alt screen
2366 				normalScreen[cursorY * screenWidth + cursorX] = tc;
2367 
2368 				addScrollbackOutput(tc);
2369 			}
2370 			// FIXME: the wraparoundMode seems to help gnu screen but then it doesn't go away properly and that messes up bash...
2371 			//if(wraparoundMode && cursorX == screenWidth - 1) {
2372 			if(cursorX == screenWidth - 1) {
2373 				// FIXME: should this check the scrolling zone instead?
2374 				newLineOnNext = true;
2375 
2376 				//if(!alternateScreenActive || cursorY < screenHeight - 1)
2377 					//newLine(false);
2378 
2379 				// scrollbackWrappingAt = cast(int) currentScrollbackLine.length;
2380 			} else
2381 				cursorX = cursorX + 1;
2382 
2383 		}
2384 
2385 		void tryEsc(ubyte[] esc) {
2386 			bool[2] sidxProcessed;
2387 			int[][2] argsAtSidx;
2388 			int[12][2] argsAtSidxBuffer;
2389 
2390 			int[12][4] argsBuffer;
2391 			int argsBufferLocation;
2392 
2393 			int[] getArgsBase(int sidx, int[] defaults) {
2394 				assert(sidx == 1 || sidx == 2);
2395 
2396 				if(sidxProcessed[sidx - 1]) {
2397 					int[] bfr = argsBuffer[argsBufferLocation++][];
2398 					if(argsBufferLocation == argsBuffer.length)
2399 						argsBufferLocation = 0;
2400 					bfr[0 .. defaults.length] = defaults[];
2401 					foreach(idx, v; argsAtSidx[sidx - 1])
2402 						if(v != int.min)
2403 							bfr[idx] = v;
2404 					return bfr[0 .. max(argsAtSidx[sidx - 1].length, defaults.length)];
2405 				}
2406 
2407 				auto end = esc.length - 1;
2408 				foreach(iii, b; esc[sidx .. end]) {
2409 					if(b >= 0x20 && b < 0x30)
2410 						end = iii + sidx;
2411 				}
2412 
2413 				auto argsSection = cast(char[]) esc[sidx .. end];
2414 				int[] args = argsAtSidxBuffer[sidx - 1][];
2415 
2416 				import std..string : split;
2417 				import std.conv : to;
2418 				int lastIdx = 0;
2419 
2420 				foreach(i, arg; split(argsSection, ";")) {
2421 					int value;
2422 					if(arg.length) {
2423 						//import std.stdio; writeln(esc);
2424 						value = to!int(arg);
2425 					} else
2426 						value = int.min; // defaults[i];
2427 
2428 					if(args.length > i)
2429 						args[i] = value;
2430 					else
2431 						assert(0);
2432 					lastIdx++;
2433 				}
2434 
2435 				argsAtSidx[sidx - 1] = args[0 .. lastIdx];
2436 				sidxProcessed[sidx - 1] = true;
2437 
2438 				return getArgsBase(sidx, defaults);
2439 			}
2440 			int[] getArgs(int[] defaults...) {
2441 				return getArgsBase(1, defaults);
2442 			}
2443 
2444 			// FIXME
2445 			// from  http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
2446 			// check out this section: "Window manipulation (from dtterm, as well as extensions)"
2447 			// especially the title stack, that should rock
2448 			/*
2449 P s = 2 2 ; 0 → Save xterm icon and window title on stack.
2450 P s = 2 2 ; 1 → Save xterm icon title on stack.
2451 P s = 2 2 ; 2 → Save xterm window title on stack.
2452 P s = 2 3 ; 0 → Restore xterm icon and window title from stack.
2453 P s = 2 3 ; 1 → Restore xterm icon title from stack.
2454 P s = 2 3 ; 2 → Restore xterm window title from stack.
2455 
2456 			*/
2457 
2458 			if(esc[0] == ']' && esc.length > 1) {
2459 				int idx = -1;
2460 				foreach(i, e; esc)
2461 					if(e == ';') {
2462 						idx = cast(int) i;
2463 						break;
2464 					}
2465 				if(idx != -1) {
2466 					auto arg = cast(char[]) esc[idx + 1 .. $-1];
2467 					switch(cast(char[]) esc[1..idx]) {
2468 						case "0":
2469 							// icon name and window title
2470 							windowTitle = iconTitle = arg.idup;
2471 							changeWindowTitle(windowTitle);
2472 							changeIconTitle(iconTitle);
2473 						break;
2474 						case "1":
2475 							// icon name
2476 							iconTitle = arg.idup;
2477 							changeIconTitle(iconTitle);
2478 						break;
2479 						case "2":
2480 							// window title
2481 							windowTitle = arg.idup;
2482 							changeWindowTitle(windowTitle);
2483 						break;
2484 						case "10":
2485 							// change default text foreground color
2486 						break;
2487 						case "11":
2488 							// change gui background color
2489 						break;
2490 						case "12":
2491 							if(arg.length)
2492 								arg = arg[1 ..$]; // skip past the thing
2493 							if(arg.length) {
2494 								cursorColor = Color.fromString(arg);
2495 								foreach(ref p; cursorColor.components[0 .. 3])
2496 									p ^= 0xff;
2497 							} else
2498 								cursorColor = Color.white;
2499 						break;
2500 						case "50":
2501 							// change font
2502 						break;
2503 						case "52":
2504 							// copy/paste control
2505 							// echo -e "\033]52;p;?\007"
2506 							// the p == primary
2507 							// c == clipboard
2508 							// q == secondary 
2509 							// s == selection
2510 							// 0-7, cut buffers
2511 							// the data after it is either base64 stuff to copy or ? to request a paste
2512 
2513 							if(arg == "p;?") {
2514 								// i'm using this to request a paste. not quite compatible with xterm, but kinda
2515 								// because xterm tends not to answer anyway.
2516 								pasteFromPrimary(&sendPasteData);
2517 							} else if(arg.length > 2 && arg[0 .. 2] == "p;") {
2518 								auto info = arg[2 .. $];
2519 								try {
2520 									import std.base64;
2521 									auto data = Base64.decode(info);
2522 									copyToPrimary(cast(string) data);
2523 								} catch(Exception e)  {}
2524 							}
2525 
2526 							if(arg == "c;?") {
2527 								// i'm using this to request a paste. not quite compatible with xterm, but kinda
2528 								// because xterm tends not to answer anyway.
2529 								pasteFromClipboard(&sendPasteData);
2530 							} else if(arg.length > 2 && arg[0 .. 2] == "c;") {
2531 								auto info = arg[2 .. $];
2532 								try {
2533 									import std.base64;
2534 									auto data = Base64.decode(info);
2535 									copyToClipboard(cast(string) data);
2536 								} catch(Exception e)  {}
2537 							}
2538 
2539 							// selection
2540 							if(arg.length > 2 && arg[0 .. 2] == "s;") {
2541 								auto info = arg[2 .. $];
2542 								try {
2543 									import std.base64;
2544 									auto data = Base64.decode(info);
2545 									clearSelectionInternal();
2546 									overriddenSelection = cast(string) data;
2547 								} catch(Exception e)  {}
2548 							}
2549 						break;
2550 						case "4":
2551 							// palette change or query
2552 							        // set color #0 == black
2553 							// echo -e '\033]4;0;black\007'
2554 							/*
2555 								echo -e '\033]4;9;?\007' ; cat
2556 
2557 								^[]4;9;rgb:ffff/0000/0000^G
2558 							*/
2559 
2560 							// FIXME: if the palette changes, we should redraw so the change is immediately visible (as if we were using a real palette)
2561 						break;
2562 						case "104":
2563 							// palette reset
2564 							// reset color #0
2565 							// echo -e '\033[104;0\007'
2566 						break;
2567 						/* Extensions */
2568 						case "5000":
2569 							// change window icon (send a base64 encoded image or something)
2570 							/*
2571 								The format here is width and height as a single char each
2572 									'0'-'9' == 0-9
2573 									'a'-'z' == 10 - 36
2574 									anything else is invalid
2575 								
2576 								then a palette in hex rgba format (8 chars each), up to 26 entries
2577 
2578 								then a capital Z
2579 
2580 								if a palette entry == 'P', it means pull from the current palette (FIXME not implemented)
2581 
2582 								then 256 characters between a-z (must be lowercase!) which are the palette entries for
2583 								the pixels, top to bottom, left to right, so the image is 16x16. if it ends early, the
2584 								rest of the data is assumed to be zero
2585 
2586 								you can also do e.g. 22a, which means repeat a 22 times for some RLE.
2587 
2588 								anything out of range aborts the operation
2589 							*/
2590 							auto img = readSmallTextImage(arg);
2591 							windowIcon = img;
2592 							changeWindowIcon(img);
2593 						break;
2594 						case "5001":
2595 							// demand attention
2596 							attentionDemanded = true;
2597 							demandAttention();
2598 						break;
2599 						default:
2600 							unknownEscapeSequence("" ~ cast(char) esc[1]);
2601 					}
2602 				}
2603 			} else if(esc[0] == '[' && esc.length > 1) {
2604 				switch(esc[$-1]) {
2605 					case 'Z':
2606 						// CSI Ps Z  Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
2607 						// FIXME?
2608 					break;
2609 					case 'n':
2610 						switch(esc[$-2]) {
2611 							import std..string;
2612 							// request status report, reply OK
2613 							case '5': sendToApplication("\033[0n"); break;
2614 							// request cursor position
2615 							case '6': sendToApplication(format("\033[%d;%dR", cursorY + 1, cursorX + 1)); break;
2616 							default: unknownEscapeSequence(cast(string) esc);
2617 						}
2618 					break;
2619 					case 'A': if(cursorY) cursorY = cursorY - getArgs(1)[0]; break;
2620 					case 'B': if(cursorY != this.screenHeight - 1) cursorY = cursorY + getArgs(1)[0]; break;
2621 					case 'D': if(cursorX) cursorX = cursorX - getArgs(1)[0]; setTentativeScrollback(cursorX); break;
2622 					case 'C': if(cursorX != this.screenWidth - 1) cursorX = cursorX + getArgs(1)[0]; break;
2623 
2624 					case 'd': cursorY = getArgs(1)[0]-1; break;
2625 
2626 					case 'E': cursorY = cursorY + getArgs(1)[0]; cursorX = 0; break;
2627 					case 'F': cursorY = cursorY - getArgs(1)[0]; cursorX = 0; break;
2628 					case 'G': cursorX = getArgs(1)[0] - 1; break;
2629 					case 'H':
2630 						auto got = getArgs(1, 1);
2631 						cursorX = got[1] - 1;
2632 
2633 						if(got[0] - 1 == cursorY)
2634 							setTentativeScrollback(cursorX);
2635 						else
2636 							setTentativeScrollback(0);
2637 
2638 						cursorY = got[0] - 1;
2639 						newLineOnNext = false;
2640 					break;
2641 					case 'L':
2642 						// insert lines
2643 						scrollDown(getArgs(1)[0]);
2644 					break;
2645 					case 'M':
2646 						// delete lines
2647 						if(cursorY + 1 < screenHeight) {
2648 							TerminalCell plain;
2649 							plain.ch = ' ';
2650 							plain.attributes = defaultTextAttributes();
2651 							foreach(i; 0 .. getArgs(1)[0]) {
2652 								foreach(y; cursorY .. scrollZoneBottom)
2653 								foreach(x; 0 .. screenWidth) {
2654 									ASS[y][x] = ASS[y + 1][x];
2655 									ASS[y][x].invalidated = true;
2656 								}
2657 								foreach(x; 0 .. screenWidth) {
2658 									ASS[scrollZoneBottom][x] = plain;
2659 								}
2660 							}
2661 						}
2662 					break;
2663 					case 'K':
2664 						auto arg = getArgs(0)[0];
2665 						int start, end;
2666 						if(arg == 0) {
2667 							// clear from cursor to end of line
2668 							start = cursorX;
2669 							end = this.screenWidth;
2670 						} else if(arg == 1) {
2671 							// clear from cursor to beginning of line
2672 							start = 0;
2673 							end = cursorX + 1;
2674 						} else if(arg == 2) {
2675 							// clear entire line
2676 							start = 0;
2677 							end = this.screenWidth;
2678 						}
2679 
2680 						TerminalCell plain;
2681 						plain.ch = ' ';
2682 						plain.attributes = currentAttributes;
2683 
2684 						for(int i = start; i < end; i++) {
2685 							if(ASS[cursorY][i].selected)
2686 								clearSelection();
2687 							ASS[cursorY]
2688 								[i] = plain;
2689 						}
2690 					break;
2691 					case 's':
2692 						pushSavedCursor(cursorPosition);
2693 					break;
2694 					case 'u':
2695 						cursorPosition = popSavedCursor();
2696 					break;
2697 					case 'g':
2698 						auto arg = getArgs(0)[0];
2699 						TerminalCell plain;
2700 						plain.ch = ' ';
2701 						plain.attributes = currentAttributes;
2702 						if(arg == 0) {
2703 							// clear current column
2704 							for(int i = 0; i < this.screenHeight; i++)
2705 								ASS[i]
2706 									[cursorY] = plain;
2707 						} else if(arg == 3) {
2708 							// clear all
2709 							cls();
2710 						}
2711 					break;
2712 					case 'q':
2713 						// xterm also does blinks on the odd numbers (x-1)
2714 						if(esc == "[0 q")
2715 							cursorStyle = CursorStyle.block; // FIXME: restore default
2716 						if(esc == "[2 q")
2717 							cursorStyle = CursorStyle.block;
2718 						else if(esc == "[4 q")
2719 							cursorStyle = CursorStyle.underline;
2720 						else if(esc == "[6 q")
2721 							cursorStyle = CursorStyle.bar;
2722 
2723 						changeCursorStyle(cursorStyle);
2724 					break;
2725 					case 't':
2726 						// window commands
2727 						// i might support more of these but for now i just want the stack stuff.
2728 
2729 						auto args = getArgs(0, 0);
2730 						if(args[0] == 22) {
2731 							// save window title to stack
2732 							// xterm says args[1] should tell if it is the window title, the icon title, or both, but meh
2733 							titleStack ~= windowTitle;
2734 							iconStack ~= windowIcon;
2735 						} else if(args[0] == 23) {
2736 							// restore from stack
2737 							if(titleStack.length) {
2738 								windowTitle = titleStack[$ - 1];
2739 								changeWindowTitle(titleStack[$ - 1]);
2740 								titleStack = titleStack[0 .. $ - 1];
2741 							}
2742 
2743 							if(iconStack.length) {
2744 								windowIcon = iconStack[$ - 1];
2745 								changeWindowIcon(iconStack[$ - 1]);
2746 								iconStack = iconStack[0 .. $ - 1];
2747 							}
2748 						}
2749 					break;
2750 					case 'm':
2751 						// FIXME  used by xterm to decide whether to construct
2752 						// CSI > Pp ; Pv m CSI > Pp m Set/reset key modifier options, xterm.
2753 						if(esc[1] == '>')
2754 							goto default;
2755 						// done
2756 						argsLoop: foreach(argIdx, arg; getArgs(0))
2757 						switch(arg) {
2758 							case 0:
2759 							// normal
2760 								currentAttributes = defaultTextAttributes;
2761 							break;
2762 							case 1:
2763 								currentAttributes.bold = true;
2764 							break;
2765 							case 2:
2766 								currentAttributes.faint = true;
2767 							break;
2768 							case 3:
2769 								currentAttributes.italic = true;
2770 							break;
2771 							case 4:
2772 								currentAttributes.underlined = true;
2773 							break;
2774 							case 5:
2775 								currentAttributes.blink = true;
2776 							break;
2777 							case 6:
2778 								// rapid blink, treating the same as regular blink
2779 								currentAttributes.blink = true;
2780 							break;
2781 							case 7:
2782 								currentAttributes.inverse = true;
2783 							break;
2784 							case 8:
2785 								currentAttributes.invisible = true;
2786 							break;
2787 							case 9:
2788 								currentAttributes.strikeout = true;
2789 							break;
2790 							case 10:
2791 								// primary font
2792 							break;
2793 							case 11: .. case 19:
2794 								// alternate fonts
2795 							break;
2796 							case 20:
2797 								// Fraktur font
2798 							break;
2799 							case 21:
2800 								// bold off and doubled underlined
2801 							break;
2802 							case 22:
2803 								currentAttributes.bold = false;
2804 								currentAttributes.faint = false;
2805 							break;
2806 							case 23:
2807 								currentAttributes.italic = false;
2808 							break;
2809 							case 24:
2810 								currentAttributes.underlined = false;
2811 							break;
2812 							case 25:
2813 								currentAttributes.blink = false;
2814 							break;
2815 							case 26:
2816 								// reserved
2817 							break;
2818 							case 27:
2819 								currentAttributes.inverse = false;
2820 							break;
2821 							case 28:
2822 								currentAttributes.invisible = false;
2823 							break;
2824 							case 29:
2825 								currentAttributes.strikeout = false;
2826 							break;
2827 							case 30:
2828 							..
2829 							case 37:
2830 							// set foreground color
2831 								/*
2832 								Color nc;
2833 								ubyte multiplier = currentAttributes.bold ? 255 : 127;
2834 								nc.r = cast(ubyte)((arg - 30) & 1) * multiplier;
2835 								nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier;
2836 								nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier;
2837 								nc.a = 255;
2838 								*/
2839 								currentAttributes.foregroundIndex = cast(ubyte)(arg - 30);
2840 								version(with_24_bit_color)
2841 								currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)];
2842 							break;
2843 							case 38:
2844 								// xterm 256 color set foreground color
2845 								auto args = getArgs()[argIdx + 1 .. $];
2846 								if(args.length > 3 && args[0] == 2) {
2847 									// set color to closest match in palette. but since we have full support, we'll just take it directly
2848 									auto fg = Color(args[1], args[2], args[3]);
2849 									version(with_24_bit_color)
2850 										currentAttributes.foreground = fg;
2851 									// and try to find a low default palette entry for maximum compatibility
2852 									// 0x8000 == approximation
2853 									currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg);
2854 								} else if(args.length > 1 && args[0] == 5) {
2855 									// set to palette index
2856 									version(with_24_bit_color)
2857 										currentAttributes.foreground = palette[args[1]];
2858 									currentAttributes.foregroundIndex = cast(ushort) args[1];
2859 								}
2860 								break argsLoop;
2861 							case 39:
2862 							// default foreground color
2863 								auto dflt = defaultTextAttributes();
2864 
2865 								version(with_24_bit_color)
2866 									currentAttributes.foreground = dflt.foreground;
2867 								currentAttributes.foregroundIndex = dflt.foregroundIndex;
2868 							break;
2869 							case 40:
2870 							..
2871 							case 47:
2872 							// set background color
2873 								/*
2874 								Color nc;
2875 								nc.r = cast(ubyte)((arg - 40) & 1) * 255;
2876 								nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255;
2877 								nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255;
2878 								nc.a = 255;
2879 								*/
2880 
2881 								currentAttributes.backgroundIndex = cast(ubyte)(arg - 40);
2882 								//currentAttributes.background = nc;
2883 								version(with_24_bit_color)
2884 									currentAttributes.background = palette[arg-40];
2885 							break;
2886 							case 48:
2887 								// xterm 256 color set background color
2888 								auto args = getArgs()[argIdx + 1 .. $];
2889 								if(args.length > 3 && args[0] == 2) {
2890 									// set color to closest match in palette. but since we have full support, we'll just take it directly
2891 									auto bg = Color(args[1], args[2], args[3]);
2892 									version(with_24_bit_color)
2893 										currentAttributes.background = Color(args[1], args[2], args[3]);
2894 
2895 									// and try to find a low default palette entry for maximum compatibility
2896 									// 0x8000 == this is an approximation
2897 									currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg);
2898 								} else if(args.length > 1 && args[0] == 5) {
2899 									// set to palette index
2900 									version(with_24_bit_color)
2901 										currentAttributes.background = palette[args[1]];
2902 									currentAttributes.backgroundIndex = cast(ushort) args[1];
2903 								}
2904 
2905 								break argsLoop;
2906 							case 49:
2907 							// default background color
2908 								auto dflt = defaultTextAttributes();
2909 
2910 								version(with_24_bit_color)
2911 									currentAttributes.background = dflt.background;
2912 								currentAttributes.backgroundIndex = dflt.backgroundIndex;
2913 							break;
2914 							case 51:
2915 								// framed
2916 							break;
2917 							case 52:
2918 								// encircled
2919 							break;
2920 							case 53:
2921 								// overlined
2922 							break;
2923 							case 54:
2924 								// not framed or encircled
2925 							break;
2926 							case 55:
2927 								// not overlined
2928 							break;
2929 							case 90: .. case 97:
2930 								// high intensity foreground color
2931 							break;
2932 							case 100: .. case 107:
2933 								// high intensity background color
2934 							break;
2935 							default:
2936 								unknownEscapeSequence(cast(string) esc);
2937 						}
2938 					break;
2939 					case 'J':
2940 						// erase in display
2941 						auto arg = getArgs(0)[0];
2942 						switch(arg) {
2943 							case 0:
2944 								TerminalCell plain;
2945 								plain.ch = ' ';
2946 								plain.attributes = currentAttributes;
2947 								// erase below
2948 								foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) {
2949 									if(alternateScreenActive)
2950 										alternateScreen[i] = plain;
2951 									else
2952 										normalScreen[i] = plain;
2953 								}
2954 							break;
2955 							case 1:
2956 								// erase above
2957 								unknownEscapeSequence("FIXME");
2958 							break;
2959 							case 2:
2960 								// erase all
2961 								cls();
2962 							break;
2963 							default: unknownEscapeSequence(cast(string) esc);
2964 						}
2965 					break;
2966 					case 'r':
2967 						if(esc[1] != '?') {
2968 							// set scrolling zone
2969 							// default should be full size of window
2970 							auto args = getArgs(1, screenHeight);
2971 
2972 							// FIXME: these are supposed to be per-buffer
2973 							scrollZoneTop = args[0] - 1;
2974 							scrollZoneBottom = args[1] - 1;
2975 
2976 							if(scrollZoneTop < 0)
2977 								scrollZoneTop = 0;
2978 							if(scrollZoneBottom > screenHeight)
2979 								scrollZoneBottom = screenHeight - 1;
2980 						} else {
2981 							// restore... something FIXME
2982 						}
2983 					break;
2984 					case 'h':
2985 						if(esc[1] != '?')
2986 						foreach(arg; getArgs())
2987 						switch(arg) {
2988 							case 4:
2989 								insertMode = true;
2990 							break;
2991 							case 34:
2992 								// no idea. vim inside screen sends it
2993 							break;
2994 							default: unknownEscapeSequence(cast(string) esc);
2995 						}
2996 						else
2997 					//import std.stdio; writeln("h magic ", cast(string) esc);
2998 						foreach(arg; getArgsBase(2, null)) {
2999 							if(arg > 65535) {
3000 								/* Extensions */
3001 								if(arg < 65536 + 65535) {
3002 									// activate hyperlink
3003 									hyperlinkFlipper = !hyperlinkFlipper;
3004 									hyperlinkActive = true;
3005 									hyperlinkNumber = arg - 65536;
3006 								}
3007 							} else
3008 							switch(arg) {
3009 								case 1:
3010 									// application cursor keys
3011 									applicationCursorKeys = true;
3012 								break;
3013 								case 3:
3014 									// 132 column mode
3015 								break;
3016 								case 4:
3017 									// smooth scroll
3018 								break;
3019 								case 5:
3020 									// reverse video
3021 									reverseVideo = true;
3022 								break;
3023 								case 6:
3024 									// origin mode
3025 								break;
3026 								case 7:
3027 									// wraparound mode
3028 									wraparoundMode = false;
3029 									// FIXME: wraparoundMode i think is supposed to be off by default but then bash doesn't work right so idk, this gives the best results
3030 								break;
3031 								case 9:
3032 									allMouseTrackingOff();
3033 									mouseButtonTracking = true;
3034 								break;
3035 								case 12:
3036 									// start blinking cursor
3037 								break;
3038 								case 1034:
3039 									// meta keys????
3040 								break;
3041 								case 1049:
3042 									// Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first.
3043 									alternateScreenActive = true;
3044 									scrollLock = false;
3045 									pushSavedCursor(cursorPosition);
3046 									cls();
3047 									notifyScrollbarRelevant(false, false);
3048 								break;
3049 								case 1000:
3050 									// send mouse X&Y on button press and release
3051 									allMouseTrackingOff();
3052 									mouseButtonTracking = true;
3053 									mouseButtonReleaseTracking = true;
3054 								break;
3055 								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
3056 								break;
3057 								case 1002:
3058 									allMouseTrackingOff();
3059 									mouseButtonTracking = true;
3060 									mouseButtonReleaseTracking = true;
3061 									mouseButtonMotionTracking = true;
3062 									// use cell motion mouse tracking
3063 								break;
3064 								case 1003:
3065 									// ALL motion is sent
3066 									allMouseTrackingOff();
3067 									mouseButtonTracking = true;
3068 									mouseButtonReleaseTracking = true;
3069 									mouseMotionTracking = true;
3070 								break;
3071 								case 1004:
3072 									sendFocusEvents = true;
3073 								break;
3074 								case 1005:
3075 									// enable utf-8 mouse mode
3076 									/*
3077 UTF-8 (1005)
3078           This enables UTF-8 encoding for Cx and Cy under all tracking
3079           modes, expanding the maximum encodable position from 223 to
3080           2015.  For positions less than 95, the resulting output is
3081           identical under both modes.  Under extended mouse mode, posi-
3082           tions greater than 95 generate "extra" bytes which will con-
3083           fuse applications which do not treat their input as a UTF-8
3084           stream.  Likewise, Cb will be UTF-8 encoded, to reduce confu-
3085           sion with wheel mouse events.
3086           Under normal mouse mode, positions outside (160,94) result in
3087           byte pairs which can be interpreted as a single UTF-8 charac-
3088           ter; applications which do treat their input as UTF-8 will
3089           almost certainly be confused unless extended mouse mode is
3090           active.
3091           This scheme has the drawback that the encoded coordinates will
3092           not pass through luit unchanged, e.g., for locales using non-
3093           UTF-8 encoding.
3094 									*/
3095 								break;
3096 								case 1006:
3097 								/*
3098 SGR (1006)
3099           The normal mouse response is altered to use CSI < followed by
3100           semicolon-separated encoded button value, the Cx and Cy ordi-
3101           nates and a final character which is M  for button press and m
3102           for button release.
3103           o The encoded button value in this case does not add 32 since
3104             that was useful only in the X10 scheme for ensuring that the
3105             byte containing the button value is a printable code.
3106           o The modifiers are encoded in the same way.
3107           o A different final character is used for button release to
3108             resolve the X10 ambiguity regarding which button was
3109             released.
3110           The highlight tracking responses are also modified to an SGR-
3111           like format, using the same SGR-style scheme and button-encod-
3112           ings.
3113 								*/
3114 								break;
3115 								case 1014:
3116 									// ARSD extension: it is 1002 but selective, only
3117 									// on top row, row with cursor, or else if middle click/wheel.
3118 									//
3119 									// Quite specifically made for my getline function!
3120 									allMouseTrackingOff();
3121 
3122 									mouseButtonMotionTracking = true;
3123 									mouseButtonTracking = true;
3124 									mouseButtonReleaseTracking = true;
3125 									selectiveMouseTracking = true;
3126 								break;
3127 								case 1015:
3128 								/*
3129 URXVT (1015)
3130           The normal mouse response is altered to use CSI followed by
3131           semicolon-separated encoded button value, the Cx and Cy ordi-
3132           nates and final character M .
3133           This uses the same button encoding as X10, but printing it as
3134           a decimal integer rather than as a single byte.
3135           However, CSI M  can be mistaken for DL (delete lines), while
3136           the highlight tracking CSI T  can be mistaken for SD (scroll
3137           down), and the Window manipulation controls.  For these rea-
3138           sons, the 1015 control is not recommended; it is not an
3139           improvement over 1005.
3140 								*/
3141 								break;
3142 								case 1048:
3143 									pushSavedCursor(cursorPosition);
3144 								break;
3145 								case 2004:
3146 									bracketedPasteMode = true;
3147 								break;
3148 								case 3004:
3149 									bracketedHyperlinkMode = true;
3150 								break;
3151 								case 1047:
3152 								case 47:
3153 									alternateScreenActive = true;
3154 									scrollLock = false;
3155 									cls();
3156 									notifyScrollbarRelevant(false, false);
3157 								break;
3158 								case 25:
3159 									cursorShowing = true;
3160 								break;
3161 
3162 								/* Done */
3163 								default: unknownEscapeSequence(cast(string) esc);
3164 							}
3165 						}
3166 					break;
3167 					case 'p':
3168 						// it is asking a question... and tbh i don't care.
3169 					break;
3170 					case 'l':
3171 					//import std.stdio; writeln("l magic ", cast(string) esc);
3172 						if(esc[1] != '?')
3173 						foreach(arg; getArgs())
3174 						switch(arg) {
3175 							case 4:
3176 								insertMode = false;
3177 							break;
3178 							case 34:
3179 								// no idea. vim inside screen sends it
3180 							break;
3181 							case 1004:
3182 								sendFocusEvents = false;
3183 							break;
3184 							case 1005:
3185 								// turn off utf-8 mouse
3186 							break;
3187 							case 1006:
3188 								// turn off sgr mouse
3189 							break;
3190 							case 1015:
3191 								// turn off urxvt mouse
3192 							break;
3193 							default: unknownEscapeSequence(cast(string) esc);
3194 						}
3195 						else
3196 						foreach(arg; getArgsBase(2, null)) {
3197 							if(arg > 65535) {
3198 								/* Extensions */
3199 								if(arg < 65536 + 65535)
3200 									hyperlinkActive = false;
3201 							}
3202 							switch(arg) {
3203 								case 1:
3204 									// normal cursor keys
3205 									applicationCursorKeys = false;
3206 								break;
3207 								case 3:
3208 									// 80 column mode
3209 								break;
3210 								case 4:
3211 									// smooth scroll
3212 								break;
3213 								case 5:
3214 									// normal video
3215 									reverseVideo = false;
3216 								break;
3217 								case 6:
3218 									// normal cursor mode
3219 								break;
3220 								case 7:
3221 									// wraparound mode
3222 									wraparoundMode = true;
3223 								break;
3224 								case 12:
3225 									// stop blinking cursor
3226 								break;
3227 								case 1034:
3228 									// meta keys????
3229 								break;
3230 								case 1049:
3231 									cursorPosition = popSavedCursor;
3232 									wraparoundMode = true;
3233 
3234 									returnToNormalScreen();
3235 								break;
3236 								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
3237 								break;
3238 								case 9:
3239 								case 1000:
3240 								case 1002:
3241 								case 1003:
3242 								case 1014: // arsd extension
3243 									allMouseTrackingOff();
3244 								break;
3245 								case 1005:
3246 								case 1006:
3247 									// idk
3248 								break;
3249 								case 1048:
3250 									cursorPosition = popSavedCursor;
3251 								break;
3252 								case 2004:
3253 									bracketedPasteMode = false;
3254 								break;
3255 								case 3004:
3256 									bracketedHyperlinkMode = false;
3257 								break;
3258 								case 1047:
3259 								case 47:
3260 									returnToNormalScreen();
3261 								break;
3262 								case 25:
3263 									cursorShowing = false;
3264 								break;
3265 								default: unknownEscapeSequence(cast(string) esc);
3266 							}
3267 						}
3268 					break;
3269 					case 'X':
3270 						// erase characters
3271 						auto count = getArgs(1)[0];
3272 						TerminalCell plain;
3273 						plain.ch = ' ';
3274 						plain.attributes = currentAttributes;
3275 						foreach(cnt; 0 .. count) {
3276 							ASS[cursorY][cnt + cursorX] = plain;
3277 						}
3278 					break;
3279 					case 'S':
3280 						auto count = getArgs(1)[0];
3281 						// scroll up
3282 						scrollUp(count);
3283 					break;
3284 					case 'T':
3285 						auto count = getArgs(1)[0];
3286 						// scroll down
3287 						scrollDown(count);
3288 					break;
3289 					case 'P':
3290 						auto count = getArgs(1)[0];
3291 						// delete characters
3292 
3293 						foreach(cnt; 0 .. count) {
3294 							for(int i = cursorX; i < this.screenWidth-1; i++) {
3295 								if(ASS[cursorY][i].selected)
3296 									clearSelection();
3297 								ASS[cursorY][i] = ASS[cursorY][i + 1];
3298 								ASS[cursorY][i].invalidated = true;
3299 							}
3300 
3301 							if(ASS[cursorY][this.screenWidth - 1].selected)
3302 								clearSelection();
3303 							ASS[cursorY][this.screenWidth-1].ch = ' ';
3304 							ASS[cursorY][this.screenWidth-1].invalidated = true;
3305 						}
3306 					break;
3307 					case '@':
3308 						// insert blank characters
3309 						auto count = getArgs(1)[0];
3310 						foreach(idx; 0 .. count) {
3311 							for(int i = this.screenWidth - 1; i > cursorX; i--) {
3312 								ASS[cursorY][i] = ASS[cursorY][i - 1];
3313 								ASS[cursorY][i].invalidated = true;
3314 							}
3315 							ASS[cursorY][cursorX].ch = ' ';
3316 							ASS[cursorY][cursorX].invalidated = true;
3317 						}
3318 					break;
3319 					case 'c':
3320 						// send device attributes
3321 						// FIXME: what am i supposed to do here?
3322 						//sendToApplication("\033[>0;138;0c");
3323 						//sendToApplication("\033[?62;");
3324 						sendToApplication(terminalIdCode);
3325 					break;
3326 					default:
3327 						// [42\esc] seems to have gotten here once somehow
3328 						// also [24\esc]
3329 						unknownEscapeSequence("" ~ cast(string) esc);
3330 				}
3331 			} else {
3332 				unknownEscapeSequence(cast(string) esc);
3333 			}
3334 		}
3335 	}
3336 }
3337 
3338 // These match the numbers in terminal.d, so you can just cast it back and forth
3339 // and the names match simpledisplay.d so you can convert that automatically too
3340 enum TerminalKey : int {
3341 	Escape = 0x1b + 0xF0000, /// .
3342 	F1 = 0x70 + 0xF0000, /// .
3343 	F2 = 0x71 + 0xF0000, /// .
3344 	F3 = 0x72 + 0xF0000, /// .
3345 	F4 = 0x73 + 0xF0000, /// .
3346 	F5 = 0x74 + 0xF0000, /// .
3347 	F6 = 0x75 + 0xF0000, /// .
3348 	F7 = 0x76 + 0xF0000, /// .
3349 	F8 = 0x77 + 0xF0000, /// .
3350 	F9 = 0x78 + 0xF0000, /// .
3351 	F10 = 0x79 + 0xF0000, /// .
3352 	F11 = 0x7A + 0xF0000, /// .
3353 	F12 = 0x7B + 0xF0000, /// .
3354 	Left = 0x25 + 0xF0000, /// .
3355 	Right = 0x27 + 0xF0000, /// .
3356 	Up = 0x26 + 0xF0000, /// .
3357 	Down = 0x28 + 0xF0000, /// .
3358 	Insert = 0x2d + 0xF0000, /// .
3359 	Delete = 0x2e + 0xF0000, /// .
3360 	Home = 0x24 + 0xF0000, /// .
3361 	End = 0x23 + 0xF0000, /// .
3362 	PageUp = 0x21 + 0xF0000, /// .
3363 	PageDown = 0x22 + 0xF0000, /// .
3364 	ScrollLock = 0x91 + 0xF0000, 
3365 }
3366 
3367 /* These match simpledisplay.d which match terminal.d, so you can just cast them */
3368 
3369 enum MouseEventType : int {
3370 	motion = 0,
3371 	buttonPressed = 1,
3372 	buttonReleased = 2,
3373 }
3374 
3375 enum MouseButton : int {
3376 	// these names assume a right-handed mouse
3377 	left = 1,
3378 	right = 2,
3379 	middle = 4,
3380 	wheelUp = 8,
3381 	wheelDown = 16,
3382 }
3383 
3384 
3385 
3386 /*
3387 mixin template ImageSupport() {
3388 	import arsd.png;
3389 	import arsd.bmp;
3390 }
3391 */
3392 
3393 
3394 /* helper functions that are generally useful but not necessarily required */
3395 
3396 version(use_libssh2) {
3397 	import arsd.libssh2;
3398 	void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) {
3399 
3400 	int tries = 0;
3401 	try_again:
3402 	try {
3403 		import std.socket;
3404 
3405 		if(libssh2_init(0))
3406 			throw new Exception("libssh2_init");
3407 		scope(exit)
3408 			libssh2_exit();
3409 
3410 		auto socket = new Socket(AddressFamily.INET, SocketType.STREAM);
3411 		socket.connect(new InternetAddress(host, port));
3412 		scope(exit) socket.close();
3413 
3414 		auto session = libssh2_session_init_ex(null, null, null, null);
3415 		if(session is null) throw new Exception("init session");
3416 		scope(exit)
3417 			libssh2_session_disconnect_ex(session, 0, "normal", "EN");
3418 
3419 		libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1);
3420 
3421 		if(libssh2_session_handshake(session, socket.handle))
3422 			throw new Exception("handshake");
3423 
3424 		auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
3425 		if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint)
3426 			throw new Exception("fingerprint");
3427 
3428 		import std..string : toStringz;
3429 		if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
3430 			throw new Exception("auth");
3431 
3432 
3433 		auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0);
3434 
3435 		if(channel is null)
3436 			throw new Exception("channel open");
3437 
3438 		scope(exit)
3439 			libssh2_channel_free(channel);
3440 
3441 		// libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length);
3442 
3443 		if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0))
3444 			throw new Exception("pty");
3445 
3446 		if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0))
3447 			throw new Exception("process_startup");
3448 
3449 		libssh2_keepalive_config(session, 0, 60);
3450 		libssh2_session_set_blocking(session, 0);
3451 
3452 		masterFunc(socket, session, channel);
3453 	} catch(Exception e) {
3454 		if(e.msg == "handshake") {
3455 			tries++;
3456 			import core.thread;
3457 			Thread.sleep(200.msecs);
3458 			if(tries < 10)
3459 				goto try_again;
3460 		}
3461 
3462 		throw e;
3463 	}
3464 	}
3465 
3466 } else
3467 version(Posix) {
3468 	extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp);
3469 	pragma(lib, "util");
3470 
3471 	/// this is good
3472 	void startChild(alias masterFunc)(string program, string[] args) {
3473 		import core.sys.posix.termios;
3474 		import core.sys.posix.signal;
3475 		import core.sys.posix.sys.wait;
3476 		__gshared static int childrenAlive = 0;
3477 		extern(C) nothrow static @nogc
3478 		void childdead(int) {
3479 			childrenAlive--;
3480 
3481 			wait(null);
3482 
3483 			version(with_eventloop)
3484 			try {
3485 				import arsd.eventloop;
3486 				if(childrenAlive <= 0)
3487 					exit();
3488 			} catch(Exception e){}
3489 		}
3490 
3491 		signal(SIGCHLD, &childdead);
3492 
3493 		int master;
3494 		int pid = forkpty(&master, null, null, null);
3495 		if(pid == -1)
3496 			throw new Exception("forkpty");
3497 		if(pid == 0) {
3498 			import std.process;
3499 			environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes
3500 			environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions
3501 
3502 			import std..string;
3503 			if(environment["LANG"].indexOf("UTF-8") == -1)
3504 				environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?)
3505 
3506 			import core.sys.posix.unistd;
3507 
3508 			import core.stdc.stdlib;
3509 			char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1));
3510 			if(argv is null) throw new Exception("malloc");
3511 			foreach(i, arg; args) {
3512 				argv[i] = cast(char*) malloc(arg.length + 1);
3513 				if(argv[i] is null) throw new Exception("malloc");
3514 				argv[i][0 .. arg.length] = arg[];
3515 				argv[i][arg.length] = 0;
3516 			}
3517 
3518 			argv[args.length] = null;
3519 
3520 			core.sys.posix.unistd.execv(argv[0], argv);
3521 		} else {
3522 			childrenAlive = 1;
3523 			masterFunc(master);
3524 		}
3525 	}
3526 } else
3527 version(Windows) {
3528 	import core.sys.windows.windows;
3529 
3530 	version(winpty) {
3531 		alias HPCON = HANDLE;
3532 		extern(Windows)
3533 			HRESULT function(HPCON, COORD) ResizePseudoConsole;
3534 		extern(Windows)
3535 			HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole;
3536 		extern(Windows)
3537 			void function(HPCON) ClosePseudoConsole;
3538 	}
3539 
3540 	extern(Windows)
3541 		BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD);
3542 	extern(Windows)
3543 		BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL);
3544 	extern(Windows)
3545 		private BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*);
3546 	extern(Windows)
3547 		BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
3548 
3549 	extern(Windows)
3550 		BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM);
3551 	extern(Windows)
3552 		BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags);
3553 	extern(Windows)
3554 		BOOL SetHandleInformation(HANDLE, DWORD, DWORD);
3555 	extern(Windows)
3556 	HANDLE CreateNamedPipeA(
3557 		const(char)* lpName,
3558 		DWORD dwOpenMode,
3559 		DWORD dwPipeMode,
3560 		DWORD nMaxInstances,
3561 		DWORD nOutBufferSize,
3562 		DWORD nInBufferSize,
3563 		DWORD nDefaultTimeOut,
3564 		LPSECURITY_ATTRIBUTES lpSecurityAttributes
3565 	);
3566 	extern(Windows)
3567 	BOOL UnregisterWait(HANDLE);
3568 
3569 	struct STARTUPINFOEXA {
3570 		STARTUPINFOA StartupInfo;
3571 		void* lpAttributeList;
3572 	}
3573 
3574 	enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
3575 	enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
3576 
3577 	extern(Windows)
3578 	BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T);
3579 	extern(Windows)
3580 	BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T);
3581 
3582 	__gshared HANDLE waitHandle;
3583 	__gshared bool childDead;
3584 	extern(Windows)
3585 	void childCallback(void* tidp, bool) {
3586 		auto tid = cast(DWORD) tidp;
3587 		UnregisterWait(waitHandle);
3588 
3589 		PostThreadMessageA(tid, WM_QUIT, 0, 0);
3590 		childDead = true;
3591 		//stupidThreadAlive = false;
3592 	}
3593 
3594 
3595 
3596 	extern(Windows)
3597 	void SetLastError(DWORD);
3598 
3599 	/// this is good. best to call it with plink.exe so it can talk to unix
3600 	/// note that plink asks for the password out of band, so it won't actually work like that.
3601 	/// thus specify the password on the command line or better yet, use a private key file
3602 	/// e.g.
3603 	/// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\"");
3604 	void startChild(alias masterFunc)(string program, string commandLine) {
3605 		import core.sys.windows.windows;
3606 		// thanks for a random person on stack overflow for this function
3607 		static BOOL MyCreatePipeEx(
3608 			PHANDLE lpReadPipe,
3609 			PHANDLE lpWritePipe,
3610 			LPSECURITY_ATTRIBUTES lpPipeAttributes,
3611 			DWORD nSize,
3612 			DWORD dwReadMode,
3613 			DWORD dwWriteMode
3614 		)
3615 		{
3616 			HANDLE ReadPipeHandle, WritePipeHandle;
3617 			DWORD dwError;
3618 			CHAR[MAX_PATH] PipeNameBuffer;
3619 
3620 			if (nSize == 0) {
3621 				nSize = 4096;
3622 			}
3623 
3624 			static int PipeSerialNumber = 0;
3625 
3626 			import core.stdc..string;
3627 			import core.stdc.stdio;
3628 
3629 			sprintf(PipeNameBuffer.ptr,
3630 				"\\\\.\\Pipe\\TerminalEmulatorPipe.%08x.%08x".ptr,
3631 				GetCurrentProcessId(),
3632 				PipeSerialNumber++
3633 			);
3634 
3635 			ReadPipeHandle = CreateNamedPipeA(
3636 				PipeNameBuffer.ptr,
3637 				1/*PIPE_ACCESS_INBOUND*/ | dwReadMode,
3638 				0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/,
3639 				1,             // Number of pipes
3640 				nSize,         // Out buffer size
3641 				nSize,         // In buffer size
3642 				120 * 1000,    // Timeout in ms
3643 				lpPipeAttributes
3644 			);
3645 
3646 			if (! ReadPipeHandle) {
3647 				return FALSE;
3648 			}
3649 
3650 			WritePipeHandle = CreateFileA(
3651 				PipeNameBuffer.ptr,
3652 				GENERIC_WRITE,
3653 				0,                         // No sharing
3654 				lpPipeAttributes,
3655 				OPEN_EXISTING,
3656 				FILE_ATTRIBUTE_NORMAL | dwWriteMode,
3657 				null                       // Template file
3658 			);
3659 
3660 			if (INVALID_HANDLE_VALUE == WritePipeHandle) {
3661 				dwError = GetLastError();
3662 				CloseHandle( ReadPipeHandle );
3663 				SetLastError(dwError);
3664 				return FALSE;
3665 			}
3666 
3667 			*lpReadPipe = ReadPipeHandle;
3668 			*lpWritePipe = WritePipeHandle;
3669 			return( TRUE );
3670 		}
3671 
3672 
3673 
3674 
3675 
3676 		import std.conv;
3677 
3678 		SECURITY_ATTRIBUTES saAttr;
3679 		saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
3680 		saAttr.bInheritHandle = true;
3681 		saAttr.lpSecurityDescriptor = null;
3682 
3683 		HANDLE inreadPipe;
3684 		HANDLE inwritePipe;
3685 		if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0)
3686 			throw new Exception("CreatePipe");
3687 		if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
3688 			throw new Exception("SetHandleInformation");
3689 		HANDLE outreadPipe;
3690 		HANDLE outwritePipe;
3691 
3692 		version(winpty)
3693 			auto flags = 0;
3694 		else
3695 			auto flags = FILE_FLAG_OVERLAPPED;
3696 
3697 		if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0)
3698 			throw new Exception("CreatePipe");
3699 		if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
3700 			throw new Exception("SetHandleInformation");
3701 
3702 		version(winpty) {
3703 
3704 			auto lib = LoadLibrary("kernel32.dll");
3705 			if(lib is null) throw new Exception("holy wtf batman");
3706 			scope(exit) FreeLibrary(lib);
3707 
3708 			CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole");
3709 			ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole");
3710 			ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole");
3711 
3712 			if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null)
3713 				throw new Exception("Windows pseudo console not available on this version");
3714 
3715 			initPipeHack(outreadPipe);
3716 
3717 			HPCON hpc;
3718 			auto result = CreatePseudoConsole(
3719 				COORD(80, 24),
3720 				inreadPipe,
3721 				outwritePipe,
3722 				0, // flags
3723 				&hpc
3724 			);
3725 
3726 			assert(result == S_OK);
3727 
3728 			scope(exit)
3729 				ClosePseudoConsole(hpc);
3730 		}
3731 
3732 		STARTUPINFOEXA siex;
3733 		siex.StartupInfo.cb = siex.sizeof;
3734 
3735 		version(winpty) {
3736 			size_t size;
3737 			InitializeProcThreadAttributeList(null, 1, 0, &size);
3738 			ubyte[] wtf = new ubyte[](size);
3739 			siex.lpAttributeList = wtf.ptr;
3740 			InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size);
3741 			UpdateProcThreadAttribute(
3742 				siex.lpAttributeList,
3743 				0,
3744 				PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
3745 				hpc,
3746 				hpc.sizeof,
3747 				null,
3748 				null
3749 			);
3750 		} {//else {
3751 			siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
3752 			siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe;
3753 			siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe;
3754 			siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe;
3755 		}
3756 
3757 		PROCESS_INFORMATION pi;
3758 		import std.conv;
3759 
3760 		if(commandLine.length > 255)
3761 			throw new Exception("command line too long");
3762 		char[256] cmdLine;
3763 		cmdLine[0 .. commandLine.length] = commandLine[];
3764 		cmdLine[commandLine.length] = 0;
3765 		import std..string;
3766 		if(CreateProcessA(program is null ? null : toStringz(program), cmdLine.ptr, null, null, true, EXTENDED_STARTUPINFO_PRESENT /*0x08000000 /* CREATE_NO_WINDOW */, null /* environment */, null, cast(STARTUPINFOA*) &siex, &pi) == 0)
3767 			throw new Exception("CreateProcess " ~ to!string(GetLastError()));
3768 
3769 		if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0)
3770 			throw new Exception("RegisterWaitForSingleObject");
3771 
3772 		version(winpty)
3773 			masterFunc(hpc, inwritePipe, outreadPipe);
3774 		else
3775 			masterFunc(inwritePipe, outreadPipe);
3776 
3777 		//stupidThreadAlive = false;
3778 
3779 		//term.stupidThread.join();
3780 
3781 		/* // FIXME: we should close but only if we're legit done
3782 		// masterFunc typically runs an event loop but it might not.
3783 		CloseHandle(inwritePipe);
3784 		CloseHandle(outreadPipe);
3785 
3786 		CloseHandle(pi.hThread);
3787 		CloseHandle(pi.hProcess);
3788 		*/
3789 	}
3790 }
3791 
3792 /// Implementation of TerminalEmulator's abstract functions that forward them to output
3793 mixin template ForwardVirtuals(alias writer) {
3794 	static import arsd.color;
3795 
3796 	protected override void changeCursorStyle(CursorStyle style) {
3797 		// FIXME: this should probably just import utility
3798 		final switch(style) {
3799 			case TerminalEmulator.CursorStyle.block:
3800 				writer("\033[2 q");
3801 			break;
3802 			case TerminalEmulator.CursorStyle.underline:
3803 				writer("\033[4 q");
3804 			break;
3805 			case TerminalEmulator.CursorStyle.bar:
3806 				writer("\033[6 q");
3807 			break;
3808 		}
3809 	}
3810 
3811 	protected override void changeWindowTitle(string t) {
3812 		import std.process;
3813 		if(t.length && environment["TERM"] != "linux")
3814 			writer("\033]0;"~t~"\007");
3815 	}
3816 
3817 	protected override void changeWindowIcon(arsd.color.IndexedImage t) {
3818 		if(t !is null) {
3819 			// forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux
3820 			import std.process;
3821 			if(environment["TERM"] != "linux")
3822 				writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007");
3823 		}
3824 	}
3825 
3826 	protected override void changeIconTitle(string) {} // FIXME
3827 	protected override void changeTextAttributes(TextAttributes) {} // FIXME
3828 	protected override void soundBell() {
3829 		writer("\007");
3830 	}
3831 	protected override void demandAttention() {
3832 		import std.process;
3833 		if(environment["TERM"] != "linux")
3834 			writer("\033]5001;1\007"); // the 1 there means true but is currently ignored
3835 	}
3836 	protected override void copyToClipboard(string text) {
3837 		// this is xterm compatible, though xterm rarely implements it
3838 		import std.base64;
3839 				// idk why the cast is needed here
3840 		writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007");
3841 	}
3842 	protected override void pasteFromClipboard(void delegate(in char[]) dg) {
3843 		// this is a slight extension. xterm invented the string - it means request the primary selection -
3844 		// but it generally doesn't actually get a reply. so i'm using it to request the primary which will be
3845 		// sent as a pasted strong.
3846 		// (xterm prolly doesn't do it by default because it is potentially insecure, letting a naughty app steal your clipboard data, but meh, any X application can do that too and it is useful here for nesting.)
3847 		writer("\033]52;c;?\007");
3848 	}
3849 	protected override void copyToPrimary(string text) {
3850 		import std.base64;
3851 		writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007");
3852 	}
3853 	protected override void pasteFromPrimary(void delegate(in char[]) dg) {
3854 		writer("\033]52;p;?\007");
3855 	}
3856 
3857 }
3858 
3859 /// you can pass this as PtySupport's arguments when you just don't care
3860 final void doNothing() {}
3861 
3862 version(winpty) {
3863 		__gshared static HANDLE inputEvent;
3864 		__gshared static HANDLE magicEvent;
3865 		__gshared static ubyte[] helperBuffer;
3866 		__gshared static HANDLE helperThread;
3867 
3868 		static void initPipeHack(void* ptr) {
3869 			inputEvent = CreateEvent(null, false, false, null);
3870 			assert(inputEvent !is null);
3871 			magicEvent = CreateEvent(null, false, true, null);
3872 			assert(magicEvent !is null);
3873 
3874 			helperThread = CreateThread(
3875 				null,
3876 				0,
3877 				&actuallyRead,
3878 				ptr,
3879 				0,
3880 				null
3881 			);
3882 
3883 			assert(helperThread !is null);
3884 		}
3885 
3886 		extern(Windows) static
3887 		uint actuallyRead(void* ptr) {
3888 			ubyte[4096] buffer;
3889 			DWORD got;
3890 			while(true) {
3891 				// wait for the other thread to tell us they
3892 				// are done...
3893 				WaitForSingleObject(magicEvent, INFINITE);
3894 				auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null);
3895 				helperBuffer = buffer[0 .. got];
3896 				// tells the other thread it is allowed to read
3897 				// readyToReadPty
3898 				SetEvent(inputEvent);
3899 			}
3900 			assert(0);
3901 		}
3902 
3903 
3904 }
3905 
3906 /// You must implement a function called redraw() and initialize the members in your constructor
3907 mixin template PtySupport(alias resizeHelper) {
3908 	// Initialize these!
3909 
3910 	final void redraw_() {
3911 		if(invalidateAll) {
3912 			if(alternateScreenActive)
3913 				foreach(ref t; alternateScreen)
3914 					t.invalidated = true;
3915 			else
3916 				foreach(ref t; normalScreen)
3917 					t.invalidated = true;
3918 			invalidateAll = false;
3919 		}
3920 		redraw();
3921 		//soundBell();
3922 	}
3923 
3924 	version(use_libssh2) {
3925 		import arsd.libssh2;
3926 		LIBSSH2_CHANNEL* sshChannel;
3927 	} else version(Windows) {
3928 		import core.sys.windows.windows;
3929 		HANDLE stdin;
3930 		HANDLE stdout;
3931 	} else version(Posix) {
3932 		int master;
3933 	}
3934 
3935 	version(use_libssh2) { }
3936 	else version(Posix) {
3937 		int previousProcess = 0;
3938 		int activeProcess = 0;
3939 		int activeProcessWhenResized = 0;
3940 		bool resizedRecently;
3941 
3942 		/*
3943 			so, this isn't perfect, but it is meant to send the resize signal to an existing process
3944 			when it isn't in the front when you resize.
3945 
3946 			For example, open vim and resize. Then exit vim. We want bash to be updated.
3947 
3948 			But also don't want to do too many spurious signals.
3949 
3950 			It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but
3951 			the outer bash won't see it. I guess I need some kind of process stack.
3952 
3953 			but it is okish.
3954 		*/
3955 		override void outputOccurred() {
3956 			import core.sys.posix.unistd;
3957 			auto pgrp = tcgetpgrp(master);
3958 			if(pgrp != -1) {
3959 				if(pgrp != activeProcess) {
3960 					auto previousProcessAtStartup = previousProcess;
3961 
3962 					previousProcess = activeProcess;
3963 					activeProcess = pgrp;
3964 
3965 					if(resizedRecently) {
3966 						if(activeProcess != activeProcessWhenResized) {
3967 							resizedRecently = false;
3968 
3969 							if(activeProcess == previousProcessAtStartup) {
3970 								//import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight);
3971 
3972 								import core.sys.posix.signal;
3973 								kill(-activeProcess, 28 /* 28 == SIGWINCH*/);
3974 							}
3975 						}
3976 					}
3977 				}
3978 			}
3979 
3980 
3981 			super.outputOccurred();
3982 		}
3983 		//return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline");
3984 	}
3985 
3986 
3987 	override void resizeTerminal(int w, int h) {
3988 		version(Posix) {
3989 			activeProcessWhenResized = activeProcess;
3990 			resizedRecently = true;
3991 		}
3992 
3993 		resizeHelper();
3994 
3995 		super.resizeTerminal(w, h);
3996 
3997 		version(use_libssh2) {
3998 			libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0);
3999 		} else version(Posix) {
4000 			import core.sys.posix.sys.ioctl;
4001 			winsize win;
4002 			win.ws_col = cast(ushort) w;
4003 			win.ws_row = cast(ushort) h;
4004 
4005 			ioctl(master, TIOCSWINSZ, &win);
4006 		} else version(Windows) {
4007 			version(winpty) {
4008 				COORD coord;
4009 				coord.X = cast(ushort) w;
4010 				coord.Y = cast(ushort) h;
4011 				ResizePseudoConsole(hpc, coord);
4012 			} else {
4013 				sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]);
4014 			}
4015 		} else static assert(0);
4016 	}
4017 
4018 	protected override void sendToApplication(scope const(void)[] data) {
4019 		version(use_libssh2) {
4020 			while(data.length) {
4021 				auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length);
4022 				if(sent < 0)
4023 					throw new Exception("libssh2_channel_write_ex");
4024 				data = data[sent .. $];
4025 			}
4026 		} else version(Windows) {
4027 			import std.conv;
4028 			uint written;
4029 			if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0)
4030 				throw new Exception("WriteFile " ~ to!string(GetLastError()));
4031 		} else version(Posix) {
4032 			import core.sys.posix.unistd;
4033 			int frozen;
4034 			while(data.length) {
4035 				enum MAX_SEND = 1024 * 20;
4036 				auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length);
4037 				//import std.stdio; writeln("ROFL ", sent, " ", data.length);
4038 
4039 				import core.stdc.errno;
4040 				if(sent == -1 && errno == 11) {
4041 					import core.thread;
4042 					if(frozen == 50)
4043 						throw new Exception("write froze up");
4044 					frozen++;
4045 					Thread.sleep(10.msecs);
4046 					//import std.stdio; writeln("lol");
4047 					continue; // just try again
4048 				}
4049 
4050 				frozen = 0;
4051 
4052 				import std.conv;
4053 				if(sent < 0)
4054 					throw new Exception("write " ~ to!string(errno));
4055 
4056 				data = data[sent .. $];
4057 			}
4058 		} else static assert(0);
4059 	}
4060 
4061 	version(use_libssh2) {
4062 		int readyToRead(int fd) {
4063 			int count = 0; // if too much stuff comes at once, we still want to be responsive
4064 			while(true) {
4065 				ubyte[4096] buffer;
4066 				auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length);
4067 				if(got == LIBSSH2_ERROR_EAGAIN)
4068 					break; // got it all for now
4069 				if(got < 0)
4070 					throw new Exception("libssh2_channel_read_ex");
4071 				if(got == 0)
4072 					break; // NOT an error!
4073 
4074 				super.sendRawInput(buffer[0 .. got]);
4075 				count++;
4076 
4077 				if(count == 5) {
4078 					count = 0;
4079 					redraw_();
4080 					justRead();
4081 				}
4082 			}
4083 
4084 			if(libssh2_channel_eof(sshChannel)) {
4085 				libssh2_channel_close(sshChannel);
4086 				libssh2_channel_wait_closed(sshChannel);
4087 
4088 				return 1;
4089 			}
4090 
4091 			if(count != 0) {
4092 				redraw_();
4093 				justRead();
4094 			}
4095 			return 0;
4096 		}
4097 	} else version(winpty) {
4098 		void readyToReadPty() {
4099 			super.sendRawInput(helperBuffer);
4100 			SetEvent(magicEvent); // tell the other thread we have finished
4101 			redraw_();
4102 			justRead();
4103 		}
4104 	} else version(Windows) {
4105 		OVERLAPPED* overlapped;
4106 		bool overlappedBufferLocked;
4107 		ubyte[4096] overlappedBuffer;
4108 		extern(Windows)
4109 		static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) {
4110 			assert(overlapped !is null);
4111 			typeof(this) w = cast(typeof(this)) overlapped.hEvent;
4112 
4113 			if(numberOfBytes) {
4114 				w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]);
4115 				w.redraw_();
4116 			}
4117 			import std.conv;
4118 
4119 			if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) {
4120 				if(GetLastError() == 997)
4121 				{ } // there's pending i/o, let's just ignore for now and it should tell us later that it completed
4122 				else
4123 				throw new Exception("ReadFileEx " ~ to!string(GetLastError()));
4124 			} else {
4125 			}
4126 
4127 			w.justRead();
4128 		}
4129 	} else version(Posix) {
4130 		void readyToRead(int fd) {
4131 			import core.sys.posix.unistd;
4132 			ubyte[4096] buffer;
4133 
4134 			// the count is to limit how long we spend in this loop
4135 			// when it runs out, it goes back to the main event loop
4136 			// for a while (btw use level triggered events so the remaining
4137 			// data continues to get processed!) giving a chance to redraw
4138 			// and process user input periodically during insanely long and
4139 			// rapid output.
4140 			int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests
4141 
4142 			version(arsd_te_conservative_draws)
4143 				cnt = 400;
4144 
4145 			// FIXME: if connected by ssh, up the count so we don't redraw as frequently.
4146 			// it'd save bandwidth
4147 
4148 			while(--cnt) {
4149 				auto len = read(fd, buffer.ptr, 4096);
4150 				if(len < 0) {
4151 					import core.stdc.errno;
4152 					if(errno == EAGAIN || errno == EWOULDBLOCK) {
4153 						break; // we got it all
4154 					} else {
4155 						//import std.conv;
4156 						//throw new Exception("read failed " ~ to!string(errno));
4157 						return;
4158 					}
4159 				}
4160 
4161 				if(len == 0) {
4162 					close(fd);
4163 					requestExit();
4164 					break;
4165 				}
4166 
4167 				auto data = buffer[0 .. len];
4168 
4169 				if(debugMode) {
4170 					import std.array; import std.stdio; writeln("GOT ", data, "\nOR ", 
4171 						replace(cast(string) data, "\033", "\\")
4172 						.replace("\010", "^H")
4173 						.replace("\r", "^M")
4174 						.replace("\n", "^J")
4175 						);
4176 				}
4177 				super.sendRawInput(data);
4178 			}
4179 
4180 			outputOccurred();
4181 
4182 			redraw_();
4183 
4184 			// HACK: I don't even know why this works, but with this
4185 			// sleep in place, it gives X events from that socket a
4186 			// chance to be processed. It can add a few seconds to a huge
4187 			// output (like `find /usr`), but meh, that's worth it to me
4188 			// to have a chance to ctrl+c.
4189 			import core.thread;
4190 			Thread.sleep(dur!"msecs"(5));
4191 
4192 			justRead();
4193 		}
4194 	}
4195 }
4196 
4197 mixin template SdpyImageSupport() {
4198 	class NonCharacterData_Image : NonCharacterData {
4199 		Image data;
4200 		int imageOffsetX;
4201 		int imageOffsetY;
4202 
4203 		this(Image data, int x, int y) {
4204 			this.data = data;
4205 			this.imageOffsetX = x;
4206 			this.imageOffsetY = y;
4207 		}
4208 	}
4209 
4210 	version(TerminalDirectToEmulator)
4211 	class NonCharacterData_Widget : NonCharacterData {
4212 		this(void* data, size_t idx, int width, int height) {
4213 			this.window = cast(SimpleWindow) data;
4214 			this.idx = idx;
4215 			this.width = width;
4216 			this.height = height;
4217 		}
4218 
4219 		void position(int posx, int posy, int width, int height) {
4220 			if(posx == this.posx && posy == this.posy && width == this.pixelWidth && height == this.pixelHeight)
4221 				return;
4222 			this.posx = posx;
4223 			this.posy = posy;
4224 			this.pixelWidth = width;
4225 			this.pixelHeight = height;
4226 
4227 			window.moveResize(posx, posy, width, height);
4228 			import std.stdio; writeln(posx, " ", posy, " ", width, " ", height);
4229 
4230 			auto painter = this.window.draw;
4231 			painter.outlineColor = Color.red;
4232 			painter.fillColor = Color.green;
4233 			painter.drawRectangle(Point(0, 0), width, height);
4234 
4235 
4236 		}
4237 
4238 		SimpleWindow window;
4239 		size_t idx;
4240 		int width;
4241 		int height;
4242 
4243 		int posx;
4244 		int posy;
4245 		int pixelWidth;
4246 		int pixelHeight;
4247 	}
4248 
4249 	private struct CachedImage {
4250 		ulong hash;
4251 		BinaryDataTerminalRepresentation bui;
4252 		int timesSeen;
4253 		import core.time;
4254 		MonoTime lastUsed;
4255 	}
4256 	private CachedImage[] imageCache;
4257 	private CachedImage* findInCache(ulong hash) {
4258 		if(hash == 0)
4259 			return null;
4260 
4261 		/*
4262 		import std.stdio;
4263 		writeln("***");
4264 		foreach(cache; imageCache) {
4265 			writeln(cache.hash, " ", cache.timesSeen, " ", cache.lastUsed);
4266 		}
4267 		*/
4268 
4269 		foreach(ref i; imageCache)
4270 			if(i.hash == hash) {
4271 				import core.time;
4272 				i.lastUsed = MonoTime.currTime;
4273 				i.timesSeen++;
4274 				return &i;
4275 			}
4276 		return null;
4277 	}
4278 	private BinaryDataTerminalRepresentation addImageCache(ulong hash, BinaryDataTerminalRepresentation bui) {
4279 		import core.time;
4280 		if(imageCache.length == 0)
4281 			imageCache.length = 8;
4282 
4283 		auto now = MonoTime.currTime;
4284 
4285 		size_t oldestIndex;
4286 		MonoTime oldestTime = now;
4287 
4288 		size_t leastUsedIndex;
4289 		int leastUsedCount = int.max;
4290 		foreach(idx, ref cached; imageCache) {
4291 			if(cached.hash == 0) {
4292 				cached.hash = hash;
4293 				cached.bui = bui;
4294 				cached.timesSeen = 1;
4295 				cached.lastUsed = now;
4296 
4297 				return bui;
4298 			} else {
4299 				if(cached.timesSeen < leastUsedCount) {
4300 					leastUsedCount = cached.timesSeen;
4301 					leastUsedIndex = idx;
4302 				}
4303 				if(cached.lastUsed < oldestTime) {
4304 					oldestTime = cached.lastUsed;
4305 					oldestIndex = idx;
4306 				}
4307 			}
4308 		}
4309 
4310 		// need to overwrite one of the cached items, I'll just use the oldest one here
4311 		// but maybe that could be smarter later
4312 
4313 		imageCache[oldestIndex].hash = hash;
4314 		imageCache[oldestIndex].bui = bui;
4315 		imageCache[oldestIndex].timesSeen = 1;
4316 		imageCache[oldestIndex].lastUsed = now;
4317 
4318 		return bui;
4319 	}
4320 
4321 	// It has a cache of the 8 most recently used items right now so if there's a loop of 9 you get pwned
4322 	// but still the cache does an ok job at helping things while balancing out the big memory consumption it
4323 	// could do if just left to grow and grow. i hope.
4324 	protected override BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[] binaryData) {
4325 
4326 		{
4327 		//version(TerminalDirectToEmulator)
4328 		//if(binaryData.length == size_t.sizeof + 10) {
4329 			//if((cast(uint[]) binaryData[0 .. 4])[0] == 0xdeadbeef && (cast(uint[]) binaryData[$-4 .. $])[0] == 0xabcdef32) {
4330 				//auto widthInCharacterCells = binaryData[4];
4331 				//auto heightInCharacterCells = binaryData[5];
4332 				//auto pointer = (cast(void*[]) binaryData[6 .. $-4])[0];
4333 
4334 				auto widthInCharacterCells = 30;
4335 				auto heightInCharacterCells = 20;
4336 				SimpleWindow pwin;
4337 				foreach(k, v; SimpleWindow.nativeMapping) {
4338 					if(v.type == WindowTypes.normal)
4339 					pwin = v;
4340 				}
4341 				auto pointer = cast(void*) (new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin));
4342 
4343 				BinaryDataTerminalRepresentation bi;
4344 				bi.width = widthInCharacterCells;
4345 				bi.height = heightInCharacterCells;
4346 				bi.representation.length = bi.width * bi.height;
4347 
4348 				foreach(idx, ref cell; bi.representation) {
4349 					cell.nonCharacterData = new NonCharacterData_Widget(pointer, idx, widthInCharacterCells, heightInCharacterCells);
4350 				}
4351 
4352 				return bi;
4353 			//}
4354 		}
4355 
4356 		import std.digest.md;
4357 
4358 		ulong hash = * (cast(ulong*) md5Of(binaryData).ptr);
4359 
4360 		if(auto cached = findInCache(hash))
4361 			return cached.bui;
4362 
4363 		TrueColorImage mi;
4364 
4365 		if(binaryData.length > 8 && binaryData[1] == 'P' && binaryData[2] == 'N' && binaryData[3] == 'G') {
4366 			import arsd.png;
4367 			mi = imageFromPng(readPng(binaryData)).getAsTrueColorImage();
4368 		} else if(binaryData.length > 8 && binaryData[0] == 'B' && binaryData[1] == 'M') {
4369 			import arsd.bmp;
4370 			mi = readBmp(binaryData).getAsTrueColorImage();
4371 		} else if(binaryData.length > 2 && binaryData[0] == 0xff && binaryData[1] == 0xd8) {
4372 			import arsd.jpeg;
4373 			mi = readJpegFromMemory(binaryData).getAsTrueColorImage();
4374 		} else if(binaryData.length > 2 && binaryData[0] == '<') {
4375 			import arsd.svg;
4376 			NSVG* image = nsvgParse(cast(const(char)[]) binaryData);
4377 			if(image is null)
4378 				return BinaryDataTerminalRepresentation();
4379 
4380 			int w = cast(int) image.width + 1;
4381 			int h = cast(int) image.height + 1;
4382 			NSVGrasterizer rast = nsvgCreateRasterizer();
4383 			mi = new TrueColorImage(w, h);
4384 			rasterize(rast, image, 0, 0, 1, mi.imageData.bytes.ptr, w, h, w*4);
4385 			image.kill();
4386 		} else {
4387 			return BinaryDataTerminalRepresentation();
4388 		}
4389 
4390 		BinaryDataTerminalRepresentation bi;
4391 		bi.width = mi.width / fontWidth + ((mi.width%fontWidth) ? 1 : 0);
4392 		bi.height = mi.height / fontHeight + ((mi.height%fontHeight) ? 1 : 0);
4393 
4394 		bi.representation.length = bi.width * bi.height;
4395 
4396 		Image data = Image.fromMemoryImage(mi);
4397 
4398 		int ix, iy;
4399 		foreach(ref cell; bi.representation) {
4400 			/*
4401 			Image data = new Image(fontWidth, fontHeight);
4402 			foreach(y; 0 .. fontHeight) {
4403 				foreach(x; 0 .. fontWidth) {
4404 					if(x + ix >= mi.width || y + iy >= mi.height) {
4405 						data.putPixel(x, y, defaultTextAttributes.background);
4406 						continue;
4407 					}
4408 					data.putPixel(x, y, mi.imageData.colors[(iy + y) * mi.width + (ix + x)]);
4409 				}
4410 			}
4411 			*/
4412 
4413 			cell.nonCharacterData = new NonCharacterData_Image(data, ix, iy);
4414 
4415 			ix += fontWidth;
4416 
4417 			if(ix >= mi.width) {
4418 				ix = 0;
4419 				iy += fontHeight;
4420 			}
4421 		}
4422 
4423 		return addImageCache(hash, bi);
4424 		//return bi;
4425 	}
4426 
4427 }
4428 
4429 // this assumes you have imported arsd.simpledisplay and/or arsd.minigui in the mixin scope
4430 mixin template SdpyDraw() {
4431 
4432 	// black bg, make the colors more visible
4433 	static Color contrastify(Color c) {
4434 		if(c == Color(0xcd, 0, 0))
4435 			return Color.fromHsl(0, 1.0, 0.75);
4436 		else if(c == Color(0, 0, 0xcd))
4437 			return Color.fromHsl(240, 1.0, 0.75);
4438 		else if(c == Color(229, 229, 229))
4439 			return Color(0x99, 0x99, 0x99);
4440 		else if(c == Color.black)
4441 			return Color(128, 128, 128);
4442 		else return c;
4443 	}
4444 
4445 	// white bg, make them more visible
4446 	static Color antiContrastify(Color c) {
4447 		if(c == Color(0xcd, 0xcd, 0))
4448 			return Color.fromHsl(60, 1.0, 0.25);
4449 		else if(c == Color(0, 0xcd, 0xcd))
4450 			return Color.fromHsl(180, 1.0, 0.25);
4451 		else if(c == Color(229, 229, 229))
4452 			return Color(0x99, 0x99, 0x99);
4453 		else if(c == Color.white)
4454 			return Color(128, 128, 128);
4455 		else return c;
4456 	}
4457 
4458 	struct SRectangle {
4459 		int left;
4460 		int top;
4461 		int right;
4462 		int bottom;
4463 	}
4464 
4465 	mixin SdpyImageSupport;
4466 
4467 	OperatingSystemFont font;
4468 	int fontWidth;
4469 	int fontHeight;
4470 
4471 	enum paddingLeft = 2;
4472 	enum paddingTop = 1;
4473 
4474 	void loadDefaultFont(int size = 14) {
4475 		static if(UsingSimpledisplayX11) {
4476 			font = new OperatingSystemFont("core:fixed", size, FontWeight.medium);
4477 			//font = new OperatingSystemFont("monospace", size, FontWeight.medium);
4478 			if(font.isNull) {
4479 				// didn't work, it is using a
4480 				// fallback, prolly fixed-13 is best
4481 				font = new OperatingSystemFont("core:fixed", 13, FontWeight.medium);
4482 			}
4483 		} else version(Windows) {
4484 			this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium);
4485 			if(!this.font.isNull && !this.font.isMonospace) 
4486 				this.font.unload(); // non-monospace fonts are unusable here. This should never happen anyway though as Courier New comes with Windows
4487 		}
4488 
4489 		if(font.isNull) {
4490 			// no way to really tell... just guess so it doesn't crash but like eeek.
4491 			fontWidth = size / 2;
4492 			fontHeight = size;
4493 		} else {
4494 			fontWidth = font.averageWidth;
4495 			fontHeight = font.height;
4496 		}
4497 	}
4498 
4499 	bool lastDrawAlternativeScreen;
4500 	final SRectangle redrawPainter(T)(T painter, bool forceRedraw) {
4501 		SRectangle invalidated;
4502 
4503 		// FIXME: anything we can do to make this faster is good
4504 		// on both, the XImagePainter could use optimizations
4505 		// on both, drawing blocks would probably be good too - not just one cell at a time, find whole blocks of stuff
4506 		// on both it might also be good to keep scroll commands high level somehow. idk.
4507 
4508 		// FIXME on Windows it would definitely help a lot to do just one ExtTextOutW per line, if possible. the current code is brutally slow
4509 
4510 		// Or also see https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-polytextoutw
4511 
4512 		static if(is(T == WidgetPainter) || is(T == ScreenPainter)) {
4513 			if(font)
4514 				painter.setFont(font);
4515 		}
4516 
4517 
4518 		int posx = paddingLeft;
4519 		int posy = paddingTop;
4520 
4521 
4522 		char[512] bufferText;
4523 		bool hasBufferedInfo;
4524 		int bufferTextLength;
4525 		Color bufferForeground;
4526 		Color bufferBackground;
4527 		int bufferX = -1;
4528 		int bufferY = -1;
4529 		bool bufferReverse;
4530 		void flushBuffer() {
4531 			if(!hasBufferedInfo) {
4532 				return;
4533 			}
4534 
4535 			assert(posx - bufferX - 1 > 0);
4536 
4537 			painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
4538 			painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;
4539 
4540 			painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
4541 			painter.fillColor = Color.transparent;
4542 			// Hack for contrast!
4543 			if(bufferBackground == Color.black && !bufferReverse) {
4544 				// brighter than normal in some cases so i can read it easily
4545 				painter.outlineColor = contrastify(bufferForeground);
4546 			} else if(bufferBackground == Color.white && !bufferReverse) {
4547 				// darker than normal so i can read it
4548 				painter.outlineColor = antiContrastify(bufferForeground);
4549 			} else if(bufferForeground == bufferBackground) {
4550 				// color on itself, I want it visible too
4551 				auto hsl = toHsl(bufferForeground, true);
4552 				if(hsl[2] < 0.5)
4553 					hsl[2] += 0.5;
4554 				else
4555 					hsl[2] -= 0.5;
4556 				painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);
4557 
4558 			} else {
4559 				// normal
4560 				painter.outlineColor = bufferReverse ? bufferBackground : bufferForeground;
4561 			}
4562 
4563 			// FIXME: make sure this clips correctly
4564 			painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);
4565 
4566 			hasBufferedInfo = false;
4567 
4568 			bufferReverse = false;
4569 			bufferTextLength = 0;
4570 			bufferX = -1;
4571 			bufferY = -1;
4572 		}
4573 
4574 
4575 
4576 		int x;
4577 		foreach(idx, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
4578 			if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
4579 				flushBuffer();
4580 				goto skipDrawing;
4581 			}
4582 			cell.invalidated = false;
4583 			version(none) if(bufferX == -1) { // why was this ever here?
4584 				bufferX = posx;
4585 				bufferY = posy;
4586 			}
4587 
4588 			if(!cell.hasNonCharacterData) {
4589 
4590 				invalidated.left = posx < invalidated.left ? posx : invalidated.left;
4591 				invalidated.top = posy < invalidated.top ? posy : invalidated.top;
4592 				int xmax = posx + fontWidth;
4593 				int ymax = posy + fontHeight;
4594 				invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
4595 				invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
4596 
4597 				// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
4598 				{
4599 
4600 					bool reverse = (cell.attributes.inverse != reverseVideo);
4601 					if(cell.selected)
4602 						reverse = !reverse;
4603 
4604 					version(with_24_bit_color) {
4605 						auto fgc = cell.attributes.foreground;
4606 						auto bgc = cell.attributes.background;
4607 
4608 						if(!(cell.attributes.foregroundIndex & 0xff00)) {
4609 							// this refers to a specific palette entry, which may change, so we should use that
4610 							fgc = palette[cell.attributes.foregroundIndex];
4611 						}
4612 						if(!(cell.attributes.backgroundIndex & 0xff00)) {
4613 							// this refers to a specific palette entry, which may change, so we should use that
4614 							bgc = palette[cell.attributes.backgroundIndex];
4615 						}
4616 
4617 					} else {
4618 						auto fgc = cell.attributes.foregroundIndex == 256 ? defaultForeground : palette[cell.attributes.foregroundIndex & 0xff];
4619 						auto bgc = cell.attributes.backgroundIndex == 256 ? defaultBackground : palette[cell.attributes.backgroundIndex & 0xff];
4620 					}
4621 
4622 					if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
4623 						flushBuffer();
4624 					bufferReverse = reverse;
4625 					bufferBackground = bgc;
4626 					bufferForeground = fgc;
4627 				}
4628 			}
4629 
4630 				if(!cell.hasNonCharacterData) {
4631 					char[4] str;
4632 					import std.utf;
4633 					// now that it is buffered, we do want to draw it this way...
4634 					//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
4635 						try {
4636 							auto stride = encode(str, cell.ch);
4637 							if(bufferTextLength + stride > bufferText.length)
4638 								flushBuffer();
4639 							bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
4640 							bufferTextLength += stride;
4641 
4642 							if(bufferX == -1) {
4643 								bufferX = posx;
4644 								bufferY = posy;
4645 							}
4646 							hasBufferedInfo = true;
4647 						} catch(Exception e) {
4648 							// import std.stdio; writeln(cast(uint) cell.ch, " :: ", e.msg);
4649 						}
4650 					//}
4651 				} else if(cell.nonCharacterData !is null) {
4652 					//import std.stdio; writeln(cast(void*) cell.nonCharacterData);
4653 					if(auto ncdi = cast(NonCharacterData_Image) cell.nonCharacterData) {
4654 						flushBuffer();
4655 						painter.outlineColor = defaultBackground;
4656 						painter.fillColor = defaultBackground;
4657 						painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
4658 						painter.drawImage(Point(posx, posy), ncdi.data, Point(ncdi.imageOffsetX, ncdi.imageOffsetY), fontWidth, fontHeight);
4659 					}
4660 					version(TerminalDirectToEmulator)
4661 					if(auto wdi = cast(NonCharacterData_Widget) cell.nonCharacterData) {
4662 						flushBuffer();
4663 						if(wdi.idx == 0) {
4664 							wdi.position(posx, posy, fontWidth * wdi.width, fontHeight * wdi.height);
4665 							/*
4666 							painter.outlineColor = defaultBackground;
4667 							painter.fillColor = defaultBackground;
4668 							painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
4669 							*/
4670 						}
4671 
4672 					}
4673 				}
4674 
4675 				if(!cell.hasNonCharacterData)
4676 				if(cell.attributes.underlined) {
4677 					// the posx adjustment is because the buffer assumes it is going
4678 					// to be flushed after advancing, but here, we're doing it mid-character
4679 					// FIXME: we should just underline the whole thing consecutively, with the buffer
4680 					posx += fontWidth;
4681 					flushBuffer();
4682 					posx -= fontWidth;
4683 					painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
4684 				}
4685 			skipDrawing:
4686 
4687 				posx += fontWidth;
4688 			x++;
4689 			if(x == screenWidth) {
4690 				flushBuffer();
4691 				x = 0;
4692 				posy += fontHeight;
4693 				posx = paddingLeft;
4694 			}
4695 		}
4696 
4697 		if(cursorShowing) {
4698 			painter.fillColor = cursorColor;
4699 			painter.outlineColor = cursorColor;
4700 			painter.rasterOp = RasterOp.xor;
4701 
4702 			posx = cursorPosition.x * fontWidth + paddingLeft;
4703 			posy = cursorPosition.y * fontHeight + paddingTop;
4704 
4705 			int cursorWidth = fontWidth;
4706 			int cursorHeight = fontHeight;
4707 
4708 			final switch(cursorStyle) {
4709 				case CursorStyle.block:
4710 					painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
4711 				break;
4712 				case CursorStyle.underline:
4713 					painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
4714 				break;
4715 				case CursorStyle.bar:
4716 					painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
4717 				break;
4718 			}
4719 			painter.rasterOp = RasterOp.normal;
4720 
4721 			// since the cursor draws over the cell, we need to make sure it is redrawn each time too
4722 			auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
4723 			if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
4724 				(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
4725 			}
4726 
4727 			invalidated.left = posx < invalidated.left ? posx : invalidated.left;
4728 			invalidated.top = posy < invalidated.top ? posy : invalidated.top;
4729 			int xmax = posx + fontWidth;
4730 			int ymax = xmax + fontHeight;
4731 			invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
4732 			invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
4733 		}
4734 
4735 		lastDrawAlternativeScreen = alternateScreenActive;
4736 
4737 		return invalidated;
4738 	}
4739 }
4740 
4741 string encodeSmallTextImage(IndexedImage ii) {
4742 	char encodeNumeric(int c) {
4743 		if(c < 10)
4744 			return cast(char)(c + '0');
4745 		if(c < 10 + 26)
4746 			return cast(char)(c - 10 + 'a');
4747 		assert(0);
4748 	}
4749 
4750 	string s;
4751 	s ~= encodeNumeric(ii.width);
4752 	s ~= encodeNumeric(ii.height);
4753 
4754 	foreach(entry; ii.palette)
4755 		s ~= entry.toRgbaHexString();
4756 	s ~= "Z";
4757 
4758 	ubyte rleByte;
4759 	int rleCount;
4760 
4761 	void rleCommit() {
4762 		if(rleByte >= 26)
4763 			assert(0); // too many colors for us to handle
4764 		if(rleCount == 0)
4765 			goto finish;
4766 		if(rleCount == 1) {
4767 			s ~= rleByte + 'a';
4768 			goto finish;
4769 		}
4770 
4771 		import std.conv;
4772 		s ~= to!string(rleCount);
4773 		s ~= rleByte + 'a';
4774 
4775 		finish:
4776 			rleByte = 0;
4777 			rleCount = 0;
4778 	}
4779 
4780 	foreach(b; ii.data) {
4781 		if(b == rleByte)
4782 			rleCount++;
4783 		else {
4784 			rleCommit();
4785 			rleByte = b;
4786 			rleCount = 1;
4787 		}
4788 	}
4789 
4790 	rleCommit();
4791 
4792 	return s;
4793 }
4794 
4795 IndexedImage readSmallTextImage(scope const(char)[] arg) {
4796 	auto origArg = arg;
4797 	int width;
4798 	int height;
4799 
4800 	int readNumeric(char c) {
4801 		if(c >= '0' && c <= '9')
4802 			return c - '0';
4803 		if(c >= 'a' && c <= 'z')
4804 			return c - 'a' + 10;
4805 		return 0;
4806 	}
4807 
4808 	if(arg.length > 2) {
4809 		width = readNumeric(arg[0]);
4810 		height = readNumeric(arg[1]);
4811 		arg = arg[2 .. $];
4812 	}
4813 
4814 	import std.conv;
4815 	assert(width == 16, to!string(width));
4816 	assert(height == 16, to!string(width));
4817 
4818 	Color[] palette;
4819 	ubyte[256] data;
4820 	int didx = 0;
4821 	bool readingPalette = true;
4822 	outer: while(arg.length) {
4823 		if(readingPalette) {
4824 			if(arg[0] == 'Z') {
4825 				readingPalette = false;
4826 				arg = arg[1 .. $];
4827 				continue;
4828 			}
4829 			if(arg.length < 8)
4830 				break;
4831 			foreach(a; arg[0..8]) {
4832 				// if not strict hex, forget it
4833 				if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z')))
4834 					break outer;
4835 			}
4836 			palette ~= Color.fromString(arg[0 .. 8]);
4837 			arg = arg[8 .. $];
4838 		} else {
4839 			char[3] rleChars;
4840 			int rlePos;
4841 			while(arg.length && arg[0] >= '0' && arg[0] <= '9') {
4842 				rleChars[rlePos] = arg[0];
4843 				arg = arg[1 .. $];
4844 				rlePos++;
4845 				if(rlePos >= rleChars.length)
4846 					break;
4847 			}
4848 			if(arg.length == 0)
4849 				break;
4850 
4851 			int rle;
4852 			if(rlePos == 0)
4853 				rle = 1;
4854 			else {
4855 				// 100
4856 				// rleChars[0] == '1'
4857 				foreach(c; rleChars[0 .. rlePos]) {
4858 					rle *= 10;
4859 					rle += c - '0';
4860 				}
4861 			}
4862 
4863 			foreach(i; 0 .. rle) {
4864 				if(arg[0] >= 'a' && arg[0] <= 'z')
4865 					data[didx] = cast(ubyte)(arg[0] - 'a');
4866 
4867 				didx++;
4868 				if(didx == data.length)
4869 					break outer;
4870 			}
4871 
4872 			arg = arg[1 .. $];
4873 		}
4874 	}
4875 
4876 	// width, height, palette, data is set up now
4877 
4878 	if(palette.length) {
4879 		auto ii = new IndexedImage(width, height);
4880 		ii.palette = palette;
4881 		ii.data = data.dup;
4882 
4883 		return ii;
4884 	}// else assert(0, origArg);
4885 	return null;
4886 }
4887 
4888 
4889 // workaround dmd bug fixed in next release
4890 //static immutable Color[256] xtermPalette = [
4891 immutable(Color)[] xtermPalette() {
4892 
4893 	// This is an approximation too for a few entries, but a very close one.
4894 	Color xtermPaletteIndexToColor(int paletteIdx) {
4895 		Color color;
4896 		color.a = 255;
4897 
4898 		if(paletteIdx < 16) {
4899 			if(paletteIdx == 7)
4900 				return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see
4901 			else if(paletteIdx == 8)
4902 				return Color(0x80, 0x80, 0x80);
4903 
4904 			// real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see
4905 			color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
4906 			color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
4907 			color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
4908 
4909 		} else if(paletteIdx < 232) {
4910 			// color ramp, 6x6x6 cube
4911 			color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55);
4912 			color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55);
4913 			color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55);
4914 
4915 			if(color.r == 55) color.r = 0;
4916 			if(color.g == 55) color.g = 0;
4917 			if(color.b == 55) color.b = 0;
4918 		} else {
4919 			// greyscale ramp, from 0x8 to 0xee
4920 			color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10);
4921 			color.g = color.r;
4922 			color.b = color.g;
4923 		}
4924 
4925 		return color;
4926 	}
4927 
4928 	static immutable(Color)[] ret;
4929 	if(ret.length == 256)
4930 		return ret;
4931 
4932 	ret.reserve(256);
4933 	foreach(i; 0 .. 256)
4934 		ret ~= xtermPaletteIndexToColor(i);
4935 
4936 	return ret;
4937 }
4938 
4939 static shared immutable dchar[dchar] lineDrawingCharacterSet;
4940 shared static this() {
4941 	lineDrawingCharacterSet = [
4942 		'a' : ':',
4943 		'j' : '+',
4944 		'k' : '+',
4945 		'l' : '+',
4946 		'm' : '+',
4947 		'n' : '+',
4948 		'q' : '-',
4949 		't' : '+',
4950 		'u' : '+',
4951 		'v' : '+',
4952 		'w' : '+',
4953 		'x' : '|',
4954 	];
4955 
4956 	// this is what they SHOULD be but the font i use doesn't support all these
4957 	// the ascii fallback above looks pretty good anyway though.
4958 	version(none)
4959 	lineDrawingCharacterSet = [
4960 		'a' : '\u2592',
4961 		'j' : '\u2518',
4962 		'k' : '\u2510',
4963 		'l' : '\u250c',
4964 		'm' : '\u2514',
4965 		'n' : '\u253c',
4966 		'q' : '\u2500',
4967 		't' : '\u251c',
4968 		'u' : '\u2524',
4969 		'v' : '\u2534',
4970 		'w' : '\u252c',
4971 		'x' : '\u2502',
4972 	];
4973 }
4974 
4975 /+
4976 Copyright: Adam D. Ruppe, 2013 - 2020
4977 License:   [http://www.boost.org/LICENSE_1_0.txt|Boost Software License 1.0]
4978 Authors: Adam D. Ruppe
4979 +/
Suggestion Box / Bug Report