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