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 }