From: Christian Heller Date: Sat, 29 Nov 2025 16:37:48 +0000 (+0100) Subject: Clean up TUI widgets code, especially that of HistoryWidget. X-Git-Url: https://plomlompom.com/repos/%22https:/validator.w3.org/calendar?a=commitdiff_plain;p=ircplom Clean up TUI widgets code, especially that of HistoryWidget. --- diff --git a/src/ircplom/client_tui.py b/src/ircplom/client_tui.py index 4080f2b..d7da1c5 100644 --- a/src/ircplom/client_tui.py +++ b/src/ircplom/client_tui.py @@ -138,7 +138,7 @@ class _ChatPrompt(PromptWidget): def set_prefix_data(self, nick: str) -> None: 'Update prompt prefix with nickname data.' if nick != self._nickname: - self._tainted = True + self.tainted = True self._nickname = nick def enter(self) -> str: diff --git a/src/ircplom/testing.py b/src/ircplom/testing.py index 0199e52..e6f6d35 100644 --- a/src/ircplom/testing.py +++ b/src/ircplom/testing.py @@ -70,12 +70,12 @@ class TestTerminal(QueueMixin, TerminalInterface): for _ in range(self.size.x): line += [(tuple(), ' ')] - def length_to_term(self, text: str) -> int: + def len_to_term(self, text: str) -> int: return len(text) + len(tuple(c for c in text if c == '💓')) def _write_w_attrs(self, text: str, attrs: tuple[str, ...]) -> None: for c in text: - len_to_term = self.length_to_term(c) + len_to_term = self.len_to_term(c) for i in range(len_to_term): self._screen[self._cursor_y][self._cursor_x+i]\ = None if i else (attrs, c) @@ -100,7 +100,7 @@ class TestTerminal(QueueMixin, TerminalInterface): assert 0 <= y < self.size.y if text.endswith(_SCREENLINE_PADDING_SUFFIX): text = text[:-len(_SCREENLINE_PADDING_SUFFIX)] - text += ' ' * (self.size.x - self.length_to_term(text)) + text += ' ' * (self.size.x - self.len_to_term(text)) jumped_nones = 0 for idx, cell_expected in enumerate( (tuple(attrs_str.split(_SEP_1)), c) for c in text): diff --git a/src/ircplom/tui_base.py b/src/ircplom/tui_base.py index 2181a8b..4637017 100644 --- a/src/ircplom/tui_base.py +++ b/src/ircplom/tui_base.py @@ -139,7 +139,10 @@ class FormattingString: next_part += c return tuple(to_ret) - def wrap(self, width: int, length_to_term: Callable) -> tuple[Self, ...]: + def wrap(self, + width: int, + len_to_term: Callable[[str], int] + ) -> tuple[Self, ...]: 'Break into sequence respecting width, preserving attributes per part.' wrapped_lines: list[str] = [] next_part_w_code = '' @@ -156,7 +159,7 @@ class FormattingString: = self._parse_for_formattings(c, in_format_def, formattings) if do_not_print: continue - len_flattened += length_to_term(c) + len_flattened += len_to_term(c) if len_flattened <= width: continue walk_back = ([-(i+1) for i in range(len(next_part_w_code) @@ -182,60 +185,73 @@ class _YX(NamedTuple): x: int -class _Widget(ABC): - _tainted: bool = True - _sizes = _YX(-1, -1) +_SIZE_UNSET = _YX(-1, -1) - @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.' - self._tainted = True +class _Widget(ABC): + _size = _SIZE_UNSET @property def tainted(self) -> bool: 'If in need of re-drawing.' return self._tainted - def set_geometry(self, sizes: _YX) -> None: - 'Update widget\'s sizues, re-generate content where necessary.' - self.taint() - self._sizes = sizes + @tainted.setter + def tainted(self, value: bool) -> None: + 'Declare need of re-drawing.' + self._tainted = value + + @property + def _drawable(self) -> bool: + return len([length for length in self._size if length < 1]) == 0 + + @abstractmethod + def set_geometry(self, size: _YX) -> None: + 'Update widget\'s sizes, re-generate content where necessary.' + + @abstractmethod + def draw_tainted(self) -> None: + "If .tainted, print widget's content in .set_geometry()-defined shape." + + +class _WidgetAtom(_Widget, ABC): + _tainted: bool = True 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 + right) + width_gap = self._size.x - len(left + right) if width_gap < 0: - half_width = (self._sizes.x - len(left)) // 2 + half_width = (self._size.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 + if len(to_write) > self._size.x: + 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 - def draw(self) -> None: - 'Print widget\'s content in shape appropriate to set geometry.' - if self._drawable: + def set_geometry(self, size: _YX) -> None: + self.tainted = True + self._size = size + + def draw_tainted(self) -> None: + if self._drawable and self.tainted: self._draw() - self._tainted = False + self.tainted = False @abstractmethod def _draw(self) -> None: pass -class _ScrollableWidget(_Widget): +class _ScrollableWidget(_WidgetAtom): _history_idx_neg: int def __init__(self, write: Callable[..., None], **kwargs) -> None: @@ -249,137 +265,160 @@ class _ScrollableWidget(_Widget): @abstractmethod def _scroll(self, up=True) -> None: - self.taint() + self.tainted = True def cmd__scroll(self, direction: str) -> None: 'Scroll through stored content/history.' self._scroll(up=direction == 'up') +class _WrappedHistoryLine(NamedTuple): + history_idx_pos: int + text: FormattingString + + class _HistoryWidget(_ScrollableWidget): - _wrapped_idx_neg: int + _wrapped_idx_neg: int # negative index of lowest visible wrapped line _y_pgscroll: int _UNSET_IDX_NEG: int = 0 _UNSET_IDX_POS: int = -1 + _BOTTOM_IDX_NEG: int = -1 _COMPARAND_HISTORY_IDX_POS: int = -2 _BOOKMARK_HISTORY_IDX_POS: int = -3 _PADDING_HISTORY_IDX_POS: int = -4 def __init__(self, maxlen_log: int, - length_to_term: Callable[[str], int], + len_to_term: Callable[[str], int], **kwargs ) -> None: super().__init__(**kwargs) self._maxlen_log = maxlen_log - self._length_to_term = length_to_term - self._wrapped: list[tuple[int, FormattingString]] = [] - self._newest_read_history_idx_pos = self._UNSET_IDX_POS - self._history_offset = 0 + self._len_to_term = len_to_term + self._wrapped: list[_WrappedHistoryLine] = [] + self._lowest_read_history_idx_pos = self._UNSET_IDX_POS + self._bookmark_idx_neg = self._UNSET_IDX_NEG + self._history_n_lines_cut = 0 self._history_idx_neg = self._UNSET_IDX_NEG - def _add_wrapped(self, history_idx_pos: int, line: FormattingString - ) -> int: - lines = line.wrap(self._sizes.x, self._length_to_term) - self._wrapped += [(self._history_offset + history_idx_pos, line) - for line in lines] + def _add_wrapped_from_offset_history(self, + history_idx_pos: int, + line: FormattingString + ) -> int: + lines = [_WrappedHistoryLine( + self._history_n_lines_cut + history_idx_pos, + text) + for text in line.wrap(self._size.x, self._len_to_term)] + self._wrapped += lines return len(lines) @property def _len_full_history(self) -> int: - return self._history_offset + len(self._history) + return self._history_n_lines_cut + len(self._history) - def set_geometry(self, sizes: _YX) -> None: - super().set_geometry(sizes) + def set_geometry(self, size: _YX) -> None: + super().set_geometry(size) if self._drawable: - self._y_pgscroll = self._sizes.y // 2 + self._y_pgscroll = self._size.y // 2 self._wrapped.clear() for history_idx_pos, line in enumerate(self._history): - self._add_wrapped(history_idx_pos, - FormattingString(line, raw=True)) + self._add_wrapped_from_offset_history(history_idx_pos, + FormattingString( + line, raw=True)) # ensure that of the full line identified by ._history_idx_neg, # ._wrapped_idx_neg point to the lowest of its wrap parts self._wrapped_idx_neg = ( self._UNSET_IDX_NEG if (not self._wrapped) - else (-len(self._wrapped) - + self._last_wrapped_idx_pos_for_hist_idx_pos( + else (self._wrapped_top_idx_neg + + self._bottom_wrapped_for_history_idx_pos( self._len_full_history + max(self._history_idx_neg, - self._maxlen_log)))) self.bookmark() def append_fmt(self, to_append: FormattingString) -> None: 'Wrap .append around FormattingString, update history dependents.' + def start_unset_dec_non_bottom(attr_name: str, grow_by: int) -> None: + full_name = f'_{attr_name}_idx_neg' + cur_val = getattr(self, full_name) + if cur_val == self._UNSET_IDX_NEG: + setattr(self, full_name, self._BOTTOM_IDX_NEG) + elif cur_val < self._BOTTOM_IDX_NEG: + setattr(self, full_name, cur_val - grow_by) + super().append(str(to_append)) - self.taint() - if not self._UNSET_IDX_NEG != self._history_idx_neg >= -1: - self._history_idx_neg -= 1 + self.tainted = True + start_unset_dec_non_bottom('history', grow_by=1) if self._drawable: - n_wrapped = self._add_wrapped(len(self._history) - 1, to_append) - if not self._UNSET_IDX_NEG != self._wrapped_idx_neg >= -1: - self._wrapped_idx_neg -= n_wrapped + wrapped_growth\ + = self._add_wrapped_from_offset_history(len(self._history) - 1, + to_append) + start_unset_dec_non_bottom('wrapped', grow_by=wrapped_growth) + if self._bookmark_idx_neg != self._UNSET_IDX_NEG: + self._bookmark_idx_neg -= wrapped_growth if len(self._history) > self._maxlen_log: - self._history = self._history[1:] - self._history_offset += 1 + self._history.pop(0) + self._history_n_lines_cut += 1 self._history_idx_neg = max(self._history_idx_neg, -self._maxlen_log) - wrap_offset = 0 - for wrap_idx_pos, t in enumerate(self._wrapped): - if t[0] == self._history_offset: - wrap_offset = wrap_idx_pos - break - self._wrapped = self._wrapped[wrap_offset:] + while self._wrapped[0].history_idx_pos < self._history_n_lines_cut: + self._wrapped.pop(0) self._wrapped_idx_neg = max(self._wrapped_idx_neg, - -len(self._wrapped)) + self._wrapped_top_idx_neg) def _draw(self) -> None: add_scroll_info = self._wrapped_idx_neg < -1 - start_idx_neg = (self._wrapped_idx_neg - - self._sizes.y + 1 + bool(add_scroll_info)) - end_idx_neg = (self._wrapped_idx_neg + 1) if add_scroll_info else None - wrapped = self._wrapped[start_idx_neg:end_idx_neg] - while len(wrapped) < self._sizes.y - bool(add_scroll_info): - wrapped.insert(0, (self._PADDING_HISTORY_IDX_POS, - FormattingString(''))) - for idx, line in enumerate([lt[1] for lt in wrapped]): + low_idx_neg = (self._wrapped_idx_neg + 1) if add_scroll_info else None + high_idx_neg = (low_idx_neg or -1) - self._size.y + 1 + visible_lines = self._wrapped[high_idx_neg:low_idx_neg] + while len(visible_lines) < self._size.y - bool(add_scroll_info): + visible_lines.insert(0, _WrappedHistoryLine( + self._PADDING_HISTORY_IDX_POS, + FormattingString(''))) + for idx, line in enumerate([line.text for line in visible_lines]): self._write(idx, line) if add_scroll_info: scroll_info = f'vvv [{(-1) * self._history_idx_neg - 1}] ' - scroll_info += 'v' * (self._sizes.x - len(scroll_info)) - self._write(len(wrapped), + scroll_info += 'v' * (self._size.x - len(scroll_info)) + self._write(len(visible_lines), FormattingString(scroll_info).attrd('reverse')) - self._newest_read_history_idx_pos\ - = max(self._newest_read_history_idx_pos, wrapped[-1][0]) + self._lowest_read_history_idx_pos\ + = max(self._lowest_read_history_idx_pos, + visible_lines[-1].history_idx_pos) def bookmark(self) -> None: 'Store next idx to what most recent line we have (been) scrolled.' - bookmark = (self._BOOKMARK_HISTORY_IDX_POS, - FormattingString('-' * self._sizes.x)) - if bookmark in self._wrapped: - bookmark_idx_neg\ - = self._wrapped.index(bookmark) - len(self._wrapped) - del self._wrapped[bookmark_idx_neg] - if bookmark_idx_neg > self._wrapped_idx_neg: + if self._bookmark_idx_neg != self._UNSET_IDX_NEG\ + and len(self._wrapped) > -self._bookmark_idx_neg: + del self._wrapped[self._bookmark_idx_neg] + if self._bookmark_idx_neg > self._wrapped_idx_neg: self._wrapped_idx_neg += 1 - if self._newest_read_history_idx_pos < self._history_offset: + self._bookmark_idx_neg = self._UNSET_IDX_NEG + if self._lowest_read_history_idx_pos < self._history_n_lines_cut: return if not self._wrapped: return - self._wrapped.insert(self._bookmark_wrapped_idx_pos, bookmark) - self._wrapped_idx_neg -= int(self._bookmark_wrapped_idx_neg - > self._wrapped_idx_neg) - - @property - def _bookmark_wrapped_idx_pos(self) -> int: - return self._last_wrapped_idx_pos_for_hist_idx_pos( - self._newest_read_history_idx_pos) + 1 + bookmark = _WrappedHistoryLine(self._BOOKMARK_HISTORY_IDX_POS, + FormattingString('-' * self._size.x)) + lowest_read_wrapped_idx_neg\ + = (self._wrapped_top_idx_neg + + self._bottom_wrapped_for_history_idx_pos( + self._lowest_read_history_idx_pos)) + if lowest_read_wrapped_idx_neg == self._BOTTOM_IDX_NEG: + self._bookmark_idx_neg = self._BOTTOM_IDX_NEG + self._wrapped += [bookmark] + else: + self._bookmark_idx_neg = lowest_read_wrapped_idx_neg + self._wrapped.insert(self._bookmark_idx_neg + 1, bookmark) + if self._bookmark_idx_neg > self._wrapped_idx_neg + 1: + self._wrapped_idx_neg -= 1 @property - def _bookmark_wrapped_idx_neg(self) -> int: - return self._bookmark_wrapped_idx_pos - len(self._wrapped) + def _wrapped_top_idx_neg(self) -> int: + return -len(self._wrapped) - def _last_wrapped_idx_pos_for_hist_idx_pos(self, hist_idx_pos: int) -> int: - return [idx for idx, t in enumerate(self._wrapped) - if t[0] == hist_idx_pos][-1] + def _bottom_wrapped_for_history_idx_pos(self, hist_idx_pos: int) -> int: + return [idx for idx, line in enumerate(self._wrapped) + if line.history_idx_pos == hist_idx_pos][-1] @property def has_unread_highlight(self) -> bool: @@ -390,23 +429,23 @@ class _HistoryWidget(_ScrollableWidget): @property def n_lines_unread(self) -> int: 'How many new lines have been logged since last focus.' - return (self._len_full_history - - (self._newest_read_history_idx_pos + 1)) + return self._len_full_history - self._lowest_read_history_idx_pos - 1 def _scroll(self, up: bool = True) -> None: super()._scroll(up) if self._drawable and self._wrapped: if up and len(self._wrapped) > 2: self._wrapped_idx_neg = max( - -len(self._wrapped), + self._wrapped_top_idx_neg, self._wrapped_idx_neg - self._y_pgscroll) else: self._wrapped_idx_neg = min( - -1, self._wrapped_idx_neg + self._y_pgscroll) - idx = self._wrapped_idx_neg - int( - self._wrapped_idx_neg == self._bookmark_wrapped_idx_neg) - self._history_idx_neg = (-self._len_full_history - + max(0, self._wrapped[idx][0])) + self._BOTTOM_IDX_NEG, + self._wrapped_idx_neg + self._y_pgscroll) + idx = self._wrapped_idx_neg - int(self._wrapped_idx_neg + == self._bookmark_idx_neg) + self._history_idx_neg = (max(0, self._wrapped[idx].history_idx_pos) + - self._len_full_history) class PromptWidget(_ScrollableWidget): @@ -431,7 +470,7 @@ class PromptWidget(_ScrollableWidget): @input_buffer.setter def input_buffer(self, content) -> None: - self.taint() + self.tainted = True self._input_buffer_unsafe = content[:] def _draw(self) -> None: @@ -442,7 +481,7 @@ class PromptWidget(_ScrollableWidget): cursor_x=self._cursor_x, left=self.prefix[:], right=content) - self._write(self._sizes.y, + self._write(self._size.y, FormattingString(to_write[:x_cursor]) + FormattingString(to_write[x_cursor]).attrd('reverse') + FormattingString(to_write[x_cursor + 1:])) @@ -493,7 +532,7 @@ class PromptWidget(_ScrollableWidget): self._cursor_x += 1 else: return - self.taint() + self.tainted = True def _reset_buffer(self, content: str) -> None: self.input_buffer = content @@ -507,7 +546,7 @@ class PromptWidget(_ScrollableWidget): return to_return -class _StatusLine(_Widget): +class _StatusLine(_WidgetAtom): def __init__(self, write: Callable, windows: list['Window'], **kwargs ) -> None: @@ -533,31 +572,33 @@ class _StatusLine(_Widget): cursor_x = len(win_listing) + (len(item) // 2) win_listing += item assert isinstance(focus, Window) - self._write(self._sizes.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=focus.title + ')', + right=win_listing)[1]) -class Window: +class Window(_Widget): 'Collection of widgets filling entire screen.' _y_status: int - _drawable = False prompt: PromptWidget _title = ':start' _last_today = '' - def __init__(self, idx: int, term: 'Terminal', maxlen_log: int, **kwargs + def __init__(self, + idx: int, + write: Callable[[int, str | FormattingString], None], + len_to_term: Callable[[str], int], + maxlen_log: int, + **kwargs ) -> None: super().__init__(**kwargs) self.idx = idx - self._term = term + self._write = write self.history = _HistoryWidget(maxlen_log=maxlen_log, - length_to_term=self._term.length_to_term, - write=self._term.write) - self.prompt = self.__annotations__['prompt'](write=self._term.write) - if hasattr(self._term, 'size'): - self.set_geometry() + len_to_term=len_to_term, + write=self._write) + self.prompt = self.__annotations__['prompt'](write=self._write) def ensure_date(self, today: str) -> None: 'Log date of today if it has not been logged yet.' @@ -569,27 +610,25 @@ class Window: 'Append msg to .history.' self.history.append_fmt(msg) - def taint(self) -> None: - 'Declare all widgets as in need of re-drawing.' - self.history.taint() - self.prompt.taint() - @property def tainted(self) -> bool: - 'If any widget in need of re-drawing.' return self.history.tainted or self.prompt.tainted - def set_geometry(self) -> None: - 'Set geometry for widgets.' - self._drawable = False - if self._term.size.y < _MIN_HEIGHT or self._term.size.x < _MIN_WIDTH: + @tainted.setter + def tainted(self, value: bool) -> None: + self.history.tainted = value + self.prompt.tainted = value + + def set_geometry(self, size: _YX) -> None: + self._size = _SIZE_UNSET + if size.y < _MIN_HEIGHT or size.x < _MIN_WIDTH: for widget in (self.history, self.prompt): - widget.set_geometry(_YX(-1, -1)) + widget.set_geometry(_SIZE_UNSET) return - self._y_status = self._term.size.y - 2 - self.history.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 + self._size = size + self._y_status = self._size.y - 2 + self.history.set_geometry(_YX(self._y_status, self._size.x)) + self.prompt.set_geometry(_YX(self._size.y - 1, self._size.x)) @property def title(self) -> str: @@ -597,24 +636,25 @@ class Window: return self._title def draw_tainted(self) -> None: - 'Draw tainted widgets (or message that screen too small).' if self._drawable: - for widget in [w for w in (self.history, self.prompt) - 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(y, line) + for widget in (self.history, self.prompt): + widget.draw_tainted() + return + msg = 'screen too small' + if self._size.x * self._size.y >= len(msg): + y = 0 + line = '' + for c in msg: + line += c + if len(line) == self._size.x: + self._write(y, line) + y += 1 + line = '' def cmd__paste(self) -> None: 'Write OSC 52 ? sequence to get encoded clipboard paste into stdin.' - self._term.write(0, f'\033{_OSC52_PREFIX.decode()}?{_PASTE_DELIMITER}') - self.taint() + self._write(0, f'\033{_OSC52_PREFIX.decode()}?{_PASTE_DELIMITER}') + self.tainted = True class TuiEvent(AffectiveEvent): @@ -643,7 +683,7 @@ class TerminalInterface(ABC): 'Flush terminal.' @abstractmethod - def length_to_term(self, text: str) -> int: + def len_to_term(self, text: str) -> int: 'How many cells of terminal screen text width will demand.' @abstractmethod @@ -658,7 +698,7 @@ class TerminalInterface(ABC): attrs: tuple[str, ...] = tuple() len_written = 0 for attrs, part in text.parts_w_attrs(): - len_written += self.length_to_term(part) + len_written += self.len_to_term(part) self._write_w_attrs(part, attrs) self._write_w_attrs(' ' * (self.size.x - len_written), tuple(attrs)) @@ -710,30 +750,34 @@ class BaseTui(QueueMixin): affected_win_indices += [win.idx] win.ensure_date(today) win.log(msg) - self._status_line.taint() + self._status_line.tainted = True return tuple(affected_win_indices), msg.stripped() def _new_window(self, win_class=Window, **kwargs) -> Window: new_idx = len(self._windows) - win = win_class(idx=new_idx, term=self._term, - maxlen_log=self._MAXLEN_LOG, **kwargs) + win = win_class(idx=new_idx, + write=self._term.write, + len_to_term=self._term.len_to_term, + maxlen_log=self._MAXLEN_LOG, + **kwargs) + if hasattr(self._term, 'size'): + win.set_geometry(self._term.size) self._windows += [win] return win def redraw_affected(self) -> None: - 'On focused window call .draw, then flush screen.' + 'On focused window and status line, call .draw_tainted, then flush.' self.window.draw_tainted() - if self._status_line.tainted: - self._status_line.draw() + self._status_line.draw_tainted() self._term.flush() def _set_screen(self) -> None: 'Hide cursor, calc screen geometry into wins, call .redraw_affected.' self._term.set_size_hide_cursor() for window in self._windows: - window.set_geometry() - self._status_line.set_geometry(_YX(self._term.size.y - 2, - self._term.size.x)) + window.set_geometry(self._term.size) + self._status_line.set_geometry( + self._term.size._replace(y=self._term.size.y - 2)) self.redraw_affected() @property @@ -742,10 +786,10 @@ class BaseTui(QueueMixin): return self._windows[self._window_idx] def _switch_window(self, idx: int) -> None: - self.window.taint() + self.window.tainted = True self.window.history.bookmark() self._status_line.idx_focus = self._window_idx = idx - self._status_line.taint() + self._status_line.tainted = True @property def _commands(self) -> dict[str, tuple[Callable[..., None | Optional[str]], @@ -828,7 +872,7 @@ class BaseTui(QueueMixin): + f'(given {len(toks)}, need {n_args_min})' else: alert = cmd(*toks) - self._status_line.taint() + self._status_line.tainted = True else: alert = 'not prefixed by /' if alert: @@ -922,7 +966,7 @@ class Terminal(QueueMixin, TerminalInterface): def flush(self) -> None: print('', end='', flush=True) - def length_to_term(self, text: str) -> int: + def len_to_term(self, text: str) -> int: # ._blessed.length can slow down things notably: only use where needed! return len(text) if text.isascii() else self._blessed.length(text)