/**
	FIXME: writing a line in color then a line in ordinary does something
	wrong.

	 # huh if i do underline then change color it undoes the underline

	FIXME: make shift+enter send something special to the application
		and shift+space, etc.
		identify itself somehow too for client extensions
		ctrl+space is supposed to send char 0.

	ctrl+click on url pattern could open in browser perhaps

	FIXME: scroll stuff should be higher level  in the implementation.
	so like scroll Rect, DirectionAndAmount

	There should be a redraw thing that is given batches of instructions
	in here that the other thing just implements.

	FIXME: the save stack stuff should do cursor style too

	This is an extendible unix terminal emulator and some helper functions to help actually implement one.

	You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.

	See nestedterminalemulator.d or main.d for how I did it.
*/
module arsd.terminalemulator;

import arsd.color;
import std.algorithm : max;

enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:";

/+
	The ;90 ones are my extensions.

	90 - clipboard extensions
	91 - image extensions
	92 - hyperlink extensions
+/
enum terminalIdCode = "\033[?64;1;2;6;9;15;16;17;18;21;22;28;90;91;92c";

interface NonCharacterData {
	//const(ubyte)[] serialize();
}

struct BinaryDataTerminalRepresentation {
	int width;
	int height;
	TerminalEmulator.TerminalCell[] representation;
}

// old name, don't use in new programs anymore.
deprecated alias BrokenUpImage = BinaryDataTerminalRepresentation;

struct CustomGlyph {
	TrueColorImage image;
	dchar substitute;
}

void unknownEscapeSequence(in char[] esc) {
	import std.file;
	version(Posix) {
		debug append("/tmp/arsd-te-bad-esc-sequences.txt", esc ~ "\n");
	} else {
		debug append("arsd-te-bad-esc-sequences.txt", esc ~ "\n");
	}
}

// This is used for the double-click word selection
bool isWordSeparator(dchar ch) {
	return ch == ' ' || ch == '"' || ch == '<' || ch == '>' || ch == '(' || ch == ')' || ch == ',';
}

TerminalEmulator.TerminalCell[] sliceTrailingWhitespace(TerminalEmulator.TerminalCell[] t) {
	size_t end = t.length;
	while(end >= 1) {
		if(t[end-1].hasNonCharacterData || t[end-1].ch != ' ')
			break;
		end--;
	}

	t = t[0 .. end];

	/*
	import std.stdio;
	foreach(ch; t)
		write(ch.ch);
	writeln("*");
	*/

	return t;
}

struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
	T[maxSize] bufferInternal;
	T[] buffer;
	size_t length;
	bool isNull = true;
	T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
	void opOpAssign(string op : "~")(in T rhs) {
		if(buffer is null) buffer = bufferInternal[];
		isNull = false;
		static if(allowGrowth) {
			if(this.length == buffer.length)
				buffer.length = buffer.length * 2;

			buffer[this.length++] = rhs;
		} else {
			if(this.length < buffer.length) // i am silently discarding more crap
				buffer[this.length++] = rhs;
		}
	}
	void opOpAssign(string op : "~")(in T[] rhs) {
		if(buffer is null) buffer = bufferInternal[];
		isNull = false;
		buffer[this.length .. this.length + rhs.length] = rhs[];
		this.length += rhs.length;
	}
	void opAssign(in T[] rhs) {
		isNull = rhs is null;
		if(buffer is null) buffer = bufferInternal[];
		buffer[0 .. rhs.length] = rhs[];
		this.length = rhs.length;
	}
	void opAssign(typeof(null)) {
		isNull = true;
		length = 0;
	}
	T opIndex(size_t idx) {
		assert(!isNull);
		assert(idx < length);
		return buffer[idx];
	}
	void clear() {
		isNull = true;
		length = 0; 
	}
}

/**
	An abstract class that does terminal emulation. You'll have to subclass it to make it work.

	The terminal implements a subset of what xterm does and then, optionally, some special features.

	Its linear mode (normal) screen buffer is infinitely long and infinitely wide. It is the responsibility
	of your subclass to do line wrapping, etc., for display. This i think is actually incompatible with xterm but meh.

	actually maybe it *should* automatically wrap them. idk. I think GNU screen does both. FIXME decide.

	Its cellular mode (alternate) screen buffer can be any size you want.
*/
class TerminalEmulator {
	/* override these to do stuff on the interface.
	You might be able to stub them out if there's no state maintained on the target, since TerminalEmulator maintains its own internal state */
	protected abstract void changeWindowTitle(string); /// the title of the window
	protected abstract void changeIconTitle(string); /// the shorter window/iconified window

	protected abstract void changeWindowIcon(IndexedImage); /// change the window icon. note this may be null

	protected abstract void changeCursorStyle(CursorStyle); /// cursor style

	protected abstract void changeTextAttributes(TextAttributes); /// current text output attributes
	protected abstract void soundBell(); /// sounds the bell
	protected abstract void sendToApplication(scope const(void)[]); /// send some data to the program running in the terminal, so keypresses etc.

	protected abstract void copyToClipboard(string); /// copy the given data to the clipboard (or you can do nothing if you can't)
	protected abstract void pasteFromClipboard(void delegate(in char[])); /// requests a paste. we pass it a delegate that should accept the data

	protected abstract void copyToPrimary(string); /// copy the given data to the PRIMARY X selection (or you can do nothing if you can't)
	protected abstract void pasteFromPrimary(void delegate(in char[])); /// requests a paste from PRIMARY. we pass it a delegate that should accept the data

	abstract protected void requestExit(); /// the program is finished and the terminal emulator is requesting you to exit

	/// Signal the UI that some attention should be given, e.g. blink the taskbar or sound the bell.
	/// The default is to ignore the demand by instantly acknowledging it - if you override this, do NOT call super().
	protected void demandAttention() {
		attentionReceived();
	}

	/// After it demands attention, call this when the attention has been received
	/// you may call it immediately to ignore the demand (the default)
	public void attentionReceived() {
		attentionDemanded = false;
	}

	// I believe \033[50buffer[] and up are available for extensions everywhere.
	// 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?
	// apparently shifted F5 is ^[[15;2~
	// alt + f5 is ^[[15;3~
	// alt+shift+f5 is ^[[15;4~

	private string pasteDataPending = null;

	protected void justRead() {
		if(pasteDataPending.length) {
			sendPasteData(pasteDataPending);
			import core.thread; Thread.sleep(50.msecs); // hack to keep it from closing, broken pipe i think
		}
	}

	// my custom extension.... the data is the text content of the link, the identifier is some bits attached to the unit
	public void sendHyperlinkData(scope const(dchar)[] data, uint identifier) {
		if(bracketedHyperlinkMode) {
			sendToApplication("\033[220~");

			import std.conv;
			// FIXME: that second 0 is a "command", like which menu option, which mouse button, etc.
			sendToApplication(to!string(identifier) ~ ";0;" ~ to!string(data));

			sendToApplication("\033[221~");
		} else {
			// without bracketed hyperlink, it simulates a paste
			import std.conv;
			sendPasteData(to!string(data));
		}
	}

	public void sendPasteData(scope const(char)[] data) {
		//if(pasteDataPending.length)
			//throw new Exception("paste data being discarded, wtf, shouldnt happen");

		// FIXME: i should put it all together so the brackets don't get separated by threads

		if(bracketedPasteMode)
			sendToApplication("\033[200~");

		version(use_libssh2)
			enum MAX_PASTE_CHUNK = 4000;
		else
			enum MAX_PASTE_CHUNK = 1024 * 1024 * 10;

		if(data.length > MAX_PASTE_CHUNK) {
			// need to chunk it in order to receive echos, etc,
			// to avoid deadlocks
			pasteDataPending = data[MAX_PASTE_CHUNK .. $].idup;
			data = data[0 .. MAX_PASTE_CHUNK];
		} else {
			pasteDataPending = null;
		}

		if(data.length)
			sendToApplication(data);

		if(bracketedPasteMode)
			sendToApplication("\033[201~");
	}

	private string overriddenSelection;
	protected void cancelOverriddenSelection() {
		if(overriddenSelection.length == 0)
			return;
		overriddenSelection = null;
		sendToApplication("\033[27;0;987136~"); // fake "select none" key, see terminal.d's ProprietaryPseudoKeys for values.

		// The reason that proprietary thing is ok is setting the selection is itself a proprietary extension
		// so if it was ever set, it implies the user code is familiar with our magic.
	}

	public string getSelectedText() {
		if(overriddenSelection.length)
			return overriddenSelection;
		return getPlainText(selectionStart, selectionEnd);
	}

