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 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 25 import arsd.minigui; 26 27 import arsd.terminalemulator; 28 29 class TerminalEmulatorWidget : Widget { 30 this(string[] args, Widget parent) { 31 version(Windows) { 32 import core.sys.windows.windows : HANDLE; 33 void startup(HANDLE inwritePipe, HANDLE outreadPipe) { 34 terminalEmulator = new TerminalEmulatorInsideWidget(inwritePipe, outreadPipe, this); 35 } 36 37 import std.string; 38 startChild!startup(args[0], args.join(" ")); 39 } 40 else version(Posix) { 41 void startup(int master) { 42 int fd = master; 43 import fcntl = core.sys.posix.fcntl; 44 auto flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0); 45 if(flags == -1) 46 throw new Exception("fcntl get"); 47 flags |= fcntl.O_NONBLOCK; 48 auto s = fcntl.fcntl(fd, fcntl.F_SETFL, flags); 49 if(s == -1) 50 throw new Exception("fcntl set"); 51 52 terminalEmulator = new TerminalEmulatorInsideWidget(master, this); 53 } 54 55 import std.process; 56 auto cmd = environment.get("SHELL", "/bin/bash"); 57 startChild!startup(args[0], args); 58 } 59 60 super(parent); 61 } 62 63 TerminalEmulatorInsideWidget terminalEmulator; 64 65 override void registerMovement() { 66 super.registerMovement(); 67 terminalEmulator.resized(width, height); 68 } 69 70 override void focus() { 71 super.focus(); 72 terminalEmulator.attentionReceived(); 73 } 74 75 override MouseCursor cursor() { return GenericCursor.Text; } 76 77 override void paint(ScreenPainter painter) { 78 terminalEmulator.redrawPainter(painter, true); 79 } 80 } 81 82 83 class TerminalEmulatorInsideWidget : TerminalEmulator { 84 85 void resized(int w, int h) { 86 this.resizeTerminal(w / fontWidth, h / fontHeight); 87 clearScreenRequested = true; 88 redraw(); 89 } 90 91 92 protected override void changeCursorStyle(CursorStyle s) { } 93 94 protected override void changeWindowTitle(string t) { 95 //if(window && t.length) 96 //window.title = t; 97 } 98 protected override void changeWindowIcon(IndexedImage t) { 99 //if(window && t) 100 //window.icon = t; 101 } 102 protected override void changeIconTitle(string) {} 103 protected override void changeTextAttributes(TextAttributes) {} 104 protected override void soundBell() { 105 static if(UsingSimpledisplayX11) 106 XBell(XDisplayConnection.get(), 50); 107 } 108 109 protected override void demandAttention() { 110 //window.requestAttention(); 111 } 112 113 protected override void copyToClipboard(string text) { 114 static if(UsingSimpledisplayX11) 115 setPrimarySelection(widget.parentWindow.win, text); 116 else 117 setClipboardText(widget.parentWindow.win, text); 118 } 119 120 protected override void pasteFromClipboard(void delegate(in char[]) dg) { 121 static if(UsingSimpledisplayX11) 122 getPrimarySelection(widget.parentWindow.win, dg); 123 else 124 getClipboardText(widget.parentWindow.win, (in char[] dataIn) { 125 char[] data; 126 // change Windows \r\n to plain \n 127 foreach(char ch; dataIn) 128 if(ch != 13) 129 data ~= ch; 130 dg(data); 131 }); 132 } 133 134 void resizeImage() { } 135 mixin PtySupport!(resizeImage); 136 137 version(Posix) 138 this(int masterfd, TerminalEmulatorWidget widget) { 139 master = masterfd; 140 this(widget); 141 } 142 else version(Windows) { 143 import core.sys.windows.windows; 144 this(HANDLE stdin, HANDLE stdout, TerminalEmulatorWidget widget) { 145 this.stdin = stdin; 146 this.stdout = stdout; 147 this(widget); 148 } 149 } 150 151 bool focused; 152 153 TerminalEmulatorWidget widget; 154 OperatingSystemFont font; 155 156 private this(TerminalEmulatorWidget widget) { 157 158 this.widget = widget; 159 160 static if(UsingSimpledisplayX11) { 161 // FIXME: survive reconnects? 162 fontSize = 14; 163 font = new OperatingSystemFont("fixed", fontSize, FontWeight.medium); 164 if(font.isNull) { 165 // didn't work, it is using a 166 // fallback, prolly fixed-13 167 import std.stdio; writeln("font failed"); 168 fontWidth = 6; 169 fontHeight = 13; 170 } else { 171 fontWidth = fontSize / 2; 172 fontHeight = fontSize; 173 } 174 } else version(Windows) { 175 font = new OperatingSystemFont("Courier New", fontSize, FontWeight.medium); 176 fontHeight = fontSize; 177 fontWidth = fontSize / 2; 178 } 179 180 auto desiredWidth = 80; 181 auto desiredHeight = 24; 182 183 super(desiredWidth, desiredHeight); 184 185 bool skipNextChar = false; 186 187 widget.addEventListener("mousedown", (Event ev) { 188 int termX = (ev.clientX - paddingLeft) / fontWidth; 189 int termY = (ev.clientY - paddingTop) / fontHeight; 190 191 if(sendMouseInputToApplication(termX, termY, 192 arsd.terminalemulator.MouseEventType.buttonPressed, 193 cast(arsd.terminalemulator.MouseButton) ev.button, 194 (ev.state & ModifierState.shift) ? true : false, 195 (ev.state & ModifierState.ctrl) ? true : false 196 )) 197 redraw(); 198 }); 199 200 widget.addEventListener("mouseup", (Event ev) { 201 int termX = (ev.clientX - paddingLeft) / fontWidth; 202 int termY = (ev.clientY - paddingTop) / fontHeight; 203 204 if(sendMouseInputToApplication(termX, termY, 205 arsd.terminalemulator.MouseEventType.buttonReleased, 206 cast(arsd.terminalemulator.MouseButton) ev.button, 207 (ev.state & ModifierState.shift) ? true : false, 208 (ev.state & ModifierState.ctrl) ? true : false 209 )) 210 redraw(); 211 }); 212 213 widget.addEventListener("mousemove", (Event ev) { 214 int termX = (ev.clientX - paddingLeft) / fontWidth; 215 int termY = (ev.clientY - paddingTop) / fontHeight; 216 217 if(sendMouseInputToApplication(termX, termY, 218 arsd.terminalemulator.MouseEventType.motion, 219 cast(arsd.terminalemulator.MouseButton) ev.button, 220 (ev.state & ModifierState.shift) ? true : false, 221 (ev.state & ModifierState.ctrl) ? true : false 222 )) 223 redraw(); 224 }); 225 226 widget.addEventListener("keydown", (Event ev) { 227 if(ev.key == Key.ScrollLock) { 228 toggleScrollbackWrap(); 229 } 230 231 string magic() { 232 string code; 233 foreach(member; __traits(allMembers, TerminalKey)) 234 if(member != "Escape") 235 code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ " 236 , (ev.state & ModifierState.shift)?true:false 237 , (ev.state & ModifierState.alt)?true:false 238 , (ev.state & ModifierState.ctrl)?true:false 239 , (ev.state & ModifierState.windows)?true:false 240 )) redraw(); break;"; 241 return code; 242 } 243 244 245 switch(ev.key) { 246 //// I want the escape key to send twice to differentiate it from 247 //// other escape sequences easily. 248 //case Key.Escape: sendToApplication("\033"); break; 249 250 mixin(magic()); 251 252 default: 253 // keep going, not special 254 } 255 256 // remapping of alt+key is possible too, at least on linux. 257 /+ 258 static if(UsingSimpledisplayX11) 259 if(ev.state & ModifierState.alt) { 260 if(ev.character in altMappings) { 261 sendToApplication(altMappings[ev.character]); 262 skipNextChar = true; 263 } 264 } 265 +/ 266 267 return; // the character event handler will do others 268 }); 269 270 widget.addEventListener("char", (Event ev) { 271 dchar c = ev.character; 272 if(skipNextChar) { 273 skipNextChar = false; 274 return; 275 } 276 277 endScrollback(); 278 char[4] str; 279 import std.utf; 280 if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10 281 auto data = str[0 .. encode(str, c)]; 282 283 // 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. 284 if(c != 127) 285 sendToApplication(data); 286 }); 287 288 version(Posix) { 289 auto cls = new PosixFdReader(&readyToRead, master); 290 } else 291 version(Windows) { 292 overlapped = new OVERLAPPED(); 293 overlapped.hEvent = cast(void*) this; 294 295 //window.handleNativeEvent = &windowsRead; 296 readyToReadWindows(0, 0, overlapped); 297 redraw(); 298 } 299 } 300 301 int fontWidth; 302 int fontHeight; 303 304 static int fontSize = 14; 305 306 enum paddingLeft = 2; 307 enum paddingTop = 1; 308 309 bool clearScreenRequested = true; 310 void redraw(bool forceRedraw = false) { 311 if(widget.parentWindow is null || widget.parentWindow.win is null) 312 return; 313 auto painter = widget.draw(); 314 if(clearScreenRequested) { 315 auto clearColor = defaultTextAttributes.background; 316 painter.outlineColor = clearColor; 317 painter.fillColor = clearColor; 318 painter.drawRectangle(Point(0, 0), widget.width, widget.height); 319 clearScreenRequested = false; 320 forceRedraw = true; 321 } 322 323 redrawPainter(painter, forceRedraw); 324 } 325 326 bool lastDrawAlternativeScreen; 327 final arsd.color.Rectangle redrawPainter(T)(T painter, bool forceRedraw) { 328 arsd.color.Rectangle invalidated; 329 330 // FIXME: could prolly use optimizations 331 332 painter.setFont(font); 333 334 int posx = paddingLeft; 335 int posy = paddingTop; 336 337 338 char[512] bufferText; 339 bool hasBufferedInfo; 340 int bufferTextLength; 341 Color bufferForeground; 342 Color bufferBackground; 343 int bufferX = -1; 344 int bufferY = -1; 345 bool bufferReverse; 346 void flushBuffer() { 347 if(!hasBufferedInfo) { 348 return; 349 } 350 351 assert(posx - bufferX - 1 > 0); 352 353 painter.fillColor = bufferReverse ? bufferForeground : bufferBackground; 354 painter.outlineColor = bufferReverse ? bufferForeground : bufferBackground; 355 356 painter.drawRectangle(Point(bufferX, bufferY), posx - bufferX, fontHeight); 357 painter.fillColor = Color.transparent; 358 // Hack for contrast! 359 if(bufferBackground == Color.black && !bufferReverse) { 360 // brighter than normal in some cases so i can read it easily 361 painter.outlineColor = contrastify(bufferForeground); 362 } else if(bufferBackground == Color.white && !bufferReverse) { 363 // darker than normal so i can read it 364 painter.outlineColor = antiContrastify(bufferForeground); 365 } else if(bufferForeground == bufferBackground) { 366 // color on itself, I want it visible too 367 auto hsl = toHsl(bufferForeground, true); 368 if(hsl[2] < 0.5) 369 hsl[2] += 0.5; 370 else 371 hsl[2] -= 0.5; 372 painter.outlineColor = fromHsl(hsl[0], hsl[1], hsl[2]); 373 374 } else { 375 // normal 376 painter.outlineColor = bufferReverse ? bufferBackground : bufferForeground; 377 } 378 379 // FIXME: make sure this clips correctly 380 painter.drawText(Point(bufferX, bufferY), cast(immutable) bufferText[0 .. bufferTextLength]); 381 382 hasBufferedInfo = false; 383 384 bufferReverse = false; 385 bufferTextLength = 0; 386 bufferX = -1; 387 bufferY = -1; 388 } 389 390 391 392 int x; 393 foreach(idx, ref cell; alternateScreenActive ? alternateScreen : normalScreen) { 394 if(!forceRedraw && !cell.invalidated && lastDrawAlternativeScreen == alternateScreenActive) { 395 flushBuffer(); 396 goto skipDrawing; 397 } 398 cell.invalidated = false; 399 version(none) if(bufferX == -1) { // why was this ever here? 400 bufferX = posx; 401 bufferY = posy; 402 } 403 404 { 405 406 invalidated.left = posx < invalidated.left ? posx : invalidated.left; 407 invalidated.top = posy < invalidated.top ? posy : invalidated.top; 408 int xmax = posx + fontWidth; 409 int ymax = posy + fontHeight; 410 invalidated.right = xmax > invalidated.right ? xmax : invalidated.right; 411 invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom; 412 413 // FIXME: this could be more efficient, simpledisplay could get better graphics context handling 414 { 415 416 bool reverse = (cell.attributes.inverse != reverseVideo); 417 if(cell.selected) 418 reverse = !reverse; 419 420 auto fgc = cell.attributes.foreground; 421 auto bgc = cell.attributes.background; 422 423 if(!(cell.attributes.foregroundIndex & 0xff00)) { 424 // this refers to a specific palette entry, which may change, so we should use that 425 fgc = palette[cell.attributes.foregroundIndex]; 426 } 427 if(!(cell.attributes.backgroundIndex & 0xff00)) { 428 // this refers to a specific palette entry, which may change, so we should use that 429 bgc = palette[cell.attributes.backgroundIndex]; 430 } 431 432 if(fgc != bufferForeground || bgc != bufferBackground || reverse != bufferReverse) 433 flushBuffer(); 434 bufferReverse = reverse; 435 bufferBackground = bgc; 436 bufferForeground = fgc; 437 } 438 } 439 440 if(cell.ch != dchar.init) { 441 char[4] str; 442 import std.utf; 443 // now that it is buffered, we do want to draw it this way... 444 //if(cell.ch != ' ') { // no point wasting time drawing spaces, which are nothing; the bg rectangle already did the important thing 445 try { 446 auto stride = encode(str, cell.ch); 447 if(bufferTextLength + stride > bufferText.length) 448 flushBuffer(); 449 bufferText[bufferTextLength .. bufferTextLength + stride] = str[0 .. stride]; 450 bufferTextLength += stride; 451 452 if(bufferX == -1) { 453 bufferX = posx; 454 bufferY = posy; 455 } 456 hasBufferedInfo = true; 457 } catch(Exception e) { 458 import std.stdio; 459 writeln(cast(uint) cell.ch, " :: ", e.msg); 460 } 461 //} 462 } else if(cell.nonCharacterData !is null) { 463 } 464 465 if(cell.attributes.underlined) { 466 // the posx adjustment is because the buffer assumes it is going 467 // to be flushed after advancing, but here, we're doing it mid-character 468 // FIXME: we should just underline the whole thing consecutively, with the buffer 469 posx += fontWidth; 470 flushBuffer(); 471 posx -= fontWidth; 472 painter.drawLine(Point(posx, posy + fontHeight - 1), Point(posx + fontWidth, posy + fontHeight - 1)); 473 } 474 skipDrawing: 475 476 posx += fontWidth; 477 x++; 478 if(x == screenWidth) { 479 flushBuffer(); 480 x = 0; 481 posy += fontHeight; 482 posx = paddingLeft; 483 } 484 } 485 486 if(cursorShowing) { 487 painter.fillColor = cursorColor; 488 painter.outlineColor = cursorColor; 489 painter.rasterOp = RasterOp.xor; 490 491 posx = cursorPosition.x * fontWidth + paddingLeft; 492 posy = cursorPosition.y * fontHeight + paddingTop; 493 494 int cursorWidth = fontWidth; 495 int cursorHeight = fontHeight; 496 497 final switch(cursorStyle) { 498 case CursorStyle.block: 499 painter.drawRectangle(Point(posx, posy), cursorWidth, cursorHeight); 500 break; 501 case CursorStyle.underline: 502 painter.drawRectangle(Point(posx, posy + cursorHeight - 2), cursorWidth, 2); 503 break; 504 case CursorStyle.bar: 505 painter.drawRectangle(Point(posx, posy), 2, cursorHeight); 506 break; 507 } 508 painter.rasterOp = RasterOp.normal; 509 510 // since the cursor draws over the cell, we need to make sure it is redrawn each time too 511 auto buffer = alternateScreenActive ? (&alternateScreen) : (&normalScreen); 512 if(cursorX >= 0 && cursorY >= 0 && cursorY < screenHeight && cursorX < screenWidth) { 513 (*buffer)[cursorY * screenWidth + cursorX].invalidated = true; 514 } 515 516 invalidated.left = posx < invalidated.left ? posx : invalidated.left; 517 invalidated.top = posy < invalidated.top ? posy : invalidated.top; 518 int xmax = posx + fontWidth; 519 int ymax = xmax + fontHeight; 520 invalidated.right = xmax > invalidated.right ? xmax : invalidated.right; 521 invalidated.bottom = ymax > invalidated.bottom ? ymax : invalidated.bottom; 522 } 523 524 lastDrawAlternativeScreen = alternateScreenActive; 525 526 return invalidated; 527 } 528 529 530 // black bg, make the colors more visible 531 Color contrastify(Color c) { 532 if(c == Color(0xcd, 0, 0)) 533 return Color.fromHsl(0, 1.0, 0.75); 534 else if(c == Color(0, 0, 0xcd)) 535 return Color.fromHsl(240, 1.0, 0.75); 536 else if(c == Color(229, 229, 229)) 537 return Color(0x99, 0x99, 0x99); 538 else return c; 539 } 540 541 // white bg, make them more visible 542 Color antiContrastify(Color c) { 543 if(c == Color(0xcd, 0xcd, 0)) 544 return Color.fromHsl(60, 1.0, 0.25); 545 else if(c == Color(0, 0xcd, 0xcd)) 546 return Color.fromHsl(180, 1.0, 0.25); 547 else if(c == Color(229, 229, 229)) 548 return Color(0x99, 0x99, 0x99); 549 else return c; 550 } 551 552 bool debugMode = false; 553 }