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