	bool dragging;
	int lastDragX, lastDragY;
	public bool sendMouseInputToApplication(int termX, int termY, MouseEventType type, MouseButton button, bool shift, bool ctrl, bool alt) {
		if(termX < 0)
			termX = 0;
		if(termX >= screenWidth)
			termX = screenWidth - 1;
		if(termY < 0)
			termY = 0;
		if(termY >= screenHeight)
			termY = screenHeight - 1;

		version(Windows) {
			// I'm swapping these because my laptop doesn't have a middle button,
			// and putty swaps them too by default so whatevs.
			if(button == MouseButton.right)
				button = MouseButton.middle;
			else if(button == MouseButton.middle)
				button = MouseButton.right;
		}

		int baseEventCode() {
			int b;
			// lol the xterm mouse thing sucks like javascript! unbelievable
			// it doesn't support two buttons at once...
			if(button == MouseButton.left)
				b = 0;
			else if(button == MouseButton.right)
				b = 2;
			else if(button == MouseButton.middle)
				b = 1;
			else if(button == MouseButton.wheelUp)
				b = 64 | 0;
			else if(button == MouseButton.wheelDown)
				b = 64 | 1;
			else
				b = 3; // none pressed or button released

			if(shift)
				b |= 4;
			if(ctrl)
				b |= 16;
			if(alt) // sending alt as meta
				b |= 8;

			return b;
		}


		if(type == MouseEventType.buttonReleased) {
			// X sends press and release on wheel events, but we certainly don't care about those
			if(button == MouseButton.wheelUp || button == MouseButton.wheelDown)
				return false;

			if(dragging) {
				auto text = getSelectedText();
				if(text.length) {
					copyToPrimary(text);
				} else if(!mouseButtonReleaseTracking || shift || (selectiveMouseTracking && ((!alternateScreenActive || scrollingBack) || termY != 0) && termY != cursorY)) {
					// hyperlink check
					int idx = termY * screenWidth + termX;
					auto screen = (alternateScreenActive ? alternateScreen : normalScreen);

					if(screen[idx].hyperlinkStatus & 0x01) {
						// it is a link! need to find the beginning and the end
						auto start = idx;
						auto end = idx;
						auto value = screen[idx].hyperlinkStatus;
						while(start > 0 && screen[start].hyperlinkStatus == value)
							start--;
						if(screen[start].hyperlinkStatus != value)
							start++;
						while(end < screen.length && screen[end].hyperlinkStatus == value)
							end++;

						uint number;
						dchar[64] buffer;
						foreach(i, ch; screen[start .. end]) {
							if(i >= buffer.length)
								break;
							if(!ch.hasNonCharacterData)
								buffer[i] = ch.ch;
							if(i < 16) {
								number |= (ch.hyperlinkBit ? 1 : 0) << i;
							}
						}

						sendHyperlinkData(buffer[0 .. end - start], number);
					}
				}
			}

			dragging = false;
			if(mouseButtonReleaseTracking) {
				int b = baseEventCode;
				b |= 3; // always send none / button released
				ScopeBuffer!(char, 16) buffer;
				buffer ~= "\033[M";
				buffer ~= cast(char) (b | 32);
				addMouseCoordinates(buffer, termX, termY);
				//buffer ~= cast(char) (termX+1 + 32);
				//buffer ~= cast(char) (termY+1 + 32);
				sendToApplication(buffer[]);
			}
		}

		if(type == MouseEventType.motion) {
			if(termX != lastDragX || termY != lastDragY) {
				lastDragY = termY;
				lastDragX = termX;
				if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
					int b = baseEventCode;
					ScopeBuffer!(char, 16) buffer;
					buffer ~= "\033[M";
					buffer ~= cast(char) ((b | 32) + 32);
					addMouseCoordinates(buffer, termX, termY);
					//buffer ~= cast(char) (termX+1 + 32);
					//buffer ~= cast(char) (termY+1 + 32);
					sendToApplication(buffer[]);
				}

				if(dragging) {
					auto idx = termY * screenWidth + termX;

					// the no-longer-selected portion needs to be invalidated
					int start, end;
					if(idx > selectionEnd) {
						start = selectionEnd;
						end = idx;
					} else {
						start = idx;
						end = selectionEnd;
					}
					if(start < 0 || end >= ((alternateScreenActive ? alternateScreen.length : normalScreen.length)))
						return false;

					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
						cell.invalidated = true;
						cell.selected = false;
					}

					cancelOverriddenSelection();
					selectionEnd = idx;

					// and the freshly selected portion needs to be invalidated
					if(selectionStart > selectionEnd) {
						start = selectionEnd;
						end = selectionStart;
					} else {
						start = selectionStart;
						end = selectionEnd;
					}
					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
						cell.invalidated = true;
						cell.selected = true;
					}

					return true;
				}
			}
		}

		if(type == MouseEventType.buttonPressed) {
			// double click detection
			import std.datetime;
			static SysTime lastClickTime;
			static int consecutiveClicks = 1;

			if(button != MouseButton.wheelUp && button != MouseButton.wheelDown) {
				if(Clock.currTime() - lastClickTime < dur!"msecs"(350))
					consecutiveClicks++;
				else
					consecutiveClicks = 1;

				lastClickTime = Clock.currTime();
			}
			// end dbl click

			if(!(shift) && mouseButtonTracking) {
				if(selectiveMouseTracking && termY != 0 && termY != cursorY) {
					if(button == MouseButton.left || button == MouseButton.right)
						goto do_default_behavior;
					if((!alternateScreenActive || scrollingBack) && (button == MouseButton.wheelUp || button.MouseButton.wheelDown))
						goto do_default_behavior;
				}
				// top line only gets special cased on full screen apps
				if(selectiveMouseTracking && (!alternateScreenActive || scrollingBack) && termY == 0 && cursorY != 0)
					goto do_default_behavior;

				int b = baseEventCode;

				int x = termX;
				int y = termY;
				x++; y++; // applications expect it to be one-based

				ScopeBuffer!(char, 16) buffer;
				buffer ~= "\033[M";
				buffer ~= cast(char) (b | 32);
				addMouseCoordinates(buffer, termX, termY); 
				//buffer ~= cast(char) (x + 32);
				//buffer ~= cast(char) (y + 32);

				sendToApplication(buffer[]);
			} else {
				do_default_behavior:
				if(button == MouseButton.middle) {
					pasteFromPrimary(&sendPasteData);
				}

				if(button == MouseButton.wheelUp) {
					scrollback(alt ? 0 : (ctrl ? 10 : 1), alt ? -(ctrl ? 10 : 1) : 0);
					return true;
				}
				if(button == MouseButton.wheelDown) {
					scrollback(alt ? 0 : -(ctrl ? 10 : 1), alt ? (ctrl ? 10 : 1) : 0);
					return true;
				}

				if(button == MouseButton.left) {
					// we invalidate the old selection since it should no longer be highlighted...
					makeSelectionOffsetsSane(selectionStart, selectionEnd);

					cancelOverriddenSelection();

					auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen);
					foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) {
						cell.invalidated = true;
						cell.selected = false;
					}

					if(consecutiveClicks == 1) {
						selectionStart = termY * screenWidth + termX;
						selectionEnd = selectionStart;
					} else if(consecutiveClicks == 2) {
						selectionStart = termY * screenWidth + termX;
						selectionEnd = selectionStart;
						while(selectionStart > 0 && !isWordSeparator((*activeScreen)[selectionStart-1].ch)) {
							selectionStart--;
						}

						while(selectionEnd < (*activeScreen).length && !isWordSeparator((*activeScreen)[selectionEnd].ch)) {
							selectionEnd++;
						}

					} else if(consecutiveClicks == 3) {
						selectionStart = termY * screenWidth;
						selectionEnd = selectionStart + screenWidth;
					}
					dragging = true;
					lastDragX = termX;
					lastDragY = termY;

					// then invalidate the new selection as well since it should be highlighted
					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[selectionStart .. selectionEnd]) {
						cell.invalidated = true;
						cell.selected = true;
					}

					return true;
				}
				if(button == MouseButton.right) {

					int changed1;
					int changed2;

					cancelOverriddenSelection();

					auto click = termY * screenWidth + termX;
					if(click < selectionStart) {
						auto oldSelectionStart = selectionStart;
						selectionStart = click;
						changed1 = selectionStart;
						changed2 = oldSelectionStart;
					} else if(click > selectionEnd) {
						auto oldSelectionEnd = selectionEnd;
						selectionEnd = click;

						changed1 = oldSelectionEnd;
						changed2 = selectionEnd;
					}

					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[changed1 .. changed2]) {
						cell.invalidated = true;
						cell.selected = true;
					}

					auto text = getPlainText(selectionStart, selectionEnd);
					if(text.length) {
						copyToPrimary(text);
					}
					return true;
				}
			}
		}

		return false;
	}

	private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) {
		// 1-based stuff and 32 is the base value
		x += 1 + 32;
		y += 1 + 32;

		if(utf8MouseMode) {
			import std.utf;
			char[4] str;

			foreach(char ch; str[0 .. encode(str, x)])
				buffer ~= ch;

			foreach(char ch; str[0 .. encode(str, y)])
				buffer ~= ch;
		} else {
			buffer ~= cast(char) x;
			buffer ~= cast(char) y;
		}
	}

	protected void returnToNormalScreen() {
		alternateScreenActive = false;

		if(cueScrollback) {
			showScrollbackOnScreen(normalScreen, 0, true, 0);
			newLine(false);
			cueScrollback = false;
		}

		notifyScrollbarRelevant(true, true);
	}

	protected void outputOccurred() { }

	private int selectionStart; // an offset into the screen buffer
	private int selectionEnd; // ditto

	void requestRedraw() {}


	private bool skipNextChar;
	// assuming Key is an enum with members just like the one in simpledisplay.d
	// returns true if it was handled here
	protected bool defaultKeyHandler(Key)(Key key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
		enum bool KeyHasNamedAscii = is(typeof(Key.A));

		static string magic() {
			string code;
			foreach(member; __traits(allMembers, TerminalKey))
				if(member != "Escape")
					code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
						, shift ?true:false
						, alt ?true:false
						, ctrl ?true:false
						, windows ?true:false
					)) requestRedraw(); return true;";
			return code;
		}

		void specialAscii(dchar what) {
			if(!alt)
				skipNextChar = true;
			if(sendKeyToApplication(
				cast(TerminalKey) what
				, shift ? true:false
				, alt ? true:false
				, ctrl ? true:false
				, windows ? true:false
			)) requestRedraw();
		}

		static if(KeyHasNamedAscii) {
			enum Space = Key.Space;
			enum Enter = Key.Enter;
			enum Backspace = Key.Backspace;
			enum Tab = Key.Tab;
			enum Escape = Key.Escape;
		} else {
			enum Space = ' ';
			enum Enter = '\n';
			enum Backspace = '\b';
			enum Tab = '\t';
			enum Escape = '\033';
		}


		switch(key) {
			//// I want the escape key to send twice to differentiate it from
			//// other escape sequences easily.
			//case Key.Escape: sendToApplication("\033"); break;

			/*
			case Key.V:
			case Key.C:
				if(shift && ctrl) {
					skipNextChar = true;
					if(key == Key.V)
						pasteFromClipboard(&sendPasteData);
					else if(key == Key.C)
						copyToClipboard(getSelectedText());
				}
			break;
			*/

			// expansion of my own for like shift+enter to terminal.d users
			case Enter, Backspace, Tab, Escape:
				if(shift || alt || ctrl) {
					static if(KeyHasNamedAscii) {
						specialAscii(
							cast(TerminalKey) (
								key == Key.Enter ? '\n' :
								key == Key.Tab ? '\t' :
								key == Key.Backspace ? '\b' :
								key == Key.Escape ? '\033' :
									0 /* assert(0) */
							)
						);
					} else {
						specialAscii(key);
					}
					return true;
				}
			break;
			case Space:
				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.
					// ctrl+space sends 0 per normal translation char rules
					specialAscii(' ');
					return true;
				}
			break;

			mixin(magic());

			static if(is(typeof(Key.Shift))) {
				// modifiers are not ascii, ignore them here
				case Key.Shift, Key.Ctrl, Key.Alt, Key.Windows, Key.Alt_r, Key.Shift_r, Key.Ctrl_r, Key.CapsLock, Key.NumLock:
				// nor are these special keys that don't return characters
				case Key.Menu, Key.Pause, Key.PrintScreen:
					return false;
			}

			default:
				// alt basically always get special treatment, since it doesn't
				// generate anything from the char handler. but shift and ctrl
				// do, so we'll just use that unless both are pressed, in which
				// case I want to go custom to differentiate like ctrl+c from ctrl+shift+c and such.

				// FIXME: xterm offers some control on this, see: https://invisible-island.net/xterm/xterm.faq.html#xterm_modother
				if(alt || (shift && ctrl)) {
					if(key >= 'A' && key <= 'Z')
						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
					specialAscii(key);
					if(!alt)
						skipNextChar = true;
					return true;
				}
		}

		return true;
	}
	protected bool defaultCharHandler(dchar c) {
		if(skipNextChar) {
			skipNextChar = false;
			return true;
		}

		endScrollback();
		char[4] str;
		char[5] send;

		import std.utf;
		//if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
		auto data = str[0 .. encode(str, c)];

		// 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.
		if(c != 127)
			sendToApplication(data);

		return true;
	}

	/// Send a non-character key sequence
	public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
		bool redrawRequired = false;

		if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) {
			toggleScrollLock();
			return true;
		}

		/*
			So ctrl + A-Z, [, \, ], ^, and _ are all chars 1-31
			ctrl+5 send ^]

			FIXME: for alt+keys and the other ctrl+them, send the xterm ascii magc thing terminal.d knows how to use
		*/

		// scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen
		// doesn't have scrollback anyway. Thus the key will be forwarded to the application.
		if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageUp && (shift || scrollLock)) {
			scrollback(10);
			return true;
		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageDown && (shift || scrollLock)) {
			scrollback(-10);
			return true;
		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Left && (shift || scrollLock)) {
			scrollback(0, ctrl ? -10 : -1);
			return true;
		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Right && (shift || scrollLock)) {
			scrollback(0, ctrl ? 10 : 1);
			return true;
		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Up && (shift || scrollLock)) {
			scrollback(ctrl ? 10 : 1);
			return true;
		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Down && (shift || scrollLock)) {
			scrollback(ctrl ? -10 : -1);
			return true;
		} else if((!alternateScreenActive || scrollingBack)) { // && ev.key != Key.Shift && ev.key != Key.Shift_r) {
			if(endScrollback())
				redrawRequired = true;
		}



		void sendToApplicationModified(string s, int key = 0) {
			bool anyModifier = shift || alt || ctrl || windows;
			if(!anyModifier || applicationCursorKeys)
				sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh
			else {
				ScopeBuffer!(char, 16) modifierNumber;
				char otherModifier = 0;
				if(shift && alt && ctrl) modifierNumber = "8";
				if(alt && ctrl && !shift) modifierNumber = "7";
				if(shift && ctrl && !alt) modifierNumber = "6";
				if(ctrl && !shift && !alt) modifierNumber = "5";
				if(shift && alt && !ctrl) modifierNumber = "4";
				if(alt && !shift && !ctrl) modifierNumber = "3";
				if(shift && !alt && !ctrl) modifierNumber = "2";
				// FIXME: meta and windows
				// windows is an extension
				if(windows) {
					if(modifierNumber.length)
						otherModifier = '2';
					else
						modifierNumber = "20";
					/* // the below is what we're really doing
					int mn = 0;
					if(modifierNumber.length)
						mn = modifierNumber[0] + '0';
					mn += 20;
					*/
				}

				string keyNumber;
				char terminator;

				if(s[$-1] == '~') {
					keyNumber = s[2 .. $-1];
					terminator = '~';
				} else {
					keyNumber = "1";
					terminator = s[$ - 1];
				}

				ScopeBuffer!(char, 32) buffer;
				buffer ~= "\033[";
				buffer ~= keyNumber;
				buffer ~= ";";
				if(otherModifier)
					buffer ~= otherModifier;
				buffer ~= modifierNumber[];
				if(key) {
					buffer ~= ";";
					import std.conv;
					buffer ~= to!string(key);
				}
				buffer ~= terminator;
				// the xterm style is last bit tell us what it is
				sendToApplication(buffer[]);
			}
		}

		alias TerminalKey Key;
		import std.stdio;
		// writefln("Key: %x", cast(int) key);
		switch(key) {
			case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break;
			case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break;
			case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break;
			case Key.Right: sendToApplicationModified(applicationCursorKeys ? "\033OC" : "\033[C"); break;

			case Key.Home: sendToApplicationModified(applicationCursorKeys ? "\033OH" : (1 ? "\033[H" : "\033[1~")); break;
			case Key.Insert: sendToApplicationModified("\033[2~"); break;
			case Key.Delete: sendToApplicationModified("\033[3~"); break;

			// the 1? is xterm vs gnu screen. but i really want xterm compatibility.
			case Key.End: sendToApplicationModified(applicationCursorKeys ? "\033OF" : (1 ? "\033[F" : "\033[4~")); break;
			case Key.PageUp: sendToApplicationModified("\033[5~"); break;
			case Key.PageDown: sendToApplicationModified("\033[6~"); break;

			// 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
			case Key.F1: sendToApplicationModified(1 ? "\033OP" : "\033[11~"); break;
			case Key.F2: sendToApplicationModified(1 ? "\033OQ" : "\033[12~"); break;
			case Key.F3: sendToApplicationModified(1 ? "\033OR" : "\033[13~"); break;
			case Key.F4: sendToApplicationModified(1 ? "\033OS" : "\033[14~"); break;
			case Key.F5: sendToApplicationModified("\033[15~"); break;
			case Key.F6: sendToApplicationModified("\033[17~"); break;
			case Key.F7: sendToApplicationModified("\033[18~"); break;
			case Key.F8: sendToApplicationModified("\033[19~"); break;
			case Key.F9: sendToApplicationModified("\033[20~"); break;
			case Key.F10: sendToApplicationModified("\033[21~"); break;
			case Key.F11: sendToApplicationModified("\033[23~"); break;
			case Key.F12: sendToApplicationModified("\033[24~"); break;

			case Key.Escape: sendToApplicationModified("\033"); break;

			// my extensions, see terminator.d for the other side of it
			case Key.ScrollLock: sendToApplicationModified("\033[70~"); break;

			// xterm extension for arbitrary modified unicode chars
			default:
				sendToApplicationModified("\033[27~", key);
		}

		return redrawRequired;
	}

	/// 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
	protected /*abstract*/ BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[]) {
		return BinaryDataTerminalRepresentation();
	}

	/// If you subclass this and return true, you can scroll on command without needing to redraw the entire screen;
	/// returning true here suppresses the automatic invalidation of scrolled lines (except the new one).
	protected bool scrollLines(int howMany, bool scrollUp) {
		return false;
	}

	// might be worth doing the redraw magic in here too.
	// FIXME: not implemented
	@disable protected void drawTextSection(int x, int y, TextAttributes attributes, in dchar[] text, bool isAllSpaces) {
		// 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
		// or you can redraw based on the invalidated flag on the buffer
	}
	// FIXME: what about image sections? maybe it is still necessary to loop through them

	/// Style of the cursor
	enum CursorStyle {
		block, /// a solid block over the position (like default xterm or many gui replace modes)
		underline, /// underlining the position (like the vga text mode default)
		bar, /// a bar on the left side of the cursor position (like gui insert modes)
	}

	// these can be overridden, but don't have to be
	TextAttributes defaultTextAttributes() {
		TextAttributes ta;

		ta.foregroundIndex = 256; // terminal.d uses this as Color.DEFAULT
		ta.backgroundIndex = 256;

		import std.process;
		// I'm using the environment for this because my programs and scripts
		// already know this variable and then it gets nicely inherited. It is
		// also easy to set without buggering with other arguments. So works for me.
		version(with_24_bit_color) {
			if(environment.get("ELVISBG") == "dark") {
				ta.foreground = Color.white;
				ta.background = Color.black;
			} else {
				ta.foreground = Color.black;
				ta.background = Color.white;
			}
		}

		return ta;
	}

	Color defaultForeground;
	Color defaultBackground;

	Color[256] palette;

	/// .
	static struct TextAttributes {
		align(1):
		bool bold() { return (attrStore & 1) ? true : false; } ///
		void bold(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } ///

		bool blink() { return (attrStore & 2) ? true : false; } ///
		void blink(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } ///

		bool invisible() { return (attrStore & 4) ? true : false; } ///
		void invisible(bool t) { attrStore &= ~4; if(t) attrStore |= 4; } ///

		bool inverse() { return (attrStore & 8) ? true : false; } ///
		void inverse(bool t) { attrStore &= ~8; if(t) attrStore |= 8; } ///

		bool underlined() { return (attrStore & 16) ? true : false; } ///
		void underlined(bool t) { attrStore &= ~16; if(t) attrStore |= 16; } ///

		bool italic() { return (attrStore & 32) ? true : false; } ///
		void italic(bool t) { attrStore &= ~32; if(t) attrStore |= 32; } ///

		bool strikeout() { return (attrStore & 64) ? true : false; } ///
		void strikeout(bool t) { attrStore &= ~64; if(t) attrStore |= 64; } ///

		bool faint() { return (attrStore & 128) ? true : false; } ///
		void faint(bool t) { attrStore &= ~128; if(t) attrStore |= 128; } ///

		// 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

		bool foregroundIsDefault() { return (attrStore & 256) ? true : false; } ///
		void foregroundIsDefault(bool t) { attrStore &= ~256; if(t) attrStore |= 256; } ///

		bool backgroundIsDefault() { return (attrStore & 512) ? true : false; } ///
		void backgroundIsDefault(bool t) { attrStore &= ~512; if(t) attrStore |= 512; } ///

		// I am doing all this to  get the store a bit smaller but
		// I could go back to just plain `ushort foregroundIndex` etc.

		///
		@property ushort foregroundIndex() {
			if(foregroundIsDefault)
				return 256;
			else
				return foregroundIndexStore;
		}
		///
		@property ushort backgroundIndex() {
			if(backgroundIsDefault)
				return 256;
			else
				return backgroundIndexStore;
		}
		///
		@property void foregroundIndex(ushort v) {
			if(v == 256)
				foregroundIsDefault = true;
			else
				foregroundIsDefault = false;
			foregroundIndexStore = cast(ubyte) v;
		}
		///
		@property void backgroundIndex(ushort v) {
			if(v == 256)
				backgroundIsDefault = true;
			else
				backgroundIsDefault = false;
			backgroundIndexStore = cast(ubyte) v;
		}

		ubyte foregroundIndexStore; /// the internal storage
		ubyte backgroundIndexStore; /// ditto
		ushort attrStore = 0; /// ditto

		version(with_24_bit_color) {
			Color foreground; /// ditto
			Color background; /// ditto
		}
	}

		//pragma(msg, TerminalCell.sizeof);
	/// represents one terminal cell
	align((void*).sizeof)
	static struct TerminalCell {
	align(1):
		private union {
			// OMG the top 11 bits of a dchar are always 0
			// and i can reuse them!!!
			struct {
				dchar chStore = ' '; /// the character
				TextAttributes attributesStore; /// color, etc.
			}
			// 64 bit pointer also has unused 16 bits but meh.
			NonCharacterData nonCharacterDataStore; /// iff hasNonCharacterData
		}

		dchar ch() {
			assert(!hasNonCharacterData);
			return chStore;
		}
		void ch(dchar c) { 
			hasNonCharacterData = false;
			chStore = c;
		}
		ref TextAttributes attributes() return {
			assert(!hasNonCharacterData);
			return attributesStore;
		}
		NonCharacterData nonCharacterData() {
			assert(hasNonCharacterData);
			return nonCharacterDataStore;
		}
		void nonCharacterData(NonCharacterData c) {
			hasNonCharacterData = true;
			nonCharacterDataStore = c;
		}

		// bits: RRHLLNSI
		// R = reserved, H = hyperlink ID bit, L = link, N = non-character data, S = selected, I = invalidated
		ubyte attrStore = 1;  // just invalidated to start

		bool invalidated() { return (attrStore & 1) ? true : false; } /// if it needs to be redrawn
		void invalidated(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } /// ditto

		bool selected() { return (attrStore & 2) ? true : false; } /// if it is currently selected by the user (for being copied to the clipboard)
		void selected(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } /// ditto

		bool hasNonCharacterData() { return (attrStore & 4) ? true : false; } ///
		void hasNonCharacterData(bool t) { attrStore &= ~4; if(t) attrStore |= 4; }

		// 0 means it is not a hyperlink. Otherwise, it just alternates between 1 and 3 to tell adjacent links apart.
		// value of 2 is reserved for future use.
		ubyte hyperlinkStatus() { return (attrStore & 0b11000) >> 3; }
		void hyperlinkStatus(ubyte t) { assert(t < 4); attrStore &= ~0b11000; attrStore |= t << 3; }

		bool hyperlinkBit() { return (attrStore & 0b100000) >> 5; }
		void hyperlinkBit(bool t) { (attrStore &= ~0b100000); if(t) attrStore |= 0b100000; }
	}

	bool hyperlinkFlipper;
	bool hyperlinkActive;
	int hyperlinkNumber;

	/// Cursor position, zero based. (0,0) == upper left. (0, 1) == second row, first column.
	static struct CursorPosition {
		int x; /// .
		int y; /// .
		alias y row;
		alias x column;
	}

	// these public functions can be used to manipulate the terminal

	/// clear the screen
	void cls() {
		TerminalCell plain;
		plain.ch = ' ';
		plain.attributes = currentAttributes;
		plain.invalidated = true;
		foreach(i, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
			cell = plain;
		}
	}

	void makeSelectionOffsetsSane(ref int offsetStart, ref int offsetEnd) {
		auto buffer = &alternateScreen;

		if(offsetStart < 0)
			offsetStart = 0;
		if(offsetEnd < 0)
			offsetEnd = 0;
		if(offsetStart > (*buffer).length)
			offsetStart = cast(int) (*buffer).length;
		if(offsetEnd > (*buffer).length)
			offsetEnd = cast(int) (*buffer).length;

		// if it is backwards, we can flip it
		if(offsetEnd < offsetStart) {
			auto tmp = offsetStart;
			offsetStart = offsetEnd;
			offsetEnd = tmp;
		}
	}

	public string getPlainText(int offsetStart, int offsetEnd) {
		auto buffer = alternateScreenActive ? &alternateScreen : &normalScreen;

		makeSelectionOffsetsSane(offsetStart, offsetEnd);

		if(offsetStart == offsetEnd)
			return null;

		int x = offsetStart % screenWidth;
		int firstSpace = -1;
		string ret;
		foreach(cell; (*buffer)[offsetStart .. offsetEnd]) {
			if(cell.hasNonCharacterData)
				break;
			ret ~= cell.ch;

			x++;
			if(x == screenWidth) {
				x = 0;
				if(firstSpace != -1) {
					// we ended with a bunch of spaces, let's replace them with a single newline so the next is more natural
					ret = ret[0 .. firstSpace];
					ret ~= "\n";
					firstSpace = -1;
				}
			} else {
				if(cell.ch == ' ' && firstSpace == -1)
					firstSpace = cast(int) ret.length - 1;
				else if(cell.ch != ' ')
					firstSpace = -1;
			}
		}
		if(firstSpace != -1) {
			bool allSpaces = true;
			foreach(item; ret[firstSpace .. $]) {
				if(item != ' ') {
					allSpaces = false;
					break;
				}
			}

			if(allSpaces)
				ret = ret[0 .. firstSpace];
		}

		return ret;
	}

	void scrollDown(int count = 1) {
		if(cursorY + 1 < screenHeight) {
			TerminalCell plain;
			plain.ch = ' ';
			plain.attributes = defaultTextAttributes();
			plain.invalidated = true;
			foreach(i; 0 .. count) {
				// FIXME: should that be cursorY or scrollZoneTop?
				for(int y = scrollZoneBottom; y > cursorY; y--)
				foreach(x; 0 .. screenWidth) {
					ASS[y][x] = ASS[y - 1][x];
					ASS[y][x].invalidated = true;
				}

				foreach(x; 0 .. screenWidth)
					ASS[cursorY][x] = plain;
			}
		}
	}

	void scrollUp(int count = 1) {
		if(cursorY + 1 < screenHeight) {
			TerminalCell plain;
			plain.ch = ' ';
			plain.attributes = defaultTextAttributes();
			plain.invalidated = true;
			foreach(i; 0 .. count) {
				// FIXME: should that be cursorY or scrollZoneBottom?
				for(int y = scrollZoneTop; y < cursorY; y++)
				foreach(x; 0 .. screenWidth) {
					ASS[y][x] = ASS[y + 1][x];
					ASS[y][x].invalidated = true;
				}

				foreach(x; 0 .. screenWidth)
					ASS[cursorY][x] = plain;
			}
		}
	}


	int readingExtensionData = -1;
	string extensionData;

	immutable(dchar[dchar])* characterSet = null; // null means use regular UTF-8

	bool readingEsc = false;
	ScopeBuffer!(ubyte, 1024, true) esc;
	/// sends raw input data to the terminal as if the application printf()'d it or it echoed or whatever
	void sendRawInput(in ubyte[] datain) {
		const(ubyte)[] data = datain;
	//import std.array;
	//assert(!readingEsc, replace(cast(string) esc, "\033", "\\"));
		again:
		foreach(didx, b; data) {
			if(readingExtensionData >= 0) {
				if(readingExtensionData == extensionMagicIdentifier.length) {
					if(b) {
						switch(b) {
							case 13, 10:
								// ignore
							break;
							case 'A': .. case 'Z':
							case 'a': .. case 'z':
							case '0': .. case '9':
							case '=':
							case '+', '/':
							case '_', '-':
								// base64 ok
								extensionData ~= b;
							break;
							default:
								// others should abort the read
								readingExtensionData = -1;
						}
					} else {
						readingExtensionData = -1;
						import std.base64;
						auto got = handleBinaryExtensionData(Base64.decode(extensionData));

						auto rep = got.representation;
						foreach(y; 0 .. got.height) {
							foreach(x; 0 .. got.width) {
								addOutput(rep[0]);
								rep = rep[1 .. $];
							}
							newLine(true);
						}
					}
				} else {
					if(b == extensionMagicIdentifier[readingExtensionData])
						readingExtensionData++;
					else {
						// put the data back into the buffer, if possible
						// (if the data was split across two packets, this may
						//  not be possible. but in that case, meh.)
						if(cast(int) didx - cast(int) readingExtensionData >= 0)
							data = data[didx - readingExtensionData .. $];
						readingExtensionData = -1;
						goto again;
					}
				}

				continue;
			}

			if(b == 0) {
				readingExtensionData = 0;
				extensionData = null;
				continue;
			}

			if(readingEsc) {
				if(b == 27) {
					// an esc in the middle of a sequence will
					// cancel the first one
					esc = null;
					continue;
				}

				if(b == 10) {
					readingEsc = false;
				}
				esc ~= b;

				if(esc.length == 1 && esc[0] == '7') {
					pushSavedCursor(cursorPosition);
					esc = null;
					readingEsc = false;
				} else if(esc.length == 1 && esc[0] == 'M') {
					// reverse index
					esc = null;
					readingEsc = false;
					if(cursorY <= scrollZoneTop)
						scrollDown();
					else
						cursorY = cursorY - 1;
				} else if(esc.length == 1 && esc[0] == '=') {
					// application keypad
					esc = null;
					readingEsc = false;
				} else if(esc.length == 2 && esc[0] == '%' && esc[1] == 'G') {
					// UTF-8 mode
					esc = null;
					readingEsc = false;
				} else if(esc.length == 1 && esc[0] == '8') {
					cursorPosition = popSavedCursor;
					esc = null;
					readingEsc = false;
				} else if(esc.length == 1 && esc[0] == 'c') {
					// reset
					// FIXME
					esc = null;
					readingEsc = false;
				} else if(esc.length == 1 && esc[0] == '>') {
					// normal keypad
					esc = null;
					readingEsc = false;
				} else if(esc.length > 1 && (
					(esc[0] == '[' && (b >= 64 && b <= 126)) ||
					(esc[0] == ']' && b == '\007')))
				{
					try {
						tryEsc(esc[]);
					} catch(Exception e) {
						unknownEscapeSequence(e.msg ~ " :: " ~ cast(char[]) esc[]);
					}
					esc = null;
					readingEsc = false;
				} else if(esc.length == 3 && esc[0] == '%' && esc[1] == 'G') {
					// UTF-8 mode. ignored because we're always in utf-8 mode (though should we be?)
					esc = null;
					readingEsc = false;
				} else if(esc.length == 2 && esc[0] == ')') {
					// more character set selection. idk exactly how this works
					esc = null;
					readingEsc = false;
				} else if(esc.length == 2 && esc[0] == '(') {
					// xterm command for character set
					// FIXME: handling esc[1] == '0' would be pretty boss
					// and esc[1] == 'B' == united states
					if(esc[1] == '0')
						characterSet = &lineDrawingCharacterSet;
					else
						characterSet = null; // our default is UTF-8 and i don't care much about others anyway.

					esc = null;
					readingEsc = false;
				} else if(esc.length == 1 && esc[0] == 'Z') {
					// identify terminal
					sendToApplication(terminalIdCode);
				}
				continue;
			}

			if(b == 27) {
				readingEsc = true;
				debug if(esc.isNull && esc.length) {
					import std.stdio; writeln("discarding esc ", cast(string) esc[]);
				}
				esc = null;
				continue;
			}

			if(b == 13) {
				cursorX = 0;
				setTentativeScrollback(0);
				continue;
			}

			if(b == 7) {
				soundBell();
				continue;
			}

			if(b == 8) {
				cursorX = cursorX - 1;
				setTentativeScrollback(cursorX);
				continue;
			}

			if(b == 9) {
				int howMany = 8 - (cursorX % 8);
				// so apparently it is just supposed to move the cursor.
				// it breaks mutt to output spaces
				cursorX = cursorX + howMany;

				if(!alternateScreenActive)
					foreach(i; 0 .. howMany)
						addScrollbackOutput(' '); // FIXME: it would be nice to actually put a tab character there for copy/paste accuracy (ditto with newlines actually)
				continue;
			}

//			std.stdio.writeln("READ ", data[w]);
			addOutput(b);
		}
	}


	/// construct
	this(int width, int height) {
		// initialization

		import std.process;
		if(environment.get("ELVISBG") == "dark") {
			defaultForeground = Color.white;
			defaultBackground = Color.black;
		} else {
			defaultForeground = Color.black;
			defaultBackground = Color.white;
		}

		currentAttributes = defaultTextAttributes();
		cursorColor = Color.white;

		palette[] = xtermPalette[];

		resizeTerminal(width, height);

		// update the other thing
		if(windowTitle.length == 0)
			windowTitle = "Terminal Emulator";
		changeWindowTitle(windowTitle);
		changeIconTitle(iconTitle);
		changeTextAttributes(currentAttributes);
	}


	private {
		TerminalCell[] scrollbackMainScreen;
		bool scrollbackCursorShowing;
		int scrollbackCursorX;
		int scrollbackCursorY;
	}

	protected {
		bool scrollingBack;

		int currentScrollback;
		int currentScrollbackX;
	}

	// FIXME: if it is resized while scrolling back, stuff can get messed up

	private int scrollbackLength_;
	private void scrollbackLength(int i) {
		scrollbackLength_ = i;
	}

	int scrollbackLength() {
		return scrollbackLength_;
	}

	private int scrollbackWidth_;
	int scrollbackWidth() {
		return scrollbackWidth_ > screenWidth ? scrollbackWidth_ : screenWidth;
	}

	/* virtual */ void notifyScrollbackAdded() {}
	/* virtual */ void notifyScrollbarRelevant(bool isRelevantHorizontally, bool isRelevantVertically) {}
	/* virtual */ void notifyScrollbarPosition(int x, int y) {}

	// coordinates are for a scroll bar, where 0,0 is the beginning of history
	void scrollbackTo(int x, int y) {
		if(alternateScreenActive && !scrollingBack)
			return;

		if(!scrollingBack) {
			startScrollback();
		}

		if(y < 0)
			y = 0;
		if(x < 0)
			x = 0;

		currentScrollbackX = x;
		currentScrollback = scrollbackLength - y;

		if(currentScrollback < 0)
			currentScrollback = 0;
		if(currentScrollbackX < 0)
			currentScrollbackX = 0;

		if(!scrollLock && currentScrollback == 0 && currentScrollbackX == 0) {
			endScrollback();
		} else {
			cls();
			showScrollbackOnScreen(alternateScreen, currentScrollback, false, currentScrollbackX);
		}
	}

	void scrollback(int delta, int deltaX = 0) {
		if(alternateScreenActive && !scrollingBack)
			return;

		if(!scrollingBack) {
			if(delta <= 0 && deltaX == 0)
				return; // it does nothing to scroll down when not scrolling back
			startScrollback();
		}
		currentScrollback += delta;
		if(!scrollbackReflow && deltaX) {
			currentScrollbackX += deltaX;
			int max = scrollbackWidth - screenWidth;
			if(max < 0)
				max = 0;
			if(currentScrollbackX > max)
				currentScrollbackX = max;
			if(currentScrollbackX < 0)
				currentScrollbackX = 0;
		}

		int max = cast(int) scrollbackBuffer.length - screenHeight;
		if(scrollbackReflow && max < 0) {
			foreach(line; scrollbackBuffer[]) {
				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
					max += 0;
				else
					max += cast(int) line.length / screenWidth;
			}
		}

		if(max < 0)
			max = 0;

		if(scrollbackReflow && currentScrollback > max) {
			foreach(line; scrollbackBuffer[]) {
				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
					max += 0;
				else
					max += cast(int) line.length / screenWidth;
			}
		}

		if(currentScrollback > max)
			currentScrollback = max;
		if(currentScrollback < 0)
			currentScrollback = 0;

		if(!scrollLock && currentScrollback <= 0 && currentScrollbackX <= 0)
			endScrollback();
		else {
			cls();
			showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
			notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
		}
	}

	private void startScrollback() {
		if(scrollingBack)
			return;
		currentScrollback = 0;
		currentScrollbackX = 0;
		scrollingBack = true;
		scrollbackCursorX = cursorX;
		scrollbackCursorY = cursorY;
		scrollbackCursorShowing = cursorShowing;
		scrollbackMainScreen = alternateScreen.dup;
		alternateScreenActive = true;

		cursorShowing = false;
	}

	bool endScrollback() {
		//if(scrollLock)
		//	return false;
		if(!scrollingBack)
			return false;
		scrollingBack = false;
		cursorX = scrollbackCursorX;
		cursorY = scrollbackCursorY;
		cursorShowing = scrollbackCursorShowing;
		alternateScreen = scrollbackMainScreen;
		alternateScreenActive = false;

		currentScrollback = 0;
		currentScrollbackX = 0;

		if(!scrollLock) {
			scrollbackReflow = true;
			recalculateScrollbackLength();
		}

		notifyScrollbarPosition(0, int.max);

		return true;
	}

	private bool scrollbackReflow = true;
	/* deprecated? */
	public void toggleScrollbackWrap() {
		scrollbackReflow = !scrollbackReflow;
		recalculateScrollbackLength();
	}

	private bool scrollLockLockEnabled = false;
	package void scrollLockLock() {
		scrollLockLockEnabled = true;
		if(!scrollLock)
			toggleScrollLock();
	}

	private bool scrollLock = false;
	public void toggleScrollLock() {
		if(scrollLockLockEnabled && scrollLock)
			goto nochange;
		scrollLock = !scrollLock;
		scrollbackReflow = !scrollLock;

		nochange:
		recalculateScrollbackLength();

		if(scrollLock) {
			startScrollback();

			cls();
			currentScrollback = 0;
			currentScrollbackX = 0;
			showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
			notifyScrollbarPosition(currentScrollbackX, scrollbackLength - currentScrollback - screenHeight);
		} else {
			endScrollback();
		}

		//cls();
		//drawScrollback();
	}

	private void recalculateScrollbackLength() {
		int count = cast(int) scrollbackBuffer.length;
		int max;
		if(scrollbackReflow) {
			foreach(line; scrollbackBuffer[]) {
				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
					{} // intentionally blank, the count is fine since this line isn't reflowed anyway
				else
					count += cast(int) line.length / screenWidth;
			}
		} else {
			foreach(line; scrollbackBuffer[]) {
				if(line.length > max)
					max = cast(int) line.length;
			}
		}
		scrollbackWidth_ = max;
		scrollbackLength = count;
		notifyScrollbackAdded();
		notifyScrollbarPosition(currentScrollbackX, currentScrollback ? scrollbackLength - currentScrollback : int.max);
	}

	/++
		Writes the text in the scrollback buffer to the given file.

		Discards formatting information and embedded images.

		See_Also:
			[writeScrollbackToDelegate]
	+/
	public void writeScrollbackToFile(string filename) {
		import std.stdio;
		auto file = File(filename, "wt");
		foreach(line; scrollbackBuffer[]) {
			foreach(c; line)
				if(!c.hasNonCharacterData)
					file.write(c.ch); // I hope this is buffered
			file.writeln();
		}
	}

	/++
		Writes the text in the scrollback buffer to the given delegate, one character at a time.

		Discards formatting information and embedded images.

		See_Also:
			[writeScrollbackToFile]
		History:
			Added March 14, 2021 (dub version 9.4)
	+/
	public void writeScrollbackToDelegate(scope void delegate(dchar c) dg) {
		foreach(line; scrollbackBuffer[]) {
			foreach(c; line)
				if(!c.hasNonCharacterData)
					dg(c.ch);
			dg('\n');
		}
	}

	public void drawScrollback(bool useAltScreen = false) {
		showScrollbackOnScreen(useAltScreen ? alternateScreen : normalScreen, 0, true, 0);
	}

	private void showScrollbackOnScreen(ref TerminalCell[] screen, int howFar, bool reflow, int howFarX) {
		int start;

		cursorX = 0;
		cursorY = 0;

		int excess = 0;

		if(scrollbackReflow) {
			int numLines;
			int idx = cast(int) scrollbackBuffer.length - 1;
			foreach_reverse(line; scrollbackBuffer[]) {
				auto lineCount = 1 + line.length / screenWidth;

				// 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
				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
					lineCount = 1;

				numLines += lineCount;
				if(numLines >= (screenHeight + howFar)) {
					start = cast(int) idx;
					excess = numLines - (screenHeight + howFar);
					break;
				}
				idx--;
			}
		} else {
			auto termination = cast(int) scrollbackBuffer.length - howFar;
			if(termination < 0)
				termination = cast(int) scrollbackBuffer.length;

			start = termination - screenHeight;
			if(start < 0)
				start = 0;
		}

		TerminalCell overflowCell;
		overflowCell.ch = '\&raquo;';
		overflowCell.attributes.backgroundIndex = 3;
		overflowCell.attributes.foregroundIndex = 0;
		version(with_24_bit_color) {
			overflowCell.attributes.foreground = Color(40, 40, 40);
			overflowCell.attributes.background = Color.yellow;
		}

		outer: foreach(line; scrollbackBuffer[start .. $]) {
			if(excess) {
				line = line[excess * screenWidth .. $];
				excess = 0;
			}

			if(howFarX) {
				if(howFarX <= line.length)
					line = line[howFarX .. $];
				else
					line = null;
			}

			bool overflowed;
			foreach(cell; line) {
				cell.invalidated = true;
				if(overflowed) {
					screen[cursorY * screenWidth + cursorX] = overflowCell;
					break;
				} else {
					screen[cursorY * screenWidth + cursorX] = cell;
				}

				if(cursorX == screenWidth-1) {
					if(scrollbackReflow) {
						// don't attempt to reflow images
						if(cell.hasNonCharacterData)
							break;
						cursorX = 0;
						if(cursorY + 1 == screenHeight)
							break outer;
						cursorY = cursorY + 1;
					} else {
						overflowed = true;
					}
				} else
					cursorX = cursorX + 1;
			}
			if(cursorY + 1 == screenHeight)
				break;
			cursorY = cursorY + 1;
			cursorX = 0;
		}

		cursorX = 0;
	}

	protected bool cueScrollback;

	public void resizeTerminal(int w, int h) {
		if(w == screenWidth && h == screenHeight)
			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)

		// do i like this?
		if(scrollLock)
			toggleScrollLock();

		// FIXME: hack
		endScrollback();

		screenWidth = w;
		screenHeight = h;

		normalScreen.length = screenWidth * screenHeight;
		alternateScreen.length = screenWidth * screenHeight;
		scrollZoneBottom = screenHeight - 1;
		if(scrollZoneTop < 0 || scrollZoneTop >= scrollZoneBottom)
			scrollZoneTop = 0;

		// we need to make sure the state is sane all across the board, so first we'll clear everything...
		TerminalCell plain;
		plain.ch = ' ';
		plain.attributes = defaultTextAttributes;
		plain.invalidated = true;
		normalScreen[] = plain;
		alternateScreen[] = plain;

		// then, in normal mode, we'll redraw using the scrollback buffer
		//
		// if we're in the alternate screen though, keep it blank because
		// while redrawing makes sense in theory, odds are the program in
		// charge of the normal screen didn't get the resize signal.
		if(!alternateScreenActive)
			showScrollbackOnScreen(normalScreen, 0, true, 0);
		else
			cueScrollback = true;
		// but in alternate mode, it is the application's responsibility

		// the property ensures these are within bounds so this set just forces that
		cursorY = cursorY;
		cursorX = cursorX;

		recalculateScrollbackLength();
	}

	private CursorPosition popSavedCursor() {
		CursorPosition pos;
		//import std.stdio; writeln("popped");
		if(savedCursors.length) {
			pos = savedCursors[$-1];
			savedCursors = savedCursors[0 .. $-1];
			savedCursors.assumeSafeAppend(); // we never keep references elsewhere so might as well reuse the memory as much as we can
		}

		// If the screen resized after this was saved, it might be restored to a bad amount, so we need to sanity test.
		if(pos.x < 0)
			pos.x = 0;
		if(pos.y < 0)
			pos.y = 0;
		if(pos.x > screenWidth)
			pos.x = screenWidth - 1;
		if(pos.y > screenHeight)
			pos.y = screenHeight - 1;

		return pos;
	}

	private void pushSavedCursor(CursorPosition pos) {
		//import std.stdio; writeln("pushed");
		savedCursors ~= pos;
	}

	public void clearScrollbackHistory() {
		if(scrollingBack)
			endScrollback();
		scrollbackBuffer.clear();
		scrollbackLength_ = 0;
		scrollbackWidth_ = 0;

		notifyScrollbackAdded();
	}

	public void moveCursor(int x, int y) {
		cursorX = x;
		cursorY = y;
	}

	/* FIXME: i want these to be private */
	protected {
		TextAttributes currentAttributes;
		CursorPosition cursorPosition;
		CursorPosition[] savedCursors; // a stack
		CursorStyle cursorStyle;
		Color cursorColor;
		string windowTitle;
		string iconTitle;

		bool attentionDemanded;

		IndexedImage windowIcon;
		IndexedImage[] iconStack;

		string[] titleStack;

		bool bracketedPasteMode;
		bool bracketedHyperlinkMode;
		bool mouseButtonTracking;
		private bool _mouseMotionTracking;
		bool utf8MouseMode;
		bool mouseButtonReleaseTracking;
		bool mouseButtonMotionTracking;
		bool selectiveMouseTracking;
		/+
			When set, it causes xterm to send CSI I when the terminal gains focus, and CSI O  when it loses focus.
			this is turned on by mode 1004 with mouse events.

			FIXME: not implemented.
		+/
		bool sendFocusEvents;

		bool mouseMotionTracking() {
			return _mouseMotionTracking;
		}

		void mouseMotionTracking(bool b) {
			_mouseMotionTracking = b;
		}

		void allMouseTrackingOff() {
			selectiveMouseTracking = false;
			mouseMotionTracking = false;
			mouseButtonTracking = false;
			mouseButtonReleaseTracking = false;
			mouseButtonMotionTracking = false;
			sendFocusEvents = false;
		}

		bool wraparoundMode = true;

		bool alternateScreenActive;
		bool cursorShowing = true;

		bool reverseVideo;
		bool applicationCursorKeys;

		bool scrollingEnabled = true;
		int scrollZoneTop;
		int scrollZoneBottom;

		int screenWidth;
		int screenHeight;
		// assert(alternateScreen.length = screenWidth * screenHeight);
		TerminalCell[] alternateScreen;
		TerminalCell[] normalScreen;

		// the lengths can be whatever
		ScrollbackBuffer scrollbackBuffer;

		static struct ScrollbackBuffer {
			TerminalCell[][] backing;

			enum maxScrollback = 8192 / 2; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask...

			int start;
			int length_;

			size_t length() {
				return length_;
			}

			void clear() {
				start = 0;
				length_ = 0;
				backing = null;
			}

			// FIXME: if scrollback hits limits the scroll bar needs
			// to understand the circular buffer

			void opOpAssign(string op : "~")(TerminalCell[] line) {
				if(length_ < maxScrollback) {
					backing.assumeSafeAppend();
					backing ~= line;
					length_++;
				} else {
					backing[start] = line;
					start++;
					if(start == maxScrollback)
						start = 0;
				}
			}

			/*
			int opApply(scope int delegate(ref TerminalCell[]) dg) {
				foreach(ref l; backing)
					if(auto res = dg(l))
						return res;
				return 0;
			}

			int opApplyReverse(scope int delegate(size_t, ref TerminalCell[]) dg) {
				foreach_reverse(idx, ref l; backing)
					if(auto res = dg(idx, l))
						return res;
				return 0;
			}
			*/

			TerminalCell[] opIndex(int idx) {
				return backing[(start + idx) % maxScrollback];
			}

			ScrollbackBufferRange opSlice(int startOfIteration, Dollar end) {
				return ScrollbackBufferRange(&this, startOfIteration);
			}
			ScrollbackBufferRange opSlice() {
				return ScrollbackBufferRange(&this, 0);
			}

			static struct ScrollbackBufferRange {
				ScrollbackBuffer* item;
				int position;
				int remaining;
				this(ScrollbackBuffer* item, int startOfIteration) {
					this.item = item;
					position = startOfIteration;
					remaining = cast(int) item.length - startOfIteration;

				}

				TerminalCell[] front() { return (*item)[position]; }
				bool empty() { return remaining <= 0; }
				void popFront() {
					position++;
					remaining--;
				}

				TerminalCell[] back() { return (*item)[remaining - 1 - position]; }
				void popBack() {
					remaining--;
				}
			}

			static struct Dollar {};
			Dollar opDollar() { return Dollar(); }

		}

		struct Helper2 {
			size_t row;
			TerminalEmulator t;
			this(TerminalEmulator t, size_t row) {
				this.t = t;
				this.row = row;
			}

			ref TerminalCell opIndex(size_t cell) {
				auto thing = t.alternateScreenActive ? &(t.alternateScreen) : &(t.normalScreen);
				return (*thing)[row * t.screenWidth + cell];
			}
		}

		struct Helper {
			TerminalEmulator t;
			this(TerminalEmulator t) {
				this.t = t;
			}

			Helper2 opIndex(size_t row) {
				return Helper2(t, row);
			}
		}

		@property Helper ASS() {
			return Helper(this);
		}

		@property int cursorX() { return cursorPosition.x; }
		@property int cursorY() { return cursorPosition.y; }
		@property void cursorX(int x) {
			if(x < 0)
				x = 0;
			if(x >= screenWidth)
				x = screenWidth - 1;
			cursorPosition.x = x;
		}
		@property void cursorY(int y) {
			if(y < 0)
				y = 0;
			if(y >= screenHeight)
				y = screenHeight - 1;
			cursorPosition.y = y;
		}

		void addOutput(string b) {
			foreach(c; b)
				addOutput(c);
		}

		TerminalCell[] currentScrollbackLine;
		ubyte[6] utf8SequenceBuffer;
		int utf8SequenceBufferPosition;
		// int scrollbackWrappingAt = 0;
		dchar utf8Sequence;
		int utf8BytesRemaining;
		int currentUtf8Shift;
		bool newLineOnNext;
		void addOutput(ubyte b) {

			void addChar(dchar c) {
				if(newLineOnNext) {
					newLineOnNext = false;
					// only if we're still on the right side...
					if(cursorX == screenWidth - 1)
						newLine(false);
				}
				TerminalCell tc;

				if(characterSet !is null) {
					if(auto replacement = utf8Sequence in *characterSet)
						utf8Sequence = *replacement;
				}
				tc.ch = utf8Sequence;
				tc.attributes = currentAttributes;
				tc.invalidated = true;

				if(hyperlinkActive) {
					tc.hyperlinkStatus = hyperlinkFlipper ? 3 : 1;
					tc.hyperlinkBit = hyperlinkNumber & 0x01;
					hyperlinkNumber >>= 1;
				}

				addOutput(tc);
			}


			// this takes in bytes at a time, but since the input encoding is assumed to be UTF-8, we need to gather the bytes
			if(utf8BytesRemaining == 0) {
				// we're at the beginning of a sequence
				utf8Sequence = 0;
				if(b < 128) {
					utf8Sequence = cast(dchar) b;
					// one byte thing, do nothing more...
				} else {
					// the number of bytes in the sequence is the number of set bits in the first byte...
					ubyte checkingBit = 7;
					while(b & (1 << checkingBit)) {
						utf8BytesRemaining++;
						checkingBit--;
					}
					uint shifted = b & ((1 << checkingBit) - 1);
					utf8BytesRemaining--; // since this current byte counts too
					currentUtf8Shift = utf8BytesRemaining * 6;


					shifted <<= currentUtf8Shift;
					utf8Sequence = cast(dchar) shifted;

					utf8SequenceBufferPosition = 0;
					utf8SequenceBuffer[utf8SequenceBufferPosition++] = b;
				}
			} else {
				// add this to the byte we're doing right now...
				utf8BytesRemaining--;
				currentUtf8Shift -= 6;
				if((b & 0b11000000) != 0b10000000) {
					// invalid utf-8 sequence,
					// discard it and try to continue
					utf8BytesRemaining = 0;
					utf8Sequence = 0xfffd;
					foreach(i; 0 .. utf8SequenceBufferPosition)
						addChar(utf8Sequence); // put out replacement char for everything in there so far
					utf8SequenceBufferPosition = 0;
					addOutput(b); // retry sending this byte as a new sequence after abandoning the old crap
					return;
				}
				uint shifted = b;
				shifted &= 0b00111111;
				shifted <<= currentUtf8Shift;
				utf8Sequence |= shifted;

				if(utf8SequenceBufferPosition < utf8SequenceBuffer.length)
					utf8SequenceBuffer[utf8SequenceBufferPosition++] = b;
			}

			if(utf8BytesRemaining)
				return; // not enough data yet, wait for more before displaying anything

			if(utf8Sequence == 10) {
				newLineOnNext = false;
				auto cx = cursorX; // FIXME: this cx thing is a hack, newLine should prolly just do the right thing

				/*
				TerminalCell tc;
				tc.ch = utf8Sequence;
				tc.attributes = currentAttributes;
				tc.invalidated = true;
				addOutput(tc);
				*/

				newLine(true);
				cursorX = cx;
			} else {
				addChar(utf8Sequence);
			}
		}

		private int recalculationThreshold = 0;
		public void addScrollbackLine(TerminalCell[] line) {
			scrollbackBuffer ~= line;

			if(scrollbackBuffer.length_ == ScrollbackBuffer.maxScrollback) {
				recalculationThreshold++;
				if(recalculationThreshold > 100) {
					recalculateScrollbackLength();
					notifyScrollbackAdded();
					recalculationThreshold = 0;
				}
			} else {
				if(!scrollbackReflow && line.length > scrollbackWidth_)
					scrollbackWidth_ = cast(int) line.length;

				if(line.length > 2 && (line[0].hasNonCharacterData || line[$-1].hasNonCharacterData))
					scrollbackLength = scrollbackLength + 1;
				else
					scrollbackLength = cast(int) (scrollbackLength + 1 + (scrollbackBuffer[cast(int) scrollbackBuffer.length - 1].length) / screenWidth);
				notifyScrollbackAdded();
			}

			if(!alternateScreenActive)
				notifyScrollbarPosition(0, int.max);
		}

		protected int maxScrollbackLength() pure const @nogc nothrow {
			return 1024;
		}

		bool insertMode = false;
		void newLine(bool commitScrollback) {
			if(!alternateScreenActive && commitScrollback) {
				// I am limiting this because obscenely long lines are kinda useless anyway and
				// i don't want it to eat excessive memory when i spam some thing accidentally
				if(currentScrollbackLine.length < maxScrollbackLength())
					addScrollbackLine(currentScrollbackLine.sliceTrailingWhitespace);
				else
					addScrollbackLine(currentScrollbackLine[0 .. maxScrollbackLength()].sliceTrailingWhitespace);

				currentScrollbackLine = null;
				currentScrollbackLine.reserve(64);
				// scrollbackWrappingAt = 0;
			}

			cursorX = 0;
			if(scrollingEnabled && cursorY >= scrollZoneBottom) {
				size_t idx = scrollZoneTop * screenWidth;

				// When we scroll up, we need to update the selection position too
				if(selectionStart != selectionEnd) {
					selectionStart -= screenWidth;
					selectionEnd -= screenWidth;
				}
				foreach(l; scrollZoneTop .. scrollZoneBottom) {
					if(alternateScreenActive) {
						if(idx + screenWidth * 2 > alternateScreen.length)
							break;
						alternateScreen[idx .. idx + screenWidth] = alternateScreen[idx + screenWidth .. idx + screenWidth * 2];
					} else {
						if(screenWidth <= 0)
							break;
						if(idx + screenWidth * 2 > normalScreen.length)
							break;
						normalScreen[idx .. idx + screenWidth] = normalScreen[idx + screenWidth .. idx + screenWidth * 2];
					}
					idx += screenWidth;
				}
				/*
				foreach(i; 0 .. screenWidth) {
					if(alternateScreenActive) {
						alternateScreen[idx] = alternateScreen[idx + screenWidth];
						alternateScreen[idx].invalidated = true;
					} else {
						normalScreen[idx] = normalScreen[idx + screenWidth];
						normalScreen[idx].invalidated = true;
					}
					idx++;
				}
				*/
				/*
				foreach(i; 0 .. screenWidth) {
					if(alternateScreenActive) {
						alternateScreen[idx].ch = ' ';
						alternateScreen[idx].attributes = currentAttributes;
						alternateScreen[idx].invalidated = true;
					} else {
						normalScreen[idx].ch = ' ';
						normalScreen[idx].attributes = currentAttributes;
						normalScreen[idx].invalidated = true;
					}
					idx++;
				}
				*/

				TerminalCell plain;
				plain.ch = ' ';
				plain.attributes = currentAttributes;
				if(alternateScreenActive) {
					alternateScreen[idx .. idx + screenWidth] = plain;
				} else {
					normalScreen[idx .. idx + screenWidth] = plain;
				}
			} else {
				if(insertMode) {
					scrollDown();
				} else
					cursorY = cursorY + 1;
			}

			invalidateAll = true;
		}

		protected bool invalidateAll;

		void clearSelection() {
			clearSelectionInternal();
			cancelOverriddenSelection();
		}

		private void clearSelectionInternal() {
			foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen)
				if(tc.selected) {
					tc.selected = false;
					tc.invalidated = true;
				}
			selectionStart = 0;
			selectionEnd = 0;
		}

		private int tentativeScrollback = int.max;
		private void setTentativeScrollback(int a) {
			tentativeScrollback = a;
		}

		void addScrollbackOutput(dchar ch) {
			TerminalCell plain;
			plain.ch = ch;
			plain.attributes = currentAttributes;
			addScrollbackOutput(plain);
		}

		void addScrollbackOutput(TerminalCell tc) {
			if(tentativeScrollback != int.max) {
				if(tentativeScrollback >= 0 && tentativeScrollback < currentScrollbackLine.length) {
					currentScrollbackLine = currentScrollbackLine[0 .. tentativeScrollback];
					currentScrollbackLine.assumeSafeAppend();
				}
				tentativeScrollback = int.max;
			}

			/*
			TerminalCell plain;
			plain.ch = ' ';
			plain.attributes = currentAttributes;
			int lol = cursorX + scrollbackWrappingAt;
			while(lol >= currentScrollbackLine.length)
				currentScrollbackLine ~= plain;
			currentScrollbackLine[lol] = tc;
			*/

			currentScrollbackLine ~= tc;

		}

		void addOutput(TerminalCell tc) {
			if(alternateScreenActive) {
				if(alternateScreen[cursorY * screenWidth + cursorX].selected) {
					clearSelection();
				}
				alternateScreen[cursorY * screenWidth + cursorX] = tc;
			} else {
				if(normalScreen[cursorY * screenWidth + cursorX].selected) {
					clearSelection();
				}
				// FIXME: make this more efficient if it is writing the same thing,
				// then it need not be invalidated. Same with above for the alt screen
				normalScreen[cursorY * screenWidth + cursorX] = tc;

				addScrollbackOutput(tc);
			}
			// FIXME: the wraparoundMode seems to help gnu screen but then it doesn't go away properly and that messes up bash...
			//if(wraparoundMode && cursorX == screenWidth - 1) {
			if(cursorX == screenWidth - 1) {
				// FIXME: should this check the scrolling zone instead?
				newLineOnNext = true;

				//if(!alternateScreenActive || cursorY < screenHeight - 1)
					//newLine(false);

				// scrollbackWrappingAt = cast(int) currentScrollbackLine.length;
			} else
				cursorX = cursorX + 1;

		}

		void tryEsc(ubyte[] esc) {
			bool[2] sidxProcessed;
			int[][2] argsAtSidx;
			int[12][2] argsAtSidxBuffer;

			int[12][4] argsBuffer;
			int argsBufferLocation;

			int[] getArgsBase(int sidx, int[] defaults) {
				assert(sidx == 1 || sidx == 2);

				if(sidxProcessed[sidx - 1]) {
					int[] bfr = argsBuffer[argsBufferLocation++][];
					if(argsBufferLocation == argsBuffer.length)
						argsBufferLocation = 0;
					bfr[0 .. defaults.length] = defaults[];
					foreach(idx, v; argsAtSidx[sidx - 1])
						if(v != int.min)
							bfr[idx] = v;
					return bfr[0 .. max(argsAtSidx[sidx - 1].length, defaults.length)];
				}

				auto end = esc.length - 1;
				foreach(iii, b; esc[sidx .. end]) {
					if(b >= 0x20 && b < 0x30)
						end = iii + sidx;
				}

				auto argsSection = cast(char[]) esc[sidx .. end];
				int[] args = argsAtSidxBuffer[sidx - 1][];

				import std.string : split;
				import std.conv : to;
				int lastIdx = 0;

				foreach(i, arg; split(argsSection, ";")) {
					int value;
					if(arg.length) {
						//import std.stdio; writeln(esc);
						value = to!int(arg);
					} else
						value = int.min; // defaults[i];

					if(args.length > i)
						args[i] = value;
					else
						assert(0);
					lastIdx++;
				}

				argsAtSidx[sidx - 1] = args[0 .. lastIdx];
				sidxProcessed[sidx - 1] = true;

				return getArgsBase(sidx, defaults);
			}
			int[] getArgs(int[] defaults...) {
				return getArgsBase(1, defaults);
			}

			// FIXME
			// from  http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
			// check out this section: "Window manipulation (from dtterm, as well as extensions)"
			// especially the title stack, that should rock
			/*
P s = 2 2 ; 0 → Save xterm icon and window title on stack.
P s = 2 2 ; 1 → Save xterm icon title on stack.
P s = 2 2 ; 2 → Save xterm window title on stack.
P s = 2 3 ; 0 → Restore xterm icon and window title from stack.
P s = 2 3 ; 1 → Restore xterm icon title from stack.
P s = 2 3 ; 2 → Restore xterm window title from stack.

			*/

			if(esc[0] == ']' && esc.length > 1) {
				int idx = -1;
				foreach(i, e; esc)
					if(e == ';') {
						idx = cast(int) i;
						break;
					}
				if(idx != -1) {
					auto arg = cast(char[]) esc[idx + 1 .. $-1];
					switch(cast(char[]) esc[1..idx]) {
						case "0":
							// icon name and window title
							windowTitle = iconTitle = arg.idup;
							changeWindowTitle(windowTitle);
							changeIconTitle(iconTitle);
						break;
						case "1":
							// icon name
							iconTitle = arg.idup;
							changeIconTitle(iconTitle);
						break;
						case "2":
							// window title
							windowTitle = arg.idup;
							changeWindowTitle(windowTitle);
						break;
						case "10":
							// change default text foreground color
						break;
						case "11":
							// change gui background color
						break;
						case "12":
							if(arg.length)
								arg = arg[1 ..$]; // skip past the thing
							if(arg.length) {
								cursorColor = Color.fromString(arg);
								foreach(ref p; cursorColor.components[0 .. 3])
									p ^= 0xff;
							} else
								cursorColor = Color.white;
						break;
						case "50":
							// change font
						break;
						case "52":
							// copy/paste control
							// echo -e "\033]52;p;?\007"
							// the p == primary
							// c == clipboard
							// q == secondary 
							// s == selection
							// 0-7, cut buffers
							// the data after it is either base64 stuff to copy or ? to request a paste

							if(arg == "p;?") {
								// i'm using this to request a paste. not quite compatible with xterm, but kinda
								// because xterm tends not to answer anyway.
								pasteFromPrimary(&sendPasteData);
							} else if(arg.length > 2 && arg[0 .. 2] == "p;") {
								auto info = arg[2 .. $];
								try {
									import std.base64;
									auto data = Base64.decode(info);
									copyToPrimary(cast(string) data);
								} catch(Exception e)  {}
							}

							if(arg == "c;?") {
								// i'm using this to request a paste. not quite compatible with xterm, but kinda
								// because xterm tends not to answer anyway.
								pasteFromClipboard(&sendPasteData);
							} else if(arg.length > 2 && arg[0 .. 2] == "c;") {
								auto info = arg[2 .. $];
								try {
									import std.base64;
									auto data = Base64.decode(info);
									copyToClipboard(cast(string) data);
								} catch(Exception e)  {}
							}

							// selection
							if(arg.length > 2 && arg[0 .. 2] == "s;") {
								auto info = arg[2 .. $];
								try {
									import std.base64;
									auto data = Base64.decode(info);
									clearSelectionInternal();
									overriddenSelection = cast(string) data;
								} catch(Exception e)  {}
							}
						break;
						case "4":
							// palette change or query
							        // set color #0 == black
							// echo -e '\033]4;0;black\007'
							/*
								echo -e '\033]4;9;?\007' ; cat

								^[]4;9;rgb:ffff/0000/0000^G
							*/

							// FIXME: if the palette changes, we should redraw so the change is immediately visible (as if we were using a real palette)
						break;
						case "104":
							// palette reset
							// reset color #0
							// echo -e '\033[104;0\007'
						break;
						/* Extensions */
						case "5000":
							// change window icon (send a base64 encoded image or something)
							/*
								The format here is width and height as a single char each
									'0'-'9' == 0-9
									'a'-'z' == 10 - 36
									anything else is invalid
								
								then a palette in hex rgba format (8 chars each), up to 26 entries

								then a capital Z

								if a palette entry == 'P', it means pull from the current palette (FIXME not implemented)

								then 256 characters between a-z (must be lowercase!) which are the palette entries for
								the pixels, top to bottom, left to right, so the image is 16x16. if it ends early, the
								rest of the data is assumed to be zero

								you can also do e.g. 22a, which means repeat a 22 times for some RLE.

								anything out of range aborts the operation
							*/
							auto img = readSmallTextImage(arg);
							windowIcon = img;
							changeWindowIcon(img);
						break;
						case "5001":
							// demand attention
							attentionDemanded = true;
							demandAttention();
						break;
						default:
							unknownEscapeSequence("" ~ cast(char) esc[1]);
					}
				}
			} else if(esc[0] == '[' && esc.length > 1) {
				switch(esc[$-1]) {
					case 'Z':
						// CSI Ps Z  Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
						// FIXME?
					break;
					case 'n':
						switch(esc[$-2]) {
							import std.string;
							// request status report, reply OK
							case '5': sendToApplication("\033[0n"); break;
							// request cursor position
							case '6': sendToApplication(format("\033[%d;%dR", cursorY + 1, cursorX + 1)); break;
							default: unknownEscapeSequence(cast(string) esc);
						}
					break;
					case 'A': if(cursorY) cursorY = cursorY - getArgs(1)[0]; break;
					case 'B': if(cursorY != this.screenHeight - 1) cursorY = cursorY + getArgs(1)[0]; break;
					case 'D': if(cursorX) cursorX = cursorX - getArgs(1)[0]; setTentativeScrollback(cursorX); break;
					case 'C': if(cursorX != this.screenWidth - 1) cursorX = cursorX + getArgs(1)[0]; break;

					case 'd': cursorY = getArgs(1)[0]-1; break;

					case 'E': cursorY = cursorY + getArgs(1)[0]; cursorX = 0; break;
					case 'F': cursorY = cursorY - getArgs(1)[0]; cursorX = 0; break;
					case 'G': cursorX = getArgs(1)[0] - 1; break;
					case 'H':
						auto got = getArgs(1, 1);
						cursorX = got[1] - 1;

						if(got[0] - 1 == cursorY)
							setTentativeScrollback(cursorX);
						else
							setTentativeScrollback(0);

						cursorY = got[0] - 1;
						newLineOnNext = false;
					break;
					case 'L':
						// insert lines
						scrollDown(getArgs(1)[0]);
					break;
					case 'M':
						// delete lines
						if(cursorY + 1 < screenHeight) {
							TerminalCell plain;
							plain.ch = ' ';
							plain.attributes = defaultTextAttributes();
							foreach(i; 0 .. getArgs(1)[0]) {
								foreach(y; cursorY .. scrollZoneBottom)
								foreach(x; 0 .. screenWidth) {
									ASS[y][x] = ASS[y + 1][x];
									ASS[y][x].invalidated = true;
								}
								foreach(x; 0 .. screenWidth) {
									ASS[scrollZoneBottom][x] = plain;
								}
							}
						}
					break;
					case 'K':
						auto arg = getArgs(0)[0];
						int start, end;
						if(arg == 0) {
							// clear from cursor to end of line
							start = cursorX;
							end = this.screenWidth;
						} else if(arg == 1) {
							// clear from cursor to beginning of line
							start = 0;
							end = cursorX + 1;
						} else if(arg == 2) {
							// clear entire line
							start = 0;
							end = this.screenWidth;
						}

						TerminalCell plain;
						plain.ch = ' ';
						plain.attributes = currentAttributes;

						for(int i = start; i < end; i++) {
							if(ASS[cursorY][i].selected)
								clearSelection();
							ASS[cursorY]
								[i] = plain;
						}
					break;
					case 's':
						pushSavedCursor(cursorPosition);
					break;
					case 'u':
						cursorPosition = popSavedCursor();
					break;
					case 'g':
						auto arg = getArgs(0)[0];
						TerminalCell plain;
						plain.ch = ' ';
						plain.attributes = currentAttributes;
						if(arg == 0) {
							// clear current column
							for(int i = 0; i < this.screenHeight; i++)
								ASS[i]
									[cursorY] = plain;
						} else if(arg == 3) {
							// clear all
							cls();
						}
					break;
					case 'q':
						// xterm also does blinks on the odd numbers (x-1)
						if(esc == "[0 q")
							cursorStyle = CursorStyle.block; // FIXME: restore default
						if(esc == "[2 q")
							cursorStyle = CursorStyle.block;
						else if(esc == "[4 q")
							cursorStyle = CursorStyle.underline;
						else if(esc == "[6 q")
							cursorStyle = CursorStyle.bar;

						changeCursorStyle(cursorStyle);
					break;
					case 't':
						// window commands
						// i might support more of these but for now i just want the stack stuff.

						auto args = getArgs(0, 0);
						if(args[0] == 22) {
							// save window title to stack
							// xterm says args[1] should tell if it is the window title, the icon title, or both, but meh
							titleStack ~= windowTitle;
							iconStack ~= windowIcon;
						} else if(args[0] == 23) {
							// restore from stack
							if(titleStack.length) {
								windowTitle = titleStack[$ - 1];
								changeWindowTitle(titleStack[$ - 1]);
								titleStack = titleStack[0 .. $ - 1];
							}

							if(iconStack.length) {
								windowIcon = iconStack[$ - 1];
								changeWindowIcon(iconStack[$ - 1]);
								iconStack = iconStack[0 .. $ - 1];
							}
						}
					break;
					case 'm':
						// FIXME  used by xterm to decide whether to construct
						// CSI > Pp ; Pv m CSI > Pp m Set/reset key modifier options, xterm.
						if(esc[1] == '>')
							goto default;
						// done
						argsLoop: foreach(argIdx, arg; getArgs(0))
						switch(arg) {
							case 0:
							// normal
								currentAttributes = defaultTextAttributes;
							break;
							case 1:
								currentAttributes.bold = true;
							break;
							case 2:
								currentAttributes.faint = true;
							break;
							case 3:
								currentAttributes.italic = true;
							break;
							case 4:
								currentAttributes.underlined = true;
							break;
							case 5:
								currentAttributes.blink = true;
							break;
							case 6:
								// rapid blink, treating the same as regular blink
								currentAttributes.blink = true;
							break;
							case 7:
								currentAttributes.inverse = true;
							break;
							case 8:
								currentAttributes.invisible = true;
							break;
							case 9:
								currentAttributes.strikeout = true;
							break;
							case 10:
								// primary font
							break;
							case 11: .. case 19:
								// alternate fonts
							break;
							case 20:
								// Fraktur font
							break;
							case 21:
								// bold off and doubled underlined
							break;
							case 22:
								currentAttributes.bold = false;
								currentAttributes.faint = false;
							break;
							case 23:
								currentAttributes.italic = false;
							break;
							case 24:
								currentAttributes.underlined = false;
							break;
							case 25:
								currentAttributes.blink = false;
							break;
							case 26:
								// reserved
							break;
							case 27:
								currentAttributes.inverse = false;
							break;
							case 28:
								currentAttributes.invisible = false;
							break;
							case 29:
								currentAttributes.strikeout = false;
							break;
							case 30:
							..
							case 37:
							// set foreground color
								/*
								Color nc;
								ubyte multiplier = currentAttributes.bold ? 255 : 127;
								nc.r = cast(ubyte)((arg - 30) & 1) * multiplier;
								nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier;
								nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier;
								nc.a = 255;
								*/
								currentAttributes.foregroundIndex = cast(ubyte)(arg - 30);
								version(with_24_bit_color)
								currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)];
							break;
							case 38:
								// xterm 256 color set foreground color
								auto args = getArgs()[argIdx + 1 .. $];
								if(args.length > 3 && args[0] == 2) {
									// set color to closest match in palette. but since we have full support, we'll just take it directly
									auto fg = Color(args[1], args[2], args[3]);
									version(with_24_bit_color)
										currentAttributes.foreground = fg;
									// and try to find a low default palette entry for maximum compatibility
									// 0x8000 == approximation
									currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg);
								} else if(args.length > 1 && args[0] == 5) {
									// set to palette index
									version(with_24_bit_color)
										currentAttributes.foreground = palette[args[1]];
									currentAttributes.foregroundIndex = cast(ushort) args[1];
								}
								break argsLoop;
							case 39:
							// default foreground color
								auto dflt = defaultTextAttributes();

								version(with_24_bit_color)
									currentAttributes.foreground = dflt.foreground;
								currentAttributes.foregroundIndex = dflt.foregroundIndex;
							break;
							case 40:
							..
							case 47:
							// set background color
								/*
								Color nc;
								nc.r = cast(ubyte)((arg - 40) & 1) * 255;
								nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255;
								nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255;
								nc.a = 255;
								*/

								currentAttributes.backgroundIndex = cast(ubyte)(arg - 40);
								//currentAttributes.background = nc;
								version(with_24_bit_color)
									currentAttributes.background = palette[arg-40];
							break;
							case 48:
								// xterm 256 color set background color
								auto args = getArgs()[argIdx + 1 .. $];
								if(args.length > 3 && args[0] == 2) {
									// set color to closest match in palette. but since we have full support, we'll just take it directly
									auto bg = Color(args[1], args[2], args[3]);
									version(with_24_bit_color)
										currentAttributes.background = Color(args[1], args[2], args[3]);

									// and try to find a low default palette entry for maximum compatibility
									// 0x8000 == this is an approximation
									currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg);
								} else if(args.length > 1 && args[0] == 5) {
									// set to palette index
									version(with_24_bit_color)
										currentAttributes.background = palette[args[1]];
									currentAttributes.backgroundIndex = cast(ushort) args[1];
								}

								break argsLoop;
							case 49:
							// default background color
								auto dflt = defaultTextAttributes();

								version(with_24_bit_color)
									currentAttributes.background = dflt.background;
								currentAttributes.backgroundIndex = dflt.backgroundIndex;
							break;
							case 51:
								// framed
							break;
							case 52:
								// encircled
							break;
							case 53:
								// overlined
							break;
							case 54:
								// not framed or encircled
							break;
							case 55:
								// not overlined
							break;
							case 90: .. case 97:
								// high intensity foreground color
							break;
							case 100: .. case 107:
								// high intensity background color
							break;
							default:
								unknownEscapeSequence(cast(string) esc);
						}
					break;
					case 'J':
						// erase in display
						auto arg = getArgs(0)[0];
						switch(arg) {
							case 0:
								TerminalCell plain;
								plain.ch = ' ';
								plain.attributes = currentAttributes;
								// erase below
								foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) {
									if(alternateScreenActive)
										alternateScreen[i] = plain;
									else
										normalScreen[i] = plain;
								}
							break;
							case 1:
								// erase above
								unknownEscapeSequence("FIXME");
							break;
							case 2:
								// erase all
								cls();
							break;
							default: unknownEscapeSequence(cast(string) esc);
						}
					break;
					case 'r':
						if(esc[1] != '?') {
							// set scrolling zone
							// default should be full size of window
							auto args = getArgs(1, screenHeight);

							// FIXME: these are supposed to be per-buffer
							scrollZoneTop = args[0] - 1;
							scrollZoneBottom = args[1] - 1;

							if(scrollZoneTop < 0)
								scrollZoneTop = 0;
							if(scrollZoneBottom > screenHeight)
								scrollZoneBottom = screenHeight - 1;
						} else {
							// restore... something FIXME
						}
					break;
					case 'h':
						if(esc[1] != '?')
						foreach(arg; getArgs())
						switch(arg) {
							case 4:
								insertMode = true;
							break;
							case 34:
								// no idea. vim inside screen sends it
							break;
							default: unknownEscapeSequence(cast(string) esc);
						}
						else
					//import std.stdio; writeln("h magic ", cast(string) esc);
						foreach(arg; getArgsBase(2, null)) {
							if(arg > 65535) {
								/* Extensions */
								if(arg < 65536 + 65535) {
									// activate hyperlink
									hyperlinkFlipper = !hyperlinkFlipper;
									hyperlinkActive = true;
									hyperlinkNumber = arg - 65536;
								}
							} else
							switch(arg) {
								case 1:
									// application cursor keys
									applicationCursorKeys = true;
								break;
								case 3:
									// 132 column mode
								break;
								case 4:
									// smooth scroll
								break;
								case 5:
									// reverse video
									reverseVideo = true;
								break;
								case 6:
									// origin mode
								break;
								case 7:
									// wraparound mode
									wraparoundMode = false;
									// 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
								break;
								case 9:
									allMouseTrackingOff();
									mouseButtonTracking = true;
								break;
								case 12:
									// start blinking cursor
								break;
								case 1034:
									// meta keys????
								break;
								case 1049:
									// Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first.
									alternateScreenActive = true;
									scrollLock = false;
									pushSavedCursor(cursorPosition);
									cls();
									notifyScrollbarRelevant(false, false);
								break;
								case 1000:
									// send mouse X&Y on button press and release
									allMouseTrackingOff();
									mouseButtonTracking = true;
									mouseButtonReleaseTracking = true;
								break;
								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
								break;
								case 1002:
									allMouseTrackingOff();
									mouseButtonTracking = true;
									mouseButtonReleaseTracking = true;
									mouseButtonMotionTracking = true;
									// use cell motion mouse tracking
								break;
								case 1003:
									// ALL motion is sent
									allMouseTrackingOff();
									mouseButtonTracking = true;
									mouseButtonReleaseTracking = true;
									mouseMotionTracking = true;
								break;
								case 1004:
									sendFocusEvents = true;
								break;
								case 1005:
									utf8MouseMode = true;
									// enable utf-8 mouse mode
									/*
UTF-8 (1005)
          This enables UTF-8 encoding for Cx and Cy under all tracking
          modes, expanding the maximum encodable position from 223 to
          2015.  For positions less than 95, the resulting output is
          identical under both modes.  Under extended mouse mode, posi-
          tions greater than 95 generate "extra" bytes which will con-
          fuse applications which do not treat their input as a UTF-8
          stream.  Likewise, Cb will be UTF-8 encoded, to reduce confu-
          sion with wheel mouse events.
          Under normal mouse mode, positions outside (160,94) result in
          byte pairs which can be interpreted as a single UTF-8 charac-
          ter; applications which do treat their input as UTF-8 will
          almost certainly be confused unless extended mouse mode is
          active.
          This scheme has the drawback that the encoded coordinates will
          not pass through luit unchanged, e.g., for locales using non-
          UTF-8 encoding.
									*/
								break;
								case 1006:
								/*
SGR (1006)
          The normal mouse response is altered to use CSI < followed by
          semicolon-separated encoded button value, the Cx and Cy ordi-
          nates and a final character which is M  for button press and m
          for button release.
          o The encoded button value in this case does not add 32 since
            that was useful only in the X10 scheme for ensuring that the
            byte containing the button value is a printable code.
          o The modifiers are encoded in the same way.
          o A different final character is used for button release to
            resolve the X10 ambiguity regarding which button was
            released.
          The highlight tracking responses are also modified to an SGR-
          like format, using the same SGR-style scheme and button-encod-
          ings.
								*/
								break;
								case 1014:
									// ARSD extension: it is 1002 but selective, only
									// on top row, row with cursor, or else if middle click/wheel.
									//
									// Quite specifically made for my getline function!
									allMouseTrackingOff();

									mouseButtonMotionTracking = true;
									mouseButtonTracking = true;
									mouseButtonReleaseTracking = true;
									selectiveMouseTracking = true;
								break;
								case 1015:
								/*
URXVT (1015)
          The normal mouse response is altered to use CSI followed by
          semicolon-separated encoded button value, the Cx and Cy ordi-
          nates and final character M .
          This uses the same button encoding as X10, but printing it as
          a decimal integer rather than as a single byte.
          However, CSI M  can be mistaken for DL (delete lines), while
          the highlight tracking CSI T  can be mistaken for SD (scroll
          down), and the Window manipulation controls.  For these rea-
          sons, the 1015 control is not recommended; it is not an
          improvement over 1005.
								*/
								break;
								case 1048:
									pushSavedCursor(cursorPosition);
								break;
								case 2004:
									bracketedPasteMode = true;
								break;
								case 3004:
									bracketedHyperlinkMode = true;
								break;
								case 1047:
								case 47:
									alternateScreenActive = true;
									scrollLock = false;
									cls();
									notifyScrollbarRelevant(false, false);
								break;
								case 25:
									cursorShowing = true;
								break;

								/* Done */
								default: unknownEscapeSequence(cast(string) esc);
							}
						}
					break;
					case 'p':
						// it is asking a question... and tbh i don't care.
					break;
					case 'l':
					//import std.stdio; writeln("l magic ", cast(string) esc);
						if(esc[1] != '?')
						foreach(arg; getArgs())
						switch(arg) {
							case 4:
								insertMode = false;
							break;
							case 34:
								// no idea. vim inside screen sends it
							break;
							case 1004:
								sendFocusEvents = false;
							break;
							case 1005:
								// turn off utf-8 mouse
								utf8MouseMode = false;
							break;
							case 1006:
								// turn off sgr mouse
							break;
							case 1015:
								// turn off urxvt mouse
							break;
							default: unknownEscapeSequence(cast(string) esc);
						}
						else
						foreach(arg; getArgsBase(2, null)) {
							if(arg > 65535) {
								/* Extensions */
								if(arg < 65536 + 65535)
									hyperlinkActive = false;
							} else
							switch(arg) {
								case 1:
									// normal cursor keys
									applicationCursorKeys = false;
								break;
								case 3:
									// 80 column mode
								break;
								case 4:
									// smooth scroll
								break;
								case 5:
									// normal video
									reverseVideo = false;
								break;
								case 6:
									// normal cursor mode
								break;
								case 7:
									// wraparound mode
									wraparoundMode = true;
								break;
								case 12:
									// stop blinking cursor
								break;
								case 1034:
									// meta keys????
								break;
								case 1049:
									cursorPosition = popSavedCursor;
									wraparoundMode = true;

									returnToNormalScreen();
								break;
								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
								break;
								case 9:
								case 1000:
								case 1002:
								case 1003:
								case 1014: // arsd extension
									allMouseTrackingOff();
								break;
								case 1005:
								case 1006:
									// idk
								break;
								case 1048:
									cursorPosition = popSavedCursor;
								break;
								case 2004:
									bracketedPasteMode = false;
								break;
								case 3004:
									bracketedHyperlinkMode = false;
								break;
								case 1047:
								case 47:
									returnToNormalScreen();
								break;
								case 25:
									cursorShowing = false;
								break;
								default: unknownEscapeSequence(cast(string) esc);
							}
						}
					break;
					case 'X':
						// erase characters
						auto count = getArgs(1)[0];
						TerminalCell plain;
						plain.ch = ' ';
						plain.attributes = currentAttributes;
						foreach(cnt; 0 .. count) {
							ASS[cursorY][cnt + cursorX] = plain;
						}
					break;
					case 'S':
						auto count = getArgs(1)[0];
						// scroll up
						scrollUp(count);
					break;
					case 'T':
						auto count = getArgs(1)[0];
						// scroll down
						scrollDown(count);
					break;
					case 'P':
						auto count = getArgs(1)[0];
						// delete characters

						foreach(cnt; 0 .. count) {
							for(int i = cursorX; i < this.screenWidth-1; i++) {
								if(ASS[cursorY][i].selected)
									clearSelection();
								ASS[cursorY][i] = ASS[cursorY][i + 1];
								ASS[cursorY][i].invalidated = true;
							}

							if(ASS[cursorY][this.screenWidth - 1].selected)
								clearSelection();
							ASS[cursorY][this.screenWidth-1].ch = ' ';
							ASS[cursorY][this.screenWidth-1].invalidated = true;
						}
					break;
					case '@':
						// insert blank characters
						auto count = getArgs(1)[0];
						foreach(idx; 0 .. count) {
							for(int i = this.screenWidth - 1; i > cursorX; i--) {
								ASS[cursorY][i] = ASS[cursorY][i - 1];
								ASS[cursorY][i].invalidated = true;
							}
							ASS[cursorY][cursorX].ch = ' ';
							ASS[cursorY][cursorX].invalidated = true;
						}
					break;
					case 'c':
						// send device attributes
						// FIXME: what am i supposed to do here?
						//sendToApplication("\033[>0;138;0c");
						//sendToApplication("\033[?62;");
						sendToApplication(terminalIdCode);
					break;
					default:
						// [42\esc] seems to have gotten here once somehow
						// also [24\esc]
						unknownEscapeSequence("" ~ cast(string) esc);
				}
			} else {
				unknownEscapeSequence(cast(string) esc);
			}
		}
	}
}

