The OpenD Programming Language

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