home · contact · privacy
Handle tiny screen sizes in TUI rendering.
authorChristian Heller <c.heller@plomlompom.de>
Tue, 17 Jun 2025 15:30:21 +0000 (17:30 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Tue, 17 Jun 2025 15:30:21 +0000 (17:30 +0200)
ircplom/tui.py

index 61589b8464201da28ff3c35e02d9c0fbce547c32..77d098d3ecc0de84e8a69006eac197cf6bcf663d 100644 (file)
@@ -17,6 +17,8 @@ from ircplom.irc_conn import (
         InitConnWindowEvent, InitReconnectEvent, IrcMessage, LoginNames,
         LogConnEvent, NickSetEvent, SendEvent, TIMEOUT_LOOP)
 
+_MIN_HEIGHT = 4
+_MIN_WIDTH = 32
 
 _B64_PREFIX = 'b64:'
 _OSC52_PREFIX = ']52;c;'
@@ -71,16 +73,22 @@ class _Widget(ABC):
     @abstractmethod
     def __init__(self, *args, **kwargs) -> None:
         self.tainted = True
+        self._drawable = False
 
     @abstractmethod
-    def set_geometry(self, measurements: _YX) -> None:
+    def set_geometry(self, measurements: _YX) -> bool:
         'Update widget\'s measurements, re-generate content where necessary.'
         self.tainted = True
+        self._drawable = len([m for m in measurements if m < 0]) == 0
+        return self._drawable
 
     @abstractmethod
-    def draw(self) -> None:
+    def draw(self) -> bool:
         'Print widget\'s content in shape appropriate to set geometry.'
+        if not self._drawable:
+            return False
         self.tainted = False
+        return True
 
 
 class _ScrollableWidget(_Widget, ABC):
@@ -122,14 +130,15 @@ class _LogWidget(_ScrollableWidget):
         self._wrapped += [(idx_original, line) for line in wrapped_lines]
         return len(wrapped_lines)
 
-    def set_geometry(self, measurements: _YX) -> None:
-        super().set_geometry(measurements)
+    def set_geometry(self, measurements: _YX) -> bool:
+        if not super().set_geometry(measurements):
+            return False
         self._view_size = measurements
         self._y_pgscroll = self._view_size.y // 2
         self._wrapped.clear()
         self._wrapped += [(None, '')] * self._view_size.y
         if not self._history:
-            return
+            return True
         for idx_history, line in enumerate(self._history):
             self._add_wrapped(idx_history, line)
         wrapped_lines_for_history_idx = [
@@ -137,17 +146,22 @@ class _LogWidget(_ScrollableWidget):
                 if t[0] == len(self._history) + self._history_idx]
         idx_their_last = self._wrapped.index(wrapped_lines_for_history_idx[-1])
         self._wrapped_idx = idx_their_last - len(self._wrapped)
+        return True
 
     def append(self, to_append: str) -> None:
         super().append(to_append)
+        self.tainted = True
+        if self._history_idx < -1:
+            self._history_idx -= 1
+        if not self._drawable:
+            return
         n_wrapped_lines = self._add_wrapped(len(self._history) - 1, to_append)
         if self._wrapped_idx < -1:
-            self._history_idx -= 1
             self._wrapped_idx -= n_wrapped_lines
-        self.tainted = True
 
-    def draw(self) -> None:
-        super().draw()
+    def draw(self) -> bool:
+        if not super().draw():
+            return False
         start_idx = self._wrapped_idx - self._view_size.y + 1
         end_idx = self._wrapped_idx
         to_write = [t[1] for t in self._wrapped[start_idx:end_idx]]
@@ -159,9 +173,12 @@ class _LogWidget(_ScrollableWidget):
             to_write += [self._wrapped[self._wrapped_idx][1]]
         for i, line in enumerate(to_write):
             self._write(line, i)
+        return True
 
     def _scroll(self, up: bool = True) -> None:
         super()._scroll(up)
+        if not self._drawable:
+            return
         if up:
             self._wrapped_idx = max(self._view_size.y + 1 - len(self._wrapped),
                                     self._wrapped_idx - self._y_pgscroll)
@@ -195,12 +212,15 @@ class _PromptWidget(_ScrollableWidget):
         self.tainted = True
         self._input_buffer_unsafe = content
 
-    def set_geometry(self, measurements: _YX) -> None:
-        super().set_geometry(measurements)
+    def set_geometry(self, measurements: _YX) -> bool:
+        if not super().set_geometry(measurements):
+            return False
         self._y, self._width = measurements
+        return True
 
-    def draw(self) -> None:
-        super().draw()
+    def draw(self) -> bool:
+        if not super().draw():
+            return False
         prefix = self._prompt[:]
         content = self._input_buffer
         if self._cursor_x == len(self._input_buffer):
@@ -221,6 +241,7 @@ class _PromptWidget(_ScrollableWidget):
         self._write(to_write[cursor_x_to_write], attribute='reverse',
                     padding=False)
         self._write(to_write[cursor_x_to_write + 1:])
+        return True
 
     def _archive_prompt(self) -> None:
         self.append(self._input_buffer)
@@ -316,21 +337,38 @@ class _Window(_Widget):
         if hasattr(self._term, 'size'):
             self.set_geometry()
 
-    def set_geometry(self, _=None) -> None:
-        super().set_geometry(_)
+    def set_geometry(self, _=None) -> bool:
         assert _ is None
+        if self._term.size.y < _MIN_HEIGHT or self._term.size.x < _MIN_WIDTH:
+            bad_yx = _YX(-1, -1)
+            super().set_geometry(bad_yx)
+            self.log.set_geometry(bad_yx)
+            self.prompt.set_geometry(bad_yx)
+            return False
+        super().set_geometry(_YX(0, 0))
         self._y_status = self._term.size.y - 2
         self.log.set_geometry(_YX(self._y_status, self._term.size.x))
         self.prompt.set_geometry(_YX(self._term.size.y - 1, self._term.size.x))
+        return True
 
-    def draw(self) -> None:
-        super().draw()
+    def draw(self) -> bool:
+        self._term.clear()
+        if not super().draw():
+            if self._term.size.x > 0:
+                lines = ['']
+                for i, c in enumerate('screen too small'):
+                    if i > 0 and 0 == i % self._term.size.x:
+                        lines += ['']
+                    lines[-1] += c
+                for y, line in enumerate(lines):
+                    self._term.write(line, y)
+            return False
         idx_box = f'[{self.idx}]'
         status_line = idx_box + '=' * (self._term.size.x - len(idx_box))
-        self._term.clear()
         self.log.draw()
         self._term.write(status_line, self._y_status)
         self.prompt.draw()
+        return True
 
     def cmd__paste(self) -> None:
         'Write OSC 52 ? sequence to get encoded clipboard paste into stdin.'
@@ -575,7 +613,7 @@ class Terminal:
               padding: bool = True
               ) -> None:
         'Print to terminal, with position, padding to line end, attributes.'
-        if start_y:
+        if start_y is not None:
             self._cursor_yx = _YX(start_y, 0)
         # ._blessed.length can slow down things notably: only use where needed!
         end_x = self._cursor_yx.x + (len(msg) if msg.isascii()