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 version(tew_main)
10 unittest {
11 	import arsd.minigui;
12 	import arsd.minigui_addons.terminal_emulator_widget;
13 
14 	// 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.")
15 
16 	void main() {
17 		auto window = new MainWindow("Minigui Terminal Emulation");
18 		version(Posix)
19 			auto tew = new TerminalEmulatorWidget(["/bin/bash"], window);
20 		else version(Windows)
21 			auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
22 		window.loop();
23 	}
24 
25 	main();
26 }
27 
28 import arsd.minigui;
29 
30 import arsd.terminalemulator;
31 
32 class TerminalEmulatorWidget : Widget {
33 	this(Widget parent) {
34 		terminalEmulator = new TerminalEmulatorInsideWidget(this);
35 		super(parent);
36 	}
37 
38 	this(string[] args, Widget parent) {
39 		version(Windows) {
40 			import core.sys.windows.windows : HANDLE;
41 			void startup(HANDLE inwritePipe, HANDLE outreadPipe) {
42 				terminalEmulator = new TerminalEmulatorInsideWidget(inwritePipe, outreadPipe, this);
43 			}
44 
45 			import std.string;
46 			startChild!startup(args[0], args.join(" "));
47 		}
48 		else version(Posix) {
49 			void startup(int master) {
50 				int fd = master;
51 				import fcntl = core.sys.posix.fcntl;
52 				auto flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0);
53 				if(flags == -1)
54 					throw new Exception("fcntl get");
55 				flags |= fcntl.O_NONBLOCK;
56 				auto s = fcntl.fcntl(fd, fcntl.F_SETFL, flags);
57 				if(s == -1)
58 					throw new Exception("fcntl set");
59 
60 				terminalEmulator = new TerminalEmulatorInsideWidget(master, this);
61 			}
62 
63 			import std.process;
64 			auto cmd = environment.get("SHELL", "/bin/bash");
65 			startChild!startup(args[0], args);
66 		}
67 
68 		super(parent);
69 	}
70 
71 	TerminalEmulatorInsideWidget terminalEmulator;
72 
73 	override void registerMovement() {
74 		super.registerMovement();
75 		terminalEmulator.resized(width, height);
76 	}
77 
78 	override void focus() {
79 		super.focus();
80 		terminalEmulator.attentionReceived();
81 	}
82 
83 	override MouseCursor cursor() { return GenericCursor.Text; }
84 
85 	override void paint(WidgetPainter painter) {
86 		terminalEmulator.redrawPainter(painter, true);
87 	}
88 }
89 
90 
91 class TerminalEmulatorInsideWidget : TerminalEmulator {
92 
93 	void resized(int w, int h) {
94 		this.resizeTerminal(w / fontWidth, h / fontHeight);
95 		clearScreenRequested = true;
96 		redraw();
97 	}
98 
99 
100 	protected override void changeCursorStyle(CursorStyle s) { }
101 
102 	protected override void changeWindowTitle(string t) {
103 		//if(window && t.length)
104 			//window.title = t;
105 	}
106 	protected override void changeWindowIcon(IndexedImage t) {
107 		//if(window && t)
108 			//window.icon = t;
109 	}
110 	protected override void changeIconTitle(string) {}
111 	protected override void changeTextAttributes(TextAttributes) {}
112 	protected override void soundBell() {
113 		static if(UsingSimpledisplayX11)
114 			XBell(XDisplayConnection.get(), 50);
115 	}
116 
117 	protected override void demandAttention() {
118 		//window.requestAttention();
119 	}
120 
121 	protected override void copyToClipboard(string text) {
122 		setClipboardText(widget.parentWindow.win, text);
123 	}
124 
125 	protected override void pasteFromClipboard(void delegate(in char[]) dg) {
126 		static if(UsingSimpledisplayX11)
127 			getPrimarySelection(widget.parentWindow.win, dg);
128 		else
129 			getClipboardText(widget.parentWindow.win, (in char[] dataIn) {
130 				char[] data;
131 				// change Windows \r\n to plain \n
132 				foreach(char ch; dataIn)
133 					if(ch != 13)
134 						data ~= ch;
135 				dg(data);
136 			});
137 	}
138 
139 	protected override void copyToPrimary(string text) {
140 		static if(UsingSimpledisplayX11)
141 			setPrimarySelection(widget.parentWindow.win, text);
142 		else
143 			{}
144 	}
145 	protected override void pasteFromPrimary(void delegate(in char[]) dg) {
146 		static if(UsingSimpledisplayX11)
147 			getPrimarySelection(widget.parentWindow.win, dg);
148 	}
149 
150 	override void requestExit() {
151 		// FIXME
152 	}
153 
154 
155 
156 	void resizeImage() { }
157 	mixin PtySupport!(resizeImage);
158 
159 	version(Posix)
160 		this(int masterfd, TerminalEmulatorWidget widget) {
161 			master = masterfd;
162 			this(widget);
163 		}
164 	else version(Windows) {
165 		import core.sys.windows.windows;
166 		this(HANDLE stdin, HANDLE stdout, TerminalEmulatorWidget widget) {
167 			this.stdin = stdin;
168 			this.stdout = stdout;
169 			this(widget);
170 		}
171 	}
172 
173 	bool focused;
174 
175 	TerminalEmulatorWidget widget;
176 
177 	mixin SdpyDraw;
178 
179 	private this(TerminalEmulatorWidget widget) {
180 
181 		this.widget = widget;
182 
183 		fontSize = 14;
184 		loadDefaultFont();
185 
186 		auto desiredWidth = 80;
187 		auto desiredHeight = 24;
188 
189 		super(desiredWidth, desiredHeight);
190 
191 		bool skipNextChar = false;
192 
193 		widget.addEventListener("mousedown", (Event ev) {
194 			int termX = (ev.clientX - paddingLeft) / fontWidth;
195 			int termY = (ev.clientY - paddingTop) / fontHeight;
196 
197 			if(sendMouseInputToApplication(termX, termY,
198 				arsd.terminalemulator.MouseEventType.buttonPressed,
199 				cast(arsd.terminalemulator.MouseButton) ev.button,
200 				(ev.state & ModifierState.shift) ? true : false,
201 				(ev.state & ModifierState.ctrl) ? true : false,
202 				(ev.state & ModifierState.alt) ? true : false
203 			))
204 				redraw();
205 		});
206 
207 		widget.addEventListener("mouseup", (Event ev) {
208 			int termX = (ev.clientX - paddingLeft) / fontWidth;
209 			int termY = (ev.clientY - paddingTop) / fontHeight;
210 
211 			if(sendMouseInputToApplication(termX, termY,
212 				arsd.terminalemulator.MouseEventType.buttonReleased,
213 				cast(arsd.terminalemulator.MouseButton) ev.button,
214 				(ev.state & ModifierState.shift) ? true : false,
215 				(ev.state & ModifierState.ctrl) ? true : false,
216 				(ev.state & ModifierState.alt) ? true : false
217 			))
218 				redraw();
219 		});
220 
221 		widget.addEventListener("mousemove", (Event ev) {
222 			int termX = (ev.clientX - paddingLeft) / fontWidth;
223 			int termY = (ev.clientY - paddingTop) / fontHeight;
224 
225 			if(sendMouseInputToApplication(termX, termY,
226 				arsd.terminalemulator.MouseEventType.motion,
227 				cast(arsd.terminalemulator.MouseButton) ev.button,
228 				(ev.state & ModifierState.shift) ? true : false,
229 				(ev.state & ModifierState.ctrl) ? true : false,
230 				(ev.state & ModifierState.alt) ? true : false
231 			))
232 				redraw();
233 		});
234 
235 		widget.addEventListener("keydown", (Event ev) {
236 			if(ev.key == Key.ScrollLock) {
237 				toggleScrollbackWrap();
238 			}
239 
240 			string magic() {
241 				string code;
242 				foreach(member; __traits(allMembers, TerminalKey))
243 					if(member != "Escape")
244 						code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
245 							, (ev.state & ModifierState.shift)?true:false
246 							, (ev.state & ModifierState.alt)?true:false
247 							, (ev.state & ModifierState.ctrl)?true:false
248 							, (ev.state & ModifierState.windows)?true:false
249 						)) redraw(); break;";
250 				return code;
251 			}
252 
253 
254 			switch(ev.key) {
255 				//// I want the escape key to send twice to differentiate it from
256 				//// other escape sequences easily.
257 				//case Key.Escape: sendToApplication("\033"); break;
258 
259 				mixin(magic());
260 
261 				default:
262 					// keep going, not special
263 			}
264 
265 			// remapping of alt+key is possible too, at least on linux.
266 			/+
267 			static if(UsingSimpledisplayX11)
268 			if(ev.state & ModifierState.alt) {
269 				if(ev.character in altMappings) {
270 					sendToApplication(altMappings[ev.character]);
271 					skipNextChar = true;
272 				}
273 			}
274 			+/
275 
276 			return; // the character event handler will do others
277 		});
278 
279 		widget.addEventListener("char", (Event ev) {
280 			dchar c = ev.character;
281 			if(skipNextChar) {
282 				skipNextChar = false;
283 				return;
284 			}
285 
286 			endScrollback();
287 			char[4] str;
288 			import std.utf;
289 			if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
290 			auto data = str[0 .. encode(str, c)];
291 
292 			// 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.
293 			if(c != 127)
294 				sendToApplication(data);
295 		});
296 
297 		version(Posix) {
298 			auto cls = new PosixFdReader(&readyToRead, master);
299 		} else 
300 		version(Windows) {
301 			overlapped = new OVERLAPPED();
302 			overlapped.hEvent = cast(void*) this;
303 
304 			//window.handleNativeEvent = &windowsRead;
305 			readyToReadWindows(0, 0, overlapped);
306 			redraw();
307 		}
308 	}
309 
310 	static int fontSize = 14;
311 
312 	bool clearScreenRequested = true;
313 	void redraw(bool forceRedraw = false) {
314 		if(widget.parentWindow is null || widget.parentWindow.win is null)
315 			return;
316 		auto painter = widget.draw();
317 		if(clearScreenRequested) {
318 			auto clearColor = defaultBackground;
319 			painter.outlineColor = clearColor;
320 			painter.fillColor = clearColor;
321 			painter.drawRectangle(Point(0, 0), widget.width, widget.height);
322 			clearScreenRequested = false;
323 			forceRedraw = true;
324 		}
325 
326 		redrawPainter(painter, forceRedraw);
327 	}
328 
329 	bool debugMode = false;
330 }