1 /++
2 	Creates a UNIX terminal emulator, nested in a minigui widget.
3 
4 	Depends on my terminalemulator.d core. Get it here:
5 	https://github.com/adamdruppe/terminal-emulator/blob/master/terminalemulator.d
6 +/
7 module arsd.minigui_addons.terminal_emulator_widget;
8 ///
9 unittest {
10 	import arsd.minigui;
11 	import arsd.minigui_addons.terminal_emulator_widget;
12 
13 	// version(linux) {} else static assert(0, "Terminal emulation kinda works on other platforms (it runs on Windows, but has no compatible shell program to run there!), but it is actually useful on Linux.")
14 
15 	void main() {
16 		auto window = new MainWindow("Minigui Terminal Emulation");
17 		version(Posix)
18 			auto tew = new TerminalEmulatorWidget(["/bin/bash"], window);
19 		else version(Windows)
20 			auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
21 		window.loop();
22 	}
23 }
24 
25 import arsd.minigui;
26 
27 import arsd.terminalemulator;
28 
29 class TerminalEmulatorWidget : Widget {
30 	this(string[] args, Widget parent) {
31 		version(Windows) {
32 			import core.sys.windows.windows : HANDLE;
33 			void startup(HANDLE inwritePipe, HANDLE outreadPipe) {
34 				terminalEmulator = new TerminalEmulatorInsideWidget(inwritePipe, outreadPipe, this);
35 			}
36 
37 			import std.string;
38 			startChild!startup(args[0], args.join(" "));
39 		}
40 		else version(Posix) {
41 			void startup(int master) {
42 				int fd = master;
43 				import fcntl = core.sys.posix.fcntl;
44 				auto flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0);
45 				if(flags == -1)
46 					throw new Exception("fcntl get");
47 				flags |= fcntl.O_NONBLOCK;
48 				auto s = fcntl.fcntl(fd, fcntl.F_SETFL, flags);
49 				if(s == -1)
50 					throw new Exception("fcntl set");
51 
52 				terminalEmulator = new TerminalEmulatorInsideWidget(master, this);
53 			}
54 
55 			import std.process;
56 			auto cmd = environment.get("SHELL", "/bin/bash");
57 			startChild!startup(args[0], args);
58 		}
59 
60 		super(parent);
61 	}
62 
63 	TerminalEmulatorInsideWidget terminalEmulator;
64 
65 	override void registerMovement() {
66 		super.registerMovement();
67 		terminalEmulator.resized(width, height);
68 	}
69 
70 	override void focus() {
71 		super.focus();
72 		terminalEmulator.attentionReceived();
73 	}
74 
75 	override MouseCursor cursor() { return GenericCursor.Text; }
76 
77 	override void paint(ScreenPainter painter) {
78 		terminalEmulator.redrawPainter(painter, true);
79 	}
80 }
81 
82 
83 class TerminalEmulatorInsideWidget : TerminalEmulator {
84 
85 	void resized(int w, int h) {
86 		this.resizeTerminal(w / fontWidth, h / fontHeight);
87 		clearScreenRequested = true;
88 		redraw();
89 	}
90 
91 
92 	protected override void changeCursorStyle(CursorStyle s) { }
93 
94 	protected override void changeWindowTitle(string t) {
95 		//if(window && t.length)
96 			//window.title = t;
97 	}
98 	protected override void changeWindowIcon(IndexedImage t) {
99 		//if(window && t)
100 			//window.icon = t;
101 	}
102 	protected override void changeIconTitle(string) {}
103 	protected override void changeTextAttributes(TextAttributes) {}
104 	protected override void soundBell() {
105 		static if(UsingSimpledisplayX11)
106 			XBell(XDisplayConnection.get(), 50);
107 	}
108 
109 	protected override void demandAttention() {
110 		//window.requestAttention();
111 	}
112 
113 	protected override void copyToClipboard(string text) {
114 		static if(UsingSimpledisplayX11)
115 			setPrimarySelection(widget.parentWindow.win, text);
116 		else
117 			setClipboardText(widget.parentWindow.win, text);
118 	}
119 
120 	protected override void pasteFromClipboard(void delegate(in char[]) dg) {
121 		static if(UsingSimpledisplayX11)
122 			getPrimarySelection(widget.parentWindow.win, dg);
123 		else
124 			getClipboardText(widget.parentWindow.win, (in char[] dataIn) {
125 				char[] data;
126 				// change Windows \r\n to plain \n
127 				foreach(char ch; dataIn)
128 					if(ch != 13)
129 						data ~= ch;
130 				dg(data);
131 			});
132 	}
133 
134 	void resizeImage() { }
135 	mixin PtySupport!(resizeImage);
136 
137 	version(Posix)
138 		this(int masterfd, TerminalEmulatorWidget widget) {
139 			master = masterfd;
140 			this(widget);
141 		}
142 	else version(Windows) {
143 		import core.sys.windows.windows;
144 		this(HANDLE stdin, HANDLE stdout, TerminalEmulatorWidget widget) {
145 			this.stdin = stdin;
146 			this.stdout = stdout;
147 			this(widget);
148 		}
149 	}
150 
151 	bool focused;
152 
153 	TerminalEmulatorWidget widget;
154 	OperatingSystemFont font;
155 
156 	private this(TerminalEmulatorWidget widget) {
157 
158 		this.widget = widget;
159 
160 		static if(UsingSimpledisplayX11) {
161 			// FIXME: survive reconnects?
162 			fontSize = 14;
163 			font = new OperatingSystemFont("fixed", fontSize, FontWeight.medium);
164 			if(font.isNull) {
165 				// didn't work, it is using a
166 				// fallback, prolly fixed-13
167 				import std.stdio; writeln("font failed");
168 				fontWidth = 6;
169 				fontHeight = 13;
170 			} else {
171 				fontWidth = fontSize / 2;
172 				fontHeight = fontSize;
173 			}
174 		} else version(Windows) {
175 			font = new OperatingSystemFont("Courier New", fontSize, FontWeight.medium);
176 			fontHeight = fontSize;
177 			fontWidth = fontSize / 2;
178 		}
179 
180 		auto desiredWidth = 80;
181 		auto desiredHeight = 24;
182 
183 		super(desiredWidth, desiredHeight);
184 
185 		bool skipNextChar = false;
186 
187 		widget.addEventListener("mousedown", (Event ev) {
188 			int termX = (ev.clientX - paddingLeft) / fontWidth;
189 			int termY = (ev.clientY - paddingTop) / fontHeight;
190 
191 			if(sendMouseInputToApplication(termX, termY,
192 				arsd.terminalemulator.MouseEventType.buttonPressed,
193 				cast(arsd.terminalemulator.MouseButton) ev.button,
194 				(ev.state & ModifierState.shift) ? true : false,
195 				(ev.state & ModifierState.ctrl) ? true : false
196 			))
197 				redraw();
198 		});
199 
200 		widget.addEventListener("mouseup", (Event ev) {
201 			int termX = (ev.clientX - paddingLeft) / fontWidth;
202 			int termY = (ev.clientY - paddingTop) / fontHeight;
203 
204 			if(sendMouseInputToApplication(termX, termY,
205 				arsd.terminalemulator.MouseEventType.buttonReleased,
206 				cast(arsd.terminalemulator.MouseButton) ev.button,
207 				(ev.state & ModifierState.shift) ? true : false,
208 				(ev.state & ModifierState.ctrl) ? true : false
209 			))
210 				redraw();
211 		});
212 
213 		widget.addEventListener("mousemove", (Event ev) {
214 			int termX = (ev.clientX - paddingLeft) / fontWidth;
215 			int termY = (ev.clientY - paddingTop) / fontHeight;
216 
217 			if(sendMouseInputToApplication(termX, termY,
218 				arsd.terminalemulator.MouseEventType.motion,
219 				cast(arsd.terminalemulator.MouseButton) ev.button,
220 				(ev.state & ModifierState.shift) ? true : false,
221 				(ev.state & ModifierState.ctrl) ? true : false
222 			))
223 				redraw();
224 		});
225 
226 		widget.addEventListener("keydown", (Event ev) {
227 			if(ev.key == Key.ScrollLock) {
228 				toggleScrollbackWrap();
229 			}
230 
231 			string magic() {
232 				string code;
233 				foreach(member; __traits(allMembers, TerminalKey))
234 					if(member != "Escape")
235 						code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
236 							, (ev.state & ModifierState.shift)?true:false
237 							, (ev.state & ModifierState.alt)?true:false
238 							, (ev.state & ModifierState.ctrl)?true:false
239 							, (ev.state & ModifierState.windows)?true:false
240 						)) redraw(); break;";
241 				return code;
242 			}
243 
244 
245 			switch(ev.key) {
246 				//// I want the escape key to send twice to differentiate it from
247 				//// other escape sequences easily.
248 				//case Key.Escape: sendToApplication("\033"); break;
249 
250 				mixin(magic());
251 
252 				default:
253 					// keep going, not special
254 			}
255 
256 			// remapping of alt+key is possible too, at least on linux.
257 			/+
258 			static if(UsingSimpledisplayX11)
259 			if(ev.state & ModifierState.alt) {
260 				if(ev.character in altMappings) {
261 					sendToApplication(altMappings[ev.character]);
262 					skipNextChar = true;
263 				}
264 			}
265 			+/
266 
267 			return; // the character event handler will do others
268 		});
269 
270 		widget.addEventListener("char", (Event ev) {
271 			dchar c = ev.character;
272 			if(skipNextChar) {
273 				skipNextChar = false;
274 				return;
275 			}
276 
277 			endScrollback();
278 			char[4] str;
279 			import std.utf;
280 			if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
281 			auto data = str[0 .. encode(str, c)];
282 
283 			// on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler.
284 			if(c != 127)
285 				sendToApplication(data);
286 		});
287 
288 		version(Posix) {
289 			auto cls = new PosixFdReader(&readyToRead, master);
290 		} else 
291 		version(Windows) {
292 			overlapped = new OVERLAPPED();
293 			overlapped.hEvent = cast(void*) this;
294 
295 			//window.handleNativeEvent = &windowsRead;
296 			readyToReadWindows(0, 0, overlapped);
297 			redraw();
298 		}
299 	}
300 
301 	int fontWidth;
302 	int fontHeight;
303 
304 	static int fontSize = 14;
305 
306 	enum paddingLeft = 2;
307 	enum paddingTop = 1;
308 
309 	bool clearScreenRequested = true;
310 	void redraw(bool forceRedraw = false) {
311 		if(widget.parentWindow is null || widget.parentWindow.win is null)
312 			return;
313 		auto painter = widget.draw();
314 		if(clearScreenRequested) {
315 			auto clearColor = defaultTextAttributes.background;
316 			painter.outlineColor = clearColor;
317 			painter.fillColor = clearColor;
318 			painter.drawRectangle(Point(0, 0), widget.width, widget.height);
319 			clearScreenRequested = false;
320 			forceRedraw = true;
321 		}
322 
323 		redrawPainter(painter, forceRedraw);
324 	}
325 
326 	bool lastDrawAlternativeScreen;
327 	final arsd.color.Rectangle redrawPainter(T)(T painter, bool forceRedraw) {
328 		arsd.color.Rectangle invalidated;
329 
330 		// FIXME: could prolly use optimizations
331 
332 		painter.setFont(font);
333 
334 		int posx = paddingLeft;
335 		int posy = paddingTop;
336 
337 
338 		char[512] bufferText;
339 		bool hasBufferedInfo;
340 		int bufferTextLength;
341 		Color bufferForeground;
342 		Color bufferBackground;
343 		int bufferX = -1;
344 		int bufferY = -1;
345 		bool bufferReverse;
346 		void flushBuffer() {
347 			if(!hasBufferedInfo) {
348 				return;
349 			}
350 
351 			assert(posx - bufferX - 1 > 0);
352 
353 			painter.fillColor = bufferReverse ? bufferForeground : bufferBackground;
354 			painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground;
355 
356 			painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight);
357 			painter.fillColor = Color.transparent;
358 			// Hack for contrast!
359 			if(bufferBackground == Color.black && !bufferReverse) {
360 				// brighter than normal in some cases so i can read it easily
361 				painter.outlineColor = contrastify(bufferForeground);
362 			} else if(bufferBackground == Color.white && !bufferReverse) {
363 				// darker than normal so i can read it
364 				painter.outlineColor = antiContrastify(bufferForeground);
365 			} else if(bufferForeground == bufferBackground) {
366 				// color on itself, I want it visible too
367 				auto hsl = toHsl(bufferForeground, true);
368 				if(hsl[2] < 0.5)
369 					hsl[2] += 0.5;
370 				else
371 					hsl[2] -= 0.5;
372 				painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]);
373 
374 			} else {
375 				// normal
376 				painter.outlineColor = bufferReverse ? bufferBackground : bufferForeground;
377 			}
378 
379 			// FIXME: make sure this clips correctly
380 			painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]);
381 
382 			hasBufferedInfo = false;
383 
384 			bufferReverse = false;
385 			bufferTextLength = 0;
386 			bufferX = -1;
387 			bufferY = -1;
388 		}
389 
390 
391 
392 		int x;
393 		foreach(idx, ref cell; alternateScreenActive ? alternateScreen : normalScreen) {
394 			if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) {
395 				flushBuffer();
396 				goto skipDrawing;
397 			}
398 			cell.invalidated = false;
399 			version(none) if(bufferX == -1) { // why was this ever here?
400 				bufferX = posx;
401 				bufferY = posy;
402 			}
403 
404 			{
405 
406 				invalidated.left = posx < invalidated.left ? posx : invalidated.left;
407 				invalidated.top = posy < invalidated.top ? posy : invalidated.top;
408 				int xmax = posx + fontWidth;
409 				int ymax = posy + fontHeight;
410 				invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
411 				invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
412 
413 				// FIXME: this could be more efficient, simpledisplay could get better graphics context handling
414 				{
415 
416 					bool reverse = (cell.attributes.inverse != reverseVideo);
417 					if(cell.selected)
418 						reverse = !reverse;
419 
420 					auto fgc = cell.attributes.foreground;
421 					auto bgc = cell.attributes.background;
422 
423 					if(!(cell.attributes.foregroundIndex & 0xff00)) {
424 						// this refers to a specific palette entry, which may change, so we should use that
425 						fgc = palette[cell.attributes.foregroundIndex];
426 					}
427 					if(!(cell.attributes.backgroundIndex & 0xff00)) {
428 						// this refers to a specific palette entry, which may change, so we should use that
429 						bgc = palette[cell.attributes.backgroundIndex];
430 					}
431 
432 					if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse)
433 						flushBuffer();
434 					bufferReverse = reverse;
435 					bufferBackground = bgc;
436 					bufferForeground = fgc;
437 				}
438 			}
439 
440 				if(cell.ch != dchar.init) {
441 					char[4] str;
442 					import std.utf;
443 					// now that it is buffered, we do want to draw it this way...
444 					//if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing
445 						try {
446 							auto stride = encode(str, cell.ch);
447 							if(bufferTextLength + stride > bufferText.length)
448 								flushBuffer();
449 							bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride];
450 							bufferTextLength += stride;
451 
452 							if(bufferX == -1) {
453 								bufferX = posx;
454 								bufferY = posy;
455 							}
456 							hasBufferedInfo = true;
457 						} catch(Exception e) {
458 							import std.stdio;
459 							writeln(cast(uint) cell.ch, " :: ", e.msg);
460 						}
461 					//}
462 				} else if(cell.nonCharacterData !is null) {
463 				}
464 
465 				if(cell.attributes.underlined) {
466 					// the posx adjustment is because the buffer assumes it is going
467 					// to be flushed after advancing, but here, we're doing it mid-character
468 					// FIXME: we should just underline the whole thing consecutively, with the buffer
469 					posx += fontWidth;
470 					flushBuffer();
471 					posx -= fontWidth;
472 					painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1));
473 				}
474 			skipDrawing:
475 
476 				posx += fontWidth;
477 			x++;
478 			if(x == screenWidth) {
479 				flushBuffer();
480 				x = 0;
481 				posy += fontHeight;
482 				posx = paddingLeft;
483 			}
484 		}
485 
486 		if(cursorShowing) {
487 			painter.fillColor = cursorColor;
488 			painter.outlineColor = cursorColor;
489 			painter.rasterOp = RasterOp.xor;
490 
491 			posx = cursorPosition.x * fontWidth + paddingLeft;
492 			posy = cursorPosition.y * fontHeight + paddingTop;
493 
494 			int cursorWidth = fontWidth;
495 			int cursorHeight = fontHeight;
496 
497 			final switch(cursorStyle) {
498 				case CursorStyle.block:
499 					painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight);
500 				break;
501 				case CursorStyle.underline:
502 					painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2);
503 				break;
504 				case CursorStyle.bar:
505 					painter.drawRectangle(Point(posx, posy), 2, cursorHeight);
506 				break;
507 			}
508 			painter.rasterOp = RasterOp.normal;
509 
510 			// since the cursor draws over the cell, we need to make sure it is redrawn each time too
511 			auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen);
512 			if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) {
513 				(*buffer)[cursorY * screenWidth + cursorX].invalidated = true;
514 			}
515 
516 			invalidated.left = posx < invalidated.left ? posx : invalidated.left;
517 			invalidated.top = posy < invalidated.top ? posy : invalidated.top;
518 			int xmax = posx + fontWidth;
519 			int ymax = xmax + fontHeight;
520 			invalidated.right = xmax > invalidated.right ? xmax : invalidated.right;
521 			invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom;
522 		}
523 
524 		lastDrawAlternativeScreen = alternateScreenActive;
525 
526 		return invalidated;
527 	}
528 
529 
530 	// black bg, make the colors more visible
531 	Color contrastify(Color c) {
532 		if(c == Color(0xcd, 0, 0))
533 			return Color.fromHsl(0, 1.0, 0.75);
534 		else if(c == Color(0, 0, 0xcd))
535 			return Color.fromHsl(240, 1.0, 0.75);
536 		else if(c == Color(229, 229, 229))
537 			return Color(0x99, 0x99, 0x99);
538 		else return c;
539 	}
540 
541 	// white bg, make them more visible
542 	Color antiContrastify(Color c) {
543 		if(c == Color(0xcd, 0xcd, 0))
544 			return Color.fromHsl(60, 1.0, 0.25);
545 		else if(c == Color(0, 0xcd, 0xcd))
546 			return Color.fromHsl(180, 1.0, 0.25);
547 		else if(c == Color(229, 229, 229))
548 			return Color(0x99, 0x99, 0x99);
549 		else return c;
550 	}
551 
552 	bool debugMode = false;
553 }