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