home · contact · privacy
Make .tainted hierarchical, disallow direct unsetting except on self.
authorChristian Heller <c.heller@plomlompom.de>
Mon, 11 Aug 2025 01:54:16 +0000 (03:54 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 11 Aug 2025 01:54:16 +0000 (03:54 +0200)
ircplom/client_tui.py
ircplom/tui_base.py

index 1df12ced1b46aeb4f43b40fd9f2de1ac709be66d..ea6d9f32d93719c0c45c4d9254df3f1483cc8c2f 100644 (file)
@@ -67,7 +67,7 @@ class _PrivmsgPromptWidget(PromptWidget):
         'Update prompt prefix with nickname data.'
         self._nickname = nickname
         self._nick_confirmed = nick_confirmed
-        self.tainted = True
+        self._tainted = True
 
     @classmethod
     def prefix_update_keys(cls) -> set:
@@ -159,13 +159,11 @@ class _ClientWindowsManager:
                 f'changing {key}: [{vals[0]}] -> [{vals[1]}]',
                 scope=LogScope.ALL if key == 'nickname' else LogScope.SERVER)
             setattr(self, key, vals[1])
-        tainteds = False
         if _PrivmsgPromptWidget.prefix_update_keys() | set(to_change):
             for win in [w for w in self.windows
                         if isinstance(w, _PrivmsgWindow)]:
                 self._prompt_update(win)
-                tainteds |= win.prompt.tainted
-        return tainteds
+        return bool([w for w in self.windows if w.tainted])
 
 
 class ClientTui(BaseTui):
index c829bbf180f288fb5753266ce2d13db4c5206201..97b540480d4a1268bd0e4bd232b8780b113be5a9 100644 (file)
@@ -55,13 +55,22 @@ class _Widget(ABC):
     @abstractmethod
     def __init__(self, **kwargs) -> None:
         super().__init__(**kwargs)
-        self.tainted = True
+        self._tainted = True
         self._drawable = False
 
+    def taint(self) -> None:
+        'Declare as in need of re-drawing.'
+        self._tainted = True
+
+    @property
+    def tainted(self) -> bool:
+        '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._tainted = True
         self._drawable = len([m for m in measurements if m < 0]) == 0
         return self._drawable
 
@@ -70,7 +79,7 @@ class _Widget(ABC):
         'Print widget\'s content in shape appropriate to set geometry.'
         if not self._drawable:
             return False
-        self.tainted = False
+        self._tainted = False
         return True
 
 
@@ -88,7 +97,7 @@ class _ScrollableWidget(_Widget, ABC):
 
     @abstractmethod
     def _scroll(self, up=True) -> None:
-        self.tainted = True
+        self._tainted = True
 
     def cmd__scroll(self, direction: str) -> None:
         'Scroll through stored content/history.'
@@ -131,7 +140,7 @@ class _HistoryWidget(_ScrollableWidget):
 
     def append(self, to_append: str) -> None:
         super().append(to_append)
-        self.tainted = True
+        self._tainted = True
         if self._history_idx < -1:
             self._history_idx -= 1
         if not self._drawable:
@@ -194,7 +203,7 @@ class PromptWidget(_ScrollableWidget):
 
     @_input_buffer.setter
     def _input_buffer(self, content) -> None:
-        self.tainted = True
+        self._tainted = True
         self._input_buffer_unsafe = content
 
     def set_geometry(self, measurements: _YX) -> bool:
@@ -275,7 +284,7 @@ class PromptWidget(_ScrollableWidget):
             self._cursor_x += 1
         else:
             return
-        self.tainted = True
+        self._tainted = True
 
     def _reset_buffer(self, content: str) -> None:
         self._input_buffer = content
@@ -304,6 +313,15 @@ class Window(_Widget):
         if hasattr(self._term, 'size'):
             self.set_geometry()
 
+    def taint(self) -> None:
+        super().taint()
+        self.history.taint()
+        self.prompt.taint()
+
+    @property
+    def tainted(self) -> bool:
+        return super().tainted or self.history.tainted or self.prompt.tainted
+
     def set_geometry(self, _=None) -> bool:
         assert _ is None
         if self._term.size.y < _MIN_HEIGHT or self._term.size.x < _MIN_WIDTH:
@@ -341,23 +359,20 @@ class Window(_Widget):
         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)
-        self.history.draw()
-        self.prompt.draw()
         return True
 
     def cmd__paste(self) -> None:
         'Write OSC 52 ? sequence to get encoded clipboard paste into stdin.'
         self._term.write(f'\033{_OSC52_PREFIX.decode()}?{_PASTE_DELIMITER}',
                          self._y_status)
-        self.tainted = True
+        self._tainted = True
 
     def draw_tainted(self) -> None:
         'Draw tainted parts of self.'
-        if self.tainted:
-            self.draw()
-            return
         for widget in [w for w in (self.history, self.prompt) if w.tainted]:
             widget.draw()
+        if self.tainted:
+            self.draw()
 
 
 class TuiEvent(AffectiveEvent):
@@ -416,8 +431,8 @@ class BaseTui(QueueMixin):
         return self._windows[self._window_idx]
 
     def _switch_window(self, idx: int) -> None:
+        self.window.taint()
         self._window_idx = idx
-        self.window.tainted = True
 
     @property
     def _commands(self) -> dict[str, tuple[Callable[..., None | Optional[str]],