home · contact · privacy
Have WidgetAtom._make_x_scroll work StylingString rather than str.
authorChristian Heller <c.heller@plomlompom.de>
Mon, 1 Dec 2025 20:09:22 +0000 (21:09 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 1 Dec 2025 20:09:22 +0000 (21:09 +0100)
src/ircplom/tui_base.py

index f2cee7bf2558e6076dfd9d847f4fd0a0460af8e2..ebd68c5d12a41cf9d556cf4596b0ec473780187c 100644 (file)
@@ -75,29 +75,15 @@ class StylingString:
                                                     if self._is_brace(c) else c
                                                     for c in text])
 
-    def __add__(self, other: Self) -> Self:
+    def __len__(self) -> int:
+        return len(self.stripped())
+
+    def __add__(self, other: Self | str) -> Self:
         return self.__class__(str(self) + str(other), store_raw=True)
 
     def __eq__(self, other) -> bool:
         return isinstance(other, self.__class__) and self._str == other._str
 
-    def __str__(self) -> str:
-        return self._str
-
-    def attrd(self, *attrs) -> Self:
-        'Return variant wrapped in mark-up for attrs.'
-        return self.__class__(''.join((self._BRACE_IN, ','.join(list(attrs)),
-                                       self._SEP, self._str, self._BRACE_OUT)),
-                              store_raw=True)
-
-    @classmethod
-    def _is_brace(cls, c: str) -> bool:
-        return c in {cls._BRACE_IN, cls._BRACE_OUT}
-
-    def stripped(self) -> str:
-        'Return without mark-up.'
-        return ''.join([text for _, text in self.parts_w_attrs()])
-
     @classmethod
     def _parse_for_markups(cls, char: str, in_tag: bool, markups: list[str]
                            ) -> tuple[bool, bool]:
@@ -122,8 +108,61 @@ class StylingString:
             do_print = True
         return do_print, in_tag
 
+    def __getitem__(self, idx: int | slice) -> Self:
+        if isinstance(idx, slice):
+            assert idx.step is None
+            start = idx.start or 0
+            stop = (len(self) + idx.stop
+                    if isinstance(idx.stop, int) and idx.stop < 0
+                    else idx.stop)
+        else:
+            start = idx
+            stop = idx + 1
+        raw_str = ''
+        idx_stripped = -1
+        in_tag = False
+        markups: list[str] = []
+        n_applied = 0
+        for char in self._str:
+            do_print, in_tag = self._parse_for_markups(char, in_tag, markups)
+            while len(markups) < n_applied:
+                raw_str += self._BRACE_OUT
+                n_applied -= 1
+            if not do_print:
+                continue
+            idx_stripped += 1
+            if idx_stripped == stop:
+                break
+            if idx_stripped < start:
+                continue
+            if len(markups) > n_applied:
+                raw_str += self._BRACE_IN
+                while len(markups) > n_applied:
+                    raw_str += markups[n_applied]
+                    n_applied += 1
+                raw_str += self._SEP
+            raw_str += char
+        return self.__class__(raw_str, store_raw=True)
+
+    def __str__(self) -> str:
+        return self._str
+
+    def attrd(self, *attrs) -> Self:
+        'Return variant wrapped in mark-up for attrs.'
+        return self.__class__(''.join((self._BRACE_IN, ','.join(list(attrs)),
+                                       self._SEP, self._str, self._BRACE_OUT)),
+                              store_raw=True)
+
+    @classmethod
+    def _is_brace(cls, c: str) -> bool:
+        return c in {cls._BRACE_IN, cls._BRACE_OUT}
+
+    def stripped(self) -> str:
+        'Return without mark-up.'
+        return ''.join([text for _, text in self.parts_w_attrs()])
+
     def parts_w_attrs(self) -> tuple[tuple[tuple[str, ...], str], ...]:
-        'Break into individually formatted parts, with respective attributes.'
+        'Break into styled formatted parts, with respective attributes.'
         next_part = ''
         markups: list[str] = []
         in_tag = False
@@ -213,10 +252,14 @@ class _Widget(ABC):
 class _WidgetAtom(_Widget, ABC):
     _tainted: bool = True
 
-    def _make_x_scroll(self, padchar: str, cursor_x: int, left: str, right: str
-                       ) -> tuple[int, str]:
+    def _make_x_scroll(self,
+                       padchar: str,
+                       cursor_x: int,
+                       left: StylingString,
+                       right: StylingString
+                       ) -> StylingString:
         'Build line of static left, right scrolled with cursor_x if too large.'
-        to_write = left[:]
+        to_write = left
         offset = 0
         width_gap = self._size.x - len(left + right)
         if width_gap < 0:
@@ -229,9 +272,10 @@ class _WidgetAtom(_Widget, ABC):
                 to_write = to_write[:self._size.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
+                         else (StylingString(padchar * width_gap) + right
+                               if padchar == '='
+                               else right + padchar * width_gap))
+        return to_write
 
     def set_geometry(self, size: _YX) -> None:
         self.tainted = True
@@ -482,17 +526,19 @@ class PromptWidget(_ScrollableWidget):
         self._input_buffer_unsafe = content[:]
 
     def _draw(self) -> None:
-        content = self.input_buffer
         if self._cursor_x == len(self.input_buffer):
-            content += ' '
-        x_cursor, to_write = self._make_x_scroll(padchar=' ',
-                                                 cursor_x=self._cursor_x,
-                                                 left=self.prefix[:],
-                                                 right=content)
+            content = StylingString(self.input_buffer)
+            content += StylingString(' ').attrd('reverse')
+        else:
+            content = StylingString(self.input_buffer[:self._cursor_x])
+            content += StylingString(self.input_buffer[self._cursor_x]
+                                     ).attrd('reverse')
+            content += StylingString(self.input_buffer[self._cursor_x + 1:])
         self._write(self._size.y,
-                    StylingString(to_write[:x_cursor])
-                    + StylingString(to_write[x_cursor]).attrd('reverse')
-                    + StylingString(to_write[x_cursor + 1:]))
+                    self._make_x_scroll(padchar=' ',
+                                        cursor_x=self._cursor_x,
+                                        left=StylingString(self.prefix),
+                                        right=content))
 
     def _archive_prompt(self) -> None:
         self.append(self.input_buffer)
@@ -566,7 +612,7 @@ class _StatusLine(_WidgetAtom):
     def _draw(self) -> None:
         cursor_x = 0
         focus = None
-        win_listing = ''
+        win_listing = StylingString('')
         for w in self._windows:
             win_listing += ' ' if w.idx > 0 else '('
             item = str(w.idx)
@@ -580,10 +626,11 @@ class _StatusLine(_WidgetAtom):
                 cursor_x = len(win_listing) + (len(item) // 2)
             win_listing += item
         assert isinstance(focus, Window)
-        self._write(self._size.y, self._make_x_scroll(padchar='=',
-                                                      cursor_x=cursor_x,
-                                                      left=focus.title + ')',
-                                                      right=win_listing)[1])
+        self._write(self._size.y,
+                    self._make_x_scroll(padchar='=',
+                                        cursor_x=cursor_x,
+                                        left=StylingString(focus.title + ')'),
+                                        right=win_listing))
 
 
 class Window(_Widget):