// These match the numbers in terminal.d, so you can just cast it back and forth
// and the names match simpledisplay.d so you can convert that automatically too
enum TerminalKey : int {
	Escape = 0x1b + 0xF0000, /// .
	F1 = 0x70 + 0xF0000, /// .
	F2 = 0x71 + 0xF0000, /// .
	F3 = 0x72 + 0xF0000, /// .
	F4 = 0x73 + 0xF0000, /// .
	F5 = 0x74 + 0xF0000, /// .
	F6 = 0x75 + 0xF0000, /// .
	F7 = 0x76 + 0xF0000, /// .
	F8 = 0x77 + 0xF0000, /// .
	F9 = 0x78 + 0xF0000, /// .
	F10 = 0x79 + 0xF0000, /// .
	F11 = 0x7A + 0xF0000, /// .
	F12 = 0x7B + 0xF0000, /// .
	Left = 0x25 + 0xF0000, /// .
	Right = 0x27 + 0xF0000, /// .
	Up = 0x26 + 0xF0000, /// .
	Down = 0x28 + 0xF0000, /// .
	Insert = 0x2d + 0xF0000, /// .
	Delete = 0x2e + 0xF0000, /// .
	Home = 0x24 + 0xF0000, /// .
	End = 0x23 + 0xF0000, /// .
	PageUp = 0x21 + 0xF0000, /// .
	PageDown = 0x22 + 0xF0000, /// .
	ScrollLock = 0x91 + 0xF0000, 
}

