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