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