/* These match simpledisplay.d which match terminal.d, so you can just cast them */

enum MouseEventType : int {
	motion = 0,
	buttonPressed = 1,
	buttonReleased = 2,
}

enum MouseButton : int {
	// these names assume a right-handed mouse
	left = 1,
	right = 2,
	middle = 4,
	wheelUp = 8,
	wheelDown = 16,
}



/*
mixin template ImageSupport() {
	import arsd.png;
	import arsd.bmp;
}
*/


/* helper functions that are generally useful but not necessarily required */

version(use_libssh2) {
	import arsd.libssh2;
	void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) {

	int tries = 0;
	try_again:
	try {
		import std.socket;

		if(libssh2_init(0))
			throw new Exception("libssh2_init");
		scope(exit)
			libssh2_exit();

		auto socket = new Socket(AddressFamily.INET, SocketType.STREAM);
		socket.connect(new InternetAddress(host, port));
		scope(exit) socket.close();

		auto session = libssh2_session_init_ex(null, null, null, null);
		if(session is null) throw new Exception("init session");
		scope(exit)
			libssh2_session_disconnect_ex(session, 0, "normal", "EN");

		libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1);

		if(libssh2_session_handshake(session, socket.handle))
			throw new Exception("handshake");

		auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
		if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint)
			throw new Exception("fingerprint");

		import std.string : toStringz;
		if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
			throw new Exception("auth");


		auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0);

		if(channel is null)
			throw new Exception("channel open");

		scope(exit)
			libssh2_channel_free(channel);

		// libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length);

		if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0))
			throw new Exception("pty");

		if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0))
			throw new Exception("process_startup");

		libssh2_keepalive_config(session, 0, 60);
		libssh2_session_set_blocking(session, 0);

		masterFunc(socket, session, channel);
	} catch(Exception e) {
		if(e.msg == "handshake") {
			tries++;
			import core.thread;
			Thread.sleep(200.msecs);
			if(tries < 10)
				goto try_again;
		}

		throw e;
	}
	}

} else
version(Posix) {
	extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp);
	pragma(lib, "util");

	/// this is good
	void startChild(alias masterFunc)(string program, string[] args) {
		import core.sys.posix.termios;
		import core.sys.posix.signal;
		import core.sys.posix.sys.wait;
		__gshared static int childrenAlive = 0;
		extern(C) nothrow static @nogc
		void childdead(int) {
			childrenAlive--;

			wait(null);

			version(with_eventloop)
			try {
				import arsd.eventloop;
				if(childrenAlive <= 0)
					exit();
			} catch(Exception e){}
		}

		signal(SIGCHLD, &childdead);

		int master;
		int pid = forkpty(&master, null, null, null);
		if(pid == -1)
			throw new Exception("forkpty");
		if(pid == 0) {
			import std.process;
			environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes
			environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions

			import std.string;
			if(environment["LANG"].indexOf("UTF-8") == -1)
				environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?)

			import core.sys.posix.unistd;

			import core.stdc.stdlib;
			char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1));
			if(argv is null) throw new Exception("malloc");
			foreach(i, arg; args) {
				argv[i] = cast(char*) malloc(arg.length + 1);
				if(argv[i] is null) throw new Exception("malloc");
				argv[i][0 .. arg.length] = arg[];
				argv[i][arg.length] = 0;
			}

			argv[args.length] = null;

			core.sys.posix.unistd.execv(argv[0], argv);
		} else {
			childrenAlive = 1;
			masterFunc(master);
		}
	}
} else
version(Windows) {
	import core.sys.windows.windows;

	version(winpty) {
		alias HPCON = HANDLE;
		extern(Windows)
			HRESULT function(HPCON, COORD) ResizePseudoConsole;
		extern(Windows)
			HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole;
		extern(Windows)
			void function(HPCON) ClosePseudoConsole;
	}

	extern(Windows)
		BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD);
	extern(Windows)
		BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL);
	extern(Windows)
		private BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*);
	extern(Windows)
		BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);

	extern(Windows)
		BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM);
	extern(Windows)
		BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags);
	extern(Windows)
		BOOL SetHandleInformation(HANDLE, DWORD, DWORD);
	extern(Windows)
	HANDLE CreateNamedPipeA(
		const(char)* lpName,
		DWORD dwOpenMode,
		DWORD dwPipeMode,
		DWORD nMaxInstances,
		DWORD nOutBufferSize,
		DWORD nInBufferSize,
		DWORD nDefaultTimeOut,
		LPSECURITY_ATTRIBUTES lpSecurityAttributes
	);
	extern(Windows)
	BOOL UnregisterWait(HANDLE);

	struct STARTUPINFOEXA {
		STARTUPINFOA StartupInfo;
		void* lpAttributeList;
	}

	enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
	enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000;

	extern(Windows)
	BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T);
	extern(Windows)
	BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T);

	__gshared HANDLE waitHandle;
	__gshared bool childDead;
	extern(Windows)
	void childCallback(void* tidp, bool) {
		auto tid = cast(DWORD) tidp;
		UnregisterWait(waitHandle);

		PostThreadMessageA(tid, WM_QUIT, 0, 0);
		childDead = true;
		//stupidThreadAlive = false;
	}



	extern(Windows)
	void SetLastError(DWORD);

	/// this is good. best to call it with plink.exe so it can talk to unix
	/// note that plink asks for the password out of band, so it won't actually work like that.
	/// thus specify the password on the command line or better yet, use a private key file
	/// e.g.
	/// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\"");
	void startChild(alias masterFunc)(string program, string commandLine) {
		import core.sys.windows.windows;
		// thanks for a random person on stack overflow for this function
		static BOOL MyCreatePipeEx(
			PHANDLE lpReadPipe,
			PHANDLE lpWritePipe,
			LPSECURITY_ATTRIBUTES lpPipeAttributes,
			DWORD nSize,
			DWORD dwReadMode,
			DWORD dwWriteMode
		)
		{
			HANDLE ReadPipeHandle, WritePipeHandle;
			DWORD dwError;
			CHAR[MAX_PATH] PipeNameBuffer;

			if (nSize == 0) {
				nSize = 4096;
			}

			static int PipeSerialNumber = 0;

			import core.stdc.string;
			import core.stdc.stdio;

			sprintf(PipeNameBuffer.ptr,
				"\\\\.\\Pipe\\TerminalEmulatorPipe.%08x.%08x".ptr,
				GetCurrentProcessId(),
				PipeSerialNumber++
			);

			ReadPipeHandle = CreateNamedPipeA(
				PipeNameBuffer.ptr,
				1/*PIPE_ACCESS_INBOUND*/ | dwReadMode,
				0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/,
				1,             // Number of pipes
				nSize,         // Out buffer size
				nSize,         // In buffer size
				120 * 1000,    // Timeout in ms
				lpPipeAttributes
			);

			if (! ReadPipeHandle) {
				return FALSE;
			}

			WritePipeHandle = CreateFileA(
				PipeNameBuffer.ptr,
				GENERIC_WRITE,
				0,                         // No sharing
				lpPipeAttributes,
				OPEN_EXISTING,
				FILE_ATTRIBUTE_NORMAL | dwWriteMode,
				null                       // Template file
			);

			if (INVALID_HANDLE_VALUE == WritePipeHandle) {
				dwError = GetLastError();
				CloseHandle( ReadPipeHandle );
				SetLastError(dwError);
				return FALSE;
			}

			*lpReadPipe = ReadPipeHandle;
			*lpWritePipe = WritePipeHandle;
			return( TRUE );
		}





		import std.conv;

		SECURITY_ATTRIBUTES saAttr;
		saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
		saAttr.bInheritHandle = true;
		saAttr.lpSecurityDescriptor = null;

		HANDLE inreadPipe;
		HANDLE inwritePipe;
		if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0)
			throw new Exception("CreatePipe");
		if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
			throw new Exception("SetHandleInformation");
		HANDLE outreadPipe;
		HANDLE outwritePipe;

		version(winpty)
			auto flags = 0;
		else
			auto flags = FILE_FLAG_OVERLAPPED;

		if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0)
			throw new Exception("CreatePipe");
		if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
			throw new Exception("SetHandleInformation");

		version(winpty) {

			auto lib = LoadLibrary("kernel32.dll");
			if(lib is null) throw new Exception("holy wtf batman");
			scope(exit) FreeLibrary(lib);

			CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole");
			ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole");
			ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole");

			if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null)
				throw new Exception("Windows pseudo console not available on this version");

			initPipeHack(outreadPipe);

			HPCON hpc;
			auto result = CreatePseudoConsole(
				COORD(80, 24),
				inreadPipe,
				outwritePipe,
				0, // flags
				&hpc
			);

			assert(result == S_OK);

			scope(exit)
				ClosePseudoConsole(hpc);
		}

		STARTUPINFOEXA siex;
		siex.StartupInfo.cb = siex.sizeof;

		version(winpty) {
			size_t size;
			InitializeProcThreadAttributeList(null, 1, 0, &size);
			ubyte[] wtf = new ubyte[](size);
			siex.lpAttributeList = wtf.ptr;
			InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size);
			UpdateProcThreadAttribute(
				siex.lpAttributeList,
				0,
				PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
				hpc,
				hpc.sizeof,
				null,
				null
			);
		} {//else {
			siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
			siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe;
			siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe;
			siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe;
		}

		PROCESS_INFORMATION pi;
		import std.conv;

		if(commandLine.length > 255)
			throw new Exception("command line too long");
		char[256] cmdLine;
		cmdLine[0 .. commandLine.length] = commandLine[];
		cmdLine[commandLine.length] = 0;
		import std.string;
		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)
			throw new Exception("CreateProcess " ~ to!string(GetLastError()));

		if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0)
			throw new Exception("RegisterWaitForSingleObject");

		version(winpty)
			masterFunc(hpc, inwritePipe, outreadPipe);
		else
			masterFunc(inwritePipe, outreadPipe);

		//stupidThreadAlive = false;

		//term.stupidThread.join();

		/* // FIXME: we should close but only if we're legit done
		// masterFunc typically runs an event loop but it might not.
		CloseHandle(inwritePipe);
		CloseHandle(outreadPipe);

		CloseHandle(pi.hThread);
		CloseHandle(pi.hProcess);
		*/
	}
}

