The OpenD Programming Language

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