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 }