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