/// Implementation of TerminalEmulator's abstract functions that forward them to output
mixin template ForwardVirtuals(alias writer) {
	static import arsd.color;

	protected override void changeCursorStyle(CursorStyle style) {
		// FIXME: this should probably just import utility
		final switch(style) {
			case TerminalEmulator.CursorStyle.block:
				writer("\033[2 q");
			break;
			case TerminalEmulator.CursorStyle.underline:
				writer("\033[4 q");
			break;
			case TerminalEmulator.CursorStyle.bar:
				writer("\033[6 q");
			break;
		}
	}

	protected override void changeWindowTitle(string t) {
		import std.process;
		if(t.length && environment["TERM"] != "linux")
			writer("\033]0;"~t~"\007");
	}

	protected override void changeWindowIcon(arsd.color.IndexedImage t) {
		if(t !is null) {
			// forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux
			import std.process;
			if(environment["TERM"] != "linux")
				writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007");
		}
	}

	protected override void changeIconTitle(string) {} // FIXME
	protected override void changeTextAttributes(TextAttributes) {} // FIXME
	protected override void soundBell() {
		writer("\007");
	}
	protected override void demandAttention() {
		import std.process;
		if(environment["TERM"] != "linux")
			writer("\033]5001;1\007"); // the 1 there means true but is currently ignored
	}
	protected override void copyToClipboard(string text) {
		// this is xterm compatible, though xterm rarely implements it
		import std.base64;
				// idk why the cast is needed here
		writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007");
	}
	protected override void pasteFromClipboard(void delegate(in char[]) dg) {
		// this is a slight extension. xterm invented the string - it means request the primary selection -
		// but it generally doesn't actually get a reply. so i'm using it to request the primary which will be
		// sent as a pasted strong.
		// (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.)
		writer("\033]52;c;?\007");
	}
	protected override void copyToPrimary(string text) {
		import std.base64;
		writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007");
	}
	protected override void pasteFromPrimary(void delegate(in char[]) dg) {
		writer("\033]52;p;?\007");
	}

}

