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