about summary refs log tree commit diff
diff options
context:
space:
mode:
authorsterni <sternenseemann@systemli.org>2021-06-29 10:26:15 +0200
committersterni <sternenseemann@systemli.org>2021-06-29 10:26:15 +0200
commitcc6a84c388a58515c3c038f370e8995d5b87746f (patch)
treeeaa6a9617b408eea1cf1c537611e2cb434d2546d
parenta182cda98ba3c840638dce7e1d51825455d7f5b9 (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.
-rw-r--r--saneterm/ptyparser.py76
-rw-r--r--saneterm/terminal.py15
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):