From: Christian Heller Date: Mon, 11 Aug 2025 05:15:19 +0000 (+0200) Subject: Simplify base TUI code. X-Git-Url: https://plomlompom.com/repos/booking/%7B%7B%20web_path%20%7D%7D/static/do_day?a=commitdiff_plain;h=1860ac9d62ff76e15faf926a00dc7fdc47ea781e;p=ircplom Simplify base TUI code. --- diff --git a/ircplom/tui_base.py b/ircplom/tui_base.py index 998a650..415bf7b 100644 --- a/ircplom/tui_base.py +++ b/ircplom/tui_base.py @@ -50,12 +50,13 @@ class _YX(NamedTuple): x: int -class _Widget: +class _Widget(ABC): + _tainted: bool = True + _sizes = _YX(-1, -1) - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self._tainted = True - self._drawable = False + @property + def _drawable(self) -> bool: + return len([m for m in self._sizes if m < 1]) == 0 def taint(self) -> None: 'Declare as in need of re-drawing.' @@ -66,21 +67,23 @@ class _Widget: 'If in need of re-drawing.' return self._tainted - 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 + def set_geometry(self, sizes: _YX) -> None: + 'Update widget\'s sizues, re-generate content where necessary.' + self.taint() + self._sizes = sizes - def draw(self) -> bool: + def draw(self) -> None: 'Print widget\'s content in shape appropriate to set geometry.' - if not self._drawable: - return False - self._tainted = False - return True + if self._drawable: + self._draw() + self._tainted = False + @abstractmethod + def _draw(self) -> None: + pass -class _ScrollableWidget(_Widget, ABC): + +class _ScrollableWidget(_Widget): _history_idx: int def __init__(self, write: Callable[..., None], **kwargs) -> None: @@ -94,7 +97,7 @@ class _ScrollableWidget(_Widget, ABC): @abstractmethod def _scroll(self, up=True) -> None: - self._tainted = True + self.taint() def cmd__scroll(self, direction: str) -> None: 'Scroll through stored content/history.' @@ -102,7 +105,6 @@ class _ScrollableWidget(_Widget, ABC): class _HistoryWidget(_ScrollableWidget): - _view_size: _YX _y_pgscroll: int def __init__(self, wrap: Callable[[str], list[str]], **kwargs) -> None: @@ -116,70 +118,64 @@ class _HistoryWidget(_ScrollableWidget): self._wrapped += [(idx_original, line) for line in wrapped_lines] return len(wrapped_lines) - 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 True - for idx_history, line in enumerate(self._history): - self._add_wrapped(idx_history, line) - wrapped_lines_for_history_idx = [ - t for t in self._wrapped - 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 set_geometry(self, sizes: _YX) -> None: + super().set_geometry(sizes) + if self._drawable: + self._y_pgscroll = self._sizes.y // 2 + self._wrapped.clear() + self._wrapped += [(None, '')] * self._sizes.y + if self._history: + for idx_history, line in enumerate(self._history): + self._add_wrapped(idx_history, line) + wrapped_lines_for_history_idx = [ + t for t in self._wrapped + 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) def append(self, to_append: str) -> None: super().append(to_append) - self._tainted = True + self.taint() 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._wrapped_idx -= n_wrapped_lines - - def draw(self) -> bool: - if not super().draw(): - return False - start_idx = self._wrapped_idx - self._view_size.y + 1 + if self._drawable: + n_wrapped_lines = self._add_wrapped(len(self._history) + - 1, to_append) + if self._wrapped_idx < -1: + self._wrapped_idx -= n_wrapped_lines + + def _draw(self) -> None: + start_idx = self._wrapped_idx - self._sizes.y + 1 end_idx = self._wrapped_idx to_write = [t[1] for t in self._wrapped[start_idx:end_idx]] if self._wrapped_idx < -1: scroll_info = f'vvv [{(-1) * self._wrapped_idx}] ' - scroll_info += 'v' * (self._view_size.x - len(scroll_info)) + scroll_info += 'v' * (self._sizes.x - len(scroll_info)) to_write += [scroll_info] else: 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) - else: - self._wrapped_idx = min(-1, - self._wrapped_idx + self._y_pgscroll) - history_idx_to_wrapped_idx = self._wrapped[self._wrapped_idx][0] - if history_idx_to_wrapped_idx is not None: - self._history_idx = history_idx_to_wrapped_idx - len(self._history) + if self._drawable: + if up: + self._wrapped_idx = max( + self._sizes.y + 1 - len(self._wrapped), + self._wrapped_idx - self._y_pgscroll) + else: + self._wrapped_idx = min( + -1, self._wrapped_idx + self._y_pgscroll) + history_idx_to_wrapped_idx = self._wrapped[self._wrapped_idx][0] + if history_idx_to_wrapped_idx is not None: + self._history_idx = history_idx_to_wrapped_idx\ + - len(self._history) class PromptWidget(_ScrollableWidget): 'Manages/displays keyboard input field.' - _y: int - _width: int _history_idx: int = 0 _input_buffer_unsafe: str _cursor_x: int @@ -199,39 +195,31 @@ class PromptWidget(_ScrollableWidget): @_input_buffer.setter def _input_buffer(self, content) -> None: - self._tainted = True + self.taint() self._input_buffer_unsafe = content - 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 + def _draw(self) -> None: prefix = self.prefix[:] content = self._input_buffer if self._cursor_x == len(self._input_buffer): content += ' ' - half_width = (self._width - len(prefix)) // 2 + half_width = (self._sizes.x - len(prefix)) // 2 offset = 0 - if len(prefix) + len(content) > self._width\ + if len(prefix) + len(content) > self._sizes.x\ and self._cursor_x > half_width: prefix += _PROMPT_ELL_IN - offset = min(len(prefix) + len(content) - self._width, + offset = min(len(prefix) + len(content) - self._sizes.x, self._cursor_x - half_width + len(_PROMPT_ELL_IN)) cursor_x_to_write = len(prefix) + self._cursor_x - offset to_write = f'{prefix}{content[offset:]}' - if len(to_write) > self._width: - to_write = (to_write[:self._width - len(_PROMPT_ELL_OUT)] + if len(to_write) > self._sizes.x: + to_write = (to_write[:self._sizes.x-len(_PROMPT_ELL_OUT)] + _PROMPT_ELL_OUT) - self._write(to_write[:cursor_x_to_write], self._y, padding=False) + self._write(to_write[:cursor_x_to_write], self._sizes.y, + padding=False) 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) @@ -280,7 +268,7 @@ class PromptWidget(_ScrollableWidget): self._cursor_x += 1 else: return - self._tainted = True + self.taint() def _reset_buffer(self, content: str) -> None: self._input_buffer = content @@ -295,32 +283,22 @@ class PromptWidget(_ScrollableWidget): 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 + def _draw(self) -> None: title_box = f'{self._status_title}]' - status_line = title_box + '=' * (self._width - len(title_box)) - self._write(status_line, self._y) - return True + status_line = title_box + '=' * (self._sizes.x - len(title_box)) + self._write(status_line, self._sizes.y) -class Window(_Widget): - 'Widget filling entire screen with sub-widgets like .prompt, .history.' +class Window: + 'Collection of widgets filling entire screen.' _y_status: int + _drawable = False prompt: PromptWidget def __init__(self, idx: int, term: 'Terminal', **kwargs) -> None: @@ -336,29 +314,26 @@ class Window(_Widget): self.set_geometry() def taint(self) -> None: - super().taint() self.history.taint() self._status_line.taint() self.prompt.taint() @property def tainted(self) -> bool: - return self._tainted or self.history.tainted or self.prompt.tainted + return (self._status_line.tainted or self.history.tainted + or self.prompt.tainted) - def set_geometry(self, _=None) -> bool: - assert _ is None + def set_geometry(self) -> None: + self._drawable = False 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.history.set_geometry(bad_yx) - self.prompt.set_geometry(bad_yx) - return False - super().set_geometry(_YX(0, 0)) + for widget in (self.history, self._status_line, self.prompt): + widget.set_geometry(_YX(-1, -1)) + return 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 + self._drawable = True @property def _title(self) -> str: @@ -369,28 +344,26 @@ class Window(_Widget): 'Window title to display in status line.' return f'{self.idx}) {self._title}' - def draw(self) -> bool: - 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 - for widget in [ - w for w in (self.history, self.prompt, self._status_line) - if w.tainted]: - widget.draw() - return True + def draw_tainted(self) -> None: + if self._drawable: + for widget in [ + w for w in (self.history, self.prompt, self._status_line) + if w.tainted]: + widget.draw() + elif 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) 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.taint() class TuiEvent(AffectiveEvent): @@ -433,7 +406,7 @@ class BaseTui(QueueMixin): def redraw_affected(self) -> None: 'On focused window call .draw, then flush screen.' - self.window.draw() + self.window.draw_tainted() self._term.flush() def _set_screen(self) -> None: @@ -441,7 +414,6 @@ 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