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