home · contact · privacy
Move StatusLine out of Window, show live updates on other windows' activity, drop...
authorChristian Heller <c.heller@plomlompom.de>
Mon, 11 Aug 2025 07:57:28 +0000 (09:57 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 11 Aug 2025 07:57:28 +0000 (09:57 +0200)
ircplom/client_tui.py
ircplom/tui_base.py

index 94cd62fc28341e0f6a0f18a48c2d8de8e2058aef..d9bd76f00bc8be3867eac501960863585f68f0f3 100644 (file)
@@ -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.'
index 415bf7b0079a8fef24db3b2123a8ff2ed7f3d202..13e002bc87d7ccf755c91ca6edc3110e6576f83b 100644 (file)
@@ -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.'