arsd/minigui_addons/terminal_emulator_widget.d

333 lines
8.6 KiB
D

/++
Creates a UNIX terminal emulator, nested in a minigui widget.
Depends on my terminalemulator.d core in the arsd repo.
+/
module arsd.minigui_addons.terminal_emulator_widget;
///
version(tew_main)
unittest {
import arsd.minigui;
import arsd.minigui_addons.terminal_emulator_widget;
// 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.")
void main() {
auto window = new MainWindow("Minigui Terminal Emulation");
version(Posix)
auto tew = new TerminalEmulatorWidget(["/bin/bash"], window);
else version(Windows)
auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window);
window.loop();
}
main();
}
import arsd.minigui;
import arsd.terminalemulator;
class TerminalEmulatorWidget : Widget {
this(Widget parent) {
terminalEmulator = new TerminalEmulatorInsideWidget(this);
super(parent);
}
this(string[] args, Widget parent) {
version(Windows) {
import core.sys.windows.windows : HANDLE;
void startup(HANDLE inwritePipe, HANDLE outreadPipe) {
terminalEmulator = new TerminalEmulatorInsideWidget(inwritePipe, outreadPipe, this);
}
import std.string;
startChild!startup(args[0], args.join(" "));
}
else version(Posix) {
void startup(int master) {
int fd = master;
import fcntl = core.sys.posix.fcntl;
auto flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0);
if(flags == -1)
throw new Exception("fcntl get");
flags |= fcntl.O_NONBLOCK;
auto s = fcntl.fcntl(fd, fcntl.F_SETFL, flags);
if(s == -1)
throw new Exception("fcntl set");
terminalEmulator = new TerminalEmulatorInsideWidget(master, this);
}
import std.process;
auto cmd = environment.get("SHELL", "/bin/bash");
startChild!startup(args[0], args);
}
super(parent);
}
TerminalEmulatorInsideWidget terminalEmulator;
override void registerMovement() {
super.registerMovement();
terminalEmulator.resized(width, height);
}
override void focus() {
super.focus();
terminalEmulator.attentionReceived();
}
class Style : Widget.Style {
override MouseCursor cursor() { return GenericCursor.Text; }
}
mixin OverrideStyle!Style;
override void paint(WidgetPainter painter) {
terminalEmulator.redrawPainter(painter, true);
}
}
class TerminalEmulatorInsideWidget : TerminalEmulator {
void resized(int w, int h) {
this.resizeTerminal(w / fontWidth, h / fontHeight);
clearScreenRequested = true;
redraw();
}
protected override void changeCursorStyle(CursorStyle s) { }
protected override void changeWindowTitle(string t) {
//if(window && t.length)
//window.title = t;
}
protected override void changeWindowIcon(IndexedImage t) {
//if(window && t)
//window.icon = t;
}
protected override void changeIconTitle(string) {}
protected override void changeTextAttributes(TextAttributes) {}
protected override void soundBell() {
static if(UsingSimpledisplayX11)
XBell(XDisplayConnection.get(), 50);
}
protected override void demandAttention() {
//window.requestAttention();
}
protected override void copyToClipboard(string text) {
setClipboardText(widget.parentWindow.win, text);
}
protected override void pasteFromClipboard(void delegate(in char[]) dg) {
static if(UsingSimpledisplayX11)
getPrimarySelection(widget.parentWindow.win, dg);
else
getClipboardText(widget.parentWindow.win, (in char[] dataIn) {
char[] data;
// change Windows \r\n to plain \n
foreach(char ch; dataIn)
if(ch != 13)
data ~= ch;
dg(data);
});
}
protected override void copyToPrimary(string text) {
static if(UsingSimpledisplayX11)
setPrimarySelection(widget.parentWindow.win, text);
else
{}
}
protected override void pasteFromPrimary(void delegate(in char[]) dg) {
static if(UsingSimpledisplayX11)
getPrimarySelection(widget.parentWindow.win, dg);
}
override void requestExit() {
// FIXME
}
void resizeImage() { }
mixin PtySupport!(resizeImage);
version(Posix)
this(int masterfd, TerminalEmulatorWidget widget) {
master = masterfd;
this(widget);
}
else version(Windows) {
import core.sys.windows.windows;
this(HANDLE stdin, HANDLE stdout, TerminalEmulatorWidget widget) {
this.stdin = stdin;
this.stdout = stdout;
this(widget);
}
}
bool focused;
TerminalEmulatorWidget widget;
mixin SdpyDraw;
private this(TerminalEmulatorWidget widget) {
this.widget = widget;
fontSize = 14;
loadDefaultFont();
auto desiredWidth = 80;
auto desiredHeight = 24;
super(desiredWidth, desiredHeight);
bool skipNextChar = false;
widget.addEventListener((MouseDownEvent ev) {
int termX = (ev.clientX - paddingLeft) / fontWidth;
int termY = (ev.clientY - paddingTop) / fontHeight;
if(sendMouseInputToApplication(termX, termY,
arsd.terminalemulator.MouseEventType.buttonPressed,
cast(arsd.terminalemulator.MouseButton) ev.button,
(ev.state & ModifierState.shift) ? true : false,
(ev.state & ModifierState.ctrl) ? true : false,
(ev.state & ModifierState.alt) ? true : false
))
redraw();
});
widget.addEventListener((MouseUpEvent ev) {
int termX = (ev.clientX - paddingLeft) / fontWidth;
int termY = (ev.clientY - paddingTop) / fontHeight;
if(sendMouseInputToApplication(termX, termY,
arsd.terminalemulator.MouseEventType.buttonReleased,
cast(arsd.terminalemulator.MouseButton) ev.button,
(ev.state & ModifierState.shift) ? true : false,
(ev.state & ModifierState.ctrl) ? true : false,
(ev.state & ModifierState.alt) ? true : false
))
redraw();
});
widget.addEventListener((MouseMoveEvent ev) {
int termX = (ev.clientX - paddingLeft) / fontWidth;
int termY = (ev.clientY - paddingTop) / fontHeight;
if(sendMouseInputToApplication(termX, termY,
arsd.terminalemulator.MouseEventType.motion,
cast(arsd.terminalemulator.MouseButton) ev.button,
(ev.state & ModifierState.shift) ? true : false,
(ev.state & ModifierState.ctrl) ? true : false,
(ev.state & ModifierState.alt) ? true : false
))
redraw();
});
widget.addEventListener((KeyDownEvent ev) {
if(ev.key == Key.ScrollLock) {
toggleScrollbackWrap();
}
string magic() {
string code;
foreach(member; __traits(allMembers, TerminalKey))
if(member != "Escape")
code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
, (ev.state & ModifierState.shift)?true:false
, (ev.state & ModifierState.alt)?true:false
, (ev.state & ModifierState.ctrl)?true:false
, (ev.state & ModifierState.windows)?true:false
)) redraw(); break;";
return code;
}
switch(ev.key) {
//// I want the escape key to send twice to differentiate it from
//// other escape sequences easily.
//case Key.Escape: sendToApplication("\033"); break;
mixin(magic());
default:
// keep going, not special
}
// remapping of alt+key is possible too, at least on linux.
/+
static if(UsingSimpledisplayX11)
if(ev.state & ModifierState.alt) {
if(ev.character in altMappings) {
sendToApplication(altMappings[ev.character]);
skipNextChar = true;
}
}
+/
return; // the character event handler will do others
});
widget.addEventListener((CharEvent ev) {
dchar c = ev.character;
if(skipNextChar) {
skipNextChar = false;
return;
}
endScrollback();
char[4] str;
import std.utf;
if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
auto data = str[0 .. encode(str, c)];
// 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.
if(c != 127)
sendToApplication(data);
});
version(Posix) {
auto cls = new PosixFdReader(&readyToRead, master);
} else
version(Windows) {
overlapped = new OVERLAPPED();
overlapped.hEvent = cast(void*) this;
//window.handleNativeEvent = &windowsRead;
readyToReadWindows(0, 0, overlapped);
redraw();
}
}
static int fontSize = 14;
bool clearScreenRequested = true;
void redraw(bool forceRedraw = false) {
if(widget.parentWindow is null || widget.parentWindow.win is null)
return;
auto painter = widget.draw();
if(clearScreenRequested) {
auto clearColor = defaultBackground;
painter.outlineColor = clearColor;
painter.fillColor = clearColor;
painter.drawRectangle(Point(0, 0), widget.width, widget.height);
clearScreenRequested = false;
forceRedraw = true;
}
redrawPainter(painter, forceRedraw);
}
bool debugMode = false;
}