/// you can pass this as PtySupport's arguments when you just don't care
final void doNothing() {}

version(winpty) {
		__gshared static HANDLE inputEvent;
		__gshared static HANDLE magicEvent;
		__gshared static ubyte[] helperBuffer;
		__gshared static HANDLE helperThread;

		static void initPipeHack(void* ptr) {
			inputEvent = CreateEvent(null, false, false, null);
			assert(inputEvent !is null);
			magicEvent = CreateEvent(null, false, true, null);
			assert(magicEvent !is null);

			helperThread = CreateThread(
				null,
				0,
				&actuallyRead,
				ptr,
				0,
				null
			);

			assert(helperThread !is null);
		}

		extern(Windows) static
		uint actuallyRead(void* ptr) {
			ubyte[4096] buffer;
			DWORD got;
			while(true) {
				// wait for the other thread to tell us they
				// are done...
				WaitForSingleObject(magicEvent, INFINITE);
				auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null);
				helperBuffer = buffer[0 .. got];
				// tells the other thread it is allowed to read
				// readyToReadPty
				SetEvent(inputEvent);
			}
			assert(0);
		}


}

/// You must implement a function called redraw() and initialize the members in your constructor
mixin template PtySupport(alias resizeHelper) {
	// Initialize these!

	final void redraw_() {
		if(invalidateAll) {
			if(alternateScreenActive)
				foreach(ref t; alternateScreen)
					t.invalidated = true;
			else
				foreach(ref t; normalScreen)
					t.invalidated = true;
			invalidateAll = false;
		}
		redraw();
		//soundBell();
	}

	version(use_libssh2) {
		import arsd.libssh2;
		LIBSSH2_CHANNEL* sshChannel;
	} else version(Windows) {
		import core.sys.windows.windows;
		HANDLE stdin;
		HANDLE stdout;
	} else version(Posix) {
		int master;
	}

	version(use_libssh2) { }
	else version(Posix) {
		int previousProcess = 0;
		int activeProcess = 0;
		int activeProcessWhenResized = 0;
		bool resizedRecently;

		/*
			so, this isn't perfect, but it is meant to send the resize signal to an existing process
			when it isn't in the front when you resize.

			For example, open vim and resize. Then exit vim. We want bash to be updated.

			But also don't want to do too many spurious signals.

			It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but
			the outer bash won't see it. I guess I need some kind of process stack.

			but it is okish.
		*/
		override void outputOccurred() {
			import core.sys.posix.unistd;
			auto pgrp = tcgetpgrp(master);
			if(pgrp != -1) {
				if(pgrp != activeProcess) {
					auto previousProcessAtStartup = previousProcess;

					previousProcess = activeProcess;
					activeProcess = pgrp;

					if(resizedRecently) {
						if(activeProcess != activeProcessWhenResized) {
							resizedRecently = false;

							if(activeProcess == previousProcessAtStartup) {
								//import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight);

								import core.sys.posix.signal;
								kill(-activeProcess, 28 /* 28 == SIGWINCH*/);
							}
						}
					}
				}
			}


			super.outputOccurred();
		}
		//return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline");
	}


	override void resizeTerminal(int w, int h) {
		version(Posix) {
			activeProcessWhenResized = activeProcess;
			resizedRecently = true;
		}

		resizeHelper();

		super.resizeTerminal(w, h);

		version(use_libssh2) {
			libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0);
		} else version(Posix) {
			import core.sys.posix.sys.ioctl;
			winsize win;
			win.ws_col = cast(ushort) w;
			win.ws_row = cast(ushort) h;

			ioctl(master, TIOCSWINSZ, &win);
		} else version(Windows) {
			version(winpty) {
				COORD coord;
				coord.X = cast(ushort) w;
				coord.Y = cast(ushort) h;
				ResizePseudoConsole(hpc, coord);
			} else {
				sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]);
			}
		} else static assert(0);
	}

	protected override void sendToApplication(scope const(void)[] data) {
		version(use_libssh2) {
			while(data.length) {
				auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length);
				if(sent < 0)
					throw new Exception("libssh2_channel_write_ex");
				data = data[sent .. $];
			}
		} else version(Windows) {
			import std.conv;
			uint written;
			if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0)
				throw new Exception("WriteFile " ~ to!string(GetLastError()));
		} else version(Posix) {
			import core.sys.posix.unistd;
			int frozen;
			while(data.length) {
				enum MAX_SEND = 1024 * 20;
				auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length);
				//import std.stdio; writeln("ROFL ", sent, " ", data.length);

				import core.stdc.errno;
				if(sent == -1 && errno == 11) {
					import core.thread;
					if(frozen == 50)
						throw new Exception("write froze up");
					frozen++;
					Thread.sleep(10.msecs);
					//import std.stdio; writeln("lol");
					continue; // just try again
				}

				frozen = 0;

				import std.conv;
				if(sent < 0)
					throw new Exception("write " ~ to!string(errno));

				data = data[sent .. $];
			}
		} else static assert(0);
	}

	version(use_libssh2) {
		int readyToRead(int fd) {
			int count = 0; // if too much stuff comes at once, we still want to be responsive
			while(true) {
				ubyte[4096] buffer;
				auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length);
				if(got == LIBSSH2_ERROR_EAGAIN)
					break; // got it all for now
				if(got < 0)
					throw new Exception("libssh2_channel_read_ex");
				if(got == 0)
					break; // NOT an error!

				super.sendRawInput(buffer[0 .. got]);
				count++;

				if(count == 5) {
					count = 0;
					redraw_();
					justRead();
				}
			}

			if(libssh2_channel_eof(sshChannel)) {
				libssh2_channel_close(sshChannel);
				libssh2_channel_wait_closed(sshChannel);

				return 1;
			}

			if(count != 0) {
				redraw_();
				justRead();
			}
			return 0;
		}
	} else version(winpty) {
		void readyToReadPty() {
			super.sendRawInput(helperBuffer);
			SetEvent(magicEvent); // tell the other thread we have finished
			redraw_();
			justRead();
		}
	} else version(Windows) {
		OVERLAPPED* overlapped;
		bool overlappedBufferLocked;
		ubyte[4096] overlappedBuffer;
		extern(Windows)
		static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) {
			assert(overlapped !is null);
			typeof(this) w = cast(typeof(this)) overlapped.hEvent;

			if(numberOfBytes) {
				w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]);
				w.redraw_();
			}
			import std.conv;

			if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) {
				if(GetLastError() == 997)
				{ } // there's pending i/o, let's just ignore for now and it should tell us later that it completed
				else
				throw new Exception("ReadFileEx " ~ to!string(GetLastError()));
			} else {
			}

			w.justRead();
		}
	} else version(Posix) {
		void readyToRead(int fd) {
			import core.sys.posix.unistd;
			ubyte[4096] buffer;

			// the count is to limit how long we spend in this loop
			// when it runs out, it goes back to the main event loop
			// for a while (btw use level triggered events so the remaining
			// data continues to get processed!) giving a chance to redraw
			// and process user input periodically during insanely long and
			// rapid output.
			int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests

			version(arsd_te_conservative_draws)
				cnt = 400;

			// FIXME: if connected by ssh, up the count so we don't redraw as frequently.
			// it'd save bandwidth

			while(--cnt) {
				auto len = read(fd, buffer.ptr, 4096);
				if(len < 0) {
					import core.stdc.errno;
					if(errno == EAGAIN || errno == EWOULDBLOCK) {
						break; // we got it all
					} else {
						//import std.conv;
						//throw new Exception("read failed " ~ to!string(errno));
						return;
					}
				}

				if(len == 0) {
					close(fd);
					requestExit();
					break;
				}

				auto data = buffer[0 .. len];

				if(debugMode) {
					import std.array; import std.stdio; writeln("GOT ", data, "\nOR ", 
						replace(cast(string) data, "\033", "\\")
						.replace("\010", "^H")
						.replace("\r", "^M")
						.replace("\n", "^J")
						);
				}
				super.sendRawInput(data);
			}

			outputOccurred();

			redraw_();

			// HACK: I don't even know why this works, but with this
			// sleep in place, it gives X events from that socket a
			// chance to be processed. It can add a few seconds to a huge
			// output (like `find /usr`), but meh, that's worth it to me
			// to have a chance to ctrl+c.
			import core.thread;
			Thread.sleep(dur!"msecs"(5));

			justRead();
		}
	}
}

