diff options
author | Sören Tempel <soeren+git@soeren-tempel.net> | 2021-05-16 02:03:15 +0200 |
---|---|---|
committer | Sören Tempel <soeren+git@soeren-tempel.net> | 2021-05-16 02:03:15 +0200 |
commit | f50cf65b74fdda473382af45df3343e7a5a83821 (patch) | |
tree | 9d5e0cb4d9a26c521d1b8ec0f4469cabef48eb41 /saneterm | |
parent | 64fd725512de68f166e0299ffb5e52aab9bebcf1 (diff) |
Add support for setuptools
Diffstat (limited to 'saneterm')
-rw-r--r-- | saneterm/__init__.py | 0 | ||||
-rw-r--r-- | saneterm/__main__.py | 11 | ||||
-rw-r--r-- | saneterm/input.py | 27 | ||||
-rw-r--r-- | saneterm/terminal.py | 144 |
4 files changed, 182 insertions, 0 deletions
diff --git a/saneterm/__init__.py b/saneterm/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/saneterm/__init__.py diff --git a/saneterm/__main__.py b/saneterm/__main__.py new file mode 100644 index 0000000..1cd1362 --- /dev/null +++ b/saneterm/__main__.py @@ -0,0 +1,11 @@ +import sys +from terminal import * + +def main(): + win = Terminal(["dash"]) + win.connect("destroy", Gtk.main_quit) + win.show_all() + Gtk.main() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/saneterm/input.py b/saneterm/input.py new file mode 100644 index 0000000..fdebb7c --- /dev/null +++ b/saneterm/input.py @@ -0,0 +1,27 @@ +import gi +gi.require_version("Gtk", "3.0") + +from gi.repository import Gtk +from gi.repository import GObject + +class KeyBindings(): + stylesheet = b""" + @binding-set saneterm-key-bindings { + bind "<ctrl>u" { "kill-after-output" () }; + bind "<ctrl>a" { "move-input-start" () }; + bind "<ctrl>e" { "move-input-end" () }; + } + + * { + -gtk-key-bindings: saneterm-key-bindings; + } + """ + + def __init__(self): + self.provider = Gtk.CssProvider() + self.provider.load_from_data(self.stylesheet) + + def apply(self, widget): + style_ctx = widget.get_style_context() + style_ctx.add_provider(self.provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) diff --git a/saneterm/terminal.py b/saneterm/terminal.py new file mode 100644 index 0000000..9615bc5 --- /dev/null +++ b/saneterm/terminal.py @@ -0,0 +1,144 @@ +import sys +import pty +import os +import codecs + +import input + +import gi +gi.require_version("Gtk", "3.0") + +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib +from gi.repository import GObject + +WIN_TITLE = "saneterm" +TERM = "dumb" + +# XXX: Can also be looked up using unicodedata.lookup("DEL"). +DEL_CHAR = b'\x7f' + +class PtySource(GLib.Source): + master = -1 + + def __init__(self, cmd): + GLib.Source.__init__(self) + self.cmd = cmd + + def prepare(self): + if self.master != -1: + return False, -1 + + pid, self.master = pty.fork() + if pid == pty.CHILD: + # Terminal options enforced by saneterm. + # Most importantly, local echo is disabled. Instead we show + # characters on input directly in the GTK TextView/TextBuffer. + os.system("stty -onlcr -echo") + + os.execvpe(self.cmd[0], self.cmd, {"TERM": TERM}) + + self.add_unix_fd(self.master, GLib.IOCondition.IN) + return False, -1 + + def check(self): + return False + + def dispatch(self, callback, args): + return callback(self.master) + +class Terminal(Gtk.Window): + def __init__(self, cmd): + Gtk.Window.__init__(self, title=WIN_TITLE) + + self.pty = PtySource(cmd) + self.pty.set_callback(self.handle_pty) + self.pty.attach(None) + + self.textview = Gtk.TextView() + self.textview.set_wrap_mode(Gtk.WrapMode(Gtk.WrapMode.WORD_CHAR)) + + self.textbuffer = self.textview.get_buffer() + self.textbuffer.connect("end-user-action", self.user_input) + + end = self.textbuffer.get_end_iter() + self.last_mark = self.textbuffer.create_mark(None, end, True) + self.last_output_mark = None + + # Block-wise reading from the PTY requires an incremental decoder. + self.decoder = codecs.getincrementaldecoder('UTF-8')() + + bindings = input.KeyBindings() + bindings.apply(self.textview) + + self.bind_custom_signals() + self.add(self.textview) + + def bind_custom_signals(self): + signals = { + "kill-after-output": self.kill_after_output, + "move-input-start": self.move_input_start, + "move-input-end": self.move_input_end, + } + + for signal in signals.items(): + name, func = signal + GObject.signal_new(name, self.textview, + GObject.SIGNAL_ACTION, GObject.TYPE_NONE, + ()) + + self.textview.connect(name, func) + + def handle_pty(self, master): + # XXX: Should be possible to read more than one byte here. + data = os.read(master, 1) + if not data: + raise AssertionError("expected data but did not receive any") + + end = self.textbuffer.get_end_iter() + self.textbuffer.insert(end, self.decoder.decode(data)) + + end = self.textbuffer.get_end_iter() + self.last_mark = self.textbuffer.create_mark(None, end, True) + self.last_output_mark = self.last_mark + + return GLib.SOURCE_CONTINUE + + def user_input(self, buffer): + start = buffer.get_iter_at_mark(self.last_mark) + end = buffer.get_end_iter() + + text = buffer.get_text(start, end, True) + if text == "\n": + start = buffer.get_iter_at_mark(self.last_output_mark) + line = buffer.get_text(start, end, True) + + os.write(self.pty.master, line.encode("UTF-8")) + self.last_output_mark = buffer.create_mark(None, end, True) + + self.last_mark = buffer.create_mark(None, end, True) + + def kill_after_output(self, textview): + if self.last_output_mark is None: + return + buffer = textview.get_buffer() + + start = buffer.get_iter_at_mark(self.last_output_mark) + end = buffer.get_end_iter() + + buffer.delete(start, end) + + def move_input_start(self, textview): + if self.last_output_mark is None: + return + buffer = textview.get_buffer() + + start = buffer.get_iter_at_mark(self.last_output_mark) + buffer.place_cursor(start) + + def move_input_end(self, textview): + buffer = textview.get_buffer() + + end = buffer.get_iter_at_mark(self.last_mark) + buffer.place_cursor(end) |