From: Christian Heller Date: Sun, 15 Jun 2025 18:22:44 +0000 (+0200) Subject: Reorganize how TUI determines what parts of screen to redraw. X-Git-Url: https://plomlompom.com/repos/%7B%7B%20web_path%20%7D%7D/%22https:/validator.w3.org/do_day?a=commitdiff_plain;p=ircplom Reorganize how TUI determines what parts of screen to redraw. --- diff --git a/ircplom/tui.py b/ircplom/tui.py index f36ff51..61589b8 100644 --- a/ircplom/tui.py +++ b/ircplom/tui.py @@ -68,13 +68,19 @@ class _YX(NamedTuple): class _Widget(ABC): 'Defines most basic TUI object API.' + @abstractmethod + def __init__(self, *args, **kwargs) -> None: + self.tainted = True + @abstractmethod def set_geometry(self, measurements: _YX) -> None: 'Update widget\'s measurements, re-generate content where necessary.' + self.tainted = True @abstractmethod def draw(self) -> None: 'Print widget\'s content in shape appropriate to set geometry.' + self.tainted = False class _ScrollableWidget(_Widget, ABC): @@ -92,12 +98,11 @@ class _ScrollableWidget(_Widget, ABC): @abstractmethod def _scroll(self, up=True) -> None: - pass + self.tainted = True def cmd__scroll(self, direction: str) -> None: 'Scroll through stored content/history.' self._scroll(up=direction == 'up') - self.draw() class _LogWidget(_ScrollableWidget): @@ -118,6 +123,7 @@ class _LogWidget(_ScrollableWidget): return len(wrapped_lines) def set_geometry(self, measurements: _YX) -> None: + super().set_geometry(measurements) self._view_size = measurements self._y_pgscroll = self._view_size.y // 2 self._wrapped.clear() @@ -138,8 +144,10 @@ class _LogWidget(_ScrollableWidget): if self._wrapped_idx < -1: self._history_idx -= 1 self._wrapped_idx -= n_wrapped_lines + self.tainted = True def draw(self) -> None: + super().draw() 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]] @@ -153,6 +161,7 @@ class _LogWidget(_ScrollableWidget): self._write(line, i) def _scroll(self, up: bool = True) -> None: + super()._scroll(up) if up: self._wrapped_idx = max(self._view_size.y + 1 - len(self._wrapped), self._wrapped_idx - self._y_pgscroll) @@ -170,19 +179,30 @@ class _PromptWidget(_ScrollableWidget): _width: int _prompt: str = _PROMPT_TEMPLATE _history_idx = 0 - _input_buffer: str + _input_buffer_unsafe: str _cursor_x: int def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._reset_buffer('') + @property + def _input_buffer(self) -> str: + return self._input_buffer_unsafe[:] + + @_input_buffer.setter + def _input_buffer(self, content) -> None: + self.tainted = True + self._input_buffer_unsafe = content + def set_geometry(self, measurements: _YX) -> None: + super().set_geometry(measurements) self._y, self._width = measurements def draw(self) -> None: + super().draw() prefix = self._prompt[:] - content = self._input_buffer[:] + content = self._input_buffer if self._cursor_x == len(self._input_buffer): content += ' ' half_width = (self._width - len(prefix)) // 2 @@ -203,10 +223,11 @@ class _PromptWidget(_ScrollableWidget): self._write(to_write[cursor_x_to_write + 1:]) def _archive_prompt(self) -> None: - self.append(self._input_buffer[:]) + self.append(self._input_buffer) self._reset_buffer('') def _scroll(self, up: bool = True) -> None: + super()._scroll(up) if up and -(self._history_idx) < len(self._history): if self._history_idx == 0 and self._input_buffer: self._archive_prompt() @@ -230,7 +251,6 @@ class _PromptWidget(_ScrollableWidget): + to_append + self._input_buffer[self._cursor_x - 1:]) self._history_idx = 0 - self.draw() def cmd__backspace(self) -> None: 'Truncate current content by one character, if possible.' @@ -239,7 +259,6 @@ class _PromptWidget(_ScrollableWidget): self._input_buffer = (self._input_buffer[:self._cursor_x] + self._input_buffer[self._cursor_x + 1:]) self._history_idx = 0 - self.draw() def cmd__move_cursor(self, direction: str) -> None: 'Move cursor one space into direction ("left" or "right") if possible.' @@ -250,7 +269,7 @@ class _PromptWidget(_ScrollableWidget): self._cursor_x += 1 else: return - self.draw() + self.tainted = True def _reset_buffer(self, content: str) -> None: self._input_buffer = content @@ -261,7 +280,6 @@ class _PromptWidget(_ScrollableWidget): to_return = self._input_buffer[:] if to_return: self._archive_prompt() - self.draw() return to_return @@ -274,6 +292,7 @@ class _ConnectionPromptWidget(_PromptWidget): nick: Optional[str] = None ) -> None: 'Update nickname-relevant knowledge to go into prompt string.' + self.tainted = True self._prompt = '' if nick: self._nickname = nick @@ -298,12 +317,14 @@ class _Window(_Widget): self.set_geometry() def set_geometry(self, _=None) -> None: + super().set_geometry(_) assert _ is None 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)) def draw(self) -> None: + super().draw() idx_box = f'[{self.idx}]' status_line = idx_box + '=' * (self._term.size.x - len(idx_box)) self._term.clear() @@ -315,7 +336,15 @@ class _Window(_Widget): 'Write OSC 52 ? sequence to get encoded clipboard paste into stdin.' self._term.write(f'\033{_OSC52_PREFIX}?{_PASTE_DELIMITER}', self._y_status) - self.draw() + 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.log, self.prompt) if w.tainted]: + widget.draw() class _ConnectionWindow(_Window, BroadcastConnMixin): @@ -391,10 +420,8 @@ class _TuiLoop(Loop, BroadcastMixin): self._term.calc_geometry() for window in self._windows: window.set_geometry() - self.window.draw() elif isinstance(event, _LogEvent): self.window.log.append(event.payload) - self.window.log.draw() elif isinstance(event, _TuiCmdEvent): cmd = self._cmd_name_to_cmd(event.payload[0]) assert cmd is not None @@ -418,13 +445,9 @@ class _TuiLoop(Loop, BroadcastMixin): nick=event.payload) elif isinstance(event, DisconnectedEvent): conn_win.prompt.update_prompt(nick_confirmed=False) - if conn_win == self.window: - if isinstance(event, LogConnEvent): - self.window.log.draw() - if isinstance(event, (NickSetEvent, DisconnectedEvent)): - self.window.prompt.draw() else: return True + self.window.draw_tainted() self._term.flush() return True