mixin template SdpyImageSupport() {
	class NonCharacterData_Image : NonCharacterData {
		Image data;
		int imageOffsetX;
		int imageOffsetY;

		this(Image data, int x, int y) {
			this.data = data;
			this.imageOffsetX = x;
			this.imageOffsetY = y;
		}
	}

	version(TerminalDirectToEmulator)
	class NonCharacterData_Widget : NonCharacterData {
		this(void* data, size_t idx, int width, int height) {
			this.window = cast(SimpleWindow) data;
			this.idx = idx;
			this.width = width;
			this.height = height;
		}

		void position(int posx, int posy, int width, int height) {
			if(posx == this.posx && posy == this.posy && width == this.pixelWidth && height == this.pixelHeight)
				return;
			this.posx = posx;
			this.posy = posy;
			this.pixelWidth = width;
			this.pixelHeight = height;

			window.moveResize(posx, posy, width, height);
			import std.stdio; writeln(posx, " ", posy, " ", width, " ", height);

			auto painter = this.window.draw;
			painter.outlineColor = Color.red;
			painter.fillColor = Color.green;
			painter.drawRectangle(Point(0, 0), width, height);


		}

		SimpleWindow window;
		size_t idx;
		int width;
		int height;

		int posx;
		int posy;
		int pixelWidth;
		int pixelHeight;
	}

	private struct CachedImage {
		ulong hash;
		BinaryDataTerminalRepresentation bui;
		int timesSeen;
		import core.time;
		MonoTime lastUsed;
	}
	private CachedImage[] imageCache;
	private CachedImage* findInCache(ulong hash) {
		if(hash == 0)
			return null;

		/*
		import std.stdio;
		writeln("***");
		foreach(cache; imageCache) {
			writeln(cache.hash, " ", cache.timesSeen, " ", cache.lastUsed);
		}
		*/

		foreach(ref i; imageCache)
			if(i.hash == hash) {
				import core.time;
				i.lastUsed = MonoTime.currTime;
				i.timesSeen++;
				return &i;
			}
		return null;
	}
	private BinaryDataTerminalRepresentation addImageCache(ulong hash, BinaryDataTerminalRepresentation bui) {
		import core.time;
		if(imageCache.length == 0)
			imageCache.length = 8;

		auto now = MonoTime.currTime;

		size_t oldestIndex;
		MonoTime oldestTime = now;

		size_t leastUsedIndex;
		int leastUsedCount = int.max;
		foreach(idx, ref cached; imageCache) {
			if(cached.hash == 0) {
				cached.hash = hash;
				cached.bui = bui;
				cached.timesSeen = 1;
				cached.lastUsed = now;

				return bui;
			} else {
				if(cached.timesSeen < leastUsedCount) {
					leastUsedCount = cached.timesSeen;
					leastUsedIndex = idx;
				}
				if(cached.lastUsed < oldestTime) {
					oldestTime = cached.lastUsed;
					oldestIndex = idx;
				}
			}
		}

		// need to overwrite one of the cached items, I'll just use the oldest one here
		// but maybe that could be smarter later

		imageCache[oldestIndex].hash = hash;
		imageCache[oldestIndex].bui = bui;
		imageCache[oldestIndex].timesSeen = 1;
		imageCache[oldestIndex].lastUsed = now;

		return bui;
	}

	// It has a cache of the 8 most recently used items right now so if there's a loop of 9 you get pwned
	// but still the cache does an ok job at helping things while balancing out the big memory consumption it
	// could do if just left to grow and grow. i hope.
	protected override BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[] binaryData) {

		version(none) {
		//version(TerminalDirectToEmulator)
		//if(binaryData.length == size_t.sizeof + 10) {
			//if((cast(uint[]) binaryData[0 .. 4])[0] == 0xdeadbeef && (cast(uint[]) binaryData[$-4 .. $])[0] == 0xabcdef32) {
				//auto widthInCharacterCells = binaryData[4];
				//auto heightInCharacterCells = binaryData[5];
				//auto pointer = (cast(void*[]) binaryData[6 .. $-4])[0];

				auto widthInCharacterCells = 30;
				auto heightInCharacterCells = 20;
				SimpleWindow pwin;
				foreach(k, v; SimpleWindow.nativeMapping) {
					if(v.type == WindowTypes.normal)
					pwin = v;
				}
				auto pointer = cast(void*) (new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.automaticallyScaleIfPossible, WindowTypes.nestedChild, WindowFlags.normal, pwin));

				BinaryDataTerminalRepresentation bi;
				bi.width = widthInCharacterCells;
				bi.height = heightInCharacterCells;
				bi.representation.length = bi.width * bi.height;

				foreach(idx, ref cell; bi.representation) {
					cell.nonCharacterData = new NonCharacterData_Widget(pointer, idx, widthInCharacterCells, heightInCharacterCells);
				}

				return bi;
			//}
		}

		import std.digest.md;

		ulong hash = * (cast(ulong*) md5Of(binaryData).ptr);

		if(auto cached = findInCache(hash))
			return cached.bui;

		TrueColorImage mi;

		if(binaryData.length > 8 && binaryData[1] == 'P' && binaryData[2] == 'N' && binaryData[3] == 'G') {
			import arsd.png;
			mi = imageFromPng(readPng(binaryData)).getAsTrueColorImage();
		} else if(binaryData.length > 8 && binaryData[0] == 'B' && binaryData[1] == 'M') {
			import arsd.bmp;
			mi = readBmp(binaryData).getAsTrueColorImage();
		} else if(binaryData.length > 2 && binaryData[0] == 0xff && binaryData[1] == 0xd8) {
			import arsd.jpeg;
			mi = readJpegFromMemory(binaryData).getAsTrueColorImage();
		} else if(binaryData.length > 2 && binaryData[0] == '<') {
			import arsd.svg;
			NSVG* image = nsvgParse(cast(const(char)[]) binaryData);
			if(image is null)
				return BinaryDataTerminalRepresentation();

			int w = cast(int) image.width + 1;
			int h = cast(int) image.height + 1;
			NSVGrasterizer rast = nsvgCreateRasterizer();
			mi = new TrueColorImage(w, h);
			rasterize(rast, image, 0, 0, 1, mi.imageData.bytes.ptr, w, h, w*4);
			image.kill();
		} else {
			return BinaryDataTerminalRepresentation();
		}

		BinaryDataTerminalRepresentation bi;
		bi.width = mi.width / fontWidth + ((mi.width%fontWidth) ? 1 : 0);
		bi.height = mi.height / fontHeight + ((mi.height%fontHeight) ? 1 : 0);

		bi.representation.length = bi.width * bi.height;

		Image data = Image.fromMemoryImage(mi);

		int ix, iy;
		foreach(ref cell; bi.representation) {
			/*
			Image data = new Image(fontWidth, fontHeight);
			foreach(y; 0 .. fontHeight) {
				foreach(x; 0 .. fontWidth) {
					if(x + ix >= mi.width || y + iy >= mi.height) {
						data.putPixel(x, y, defaultTextAttributes.background);
						continue;
					}
					data.putPixel(x, y, mi.imageData.colors[(iy + y) * mi.width + (ix + x)]);
				}
			}
			*/

			cell.nonCharacterData = new NonCharacterData_Image(data, ix, iy);

			ix += fontWidth;

			if(ix >= mi.width) {
				ix = 0;
				iy += fontHeight;
			}
		}

		return addImageCache(hash, bi);
		//return bi;
	}

}

