home · contact · privacy
To avoid crashes on too long status line window lists, turn those into horizontal...
authorChristian Heller <c.heller@plomlompom.de>
Wed, 26 Nov 2025 05:26:32 +0000 (06:26 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 26 Nov 2025 05:26:32 +0000 (06:26 +0100)
src/ircplom/tui_base.py
src/tests/lib/connect-to-connected
src/tests/test.test
src/tests/tui_status_line_scrolling.test [new file with mode: 0644]

index 8a9f6e5fae7644855cb29cdf162dc9dc63553c8b..cfd2e612f080bc9837ea9e9c42370ab53748f9bb 100644 (file)
@@ -31,8 +31,8 @@ _OSC52_PREFIX = b']52;c;'
 _PASTE_DELIMITER = '\007'
 
 _PROMPT_TEMPLATE = '> '
-_PROMPT_ELL_IN = '<…'
-_PROMPT_ELL_OUT = '…>'
+_ELL_IN = '<…'
+_ELL_OUT = '…>'
 
 _CHAR_RESIZE = chr(12)
 _KEYBINDINGS = {
@@ -204,6 +204,26 @@ class _Widget(ABC):
         self.taint()
         self._sizes = sizes
 
+    def make_x_scroll(self, padchar: str, cursor_x: int, left: str, right: str
+                      ) -> tuple[int, str]:
+        'Build line of static left, right scrolled with cursor_x if too large.'
+        to_write = left
+        offset = 0
+        width_gap = self._sizes.x - len(left) - len(right)
+        if width_gap < 0:
+            half_width = (self._sizes.x - len(left)) // 2
+            if cursor_x > half_width:
+                to_write += _ELL_IN
+                offset = len(_ELL_IN) + min(-width_gap, cursor_x - half_width)
+            to_write += right[offset:]
+            if len(to_write) > self._sizes.x:
+                to_write = to_write[:self._sizes.x - len(_ELL_OUT)] + _ELL_OUT
+        else:
+            to_write += (right if width_gap == 0
+                         else ((padchar * width_gap + right) if padchar == '='
+                               else (right + padchar * width_gap)))
+        return len(left) - offset + cursor_x, to_write
+
     def draw(self) -> None:
         'Print widget\'s content in shape appropriate to set geometry.'
         if self._drawable:
@@ -419,25 +439,14 @@ class PromptWidget(_ScrollableWidget):
         content = self.input_buffer
         if self._cursor_x == len(self.input_buffer):
             content += ' '
-        half_width = (self._sizes.x - len(prefix)) // 2
-        offset = 0
-        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._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._sizes.x:
-            to_write = (to_write[:self._sizes.x-len(_PROMPT_ELL_OUT)]
-                        + _PROMPT_ELL_OUT)
-        if len(to_write) < self._sizes.x:
-            to_write += ' '
+        x_cursor, to_write = self.make_x_scroll(padchar=' ',
+                                                cursor_x=self._cursor_x,
+                                                left=prefix,
+                                                right=content)
         self._write(self._sizes.y,
-                    FormattingString(to_write[:cursor_x_to_write])
-                    + FormattingString(to_write[cursor_x_to_write]
-                                       ).attrd('reverse')
-                    + FormattingString(to_write[cursor_x_to_write + 1:]))
+                    FormattingString(to_write[:x_cursor])
+                    + FormattingString(to_write[x_cursor]).attrd('reverse')
+                    + FormattingString(to_write[x_cursor + 1:]))
 
     def _archive_prompt(self) -> None:
         self.append(self.input_buffer)
@@ -509,9 +518,11 @@ class _StatusLine(_Widget):
         self._write = write
 
     def _draw(self) -> None:
-        listed = []
+        cursor_x = 0
         focused = None
+        win_listing = ''
         for w in self._windows:
+            win_listing += ' ' if w.idx > 0 else '('
             item = str(w.idx)
             if (n := w.history.n_lines_unread):
                 item = f'({item}:{n})'
@@ -520,12 +531,13 @@ class _StatusLine(_Widget):
             if w.idx == self.idx_focus:
                 focused = w
                 item = f'[{item}]'
-            listed += [item]
+                cursor_x = len(win_listing) + (len(item) // 2)
+            win_listing += 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(self._sizes.y, left + '=' * width_gap + right)
+        self._write(self._sizes.y, self.make_x_scroll(padchar='=',
+                                                      cursor_x=cursor_x,
+                                                      left=focused.title + ')',
+                                                      right=win_listing)[1])
 
 
 class Window:
index cf45c6d009007cefb4e099c80335145fe42acbbe..3dc0eeec3cf94ffc6bf15ece5eee956e4128cc09 100644 (file)
@@ -4,5 +4,5 @@ insert ./lib/conn
 # for: connect
 
 × connect-to-connected
-insert connect : +1
-insert attempting-to-connected : +1 WIN_IDS :2
+insert connect
+insert attempting-to-connected : + WIN_IDS :2
index d14e8a1bb29919579785c41e900fe55526750451..88ad5cc72ebc7f0f390a01100b1458806cb74a7b 100644 (file)
@@ -242,17 +242,6 @@ insert isupport-clear : +8
 insert disconnect1 1:-1 +8 WIN_IDS :2
 log 8 $ nickname refused for bad format, giving up
 
-# zero unread-lines counts in status line so it won't explode simulated screen
-> /window 2
-> /window 3
-> /window 4
-> /window 5
-> /window 6
-> /window 7
-> /window 8
-> /window 9
-> /window 1
-
 # test failing third connection
 insert connect : +10 foo.bar.baz :baz.baz.baz
 insert attempting-to-connected : +10 WIN_IDS=2 foo.bar.baz :baz.baz.baz
@@ -263,20 +252,8 @@ log 10 $ will retry connecting in 1 seconds
 > /window 10
 insert disconnect-to-stop-auto-reconnect : +10
 
-# zero unread-lines counts in status line so it won't explode simulated screen
-> /window 2
-> /window 3
-> /window 4
-> /window 5
-> /window 6
-> /window 7
-> /window 8
-> /window 9
-> /window 10
-> /window 11
-> /window 1
-
 # check that (save TUI tests assuming start on window 0, and no 4 yet) on reconnect, all the same effects can be expected
+> /window 1
 > /reconnect
 insert attempting :-1
 log 2 $ - password: bar
diff --git a/src/tests/tui_status_line_scrolling.test b/src/tests/tui_status_line_scrolling.test
new file mode 100644 (file)
index 0000000..96064e6
--- /dev/null
@@ -0,0 +1,62 @@
+insert ./lib/connect-to-connected
+insert ./lib/caps-neg-empty
+insert ./lib/001-to-usermode
+
+× new-hi
+insert servermsglogged : +0 MSG ::winWIN_ID!~winWIN_ID@bar.bar PRIVMSG foo :hi there
+log WIN_ID < [winWIN_ID] hi there
+
+× ×---------------------------------
+
+insert connect-to-connected
+insert caps-neg-empty
+insert 001-to-usermode
+insert new-hi : + WIN_ID :3
+insert new-hi : + WIN_ID :4
+insert new-hi : + WIN_ID :5
+insert new-hi : + WIN_ID :6
+insert new-hi : + WIN_ID :7
+insert new-hi : + WIN_ID :8
+insert new-hi : + WIN_ID :9
+insert new-hi : + WIN_ID :10
+line 22 on_black,bright_white :start)=======([0] (1:32) (2:7) (3:2) (4:2) (5:2) (6:2) (7:2) (8:2) (9:2) (10:2)§§
+
+# grow windows list to maximum before ellipsis necessary
+insert new-hi : + WIN_ID :11
+line 22 on_black,bright_white :start)([0] (1:33) (2:7) (3:2) (4:2) (5:2) (6:2) (7:2) (8:2) (9:2) (10:2) (11:2)§§
+
+# grow list beyond, with focus on left force ellipsis to the right
+insert new-hi : + WIN_ID :12
+line 22 on_black,bright_white :start)([0] (1:34) (2:7) (3:2) (4:2) (5:2) (6:2) (7:2) (8:2) (9:2) (10:2) (11:…>§§
+
+# shrink (uncut) listing but grow title to the left more, forcing ellipsis still to cut earlier
+> /window 1
+line 22 on_black,bright_white foo.bar.baz:debug)(0 [1] (2:7) (3:2) (4:2) (5:2) (6:2) (7:2) (8:2) (9:2) (10:2…>§§
+
+# further shrink uncut listing until ellipsis gone again; with focus not moving beyond left half, don't scroll yet
+> /window 3
+line 22 on_black,bright_white foo.bar.baz/win3)(0 1 (2:7) [3] (4:2) (5:2) (6:2) (7:2) (8:2) (9:2) (10:2) (11…>§§
+> /window 4
+line 22 on_black,bright_white foo.bar.baz/win4)(0 1 (2:7) 3 [4] (5:2) (6:2) (7:2) (8:2) (9:2) (10:2) (11:2) …>§§
+> /window 5
+line 22 on_black,bright_white foo.bar.baz/win5)(0 1 (2:7) 3 4 [5] (6:2) (7:2) (8:2) (9:2) (10:2) (11:2) (12:2)§§
+
+# grow uncut listing again, re-establishing ellipsis to the right
+insert new-hi : + WIN_ID :13
+line 22 on_black,bright_white foo.bar.baz/win5)(0 (1:1) (2:7) 3 4 [5] (6:2) (7:2) (8:2) (9:2) (10:2) (11:2) …>§§
+
+# move focus into middle of listing, scrolling so that ellipsis on both side
+> /window 8
+line 22 on_black,bright_white foo.bar.baz/win8)<…:1) (2:7) 3 4 5 (6:2) (7:2) [8] (9:2) (10:2) (11:2) (12:2) …>§§
+
+# move focus further to the right, so that only ellipsis on the left
+> /window 9
+line 22 on_black,bright_white foo.bar.baz/win9)<…:1) (2:7) 3 4 5 (6:2) (7:2) 8 [9] (10:2) (11:2) (12:2) (13:2)§§
+
+# shrink uncut listing to return to full view again (no ellipses) 
+> /window 13
+line 22 on_black,bright_white foo.bar.baz/win13)(0 (1:1) (2:7) 3 4 5 (6:2) (7:2) 8 9 (10:2) (11:2) (12:2) [13]§§
+
+# add new window, re-establishing ellipsis to the left, with focus remaining quite to the right
+insert new-hi : + WIN_ID :14
+line 22 on_black,bright_white foo.bar.baz/win13)<…(2:7) 3 4 5 (6:2) (7:2) 8 9 (10:2) (11:2) (12:2) [13] (14:2)§§