1 /**
2 	FIXME: writing a line in color then a line in ordinary does something
3 	wrong.
4 
5 	FIXME: make shift+enter send something special to the application
6 		and shift+space, etc.
7 		identify itself somehow too for client extensions
8 
9 
10 	There should be a redraw thing that is given batches of instructions
11 	in here that the other thing just implements.
12 
13 	FIXME: the save stack stuff should do cursor style too
14 
15 	This is an extendible unix terminal emulator and some helper functions to help actually implement one.
16 
17 	You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
18 
19 	See nestedterminalemulator.d or main.d for how I did it.
20 */
21 module arsd.terminalemulator;
22 
23 import arsd.color;
24 import std.algorithm : max;
25 
26 enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:";
27 
28 /+
29 	The ;90 ones are my extensions.
30 
31 	90 - clipboard extensions
32 	91 - image extensions
33 +/
34 enum terminalIdCode = "\033[?64;1;2;6;9;15;16;17;18;21;22;28;90;91c";
35 
36 interface NonCharacterData {
37 	//const(ubyte)[] serialize();
38 }
39 
40 struct BrokenUpImage {
41 	int width;
42 	int height;
43 	TerminalEmulator.TerminalCell[] representation;
44 }
45 
46 struct CustomGlyph {
47 	TrueColorImage image;
48 	dchar substitute;
49 }
50 
51 void unknownEscapeSequence(in char[] esc) {
52 	import std.file;
53 	version(Posix) {
54 		debug append("/tmp/arsd-te-bad-esc-sequences.txt", esc ~ "\n");
55 	} else {
56 		debug append("arsd-te-bad-esc-sequences.txt", esc ~ "\n");
57 	}
58 }
59 
60 // This is used for the double-click word selection
61 bool isWordSeparator(dchar ch) {
62 	return ch == ' ' || ch == '"' || ch == '<' || ch == '>' || ch == '(' || ch == ')' || ch == ',';
63 }
64 
65 struct ScopeBuffer(T, size_t maxSize) {
66 	T[maxSize] buffer;
67 	size_t length;
68 	bool isNull = true;
69 	T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
70 	void opOpAssign(string op : "~")(in T rhs) {
71 		isNull = false;
72 		if(this.length < buffer.length) // i am silently discarding more crap
73 			buffer[this.length++] = rhs;
74 	}
75 	void opOpAssign(string op : "~")(in T[] rhs) {
76 		isNull = false;
77 		buffer[this.length .. this.length + rhs.length] = rhs[];
78 		this.length += rhs.length;
79 	}
80 	void opAssign(in T[] rhs) {
81 		isNull = rhs is null;
82 		buffer[0 .. rhs.length] = rhs[];
83 		this.length = rhs.length;
84 	}
85 	void opAssign(typeof(null)) {
86 		isNull = true;
87 		length = 0;
88 	}
89 	T opIndex(size_t idx) {
90 		assert(!isNull);
91 		assert(idx < length);
92 		return buffer[idx];
93 	}
94 	void clear() {
95 		isNull = true;
96 		length = 0; 
97 	}
98 }
99 
100 /**
101 	An abstract class that does terminal emulation. You'll have to subclass it to make it work.
102 
103 	The terminal implements a subset of what xterm does and then, optionally, some special features.
104 
105 	Its linear mode (normal) screen buffer is infinitely long and infinitely wide. It is the responsibility
106 	of your subclass to do line wrapping, etc., for display. This i think is actually incompatible with xterm but meh.
107 
108 	actually maybe it *should* automatically wrap them. idk. I think GNU screen does both. FIXME decide.
109 
110 	Its cellular mode (alternate) screen buffer can be any size you want.
111 */
112 class TerminalEmulator {
113 	/* override these to do stuff on the interface.
114 	You might be able to stub them out if there's no state maintained on the target, since TerminalEmulator maintains its own internal state */
115 	protected abstract void changeWindowTitle(string); /// the title of the window
116 	protected abstract void changeIconTitle(string); /// the shorter window/iconified window
117 
118 	protected abstract void changeWindowIcon(IndexedImage); /// change the window icon. note this may be null
119 
120 	protected abstract void changeCursorStyle(CursorStyle); /// cursor style
121 
122 	protected abstract void changeTextAttributes(TextAttributes); /// current text output attributes
123 	protected abstract void soundBell(); /// sounds the bell
124 	protected abstract void sendToApplication(scope const(void)[]); /// send some data to the program running in the terminal, so keypresses etc.
125 
126 	protected abstract void copyToClipboard(string); /// copy the given data to the clipboard (or you can do nothing if you can't)
127 	protected abstract void pasteFromClipboard(void delegate(in char[])); /// requests a paste. we pass it a delegate that should accept the data
128 
129 	protected abstract void copyToPrimary(string); /// copy the given data to the PRIMARY X selection (or you can do nothing if you can't)
130 	protected abstract void pasteFromPrimary(void delegate(in char[])); /// requests a paste from PRIMARY. we pass it a delegate that should accept the data
131 
132 	abstract protected void requestExit(); /// the program is finished and the terminal emulator is requesting you to exit
133 
134 	/// Signal the UI that some attention should be given, e.g. blink the taskbar or sound the bell.
135 	/// The default is to ignore the demand by instantly acknowledging it - if you override this, do NOT call super().
136 	protected void demandAttention() {
137 		attentionReceived();
138 	}
139 
140 	/// After it demands attention, call this when the attention has been received
141 	/// you may call it immediately to ignore the demand (the default)
142 	public void attentionReceived() {
143 		attentionDemanded = false;
144 	}
145 
146 	// I believe \033[50buffer[] and up are available for extensions everywhere.
147 	// 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?
148 	// apparently shifted F5 is ^[[15;2~
149 	// alt + f5 is ^[[15;3~
150 	// alt+shift+f5 is ^[[15;4~
151 
152 	private string pasteDataPending = null;
153 
154 	protected void justRead() {
155 		if(pasteDataPending.length) {
156 			sendPasteData(pasteDataPending);
157 			import core.thread; Thread.sleep(50.msecs); // hack to keep it from closing, broken pipe i think
158 		}
159 	}
160 
161 	public void sendPasteData(scope const(char)[] data) {
162 		//if(pasteDataPending.length)
163 			//throw new Exception("paste data being discarded, wtf, shouldnt happen");
164 
165 		if(bracketedPasteMode)
166 			sendToApplication("\033[200~");
167 
168 		enum MAX_PASTE_CHUNK = 4000;
169 		if(data.length > MAX_PASTE_CHUNK) {
170 			// need to chunk it in order to receive echos, etc,
171 			// to avoid deadlocks
172 			pasteDataPending = data[MAX_PASTE_CHUNK .. $].idup;
173 			data = data[0 .. MAX_PASTE_CHUNK];
174 		} else {
175 			pasteDataPending = null;
176 		}
177 
178 		if(data.length)
179 			sendToApplication(data);
180 
181 		if(bracketedPasteMode)
182 			sendToApplication("\033[201~");
183 	}
184 
185 	public string getSelectedText() {
186 		return getPlainText(selectionStart, selectionEnd);
187 	}
188 
189 	bool dragging;
190 	int lastDragX, lastDragY;
191 	public bool sendMouseInputToApplication(int termX, int termY, MouseEventType type, MouseButton button, bool shift, bool ctrl, bool alt) {
192 		if(termX < 0)
193 			termX = 0;
194 		if(termX >= screenWidth)
195 			termX = screenWidth - 1;
196 		if(termY < 0)
197 			termY = 0;
198 		if(termY >= screenHeight)
199 			termY = screenHeight - 1;
200 
201 		version(Windows) {
202 			// I'm swapping these because my laptop doesn't have a middle button,
203 			// and putty swaps them too by default so whatevs.
204 			if(button == MouseButton.right)
205 				button = MouseButton.middle;
206 			else if(button == MouseButton.middle)
207 				button = MouseButton.right;
208 		}
209 
210 		int baseEventCode() {
211 			int b;
212 			// lol the xterm mouse thing sucks like javascript! unbelievable
213 			// it doesn't support two buttons at once...
214 			if(button == MouseButton.left)
215 				b = 0;
216 			else if(button == MouseButton.right)
217 				b = 2;
218 			else if(button == MouseButton.middle)
219 				b = 1;
220 			else if(button == MouseButton.wheelUp)
221 				b = 64 | 0;
222 			else if(button == MouseButton.wheelDown)
223 				b = 64 | 1;
224 			else
225 				b = 3; // none pressed or button released
226 
227 			if(shift)
228 				b |= 4;
229 			if(ctrl)
230 				b |= 16;
231 			if(alt) // sending alt as meta
232 				b |= 8;
233 
234 			return b;
235 		}
236 
237 
238 		if(type == MouseEventType.buttonReleased) {
239 			// X sends press and release on wheel events, but we certainly don't care about those
240 			if(button == MouseButton.wheelUp || button == MouseButton.wheelDown)
241 				return false;
242 
243 			if(dragging) {
244 				auto text = getSelectedText();
245 				if(text.length) {
246 					copyToPrimary(text);
247 				}
248 			}
249 
250 			dragging = false;
251 			if(mouseButtonReleaseTracking) {
252 				int b = baseEventCode;
253 				b |= 3; // always send none / button released
254 				ScopeBuffer!(char, 16) buffer;
255 				buffer ~= "\033[M";
256 				buffer ~= cast(char) (b | 32);
257 				buffer ~= cast(char) (termX+1 + 32);
258 				buffer ~= cast(char) (termY+1 + 32);
259 				sendToApplication(buffer[]);
260 			}
261 		}
262 
263 		if(type == MouseEventType.motion) {
264 			if(termX != lastDragX || termY != lastDragY) {
265 				lastDragY = termY;
266 				lastDragX = termX;
267 				if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
268 					int b = baseEventCode;
269 					ScopeBuffer!(char, 16) buffer;
270 					buffer ~= "\033[M";
271 					buffer ~= cast(char) ((b | 32) + 32);
272 					buffer ~= cast(char) (termX+1 + 32);
273 					buffer ~= cast(char) (termY+1 + 32);
274 					sendToApplication(buffer[]);
275 				}
276 
277 				if(dragging) {
278 					auto idx = termY * screenWidth + termX;
279 
280 					// the no-longer-selected portion needs to be invalidated
281 					int start, end;
282 					if(idx > selectionEnd) {
283 						start = selectionEnd;
284 						end = idx;
285 					} else {
286 						start = idx;
287 						end = selectionEnd;
288 					}
289 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
290 						cell.invalidated = true;
291 						cell.selected = false;
292 					}
293 
294 					selectionEnd = idx;
295 
296 					// and the freshly selected portion needs to be invalidated
297 					if(selectionStart > selectionEnd) {
298 						start = selectionEnd;
299 						end = selectionStart;
300 					} else {
301 						start = selectionStart;
302 						end = selectionEnd;
303 					}
304 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
305 						cell.invalidated = true;
306 						cell.selected = true;
307 					}
308 
309 					return true;
310 				}
311 			}
312 		}
313 
314 		if(type == MouseEventType.buttonPressed) {
315 			// double click detection
316 			import std.datetime;
317 			static SysTime lastClickTime;
318 			static int consecutiveClicks = 1;
319 
320 			if(button != MouseButton.wheelUp && button != MouseButton.wheelDown) {
321 				if(Clock.currTime() - lastClickTime < dur!"msecs"(350))
322 					consecutiveClicks++;
323 				else
324 					consecutiveClicks = 1;
325 
326 				lastClickTime = Clock.currTime();
327 			}
328 			// end dbl click
329 
330 			if(!(shift) && mouseButtonTracking) {
331 				int b = baseEventCode;
332 
333 				int x = termX;
334 				int y = termY;
335 				x++; y++; // applications expect it to be one-based
336 
337 				ScopeBuffer!(char, 16) buffer;
338 				buffer ~= "\033[M";
339 				buffer ~= cast(char) (b | 32);
340 				buffer ~= cast(char) (x + 32);
341 				buffer ~= cast(char) (y + 32);
342 
343 				sendToApplication(buffer[]);
344 			} else {
345 				if(button == MouseButton.middle) {
346 					pasteFromPrimary(&sendPasteData);
347 				}
348 
349 				if(button == MouseButton.wheelUp) {
350 					scrollback(alt ? 0 : (ctrl ? 10 : 1), alt ? -(ctrl ? 10 : 1) : 0);
351 					return true;
352 				}
353 				if(button == MouseButton.wheelDown) {
354 					scrollback(alt ? 0 : -(ctrl ? 10 : 1), alt ? (ctrl ? 10 : 1) : 0);
355 					return true;
356 				}
357 
358 				if(button == MouseButton.left) {
359 					// we invalidate the old selection since it should no longer be highlighted...
360 					makeSelectionOffsetsSane(selectionStart, selectionEnd);
361 
362 					auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen);
363 					foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) {
364 						cell.invalidated = true;
365 						cell.selected = false;
366 					}
367 
368 					if(consecutiveClicks == 1) {
369 						selectionStart = termY * screenWidth + termX;
370 						selectionEnd = selectionStart;
371 					} else if(consecutiveClicks == 2) {
372 						selectionStart = termY * screenWidth + termX;
373 						selectionEnd = selectionStart;
374 						while(selectionStart > 0 && !isWordSeparator((*activeScreen)[selectionStart-1].ch)) {
375 							selectionStart--;
376 						}
377 
378 						while(selectionEnd < (*activeScreen).length && !isWordSeparator((*activeScreen)[selectionEnd].ch)) {
379 							selectionEnd++;
380 						}
381 
382 					} else if(consecutiveClicks == 3) {
383 						selectionStart = termY * screenWidth;
384 						selectionEnd = selectionStart + screenWidth;
385 					}
386 					dragging = true;
387 					lastDragX = termX;
388 					lastDragY = termY;
389 
390 					// then invalidate the new selection as well since it should be highlighted
391 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[selectionStart .. selectionEnd]) {
392 						cell.invalidated = true;
393 						cell.selected = true;
394 					}
395 
396 					return true;
397 				}
398 				if(button == MouseButton.right) {
399 					auto oldSelectionEnd = selectionEnd;
400 					selectionEnd = termY * screenWidth + termX;
401 
402 					if(selectionEnd < oldSelectionEnd) {
403 						auto tmp = selectionEnd;
404 						selectionEnd = oldSelectionEnd;
405 						oldSelectionEnd = tmp;
406 					}
407 
408 					foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[oldSelectionEnd .. selectionEnd]) {
409 						cell.invalidated = true;
410 						cell.selected = true;
411 					}
412 
413 					auto text = getPlainText(selectionStart, selectionEnd);
414 					if(text.length) {
415 						copyToPrimary(text);
416 					}
417 					return true;
418 				}
419 			}
420 		}
421 
422 		return false;
423 	}
424 
425 	protected void returnToNormalScreen() {
426 		alternateScreenActive = false;
427 
428 		if(cueScrollback) {
429 			showScrollbackOnScreen(normalScreen, 0, true, 0);
430 			newLine(false);
431 			cueScrollback = false;
432 		}
433 	}
434 
435 	protected void outputOccurred() { }
436 
437 	private int selectionStart; // an offset into the screen buffer
438 	private int selectionEnd; // ditto
439 
440 	/// Send a non-character key sequence
441 	public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
442 		bool redrawRequired = false;
443 
444 		if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) {
445 			toggleScrollLock();
446 			return false;
447 		}
448 
449 		// scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen
450 		// doesn't have scrollback anyway. Thus the key will be forwarded to the application.
451 		if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageUp && (shift || scrollLock)) {
452 			scrollback(10);
453 			return true;
454 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageDown && (shift || scrollLock)) {
455 			scrollback(-10);
456 			return true;
457 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Left && (shift || scrollLock)) {
458 			scrollback(0, ctrl ? -10 : -1);
459 			return true;
460 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Right && (shift || scrollLock)) {
461 			scrollback(0, ctrl ? 10 : 1);
462 			return true;
463 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Up && (shift || scrollLock)) {
464 			scrollback(ctrl ? 10 : 1);
465 			return true;
466 		} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Down && (shift || scrollLock)) {
467 			scrollback(ctrl ? -10 : -1);
468 			return true;
469 		} else if((!alternateScreenActive || scrollingBack)) { // && ev.key != Key.Shift && ev.key != Key.Shift_r) {
470 			if(endScrollback())
471 				redrawRequired = true;
472 		}
473 
474 
475 
476 		void sendToApplicationModified(string s) {
477 			bool anyModifier = shift || alt || ctrl || windows;
478 			if(!anyModifier || applicationCursorKeys)
479 				sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh
480 			else {
481 				ScopeBuffer!(char, 16) modifierNumber;
482 				char otherModifier = 0;
483 				if(shift && alt && ctrl) modifierNumber = "8";
484 				if(alt && ctrl && !shift) modifierNumber = "7";
485 				if(shift && ctrl && !alt) modifierNumber = "6";
486 				if(ctrl && !shift && !alt) modifierNumber = "5";
487 				if(shift && alt && !ctrl) modifierNumber = "4";
488 				if(alt && !shift && !ctrl) modifierNumber = "3";
489 				if(shift && !alt && !ctrl) modifierNumber = "2";
490 				// FIXME: meta and windows
491 				// windows is an extension
492 				if(windows) {
493 					if(modifierNumber.length)
494 						otherModifier = '2';
495 					else
496 						modifierNumber = "20";
497 					/* // the below is what we're really doing
498 					int mn = 0;
499 					if(modifierNumber.length)
500 						mn = modifierNumber[0] + '0';
501 					mn += 20;
502 					*/
503 				}
504 
505 				string keyNumber;
506 				char terminator;
507 
508 				if(s[$-1] == '~') {
509 					keyNumber = s[2 .. $-1];
510 					terminator = '~';
511 				} else {
512 					keyNumber = "1";
513 					terminator = s[$ - 1];
514 				}
515 
516 				ScopeBuffer!(char, 32) buffer;
517 				buffer ~= "\033[";
518 				buffer ~= keyNumber;
519 				buffer ~= ";";
520 				if(otherModifier)
521 					buffer ~= otherModifier;
522 				buffer ~= modifierNumber[];
523 				buffer ~= terminator;
524 				// the xterm style is last bit tell us what it is
525 				sendToApplication(buffer[]);
526 			}
527 		}
528 
529 		alias TerminalKey Key;
530 		import std.stdio;
531 		// writefln("Key: %x", cast(int) key);
532 		final switch(key) {
533 			case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break;
534 			case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break;
535 			case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break;
536 			case Key.Right: sendToApplicationModified(applicationCursorKeys ? "\033OC" : "\033[C"); break;
537 
538 			case Key.Home: sendToApplicationModified(applicationCursorKeys ? "\033OH" : (1 ? "\033[H" : "\033[1~")); break;
539 			case Key.Insert: sendToApplicationModified("\033[2~"); break;
540 			case Key.Delete: sendToApplicationModified("\033[3~"); break;
541 
542 			// the 1? is xterm vs gnu screen. but i really want xterm compatibility.
543 			case Key.End: sendToApplicationModified(applicationCursorKeys ? "\033OF" : (1 ? "\033[F" : "\033[4~")); break;
544 			case Key.PageUp: sendToApplicationModified("\033[5~"); break;
545 			case Key.PageDown: sendToApplicationModified("\033[6~"); break;
546 
547 			// 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
548 			case Key.F1: sendToApplicationModified(1 ? "\033OP" : "\033[11~"); break;
549 			case Key.F2: sendToApplicationModified(1 ? "\033OQ" : "\033[12~"); break;
550 			case Key.F3: sendToApplicationModified(1 ? "\033OR" : "\033[13~"); break;
551 			case Key.F4: sendToApplicationModified(1 ? "\033OS" : "\033[14~"); break;
552 			case Key.F5: sendToApplicationModified("\033[15~"); break;
553 			case Key.F6: sendToApplicationModified("\033[17~"); break;
554 			case Key.F7: sendToApplicationModified("\033[18~"); break;
555 			case Key.F8: sendToApplicationModified("\033[19~"); break;
556 			case Key.F9: sendToApplicationModified("\033[20~"); break;
557 			case Key.F10: sendToApplicationModified("\033[21~"); break;
558 			case Key.F11: sendToApplicationModified("\033[23~"); break;
559 			case Key.F12: sendToApplicationModified("\033[24~"); break;
560 
561 			case Key.Escape: sendToApplicationModified("\033"); break;
562 
563 			// my extensions
564 			case Key.ScrollLock: sendToApplicationModified("\033[70~"); break;
565 
566 			// see terminal.d for the other side of this
567 			case cast(TerminalKey) '\n': sendToApplicationModified("\033[83~"); break;
568 			case cast(TerminalKey) '\b': sendToApplicationModified("\033[78~"); break;
569 			case cast(TerminalKey) '\t': sendToApplicationModified("\033[79~"); break;
570 		}
571 
572 		return redrawRequired;
573 	}
574 
575 	/// 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
576 	protected /*abstract*/ BrokenUpImage handleBinaryExtensionData(const(ubyte)[]) {
577 		return BrokenUpImage();
578 	}
579 
580 	/// If you subclass this and return true, you can scroll on command without needing to redraw the entire screen;
581 	/// returning true here suppresses the automatic invalidation of scrolled lines (except the new one).
582 	protected bool scrollLines(int howMany, bool scrollUp) {
583 		return false;
584 	}
585 
586 	// might be worth doing the redraw magic in here too.
587 	// FIXME: not implemented
588 	@disable protected void drawTextSection(int x, int y, TextAttributes attributes, in dchar[] text, bool isAllSpaces) {
589 		// 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
590 		// or you can redraw based on the invalidated flag on the buffer
591 	}
592 	// FIXME: what about image sections? maybe it is still necessary to loop through them
593 
594 	/// Style of the cursor
595 	enum CursorStyle {
596 		block, /// a solid block over the position (like default xterm or many gui replace modes)
597 		underline, /// underlining the position (like the vga text mode default)
598 		bar, /// a bar on the left side of the cursor position (like gui insert modes)
599 	}
600 
601 	// these can be overridden, but don't have to be
602 	TextAttributes defaultTextAttributes() {
603 		TextAttributes ta;
604 
605 		ta.foregroundIndex = 256; // terminal.d uses this as Color.DEFAULT
606 		ta.backgroundIndex = 256;
607 
608 		import std.process;
609 		// I'm using the environment for this because my programs and scripts
610 		// already know this variable and then it gets nicely inherited. It is
611 		// also easy to set without buggering with other arguments. So works for me.
612 		version(with_24_bit_color) {
613 			if(environment.get("ELVISBG") == "dark") {
614 				ta.foreground = Color.white;
615 				ta.background = Color.black;
616 			} else {
617 				ta.foreground = Color.black;
618 				ta.background = Color.white;
619 			}
620 		}
621 		if(environment.get("ELVISBG") == "dark") {
622 			defaultForeground = Color.white;
623 			defaultBackground = Color.black;
624 		} else {
625 			defaultForeground = Color.black;
626 			defaultBackground = Color.white;
627 		}
628 
629 		return ta;
630 	}
631 
632 	Color defaultForeground;
633 	Color defaultBackground;
634 
635 	Color[256] palette;
636 
637 	/// .
638 	static struct TextAttributes {
639 		align(1):
640 		bool bold() { return (attrStore & 1) ? true : false; } ///
641 		void bold(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } ///
642 
643 		bool blink() { return (attrStore & 2) ? true : false; } ///
644 		void blink(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } ///
645 
646 		bool invisible() { return (attrStore & 4) ? true : false; } ///
647 		void invisible(bool t) { attrStore &= ~4; if(t) attrStore |= 4; } ///
648 
649 		bool inverse() { return (attrStore & 8) ? true : false; } ///
650 		void inverse(bool t) { attrStore &= ~8; if(t) attrStore |= 8; } ///
651 
652 		bool underlined() { return (attrStore & 16) ? true : false; } ///
653 		void underlined(bool t) { attrStore &= ~16; if(t) attrStore |= 16; } ///
654 
655 		bool italic() { return (attrStore & 32) ? true : false; } ///
656 		void italic(bool t) { attrStore &= ~32; if(t) attrStore |= 32; } ///
657 
658 		bool strikeout() { return (attrStore & 64) ? true : false; } ///
659 		void strikeout(bool t) { attrStore &= ~64; if(t) attrStore |= 64; } ///
660 
661 		bool faint() { return (attrStore & 128) ? true : false; } ///
662 		void faint(bool t) { attrStore &= ~128; if(t) attrStore |= 128; } ///
663 
664 		// 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
665 		ushort foregroundIndex; /// .
666 		ushort backgroundIndex; /// .
667 
668 		version(with_24_bit_color) {
669 			Color foreground; /// .
670 			Color background; /// .
671 		}
672 
673 		ubyte attrStore = 0;
674 	}
675 
676 		//pragma(msg, TerminalCell.sizeof);
677 	/// represents one terminal cell
678 	align((void*).sizeof)
679 	static struct TerminalCell {
680 	align(1):
681 		private union {
682 			struct {
683 				dchar chStore = ' '; /// the character
684 				TextAttributes attributesStore; /// color, etc.
685 			}
686 			NonCharacterData nonCharacterDataStore; /// iff hasNonCharacterData
687 		}
688 
689 		dchar ch() {
690 			assert(!hasNonCharacterData);
691 			return chStore;
692 		}
693 		void ch(dchar c) { 
694 			hasNonCharacterData = false;
695 			chStore = c;
696 		}
697 		ref TextAttributes attributes() {
698 			assert(!hasNonCharacterData);
699 			return attributesStore;
700 		}
701 		NonCharacterData nonCharacterData() {
702 			assert(hasNonCharacterData);
703 			return nonCharacterDataStore;
704 		}
705 		void nonCharacterData(NonCharacterData c) {
706 			hasNonCharacterData = true;
707 			nonCharacterDataStore = c;
708 		}
709 
710 		ubyte attrStore = 1;  // just invalidated to start
711 
712 		bool invalidated() { return (attrStore & 1) ? true : false; } /// if it needs to be redrawn
713 		void invalidated(bool t) { attrStore &= ~1; if(t) attrStore |= 1; } /// ditto
714 
715 		bool selected() { return (attrStore & 2) ? true : false; } /// if it is currently selected by the user (for being copied to the clipboard)
716 		void selected(bool t) { attrStore &= ~2; if(t) attrStore |= 2; } /// ditto
717 
718 		bool hasNonCharacterData() { return (attrStore & 4) ? true : false; } ///
719 		void hasNonCharacterData(bool t) { attrStore &= ~4; if(t) attrStore |= 4; }
720 	}
721 
722 	/// Cursor position, zero based. (0,0) == upper left. (0, 1) == second row, first column.
723 	static struct CursorPosition {
724 		int x; /// .
725 		int y; /// .
726 		alias y row;
727 		alias x column;
728 	}
729 
730 	// these public functions can be used to manipulate the terminal
731 
732 	/// clear the screen
733 	void cls() {
734 		TerminalCell plain;
735 		plain.ch = ' ';
736 		plain.attributes = currentAttributes;
737 		plain.invalidated = true;
738 		foreach(i, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
739 			cell = plain;
740 		}
741 	}
742 
743 	void makeSelectionOffsetsSane(ref int offsetStart, ref int offsetEnd) {
744 		auto buffer = &alternateScreen;
745 
746 		if(offsetStart < 0)
747 			offsetStart = 0;
748 		if(offsetEnd < 0)
749 			offsetEnd = 0;
750 		if(offsetStart > (*buffer).length)
751 			offsetStart = cast(int) (*buffer).length;
752 		if(offsetEnd > (*buffer).length)
753 			offsetEnd = cast(int) (*buffer).length;
754 
755 		// if it is backwards, we can flip it
756 		if(offsetEnd < offsetStart) {
757 			auto tmp = offsetStart;
758 			offsetStart = offsetEnd;
759 			offsetEnd = tmp;
760 		}
761 	}
762 
763 	public string getPlainText(int offsetStart, int offsetEnd) {
764 		auto buffer = alternateScreenActive ? &alternateScreen : &normalScreen;
765 
766 		makeSelectionOffsetsSane(offsetStart, offsetEnd);
767 
768 		if(offsetStart == offsetEnd)
769 			return null;
770 
771 		int x = offsetStart % screenWidth;
772 		int firstSpace = -1;
773 		string ret;
774 		foreach(cell; (*buffer)[offsetStart .. offsetEnd]) {
775 			ret ~= cell.ch;
776 
777 			x++;
778 			if(x == screenWidth) {
779 				x = 0;
780 				if(firstSpace != -1) {
781 					// we ended with a bunch of spaces, let's replace them with a single newline so the next is more natural
782 					ret = ret[0 .. firstSpace];
783 					ret ~= "\n";
784 					firstSpace = -1;
785 				}
786 			} else {
787 				if(cell.ch == ' ' && firstSpace == -1)
788 					firstSpace = cast(int) ret.length - 1;
789 				else if(cell.ch != ' ')
790 					firstSpace = -1;
791 			}
792 		}
793 		if(firstSpace != -1) {
794 			bool allSpaces = true;
795 			foreach(item; ret[firstSpace .. $]) {
796 				if(item != ' ') {
797 					allSpaces = false;
798 					break;
799 				}
800 			}
801 
802 			if(allSpaces)
803 				ret = ret[0 .. firstSpace];
804 		}
805 
806 		return ret;
807 	}
808 
809 	void scrollDown(int count = 1) {
810 		if(cursorY + 1 < screenHeight) {
811 			TerminalCell plain;
812 			plain.ch = ' ';
813 			plain.attributes = defaultTextAttributes();
814 			plain.invalidated = true;
815 			foreach(i; 0 .. count) {
816 				// FIXME: should that be cursorY or scrollZoneTop?
817 				for(int y = scrollZoneBottom; y > cursorY; y--)
818 				foreach(x; 0 .. screenWidth) {
819 					ASS[y][x] = ASS[y - 1][x];
820 					ASS[y][x].invalidated = true;
821 				}
822 
823 				foreach(x; 0 .. screenWidth)
824 					ASS[cursorY][x] = plain;
825 			}
826 		}
827 	}
828 
829 	void scrollUp(int count = 1) {
830 		if(cursorY + 1 < screenHeight) {
831 			TerminalCell plain;
832 			plain.ch = ' ';
833 			plain.attributes = defaultTextAttributes();
834 			plain.invalidated = true;
835 			foreach(i; 0 .. count) {
836 				// FIXME: should that be cursorY or scrollZoneBottom?
837 				for(int y = scrollZoneTop; y < cursorY; y++)
838 				foreach(x; 0 .. screenWidth) {
839 					ASS[y][x] = ASS[y + 1][x];
840 					ASS[y][x].invalidated = true;
841 				}
842 
843 				foreach(x; 0 .. screenWidth)
844 					ASS[cursorY][x] = plain;
845 			}
846 		}
847 	}
848 
849 
850 	int readingExtensionData = -1;
851 	string extensionData;
852 
853 	immutable(dchar[dchar])* characterSet = null; // null means use regular UTF-8
854 
855 	bool readingEsc = false;
856 	ScopeBuffer!(ubyte, 1024) esc;
857 	/// sends raw input data to the terminal as if the application printf()'d it or it echoed or whatever
858 	void sendRawInput(in ubyte[] datain) {
859 		const(ubyte)[] data = datain;
860 	//import std.array;
861 	//assert(!readingEsc, replace(cast(string) esc, "\033", "\\"));
862 		again:
863 		foreach(didx, b; data) {
864 			if(readingExtensionData >= 0) {
865 				if(readingExtensionData == extensionMagicIdentifier.length) {
866 					if(b) {
867 						switch(b) {
868 							case 13, 10:
869 								// ignore
870 							break;
871 							case 'A': .. case 'Z':
872 							case 'a': .. case 'z':
873 							case '0': .. case '9':
874 							case '=':
875 							case '+', '/':
876 							case '_', '-':
877 								// base64 ok
878 								extensionData ~= b;
879 							break;
880 							default:
881 								// others should abort the read
882 								readingExtensionData = -1;
883 						}
884 					} else {
885 						readingExtensionData = -1;
886 						import std.base64;
887 						auto got = handleBinaryExtensionData(Base64.decode(extensionData));
888 
889 						auto rep = got.representation;
890 						foreach(y; 0 .. got.height) {
891 							foreach(x; 0 .. got.width) {
892 								addOutput(rep[0]);
893 								rep = rep[1 .. $];
894 							}
895 							newLine(true);
896 						}
897 					}
898 				} else {
899 					if(b == extensionMagicIdentifier[readingExtensionData])
900 						readingExtensionData++;
901 					else {
902 						// put the data back into the buffer, if possible
903 						// (if the data was split across two packets, this may
904 						//  not be possible. but in that case, meh.)
905 						if(cast(int) didx - cast(int) readingExtensionData >= 0)
906 							data = data[didx - readingExtensionData .. $];
907 						readingExtensionData = -1;
908 						goto again;
909 					}
910 				}
911 
912 				continue;
913 			}
914 
915 			if(b == 0) {
916 				readingExtensionData = 0;
917 				extensionData = null;
918 				continue;
919 			}
920 
921 			if(readingEsc) {
922 				if(b == 27) {
923 					// an esc in the middle of a sequence will
924 					// cancel the first one
925 					esc = null;
926 					continue;
927 				}
928 
929 				if(b == 10) {
930 					readingEsc = false;
931 				}
932 				esc ~= b;
933 
934 				if(esc.length == 1 && esc[0] == '7') {
935 					pushSavedCursor(cursorPosition);
936 					esc = null;
937 					readingEsc = false;
938 				} else if(esc.length == 1 && esc[0] == 'M') {
939 					// reverse index
940 					esc = null;
941 					readingEsc = false;
942 					if(cursorY <= scrollZoneTop)
943 						scrollDown();
944 					else
945 						cursorY = cursorY - 1;
946 				} else if(esc.length == 1 && esc[0] == '=') {
947 					// application keypad
948 					esc = null;
949 					readingEsc = false;
950 				} else if(esc.length == 2 && esc[0] == '%' && esc[1] == 'G') {
951 					// UTF-8 mode
952 					esc = null;
953 					readingEsc = false;
954 				} else if(esc.length == 1 && esc[0] == '8') {
955 					cursorPosition = popSavedCursor;
956 					esc = null;
957 					readingEsc = false;
958 				} else if(esc.length == 1 && esc[0] == 'c') {
959 					// reset
960 					// FIXME
961 					esc = null;
962 					readingEsc = false;
963 				} else if(esc.length == 1 && esc[0] == '>') {
964 					// normal keypad
965 					esc = null;
966 					readingEsc = false;
967 				} else if(esc.length > 1 && (
968 					(esc[0] == '[' && (b >= 64 && b <= 126)) ||
969 					(esc[0] == ']' && b == '\007')))
970 				{
971 					tryEsc(esc[]);
972 					esc = null;
973 					readingEsc = false;
974 				} else if(esc.length == 3 && esc[0] == '%' && esc[1] == 'G') {
975 					// UTF-8 mode. ignored because we're always in utf-8 mode (though should we be?)
976 					esc = null;
977 					readingEsc = false;
978 				} else if(esc.length == 2 && esc[0] == ')') {
979 					// more character set selection. idk exactly how this works
980 					esc = null;
981 					readingEsc = false;
982 				} else if(esc.length == 2 && esc[0] == '(') {
983 					// xterm command for character set
984 					// FIXME: handling esc[1] == '0' would be pretty boss
985 					// and esc[1] == 'B' == united states
986 					if(esc[1] == '0')
987 						characterSet = &lineDrawingCharacterSet;
988 					else
989 						characterSet = null; // our default is UTF-8 and i don't care much about others anyway.
990 
991 					esc = null;
992 					readingEsc = false;
993 				} else if(esc.length == 1 && esc[0] == 'Z') {
994 					// identify terminal
995 					sendToApplication(terminalIdCode);
996 				}
997 				continue;
998 			}
999 
1000 			if(b == 27) {
1001 				readingEsc = true;
1002 				debug if(esc.isNull && esc.length) {
1003 					import std.stdio; writeln("discarding esc ", cast(string) esc[]);
1004 				}
1005 				esc = null;
1006 				continue;
1007 			}
1008 
1009 			if(b == 13) {
1010 				cursorX = 0;
1011 				continue;
1012 			}
1013 
1014 			if(b == 7) {
1015 				soundBell();
1016 				continue;
1017 			}
1018 
1019 			if(b == 8) {
1020 				cursorX = cursorX - 1;
1021 				continue;
1022 			}
1023 
1024 			if(b == 9) {
1025 				int howMany = 8 - (cursorX % 8);
1026 				// so apparently it is just supposed to move the cursor.
1027 				// it breaks mutt to output spaces
1028 				cursorX = cursorX + howMany;
1029 				//foreach(i; 0 .. howMany)
1030 					//addOutput(' '); // FIXME: it would be nice to actually put a tab character there for copy/paste accuracy (ditto with newlines actually)
1031 				continue;
1032 			}
1033 
1034 //			std.stdio.writeln("READ ", data[w]);
1035 			addOutput(b);
1036 		}
1037 	}
1038 
1039 
1040 	/// construct
1041 	this(int width, int height) {
1042 		// initialization
1043 		currentAttributes = defaultTextAttributes();
1044 		cursorColor = Color.white;
1045 
1046 		palette[] = xtermPalette[];
1047 
1048 		resizeTerminal(width, height);
1049 
1050 		// update the other thing
1051 		if(windowTitle.length == 0)
1052 			windowTitle = "Terminal Emulator";
1053 		changeWindowTitle(windowTitle);
1054 		changeIconTitle(iconTitle);
1055 		changeTextAttributes(currentAttributes);
1056 	}
1057 
1058 
1059 	private {
1060 		TerminalCell[] scrollbackMainScreen;
1061 		bool scrollbackCursorShowing;
1062 		int scrollbackCursorX;
1063 		int scrollbackCursorY;
1064 		protected bool scrollingBack;
1065 
1066 		int currentScrollback;
1067 		int currentScrollbackX;
1068 	}
1069 
1070 	// FIXME: if it is resized while scrolling back, stuff can get messed up
1071 
1072 	void scrollback(int delta, int deltaX = 0) {
1073 		if(alternateScreenActive && !scrollingBack)
1074 			return;
1075 
1076 		if(!scrollingBack) {
1077 			if(delta <= 0)
1078 				return; // it does nothing to scroll down when not scrolling back
1079 			startScrollback();
1080 		}
1081 		currentScrollback += delta;
1082 		if(deltaX) {
1083 			currentScrollbackX += deltaX;
1084 			if(currentScrollbackX < 0) {
1085 				currentScrollbackX = 0;
1086 				if(!scrollLock)
1087 					scrollbackReflow = true;
1088 			} else
1089 				scrollbackReflow = false;
1090 		}
1091 
1092 		int max = cast(int) scrollbackBuffer.length - screenHeight;
1093 		if(scrollbackReflow && max < 0) {
1094 			foreach(line; scrollbackBuffer[])
1095 				max += cast(int) line.length / screenWidth;
1096 		}
1097 
1098 		if(max < 0)
1099 			max = 0;
1100 
1101 		if(scrollbackReflow && currentScrollback > max) {
1102 			foreach(line; scrollbackBuffer[])
1103 				max += cast(int) line.length / screenWidth;
1104 		}
1105 
1106 		if(currentScrollback > max)
1107 			currentScrollback = max;
1108 
1109 		if(currentScrollback <= 0)
1110 			endScrollback();
1111 		else {
1112 			cls();
1113 			showScrollbackOnScreen(alternateScreen, currentScrollback, scrollbackReflow, currentScrollbackX);
1114 		}
1115 	}
1116 
1117 	private void startScrollback() {
1118 		if(scrollingBack)
1119 			return;
1120 		currentScrollback = 0;
1121 		currentScrollbackX = 0;
1122 		scrollingBack = true;
1123 		scrollbackCursorX = cursorX;
1124 		scrollbackCursorY = cursorY;
1125 		scrollbackCursorShowing = cursorShowing;
1126 		scrollbackMainScreen = alternateScreen.dup;
1127 		alternateScreenActive = true;
1128 
1129 		cursorShowing = false;
1130 	}
1131 
1132 	bool endScrollback() {
1133 		if(!scrollingBack)
1134 			return false;
1135 		scrollingBack = false;
1136 		cursorX = scrollbackCursorX;
1137 		cursorY = scrollbackCursorY;
1138 		cursorShowing = scrollbackCursorShowing;
1139 		alternateScreen = scrollbackMainScreen;
1140 		alternateScreenActive = false;
1141 		return true;
1142 	}
1143 
1144 	private bool scrollbackReflow = true;
1145 	public void toggleScrollbackWrap() {
1146 		scrollbackReflow = !scrollbackReflow;
1147 	}
1148 
1149 	private bool scrollLock = false;
1150 	public void toggleScrollLock() {
1151 		scrollLock = !scrollLock;
1152 		scrollbackReflow = !scrollLock;
1153 	}
1154 
1155 	public void writeScrollbackToFile(string filename) {
1156 		import std.stdio;
1157 		auto file = File(filename, "wt");
1158 		foreach(line; scrollbackBuffer[]) {
1159 			foreach(c; line)
1160 				file.write(c.ch); // I hope this is buffered
1161 			file.writeln();
1162 		}
1163 	}
1164 
1165 	private void showScrollbackOnScreen(ref TerminalCell[] screen, int howFar, bool reflow, int howFarX) {
1166 		int start;
1167 
1168 		cursorX = 0;
1169 		cursorY = 0;
1170 
1171 		int excess = 0;
1172 
1173 		if(scrollbackReflow) {
1174 			int numLines;
1175 			int idx = cast(int) scrollbackBuffer.length - 1;
1176 			foreach_reverse(line; scrollbackBuffer[]) {
1177 				auto lineCount = 1 + line.length / screenWidth;
1178 				numLines += lineCount;
1179 				if(numLines >= (screenHeight + howFar)) {
1180 					start = cast(int) idx;
1181 					excess = numLines - (screenHeight + howFar);
1182 					break;
1183 				}
1184 				idx--;
1185 			}
1186 		} else {
1187 			auto termination = cast(int) scrollbackBuffer.length - howFar;
1188 			if(termination < 0)
1189 				termination = cast(int) scrollbackBuffer.length;
1190 
1191 			start = termination - screenHeight;
1192 			if(start < 0)
1193 				start = 0;
1194 		}
1195 
1196 		TerminalCell overflowCell;
1197 		overflowCell.ch = '\&raquo;';
1198 		overflowCell.attributes.backgroundIndex = 3;
1199 		overflowCell.attributes.foregroundIndex = 0;
1200 		version(with_24_bit_color) {
1201 			overflowCell.attributes.foreground = Color(40, 40, 40);
1202 			overflowCell.attributes.background = Color.yellow;
1203 		}
1204 
1205 		outer: foreach(line; scrollbackBuffer[start .. $]) {
1206 			if(excess) {
1207 				line = line[excess * screenWidth .. $];
1208 				excess = 0;
1209 			}
1210 
1211 			if(howFarX) {
1212 				if(howFarX <= line.length)
1213 					line = line[howFarX .. $];
1214 				else
1215 					line = null;
1216 			}
1217 
1218 			bool overflowed;
1219 			foreach(cell; line) {
1220 				cell.invalidated = true;
1221 				if(overflowed)
1222 					screen[cursorY * screenWidth + cursorX] = overflowCell;
1223 				else
1224 					screen[cursorY * screenWidth + cursorX] = cell;
1225 
1226 				if(cursorX == screenWidth-1) {
1227 					if(scrollbackReflow) {
1228 						cursorX = 0;
1229 						if(cursorY + 1 == screenHeight)
1230 							break outer;
1231 						cursorY = cursorY + 1;
1232 					} else {
1233 						overflowed = true;
1234 					}
1235 				} else
1236 					cursorX = cursorX + 1;
1237 			}
1238 			if(cursorY + 1 == screenHeight)
1239 				break;
1240 			cursorY = cursorY + 1;
1241 			cursorX = 0;
1242 		}
1243 
1244 		cursorX = 0;
1245 	}
1246 
1247 	protected bool cueScrollback;
1248 
1249 	public void resizeTerminal(int w, int h) {
1250 		if(w == screenWidth && h == screenHeight)
1251 			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)
1252 
1253 		endScrollback(); // FIXME: hack
1254 
1255 		screenWidth = w;
1256 		screenHeight = h;
1257 
1258 		normalScreen.length = screenWidth * screenHeight;
1259 		alternateScreen.length = screenWidth * screenHeight;
1260 		scrollZoneBottom = screenHeight - 1;
1261 
1262 		// we need to make sure the state is sane all across the board, so first we'll clear everything...
1263 		TerminalCell plain;
1264 		plain.ch = ' ';
1265 		plain.attributes = defaultTextAttributes;
1266 		plain.invalidated = true;
1267 		normalScreen[] = plain;
1268 		alternateScreen[] = plain;
1269 
1270 		// then, in normal mode, we'll redraw using the scrollback buffer
1271 		//
1272 		// if we're in the alternate screen though, keep it blank because
1273 		// while redrawing makes sense in theory, odds are the program in
1274 		// charge of the normal screen didn't get the resize signal.
1275 		if(!alternateScreenActive)
1276 			showScrollbackOnScreen(normalScreen, 0, true, 0);
1277 		else
1278 			cueScrollback = true;
1279 		// but in alternate mode, it is the application's responsibility
1280 
1281 		// the property ensures these are within bounds so this set just forces that
1282 		cursorY = cursorY;
1283 		cursorX = cursorX;
1284 	}
1285 
1286 	private CursorPosition popSavedCursor() {
1287 		CursorPosition pos;
1288 		//import std.stdio; writeln("popped");
1289 		if(savedCursors.length) {
1290 			pos = savedCursors[$-1];
1291 			savedCursors = savedCursors[0 .. $-1];
1292 			savedCursors.assumeSafeAppend(); // we never keep references elsewhere so might as well reuse the memory as much as we can
1293 		}
1294 
1295 		// If the screen resized after this was saved, it might be restored to a bad amount, so we need to sanity test.
1296 		if(pos.x < 0)
1297 			pos.x = 0;
1298 		if(pos.y < 0)
1299 			pos.y = 0;
1300 		if(pos.x > screenWidth)
1301 			pos.x = screenWidth - 1;
1302 		if(pos.y > screenHeight)
1303 			pos.y = screenHeight - 1;
1304 
1305 		return pos;
1306 	}
1307 
1308 	private void pushSavedCursor(CursorPosition pos) {
1309 		//import std.stdio; writeln("pushed");
1310 		savedCursors ~= pos;
1311 	}
1312 
1313 	/* FIXME: i want these to be private */
1314 	protected {
1315 		TextAttributes currentAttributes;
1316 		CursorPosition cursorPosition;
1317 		CursorPosition[] savedCursors; // a stack
1318 		CursorStyle cursorStyle;
1319 		Color cursorColor;
1320 		string windowTitle;
1321 		string iconTitle;
1322 
1323 		bool attentionDemanded;
1324 
1325 		IndexedImage windowIcon;
1326 		IndexedImage[] iconStack;
1327 
1328 		string[] titleStack;
1329 
1330 		bool bracketedPasteMode;
1331 		bool mouseButtonTracking;
1332 		private bool _mouseMotionTracking;
1333 		bool mouseButtonReleaseTracking;
1334 		bool mouseButtonMotionTracking;
1335 
1336 		bool mouseMotionTracking() {
1337 			return _mouseMotionTracking;
1338 		}
1339 
1340 		void mouseMotionTracking(bool b) {
1341 			_mouseMotionTracking = b;
1342 		}
1343 
1344 		void allMouseTrackingOff() {
1345 			mouseMotionTracking = false;
1346 			mouseButtonTracking = false;
1347 			mouseButtonReleaseTracking = false;
1348 			mouseButtonMotionTracking = false;
1349 		}
1350 
1351 		bool wraparoundMode = true;
1352 
1353 		bool alternateScreenActive;
1354 		bool cursorShowing = true;
1355 
1356 		bool reverseVideo;
1357 		bool applicationCursorKeys;
1358 
1359 		bool scrollingEnabled = true;
1360 		int scrollZoneTop;
1361 		int scrollZoneBottom;
1362 
1363 		int screenWidth;
1364 		int screenHeight;
1365 		// assert(alternateScreen.length = screenWidth * screenHeight);
1366 		TerminalCell[] alternateScreen;
1367 		TerminalCell[] normalScreen;
1368 
1369 		// the lengths can be whatever
1370 		ScrollbackBuffer scrollbackBuffer;
1371 
1372 		static struct ScrollbackBuffer {
1373 			TerminalCell[][] backing;
1374 
1375 			enum maxScrollback = 8192 / 2; // as a power of 2, i hope the compiler optimizes the % below to a simple bit mask...
1376 
1377 			int start;
1378 			int length_;
1379 
1380 			size_t length() {
1381 				return length_;
1382 			}
1383 
1384 			void opOpAssign(string op : "~")(TerminalCell[] line) {
1385 				if(length_ < maxScrollback) {
1386 					backing.assumeSafeAppend();
1387 					backing ~= line;
1388 					length_++;
1389 				} else {
1390 					backing[start] = line;
1391 					start++;
1392 					if(start == maxScrollback)
1393 						start = 0;
1394 				}
1395 			}
1396 
1397 			/*
1398 			int opApply(scope int delegate(ref TerminalCell[]) dg) {
1399 				foreach(ref l; backing)
1400 					if(auto res = dg(l))
1401 						return res;
1402 				return 0;
1403 			}
1404 
1405 			int opApplyReverse(scope int delegate(size_t, ref TerminalCell[]) dg) {
1406 				foreach_reverse(idx, ref l; backing)
1407 					if(auto res = dg(idx, l))
1408 						return res;
1409 				return 0;
1410 			}
1411 			*/
1412 
1413 			TerminalCell[] opIndex(int idx) {
1414 				return backing[(start + idx) % maxScrollback];
1415 			}
1416 
1417 			ScrollbackBufferRange opSlice(int startOfIteration, Dollar end) {
1418 				return ScrollbackBufferRange(&this, startOfIteration);
1419 			}
1420 			ScrollbackBufferRange opSlice() {
1421 				return ScrollbackBufferRange(&this, 0);
1422 			}
1423 
1424 			static struct ScrollbackBufferRange {
1425 				ScrollbackBuffer* item;
1426 				int position;
1427 				int remaining;
1428 				this(ScrollbackBuffer* item, int startOfIteration) {
1429 					this.item = item;
1430 					position = startOfIteration;
1431 					remaining = cast(int) item.length - startOfIteration;
1432 
1433 				}
1434 
1435 				TerminalCell[] front() { return (*item)[position]; }
1436 				bool empty() { return remaining <= 0; }
1437 				void popFront() {
1438 					position++;
1439 					remaining--;
1440 				}
1441 
1442 				TerminalCell[] back() { return (*item)[remaining - 1 - position]; }
1443 				void popBack() {
1444 					remaining--;
1445 				}
1446 			}
1447 
1448 			static struct Dollar {};
1449 			Dollar opDollar() { return Dollar(); }
1450 
1451 		}
1452 
1453 		struct Helper2 {
1454 			size_t row;
1455 			TerminalEmulator t;
1456 			this(TerminalEmulator t, size_t row) {
1457 				this.t = t;
1458 				this.row = row;
1459 			}
1460 
1461 			ref TerminalCell opIndex(size_t cell) {
1462 				auto thing = t.alternateScreenActive ? &(t.alternateScreen) : &(t.normalScreen);
1463 				return (*thing)[row * t.screenWidth + cell];
1464 			}
1465 		}
1466 
1467 		struct Helper {
1468 			TerminalEmulator t;
1469 			this(TerminalEmulator t) {
1470 				this.t = t;
1471 			}
1472 
1473 			Helper2 opIndex(size_t row) {
1474 				return Helper2(t, row);
1475 			}
1476 		}
1477 
1478 		@property Helper ASS() {
1479 			return Helper(this);
1480 		}
1481 
1482 		@property int cursorX() { return cursorPosition.x; }
1483 		@property int cursorY() { return cursorPosition.y; }
1484 		@property void cursorX(int x) {
1485 			if(x < 0)
1486 				x = 0;
1487 			if(x >= screenWidth)
1488 				x = screenWidth - 1;
1489 			cursorPosition.x = x;
1490 		}
1491 		@property void cursorY(int y) {
1492 			if(y < 0)
1493 				y = 0;
1494 			if(y >= screenHeight)
1495 				y = screenHeight - 1;
1496 			cursorPosition.y = y;
1497 		}
1498 
1499 		void addOutput(string b) {
1500 			foreach(c; b)
1501 				addOutput(c);
1502 		}
1503 
1504 		TerminalCell[] currentScrollbackLine;
1505 		int scrollbackWrappingAt = 0;
1506 		dchar utf8Sequence;
1507 		int utf8BytesRemaining;
1508 		int currentUtf8Shift;
1509 		bool newLineOnNext;
1510 		void addOutput(ubyte b) {
1511 			// this takes in bytes at a time, but since the input encoding is assumed to be UTF-8, we need to gather the bytes
1512 			if(utf8BytesRemaining == 0) {
1513 				// we're at the beginning of a sequence
1514 				utf8Sequence = 0;
1515 				if(b < 128) {
1516 					utf8Sequence = cast(dchar) b;
1517 					// one byte thing, do nothing more...
1518 				} else {
1519 					// the number of bytes in the sequence is the number of set bits in the first byte...
1520 					uint shifted =0;
1521 					bool there = false;
1522 					ubyte checkingBit = 7;
1523 					while(checkingBit) {
1524 						if(!there && b & (1 << checkingBit))
1525 							utf8BytesRemaining++;
1526 						else
1527 							there = true;
1528 						if(there)
1529 							shifted |= b & (1 << checkingBit);
1530 						checkingBit--;
1531 					}
1532 					utf8BytesRemaining--; // since this current byte counts too
1533 					currentUtf8Shift = utf8BytesRemaining * 6;
1534 
1535 					shifted <<= (currentUtf8Shift + checkingBit);
1536 					utf8Sequence = cast(dchar) shifted;
1537 				}
1538 			} else {
1539 				// add this to the byte we're doing right now...
1540 				utf8BytesRemaining--;
1541 				currentUtf8Shift -= 6;
1542 				if(!(b & 0b11000000) == 0b10000000) {
1543 					// invalid utf-8 sequence,
1544 					// discard it and try to continue
1545 					utf8BytesRemaining = 0;
1546 					return;
1547 				}
1548 				uint shifted = b;
1549 				shifted &= 0b00111111;
1550 				shifted <<= currentUtf8Shift;
1551 				utf8Sequence |= shifted;
1552 			}
1553 
1554 			if(utf8BytesRemaining)
1555 				return; // not enough data yet, wait for more before displaying anything
1556 
1557 			if(utf8Sequence == 10) {
1558 				newLineOnNext = false;
1559 				auto cx = cursorX; // FIXME: this cx thing is a hack, newLine should prolly just do the right thing
1560 
1561 				/*
1562 				TerminalCell tc;
1563 				tc.ch = utf8Sequence;
1564 				tc.attributes = currentAttributes;
1565 				tc.invalidated = true;
1566 				addOutput(tc);
1567 				*/
1568 
1569 				newLine(true);
1570 				cursorX = cx;
1571 			} else {
1572 				if(newLineOnNext) {
1573 					newLineOnNext = false;
1574 					// only if we're still on the right side...
1575 					if(cursorX == screenWidth - 1)
1576 						newLine(false);
1577 				}
1578 				TerminalCell tc;
1579 
1580 				if(characterSet !is null) {
1581 					if(auto replacement = utf8Sequence in *characterSet)
1582 						utf8Sequence = *replacement;
1583 				}
1584 				tc.ch = utf8Sequence;
1585 				tc.attributes = currentAttributes;
1586 				tc.invalidated = true;
1587 
1588 				addOutput(tc);
1589 			}
1590 		}
1591 
1592 		bool insertMode = false;
1593 		void newLine(bool commitScrollback) {
1594 			if(!alternateScreenActive && commitScrollback) {
1595 				// I am limiting this because obscenely long lines are kinda useless anyway and
1596 				// i don't want it to eat excessive memory when i spam some thing accidentally
1597 				if(currentScrollbackLine.length < 1024)
1598 					scrollbackBuffer ~= currentScrollbackLine;
1599 				else
1600 					scrollbackBuffer ~= currentScrollbackLine[0 .. 1024];
1601 
1602 				currentScrollbackLine = null;
1603 				currentScrollbackLine.reserve(64);
1604 				scrollbackWrappingAt = 0;
1605 			}
1606 
1607 			cursorX = 0;
1608 			if(scrollingEnabled && cursorY >= scrollZoneBottom) {
1609 				size_t idx = scrollZoneTop * screenWidth;
1610 
1611 				// When we scroll up, we need to update the selection position too
1612 				if(selectionStart != selectionEnd) {
1613 					selectionStart -= screenWidth;
1614 					selectionEnd -= screenWidth;
1615 				}
1616 				foreach(l; scrollZoneTop .. scrollZoneBottom) {
1617 					if(alternateScreenActive) {
1618 						if(idx + screenWidth * 2 > alternateScreen.length)
1619 							break;
1620 						alternateScreen[idx .. idx + screenWidth] = alternateScreen[idx + screenWidth .. idx + screenWidth * 2];
1621 					} else {
1622 						if(idx + screenWidth * 2 > normalScreen.length)
1623 							break;
1624 						normalScreen[idx .. idx + screenWidth] = normalScreen[idx + screenWidth .. idx + screenWidth * 2];
1625 					}
1626 					idx += screenWidth;
1627 				}
1628 				/*
1629 				foreach(i; 0 .. screenWidth) {
1630 					if(alternateScreenActive) {
1631 						alternateScreen[idx] = alternateScreen[idx + screenWidth];
1632 						alternateScreen[idx].invalidated = true;
1633 					} else {
1634 						normalScreen[idx] = normalScreen[idx + screenWidth];
1635 						normalScreen[idx].invalidated = true;
1636 					}
1637 					idx++;
1638 				}
1639 				*/
1640 				/*
1641 				foreach(i; 0 .. screenWidth) {
1642 					if(alternateScreenActive) {
1643 						alternateScreen[idx].ch = ' ';
1644 						alternateScreen[idx].attributes = currentAttributes;
1645 						alternateScreen[idx].invalidated = true;
1646 					} else {
1647 						normalScreen[idx].ch = ' ';
1648 						normalScreen[idx].attributes = currentAttributes;
1649 						normalScreen[idx].invalidated = true;
1650 					}
1651 					idx++;
1652 				}
1653 				*/
1654 
1655 				TerminalCell plain;
1656 				plain.ch = ' ';
1657 				plain.attributes = currentAttributes;
1658 				if(alternateScreenActive) {
1659 					alternateScreen[idx .. idx + screenWidth] = plain;
1660 				} else {
1661 					normalScreen[idx .. idx + screenWidth] = plain;
1662 				}
1663 			} else {
1664 				if(insertMode)
1665 					scrollDown();
1666 				else
1667 					cursorY = cursorY + 1;
1668 			}
1669 
1670 			invalidateAll = true;
1671 		}
1672 
1673 		protected bool invalidateAll;
1674 
1675 		void clearSelection() {
1676 			foreach(ref tc; alternateScreenActive ? alternateScreen : normalScreen)
1677 				if(tc.selected) {
1678 					tc.selected = false;
1679 					tc.invalidated = true;
1680 				}
1681 			selectionStart = 0;
1682 			selectionEnd = 0;
1683 		}
1684 
1685 		void addOutput(TerminalCell tc) {
1686 			if(alternateScreenActive) {
1687 				if(alternateScreen[cursorY * screenWidth + cursorX].selected) {
1688 					clearSelection();
1689 				}
1690 				alternateScreen[cursorY * screenWidth + cursorX] = tc;
1691 			} else {
1692 				if(normalScreen[cursorY * screenWidth + cursorX].selected) {
1693 					clearSelection();
1694 				}
1695 				// FIXME: make this more efficient if it is writing the same thing,
1696 				// then it need not be invalidated. Same with above for the alt screen
1697 				normalScreen[cursorY * screenWidth + cursorX] = tc;
1698 
1699 				TerminalCell plain;
1700 				plain.ch = ' ';
1701 				plain.attributes = currentAttributes;
1702 				int lol = cursorX + scrollbackWrappingAt;
1703 				while(lol >= currentScrollbackLine.length)
1704 					currentScrollbackLine ~= plain;
1705 				currentScrollbackLine[lol] = tc;
1706 			}
1707 			// FIXME: the wraparoundMode seems to help gnu screen but then it doesn't go away properly and that messes up bash...
1708 			//if(wraparoundMode && cursorX == screenWidth - 1) {
1709 			if(cursorX == screenWidth - 1) {
1710 				// FIXME: should this check the scrolling zone instead?
1711 				newLineOnNext = true;
1712 
1713 				//if(!alternateScreenActive || cursorY < screenHeight - 1)
1714 					//newLine(false);
1715 				scrollbackWrappingAt = cast(int) currentScrollbackLine.length;
1716 			} else
1717 				cursorX = cursorX + 1;
1718 
1719 		}
1720 
1721 		void tryEsc(ubyte[] esc) {
1722 			bool[2] sidxProcessed;
1723 			int[][2] argsAtSidx;
1724 			int[12][2] argsAtSidxBuffer;
1725 
1726 			int[12][4] argsBuffer;
1727 			int argsBufferLocation;
1728 
1729 			int[] getArgsBase(int sidx, int[] defaults) {
1730 				assert(sidx == 1 || sidx == 2);
1731 
1732 				if(sidxProcessed[sidx - 1]) {
1733 					int[] bfr = argsBuffer[argsBufferLocation++][];
1734 					if(argsBufferLocation == argsBuffer.length)
1735 						argsBufferLocation = 0;
1736 					bfr[0 .. defaults.length] = defaults[];
1737 					foreach(idx, v; argsAtSidx[sidx - 1])
1738 						if(v != int.min)
1739 							bfr[idx] = v;
1740 					return bfr[0 .. max(argsAtSidx[sidx - 1].length, defaults.length)];
1741 				}
1742 
1743 				auto argsSection = cast(char[]) esc[sidx .. $-1];
1744 				int[] args = argsAtSidxBuffer[sidx - 1][];
1745 
1746 				import std..string : split;
1747 				import std.conv : to;
1748 				int lastIdx = 0;
1749 
1750 				foreach(i, arg; split(argsSection, ";")) {
1751 					int value;
1752 					if(arg.length)
1753 						value = to!int(arg);
1754 					else
1755 						value = int.min; // defaults[i];
1756 
1757 					if(args.length > i)
1758 						args[i] = value;
1759 					else
1760 						assert(0);
1761 					lastIdx++;
1762 				}
1763 
1764 				argsAtSidx[sidx - 1] = args[0 .. lastIdx];
1765 				sidxProcessed[sidx - 1] = true;
1766 
1767 				return getArgsBase(sidx, defaults);
1768 			}
1769 			int[] getArgs(int[] defaults...) {
1770 				return getArgsBase(1, defaults);
1771 			}
1772 
1773 			// FIXME
1774 			// from  http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
1775 			// check out this section: "Window manipulation (from dtterm, as well as extensions)"
1776 			// especially the title stack, that should rock
1777 			/*
1778 P s = 2 2 ; 0 → Save xterm icon and window title on stack.
1779 P s = 2 2 ; 1 → Save xterm icon title on stack.
1780 P s = 2 2 ; 2 → Save xterm window title on stack.
1781 P s = 2 3 ; 0 → Restore xterm icon and window title from stack.
1782 P s = 2 3 ; 1 → Restore xterm icon title from stack.
1783 P s = 2 3 ; 2 → Restore xterm window title from stack.
1784 
1785 			*/
1786 
1787 			if(esc[0] == ']' && esc.length > 1) {
1788 				int idx = -1;
1789 				foreach(i, e; esc)
1790 					if(e == ';') {
1791 						idx = cast(int) i;
1792 						break;
1793 					}
1794 				if(idx != -1) {
1795 					auto arg = cast(char[]) esc[idx + 1 .. $-1];
1796 					switch(cast(char[]) esc[1..idx]) {
1797 						case "0":
1798 							// icon name and window title
1799 							windowTitle = iconTitle = arg.idup;
1800 							changeWindowTitle(windowTitle);
1801 							changeIconTitle(iconTitle);
1802 						break;
1803 						case "1":
1804 							// icon name
1805 							iconTitle = arg.idup;
1806 							changeIconTitle(iconTitle);
1807 						break;
1808 						case "2":
1809 							// window title
1810 							windowTitle = arg.idup;
1811 							changeWindowTitle(windowTitle);
1812 						break;
1813 						case "10":
1814 							// change default text foreground color
1815 						break;
1816 						case "11":
1817 							// change gui background color
1818 						break;
1819 						case "12":
1820 							if(arg.length)
1821 								arg = arg[1 ..$]; // skip past the thing
1822 							if(arg.length) {
1823 								cursorColor = Color.fromString(arg);
1824 								foreach(ref p; cursorColor.components[0 .. 3])
1825 									p ^= 0xff;
1826 							} else
1827 								cursorColor = Color.white;
1828 						break;
1829 						case "50":
1830 							// change font
1831 						break;
1832 						case "52":
1833 							// copy/paste control
1834 							// echo -e "\033]52;p;?\007"
1835 							// the p == primary
1836 							// the data after it is either base64 stuff to copy or ? to request a paste
1837 
1838 							if(arg == "p;?") {
1839 								// i'm using this to request a paste. not quite compatible with xterm, but kinda
1840 								// because xterm tends not to answer anyway.
1841 								pasteFromPrimary(&sendPasteData);
1842 							} else if(arg.length > 2 && arg[0 .. 2] == "p;") {
1843 								auto info = arg[2 .. $];
1844 								try {
1845 									import std.base64;
1846 									auto data = Base64.decode(info);
1847 									copyToPrimary(cast(string) data);
1848 								} catch(Exception e)  {}
1849 							}
1850 
1851 							if(arg == "c;?") {
1852 								// i'm using this to request a paste. not quite compatible with xterm, but kinda
1853 								// because xterm tends not to answer anyway.
1854 								pasteFromClipboard(&sendPasteData);
1855 							} else if(arg.length > 2 && arg[0 .. 2] == "c;") {
1856 								auto info = arg[2 .. $];
1857 								try {
1858 									import std.base64;
1859 									auto data = Base64.decode(info);
1860 									copyToClipboard(cast(string) data);
1861 								} catch(Exception e)  {}
1862 							}
1863 
1864 						break;
1865 						case "4":
1866 							// palette change or query
1867 							        // set color #0 == black
1868 							// echo -e '\033]4;0;black\007'
1869 							/*
1870 								echo -e '\033]4;9;?\007' ; cat
1871 
1872 								^[]4;9;rgb:ffff/0000/0000^G
1873 							*/
1874 
1875 							// FIXME: if the palette changes, we should redraw so the change is immediately visible (as if we were using a real palette)
1876 						break;
1877 						case "104":
1878 							// palette reset
1879 							// reset color #0
1880 							// echo -e '\033[104;0\007'
1881 						break;
1882 						/* Extensions */
1883 						case "5000":
1884 							// change window icon (send a base64 encoded image or something)
1885 							/*
1886 								The format here is width and height as a single char each
1887 									'0'-'9' == 0-9
1888 									'a'-'z' == 10 - 36
1889 									anything else is invalid
1890 								
1891 								then a palette in hex rgba format (8 chars each), up to 26 entries
1892 
1893 								then a capital Z
1894 
1895 								if a palette entry == 'P', it means pull from the current palette (FIXME not implemented)
1896 
1897 								then 256 characters between a-z (must be lowercase!) which are the palette entries for
1898 								the pixels, top to bottom, left to right, so the image is 16x16. if it ends early, the
1899 								rest of the data is assumed to be zero
1900 
1901 								you can also do e.g. 22a, which means repeat a 22 times for some RLE.
1902 
1903 								anything out of range aborts the operation
1904 							*/
1905 							auto img = readSmallTextImage(arg);
1906 							windowIcon = img;
1907 							changeWindowIcon(img);
1908 						break;
1909 						case "5001":
1910 							// demand attention
1911 							attentionDemanded = true;
1912 							demandAttention();
1913 						break;
1914 						default:
1915 							unknownEscapeSequence("" ~ cast(char) esc[1]);
1916 					}
1917 				}
1918 			} else if(esc[0] == '[' && esc.length > 1) {
1919 				switch(esc[$-1]) {
1920 					case 'Z':
1921 						// CSI Ps Z  Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
1922 						// FIXME?
1923 					break;
1924 					case 'n':
1925 						switch(esc[$-2]) {
1926 							import std..string;
1927 							// request status report, reply OK
1928 							case '5': sendToApplication("\033[0n"); break;
1929 							// request cursor position
1930 							case '6': sendToApplication(format("\033[%d;%dR", cursorY + 1, cursorX + 1)); break;
1931 							default: unknownEscapeSequence(cast(string) esc);
1932 						}
1933 					break;
1934 					case 'A': if(cursorY) cursorY = cursorY - getArgs(1)[0]; break;
1935 					case 'B': if(cursorY != this.screenHeight - 1) cursorY = cursorY + getArgs(1)[0]; break;
1936 					case 'D': if(cursorX) cursorX = cursorX - getArgs(1)[0]; break;
1937 					case 'C': if(cursorX != this.screenWidth - 1) cursorX = cursorX + getArgs(1)[0]; break;
1938 
1939 					case 'd': cursorY = getArgs(1)[0]-1; break;
1940 
1941 					case 'E': cursorY = cursorY + getArgs(1)[0]; cursorX = 0; break;
1942 					case 'F': cursorY = cursorY - getArgs(1)[0]; cursorX = 0; break;
1943 					case 'G': cursorX = getArgs(1)[0] - 1; break;
1944 					case 'H':
1945 						auto got = getArgs(1, 1);
1946 						cursorX = got[1] - 1;
1947 						cursorY = got[0] - 1;
1948 						newLineOnNext = false;
1949 					break;
1950 					case 'L':
1951 						// insert lines
1952 						scrollDown(getArgs(1)[0]);
1953 					break;
1954 					case 'M':
1955 						// delete lines
1956 						if(cursorY + 1 < screenHeight) {
1957 							TerminalCell plain;
1958 							plain.ch = ' ';
1959 							plain.attributes = defaultTextAttributes();
1960 							foreach(i; 0 .. getArgs(1)[0]) {
1961 								foreach(y; cursorY .. scrollZoneBottom)
1962 								foreach(x; 0 .. screenWidth) {
1963 									ASS[y][x] = ASS[y + 1][x];
1964 									ASS[y][x].invalidated = true;
1965 								}
1966 								foreach(x; 0 .. screenWidth) {
1967 									ASS[scrollZoneBottom][x] = plain;
1968 								}
1969 							}
1970 						}
1971 					break;
1972 					case 'K':
1973 						auto arg = getArgs(0)[0];
1974 						int start, end;
1975 						if(arg == 0) {
1976 							// clear from cursor to end of line
1977 							start = cursorX;
1978 							end = this.screenWidth;
1979 						} else if(arg == 1) {
1980 							// clear from cursor to beginning of line
1981 							start = 0;
1982 							end = cursorX + 1;
1983 						} else if(arg == 2) {
1984 							// clear entire line
1985 							start = 0;
1986 							end = this.screenWidth;
1987 						}
1988 
1989 						TerminalCell plain;
1990 						plain.ch = ' ';
1991 						plain.attributes = currentAttributes;
1992 
1993 						for(int i = start; i < end; i++) {
1994 							if(ASS[cursorY][i].selected)
1995 								clearSelection();
1996 							ASS[cursorY]
1997 								[i] = plain;
1998 						}
1999 					break;
2000 					case 's':
2001 						pushSavedCursor(cursorPosition);
2002 					break;
2003 					case 'u':
2004 						cursorPosition = popSavedCursor();
2005 					break;
2006 					case 'g':
2007 						auto arg = getArgs(0)[0];
2008 						TerminalCell plain;
2009 						plain.ch = ' ';
2010 						plain.attributes = currentAttributes;
2011 						if(arg == 0) {
2012 							// clear current column
2013 							for(int i = 0; i < this.screenHeight; i++)
2014 								ASS[i]
2015 									[cursorY] = plain;
2016 						} else if(arg == 3) {
2017 							// clear all
2018 							cls();
2019 						}
2020 					break;
2021 					case 'q':
2022 						// xterm also does blinks on the odd numbers (x-1)
2023 						if(esc == "[0 q")
2024 							cursorStyle = CursorStyle.block; // FIXME: restore default
2025 						if(esc == "[2 q")
2026 							cursorStyle = CursorStyle.block;
2027 						else if(esc == "[4 q")
2028 							cursorStyle = CursorStyle.underline;
2029 						else if(esc == "[6 q")
2030 							cursorStyle = CursorStyle.bar;
2031 
2032 						changeCursorStyle(cursorStyle);
2033 					break;
2034 					case 't':
2035 						// window commands
2036 						// i might support more of these but for now i just want the stack stuff.
2037 
2038 						auto args = getArgs(0, 0);
2039 						if(args[0] == 22) {
2040 							// save window title to stack
2041 							// xterm says args[1] should tell if it is the window title, the icon title, or both, but meh
2042 							titleStack ~= windowTitle;
2043 							iconStack ~= windowIcon;
2044 						} else if(args[0] == 23) {
2045 							// restore from stack
2046 							if(titleStack.length) {
2047 								windowTitle = titleStack[$ - 1];
2048 								changeWindowTitle(titleStack[$ - 1]);
2049 								titleStack = titleStack[0 .. $ - 1];
2050 							}
2051 
2052 							if(iconStack.length) {
2053 								windowIcon = iconStack[$ - 1];
2054 								changeWindowIcon(iconStack[$ - 1]);
2055 								iconStack = iconStack[0 .. $ - 1];
2056 							}
2057 						}
2058 					break;
2059 					case 'm':
2060 						argsLoop: foreach(argIdx, arg; getArgs(0))
2061 						switch(arg) {
2062 							case 0:
2063 							// normal
2064 								currentAttributes = defaultTextAttributes;
2065 							break;
2066 							case 1:
2067 								currentAttributes.bold = true;
2068 							break;
2069 							case 2:
2070 								currentAttributes.faint = true;
2071 							break;
2072 							case 3:
2073 								currentAttributes.italic = true;
2074 							break;
2075 							case 4:
2076 								currentAttributes.underlined = true;
2077 							break;
2078 							case 5:
2079 								currentAttributes.blink = true;
2080 							break;
2081 							case 6:
2082 								// rapid blink, treating the same as regular blink
2083 								currentAttributes.blink = true;
2084 							break;
2085 							case 7:
2086 								currentAttributes.inverse = true;
2087 							break;
2088 							case 8:
2089 								currentAttributes.invisible = true;
2090 							break;
2091 							case 9:
2092 								currentAttributes.strikeout = true;
2093 							break;
2094 							case 10:
2095 								// primary font
2096 							break;
2097 							case 11: .. case 19:
2098 								// alternate fonts
2099 							break;
2100 							case 20:
2101 								// Fraktur font
2102 							break;
2103 							case 21:
2104 								// bold off and doubled underlined
2105 							break;
2106 							case 22:
2107 								currentAttributes.bold = false;
2108 								currentAttributes.faint = false;
2109 							break;
2110 							case 23:
2111 								currentAttributes.italic = false;
2112 							break;
2113 							case 24:
2114 								currentAttributes.underlined = false;
2115 							break;
2116 							case 25:
2117 								currentAttributes.blink = false;
2118 							break;
2119 							case 26:
2120 								// reserved
2121 							break;
2122 							case 27:
2123 								currentAttributes.inverse = false;
2124 							break;
2125 							case 28:
2126 								currentAttributes.invisible = false;
2127 							break;
2128 							case 29:
2129 								currentAttributes.strikeout = false;
2130 							break;
2131 							case 30:
2132 							..
2133 							case 37:
2134 							// set foreground color
2135 								/*
2136 								Color nc;
2137 								ubyte multiplier = currentAttributes.bold ? 255 : 127;
2138 								nc.r = cast(ubyte)((arg - 30) & 1) * multiplier;
2139 								nc.g = cast(ubyte)(((arg - 30) & 2)>>1) * multiplier;
2140 								nc.b = cast(ubyte)(((arg - 30) & 4)>>2) * multiplier;
2141 								nc.a = 255;
2142 								*/
2143 								currentAttributes.foregroundIndex = cast(ubyte)(arg - 30);
2144 								version(with_24_bit_color)
2145 								currentAttributes.foreground = palette[arg-30 + (currentAttributes.bold ? 8 : 0)];
2146 							break;
2147 							case 38:
2148 								// xterm 256 color set foreground color
2149 								auto args = getArgs()[argIdx + 1 .. $];
2150 								if(args.length > 3 && args[0] == 2) {
2151 									// set color to closest match in palette. but since we have full support, we'll just take it directly
2152 									auto fg = Color(args[1], args[2], args[3]);
2153 									version(with_24_bit_color)
2154 										currentAttributes.foreground = fg;
2155 									// and try to find a low default palette entry for maximum compatibility
2156 									// 0x8000 == approximation
2157 									currentAttributes.foregroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 16], fg);
2158 								} else if(args.length > 1 && args[0] == 5) {
2159 									// set to palette index
2160 									version(with_24_bit_color)
2161 										currentAttributes.foreground = palette[args[1]];
2162 									currentAttributes.foregroundIndex = cast(ushort) args[1];
2163 								}
2164 								break argsLoop;
2165 							case 39:
2166 							// default foreground color
2167 								auto dflt = defaultTextAttributes();
2168 
2169 								version(with_24_bit_color)
2170 									currentAttributes.foreground = dflt.foreground;
2171 								currentAttributes.foregroundIndex = dflt.foregroundIndex;
2172 							break;
2173 							case 40:
2174 							..
2175 							case 47:
2176 							// set background color
2177 								/*
2178 								Color nc;
2179 								nc.r = cast(ubyte)((arg - 40) & 1) * 255;
2180 								nc.g = cast(ubyte)(((arg - 40) & 2)>>1) * 255;
2181 								nc.b = cast(ubyte)(((arg - 40) & 4)>>2) * 255;
2182 								nc.a = 255;
2183 								*/
2184 
2185 								currentAttributes.backgroundIndex = cast(ubyte)(arg - 40);
2186 								//currentAttributes.background = nc;
2187 								version(with_24_bit_color)
2188 									currentAttributes.background = palette[arg-40];
2189 							break;
2190 							case 48:
2191 								// xterm 256 color set background color
2192 								auto args = getArgs()[argIdx + 1 .. $];
2193 								if(args.length > 3 && args[0] == 2) {
2194 									// set color to closest match in palette. but since we have full support, we'll just take it directly
2195 									auto bg = Color(args[1], args[2], args[3]);
2196 									version(with_24_bit_color)
2197 										currentAttributes.background = Color(args[1], args[2], args[3]);
2198 
2199 									// and try to find a low default palette entry for maximum compatibility
2200 									// 0x8000 == this is an approximation
2201 									currentAttributes.backgroundIndex = 0x8000 | cast(ushort) findNearestColor(xtermPalette[0 .. 8], bg);
2202 								} else if(args.length > 1 && args[0] == 5) {
2203 									// set to palette index
2204 									version(with_24_bit_color)
2205 										currentAttributes.background = palette[args[1]];
2206 									currentAttributes.backgroundIndex = cast(ushort) args[1];
2207 								}
2208 
2209 								break argsLoop;
2210 							case 49:
2211 							// default background color
2212 								auto dflt = defaultTextAttributes();
2213 
2214 								version(with_24_bit_color)
2215 									currentAttributes.background = dflt.background;
2216 								currentAttributes.backgroundIndex = dflt.backgroundIndex;
2217 							break;
2218 							case 51:
2219 								// framed
2220 							break;
2221 							case 52:
2222 								// encircled
2223 							break;
2224 							case 53:
2225 								// overlined
2226 							break;
2227 							case 54:
2228 								// not framed or encircled
2229 							break;
2230 							case 55:
2231 								// not overlined
2232 							break;
2233 							case 90: .. case 97:
2234 								// high intensity foreground color
2235 							break;
2236 							case 100: .. case 107:
2237 								// high intensity background color
2238 							break;
2239 							default:
2240 								unknownEscapeSequence(cast(string) esc);
2241 						}
2242 					break;
2243 					case 'J':
2244 						// erase in display
2245 						auto arg = getArgs(0)[0];
2246 						switch(arg) {
2247 							case 0:
2248 								TerminalCell plain;
2249 								plain.ch = ' ';
2250 								plain.attributes = currentAttributes;
2251 								// erase below
2252 								foreach(i; cursorY * screenWidth + cursorX .. screenWidth * screenHeight) {
2253 									if(alternateScreenActive)
2254 										alternateScreen[i] = plain;
2255 									else
2256 										normalScreen[i] = plain;
2257 								}
2258 							break;
2259 							case 1:
2260 								// erase above
2261 								unknownEscapeSequence("FIXME");
2262 							break;
2263 							case 2:
2264 								// erase all
2265 								cls();
2266 							break;
2267 							default: unknownEscapeSequence(cast(string) esc);
2268 						}
2269 					break;
2270 					case 'r':
2271 						if(esc[1] != '?') {
2272 							// set scrolling zone
2273 							// default should be full size of window
2274 							auto args = getArgs(1, screenHeight);
2275 
2276 							// FIXME: these are supposed to be per-buffer
2277 							scrollZoneTop = args[0] - 1;
2278 							scrollZoneBottom = args[1] - 1;
2279 						} else {
2280 							// restore... something FIXME
2281 						}
2282 					break;
2283 					case 'h':
2284 						if(esc[1] != '?')
2285 						foreach(arg; getArgs())
2286 						switch(arg) {
2287 							case 4:
2288 								insertMode = true;
2289 							break;
2290 							case 34:
2291 								// no idea. vim inside screen sends it
2292 							break;
2293 							default: unknownEscapeSequence(cast(string) esc);
2294 						}
2295 						else
2296 					//import std.stdio; writeln("h magic ", cast(string) esc);
2297 						foreach(arg; getArgsBase(2, null))
2298 							switch(arg) {
2299 								case 1:
2300 									// application cursor keys
2301 									applicationCursorKeys = true;
2302 								break;
2303 								case 3:
2304 									// 132 column mode
2305 								break;
2306 								case 4:
2307 									// smooth scroll
2308 								break;
2309 								case 5:
2310 									// reverse video
2311 									reverseVideo = true;
2312 								break;
2313 								case 6:
2314 									// origin mode
2315 								break;
2316 								case 7:
2317 									// wraparound mode
2318 									wraparoundMode = false;
2319 									// 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
2320 								break;
2321 								case 9:
2322 									allMouseTrackingOff();
2323 									mouseButtonTracking = true;
2324 								break;
2325 								case 12:
2326 									// start blinking cursor
2327 								break;
2328 								case 1034:
2329 									// meta keys????
2330 								break;
2331 								case 1049:
2332 									// Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first.
2333 									alternateScreenActive = true;
2334 									scrollLock = false;
2335 									pushSavedCursor(cursorPosition);
2336 									cls();
2337 								break;
2338 								case 1000:
2339 									// send mouse X&Y on button press and release
2340 									allMouseTrackingOff();
2341 									mouseButtonTracking = true;
2342 									mouseButtonReleaseTracking = true;
2343 								break;
2344 								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
2345 								break;
2346 								case 1002:
2347 									allMouseTrackingOff();
2348 									mouseButtonTracking = true;
2349 									mouseButtonReleaseTracking = true;
2350 									mouseButtonMotionTracking = true;
2351 									// use cell motion mouse tracking
2352 								break;
2353 								case 1003:
2354 									// ALL motion is sent
2355 									allMouseTrackingOff();
2356 									mouseButtonTracking = true;
2357 									mouseButtonReleaseTracking = true;
2358 									mouseMotionTracking = true;
2359 								break;
2360 								case 1005:
2361 									// enable utf-8 mouse mode
2362 									/*
2363 UTF-8 (1005)
2364           This enables UTF-8 encoding for Cx and Cy under all tracking
2365           modes, expanding the maximum encodable position from 223 to
2366           2015.  For positions less than 95, the resulting output is
2367           identical under both modes.  Under extended mouse mode, posi-
2368           tions greater than 95 generate "extra" bytes which will con-
2369           fuse applications which do not treat their input as a UTF-8
2370           stream.  Likewise, Cb will be UTF-8 encoded, to reduce confu-
2371           sion with wheel mouse events.
2372           Under normal mouse mode, positions outside (160,94) result in
2373           byte pairs which can be interpreted as a single UTF-8 charac-
2374           ter; applications which do treat their input as UTF-8 will
2375           almost certainly be confused unless extended mouse mode is
2376           active.
2377           This scheme has the drawback that the encoded coordinates will
2378           not pass through luit unchanged, e.g., for locales using non-
2379           UTF-8 encoding.
2380 									*/
2381 								break;
2382 								case 1006:
2383 								/*
2384 SGR (1006)
2385           The normal mouse response is altered to use CSI < followed by
2386           semicolon-separated encoded button value, the Cx and Cy ordi-
2387           nates and a final character which is M  for button press and m
2388           for button release.
2389           o The encoded button value in this case does not add 32 since
2390             that was useful only in the X10 scheme for ensuring that the
2391             byte containing the button value is a printable code.
2392           o The modifiers are encoded in the same way.
2393           o A different final character is used for button release to
2394             resolve the X10 ambiguity regarding which button was
2395             released.
2396           The highlight tracking responses are also modified to an SGR-
2397           like format, using the same SGR-style scheme and button-encod-
2398           ings.
2399 								*/
2400 								break;
2401 								case 1015:
2402 								/*
2403 URXVT (1015)
2404           The normal mouse response is altered to use CSI followed by
2405           semicolon-separated encoded button value, the Cx and Cy ordi-
2406           nates and final character M .
2407           This uses the same button encoding as X10, but printing it as
2408           a decimal integer rather than as a single byte.
2409           However, CSI M  can be mistaken for DL (delete lines), while
2410           the highlight tracking CSI T  can be mistaken for SD (scroll
2411           down), and the Window manipulation controls.  For these rea-
2412           sons, the 1015 control is not recommended; it is not an
2413           improvement over 1005.
2414 								*/
2415 								break;
2416 								case 1048:
2417 									pushSavedCursor(cursorPosition);
2418 								break;
2419 								case 2004:
2420 									bracketedPasteMode = true;
2421 								break;
2422 								case 1047:
2423 								case 47:
2424 									alternateScreenActive = true;
2425 									scrollLock = false;
2426 									cls();
2427 								break;
2428 								case 25:
2429 									cursorShowing = true;
2430 								break;
2431 								/* Extensions */
2432 								default: unknownEscapeSequence(cast(string) esc);
2433 							}
2434 					break;
2435 					case 'p':
2436 						// it is asking a question... and tbh i don't care.
2437 					break;
2438 					case 'l':
2439 					//import std.stdio; writeln("l magic ", cast(string) esc);
2440 						if(esc[1] != '?')
2441 						foreach(arg; getArgs())
2442 						switch(arg) {
2443 							case 4:
2444 								insertMode = false;
2445 							break;
2446 							case 34:
2447 								// no idea. vim inside screen sends it
2448 							break;
2449 							case 1005:
2450 								// turn off utf-8 mouse
2451 							break;
2452 							case 1006:
2453 								// turn off sgr mouse
2454 							break;
2455 							case 1015:
2456 								// turn off urxvt mouse
2457 							break;
2458 							default: unknownEscapeSequence(cast(string) esc);
2459 						}
2460 						else
2461 						foreach(arg; getArgsBase(2, null))
2462 							switch(arg) {
2463 								case 1:
2464 									// normal cursor keys
2465 									applicationCursorKeys = false;
2466 								break;
2467 								case 3:
2468 									// 80 column mode
2469 								break;
2470 								case 4:
2471 									// smooth scroll
2472 								break;
2473 								case 5:
2474 									// normal video
2475 									reverseVideo = false;
2476 								break;
2477 								case 6:
2478 									// normal cursor mode
2479 								break;
2480 								case 7:
2481 									// wraparound mode
2482 									wraparoundMode = true;
2483 								break;
2484 								case 12:
2485 									// stop blinking cursor
2486 								break;
2487 								case 1034:
2488 									// meta keys????
2489 								break;
2490 								case 1049:
2491 									cursorPosition = popSavedCursor;
2492 									wraparoundMode = true;
2493 
2494 									returnToNormalScreen();
2495 								break;
2496 								case 1001: // hilight tracking, this is kinda weird so i don't think i want to implement it
2497 								break;
2498 								case 9:
2499 								case 1000:
2500 								case 1002:
2501 								case 1003:
2502 									allMouseTrackingOff();
2503 								break;
2504 								case 1005:
2505 								case 1006:
2506 									// idk
2507 								break;
2508 								case 1048:
2509 									cursorPosition = popSavedCursor;
2510 								break;
2511 								case 2004:
2512 									bracketedPasteMode = false;
2513 								break;
2514 								case 1047:
2515 								case 47:
2516 									returnToNormalScreen();
2517 								break;
2518 								case 25:
2519 									cursorShowing = false;
2520 								break;
2521 								default: unknownEscapeSequence(cast(string) esc);
2522 							}
2523 					break;
2524 					case 'X':
2525 						// erase characters
2526 						auto count = getArgs(1)[0];
2527 						TerminalCell plain;
2528 						plain.ch = ' ';
2529 						plain.attributes = currentAttributes;
2530 						foreach(cnt; 0 .. count) {
2531 							ASS[cursorY][cnt + cursorX] = plain;
2532 						}
2533 					break;
2534 					case 'S':
2535 						auto count = getArgs(1)[0];
2536 						// scroll up
2537 						scrollUp(count);
2538 					break;
2539 					case 'T':
2540 						auto count = getArgs(1)[0];
2541 						// scroll down
2542 						scrollDown(count);
2543 					break;
2544 					case 'P':
2545 						auto count = getArgs(1)[0];
2546 						// delete characters
2547 
2548 						foreach(cnt; 0 .. count) {
2549 							for(int i = cursorX; i < this.screenWidth-1; i++) {
2550 								if(ASS[cursorY][i].selected)
2551 									clearSelection();
2552 								ASS[cursorY][i] = ASS[cursorY][i + 1];
2553 								ASS[cursorY][i].invalidated = true;
2554 							}
2555 
2556 							if(ASS[cursorY][this.screenWidth - 1].selected)
2557 								clearSelection();
2558 							ASS[cursorY][this.screenWidth-1].ch = ' ';
2559 							ASS[cursorY][this.screenWidth-1].invalidated = true;
2560 						}
2561 					break;
2562 					case '@':
2563 						// insert blank characters
2564 						auto count = getArgs(1)[0];
2565 						foreach(idx; 0 .. count) {
2566 							for(int i = this.screenWidth - 1; i > cursorX; i--) {
2567 								ASS[cursorY][i] = ASS[cursorY][i - 1];
2568 								ASS[cursorY][i].invalidated = true;
2569 							}
2570 							ASS[cursorY][cursorX].ch = ' ';
2571 							ASS[cursorY][cursorX].invalidated = true;
2572 						}
2573 					break;
2574 					case 'c':
2575 						// send device attributes
2576 						// FIXME: what am i supposed to do here?
2577 						//sendToApplication("\033[>0;138;0c");
2578 						//sendToApplication("\033[?62;");
2579 						sendToApplication(terminalIdCode);
2580 					break;
2581 					default:
2582 						// [42\esc] seems to have gotten here once somehow
2583 						// also [24\esc]
2584 						unknownEscapeSequence("" ~ cast(string) esc);
2585 				}
2586 			} else {
2587 				unknownEscapeSequence(cast(string) esc);
2588 			}
2589 		}
2590 	}
2591 }
2592 
2593 // These match the numbers in terminal.d, so you can just cast it back and forth
2594 // and the names match simpledisplay.d so you can convert that automatically too
2595 enum TerminalKey : int {
2596 	Escape = 0x1b,// + 0xF0000, /// .
2597 	F1 = 0x70,// + 0xF0000, /// .
2598 	F2 = 0x71,// + 0xF0000, /// .
2599 	F3 = 0x72,// + 0xF0000, /// .
2600 	F4 = 0x73,// + 0xF0000, /// .
2601 	F5 = 0x74,// + 0xF0000, /// .
2602 	F6 = 0x75,// + 0xF0000, /// .
2603 	F7 = 0x76,// + 0xF0000, /// .
2604 	F8 = 0x77,// + 0xF0000, /// .
2605 	F9 = 0x78,// + 0xF0000, /// .
2606 	F10 = 0x79,// + 0xF0000, /// .
2607 	F11 = 0x7A,// + 0xF0000, /// .
2608 	F12 = 0x7B,// + 0xF0000, /// .
2609 	Left = 0x25,// + 0xF0000, /// .
2610 	Right = 0x27,// + 0xF0000, /// .
2611 	Up = 0x26,// + 0xF0000, /// .
2612 	Down = 0x28,// + 0xF0000, /// .
2613 	Insert = 0x2d,// + 0xF0000, /// .
2614 	Delete = 0x2e,// + 0xF0000, /// .
2615 	Home = 0x24,// + 0xF0000, /// .
2616 	End = 0x23,// + 0xF0000, /// .
2617 	PageUp = 0x21,// + 0xF0000, /// .
2618 	PageDown = 0x22,// + 0xF0000, /// .
2619 	ScrollLock = 0x91, 
2620 }
2621 
2622 /* These match simpledisplay.d which match terminal.d, so you can just cast them */
2623 
2624 enum MouseEventType : int {
2625 	motion = 0,
2626 	buttonPressed = 1,
2627 	buttonReleased = 2,
2628 }
2629 
2630 enum MouseButton : int {
2631 	// these names assume a right-handed mouse
2632 	left = 1,
2633 	right = 2,
2634 	middle = 4,
2635 	wheelUp = 8,
2636 	wheelDown = 16,
2637 }
2638 
2639 
2640 
2641 /*
2642 mixin template ImageSupport() {
2643 	import arsd.png;
2644 	import arsd.bmp;
2645 }
2646 */
2647 
2648 
2649 /* helper functions that are generally useful but not necessarily required */
2650 
2651 version(use_libssh2) {
2652 	import arsd.libssh2;
2653 	void startChild(alias masterFunc)(string host, short port, string username, string keyFile, string expectedFingerprint = null) {
2654 
2655 	int tries = 0;
2656 	try_again:
2657 	try {
2658 		import std.socket;
2659 
2660 		if(libssh2_init(0))
2661 			throw new Exception("libssh2_init");
2662 		scope(exit)
2663 			libssh2_exit();
2664 
2665 		auto socket = new Socket(AddressFamily.INET, SocketType.STREAM);
2666 		socket.connect(new InternetAddress(host, port));
2667 		scope(exit) socket.close();
2668 
2669 		auto session = libssh2_session_init_ex(null, null, null, null);
2670 		if(session is null) throw new Exception("init session");
2671 		scope(exit)
2672 			libssh2_session_disconnect_ex(session, 0, "normal", "EN");
2673 
2674 		libssh2_session_flag(session, LIBSSH2_FLAG_COMPRESS, 1);
2675 
2676 		if(libssh2_session_handshake(session, socket.handle))
2677 			throw new Exception("handshake");
2678 
2679 		auto fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);
2680 		if(expectedFingerprint !is null && fingerprint[0 .. expectedFingerprint.length] != expectedFingerprint)
2681 			throw new Exception("fingerprint");
2682 
2683 		import std..string : toStringz;
2684 		if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
2685 			throw new Exception("auth");
2686 
2687 
2688 		auto channel = libssh2_channel_open_ex(session, "session".ptr, "session".length, LIBSSH2_CHANNEL_WINDOW_DEFAULT, LIBSSH2_CHANNEL_PACKET_DEFAULT, null, 0);
2689 
2690 		if(channel is null)
2691 			throw new Exception("channel open");
2692 
2693 		scope(exit)
2694 			libssh2_channel_free(channel);
2695 
2696 		// libssh2_channel_setenv_ex(channel, "ELVISBG".dup.ptr, "ELVISBG".length, "dark".ptr, "dark".length);
2697 
2698 		if(libssh2_channel_request_pty_ex(channel, "xterm", "xterm".length, null, 0, 80, 24, 0, 0))
2699 			throw new Exception("pty");
2700 
2701 		if(libssh2_channel_process_startup(channel, "shell".ptr, "shell".length, null, 0))
2702 			throw new Exception("process_startup");
2703 
2704 		libssh2_keepalive_config(session, 0, 60);
2705 		libssh2_session_set_blocking(session, 0);
2706 
2707 		masterFunc(socket, session, channel);
2708 	} catch(Exception e) {
2709 		if(e.msg == "handshake") {
2710 			tries++;
2711 			import core.thread;
2712 			Thread.sleep(200.msecs);
2713 			if(tries < 10)
2714 				goto try_again;
2715 		}
2716 
2717 		throw e;
2718 	}
2719 	}
2720 
2721 } else
2722 version(Posix) {
2723 	extern(C) static int forkpty(int* master, /*int* slave,*/ void* name, void* termp, void* winp);
2724 	pragma(lib, "util");
2725 
2726 	/// this is good
2727 	void startChild(alias masterFunc)(string program, string[] args) {
2728 		import core.sys.posix.termios;
2729 		import core.sys.posix.signal;
2730 		import core.sys.posix.sys.wait;
2731 		__gshared static int childrenAlive = 0;
2732 		extern(C) nothrow static @nogc
2733 		void childdead(int) {
2734 			childrenAlive--;
2735 
2736 			wait(null);
2737 
2738 			version(with_eventloop)
2739 			try {
2740 				import arsd.eventloop;
2741 				if(childrenAlive <= 0)
2742 					exit();
2743 			} catch(Exception e){}
2744 		}
2745 
2746 		signal(SIGCHLD, &childdead);
2747 
2748 		int master;
2749 		int pid = forkpty(&master, null, null, null);
2750 		if(pid == -1)
2751 			throw new Exception("forkpty");
2752 		if(pid == 0) {
2753 			import std.process;
2754 			environment["TERM"] = "xterm"; // we're closest to an xterm, so definitely want to pretend to be one to the child processes
2755 			environment["TERM_EXTENSIONS"] = "arsd"; // announce our extensions
2756 
2757 			import std..string;
2758 			if(environment["LANG"].indexOf("UTF-8") == -1)
2759 				environment["LANG"] = "en_US.UTF-8"; // tell them that utf8 rox (FIXME: what about non-US?)
2760 
2761 			import core.sys.posix.unistd;
2762 
2763 			import core.stdc.stdlib;
2764 			char** argv = cast(char**) malloc((char*).sizeof * (args.length + 1));
2765 			if(argv is null) throw new Exception("malloc");
2766 			foreach(i, arg; args) {
2767 				argv[i] = cast(char*) malloc(arg.length + 1);
2768 				if(argv[i] is null) throw new Exception("malloc");
2769 				argv[i][0 .. arg.length] = arg[];
2770 				argv[i][arg.length] = 0;
2771 			}
2772 
2773 			argv[args.length] = null;
2774 
2775 			core.sys.posix.unistd.execv(argv[0], argv);
2776 		} else {
2777 			childrenAlive = 1;
2778 			masterFunc(master);
2779 		}
2780 	}
2781 } else
2782 version(Windows) {
2783 	import core.sys.windows.windows;
2784 
2785 	version(winpty) {
2786 		alias HPCON = HANDLE;
2787 		extern(Windows)
2788 			HRESULT function(HPCON, COORD) ResizePseudoConsole;
2789 		extern(Windows)
2790 			HRESULT function(COORD, HANDLE, HANDLE, DWORD, HPCON*) CreatePseudoConsole;
2791 		extern(Windows)
2792 			void function(HPCON) ClosePseudoConsole;
2793 	}
2794 
2795 	extern(Windows)
2796 		BOOL PeekNamedPipe(HANDLE, LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD);
2797 	extern(Windows)
2798 		BOOL GetOverlappedResult(HANDLE,OVERLAPPED*,LPDWORD,BOOL);
2799 	extern(Windows)
2800 		BOOL ReadFileEx(HANDLE, LPVOID, DWORD, OVERLAPPED*, void*);
2801 	extern(Windows)
2802 		BOOL PostMessageA(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
2803 
2804 	extern(Windows)
2805 		BOOL PostThreadMessageA(DWORD, UINT, WPARAM, LPARAM);
2806 	extern(Windows)
2807 		BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject, HANDLE hObject, void* Callback, PVOID Context, ULONG dwMilliseconds, ULONG dwFlags);
2808 	extern(Windows)
2809 		BOOL SetHandleInformation(HANDLE, DWORD, DWORD);
2810 	extern(Windows)
2811 	HANDLE CreateNamedPipeA(
2812 		const(char)* lpName,
2813 		DWORD dwOpenMode,
2814 		DWORD dwPipeMode,
2815 		DWORD nMaxInstances,
2816 		DWORD nOutBufferSize,
2817 		DWORD nInBufferSize,
2818 		DWORD nDefaultTimeOut,
2819 		LPSECURITY_ATTRIBUTES lpSecurityAttributes
2820 	);
2821 	extern(Windows)
2822 	BOOL UnregisterWait(HANDLE);
2823 
2824 	struct STARTUPINFOEXA {
2825 		STARTUPINFOA StartupInfo;
2826 		void* lpAttributeList;
2827 	}
2828 
2829 	enum PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
2830 	enum EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
2831 
2832 	extern(Windows)
2833 	BOOL InitializeProcThreadAttributeList(void*, DWORD, DWORD, PSIZE_T);
2834 	extern(Windows)
2835 	BOOL UpdateProcThreadAttribute(void*, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T);
2836 
2837 	__gshared HANDLE waitHandle;
2838 	__gshared bool childDead;
2839 	extern(Windows)
2840 	void childCallback(void* tidp, bool) {
2841 		auto tid = cast(DWORD) tidp;
2842 		UnregisterWait(waitHandle);
2843 
2844 		PostThreadMessageA(tid, WM_QUIT, 0, 0);
2845 		childDead = true;
2846 		//stupidThreadAlive = false;
2847 	}
2848 
2849 
2850 
2851 	extern(Windows)
2852 	void SetLastError(DWORD);
2853 
2854 	/// this is good. best to call it with plink.exe so it can talk to unix
2855 	/// note that plink asks for the password out of band, so it won't actually work like that.
2856 	/// thus specify the password on the command line or better yet, use a private key file
2857 	/// e.g.
2858 	/// startChild!something("plink.exe", "plink.exe user@server -i key.ppk \"/home/user/terminal-emulator/serverside\"");
2859 	void startChild(alias masterFunc)(string program, string commandLine) {
2860 		import core.sys.windows.windows;
2861 		// thanks for a random person on stack overflow for this function
2862 		static BOOL MyCreatePipeEx(
2863 			PHANDLE lpReadPipe,
2864 			PHANDLE lpWritePipe,
2865 			LPSECURITY_ATTRIBUTES lpPipeAttributes,
2866 			DWORD nSize,
2867 			DWORD dwReadMode,
2868 			DWORD dwWriteMode
2869 		)
2870 		{
2871 			HANDLE ReadPipeHandle, WritePipeHandle;
2872 			DWORD dwError;
2873 			CHAR[MAX_PATH] PipeNameBuffer;
2874 
2875 			if (nSize == 0) {
2876 				nSize = 4096;
2877 			}
2878 
2879 			static int PipeSerialNumber = 0;
2880 
2881 			import core.stdc..string;
2882 			import core.stdc.stdio;
2883 
2884 			sprintf(PipeNameBuffer.ptr,
2885 				"\\\\.\\Pipe\\TerminalEmulatorPipe.%08x.%08x".ptr,
2886 				GetCurrentProcessId(),
2887 				PipeSerialNumber++
2888 			);
2889 
2890 			ReadPipeHandle = CreateNamedPipeA(
2891 				PipeNameBuffer.ptr,
2892 				1/*PIPE_ACCESS_INBOUND*/ | dwReadMode,
2893 				0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/,
2894 				1,             // Number of pipes
2895 				nSize,         // Out buffer size
2896 				nSize,         // In buffer size
2897 				120 * 1000,    // Timeout in ms
2898 				lpPipeAttributes
2899 			);
2900 
2901 			if (! ReadPipeHandle) {
2902 				return FALSE;
2903 			}
2904 
2905 			WritePipeHandle = CreateFileA(
2906 				PipeNameBuffer.ptr,
2907 				GENERIC_WRITE,
2908 				0,                         // No sharing
2909 				lpPipeAttributes,
2910 				OPEN_EXISTING,
2911 				FILE_ATTRIBUTE_NORMAL | dwWriteMode,
2912 				null                       // Template file
2913 			);
2914 
2915 			if (INVALID_HANDLE_VALUE == WritePipeHandle) {
2916 				dwError = GetLastError();
2917 				CloseHandle( ReadPipeHandle );
2918 				SetLastError(dwError);
2919 				return FALSE;
2920 			}
2921 
2922 			*lpReadPipe = ReadPipeHandle;
2923 			*lpWritePipe = WritePipeHandle;
2924 			return( TRUE );
2925 		}
2926 
2927 
2928 
2929 
2930 
2931 		import std.conv;
2932 
2933 		SECURITY_ATTRIBUTES saAttr;
2934 		saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
2935 		saAttr.bInheritHandle = true;
2936 		saAttr.lpSecurityDescriptor = null;
2937 
2938 		HANDLE inreadPipe;
2939 		HANDLE inwritePipe;
2940 		if(CreatePipe(&inreadPipe, &inwritePipe, &saAttr, 0) == 0)
2941 			throw new Exception("CreatePipe");
2942 		if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
2943 			throw new Exception("SetHandleInformation");
2944 		HANDLE outreadPipe;
2945 		HANDLE outwritePipe;
2946 
2947 		version(winpty)
2948 			auto flags = 0;
2949 		else
2950 			auto flags = FILE_FLAG_OVERLAPPED;
2951 
2952 		if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, flags, 0) == 0)
2953 			throw new Exception("CreatePipe");
2954 		if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
2955 			throw new Exception("SetHandleInformation");
2956 
2957 		version(winpty) {
2958 
2959 			auto lib = LoadLibrary("kernel32.dll");
2960 			if(lib is null) throw new Exception("holy wtf batman");
2961 			scope(exit) FreeLibrary(lib);
2962 
2963 			CreatePseudoConsole = cast(typeof(CreatePseudoConsole)) GetProcAddress(lib, "CreatePseudoConsole");
2964 			ClosePseudoConsole = cast(typeof(ClosePseudoConsole)) GetProcAddress(lib, "ClosePseudoConsole");
2965 			ResizePseudoConsole = cast(typeof(ResizePseudoConsole)) GetProcAddress(lib, "ResizePseudoConsole");
2966 
2967 			if(CreatePseudoConsole is null || ClosePseudoConsole is null || ResizePseudoConsole is null)
2968 				throw new Exception("Windows pseudo console not available on this version");
2969 
2970 			initPipeHack(outreadPipe);
2971 
2972 			HPCON hpc;
2973 			auto result = CreatePseudoConsole(
2974 				COORD(80, 24),
2975 				inreadPipe,
2976 				outwritePipe,
2977 				0, // flags
2978 				&hpc
2979 			);
2980 
2981 			assert(result == S_OK);
2982 
2983 			scope(exit)
2984 				ClosePseudoConsole(hpc);
2985 		}
2986 
2987 		STARTUPINFOEXA siex;
2988 		siex.StartupInfo.cb = siex.sizeof;
2989 
2990 		version(winpty) {
2991 			size_t size;
2992 			InitializeProcThreadAttributeList(null, 1, 0, &size);
2993 			ubyte[] wtf = new ubyte[](size);
2994 			siex.lpAttributeList = wtf.ptr;
2995 			InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &size);
2996 			UpdateProcThreadAttribute(
2997 				siex.lpAttributeList,
2998 				0,
2999 				PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
3000 				hpc,
3001 				hpc.sizeof,
3002 				null,
3003 				null
3004 			);
3005 		} {//else {
3006 			siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
3007 			siex.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);//inreadPipe;
3008 			siex.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);//outwritePipe;
3009 			siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//outwritePipe;
3010 		}
3011 
3012 		PROCESS_INFORMATION pi;
3013 		import std.conv;
3014 
3015 		if(commandLine.length > 255)
3016 			throw new Exception("command line too long");
3017 		char[256] cmdLine;
3018 		cmdLine[0 .. commandLine.length] = commandLine[];
3019 		cmdLine[commandLine.length] = 0;
3020 		import std..string;
3021 		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)
3022 			throw new Exception("CreateProcess " ~ to!string(GetLastError()));
3023 
3024 		if(RegisterWaitForSingleObject(&waitHandle, pi.hProcess, &childCallback, cast(void*) GetCurrentThreadId(), INFINITE, 4 /* WT_EXECUTEINWAITTHREAD */ | 8 /* WT_EXECUTEONLYONCE */) == 0)
3025 			throw new Exception("RegisterWaitForSingleObject");
3026 
3027 		version(winpty)
3028 			masterFunc(hpc, inwritePipe, outreadPipe);
3029 		else
3030 			masterFunc(inwritePipe, outreadPipe);
3031 
3032 		//stupidThreadAlive = false;
3033 
3034 		//term.stupidThread.join();
3035 
3036 		/* // FIXME: we should close but only if we're legit done
3037 		// masterFunc typically runs an event loop but it might not.
3038 		CloseHandle(inwritePipe);
3039 		CloseHandle(outreadPipe);
3040 
3041 		CloseHandle(pi.hThread);
3042 		CloseHandle(pi.hProcess);
3043 		*/
3044 	}
3045 }
3046 
3047 /// Implementation of TerminalEmulator's abstract functions that forward them to output
3048 mixin template ForwardVirtuals(alias writer) {
3049 	static import arsd.color;
3050 
3051 	protected override void changeCursorStyle(CursorStyle style) {
3052 		// FIXME: this should probably just import utility
3053 		final switch(style) {
3054 			case TerminalEmulator.CursorStyle.block:
3055 				writer("\033[2 q");
3056 			break;
3057 			case TerminalEmulator.CursorStyle.underline:
3058 				writer("\033[4 q");
3059 			break;
3060 			case TerminalEmulator.CursorStyle.bar:
3061 				writer("\033[6 q");
3062 			break;
3063 		}
3064 	}
3065 
3066 	protected override void changeWindowTitle(string t) {
3067 		import std.process;
3068 		if(t.length && environment["TERM"] != "linux")
3069 			writer("\033]0;"~t~"\007");
3070 	}
3071 
3072 	protected override void changeWindowIcon(arsd.color.IndexedImage t) {
3073 		if(t !is null) {
3074 			// forward it via our extension. xterm and such seems to ignore this so we should be ok just sending, except to Linux
3075 			import std.process;
3076 			if(environment["TERM"] != "linux")
3077 				writer("\033]5000;" ~ encodeSmallTextImage(t) ~ "\007");
3078 		}
3079 	}
3080 
3081 	protected override void changeIconTitle(string) {} // FIXME
3082 	protected override void changeTextAttributes(TextAttributes) {} // FIXME
3083 	protected override void soundBell() {
3084 		writer("\007");
3085 	}
3086 	protected override void demandAttention() {
3087 		import std.process;
3088 		if(environment["TERM"] != "linux")
3089 			writer("\033]5001;1\007"); // the 1 there means true but is currently ignored
3090 	}
3091 	protected override void copyToClipboard(string text) {
3092 		// this is xterm compatible, though xterm rarely implements it
3093 		import std.base64;
3094 				// idk why the cast is needed here
3095 		writer("\033]52;c;"~Base64.encode(cast(ubyte[])text)~"\007");
3096 	}
3097 	protected override void pasteFromClipboard(void delegate(in char[]) dg) {
3098 		// this is a slight extension. xterm invented the string - it means request the primary selection -
3099 		// but it generally doesn't actually get a reply. so i'm using it to request the primary which will be
3100 		// sent as a pasted strong.
3101 		// (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.)
3102 		writer("\033]52;c;?\007");
3103 	}
3104 	protected override void copyToPrimary(string text) {
3105 		import std.base64;
3106 		writer("\033]52;p;"~Base64.encode(cast(ubyte[])text)~"\007");
3107 	}
3108 	protected override void pasteFromPrimary(void delegate(in char[]) dg) {
3109 		writer("\033]52;p;?\007");
3110 	}
3111 
3112 }
3113 
3114 /// you can pass this as PtySupport's arguments when you just don't care
3115 final void doNothing() {}
3116 
3117 version(winpty) {
3118 		__gshared static HANDLE inputEvent;
3119 		__gshared static HANDLE magicEvent;
3120 		__gshared static ubyte[] helperBuffer;
3121 		__gshared static HANDLE helperThread;
3122 
3123 		static void initPipeHack(void* ptr) {
3124 			inputEvent = CreateEvent(null, false, false, null);
3125 			assert(inputEvent !is null);
3126 			magicEvent = CreateEvent(null, false, true, null);
3127 			assert(magicEvent !is null);
3128 
3129 			helperThread = CreateThread(
3130 				null,
3131 				0,
3132 				&actuallyRead,
3133 				ptr,
3134 				0,
3135 				null
3136 			);
3137 
3138 			assert(helperThread !is null);
3139 		}
3140 
3141 		extern(Windows) static
3142 		uint actuallyRead(void* ptr) {
3143 			ubyte[4096] buffer;
3144 			DWORD got;
3145 			while(true) {
3146 				// wait for the other thread to tell us they
3147 				// are done...
3148 				WaitForSingleObject(magicEvent, INFINITE);
3149 				auto ret = ReadFile(ptr, buffer.ptr, cast(DWORD) buffer.length, &got, null);
3150 				helperBuffer = buffer[0 .. got];
3151 				// tells the other thread it is allowed to read
3152 				// readyToReadPty
3153 				SetEvent(inputEvent);
3154 			}
3155 			assert(0);
3156 		}
3157 
3158 
3159 }
3160 
3161 /// You must implement a function called redraw() and initialize the members in your constructor
3162 mixin template PtySupport(alias resizeHelper) {
3163 	// Initialize these!
3164 
3165 	final void redraw_() {
3166 		if(invalidateAll) {
3167 			if(alternateScreenActive)
3168 				foreach(ref t; alternateScreen)
3169 					t.invalidated = true;
3170 			else
3171 				foreach(ref t; normalScreen)
3172 					t.invalidated = true;
3173 			invalidateAll = false;
3174 		}
3175 		redraw();
3176 		//soundBell();
3177 	}
3178 
3179 	version(use_libssh2) {
3180 		import arsd.libssh2;
3181 		LIBSSH2_CHANNEL* sshChannel;
3182 	} else version(Windows) {
3183 		import core.sys.windows.windows;
3184 		HANDLE stdin;
3185 		HANDLE stdout;
3186 	} else version(Posix) {
3187 		int master;
3188 	}
3189 
3190 	version(use_libssh2) { }
3191 	else version(Posix) {
3192 		int previousProcess = 0;
3193 		int activeProcess = 0;
3194 		int activeProcessWhenResized = 0;
3195 		bool resizedRecently;
3196 
3197 		/*
3198 			so, this isn't perfect, but it is meant to send the resize signal to an existing process
3199 			when it isn't in the front when you resize.
3200 
3201 			For example, open vim and resize. Then exit vim. We want bash to be updated.
3202 
3203 			But also don't want to do too many spurious signals.
3204 
3205 			It doesn't handle the case of bash -> vim -> :sh resize, then vim gets signal but
3206 			the outer bash won't see it. I guess I need some kind of process stack.
3207 
3208 			but it is okish.
3209 		*/
3210 		override void outputOccurred() {
3211 			import core.sys.posix.unistd;
3212 			auto pgrp = tcgetpgrp(master);
3213 			if(pgrp != -1) {
3214 				if(pgrp != activeProcess) {
3215 					auto previousProcessAtStartup = previousProcess;
3216 
3217 					previousProcess = activeProcess;
3218 					activeProcess = pgrp;
3219 
3220 					if(resizedRecently) {
3221 						if(activeProcess != activeProcessWhenResized) {
3222 							resizedRecently = false;
3223 
3224 							if(activeProcess == previousProcessAtStartup) {
3225 								//import std.stdio; writeln("informing new process ", activeProcess, " of size ", screenWidth, " x ", screenHeight);
3226 
3227 								import core.sys.posix.signal;
3228 								kill(-activeProcess, 28 /* 28 == SIGWINCH*/);
3229 							}
3230 						}
3231 					}
3232 				}
3233 			}
3234 
3235 
3236 			super.outputOccurred();
3237 		}
3238 		//return std.file.readText("/proc/" ~ to!string(pgrp) ~ "/cmdline");
3239 	}
3240 
3241 
3242 	override void resizeTerminal(int w, int h) {
3243 		version(Posix) {
3244 			activeProcessWhenResized = activeProcess;
3245 			resizedRecently = true;
3246 		}
3247 
3248 		resizeHelper();
3249 
3250 		super.resizeTerminal(w, h);
3251 
3252 		version(use_libssh2) {
3253 			libssh2_channel_request_pty_size_ex(sshChannel, w, h, 0, 0);
3254 		} else version(Posix) {
3255 			import core.sys.posix.sys.ioctl;
3256 			winsize win;
3257 			win.ws_col = cast(ushort) w;
3258 			win.ws_row = cast(ushort) h;
3259 
3260 			ioctl(master, TIOCSWINSZ, &win);
3261 		} else version(Windows) {
3262 			version(winpty) {
3263 				COORD coord;
3264 				coord.X = cast(ushort) w;
3265 				coord.Y = cast(ushort) h;
3266 				ResizePseudoConsole(hpc, coord);
3267 			} else {
3268 				sendToApplication([cast(ubyte) 254, cast(ubyte) w, cast(ubyte) h]);
3269 			}
3270 		} else static assert(0);
3271 	}
3272 
3273 	protected override void sendToApplication(scope const(void)[] data) {
3274 		version(use_libssh2) {
3275 			while(data.length) {
3276 				auto sent = libssh2_channel_write_ex(sshChannel, 0, data.ptr, data.length);
3277 				if(sent < 0)
3278 					throw new Exception("libssh2_channel_write_ex");
3279 				data = data[sent .. $];
3280 			}
3281 		} else version(Windows) {
3282 			import std.conv;
3283 			uint written;
3284 			if(WriteFile(stdin, data.ptr, cast(uint)data.length, &written, null) == 0)
3285 				throw new Exception("WriteFile " ~ to!string(GetLastError()));
3286 		} else version(Posix) {
3287 			import core.sys.posix.unistd;
3288 			while(data.length) {
3289 				enum MAX_SEND = 1024 * 20;
3290 				auto sent = write(master, data.ptr, data.length > MAX_SEND ? MAX_SEND : cast(int) data.length);
3291 				//import std.stdio; writeln("ROFL ", sent, " ", data.length);
3292 
3293 				import core.stdc.errno;
3294 				/*
3295 				if(sent == -1 && errno == 11) {
3296 					import core.thread;
3297 					Thread.sleep(100.msecs);
3298 					//import std.stdio; writeln("lol");
3299 					continue; // just try again
3300 				}
3301 				*/
3302 
3303 				import std.conv;
3304 				if(sent < 0)
3305 					throw new Exception("write " ~ to!string(errno));
3306 
3307 				data = data[sent .. $];
3308 			}
3309 		} else static assert(0);
3310 	}
3311 
3312 	version(use_libssh2) {
3313 		int readyToRead(int fd) {
3314 			int count = 0; // if too much stuff comes at once, we still want to be responsive
3315 			while(true) {
3316 				ubyte[4096] buffer;
3317 				auto got = libssh2_channel_read_ex(sshChannel, 0, buffer.ptr, buffer.length);
3318 				if(got == LIBSSH2_ERROR_EAGAIN)
3319 					break; // got it all for now
3320 				if(got < 0)
3321 					throw new Exception("libssh2_channel_read_ex");
3322 				if(got == 0)
3323 					break; // NOT an error!
3324 
3325 				super.sendRawInput(buffer[0 .. got]);
3326 				count++;
3327 
3328 				if(count == 5) {
3329 					count = 0;
3330 					redraw_();
3331 					justRead();
3332 				}
3333 			}
3334 
3335 			if(libssh2_channel_eof(sshChannel)) {
3336 				libssh2_channel_close(sshChannel);
3337 				libssh2_channel_wait_closed(sshChannel);
3338 
3339 				return 1;
3340 			}
3341 
3342 			if(count != 0) {
3343 				redraw_();
3344 				justRead();
3345 			}
3346 			return 0;
3347 		}
3348 	} else version(winpty) {
3349 		void readyToReadPty() {
3350 			super.sendRawInput(helperBuffer);
3351 			SetEvent(magicEvent); // tell the other thread we have finished
3352 			redraw_();
3353 			justRead();
3354 		}
3355 	} else version(Windows) {
3356 		OVERLAPPED* overlapped;
3357 		bool overlappedBufferLocked;
3358 		ubyte[4096] overlappedBuffer;
3359 		extern(Windows)
3360 		static final void readyToReadWindows(DWORD errorCode, DWORD numberOfBytes, OVERLAPPED* overlapped) {
3361 			assert(overlapped !is null);
3362 			typeof(this) w = cast(typeof(this)) overlapped.hEvent;
3363 
3364 			if(numberOfBytes) {
3365 				w.sendRawInput(w.overlappedBuffer[0 .. numberOfBytes]);
3366 				w.redraw_();
3367 			}
3368 			import std.conv;
3369 
3370 			if(ReadFileEx(w.stdout, w.overlappedBuffer.ptr, w.overlappedBuffer.length, overlapped, &readyToReadWindows) == 0) {
3371 				if(GetLastError() == 997)
3372 				{ } // there's pending i/o, let's just ignore for now and it should tell us later that it completed
3373 				else
3374 				throw new Exception("ReadFileEx " ~ to!string(GetLastError()));
3375 			} else {
3376 			}
3377 
3378 			w.justRead();
3379 		}
3380 	} else version(Posix) {
3381 		void readyToRead(int fd) {
3382 			import core.sys.posix.unistd;
3383 			ubyte[4096] buffer;
3384 
3385 			// the count is to limit how long we spend in this loop
3386 			// when it runs out, it goes back to the main event loop
3387 			// for a while (btw use level triggered events so the remaining
3388 			// data continues to get processed!) giving a chance to redraw
3389 			// and process user input periodically during insanely long and
3390 			// rapid output.
3391 			int cnt = 50; // the actual count is arbitrary, it just seems nice in my tests
3392 
3393 			version(arsd_te_conservative_draws)
3394 				cnt = 400;
3395 
3396 			// FIXME: if connected by ssh, up the count so we don't redraw as frequently.
3397 			// it'd save bandwidth
3398 
3399 			while(--cnt) {
3400 				auto len = read(fd, buffer.ptr, 4096);
3401 				if(len < 0) {
3402 					import core.stdc.errno;
3403 					if(errno == EAGAIN || errno == EWOULDBLOCK) {
3404 						break; // we got it all
3405 					} else {
3406 						//import std.conv;
3407 						//throw new Exception("read failed " ~ to!string(errno));
3408 						return;
3409 					}
3410 				}
3411 
3412 				if(len == 0) {
3413 					close(fd);
3414 					requestExit();
3415 					break;
3416 				}
3417 
3418 				auto data = buffer[0 .. len];
3419 
3420 				if(debugMode) {
3421 					import std.array; import std.stdio; writeln("GOT ", data, "\nOR ", 
3422 						replace(cast(string) data, "\033", "\\")
3423 						.replace("\010", "^H")
3424 						.replace("\r", "^M")
3425 						.replace("\n", "^J")
3426 						);
3427 				}
3428 				super.sendRawInput(data);
3429 			}
3430 
3431 			outputOccurred();
3432 
3433 			redraw_();
3434 
3435 			// HACK: I don't even know why this works, but with this
3436 			// sleep in place, it gives X events from that socket a
3437 			// chance to be processed. It can add a few seconds to a huge
3438 			// output (like `find /usr`), but meh, that's worth it to me
3439 			// to have a chance to ctrl+c.
3440 			import core.thread;
3441 			Thread.sleep(dur!"msecs"(5));
3442 
3443 			justRead();
3444 		}
3445 	}
3446 }
3447 
3448 string encodeSmallTextImage(IndexedImage ii) {
3449 	char encodeNumeric(int c) {
3450 		if(c < 10)
3451 			return cast(char)(c + '0');
3452 		if(c < 10 + 26)
3453 			return cast(char)(c - 10 + 'a');
3454 		assert(0);
3455 	}
3456 
3457 	string s;
3458 	s ~= encodeNumeric(ii.width);
3459 	s ~= encodeNumeric(ii.height);
3460 
3461 	foreach(entry; ii.palette)
3462 		s ~= entry.toRgbaHexString();
3463 	s ~= "Z";
3464 
3465 	ubyte rleByte;
3466 	int rleCount;
3467 
3468 	void rleCommit() {
3469 		if(rleByte >= 26)
3470 			assert(0); // too many colors for us to handle
3471 		if(rleCount == 0)
3472 			goto finish;
3473 		if(rleCount == 1) {
3474 			s ~= rleByte + 'a';
3475 			goto finish;
3476 		}
3477 
3478 		import std.conv;
3479 		s ~= to!string(rleCount);
3480 		s ~= rleByte + 'a';
3481 
3482 		finish:
3483 			rleByte = 0;
3484 			rleCount = 0;
3485 	}
3486 
3487 	foreach(b; ii.data) {
3488 		if(b == rleByte)
3489 			rleCount++;
3490 		else {
3491 			rleCommit();
3492 			rleByte = b;
3493 			rleCount = 1;
3494 		}
3495 	}
3496 
3497 	rleCommit();
3498 
3499 	return s;
3500 }
3501 
3502 IndexedImage readSmallTextImage(scope const(char)[] arg) {
3503 	auto origArg = arg;
3504 	int width;
3505 	int height;
3506 
3507 	int readNumeric(char c) {
3508 		if(c >= '0' && c <= '9')
3509 			return c - '0';
3510 		if(c >= 'a' && c <= 'z')
3511 			return c - 'a' + 10;
3512 		return 0;
3513 	}
3514 
3515 	if(arg.length > 2) {
3516 		width = readNumeric(arg[0]);
3517 		height = readNumeric(arg[1]);
3518 		arg = arg[2 .. $];
3519 	}
3520 
3521 	import std.conv;
3522 	assert(width == 16, to!string(width));
3523 	assert(height == 16, to!string(width));
3524 
3525 	Color[] palette;
3526 	ubyte[256] data;
3527 	int didx = 0;
3528 	bool readingPalette = true;
3529 	outer: while(arg.length) {
3530 		if(readingPalette) {
3531 			if(arg[0] == 'Z') {
3532 				readingPalette = false;
3533 				arg = arg[1 .. $];
3534 				continue;
3535 			}
3536 			if(arg.length < 8)
3537 				break;
3538 			foreach(a; arg[0..8]) {
3539 				// if not strict hex, forget it
3540 				if(!((a >= '0' && a <= '9') || (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z')))
3541 					break outer;
3542 			}
3543 			palette ~= Color.fromString(arg[0 .. 8]);
3544 			arg = arg[8 .. $];
3545 		} else {
3546 			char[3] rleChars;
3547 			int rlePos;
3548 			while(arg.length && arg[0] >= '0' && arg[0] <= '9') {
3549 				rleChars[rlePos] = arg[0];
3550 				arg = arg[1 .. $];
3551 				rlePos++;
3552 				if(rlePos >= rleChars.length)
3553 					break;
3554 			}
3555 			if(arg.length == 0)
3556 				break;
3557 
3558 			int rle;
3559 			if(rlePos == 0)
3560 				rle = 1;
3561 			else {
3562 				// 100
3563 				// rleChars[0] == '1'
3564 				foreach(c; rleChars[0 .. rlePos]) {
3565 					rle *= 10;
3566 					rle += c - '0';
3567 				}
3568 			}
3569 
3570 			foreach(i; 0 .. rle) {
3571 				if(arg[0] >= 'a' && arg[0] <= 'z')
3572 					data[didx] = cast(ubyte)(arg[0] - 'a');
3573 
3574 				didx++;
3575 				if(didx == data.length)
3576 					break outer;
3577 			}
3578 
3579 			arg = arg[1 .. $];
3580 		}
3581 	}
3582 
3583 	// width, height, palette, data is set up now
3584 
3585 	if(palette.length) {
3586 		auto ii = new IndexedImage(width, height);
3587 		ii.palette = palette;
3588 		ii.data = data.dup;
3589 
3590 		return ii;
3591 	}// else assert(0, origArg);
3592 	return null;
3593 }
3594 
3595 
3596 // workaround dmd bug fixed in next release
3597 //static immutable Color[256] xtermPalette = [
3598 immutable(Color)[] xtermPalette() {
3599 
3600 	// This is an approximation too for a few entries, but a very close one.
3601 	Color xtermPaletteIndexToColor(int paletteIdx) {
3602 		Color color;
3603 		color.a = 255;
3604 
3605 		if(paletteIdx < 16) {
3606 			if(paletteIdx == 7)
3607 				return Color(229, 229, 229); // real is 0xc0 but i think this is easier to see
3608 			else if(paletteIdx == 8)
3609 				return Color(0x80, 0x80, 0x80);
3610 
3611 			// real xterm uses 0x88 here, but I prefer 0xcd because it is easier for me to see
3612 			color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
3613 			color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
3614 			color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0xcd) : 0x00;
3615 
3616 		} else if(paletteIdx < 232) {
3617 			// color ramp, 6x6x6 cube
3618 			color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55);
3619 			color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55);
3620 			color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55);
3621 
3622 			if(color.r == 55) color.r = 0;
3623 			if(color.g == 55) color.g = 0;
3624 			if(color.b == 55) color.b = 0;
3625 		} else {
3626 			// greyscale ramp, from 0x8 to 0xee
3627 			color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10);
3628 			color.g = color.r;
3629 			color.b = color.g;
3630 		}
3631 
3632 		return color;
3633 	}
3634 
3635 	static immutable(Color)[] ret;
3636 	if(ret.length == 256)
3637 		return ret;
3638 
3639 	ret.reserve(256);
3640 	foreach(i; 0 .. 256)
3641 		ret ~= xtermPaletteIndexToColor(i);
3642 
3643 	return ret;
3644 }
3645 
3646 static shared immutable dchar[dchar] lineDrawingCharacterSet;
3647 shared static this() {
3648 	lineDrawingCharacterSet = [
3649 		'a' : ':',
3650 		'j' : '+',
3651 		'k' : '+',
3652 		'l' : '+',
3653 		'm' : '+',
3654 		'n' : '+',
3655 		'q' : '-',
3656 		't' : '+',
3657 		'u' : '+',
3658 		'v' : '+',
3659 		'w' : '+',
3660 		'x' : '|',
3661 	];
3662 
3663 	// this is what they SHOULD be but the font i use doesn't support all these
3664 	// the ascii fallback above looks pretty good anyway though.
3665 	version(none)
3666 	lineDrawingCharacterSet = [
3667 		'a' : '\u2592',
3668 		'j' : '\u2518',
3669 		'k' : '\u2510',
3670 		'l' : '\u250c',
3671 		'm' : '\u2514',
3672 		'n' : '\u253c',
3673 		'q' : '\u2500',
3674 		't' : '\u251c',
3675 		'u' : '\u2524',
3676 		'v' : '\u2534',
3677 		'w' : '\u252c',
3678 		'x' : '\u2502',
3679 	];
3680 }