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 }