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