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
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)
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 []
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:
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',):
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])
self._client_tui_trigger(
'log',
scope=_LogScope.CHAT if target else _LogScope.DEBUG,
- msg=msg,
+ msg=FormattingString(msg),
alert=alert,
target=target,
out=out)
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
_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()])
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:
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 = ''
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):
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)
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 = (
- 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
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:
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)
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)
'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.'
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))
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)