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 'f': // wikipedia says it is the same except it is a format func instead of editor func. idk what the diff is
2734 					case 'H':
2735 						auto got = getArgs(1, 1);
2736 						cursorX = got[1] - 1;
2737 
2738 						if(got[0] - 1 == cursorY)
2739 							setTentativeScrollback(cursorX);
2740 						else
2741 							setTentativeScrollback(0);
2742 
2743 						cursorY = got[0] - 1;
2744 						newLineOnNext = false;
2745 					break;
2746 					case 'L':
2747 						// insert lines
2748 						scrollDown(getArgs(1)[0]);
2749 					break;
2750 					case 'M':
2751 						// delete lines
2752 						if(cursorY + 1 < screenHeight) {
2753 							TerminalCell plain;
2754 							plain.ch = ' ';
2755 							plain.attributes = defaultTextAttributes();
2756 							foreach(i; 0 .. getArgs(1)[0]) {
2757 								foreach(y; cursorY .. scrollZoneBottom)
2758 								foreach(x; 0 .. screenWidth) {
2759 									ASS[y][x] = ASS[y + 1][x];
2760 									ASS[y][x].invalidated = true;
2761 								}
2762 								foreach(x; 0 .. screenWidth) {
2763 									ASS[scrollZoneBottom][x] = plain;
2764 								}
2765 							}
2766 
2767 							extendInvalidatedRange();
2768 						}
2769 					break;
2770 					case 'K':
2771 						auto arg = getArgs(0)[0];
2772 						int start, end;
2773 						if(arg == 0) {
2774 							// clear from cursor to end of line
2775 							start = cursorX;
2776 							end = this.screenWidth;
2777 						} else if(arg == 1) {
2778 							// clear from cursor to beginning of line
2779 							start = 0;
2780 							end = cursorX + 1;
2781 						} else if(arg == 2) {
2782 							// clear entire line
2783 							start = 0;
2784 							end = this.screenWidth;
2785 						}
2786 
2787 						TerminalCell plain;
2788 						plain.ch = ' ';
2789 						plain.attributes = currentAttributes;
2790 
2791 						for(int i = start; i < end; i++) {
2792 							if(ASS[cursorY][i].selected)
2793 								clearSelection();
2794 							ASS[cursorY]
2795 								[i] = plain;
2796 						}
2797 					break;
2798 					case 's':
2799 						pushSavedCursor(cursorPosition);
2800 					break;
2801 					case 'u':
2802 						cursorPosition = popSavedCursor();
2803 					break;
2804 					case 'g':
2805 						auto arg = getArgs(0)[0];
2806 						TerminalCell plain;
2807 						plain.ch = ' ';
2808 						plain.attributes = currentAttributes;
2809 						if(arg == 0) {
2810 							// clear current column
2811 							for(int i = 0; i < this.screenHeight; i++)
2812 								ASS[i]
2813 									[cursorY] = plain;
2814 						} else if(arg == 3) {
2815 							// clear all
2816 							cls();
2817 						}
2818 					break;
2819 					case 'q':
2820 						// xterm also does blinks on the odd numbers (x-1)
2821 						if(esc == "[0 q")
2822 							cursorStyle = CursorStyle.block; // FIXME: restore default
2823 						if(esc == "[2 q")
2824 							cursorStyle = CursorStyle.block;
2825 						else if(esc == "[4 q")
2826 							cursorStyle = CursorStyle.underline;
2827 						else if(esc == "[6 q")
2828 							cursorStyle = CursorStyle.bar;
2829 
2830 						changeCursorStyle(cursorStyle);
2831 					break;
2832 					case 't':
2833 						// window commands
2834 						// i might support more of these but for now i just want the stack stuff.
2835 
2836 						auto args = getArgs(0, 0);
2837 						if(args[0] == 22) {
2838 							// save window title to stack
2839 							// xterm says args[1] should tell if it is the window title, the icon title, or both, but meh
2840 							titleStack ~= windowTitle;
2841 							iconStack ~= windowIcon;
2842 						} else if(args[0] == 23) {
2843 							// restore from stack
2844 							if(titleStack.length) {
2845 								windowTitle = titleStack[$ - 1];
2846 								changeWindowTitle(titleStack[$ - 1]);
2847 								titleStack = titleStack[0 .. $ - 1];
2848 							}
2849 
2850 							if(iconStack.length) {
2851 								windowIcon = iconStack[$ - 1];
2852 								changeWindowIcon(iconStack[$ - 1]);
2853 								iconStack = iconStack[0 .. $ - 1];
2854 							}
2855 						}
2856 					break;
2857 					case 'm':
2858 						// FIXME  used by xterm to decide whether to construct
2859 						// CSI > Pp ; Pv m CSI > Pp m Set/reset key modifier options, xterm.
2860 						if(esc[1] == '>')
2861 							goto default;
2862 						// done
2863 						argsLoop: foreach(argIdx, arg; getArgs(0))
2864 						switch(arg) {
2865 							case 0:
2866 							// normal
2867 								currentAttributes = defaultTextAttributes;
2868 							break;
2869 							case 1:
2870 								currentAttributes.bold = true;
2871 							break;
2872 							case 2:
2873 								currentAttributes.faint = true;
2874 							break;
2875 							case 3:
2876 								currentAttributes.italic = true;
2877 							break;
2878 							case 4:
2879 								currentAttributes.underlined = true;
2880 							break;
2881 							case 5:
2882 								currentAttributes.blink = true;
2883 							break;
2884 							case 6:
2885 								// rapid blink, treating the same as regular blink
2886 								currentAttributes.blink = true;
2887 							break;
2888 							case 7:
2889 								currentAttributes.inverse = true;
2890 							break;
2891 							case 8:
2892 								currentAttributes.invisible = true;
2893 							break;
2894 							case 9:
2895 								currentAttributes.strikeout = true;
2896 							break;
2897 							case 10:
2898 								// primary font
2899 							break;
2900 							case 11: .. case 19:
2901 								// alternate fonts
2902 							break;
2903 							case 20:
2904 								// Fraktur font
2905 							break;
2906 							case 21:
2907 								// bold off and doubled underlined
2908 							break;
2909 							case 22:
2910 								currentAttributes.bold = false;
2911 								currentAttributes.faint = false;
2912 							break;
2913 							case 23:
2914 								currentAttributes.italic = false;
2915 							break;
2916 							case 24:
2917 								currentAttributes.underlined = false;
2918 							break;
2919 							case 25:
2920 								currentAttributes.blink = false;
2921 							break;
2922 							case 26:
2923 								// reserved
2924 							break;
2925 							case 27:
2926 								currentAttributes.inverse = false;
2927 							break;
2928 							case 28:
2929 								currentAttributes.invisible = false;
2930 							break;
2931 							case 29:
2932 								currentAttributes.strikeout = false;
2933 							break;
2934 							case 30:
2935 							..
2936 							case 37:
2937 							// set foreground color
2938 								/*
2939 								Color nc;
2940 								ubyte multiplier = currentAttributes.bold ? 255 : 127;
2941 								nc.r = cast(ubyte)((arg - 30) & 1) * multiplier;
2942 								nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier;
2943 								nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier;
2944 								nc.a = 255;
2945 								*/
2946 								currentAttributes.foregroundIndex = cast(ubyte)(arg - 30);
2947 								version(with_24_bit_color)
2948 								currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)];
2949 							break;
2950 							case 38:
2951 								// xterm 256 color set foreground color
2952 								auto args = getArgs()[argIdx + 1 .. $];
2953 								if(args.length > 3 && args[0] == 2) {
2954 									// set color to closest match in palette. but since we have full support, we'll just take it directly
2955 									auto fg = Color(args[1], args[2], args[3]);
2956 									version(with_24_bit_color)
2957 										currentAttributes.foreground = fg;
2958 									// and try to find a low default palette entry for maximum compatibility
2959 									// 0x8000 == approximation
2960 									currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg);
2961 								} else if(args.length > 1 && args[0] == 5) {
2962 									// set to palette index
2963 									version(with_24_bit_color)
2964 										currentAttributes.foreground = palette[args[1]];
2965 									currentAttributes.foregroundIndex = cast(ushort) args[1];
2966 								}
2967 								break argsLoop;
2968 							case 39:
2969 							// default foreground color
2970 								auto dflt = defaultTextAttributes();
2971 
2972 								version(with_24_bit_color)
2973 									currentAttributes.foreground = dflt.foreground;
2974 								currentAttributes.foregroundIndex = dflt.foregroundIndex;
2975 							break;
2976 							case 40:
2977 							..
2978 							case 47:
2979 							// set background color
2980 								/*
2981 								Color nc;
2982 								nc.r = cast(ubyte)((arg - 40) & 1) * 255;
2983 								nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255;
2984 								nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255;
2985 								nc.a = 255;
2986 								*/
2987 
2988 								currentAttributes.backgroundIndex = cast(ubyte)(arg - 40);
2989 								//currentAttributes.background = nc;
2990 								version(with_24_bit_color)
2991 									currentAttributes.background = palette[arg-40];
2992 							break;
2993 							case 48:
2994 								// xterm 256 color set background color
2995 								auto args = getArgs()[argIdx + 1 .. $];
2996 								if(args.length > 3 && args[0] == 2) {
2997 									// set color to closest match in palette. but since we have full support, we'll just take it directly
2998 									auto bg = Color(args[1], args[2], args[3]);
2999 									version(with_24_bit_color)
3000 										currentAttributes.background = Color(args[1], args[2], args[3]);
3001 
3002 									// and try to find a low default palette entry for maximum compatibility
3003 									// 0x8000 == this is an approximation
3004 									currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg);
3005 								} else if(args.length > 1 && args[0] == 5) {
3006 									// set to palette index
3007 									version(with_24_bit_color)
3008 										currentAttributes.background = palette[args[1]];
3009 									currentAttributes.backgroundIndex = cast(ushort) args[1];
3010 								}
3011 
3012 								break argsLoop;
3013 							case 49:
3014 							// default background color
3015 								auto dflt = defaultTextAttributes();
3016 
3017 								version(with_24_bit_color)
3018 									currentAttributes.background = dflt.background;
3019 								currentAttributes.backgroundIndex = dflt.backgroundIndex;
3020 							break;
3021 							case 51:
3022 								// framed
3023 							break;
3024 							case 52:
3025 								// encircled
3026 							break;
3027 							case 53:
3028 								// overlined
3029 							break;
3030 							case 54:
3031 								// not framed or encircled
3032 							break;
3033 							case 55:
3034 								// not overlined
3035 							break;
3036 							case 90: .. case 97:
3037 								// high intensity foreground color
3038 							break;
3039 							case 100: .. case 107:
3040 								// high intensity background color
3041 							break;
3042 							default:
3043 								unknownEscapeSequence(cast(string) esc);
3044 						}
3045 					break;
3046 					case 'J':
3047 						// erase in display
3048 						auto arg = getArgs(0)[0];
3049 						switch(arg) {
3050 							case 0:
3051 								TerminalCell plain;
3052 								plain.ch = ' ';
3053 								plain.attributes = currentAttributes;
3054 								// erase below
3055 								foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) {
3056 									if(alternateScreenActive)
3057 										alternateScreen[i] = plain;
3058 									else
3059 										normalScreen[i] = plain;
3060 								}
3061 							break;
3062 							case 1:
3063 								// erase above
3064 								unknownEscapeSequence("FIXME");
3065 							break;
3066 							case 2:
3067 								// erase all
3068 								cls();
3069 							break;
3070 							default: unknownEscapeSequence(cast(string) esc);
3071 						}
3072 					break;
3073 					case 'r':
3074 						if(esc[1] != '?') {
3075 							// set scrolling zone
3076 							// default should be full size of window
3077 							auto args = getArgs(1, screenHeight);
3078 
3079 							// FIXME: these are supposed to be per-buffer
3080 							scrollZoneTop = args[0] - 1;
3081 							scrollZoneBottom = args[1] - 1;
3082 
3083 							if(scrollZoneTop < 0)
3084 								scrollZoneTop = 0;
3085 							if(scrollZoneBottom > screenHeight)
3086 								scrollZoneBottom = screenHeight - 1;
3087 						} else {
3088 							// restore... something FIXME
3089 						}
3090 					break;
3091 					case 'h':
3092 						if(esc[1] != '?')
3093 						foreach(arg; getArgs())
3094 						switch(arg) {
3095 							case 4:
3096 								insertMode = true;
3097 							break;
3098 							case 34:
3099 								// no idea. vim inside screen sends it
3100 							break;
3101 							default: unknownEscapeSequence(cast(string) esc);
3102 						}
3103 						else
3104 					//import std.stdio; writeln("h magic ", cast(string) esc);
3105 						foreach(arg; getArgsBase(2, null)) {
3106 							if(arg > 65535) {
3107 								/* Extensions */
3108 								if(arg < 65536 + 65535) {
3109 									// activate hyperlink
3110 									hyperlinkFlipper = !hyperlinkFlipper;
3111 									hyperlinkActive = true;
3112 									hyperlinkNumber = arg - 65536;
3113 								}
3114 							} else
3115 							switch(arg) {
3116 								case 1:
3117 									// application cursor keys
3118 									applicationCursorKeys = true;
3119 								break;
3120 								case 3:
3121 									// 132 column mode
3122 								break;
3123 								case 4:
3124 									// smooth scroll
3125 								break;
3126 								case 5:
3127 									// reverse video
3128 									reverseVideo = true;
3129 								break;
3130 								case 6:
3131 									// origin mode
3132 								break;
3133 								case 7:
3134 									// wraparound mode
3135 									wraparoundMode = false;
3136 									// 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
3137 								break;
3138 								case 9:
3139 									allMouseTrackingOff();
3140 									mouseButtonTracking = true;
3141 								break;
3142 								case 12:
3143 									// start blinking cursor
3144 								break;
3145 								case 1034:
3146 									// meta keys????
3147 								break;
3148 								case 1049:
3149 									// Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first.
3150 									alternateScreenActive = true;
3151 									scrollLock = false;
3152 									pushSavedCursor(cursorPosition);
3153 									cls();
3154 									notifyScrollbarRelevant(false, false);
3155 								break;
3156 								case 1000:
3157 									// send mouse X&Y on button press and release
3158 									allMouseTrackingOff();
3159 									mouseButtonTracking = true;
3160 									mouseButtonReleaseTracking = true;
3161 								break;
3162 								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
3163 								break;
3164 								case 1002:
3165 									allMouseTrackingOff();
3166 									mouseButtonTracking = true;
3167 									mouseButtonReleaseTracking = true;
3168 									mouseButtonMotionTracking = true;
3169 									// use cell motion mouse tracking
3170 								break;
3171 								case 1003:
3172 									// ALL motion is sent
3173 									allMouseTrackingOff();
3174 									mouseButtonTracking = true;
3175 									mouseButtonReleaseTracking = true;
3176 									mouseMotionTracking = true;
3177 								break;
3178 								case 1004:
3179 									sendFocusEvents = true;
3180 								break;
3181 								case 1005:
3182 									utf8MouseMode = true;
3183 									// enable utf-8 mouse mode
3184 									/*
3185 UTF-8 (1005)
3186           This enables UTF-8 encoding for Cx and Cy under all tracking
3187           modes, expanding the maximum encodable position from 223 to
3188           2015.  For positions less than 95, the resulting output is
3189           identical under both modes.  Under extended mouse mode, posi-
3190           tions greater than 95 generate "extra" bytes which will con-
3191           fuse applications which do not treat their input as a UTF-8
3192           stream.  Likewise, Cb will be UTF-8 encoded, to reduce confu-
3193           sion with wheel mouse events.
3194           Under normal mouse mode, positions outside (160,94) result in
3195           byte pairs which can be interpreted as a single UTF-8 charac-
3196           ter; applications which do treat their input as UTF-8 will
3197           almost certainly be confused unless extended mouse mode is
3198           active.
3199           This scheme has the drawback that the encoded coordinates will
3200           not pass through luit unchanged, e.g., for locales using non-
3201           UTF-8 encoding.
3202 									*/
3203 								break;
3204 								case 1006:
3205 								/*
3206 SGR (1006)
3207           The normal mouse response is altered to use CSI < followed by
3208           semicolon-separated encoded button value, the Cx and Cy ordi-
3209           nates and a final character which is M  for button press and m
3210           for button release.
3211           o The encoded button value in this case does not add 32 since
3212             that was useful only in the X10 scheme for ensuring that the
3213             byte containing the button value is a printable code.
3214           o The modifiers are encoded in the same way.
3215           o A different final character is used for button release to
3216             resolve the X10 ambiguity regarding which button was
3217             released.
3218           The highlight tracking responses are also modified to an SGR-
3219           like format, using the same SGR-style scheme and button-encod-
3220           ings.
3221 								*/
3222 								break;
3223 								case 1014:
3224 									// ARSD extension: it is 1002 but selective, only
3225 									// on top row, row with cursor, or else if middle click/wheel.
3226 									//
3227 									// Quite specifically made for my getline function!
3228 									allMouseTrackingOff();
3229 
3230 									mouseButtonMotionTracking = true;
3231 									mouseButtonTracking = true;
3232 									mouseButtonReleaseTracking = true;
3233 									selectiveMouseTracking = true;
3234 								break;
3235 								case 1015:
3236 								/*
3237 URXVT (1015)
3238           The normal mouse response is altered to use CSI followed by
3239           semicolon-separated encoded button value, the Cx and Cy ordi-
3240           nates and final character M .
3241           This uses the same button encoding as X10, but printing it as
3242           a decimal integer rather than as a single byte.
3243           However, CSI M  can be mistaken for DL (delete lines), while
3244           the highlight tracking CSI T  can be mistaken for SD (scroll
3245           down), and the Window manipulation controls.  For these rea-
3246           sons, the 1015 control is not recommended; it is not an
3247           improvement over 1005.
3248 								*/
3249 								break;
3250 								case 1048:
3251 									pushSavedCursor(cursorPosition);
3252 								break;
3253 								case 2004:
3254 									bracketedPasteMode = true;
3255 								break;
3256 								case 3004:
3257 									bracketedHyperlinkMode = true;
3258 								break;
3259 								case 1047:
3260 								case 47:
3261 									alternateScreenActive = true;
3262 									scrollLock = false;
3263 									cls();
3264 									notifyScrollbarRelevant(false, false);
3265 								break;
3266 								case 25:
3267 									cursorShowing = true;
3268 								break;
3269 
3270 								/* Done */
3271 								default: unknownEscapeSequence(cast(string) esc);
3272 							}
3273 						}
3274 					break;
3275 					case 'p':
3276 						// it is asking a question... and tbh i don't care.
3277 					break;
3278 					case 'l':
3279 					//import std.stdio; writeln("l magic ", cast(string) esc);
3280 						if(esc[1] != '?')
3281 						foreach(arg; getArgs())
3282 						switch(arg) {
3283 							case 4:
3284 								insertMode = false;
3285 							break;
3286 							case 34:
3287 								// no idea. vim inside screen sends it
3288 							break;
3289 							case 1004:
3290 								sendFocusEvents = false;
3291 							break;
3292 							case 1005:
3293 								// turn off utf-8 mouse
3294 								utf8MouseMode = false;
3295 							break;
3296 							case 1006:
3297 								// turn off sgr mouse
3298 							break;
3299 							case 1015:
3300 								// turn off urxvt mouse
3301 							break;
3302 							default: unknownEscapeSequence(cast(string) esc);
3303 						}
3304 						else
3305 						foreach(arg; getArgsBase(2, null)) {
3306 							if(arg > 65535) {
3307 								/* Extensions */
3308 								if(arg < 65536 + 65535)
3309 									hyperlinkActive = false;
3310 							} else
3311 							switch(arg) {
3312 								case 1:
3313 									// normal cursor keys
3314 									applicationCursorKeys = false;
3315 								break;
3316 								case 3:
3317 									// 80 column mode
3318 								break;
3319 								case 4:
3320 									// smooth scroll
3321 								break;
3322 								case 5:
3323 									// normal video
3324 									reverseVideo = false;
3325 								break;
3326 								case 6:
3327 									// normal cursor mode
3328 								break;
3329 								case 7:
3330 									// wraparound mode
3331 									wraparoundMode = true;
3332 								break;
3333 								case 12:
3334 									// stop blinking cursor
3335 								break;
3336 								case 1034:
3337 									// meta keys????
3338 								break;
3339 								case 1049:
3340 									cursorPosition = popSavedCursor;
3341 									wraparoundMode = true;
3342 
3343 									returnToNormalScreen();
3344 								break;
3345 								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
3346 								break;
3347 								case 9:
3348 								case 1000:
3349 								case 1002:
3350 								case 1003:
3351 								case 1014: // arsd extension
3352 									allMouseTrackingOff();
3353 								break;
3354 								case 1005:
3355 								case 1006:
3356 									// idk
3357 								break;
3358 								case 1048:
3359 									cursorPosition = popSavedCursor;
3360 								break;
3361 								case 2004:
3362 									bracketedPasteMode = false;
3363 								break;
3364 								case 3004:
3365 									bracketedHyperlinkMode = false;
3366 								break;
3367 								case 1047:
3368 								case 47:
3369 									returnToNormalScreen();
3370 								break;
3371 								case 25:
3372 									cursorShowing = false;
3373 								break;
3374 								default: unknownEscapeSequence(cast(string) esc);
3375 							}
3376 						}
3377 					break;
3378 					case 'X':
3379 						// erase characters
3380 						auto count = getArgs(1)[0];
3381 						TerminalCell plain;
3382 						plain.ch = ' ';
3383 						plain.attributes = currentAttributes;
3384 						foreach(cnt; 0 .. count) {
3385 							ASS[cursorY][cnt + cursorX] = plain;
3386 						}
3387 					break;
3388 					case 'S':
3389 						auto count = getArgs(1)[0];
3390 						// scroll up
3391 						scrollUp(count);
3392 					break;
3393 					case 'T':
3394 						auto count = getArgs(1)[0];
3395 						// scroll down
3396 						scrollDown(count);
3397 					break;
3398 					case 'P':
3399 						auto count = getArgs(1)[0];
3400 						// delete characters
3401 
3402 						foreach(cnt; 0 .. count) {
3403 							for(int i = cursorX; i < this.screenWidth-1; i++) {
3404 								if(ASS[cursorY][i].selected)
3405 									clearSelection();
3406 								ASS[cursorY][i] = ASS[cursorY][i + 1];
3407 								ASS[cursorY][i].invalidated = true;
3408 							}
3409 
3410 							if(ASS[cursorY][this.screenWidth - 1].selected)
3411 								clearSelection();
3412 							ASS[cursorY][this.screenWidth-1].ch = ' ';
3413 							ASS[cursorY][this.screenWidth-1].invalidated = true;
3414 						}
3415 
3416 						extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY);
3417 					break;
3418 					case '@':
3419 						// insert blank characters
3420 						auto count = getArgs(1)[0];
3421 						foreach(idx; 0 .. count) {
3422 							for(int i = this.screenWidth - 1; i > cursorX; i--) {
3423 								ASS[cursorY][i] = ASS[cursorY][i - 1];
3424 								ASS[cursorY][i].invalidated = true;
3425 							}
3426 							ASS[cursorY][cursorX].ch = ' ';
3427 							ASS[cursorY][cursorX].invalidated = true;
3428 						}
3429 
3430 						extendInvalidatedRange(cursorX, cursorY, this.screenWidth, cursorY);
3431 					break;
3432 					case 'c':
3433 						// send device attributes
3434 						// FIXME: what am i supposed to do here?
3435 						//sendToApplication("\033[>0;138;0c");
3436 						//sendToApplication("\033[?62;");
3437 						sendToApplication(terminalIdCode);
3438 					break;
3439 					default:
3440 						// [42\esc] seems to have gotten here once somehow
3441 						// also [24\esc]
3442 						unknownEscapeSequence("" ~ cast(string) esc);
3443 				}
3444 			} else {
3445 				unknownEscapeSequence(cast(string) esc);
3446 			}
3447 		}
3448 	}
3449 }
3450 
3451 // These match the numbers in terminal.d, so you can just cast it back and forth
3452 // and the names match simpledisplay.d so you can convert that automatically too
3453 enum TerminalKey : int {
3454 	Escape = 0x1b + 0xF0000, /// .
3455 	F1 = 0x70 + 0xF0000, /// .
3456 	F2 = 0x71 + 0xF0000, /// .
3457 	F3 = 0x72 + 0xF0000, /// .
3458 	F4 = 0x73 + 0xF0000, /// .
3459 	F5 = 0x74 + 0xF0000, /// .
3460 	F6 = 0x75 + 0xF0000, /// .
3461 	F7 = 0x76 + 0xF0000, /// .
3462 	F8 = 0x77 + 0xF0000, /// .
3463 	F9 = 0x78 + 0xF0000, /// .
3464 	F10 = 0x79 + 0xF0000, /// .
3465 	F11 = 0x7A + 0xF0000, /// .
3466 	F12 = 0x7B + 0xF0000, /// .
3467 	Left = 0x25 + 0xF0000, /// .
3468 	Right = 0x27 + 0xF0000, /// .
3469 	Up = 0x26 + 0xF0000, /// .
3470 	Down = 0x28 + 0xF0000, /// .
3471 	Insert = 0x2d + 0xF0000, /// .
3472 	Delete = 0x2e + 0xF0000, /// .
3473 	Home = 0x24 + 0xF0000, /// .
3474 	End = 0x23 + 0xF0000, /// .
3475 	PageUp = 0x21 + 0xF0000, /// .
3476 	PageDown = 0x22 + 0xF0000, /// .
3477 	ScrollLock = 0x91 + 0xF0000,
3478 }
3479 
3480 /* These match simpledisplay.d which match terminal.d, so you can just cast them */
3481 
3482 enum MouseEventType : int {
3483 	motion = 0,
3484 	buttonPressed = 1,
3485 	buttonReleased = 2,
3486 }
3487 
3488 enum MouseButton : int {
3489 	// these names assume a right-handed mouse
3490 	left = 1,
3491 	right = 2,
3492 	middle = 4,
3493 	wheelUp = 8,
3494 	wheelDown = 16,
3495 }
3496 
3497 
3498 
3499 /*
3500 mixin template ImageSupport() {
3501 	import arsd.png;
3502 	import arsd.bmp;
3503 }
3504 */
3505 
3506 
3507 /* helper functions that are generally useful but not necessarily required */
3508 
3509 version(use_libssh2) {
3510 	import arsd.libssh2;
3511 	void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) {
3512 
3513 	int tries = 0;
3514 	try_again:
3515 	try {
3516 		import std.socket;
3517 
3518 		if(libssh2_init(0))
3519 			throw new Exception("libssh2_init");
3520 		scope(exit)
3521 			libssh2_exit();
3522 
3523 		auto socket = new Socket(AddressFamily.INET, SocketType.STREAM);
3524 		socket.connect(new InternetAddress(host, port));
3525 		scope(exit) socket.close();
3526 
3527 		auto session = libssh2_session_init_ex(null, null, null, null);
3528 		if(session is null) throw new Exception("init session");
3529 		scope(exit)
3530 			libssh2_session_disconnect_ex(session, 0, "normal", "EN");
3531 
3532 		libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1);
3533 
3534 		if(libssh2_session_handshake(session, socket.handle))
3535 			throw new Exception("handshake");
3536 
3537 		auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
3538 		if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint)
3539 			throw new Exception("fingerprint");
3540 
3541 		import std.string : toStringz;
3542 		if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
3543 			throw new Exception("auth");
3544 
3545 
3546 		auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0);
3547 
3548 		if(channel is null)
3549 			throw new Exception("channel open");
3550 
3551 		scope(exit)
3552 			libssh2_channel_free(channel);
3553 
3554 		// libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length);
3555 
3556 		if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0))
3557 			throw new Exception("pty");
3558 
3559 		if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0))
3560 			throw new Exception("process_startup");
3561 
3562 		libssh2_keepalive_config(session, 0, 60);
3563 		libssh2_session_set_blocking(session, 0);
3564 
3565 		masterFunc(socket, session, channel);
3566 	} catch(Exception e) {
3567 		if(e.msg == "handshake") {
3568 			tries++;
3569 			import core.thread;
3570 			Thread.sleep(200.msecs);
3571 			if(tries < 10)
3572 				goto try_again;
3573 		}
3574 
3575 		throw e;
3576 	}
3577 	}
3578 
3579 } else
3580 version(Posix) {
3581 	extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp);
3582 	pragma(lib, "util");
3583 
3584 	/// this is good
3585 	void startChild(alias masterFunc)(string program, string[] args) {
3586 		import core.sys.posix.termios;
3587 		import core.sys.posix.signal;
3588 		import core.sys.posix.sys.wait;
3589 		__gshared static int childrenAlive = 0;
3590 		extern(C) nothrow static @nogc
3591 		void childdead(int) {
3592 			childrenAlive--;
3593 
3594 			wait(null);
3595 
3596 			version(with_eventloop)
3597 			try {
3598 				import arsd.eventloop;
3599 				if(childrenAlive <= 0)
3600 					exit();
3601 			} catch(Exception e){}
3602 		}
3603 
3604 		signal(SIGCHLD, &childdead);
3605 
3606 		int master;
3607 		int pid = forkpty(&master, null, null, null);
3608 		if(pid == -1)
3609 			throw new Exception("forkpty");
3610 		if(pid == 0) {
3611 			import std.process;
3612 			environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes
3613 			environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions
3614 
3615 			import std.string;
3616 			if(environment["LANG"].indexOf("UTF-8") == -1)
3617 				environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?)
3618 
3619 			import core.sys.posix.unistd;
3620 
3621 			import core.stdc.stdlib;
3622 			char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1));
3623 			if(argv is null) throw new Exception("malloc");
3624 			foreach(i, arg; args) {
3625 				argv[i] = cast(char*) malloc(arg.length + 1);
3626 				if(argv[i] is null) throw new Exception("malloc");
3627 				argv[i][0 .. arg.length] = arg[];
3628 				argv[i][arg.length] = 0;
3629 			}
3630 
3631 			argv[args.length] = null;
3632 
3633 			termios info;
3634 			ubyte[128] hack; // jic that druntime definition is still wrong
3635 			tcgetattr(master, &info);
3636 			info.c_cc[VERASE] = '\b';
3637 			tcsetattr(master, TCSANOW, &info);
3638 
3639 			core.sys.posix.unistd.execv(argv[0], argv);
3640 		} else {
3641 			childrenAlive = 1;
3642 			masterFunc(master);
3643 		}
3644 	}
3645 } else
3646 version(Windows) {
3647 	import core.sys.windows.windows;
3648 
3649 	version(winpty) {
3650 		alias HPCON = HANDLE;
3651 		extern(Windows)
3652 			HRESULT function(HPCON, COORD) ResizePseudoConsole;
3653 		extern(Windows)
3654 			HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole;
3655 		extern(Windows)
3656 			void function(HPCON) ClosePseudoConsole;
3657 	}
3658 
3659 	extern(Windows)
3660 		BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD);
3661 	extern(Windows)
3662 		BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL);
3663 	extern(Windows)
3664 		private BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*);
3665 	extern(Windows)
3666 		BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
3667 
3668 	extern(Windows)
3669 		BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM);
3670 	extern(Windows)
3671 		BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags);
3672 	extern(Windows)
3673 		BOOL SetHandleInformation(HANDLE, DWORD, DWORD);
3674 	extern(Windows)
3675 	HANDLE CreateNamedPipeA(
3676 		const(char)* lpName,
3677 		DWORD dwOpenMode,
3678 		DWORD dwPipeMode,
3679 		DWORD nMaxInstances,
3680 		DWORD nOutBufferSize,
3681 		DWORD nInBufferSize,
3682 		DWORD nDefaultTimeOut,
3683 		LPSECURITY_ATTRIBUTES lpSecurityAttributes
3684 	);
3685 	extern(Windows)
3686 	BOOL UnregisterWait(HANDLE);
3687 
3688 	struct STARTUPINFOEXA {
3689 		STARTUPINFOA StartupInfo;
3690 		void* lpAttributeList;
3691 	}
3692 
3693 	enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
3694 	enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
3695 
3696 	extern(Windows)
3697 	BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T);
3698 	extern(Windows)
3699 	BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T);
3700 
3701 	__gshared HANDLE waitHandle;
3702 	__gshared bool childDead;
3703 	extern(Windows)
3704 	void childCallback(void* tidp, bool) {
3705 		auto tid = cast(DWORD) tidp;
3706 		UnregisterWait(waitHandle);
3707 
3708 		PostThreadMessageA(tid, WM_QUIT, 0, 0);
3709 		childDead = true;
3710 		//stupidThreadAlive = false;
3711 	}
3712 
3713 
3714 
3715 	extern(Windows)
3716 	void SetLastError(DWORD);
3717 
3718 	/// this is good. best to call it with plink.exe so it can talk to unix
3719 	/// note that plink asks for the password out of band, so it won't actually work like that.
3720 	/// thus specify the password on the command line or better yet, use a private key file
3721 	/// e.g.
3722 	/// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\"");
3723 	void startChild(alias masterFunc)(string program, string commandLine) {
3724 		import core.sys.windows.windows;
3725 
3726 		import arsd.core : MyCreatePipeEx;
3727 
3728 		import std.conv;
3729 
3730 		SECURITY_ATTRIBUTES saAttr;
3731 		saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
3732 		saAttr.bInheritHandle = true;
3733 		saAttr.lpSecurityDescriptor = null;
3734 
3735 		HANDLE inreadPipe;
3736 		HANDLE inwritePipe;
3737 		if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0)
3738 			throw new Exception("CreatePipe");
3739 		if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
3740 			throw new Exception("SetHandleInformation");
3741 		HANDLE outreadPipe;
3742 		HANDLE outwritePipe;
3743 
3744 		version(winpty)
3745 			auto flags = 0;
3746 		else
3747 			auto flags = FILE_FLAG_OVERLAPPED;
3748 
3749 		if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0)
3750 			throw new Exception("CreatePipe");
3751 		if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
3752 			throw new Exception("SetHandleInformation");
3753 
3754 		version(winpty) {
3755 
3756 			auto lib = LoadLibrary("kernel32.dll");
3757 			if(lib is null) throw new Exception("holy wtf batman");
3758 			scope(exit) FreeLibrary(lib);
3759 
3760 			CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole");
3761 			ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole");
3762 			ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole");
3763 
3764 			if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null)
3765 				throw new Exception("Windows pseudo console not available on this version");
3766 
3767 			initPipeHack(outreadPipe);
3768 
3769 			HPCON hpc;
3770 			auto result = CreatePseudoConsole(
3771 				COORD(80, 24),
3772 				inreadPipe,
3773 				outwritePipe,
3774 				0, // flags
3775 				&hpc
3776 			);
3777 
3778 			assert(result == S_OK);
3779 
3780 			scope(exit)
3781 				ClosePseudoConsole(hpc);
3782 		}
3783 
3784 		STARTUPINFOEXA siex;
3785 		siex.StartupInfo.cb = siex.sizeof;
3786 
3787 		version(winpty) {
3788 			size_t size;
3789 			InitializeProcThreadAttributeList(null, 1, 0, &size);
3790 			ubyte[] wtf = new ubyte[](size);
3791 			siex.lpAttributeList = wtf.ptr;
3792 			InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size);
3793 			UpdateProcThreadAttribute(
3794 				siex.lpAttributeList,
3795 				0,
3796 				PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
3797 				hpc,
3798 				hpc.sizeof,
3799 				null,
3800 				null
3801 			);
3802 		} {//else {
3803 			siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
3804 			siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe;
3805 			siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe;
3806 			siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe;
3807 		}
3808 
3809 		PROCESS_INFORMATION pi;
3810 		import std.conv;
3811 
3812 		if(commandLine.length > 255)
3813 			throw new Exception("command line too long");
3814 		char[256] cmdLine;
3815 		cmdLine[0 .. commandLine.length] = commandLine[];
3816 		cmdLine[commandLine.length] = 0;
3817 		import std.string;
3818 		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)
3819 			throw new Exception("CreateProcess " ~ to!string(GetLastError()));
3820 
3821 		if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0)
3822 			throw new Exception("RegisterWaitForSingleObject");
3823 
3824 		version(winpty)
3825 			masterFunc(hpc, inwritePipe, outreadPipe);
3826 		else
3827 			masterFunc(inwritePipe, outreadPipe);
3828 
3829 		//stupidThreadAlive = false;
3830 
3831 		//term.stupidThread.join();
3832 
3833 		/* // FIXME: we should close but only if we're legit done
3834 		// masterFunc typically runs an event loop but it might not.
3835 		CloseHandle(inwritePipe);
3836 		CloseHandle(outreadPipe);
3837 
3838 		CloseHandle(pi.hThread);
3839 		CloseHandle(pi.hProcess);
3840 		*/
3841 	}
3842 }
3843 
3844 /// Implementation of TerminalEmulator's abstract functions that forward them to output
3845 mixin template ForwardVirtuals(alias writer) {
3846 	static import arsd.color;
3847 
3848 	protected override void changeCursorStyle(CursorStyle style) {
3849 		// FIXME: this should probably just import utility
3850 		final switch(style) {
3851 			case TerminalEmulator.CursorStyle.block:
3852 				writer("\033[2 q");
3853 			break;
3854 			case TerminalEmulator.CursorStyle.underline:
3855 				writer("\033[4 q");
3856 			break;
3857 			case TerminalEmulator.CursorStyle.bar:
3858 				writer("\033[6 q");
3859 			break;
3860 		}
3861 	}
3862 
3863 	protected override void changeWindowTitle(string t) {
3864 		import std.process;
3865 		if(t.length && environment["TERM"] != "linux")
3866 			writer("\033]0;"~t~"\007");
3867 	}
3868 
3869 	protected override void changeWindowIcon(arsd.color.IndexedImage t) {
3870 		if(t !is null) {
3871 			// forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux
3872 			import std.process;
3873 			if(environment["TERM"] != "linux")
3874 				writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007");
3875 		}
3876 	}
3877 
3878 	protected override void changeIconTitle(string) {} // FIXME
3879 	protected override void changeTextAttributes(TextAttributes) {} // FIXME
3880 	protected override void soundBell() {
3881 		writer("\007");
3882 	}
3883 	protected override void demandAttention() {
3884 		import std.process;
3885 		if(environment["TERM"] != "linux")
3886 			writer("\033]5001;1\007"); // the 1 there means true but is currently ignored
3887 	}
3888 	protected override void copyToClipboard(string text) {
3889 		// this is xterm compatible, though xterm rarely implements it
3890 		import std.base64;
3891 				// idk why the cast is needed here
3892 		writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007");
3893 	}
3894 	protected override void pasteFromClipboard(void delegate(in char[]) dg) {
3895 		// this is a slight extension. xterm invented the string - it means request the primary selection -
3896 		// but it generally doesn't actually get a reply. so i'm using it to request the primary which will be
3897 		// sent as a pasted strong.
3898 		// (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.)
3899 		writer("\033]52;c;?\007");
3900 	}
3901 	protected override void copyToPrimary(string text) {
3902 		import std.base64;
3903 		writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007");
3904 	}
3905 	protected override void pasteFromPrimary(void delegate(in char[]) dg) {
3906 		writer("\033]52;p;?\007");
3907 	}
3908 
3909 }
3910 
3911 /// you can pass this as PtySupport's arguments when you just don't care
3912 final void doNothing() {}
3913 
3914 version(winpty) {
3915 		__gshared static HANDLE inputEvent;
3916 		__gshared static HANDLE magicEvent;
3917 		__gshared static ubyte[] helperBuffer;
3918 		__gshared static HANDLE helperThread;
3919 
3920 		static void initPipeHack(void* ptr) {
3921 			inputEvent = CreateEvent(null, false, false, null);
3922 			assert(inputEvent !is null);
3923 			magicEvent = CreateEvent(null, false, true, null);
3924 			assert(magicEvent !is null);
3925 
3926 			helperThread = CreateThread(
3927 				null,
3928 				0,
3929 				&actuallyRead,
3930 				ptr,
3931 				0,
3932 				null
3933 			);
3934 
3935 			assert(helperThread !is null);
3936 		}
3937 
3938 		extern(Windows) static
3939 		uint actuallyRead(void* ptr) {
3940 			ubyte[4096] buffer;
3941 			DWORD got;
3942 			while(true) {
3943 				// wait for the other thread to tell us they
3944 				// are done...
3945 				WaitForSingleObject(magicEvent, INFINITE);
3946 				auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null);
3947 				helperBuffer = buffer[0 .. got];
3948 				// tells the other thread it is allowed to read
3949 				// readyToReadPty
3950 				SetEvent(inputEvent);
3951 			}
3952 			assert(0);
3953 		}
3954 
3955 
3956 }
3957 
3958 /// You must implement a function called redraw() and initialize the members in your constructor
3959 mixin template PtySupport(alias resizeHelper) {
3960 	// Initialize these!
3961 
3962 	final void redraw_() {
3963 		if(invalidateAll) {
3964 			extendInvalidatedRange(0, 0, this.screenWidth, this.screenHeight);
3965 			if(alternateScreenActive)
3966 				foreach(ref t; alternateScreen)
3967 					t.invalidated = true;
3968 			else
3969 				foreach(ref t; normalScreen)
3970 					t.invalidated = true;
3971 			invalidateAll = false;
3972 		}
3973 		redraw();
3974 		//soundBell();
3975 	}
3976 
3977 	version(use_libssh2) {
3978 		import arsd.libssh2;
3979 		LIBSSH2_CHANNEL* sshChannel;
3980 	} else version(Windows) {
3981 		import core.sys.windows.windows;
3982 		HANDLE stdin;
3983 		HANDLE stdout;
3984 	} else version(Posix) {
3985 		int master;
3986 	}
3987 
3988 	version(use_libssh2) { }
3989 	else version(Posix) {
3990 		int previousProcess = 0;
3991 		int activeProcess = 0;
3992 		int activeProcessWhenResized = 0;
3993 		bool resizedRecently;
3994 
3995 		/*
3996 			so, this isn't perfect, but it is meant to send the resize signal to an existing process
3997 			when it isn't in the front when you resize.
3998 
3999 			For example, open vim and resize. Then exit vim. We want bash to be updated.
4000 
4001 			But also don't want to do too many spurious signals.
4002 
4003 			It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but
4004 			the outer bash won't see it. I guess I need some kind of process stack.
4005 
4006 			but it is okish.
4007 		*/
4008 		override void outputOccurred() {
4009 			import core.sys.posix.unistd;
4010 			auto pgrp = tcgetpgrp(master);
4011 			if(pgrp != -1) {
4012 				if(pgrp != activeProcess) {
4013 					auto previousProcessAtStartup = previousProcess;
4014 
4015 					previousProcess = activeProcess;
4016 					activeProcess = pgrp;
4017 
4018 					if(resizedRecently) {
4019 						if(activeProcess != activeProcessWhenResized) {
4020 							resizedRecently = false;
4021 
4022 							if(activeProcess == previousProcessAtStartup) {
4023 								//import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight);
4024 
4025 								import core.sys.posix.signal;
4026 								kill(-activeProcess, 28 /* 28 == SIGWINCH*/);
4027 							}
4028 						}
4029 					}
4030 				}
4031 			}
4032 
4033 
4034 			super.outputOccurred();
4035 		}
4036 		//return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline");
4037 	}
4038 
4039 
4040 	override void resizeTerminal(int w, int h) {
4041 		version(Posix) {
4042 			activeProcessWhenResized = activeProcess;
4043 			resizedRecently = true;
4044 		}
4045 
4046 		resizeHelper();
4047 
4048 		super.resizeTerminal(w, h);
4049 
4050 		version(use_libssh2) {
4051 			libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0);
4052 		} else version(Posix) {
4053 			import core.sys.posix.sys.ioctl;
4054 			winsize win;
4055 			win.ws_col = cast(ushort) w;
4056 			win.ws_row = cast(ushort) h;
4057 
4058 			ioctl(master, TIOCSWINSZ, &win);
4059 		} else version(Windows) {
4060 			version(winpty) {
4061 				COORD coord;
4062 				coord.X = cast(ushort) w;
4063 				coord.Y = cast(ushort) h;
4064 				ResizePseudoConsole(hpc, coord);
4065 			} else {
4066 				sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]);
4067 			}
4068 		} else static assert(0);
4069 	}
4070 
4071 	protected override void sendToApplication(scope const(void)[] data) {
4072 		version(use_libssh2) {
4073 			while(data.length) {
4074 				auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length);
4075 				if(sent < 0)
4076 					throw new Exception("libssh2_channel_write_ex");
4077 				data = data[sent .. $];
4078 			}
4079 		} else version(Windows) {
4080 			import std.conv;
4081 			uint written;
4082 			if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0)
4083 				throw new Exception("WriteFile " ~ to!string(GetLastError()));
4084 		} else version(Posix) {
4085 			import core.sys.posix.unistd;
4086 			int frozen;
4087 			while(data.length) {
4088 				enum MAX_SEND = 1024 * 20;
4089 				auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length);
4090 				//import std.stdio; writeln("ROFL ", sent, " ", data.length);
4091 
4092 				import core.stdc.errno;
4093 				if(sent == -1 && errno == 11) {
4094 					import core.thread;
4095 					if(frozen == 50)
4096 						throw new Exception("write froze up");
4097 					frozen++;
4098 					Thread.sleep(10.msecs);
4099 					//import std.stdio; writeln("lol");
4100 					continue; // just try again
4101 				}
4102 
4103 				frozen = 0;
4104 
4105 				import std.conv;
4106 				if(sent < 0)
4107 					throw new Exception("write " ~ to!string(errno));
4108 
4109 				data = data[sent .. $];
4110 			}
4111 		} else static assert(0);
4112 	}
4113 
4114 	version(use_libssh2) {
4115 		int readyToRead(int fd) {
4116 			int count = 0; // if too much stuff comes at once, we still want to be responsive
4117 			while(true) {
4118 				ubyte[4096] buffer;
4119 				auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length);
4120 				if(got == LIBSSH2_ERROR_EAGAIN)
4121 					break; // got it all for now
4122 				if(got < 0)
4123 					throw new Exception("libssh2_channel_read_ex");
4124 				if(got == 0)
4125 					break; // NOT an error!
4126 
4127 				super.sendRawInput(buffer[0 .. got]);
4128 				count++;
4129 
4130 				if(count == 5) {
4131 					count = 0;
4132 					redraw_();
4133 					justRead();
4134 				}
4135 			}
4136 
4137 			if(libssh2_channel_eof(sshChannel)) {
4138 				libssh2_channel_close(sshChannel);
4139 				libssh2_channel_wait_closed(sshChannel);
4140 
4141 				return 1;
4142 			}
4143 
4144 			if(count != 0) {
4145 				redraw_();
4146 				justRead();
4147 			}
4148 			return 0;
4149 		}
4150 	} else version(winpty) {
4151 		void readyToReadPty() {
4152 			super.sendRawInput(helperBuffer);
4153 			SetEvent(magicEvent); // tell the other thread we have finished
4154 			redraw_();
4155 			justRead();
4156 		}
4157 	} else version(Windows) {
4158 		OVERLAPPED* overlapped;
4159 		bool overlappedBufferLocked;
4160 		ubyte[4096] overlappedBuffer;
4161 		extern(Windows)
4162 		static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) {
4163 			assert(overlapped !is null);
4164 			typeof(this) w = cast(typeof(this)) overlapped.hEvent;
4165 
4166 			if(numberOfBytes) {
4167 				w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]);
4168 				w.redraw_();
4169 			}
4170 			import std.conv;
4171 
4172 			if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) {
4173 				if(GetLastError() == 997)
4174 				{ } // there's pending i/o, let's just ignore for now and it should tell us later that it completed
4175 				else
4176 				throw new Exception("ReadFileEx " ~ to!string(GetLastError()));
4177 			} else {
4178 			}
4179 
4180 			w.justRead();
4181 		}
4182 	} else version(Posix) {
4183 		void readyToRead(int fd) {
4184 			import core.sys.posix.unistd;
4185 			ubyte[4096] buffer;
4186 
4187 			// the count is to limit how long we spend in this loop
4188 			// when it runs out, it goes back to the main event loop
4189 			// for a while (btw use level triggered events so the remaining
4190 			// data continues to get processed!) giving a chance to redraw
4191 			// and process user input periodically during insanely long and
4192 			// rapid output.
4193 			int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests
4194 
4195 			version(arsd_te_conservative_draws)
4196 				cnt = 400;
4197 
4198 			// FIXME: if connected by ssh, up the count so we don't redraw as frequently.
4199 			// it'd save bandwidth
4200 
4201 			while(--cnt) {
4202 				auto len = read(fd, buffer.ptr, 4096);
4203 				if(len < 0) {
4204 					import core.stdc.errno;
4205 					if(errno == EAGAIN || errno == EWOULDBLOCK) {
4206 						break; // we got it all
4207 					} else {
4208 						//import std.conv;
4209 						//throw new Exception("read failed " ~ to!string(errno));
4210 						return;
4211 					}
4212 				}
4213 
4214 				if(len == 0) {
4215 					close(fd);
4216 					requestExit();
4217 					break;
4218 				}
4219 
4220 				auto data = buffer[0 .. len];
4221 
4222 				if(debugMode) {
4223 					import std.array; import std.stdio; writeln("GOT ", data, "\nOR ",
4224 						replace(cast(string) data, "\033", "\\")
4225 						.replace("\010", "^H")
4226 						.replace("\r", "^M")
4227 						.replace("\n", "^J")
4228 						);
4229 				}
4230 				super.sendRawInput(data);
4231 			}
4232 
4233 			outputOccurred();
4234 
4235 			redraw_();
4236 
4237 			// HACK: I don't even know why this works, but with this
4238 			// sleep in place, it gives X events from that socket a
4239 			// chance to be processed. It can add a few seconds to a huge
4240 			// output (like `find /usr`), but meh, that's worth it to me
4241 			// to have a chance to ctrl+c.
4242 			import core.thread;
4243 			Thread.sleep(dur!"msecs"(5));
4244 
4245 			justRead();
4246 		}
4247 	}
4248 }
4249 
4250 mixin template SdpyImageSupport() {
4251 	class NonCharacterData_Image : NonCharacterData {
4252 		Image data;
4253 		int imageOffsetX;
4254 		int imageOffsetY;
4255 
4256 		this(Image data, int x, int y) {
4257 			this.data = data;
4258 			this.imageOffsetX = x;
4259 			this.imageOffsetY = y;
4260 		}
4261 	}
4262 
4263 	version(TerminalDirectToEmulator)
4264 	class NonCharacterData_Widget : NonCharacterData {
4265 		this(void* data, size_t idx, int width, int height) {
4266 			this.window = cast(SimpleWindow) data;
4267 			this.idx = idx;
4268 			this.width = width;
4269 			this.height = height;
4270 		}
4271 
4272 		void position(int posx, int posy, int width, int height) {
4273 			if(posx == this.posx && posy == this.posy && width == this.pixelWidth && height == this.pixelHeight)
4274 				return;
4275 			this.posx = posx;
4276 			this.posy = posy;
4277 			this.pixelWidth = width;
4278 			this.pixelHeight = height;
4279 
4280 			window.moveResize(posx, posy, width, height);
4281 			import std.stdio; writeln(posx, " ", posy, " ", width, " ", height);
4282 
4283 			auto painter = this.window.draw;
4284 			painter.outlineColor = Color.red;
4285 			painter.fillColor = Color.green;
4286 			painter.drawRectangle(Point(0, 0), width, height);
4287 
4288 
4289 		}
4290 
4291 		SimpleWindow window;
4292 		size_t idx;
4293 		int width;
4294 		int height;
4295 
4296 		int posx;
4297 		int posy;
4298 		int pixelWidth;
4299 		int pixelHeight;
4300 	}
4301 
4302 	private struct CachedImage {
4303 		ulong hash;
4304 		BinaryDataTerminalRepresentation bui;
4305 		int timesSeen;
4306 		import core.time;
4307 		MonoTime lastUsed;
4308 	}
4309 	private CachedImage[] imageCache;
4310 	private CachedImage* findInCache(ulong hash) {
4311 		if(hash == 0)
4312 			return null;
4313 
4314 		/*
4315 		import std.stdio;
4316 		writeln("***");
4317 		foreach(cache; imageCache) {
4318 			writeln(cache.hash, " ", cache.timesSeen, " ", cache.lastUsed);
4319 		}
4320 		*/
4321 
4322 		foreach(ref i; imageCache)
4323 			if(i.hash == hash) {
4324 				import core.time;
4325 				i.lastUsed = MonoTime.currTime;
4326 				i.timesSeen++;
4327 				return &i;
4328 			}
4329 		return null;
4330 	}
4331 	private BinaryDataTerminalRepresentation addImageCache(ulong hash, BinaryDataTerminalRepresentation bui) {
4332 		import core.time;
4333 		if(imageCache.length == 0)
4334 			imageCache.length = 8;
4335 
4336 		auto now = MonoTime.currTime;
4337 
4338 		size_t oldestIndex;
4339 		MonoTime oldestTime = now;
4340 
4341 		size_t leastUsedIndex;
4342 		int leastUsedCount = int.max;
4343 		foreach(idx, ref cached; imageCache) {
4344 			if(cached.hash == 0) {
4345 				cached.hash = hash;
4346 				cached.bui = bui;
4347 				cached.timesSeen = 1;
4348 				cached.lastUsed = now;
4349 
4350 				return bui;
4351 			} else {
4352 				if(cached.timesSeen < leastUsedCount) {
4353 					leastUsedCount = cached.timesSeen;
4354 					leastUsedIndex = idx;
4355 				}
4356 				if(cached.lastUsed < oldestTime) {
4357 					oldestTime = cached.lastUsed;
4358 					oldestIndex = idx;
4359 				}
4360 			}
4361 		}
4362 
4363 		// need to overwrite one of the cached items, I'll just use the oldest one here
4364 		// but maybe that could be smarter later
4365 
4366 		imageCache[oldestIndex].hash = hash;
4367 		imageCache[oldestIndex].bui = bui;
4368 		imageCache[oldestIndex].timesSeen = 1;
4369 		imageCache[oldestIndex].lastUsed = now;
4370 
4371 		return bui;
4372 	}
4373 
4374 	// It has a cache of the 8 most recently used items right now so if there's a loop of 9 you get pwned
4375 	// but still the cache does an ok job at helping things while balancing out the big memory consumption it
4376 	// could do if just left to grow and grow. i hope.
4377 	protected override BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[] binaryData) {
4378 
4379 		version(none) {
4380 		//version(TerminalDirectToEmulator)
4381 		//if(binaryData.length == size_t.sizeof + 10) {
4382 			//if((cast(uint[]) binaryData[0 .. 4])[0] == 0xdeadbeef && (cast(uint[]) binaryData[$-4 .. $])[0] == 0xabcdef32) {
4383 				//auto widthInCharacterCells = binaryData[4];
4384 				//auto heightInCharacterCells = binaryData[5];
4385 				//auto pointer = (cast(void*[]) binaryData[6 .. $-4])[0];
4386 
4387 				auto widthInCharacterCells = 30;
4388 				auto heightInCharacterCells = 20;
4389 				SimpleWindow pwin;
4390 				foreach(k, v; SimpleWindow.nativeMapping) {
4391 					if(v.type == WindowTypes.normal)
4392 					pwin = v;
4393 				}
4394 				auto pointer = cast(void*) (new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin));
4395 
4396 				BinaryDataTerminalRepresentation bi;
4397 				bi.width = widthInCharacterCells;
4398 				bi.height = heightInCharacterCells;
4399 				bi.representation.length = bi.width * bi.height;
4400 
4401 				foreach(idx, ref cell; bi.representation) {
4402 					cell.nonCharacterData = new NonCharacterData_Widget(pointer, idx, widthInCharacterCells, heightInCharacterCells);
4403 				}
4404 
4405 				return bi;
4406 			//}
4407 		}
4408 
4409 		import std.digest.md;
4410 
4411 		ulong hash = * (cast(ulong*) md5Of(binaryData).ptr);
4412 
4413 		if(auto cached = findInCache(hash))
4414 			return cached.bui;
4415 
4416 		TrueColorImage mi;
4417 
4418 		if(binaryData.length > 8 && binaryData[1] == 'P' && binaryData[2] == 'N' && binaryData[3] == 'G') {
4419 			import arsd.png;
4420 			mi = imageFromPng(readPng(binaryData)).getAsTrueColorImage();
4421 		} else if(binaryData.length > 8 && binaryData[0] == 'B' && binaryData[1] == 'M') {
4422 			import arsd.bmp;
4423 			mi = readBmp(binaryData).getAsTrueColorImage();
4424 		} else if(binaryData.length > 2 && binaryData[0] == 0xff && binaryData[1] == 0xd8) {
4425 			import arsd.jpeg;
4426 			mi = readJpegFromMemory(binaryData).getAsTrueColorImage();
4427 		} else if(binaryData.length > 2 && binaryData[0] == '<') {
4428 			import arsd.svg;
4429 			NSVG* image = nsvgParse(cast(const(char)[]) binaryData);
4430 			if(image is null)
4431 				return BinaryDataTerminalRepresentation();
4432 
4433 			int w = cast(int) image.width + 1;
4434 			int h = cast(int) image.height + 1;
4435 			NSVGrasterizer rast = nsvgCreateRasterizer();
4436 			mi = new TrueColorImage(w, h);
4437 			rasterize(rast, image, 0, 0, 1, mi.imageData.bytes.ptr, w, h, w*4);
4438 			image.kill();
4439 		} else {
4440 			return BinaryDataTerminalRepresentation();
4441 		}
4442 
4443 		BinaryDataTerminalRepresentation bi;
4444 		bi.width = mi.width / fontWidth + ((mi.width%fontWidth) ? 1 : 0);
4445 		bi.height = mi.height / fontHeight + ((mi.height%fontHeight) ? 1 : 0);
4446 
4447 		bi.representation.length = bi.width * bi.height;
4448 
4449 		Image data = Image.fromMemoryImage(mi);
4450 
4451 		int ix, iy;
4452 		foreach(ref cell; bi.representation) {
4453 			/*
4454 			Image data = new Image(fontWidth, fontHeight);
4455 			foreach(y; 0 .. fontHeight) {
4456 				foreach(x; 0 .. fontWidth) {
4457 					if(x + ix >= mi.width || y + iy >= mi.height) {
4458 						data.putPixel(x, y, defaultTextAttributes.background);
4459 						continue;
4460 					}
4461 					data.putPixel(x, y, mi.imageData.colors[(iy + y) * mi.width + (ix + x)]);
4462 				}
4463 			}
4464 			*/
4465 
4466 			cell.nonCharacterData = new NonCharacterData_Image(data, ix, iy);
4467 
4468 			ix += fontWidth;
4469 
4470 			if(ix >= mi.width) {
4471 				ix = 0;
4472 				iy += fontHeight;
4473 			}
4474 		}
4475 
4476 		return addImageCache(hash, bi);
4477 		//return bi;
4478 	}
4479 
4480 }
4481 
4482 // this assumes you have imported arsd.simpledisplay and/or arsd.minigui in the mixin scope
4483 mixin template SdpyDraw() {
4484 
4485 	// black bg, make the colors more visible
4486 	static Color contrastify(Color c) {
4487 		if(c == Color(0xcd, 0, 0))
4488 			return Color.fromHsl(0, 1.0, 0.75);
4489 		else if(c == Color(0, 0, 0xcd))
4490 			return Color.fromHsl(240, 1.0, 0.75);
4491 		else if(c == Color(229, 229, 229))
4492 			return Color(0x99, 0x99, 0x99);
4493 		else if(c == Color.black)
4494 			return Color(128, 128, 128);
4495 		else return c;
4496 	}
4497 
4498 	// white bg, make them more visible
4499 	static Color antiContrastify(Color c) {
4500 		if(c == Color(0xcd, 0xcd, 0))
4501 			return Color.fromHsl(60, 1.0, 0.25);
4502 		else if(c == Color(0, 0xcd, 0xcd))
4503 			return Color.fromHsl(180, 1.0, 0.25);
4504 		else if(c == Color(229, 229, 229))
4505 			return Color(0x99, 0x99, 0x99);
4506 		else if(c == Color.white)
4507 			return Color(128, 128, 128);
4508 		else return c;
4509 	}
4510 
4511 	struct SRectangle {
4512 		int left;
4513 		int top;
4514 		int right;
4515 		int bottom;
4516 	}
4517 
4518 	mixin SdpyImageSupport;
4519 
4520 	OperatingSystemFont font;
4521 	int fontWidth;
4522 	int fontHeight;
4523 
4524 	enum paddingLeft = 2;
4525 	enum paddingTop = 1;
4526 
4527 	void loadDefaultFont(int size = 14) {
4528 		static if(UsingSimpledisplayX11) {
4529 			font = new OperatingSystemFont("core:fixed", size, FontWeight.medium);
4530 			//font = new OperatingSystemFont("monospace", size, FontWeight.medium);
4531 			if(font.isNull) {
4532 				// didn't work, it is using a
4533 				// fallback, prolly fixed-13 is best
4534 				font = new OperatingSystemFont("core:fixed", 13, FontWeight.medium);
4535 			}
4536 		} else version(Windows) {
4537 			this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium);
4538 			if(!this.font.isNull && !this.font.isMonospace)
4539 				this.font.unload(); // non-monospace fonts are unusable here. This should never happen anyway though as Courier New comes with Windows
4540 		}
4541 
4542 		if(font.isNull) {
4543 			// no way to really tell... just guess so it doesn't crash but like eeek.
4544 			fontWidth = size / 2;
4545 			fontHeight = size;
4546 		} else {
4547 			fontWidth = font.averageWidth;
4548 			fontHeight = font.height;
4549 		}
4550 	}
4551 
4552 	bool lastDrawAlternativeScreen;
4553 	final SRectangle redrawPainter(T)(T painter, bool forceRedraw) {
4554 		SRectangle invalidated;
4555 
4556 		// FIXME: anything we can do to make this faster is good
4557 		// on both, the XImagePainter could use optimizations
4558 		// on both, drawing blocks would probably be good too - not just one cell at a time, find whole blocks of stuff
4559 		// on both it might also be good to keep scroll commands high level somehow. idk.
4560 
4561 		// FIXME on Windows it would definitely help a lot to do just one ExtTextOutW per line, if possible. the current code is brutally slow
4562 
4563 		// Or also see https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-polytextoutw
4564 
4565 		static if(is(T == WidgetPainter) || is(T == ScreenPainter)) {
4566 			if(font)
4567 				painter.setFont(font);
4568 		}
4569 
4570 
4571 		int posx = paddingLeft;
4572 		int posy = paddingTop;
4573 
4574 
4575 		char[512] bufferText;
4576 		bool hasBufferedInfo;
4577 		int bufferTextLength;
4578 		Color bufferForeground;
4579 		Color bufferBackground;
4580 		int bufferX = -1;
4581 		int bufferY = -1;
4582 		bool bufferReverse;
4583 		void flushBuffer() {
4584 			if(!hasBufferedInfo) {
4585 				return;
4586 			}
4587 
4588 			assert(posx - bufferX - 1 > 0);
4589 
4590 			painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
4591 			painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;
4592 
4593 			painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
4594 			painter.fillColor = Color.transparent;
4595 			// Hack for contrast!
4596 			if(bufferBackground == Color.black && !bufferReverse) {
4597 				// brighter than normal in some cases so i can read it easily
4598 				painter.outlineColor = contrastify(bufferForeground);
4599 			} else if(bufferBackground == Color.white && !bufferReverse) {
4600 				// darker than normal so i can read it
4601 				painter.outlineColor = antiContrastify(bufferForeground);
4602 			} else if(bufferForeground == bufferBackground) {
4603 				// color on itself, I want it visible too
4604 				auto hsl = toHsl(bufferForeground, true);
4605 				if(hsl[0] == 240) {
4606 					// blue is a bit special, it generally looks darker
4607 					// so we want to get very bright or very dark
4608 					if(hsl[2] < 0.7)
4609 						hsl[2] = 0.9;
4610 					else
4611 						hsl[2] = 0.1;
4612 				} else {
4613 					if(hsl[2] < 0.5)
4614 						hsl[2] += 0.5;
4615 					else
4616 						hsl[2] -= 0.5;
4617 				}
4618 				painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);
4619 			} else {
4620 				auto drawColor = bufferReverse ? bufferBackground : bufferForeground;
4621 				///+
4622 					// try to ensure legible contrast with any arbitrary combination
4623 				auto bgColor = bufferReverse ? bufferForeground : bufferBackground;
4624 				auto fghsl = toHsl(drawColor, true);
4625 				auto bghsl = toHsl(bgColor, true);
4626 
4627 				if(fghsl[2] > 0.5 && bghsl[2] > 0.5) {
4628 					// bright color on bright background
4629 					painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2);
4630 				} else if(fghsl[2] < 0.5 && bghsl[2] < 0.5) {
4631 					// dark color on dark background
4632 					if(fghsl[0] == 240 && bghsl[0] >= 60 && bghsl[0] <= 180)
4633 						// blue on green looks dark to the algorithm but isn't really
4634 						painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.2);
4635 					else
4636 						painter.outlineColor = fromHsl(fghsl[0], fghsl[1], 0.8);
4637 				} else {
4638 					// normal
4639 					painter.outlineColor = drawColor;
4640 				}
4641 				//+/
4642 
4643 				// normal
4644 				//painter.outlineColor = drawColor;
4645 			}
4646 
4647 			// FIXME: make sure this clips correctly
4648 			painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);
4649 
4650 			// import std.stdio; writeln(bufferX, " ", bufferY);
4651 
4652 			hasBufferedInfo = false;
4653 
4654 			bufferReverse = false;
4655 			bufferTextLength = 0;
4656 			bufferX = -1;
4657 			bufferY = -1;
4658 		}
4659 
4660 
4661 
4662 		int x;
4663 		auto bfr = alternateScreenActive ? alternateScreen : normalScreen;
4664 
4665 		version(invalidator_2) {
4666 		if(invalidatedMax > bfr.length)
4667 			invalidatedMax = cast(int) bfr.length;
4668 		if(invalidatedMin > invalidatedMax)
4669 			invalidatedMin = invalidatedMax;
4670 		if(invalidatedMin >= 0)
4671 			bfr = bfr[invalidatedMin .. invalidatedMax];
4672 
4673 		posx += (invalidatedMin % screenWidth) * fontWidth;
4674 		posy += (invalidatedMin / screenWidth) * fontHeight;
4675 
4676 		//import std.stdio; writeln(invalidatedMin, " to ", invalidatedMax, " ", posx, "x", posy);
4677 		invalidated.left = posx;
4678 		invalidated.top = posy;
4679 		invalidated.right = posx;
4680 		invalidated.top = posy;
4681 
4682 		clearInvalidatedRange();
4683 		}
4684 
4685 		foreach(idx, ref cell; bfr) {
4686 			if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
4687 				flushBuffer();
4688 				goto skipDrawing;
4689 			}
4690 			cell.invalidated = false;
4691 			version(none) if(bufferX == -1) { // why was this ever here?
4692 				bufferX = posx;
4693 				bufferY = posy;
4694 			}
4695 
4696 			if(!cell.hasNonCharacterData) {
4697 
4698 				invalidated.left = posx < invalidated.left ? posx : invalidated.left;
4699 				invalidated.top = posy < invalidated.top ? posy : invalidated.top;
4700 				int xmax = posx + fontWidth;
4701 				int ymax = posy + fontHeight;
4702 				invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
4703 				invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
4704 
4705 				// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
4706 				{
4707 
4708 					bool reverse = (cell.attributes.inverse != reverseVideo);
4709 					if(cell.selected)
4710 						reverse = !reverse;
4711 
4712 					version(with_24_bit_color) {
4713 						auto fgc = cell.attributes.foreground;
4714 						auto bgc = cell.attributes.background;
4715 
4716 						if(!(cell.attributes.foregroundIndex & 0xff00)) {
4717 							// this refers to a specific palette entry, which may change, so we should use that
4718 							fgc = palette[cell.attributes.foregroundIndex];
4719 						}
4720 						if(!(cell.attributes.backgroundIndex & 0xff00)) {
4721 							// this refers to a specific palette entry, which may change, so we should use that
4722 							bgc = palette[cell.attributes.backgroundIndex];
4723 						}
4724 
4725 					} else {
4726 						auto fgc = cell.attributes.foregroundIndex == 256 ? defaultForeground : palette[cell.attributes.foregroundIndex & 0xff];
4727 						auto bgc = cell.attributes.backgroundIndex == 256 ? defaultBackground : palette[cell.attributes.backgroundIndex & 0xff];
4728 					}
4729 
4730 					if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
4731 						flushBuffer();
4732 					bufferReverse = reverse;
4733 					bufferBackground = bgc;
4734 					bufferForeground = fgc;
4735 				}
4736 			}
4737 
4738 				if(!cell.hasNonCharacterData) {
4739 					char[4] str;
4740 					import std.utf;
4741 					// now that it is buffered, we do want to draw it this way...
4742 					//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
4743 						try {
4744 							auto stride = encode(str, cell.ch);
4745 							if(bufferTextLength + stride > bufferText.length)
4746 								flushBuffer();
4747 							bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
4748 							bufferTextLength += stride;
4749 
4750 							if(bufferX == -1) {
4751 								bufferX = posx;
4752 								bufferY = posy;
4753 							}
4754 							hasBufferedInfo = true;
4755 						} catch(Exception e) {
4756 							// import std.stdio; writeln(cast(uint) cell.ch, " :: ", e.msg);
4757 						}
4758 					//}
4759 				} else if(cell.nonCharacterData !is null) {
4760 					//import std.stdio; writeln(cast(void*) cell.nonCharacterData);
4761 					if(auto ncdi = cast(NonCharacterData_Image) cell.nonCharacterData) {
4762 						flushBuffer();
4763 						painter.outlineColor = defaultBackground;
4764 						painter.fillColor = defaultBackground;
4765 						painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
4766 						painter.drawImage(Point(posx, posy), ncdi.data, Point(ncdi.imageOffsetX, ncdi.imageOffsetY), fontWidth, fontHeight);
4767 					}
4768 					version(TerminalDirectToEmulator)
4769 					if(auto wdi = cast(NonCharacterData_Widget) cell.nonCharacterData) {
4770 						flushBuffer();
4771 						if(wdi.idx == 0) {
4772 							wdi.position(posx, posy, fontWidth * wdi.width, fontHeight * wdi.height);
4773 							/*
4774 							painter.outlineColor = defaultBackground;
4775 							painter.fillColor = defaultBackground;
4776 							painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
4777 							*/
4778 						}
4779 
4780 					}
4781 				}
4782 
4783 				if(!cell.hasNonCharacterData)
4784 				if(cell.attributes.underlined) {
4785 					// the posx adjustment is because the buffer assumes it is going
4786 					// to be flushed after advancing, but here, we're doing it mid-character
4787 					// FIXME: we should just underline the whole thing consecutively, with the buffer
4788 					posx += fontWidth;
4789 					flushBuffer();
4790 					posx -= fontWidth;
4791 					painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
4792 				}
4793 			skipDrawing:
4794 
4795 				posx += fontWidth;
4796 			x++;
4797 			if(x == screenWidth) {
4798 				flushBuffer();
4799 				x = 0;
4800 				posy += fontHeight;
4801 				posx = paddingLeft;
4802 			}
4803 		}
4804 
4805 		flushBuffer();
4806 
4807 		if(cursorShowing) {
4808 			painter.fillColor = cursorColor;
4809 			painter.outlineColor = cursorColor;
4810 			painter.rasterOp = RasterOp.xor;
4811 
4812 			posx = cursorPosition.x * fontWidth + paddingLeft;
4813 			posy = cursorPosition.y * fontHeight + paddingTop;
4814 
4815 			int cursorWidth = fontWidth;
4816 			int cursorHeight = fontHeight;
4817 
4818 			final switch(cursorStyle) {
4819 				case CursorStyle.block:
4820 					painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
4821 				break;
4822 				case CursorStyle.underline:
4823 					painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
4824 				break;
4825 				case CursorStyle.bar:
4826 					painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
4827 				break;
4828 			}
4829 			painter.rasterOp = RasterOp.normal;
4830 
4831 			painter.notifyCursorPosition(posx, posy, cursorWidth, cursorHeight);
4832 
4833 			// since the cursor draws over the cell, we need to make sure it is redrawn each time too
4834 			auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
4835 			if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
4836 				(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
4837 			}
4838 
4839 			extendInvalidatedRange(cursorX, cursorY, cursorX + 1, cursorY);
4840 
4841 			invalidated.left = posx < invalidated.left ? posx : invalidated.left;
4842 			invalidated.top = posy < invalidated.top ? posy : invalidated.top;
4843 			int xmax = posx + fontWidth;
4844 			int ymax = xmax + fontHeight;
4845 			invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
4846 			invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
4847 		}
4848 
4849 		lastDrawAlternativeScreen = alternateScreenActive;
4850 
4851 		return invalidated;
4852 	}
4853 }
4854 
4855 string encodeSmallTextImage(IndexedImage ii) {
4856 	char encodeNumeric(int c) {
4857 		if(c < 10)
4858 			return cast(char)(c + '0');
4859 		if(c < 10 + 26)
4860 			return cast(char)(c - 10 + 'a');
4861 		assert(0);
4862 	}
4863 
4864 	string s;
4865 	s ~= encodeNumeric(ii.width);
4866 	s ~= encodeNumeric(ii.height);
4867 
4868 	foreach(entry; ii.palette)
4869 		s ~= entry.toRgbaHexString();
4870 	s ~= "Z";
4871 
4872 	ubyte rleByte;
4873 	int rleCount;
4874 
4875 	void rleCommit() {
4876 		if(rleByte >= 26)
4877 			assert(0); // too many colors for us to handle
4878 		if(rleCount == 0)
4879 			goto finish;
4880 		if(rleCount == 1) {
4881 			s ~= rleByte + 'a';
4882 			goto finish;
4883 		}
4884 
4885 		import std.conv;
4886 		s ~= to!string(rleCount);
4887 		s ~= rleByte + 'a';
4888 
4889 		finish:
4890 			rleByte = 0;
4891 			rleCount = 0;
4892 	}
4893 
4894 	foreach(b; ii.data) {
4895 		if(b == rleByte)
4896 			rleCount++;
4897 		else {
4898 			rleCommit();
4899 			rleByte = b;
4900 			rleCount = 1;
4901 		}
4902 	}
4903 
4904 	rleCommit();
4905 
4906 	return s;
4907 }
4908 
4909 IndexedImage readSmallTextImage(scope const(char)[] arg) {
4910 	auto origArg = arg;
4911 	int width;
4912 	int height;
4913 
4914 	int readNumeric(char c) {
4915 		if(c >= '0' && c <= '9')
4916 			return c - '0';
4917 		if(c >= 'a' && c <= 'z')
4918 			return c - 'a' + 10;
4919 		return 0;
4920 	}
4921 
4922 	if(arg.length > 2) {
4923 		width = readNumeric(arg[0]);
4924 		height = readNumeric(arg[1]);
4925 		arg = arg[2 .. $];
4926 	}
4927 
4928 	import std.conv;
4929 	assert(width == 16, to!string(width));
4930 	assert(height == 16, to!string(width));
4931 
4932 	Color[] palette;
4933 	ubyte[256] data;
4934 	int didx = 0;
4935 	bool readingPalette = true;
4936 	outer: while(arg.length) {
4937 		if(readingPalette) {
4938 			if(arg[0] == 'Z') {
4939 				readingPalette = false;
4940 				arg = arg[1 .. $];
4941 				continue;
4942 			}
4943 			if(arg.length < 8)
4944 				break;
4945 			foreach(a; arg[0..8]) {
4946 				// if not strict hex, forget it
4947 				if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z')))
4948 					break outer;
4949 			}
4950 			palette ~= Color.fromString(arg[0 .. 8]);
4951 			arg = arg[8 .. $];
4952 		} else {
4953 			char[3] rleChars;
4954 			int rlePos;
4955 			while(arg.length && arg[0] >= '0' && arg[0] <= '9') {
4956 				rleChars[rlePos] = arg[0];
4957 				arg = arg[1 .. $];
4958 				rlePos++;
4959 				if(rlePos >= rleChars.length)
4960 					break;
4961 			}
4962 			if(arg.length == 0)
4963 				break;
4964 
4965 			int rle;
4966 			if(rlePos == 0)
4967 				rle = 1;
4968 			else {
4969 				// 100
4970 				// rleChars[0] == '1'
4971 				foreach(c; rleChars[0 .. rlePos]) {
4972 					rle *= 10;
4973 					rle += c - '0';
4974 				}
4975 			}
4976 
4977 			foreach(i; 0 .. rle) {
4978 				if(arg[0] >= 'a' && arg[0] <= 'z')
4979 					data[didx] = cast(ubyte)(arg[0] - 'a');
4980 
4981 				didx++;
4982 				if(didx == data.length)
4983 					break outer;
4984 			}
4985 
4986 			arg = arg[1 .. $];
4987 		}
4988 	}
4989 
4990 	// width, height, palette, data is set up now
4991 
4992 	if(palette.length) {
4993 		auto ii = new IndexedImage(width, height);
4994 		ii.palette = palette;
4995 		ii.data = data.dup;
4996 
4997 		return ii;
4998 	}// else assert(0, origArg);
4999 	return null;
5000 }
5001 
5002 
5003 // workaround dmd bug fixed in next release
5004 //static immutable Color[256] xtermPalette = [
5005 immutable(Color)[] xtermPalette() {
5006 
5007 	// This is an approximation too for a few entries, but a very close one.
5008 	Color xtermPaletteIndexToColor(int paletteIdx) {
5009 		Color color;
5010 		color.a = 255;
5011 
5012 		if(paletteIdx < 16) {
5013 			if(paletteIdx == 7)
5014 				return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see
5015 			else if(paletteIdx == 8)
5016 				return Color(0x80, 0x80, 0x80);
5017 
5018 			// real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see
5019 			color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
5020 			color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
5021 			color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
5022 
5023 		} else if(paletteIdx < 232) {
5024 			// color ramp, 6x6x6 cube
5025 			color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55);
5026 			color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55);
5027 			color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55);
5028 
5029 			if(color.r == 55) color.r = 0;
5030 			if(color.g == 55) color.g = 0;
5031 			if(color.b == 55) color.b = 0;
5032 		} else {
5033 			// greyscale ramp, from 0x8 to 0xee
5034 			color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10);
5035 			color.g = color.r;
5036 			color.b = color.g;
5037 		}
5038 
5039 		return color;
5040 	}
5041 
5042 	static immutable(Color)[] ret;
5043 	if(ret.length == 256)
5044 		return ret;
5045 
5046 	ret.reserve(256);
5047 	foreach(i; 0 .. 256)
5048 		ret ~= xtermPaletteIndexToColor(i);
5049 
5050 	return ret;
5051 }
5052 
5053 static shared immutable dchar[dchar] lineDrawingCharacterSet;
5054 shared static this() {
5055 	lineDrawingCharacterSet = [
5056 		'a' : ':',
5057 		'j' : '+',
5058 		'k' : '+',
5059 		'l' : '+',
5060 		'm' : '+',
5061 		'n' : '+',
5062 		'q' : '-',
5063 		't' : '+',
5064 		'u' : '+',
5065 		'v' : '+',
5066 		'w' : '+',
5067 		'x' : '|',
5068 	];
5069 
5070 	// this is what they SHOULD be but the font i use doesn't support all these
5071 	// the ascii fallback above looks pretty good anyway though.
5072 	version(none)
5073 	lineDrawingCharacterSet = [
5074 		'a' : '\u2592',
5075 		'j' : '\u2518',
5076 		'k' : '\u2510',
5077 		'l' : '\u250c',
5078 		'm' : '\u2514',
5079 		'n' : '\u253c',
5080 		'q' : '\u2500',
5081 		't' : '\u251c',
5082 		'u' : '\u2524',
5083 		'v' : '\u2534',
5084 		'w' : '\u252c',
5085 		'x' : '\u2502',
5086 	];
5087 }
5088 
5089 /+
5090 Copyright: Adam D. Ruppe, 2013 - 2020
5091 License:   [http://www.boost.org/LICENSE_1_0.txt|Boost Software License 1.0]
5092 Authors: Adam D. Ruppe
5093 +/