home · contact · privacy
Turn status line into its own widget. master
authorChristian Heller <c.heller@plomlompom.de>
Mon, 11 Aug 2025 02:30:32 +0000 (04:30 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 11 Aug 2025 02:30:32 +0000 (04:30 +0200)
ircplom/tui_base.py

index 97b540480d4a1268bd0e4bd232b8780b113be5a9..998a650d4cc25313ea8d79aa46d2d9ce7d15267b 100644 (file)
@@ -50,9 +50,8 @@ class _YX(NamedTuple):
     x: int
 
 
-class _Widget(ABC):
+class _Widget:
 
-    @abstractmethod
     def __init__(self, **kwargs) -> None:
         super().__init__(**kwargs)
         self._tainted = True
@@ -67,14 +66,12 @@ class _Widget(ABC):
         'If in need of re-drawing.'
         return self._tainted
 
-    @abstractmethod
     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) -> bool:
         'Print widget\'s content in shape appropriate to set geometry.'
         if not self._drawable:
@@ -108,8 +105,7 @@ class _HistoryWidget(_ScrollableWidget):
     _view_size: _YX
     _y_pgscroll: int
 
-    def __init__(self, wrap: Callable[[str], list[str]], **kwargs
-                 ) -> None:
+    def __init__(self, wrap: Callable[[str], list[str]], **kwargs) -> None:
         super().__init__(**kwargs)
         self._wrap = wrap
         self._wrapped_idx = self._history_idx = -1
@@ -298,6 +294,30 @@ class PromptWidget(_ScrollableWidget):
         return to_return
 
 
+class _StatusLine(_Widget):
+    _y: int
+    _width: int
+
+    def __init__(self, write: Callable, status_title: str, **kwargs) -> None:
+        super().__init__(**kwargs)
+        self._write = write
+        self._status_title = status_title
+
+    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) -> bool:
+        if not super().draw():
+            return False
+        title_box = f'{self._status_title}]'
+        status_line = title_box + '=' * (self._width - len(title_box))
+        self._write(status_line, self._y)
+        return True
+
+
 class Window(_Widget):
     'Widget filling entire screen with sub-widgets like .prompt, .history.'
     _y_status: int
@@ -309,6 +329,8 @@ class Window(_Widget):
         self._term = term
         self.history = _HistoryWidget(wrap=self._term.wrap,
                                       write=self._term.write)
+        self._status_line = _StatusLine(write=self._term.write,
+                                        status_title=self.status_title)
         self.prompt = self.__annotations__['prompt'](write=self._term.write)
         if hasattr(self._term, 'size'):
             self.set_geometry()
@@ -316,11 +338,12 @@ class Window(_Widget):
     def taint(self) -> None:
         super().taint()
         self.history.taint()
+        self._status_line.taint()
         self.prompt.taint()
 
     @property
     def tainted(self) -> bool:
-        return super().tainted or self.history.tainted or self.prompt.tainted
+        return self._tainted or self.history.tainted or self.prompt.tainted
 
     def set_geometry(self, _=None) -> bool:
         assert _ is None
@@ -333,6 +356,7 @@ class Window(_Widget):
         super().set_geometry(_YX(0, 0))
         self._y_status = self._term.size.y - 2
         self.history.set_geometry(_YX(self._y_status, self._term.size.x))
+        self._status_line.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
 
@@ -356,9 +380,10 @@ class Window(_Widget):
                 for y, line in enumerate(lines):
                     self._term.write(line, y)
             return False
-        title_box = f'{self.status_title}]'
-        status_line = title_box + '=' * (self._term.size.x - len(title_box))
-        self._term.write(status_line, self._y_status)
+        for widget in [
+                w for w in (self.history, self.prompt, self._status_line)
+                if w.tainted]:
+            widget.draw()
         return True
 
     def cmd__paste(self) -> None:
@@ -367,13 +392,6 @@ class Window(_Widget):
                          self._y_status)
         self._tainted = True
 
-    def draw_tainted(self) -> None:
-        'Draw tainted parts of self.'
-        for widget in [w for w in (self.history, self.prompt) if w.tainted]:
-            widget.draw()
-        if self.tainted:
-            self.draw()
-
 
 class TuiEvent(AffectiveEvent):
     'To affect TUI.'
@@ -414,8 +432,8 @@ class BaseTui(QueueMixin):
         return win
 
     def redraw_affected(self) -> None:
-        'On focused window call .draw_tainted, then flush screen.'
-        self.window.draw_tainted()
+        'On focused window call .draw, then flush screen.'
+        self.window.draw()
         self._term.flush()
 
     def _set_screen(self) -> None:
@@ -423,6 +441,7 @@ class BaseTui(QueueMixin):
         self._term.calc_geometry()
         for window in self._windows:
             window.set_geometry()
+        self._y_status = self._term.size.y - 2
         self.redraw_affected()
 
     @property