From: Christian Heller Date: Tue, 28 Oct 2025 21:05:38 +0000 (+0100) Subject: Use FormattingString for type checks and to replace external escaping hints. X-Git-Url: https://plomlompom.com/repos/%7B%7B%20web_path%20%7D%7D/%7B%7Bdb.prefix%7D%7D/%7B%7Bprefix%7D%7D/add_structured?a=commitdiff_plain;ds=sidebyside;p=ircplom Use FormattingString for type checks and to replace external escaping hints. --- diff --git a/src/ircplom/client_tui.py b/src/ircplom/client_tui.py index eca0d32..eb02f87 100644 --- a/src/ircplom/client_tui.py +++ b/src/ircplom/client_tui.py @@ -59,7 +59,7 @@ class _ClientWindow(Window, ClientQueueMixin): def title(self) -> str: return f'{self.client_id}{self._title_separator}{self._title}' - def log(self, msg: str) -> None: + def log(self, msg: FormattingString) -> None: super().log(msg) if self._path_logs is None: return @@ -81,7 +81,7 @@ class _ClientWindow(Window, ClientQueueMixin): chat_path.mkdir(parents=True) with chat_path.joinpath(f'{self._last_today}.txt' ).open('a', encoding='utf8') as f: - f.write(FormattingString(msg).stripped() + '\n') + f.write(msg.stripped() + '\n') def _send_msg(self, verb: str, params: tuple[str, ...]) -> None: self._client_trigger('send_w_params_tuple', verb=verb, params=params) @@ -433,12 +433,11 @@ class _ClientWindowsManager: return ret def log(self, - msg: str, + msg: FormattingString, scope: _LogScope, alert=False, target='', - out: Optional[bool] = None, - escape=True + out: Optional[bool] = None ) -> None: 'From parsing scope, kwargs, build prefix before sending to logger.' formatting_tags = [LOG_FMT_TAG_ALERT] if alert else [] @@ -447,12 +446,11 @@ class _ClientWindowsManager: break kwargs = {'target': target} if target else {} self._tui_log( - msg, + msg=msg, formatting_tags=tuple(formatting_tags), scope=scope, prefix_char=(_LOG_PREFIX_SERVER if out is None else (_LOG_PREFIX_OUT if out else LOG_PREFIX_IN)), - escape=escape, **kwargs) def update_db(self, update: _Update) -> bool: @@ -461,14 +459,13 @@ class _ClientWindowsManager: if not update.results: return False for scope, result in update.results: - msg = '' + msg = FormattingString('') for item in result: transform, content = item.split(':', maxsplit=1) if transform in {'NICK', 'NUH'}: nuh = self.db.users[content] content = str(nuh) if transform == 'NUH' else nuh.nick - msg += (content if transform == 'RAW' - else FormattingString(content).escape()) + msg += FormattingString(content, raw=transform == 'RAW') out: Optional[bool] = None target = '' if update.full_path == ('message',): @@ -478,7 +475,7 @@ class _ClientWindowsManager: elif scope in {_LogScope.CHAT, _LogScope.USER, _LogScope.USER_NO_CHANNELS}: target = update.full_path[1] - self.log(msg, scope=scope, target=target, out=out, escape=False) + self.log(msg, scope=scope, target=target, out=out) for win in [w for w in self.windows if isinstance(w, _ChatWindow)]: win.set_prompt_prefix() return bool([w for w in self.windows if w.tainted]) @@ -551,7 +548,7 @@ class ClientKnowingTui(Client): self._client_tui_trigger( 'log', scope=_LogScope.CHAT if target else _LogScope.DEBUG, - msg=msg, + msg=FormattingString(msg), alert=alert, target=target, out=out) diff --git a/src/ircplom/testing.py b/src/ircplom/testing.py index 6ddb1c2..7232ad5 100644 --- a/src/ircplom/testing.py +++ b/src/ircplom/testing.py @@ -80,7 +80,7 @@ class TestTerminal(QueueMixin, TerminalInterface): = None if i else (attrs, c) self._cursor_x += len_to_term - def write(self, y: int, text: str) -> None: + def write(self, y: int, text: str | FormattingString) -> None: self._cursor_x = 0 super().write(y, text) @@ -436,7 +436,7 @@ class TestingClientTui(ClientTui): return client def log(self, - msg: str, + msg: str | FormattingString, formatting_tags=tuple(), prefix_char: Optional[str] = None, escape=True, @@ -445,8 +445,7 @@ class TestingClientTui(ClientTui): def test_after(cmd_name: str, args: tuple[str, ...], ret) -> None: assert cmd_name == _MARK_LOG, f'WANTED {_MARK_LOG}, GOT {cmd_name}' win_ids, logged = ret - fmt, time_str, msg_sans_time\ - = FormattingString(logged).stripped().split(' ', maxsplit=2) + fmt, time_str, msg_sans_time = logged.split(' ', maxsplit=2) msg_sans_time = fmt + ' ' + msg_sans_time assert len(time_str) == 8 for c in time_str[:2] + time_str[3:5] + time_str[6:]: diff --git a/src/ircplom/tui_base.py b/src/ircplom/tui_base.py index 208220f..68decea 100644 --- a/src/ircplom/tui_base.py +++ b/src/ircplom/tui_base.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from datetime import datetime from inspect import _empty as inspect_empty, signature, stack from signal import SIGWINCH, signal -from typing import (Callable, Generator, Iterator, NamedTuple, Optional, +from typing import (Callable, Generator, Iterator, NamedTuple, Optional, Self, Sequence) # requirements.txt from blessed import Terminal as BlessedTerminal @@ -57,29 +57,33 @@ class FormattingString: _BRACKET_OUT = '}' _SEP = '|' - def __init__(self, text: str) -> None: - self._text = text + def __init__(self, text: str, raw=False) -> None: + self._text: str = ( + text if raw + else ''.join([(self._BRACKET_IN + c if self._is_bracket(c) + else c) for c in text])) + + def __add__(self, other: Self) -> Self: + return self.__class__(str(self) + str(other), raw=True) + + def __eq__(self, other) -> bool: + return isinstance(other, self.__class__) and self._text == other._text def __str__(self) -> str: return self._text - def attrd(self, *attrs) -> str: + def attrd(self, *attrs) -> Self: 'Wrap in formatting applying attrs.' - return (self._BRACKET_IN - + ','.join(list(attrs)) - + self._SEP - + self._text - + self._BRACKET_OUT) + return self.__class__(raw=True, text=(self._BRACKET_IN + + ','.join(list(attrs)) + + self._SEP + + self._text + + self._BRACKET_OUT)) @classmethod def _is_bracket(cls, c: str) -> bool: return c in {cls._BRACKET_IN, cls._BRACKET_OUT} - def escape(self) -> str: - 'Preserve formatting characters by prefixing them with ._BRACKET_IN.' - return ''.join([(self._BRACKET_IN + c if self._is_bracket(c) - else c) for c in self._text]) - def stripped(self) -> str: 'Return without formatting directives.' return ''.join([text for _, text in self.parts_w_attrs()]) @@ -117,7 +121,7 @@ class FormattingString: formattings: list[str] = [] in_format_def = False to_ret: list[tuple[tuple[str, ...], str]] = [] - for c in self.attrd(): + for c in str(self.attrd()): if (not in_format_def) and self._is_bracket(c): attrs_to_pass = ['on_black', 'bright_white'] for formatting in formattings: @@ -135,7 +139,7 @@ class FormattingString: next_part += c return tuple(to_ret) - def wrap(self, width: int, length_to_term: Callable) -> tuple[str, ...]: + def wrap(self, width: int, length_to_term: Callable) -> tuple[Self, ...]: 'Break into sequence respecting width, preserving attributes per part.' wrapped_lines: list[str] = [] next_part_w_code = '' @@ -170,7 +174,7 @@ class FormattingString: if next_part_w_code.rstrip(): wrapped_lines += [next_part_w_code] assert not formattings - return tuple(wrapped_lines) + return tuple(self.__class__(text, raw=True) for text in wrapped_lines) class _YX(NamedTuple): @@ -249,14 +253,14 @@ class _HistoryWidget(_ScrollableWidget): super().__init__(**kwargs) self._maxlen_log = maxlen_log self._length_to_term = length_to_term - self._wrapped: list[tuple[int, str]] = [] + self._wrapped: list[tuple[int, FormattingString]] = [] self._newest_read_history_idx_pos = self._UNSET_IDX_POS self._history_offset = 0 self._history_idx_neg = self._UNSET_IDX_NEG - def _add_wrapped(self, history_idx_pos: int, line: str) -> int: - lines = FormattingString(line).wrap(self._sizes.x, - self._length_to_term) + 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] return len(lines) @@ -271,7 +275,8 @@ class _HistoryWidget(_ScrollableWidget): self._y_pgscroll = self._sizes.y // 2 self._wrapped.clear() for history_idx_pos, line in enumerate(self._history): - self._add_wrapped(history_idx_pos, line) + self._add_wrapped(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 = ( @@ -282,8 +287,9 @@ class _HistoryWidget(_ScrollableWidget): - self._maxlen_log)))) self.bookmark() - def append(self, to_append: str) -> None: - super().append(to_append) + def append_fmt(self, to_append: FormattingString) -> None: + 'Wrap .append around FormattingString, update history dependents.' + super().append(str(to_append)) self.taint() if not self._UNSET_IDX_NEG != self._history_idx_neg >= -1: self._history_idx_neg -= 1 @@ -312,7 +318,8 @@ class _HistoryWidget(_ScrollableWidget): 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, '')) + wrapped.insert(0, (self._PADDING_HISTORY_IDX_POS, + FormattingString(''))) for idx, line in enumerate([lt[1] for lt in wrapped]): self._write(idx, line) if add_scroll_info: @@ -325,7 +332,8 @@ class _HistoryWidget(_ScrollableWidget): def bookmark(self) -> None: 'Store next idx to what most recent line we have (been) scrolled.' - bookmark = (self._BOOKMARK_HISTORY_IDX_POS, '-' * self._sizes.x) + 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) @@ -426,10 +434,10 @@ class PromptWidget(_ScrollableWidget): if len(to_write) < self._sizes.x: to_write += ' ' self._write(self._sizes.y, - to_write[:cursor_x_to_write] + FormattingString(to_write[:cursor_x_to_write]) + FormattingString(to_write[cursor_x_to_write] ).attrd('reverse') - + to_write[cursor_x_to_write + 1:]) + + FormattingString(to_write[cursor_x_to_write + 1:])) def _archive_prompt(self) -> None: self.append(self.input_buffer) @@ -544,11 +552,11 @@ class Window: 'Log date of today if it has not been logged yet.' if today != self._last_today: self._last_today = today - self.log(today) + self.log(FormattingString(today)) - def log(self, msg: str) -> None: + def log(self, msg: FormattingString) -> None: 'Append msg to .history.' - self.history.append(msg) + self.history.append_fmt(msg) def taint(self) -> None: 'Declare all widgets as in need of re-drawing.' @@ -631,12 +639,14 @@ class TerminalInterface(ABC): def _write_w_attrs(self, text: str, attrs: tuple[str, ...]) -> None: pass - def write(self, y: int, text: str) -> None: + def write(self, y: int, text: str | FormattingString) -> None: 'Write line of text at y, enacting FormattingString directives.' + if isinstance(text, str): + text = FormattingString(text) self._cursor_y = y attrs: tuple[str, ...] = tuple() len_written = 0 - for attrs, part in FormattingString(text).parts_w_attrs(): + for attrs, part in text.parts_w_attrs(): len_written += self.length_to_term(part) self._write_w_attrs(part, attrs) self._write_w_attrs(' ' * (self.size.x - len_written), tuple(attrs)) @@ -666,31 +676,30 @@ class BaseTui(QueueMixin): return [self.window] def log(self, - msg: str, + msg: str | FormattingString, formatting_tags=tuple(), prefix_char: Optional[str] = None, - escape=True, **kwargs ) -> Optional[tuple[tuple[int, ...], str]]: 'Write with timestamp, prefix to what window ._log_target_wins offers.' - if escape: - msg = FormattingString(msg).escape() + if isinstance(msg, str): + msg = FormattingString(msg) if prefix_char is None: prefix_char = _LOG_PREFIX_DEFAULT now = str(datetime.now()) today, time = now[:10], now[11:19] - msg = f'{prefix_char}{LOG_FMT_SEP}{time} {msg}' + msg = FormattingString(f'{prefix_char}{LOG_FMT_SEP}{time} ') + msg msg_attrs: list[str] = list(LOG_FMT_ATTRS[prefix_char]) for tag in formatting_tags: msg_attrs += list(LOG_FMT_ATTRS.get(tag, tuple())) - msg = FormattingString(msg).attrd(*msg_attrs) + msg = msg.attrd(*msg_attrs) affected_win_indices = [] for win in self._log_target_wins(**kwargs): affected_win_indices += [win.idx] win.ensure_date(today) win.log(msg) self._status_line.taint() - return tuple(affected_win_indices), msg + return tuple(affected_win_indices), msg.stripped() def _new_window(self, win_class=Window, **kwargs) -> Window: new_idx = len(self._windows)