// this assumes you have imported arsd.simpledisplay and/or arsd.minigui in the mixin scope
mixin template SdpyDraw() {

	// black bg, make the colors more visible
	static Color contrastify(Color c) {
		if(c == Color(0xcd, 0, 0))
			return Color.fromHsl(0, 1.0, 0.75);
		else if(c == Color(0, 0, 0xcd))
			return Color.fromHsl(240, 1.0, 0.75);
		else if(c == Color(229, 229, 229))
			return Color(0x99, 0x99, 0x99);
		else if(c == Color.black)
			return Color(128, 128, 128);
		else return c;
	}

	// white bg, make them more visible
	static Color antiContrastify(Color c) {
		if(c == Color(0xcd, 0xcd, 0))
			return Color.fromHsl(60, 1.0, 0.25);
		else if(c == Color(0, 0xcd, 0xcd))
			return Color.fromHsl(180, 1.0, 0.25);
		else if(c == Color(229, 229, 229))
			return Color(0x99, 0x99, 0x99);
		else if(c == Color.white)
			return Color(128, 128, 128);
		else return c;
	}

	struct SRectangle {
		int left;
		int top;
		int right;
		int bottom;
	}

	mixin SdpyImageSupport;

	OperatingSystemFont font;
	int fontWidth;
	int fontHeight;

	enum paddingLeft = 2;
	enum paddingTop = 1;

	void loadDefaultFont(int size = 14) {
		static if(UsingSimpledisplayX11) {
			font = new OperatingSystemFont("core:fixed", size, FontWeight.medium);
			//font = new OperatingSystemFont("monospace", size, FontWeight.medium);
			if(font.isNull) {
				// didn't work, it is using a
				// fallback, prolly fixed-13 is best
				font = new OperatingSystemFont("core:fixed", 13, FontWeight.medium);
			}
		} else version(Windows) {
			this.font = new OperatingSystemFont("Courier New", size, FontWeight.medium);
			if(!this.font.isNull && !this.font.isMonospace) 
				this.font.unload(); // non-monospace fonts are unusable here. This should never happen anyway though as Courier New comes with Windows
		}

		if(font.isNull) {
			// no way to really tell... just guess so it doesn't crash but like eeek.
			fontWidth = size / 2;
			fontHeight = size;
		} else {
			fontWidth = font.averageWidth;
			fontHeight = font.height;
		}
	}

	bool lastDrawAlternativeScreen;
	final SRectangle redrawPainter(T)(T painter, bool forceRedraw) {
		SRectangle invalidated;

		// FIXME: anything we can do to make this faster is good
		// on both, the XImagePainter could use optimizations
		// on both, drawing blocks would probably be good too - not just one cell at a time, find whole blocks of stuff
		// on both it might also be good to keep scroll commands high level somehow. idk.

		// FIXME on Windows it would definitely help a lot to do just one ExtTextOutW per line, if possible. the current code is brutally slow

		// Or also see https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-polytextoutw

		static if(is(T == WidgetPainter) || is(T == ScreenPainter)) {
			if(font)
				painter.setFont(font);
		}


		int posx = paddingLeft;
		int posy = paddingTop;


		char[512] bufferText;
		bool hasBufferedInfo;
		int bufferTextLength;
		Color bufferForeground;
		Color bufferBackground;
		int bufferX = -1;
		int bufferY = -1;
		bool bufferReverse;
		void flushBuffer() {
			if(!hasBufferedInfo) {
				return;
			}

			assert(posx - bufferX - 1 > 0);

			painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
			painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;

			painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
			painter.fillColor = Color.transparent;
			// Hack for contrast!
			if(bufferBackground == Color.black && !bufferReverse) {
				// brighter than normal in some cases so i can read it easily
				painter.outlineColor = contrastify(bufferForeground);
			} else if(bufferBackground == Color.white && !bufferReverse) {
				// darker than normal so i can read it
				painter.outlineColor = antiContrastify(bufferForeground);
			} else if(bufferForeground == bufferBackground) {
				// color on itself, I want it visible too
				auto hsl = toHsl(bufferForeground, true);
				if(hsl[2] < 0.5)
					hsl[2] += 0.5;
				else
					hsl[2] -= 0.5;
				painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);

			} else {
				// normal
				painter.outlineColor = bufferReverse ? bufferBackground : bufferForeground;
			}

			// FIXME: make sure this clips correctly
			painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);

			hasBufferedInfo = false;

			bufferReverse = false;
			bufferTextLength = 0;
			bufferX = -1;
			bufferY = -1;
		}



		int x;
		foreach(idx, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
			if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
				flushBuffer();
				goto skipDrawing;
			}
			cell.invalidated = false;
			version(none) if(bufferX == -1) { // why was this ever here?
				bufferX = posx;
				bufferY = posy;
			}

			if(!cell.hasNonCharacterData) {

				invalidated.left = posx < invalidated.left ? posx : invalidated.left;
				invalidated.top = posy < invalidated.top ? posy : invalidated.top;
				int xmax = posx + fontWidth;
				int ymax = posy + fontHeight;
				invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
				invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;

				// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
				{

					bool reverse = (cell.attributes.inverse != reverseVideo);
					if(cell.selected)
						reverse = !reverse;

					version(with_24_bit_color) {
						auto fgc = cell.attributes.foreground;
						auto bgc = cell.attributes.background;

						if(!(cell.attributes.foregroundIndex & 0xff00)) {
							// this refers to a specific palette entry, which may change, so we should use that
							fgc = palette[cell.attributes.foregroundIndex];
						}
						if(!(cell.attributes.backgroundIndex & 0xff00)) {
							// this refers to a specific palette entry, which may change, so we should use that
							bgc = palette[cell.attributes.backgroundIndex];
						}

					} else {
						auto fgc = cell.attributes.foregroundIndex == 256 ? defaultForeground : palette[cell.attributes.foregroundIndex & 0xff];
						auto bgc = cell.attributes.backgroundIndex == 256 ? defaultBackground : palette[cell.attributes.backgroundIndex & 0xff];
					}

					if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
						flushBuffer();
					bufferReverse = reverse;
					bufferBackground = bgc;
					bufferForeground = fgc;
				}
			}

				if(!cell.hasNonCharacterData) {
					char[4] str;
					import std.utf;
					// now that it is buffered, we do want to draw it this way...
					//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
						try {
							auto stride = encode(str, cell.ch);
							if(bufferTextLength + stride > bufferText.length)
								flushBuffer();
							bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
							bufferTextLength += stride;

							if(bufferX == -1) {
								bufferX = posx;
								bufferY = posy;
							}
							hasBufferedInfo = true;
						} catch(Exception e) {
							// import std.stdio; writeln(cast(uint) cell.ch, " :: ", e.msg);
						}
					//}
				} else if(cell.nonCharacterData !is null) {
					//import std.stdio; writeln(cast(void*) cell.nonCharacterData);
					if(auto ncdi = cast(NonCharacterData_Image) cell.nonCharacterData) {
						flushBuffer();
						painter.outlineColor = defaultBackground;
						painter.fillColor = defaultBackground;
						painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
						painter.drawImage(Point(posx, posy), ncdi.data, Point(ncdi.imageOffsetX, ncdi.imageOffsetY), fontWidth, fontHeight);
					}
					version(TerminalDirectToEmulator)
					if(auto wdi = cast(NonCharacterData_Widget) cell.nonCharacterData) {
						flushBuffer();
						if(wdi.idx == 0) {
							wdi.position(posx, posy, fontWidth * wdi.width, fontHeight * wdi.height);
							/*
							painter.outlineColor = defaultBackground;
							painter.fillColor = defaultBackground;
							painter.drawRectangle(Point(posx, posy), fontWidth, fontHeight);
							*/
						}

					}
				}

				if(!cell.hasNonCharacterData)
				if(cell.attributes.underlined) {
					// the posx adjustment is because the buffer assumes it is going
					// to be flushed after advancing, but here, we're doing it mid-character
					// FIXME: we should just underline the whole thing consecutively, with the buffer
					posx += fontWidth;
					flushBuffer();
					posx -= fontWidth;
					painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
				}
			skipDrawing:

				posx += fontWidth;
			x++;
			if(x == screenWidth) {
				flushBuffer();
				x = 0;
				posy += fontHeight;
				posx = paddingLeft;
			}
		}

		if(cursorShowing) {
			painter.fillColor = cursorColor;
			painter.outlineColor = cursorColor;
			painter.rasterOp = RasterOp.xor;

			posx = cursorPosition.x * fontWidth + paddingLeft;
			posy = cursorPosition.y * fontHeight + paddingTop;

			int cursorWidth = fontWidth;
			int cursorHeight = fontHeight;

			final switch(cursorStyle) {
				case CursorStyle.block:
					painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
				break;
				case CursorStyle.underline:
					painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
				break;
				case CursorStyle.bar:
					painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
				break;
			}
			painter.rasterOp = RasterOp.normal;

			// since the cursor draws over the cell, we need to make sure it is redrawn each time too
			auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
			if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
				(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
			}

			invalidated.left = posx < invalidated.left ? posx : invalidated.left;
			invalidated.top = posy < invalidated.top ? posy : invalidated.top;
			int xmax = posx + fontWidth;
			int ymax = xmax + fontHeight;
			invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
			invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
		}

		lastDrawAlternativeScreen = alternateScreenActive;

		return invalidated;
	}
}

string encodeSmallTextImage(IndexedImage ii) {
	char encodeNumeric(int c) {
		if(c < 10)
			return cast(char)(c + '0');
		if(c < 10 + 26)
			return cast(char)(c - 10 + 'a');
		assert(0);
	}

	string s;
	s ~= encodeNumeric(ii.width);
	s ~= encodeNumeric(ii.height);

	foreach(entry; ii.palette)
		s ~= entry.toRgbaHexString();
	s ~= "Z";

	ubyte rleByte;
	int rleCount;

	void rleCommit() {
		if(rleByte >= 26)
			assert(0); // too many colors for us to handle
		if(rleCount == 0)
			goto finish;
		if(rleCount == 1) {
			s ~= rleByte + 'a';
			goto finish;
		}

		import std.conv;
		s ~= to!string(rleCount);
		s ~= rleByte + 'a';

		finish:
			rleByte = 0;
			rleCount = 0;
	}

	foreach(b; ii.data) {
		if(b == rleByte)
			rleCount++;
		else {
			rleCommit();
			rleByte = b;
			rleCount = 1;
		}
	}

	rleCommit();

	return s;
}

IndexedImage readSmallTextImage(scope const(char)[] arg) {
	auto origArg = arg;
	int width;
	int height;

	int readNumeric(char c) {
		if(c >= '0' && c <= '9')
			return c - '0';
		if(c >= 'a' && c <= 'z')
			return c - 'a' + 10;
		return 0;
	}

	if(arg.length > 2) {
		width = readNumeric(arg[0]);
		height = readNumeric(arg[1]);
		arg = arg[2 .. $];
	}

	import std.conv;
	assert(width == 16, to!string(width));
	assert(height == 16, to!string(width));

	Color[] palette;
	ubyte[256] data;
	int didx = 0;
	bool readingPalette = true;
	outer: while(arg.length) {
		if(readingPalette) {
			if(arg[0] == 'Z') {
				readingPalette = false;
				arg = arg[1 .. $];
				continue;
			}
			if(arg.length < 8)
				break;
			foreach(a; arg[0..8]) {
				// if not strict hex, forget it
				if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z')))
					break outer;
			}
			palette ~= Color.fromString(arg[0 .. 8]);
			arg = arg[8 .. $];
		} else {
			char[3] rleChars;
			int rlePos;
			while(arg.length && arg[0] >= '0' && arg[0] <= '9') {
				rleChars[rlePos] = arg[0];
				arg = arg[1 .. $];
				rlePos++;
				if(rlePos >= rleChars.length)
					break;
			}
			if(arg.length == 0)
				break;

			int rle;
			if(rlePos == 0)
				rle = 1;
			else {
				// 100
				// rleChars[0] == '1'
				foreach(c; rleChars[0 .. rlePos]) {
					rle *= 10;
					rle += c - '0';
				}
			}

			foreach(i; 0 .. rle) {
				if(arg[0] >= 'a' && arg[0] <= 'z')
					data[didx] = cast(ubyte)(arg[0] - 'a');

				didx++;
				if(didx == data.length)
					break outer;
			}

			arg = arg[1 .. $];
		}
	}

	// width, height, palette, data is set up now

	if(palette.length) {
		auto ii = new IndexedImage(width, height);
		ii.palette = palette;
		ii.data = data.dup;

		return ii;
	}// else assert(0, origArg);
	return null;
}


// workaround dmd bug fixed in next release
//static immutable Color[256] xtermPalette = [
immutable(Color)[] xtermPalette() {

	// This is an approximation too for a few entries, but a very close one.
	Color xtermPaletteIndexToColor(int paletteIdx) {
		Color color;
		color.a = 255;

		if(paletteIdx < 16) {
			if(paletteIdx == 7)
				return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see
			else if(paletteIdx == 8)
				return Color(0x80, 0x80, 0x80);

			// real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see
			color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
			color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
			color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;

		} else if(paletteIdx < 232) {
			// color ramp, 6x6x6 cube
			color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55);
			color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55);
			color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55);

			if(color.r == 55) color.r = 0;
			if(color.g == 55) color.g = 0;
			if(color.b == 55) color.b = 0;
		} else {
			// greyscale ramp, from 0x8 to 0xee
			color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10);
			color.g = color.r;
			color.b = color.g;
		}

		return color;
	}

	static immutable(Color)[] ret;
	if(ret.length == 256)
		return ret;

	ret.reserve(256);
	foreach(i; 0 .. 256)
		ret ~= xtermPaletteIndexToColor(i);

	return ret;
}

static shared immutable dchar[dchar] lineDrawingCharacterSet;
shared static this() {
	lineDrawingCharacterSet = [
		'a' : ':',
		'j' : '+',
		'k' : '+',
		'l' : '+',
		'm' : '+',
		'n' : '+',
		'q' : '-',
		't' : '+',
		'u' : '+',
		'v' : '+',
		'w' : '+',
		'x' : '|',
	];

	// this is what they SHOULD be but the font i use doesn't support all these
	// the ascii fallback above looks pretty good anyway though.
	version(none)
	lineDrawingCharacterSet = [
		'a' : '\u2592',
		'j' : '\u2518',
		'k' : '\u2510',
		'l' : '\u250c',
		'm' : '\u2514',
		'n' : '\u253c',
		'q' : '\u2500',
		't' : '\u251c',
		'u' : '\u2524',
		'v' : '\u2534',
		'w' : '\u252c',
		'x' : '\u2502',
	];
}

/+
Copyright: Adam D. Ruppe, 2013 - 2020
License:   [http://www.boost.org/LICENSE_1_0.txt|Boost Software License 1.0]
Authors: Adam D. Ruppe
+/
