diff options
author | sterni <sternenseemann@systemli.org> | 2021-06-29 10:26:15 +0200 |
---|---|---|
committer | sterni <sternenseemann@systemli.org> | 2021-06-29 10:26:15 +0200 |
commit | cc6a84c388a58515c3c038f370e8995d5b87746f (patch) | |
tree | eaa6a9617b408eea1cf1c537611e2cb434d2546d /saneterm | |
parent | a182cda98ba3c840638dce7e1d51825455d7f5b9 (diff) |
Support bell character by parsing pty input into events
This commit introduces a new object, the PtyParser, which will be handling pty input in the future. It receives (utf-8 decoded) input and emits a series of events which encode pty output we want to represent in the UI: * The most basic event type is TEXT which is plain text we need to add to the TermBuffer. * Additionally BELL is now supported which indicates we want to beep (if enabled in gtk) and set the urgency flag of the window. Events seems to be a simple way to indicate future special behavior and by splitting the data read from the pty we can easily skip control characters which we don't want to render (and thus get rid of ugly codepoint boxes). Due to the use of yield and string slices, this approach should also be efficient enough. Using events instead of triggering the desired action in handle_pty ad-hoc also has the advantage that we can split the parsing logic into a separate object which is interesting for the following reasons: * Especially when we want to support a subset of ANSI control sequences we'll need to track parser state: - ANSI escape sequences are multiple codepoints long, so we need to track a parser state in order to keep it incremental. - ANSI escape sequences allow changing the font properties (style, weight, color) which is implemented in a stateful way, we'll also need to keep track of. * The parser is implemented independently of the rest of the application and especially the UI, so we'll be able to unit test it easily.
Diffstat (limited to 'saneterm')
-rw-r--r-- | saneterm/ptyparser.py | 76 | ||||
-rw-r--r-- | saneterm/terminal.py | 15 |
2 files changed, 90 insertions, 1 deletions
diff --git a/saneterm/ptyparser.py b/saneterm/ptyparser.py new file mode 100644 index 0000000..2a0d4bd --- /dev/null +++ b/saneterm/ptyparser.py @@ -0,0 +1,76 @@ +from enum import Enum, auto + +class PtyEventType(Enum): + TEXT = auto() + BELL = auto() + +class PtyParser(object): + """ + Parses a subset of special control sequences read from + a pty device. It is somewhat high level: Given a decoded, + proper Python string it will emit a series of events + which just need to be reflected in the UI while any state + is tracked in the PtyParser object. + """ + def __init__(self): + # no state, yet + pass + + def parse(self, input): + """ + Main interface of PtyParser. Given a proper decoded + Python string , it yields a series of tuples of the + form (PtyEventType, payload) which the caller can + iterate through. Valid events are: + + * PtyEventType.TEXT has a string slice as its payload + which should be appended to the terminal buffer as is. + + * PtyEventType.BELL has no payload and indicates that + the bell character '\a' was in the terminal input. + This usually should trigger the machine to beep + and/or the window to set the urgent flag. + """ + # keep track of the start and potential end position + # of the slice we want to emit as a TEXT event + start = 0 + pos = 0 + # TODO: can we check for the last element more efficiently? + size = len(input) + + # we expect a decoded string as input, + # so we don't need to handle incremental + # decoding here as well + for code in input: + # if flush_until is set, a slice of the buffer + # from start to flush_until will be emitted as + # a TEXT event + flush_until = None + # if not None, will be yielded as is, but only + # after any necessary flushing + special_ev = None + + # control characters flush before advancing pos + # in order to not add them to the buffer -- we + # want to handle them ourselves instead of + # relying of gtk's default behavior. + if code == '\a': + flush_until = pos + special_ev = (PtyEventType.BELL, None) + + pos += 1 + + # at the end of input, flush if we aren't already + if flush_until == None and pos >= size: + flush_until = pos + + # only generate text event if it is non empty, … + if flush_until != None and flush_until > start: + yield (PtyEventType.TEXT, input[start:flush_until]) + + # … but advance as if we had flushed + if flush_until != None: + start = pos + + if special_ev != None: + yield special_ev diff --git a/saneterm/terminal.py b/saneterm/terminal.py index 827f46c..70f538c 100644 --- a/saneterm/terminal.py +++ b/saneterm/terminal.py @@ -11,6 +11,7 @@ from . import proc from .search import SearchBar from .history import History from .termview import * +from .ptyparser import PtyParser, PtyEventType from gi.repository import Gtk from gi.repository import Gdk @@ -71,6 +72,8 @@ class Terminal(Gtk.Window): self.pty.set_callback(self.handle_pty) self.pty.attach(None) + self.pty_parser = PtyParser() + self.termview = TermView(self.complete, limit) # Block-wise reading from the PTY requires an incremental decoder. @@ -195,7 +198,17 @@ class Terminal(Gtk.Window): if not data: raise AssertionError("expected data but did not receive any") - self.termview.insert_data(self.decoder.decode(data)) + decoded = self.decoder.decode(data) + + for (ev, data) in self.pty_parser.parse(decoded): + if ev is PtyEventType.TEXT: + self.termview.insert_data(data) + elif ev is PtyEventType.BELL: + self.termview.error_bell() + self.set_urgency_hint(True) + else: + raise AssertionError("unknown PtyEventType") + return GLib.SOURCE_CONTINUE def toggle_search(self, termview, search_bar): |