From: Christian Heller Date: Mon, 11 Aug 2025 07:57:28 +0000 (+0200) Subject: Move StatusLine out of Window, show live updates on other windows' activity, drop... X-Git-Url: https://plomlompom.com/repos/booking/%7B%7Bprefix%7D%7D/copy_structured?a=commitdiff_plain;h=1fef7b0dd009c5d6ed450c7572aac68cc26c11d6;p=ircplom Move StatusLine out of Window, show live updates on other windows' activity, drop auto-switching to new windows. --- diff --git a/ircplom/client_tui.py b/ircplom/client_tui.py index 94cd62f..d9bd76f 100644 --- a/ircplom/client_tui.py +++ b/ircplom/client_tui.py @@ -27,11 +27,8 @@ class _ClientWindow(Window, ClientQueueMixin): self.scope = scope self._log = log super().__init__(**kwargs) - - @property - def _title(self) -> str: - return f'{self.client_id} '\ - + f':{"SERVER" if self.scope == LogScope.SERVER else "RAW"}' + self._title = f'{self.client_id} '\ + + f':{"SERVER" if self.scope == LogScope.SERVER else "RAW"}' def _send_msg(self, verb: str, params: tuple[str, ...], **kwargs) -> None: self._client_trigger('send', msg=IrcMessage(verb=verb, params=params), @@ -84,13 +81,10 @@ class _PrivmsgPromptWidget(PromptWidget): class _PrivmsgWindow(_ClientWindow): prompt: _PrivmsgPromptWidget - @property - def _title(self) -> str: - return f'{self.client_id} {self.nickname}' - def __init__(self, nickname: str, **kwargs) -> None: self.nickname = nickname super().__init__(**kwargs) + self._title = f'{self.client_id} {self.nickname}' def cmd__chat(self, msg: str) -> None: 'PRIVMSG to target identified by .nickname.' diff --git a/ircplom/tui_base.py b/ircplom/tui_base.py index 415bf7b..13e002b 100644 --- a/ircplom/tui_base.py +++ b/ircplom/tui_base.py @@ -105,6 +105,7 @@ class _ScrollableWidget(_Widget): class _HistoryWidget(_ScrollableWidget): + _last_read: int = 0 _y_pgscroll: int def __init__(self, wrap: Callable[[str], list[str]], **kwargs) -> None: @@ -157,6 +158,12 @@ class _HistoryWidget(_ScrollableWidget): to_write += [self._wrapped[self._wrapped_idx][1]] for i, line in enumerate(to_write): self._write(line, i) + self._last_read = len(self._history) + + @property + def n_lines_unread(self) -> int: + 'How many new lines have been logged since last focus.' + return len(self._history) - self._last_read def _scroll(self, up: bool = True) -> None: super()._scroll(up) @@ -284,15 +291,29 @@ class PromptWidget(_ScrollableWidget): class _StatusLine(_Widget): - def __init__(self, write: Callable, status_title: str, **kwargs) -> None: + def __init__(self, write: Callable, windows: list['Window'], **kwargs + ) -> None: super().__init__(**kwargs) + self.idx_focus = 0 + self._windows = windows self._write = write - self._status_title = status_title def _draw(self) -> None: - title_box = f'{self._status_title}]' - status_line = title_box + '=' * (self._sizes.x - len(title_box)) - self._write(status_line, self._sizes.y) + listed = [] + focused = None + for w in self._windows: + item = str(w.idx) + if (n := w.history.n_lines_unread): + item = f'({item}:{n})' + if w.idx == self.idx_focus: + focused = w + item = f'[{item}]' + listed += [item] + assert isinstance(focused, Window) + left = f'{focused.title})' + right = f'({" ".join(listed)}' + width_gap = max(1, (self._sizes.x - len(left) - len(right))) + self._write(left + '=' * width_gap + right, self._sizes.y) class Window: @@ -300,6 +321,7 @@ class Window: _y_status: int _drawable = False prompt: PromptWidget + _title = ':start' def __init__(self, idx: int, term: 'Terminal', **kwargs) -> None: super().__init__(**kwargs) @@ -307,48 +329,38 @@ class Window: 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() def taint(self) -> None: self.history.taint() - self._status_line.taint() self.prompt.taint() @property def tainted(self) -> bool: - return (self._status_line.tainted or self.history.tainted - or self.prompt.tainted) + return self.history.tainted or self.prompt.tainted def set_geometry(self) -> None: self._drawable = False if self._term.size.y < _MIN_HEIGHT or self._term.size.x < _MIN_WIDTH: - for widget in (self.history, self._status_line, self.prompt): + for widget in (self.history, 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)) self._drawable = True @property - def _title(self) -> str: - return ':start' - - @property - def status_title(self) -> str: + def title(self) -> str: 'Window title to display in status line.' - return f'{self.idx}) {self._title}' + return self._title 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]: + for widget in [w for w in (self.history, self.prompt) + if w.tainted]: widget.draw() elif self._term.size.x > 0: lines = [''] @@ -378,6 +390,8 @@ class BaseTui(QueueMixin): self._term = term self._window_idx = 0 self._windows: list[Window] = [] + self._status_line = _StatusLine(write=self._term.write, + windows=self._windows) self._new_window() self._set_screen() signal(SIGWINCH, lambda *_: self._set_screen()) @@ -393,20 +407,20 @@ class BaseTui(QueueMixin): msg = f'{str(datetime.now())[11:19]} {prefix} {msg}' for win in self._log_target_wins(**kwargs): win.history.append(msg) + if win != self.window: + self._status_line.taint() def _new_window(self, win_class=Window, **kwargs) -> Window: new_idx = len(self._windows) win = win_class(idx=new_idx, term=self._term, **kwargs) self._windows += [win] - # FIXME below code responsible for log messages to streams without - # window auto-switching into that newly created, which can be annoying - if new_idx != self._window_idx: - self._switch_window(new_idx) return win def redraw_affected(self) -> None: 'On focused window call .draw, then flush screen.' self.window.draw_tainted() + if self._status_line.tainted: + self._status_line.draw() self._term.flush() def _set_screen(self) -> None: @@ -414,6 +428,8 @@ class BaseTui(QueueMixin): self._term.calc_geometry() for window in self._windows: window.set_geometry() + self._status_line.set_geometry(_YX(self._term.size.y - 2, + self._term.size.x)) self.redraw_affected() @property @@ -423,7 +439,8 @@ class BaseTui(QueueMixin): def _switch_window(self, idx: int) -> None: self.window.taint() - self._window_idx = idx + self._status_line.idx_focus = self._window_idx = idx + self._status_line.taint() @property def _commands(self) -> dict[str, tuple[Callable[..., None | Optional[str]], @@ -529,7 +546,7 @@ class BaseTui(QueueMixin): 'List available windows.' self._log('windows available via /window:') for win in self._windows: - self._log(f' {win.status_title}') + self._log(f' {win.idx}) {win.title}') def cmd__quit(self) -> None: 'Trigger program exit.'