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 }