# ourselves
from ircplom.events import AffectiveEvent, Loop, QueueMixin, QuitEvent
+_UNSET_IDX_NEG = 0
+_UNSET_IDX_POS = -1
+
+_DEFAULT_MARKUP = ('on_black', 'bright_white')
+_MIN_HEIGHT = 4
+_MIN_WIDTH = 32
+
LOG_PREFIX_DEFAULT = '#'
LOG_FMT_SEP = ' '
LOG_FMT_TAG_ALERT = 'alert'
}
_WRAP_INDENT = 2
-_MIN_HEIGHT = 4
-_MIN_WIDTH = 32
-
_TIMEOUT_KEYPRESS_LOOP = 0.5
_B64_PREFIX = 'b64:'
_OSC52_PREFIX = b']52;c;'
_PASTE_DELIMITER = '\007'
_PROMPT_TEMPLATE = '> '
+
_ELL_IN = '<…'
_ELL_OUT = '…>'
}
CMD_SHORTCUTS: dict[str, str] = {}
-_UNSET_IDX_NEG = 0
-_UNSET_IDX_POS = -1
-
class _YX(NamedTuple):
y: int
_SIZE_UNSET = _YX(_UNSET_IDX_POS, _UNSET_IDX_POS)
-class FormattingString:
- 'For inserting terminal formatting directives, and escaping their syntax.'
- _BRACKET_IN = '{'
- _BRACKET_OUT = '}'
+class StylingString:
+ 'For str content with optional styling markup, e.g. "Hi {bold,red|Bob}!".'
+ _BRACE_IN = '{'
+ _BRACE_OUT = '}'
_SEP = '|'
- 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 __init__(self, text: str, store_raw=False) -> None:
+ self._str = text if store_raw else ''.join([self._BRACE_IN + c
+ if self._is_brace(c) else c
+ for c in text])
def __add__(self, other: Self) -> Self:
- return self.__class__(str(self) + str(other), raw=True)
+ return self.__class__(str(self) + str(other), store_raw=True)
def __eq__(self, other) -> bool:
- return isinstance(other, self.__class__) and self._text == other._text
+ return isinstance(other, self.__class__) and self._str == other._str
def __str__(self) -> str:
- return self._text
+ return self._str
def attrd(self, *attrs) -> Self:
- 'Wrap in formatting applying attrs.'
- return self.__class__(raw=True, text=(self._BRACKET_IN
- + ','.join(list(attrs))
- + self._SEP
- + self._text
- + self._BRACKET_OUT))
+ '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_bracket(cls, c: str) -> bool:
- return c in {cls._BRACKET_IN, cls._BRACKET_OUT}
+ def _is_brace(cls, c: str) -> bool:
+ return c in {cls._BRACE_IN, cls._BRACE_OUT}
def stripped(self) -> str:
- 'Return without formatting directives.'
+ 'Return without mark-up.'
return ''.join([text for _, text in self.parts_w_attrs()])
@classmethod
- def _parse_for_formattings(cls,
- c: str,
- in_format_def: bool,
- formattings: list[str],
- ) -> tuple[bool, bool]:
- do_not_print = True
- if in_format_def:
- if cls._is_bracket(c):
- do_not_print = False
- in_format_def = False
- assert not formattings[-1]
- formattings.pop(-1)
- elif c == cls._SEP:
- in_format_def = False
+ def _parse_for_markups(cls, char: str, in_tag: bool, markups: list[str]
+ ) -> tuple[bool, bool]:
+ do_print = False
+ if in_tag:
+ if cls._is_brace(char):
+ do_print = True
+ in_tag = False
+ assert not markups[-1]
+ markups.pop(-1)
+ elif char == cls._SEP:
+ in_tag = False
else:
- formattings[-1] += c
- elif cls._is_bracket(c):
- if c == cls._BRACKET_IN:
- in_format_def = True
- formattings += ['']
- elif c == cls._BRACKET_OUT:
- formattings.pop(-1)
+ markups[-1] += char
+ elif cls._is_brace(char):
+ if char == cls._BRACE_IN:
+ in_tag = True
+ markups += ['']
+ elif char == cls._BRACE_OUT:
+ markups.pop(-1)
else:
- do_not_print = False
- return do_not_print, in_format_def
+ do_print = True
+ return do_print, in_tag
def parts_w_attrs(self) -> tuple[tuple[tuple[str, ...], str], ...]:
'Break into individually formatted parts, with respective attributes.'
next_part = ''
- formattings: list[str] = []
- in_format_def = False
+ markups: list[str] = []
+ in_tag = False
to_ret: list[tuple[tuple[str, ...], str]] = []
- 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:
- for attr in [a for a in formatting.split(',') if a]:
- if attr.startswith('bright_'):
+ for char in str(self.attrd()):
+ if (not in_tag) and self._is_brace(char):
+ attrs_to_pass = list(_DEFAULT_MARKUP)
+ for markup in markups:
+ for attr in [a for a in markup.split(',') if a]:
+ if attr.startswith('on_'):
+ attrs_to_pass[0] = attr
+ elif attr.startswith('bright_'):
attrs_to_pass[1] = attr
else:
attrs_to_pass += [attr]
to_ret += [(tuple(attrs_to_pass), next_part)]
next_part = ''
- do_not_print, in_format_def\
- = self._parse_for_formattings(c, in_format_def, formattings)
- if do_not_print:
- continue
- next_part += c
+ do_add, in_tag = self._parse_for_markups(char, in_tag, markups)
+ if do_add:
+ next_part += char
return tuple(to_ret)
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 = ''
+ 'Break into parts within width, each preserving its source attributes.'
+ lines: list[str] = []
+ next_part = ''
len_flattened = 0
idx = -1
- formattings: list[str] = []
- in_format_def = False
- len_wrapped_prefix = 0
- while idx+1 < len(self._text):
+ markups: list[str] = []
+ in_tag = False
+ len_part_prefix = 0
+ while idx+1 < len(self._str):
idx += 1
- c = self._text[idx]
- next_part_w_code += c
- do_not_print, in_format_def\
- = self._parse_for_formattings(c, in_format_def, formattings)
- if do_not_print:
+ char = self._str[idx]
+ next_part += char
+ do_add, in_tag = self._parse_for_markups(char, in_tag, markups)
+ if not do_add:
continue
- len_flattened += len_to_term(c)
+ len_flattened += len_to_term(char)
if len_flattened <= width:
continue
- walk_back = ([-(i+1) for i in range(len(next_part_w_code)
- - 1 - len_wrapped_prefix)
- if next_part_w_code[-(i+1)].isspace()] + [-1])[0]
- wrapped_lines += [next_part_w_code[:walk_back]]
+ walk_back = ([-(i+1)
+ for i in range(len(next_part) - 1 - len_part_prefix)
+ if next_part[-(i+1)].isspace()] + [-1])[0]
+ lines += [next_part[:walk_back]]
idx = idx + walk_back
- next_part_w_code = ''
- for fmt in reversed(formattings):
- next_part_w_code += self._BRACKET_IN + fmt + self._SEP
- next_part_w_code += ' ' * _WRAP_INDENT
- len_wrapped_prefix = len(next_part_w_code)
+ next_part = ''.join([self._BRACE_IN + fmt + self._SEP
+ for fmt in reversed(markups)]
+ + [' ' * _WRAP_INDENT])
+ len_part_prefix = len(next_part)
len_flattened = _WRAP_INDENT
- wrapped_lines[-1] += self._BRACKET_OUT * len(formattings)
- if next_part_w_code.rstrip():
- wrapped_lines += [next_part_w_code]
- assert not formattings
- return tuple(self.__class__(text, raw=True) for text in wrapped_lines)
+ lines[-1] += self._BRACE_OUT * len(markups)
+ if next_part.rstrip():
+ lines += [next_part]
+ return tuple(self.__class__(line, store_raw=True) for line in lines)
class _Widget(ABC):
class _WrappedHistoryLine(NamedTuple):
history_idx_pos: int
- text: FormattingString
+ text: StylingString
class _HistoryWidget(_ScrollableWidget):
def _add_wrapped_from_offset_history(self,
history_idx_pos: int,
- line: FormattingString
+ line: StylingString
) -> int:
lines = [_WrappedHistoryLine(
self._history_n_lines_cut + history_idx_pos,
self._y_pgscroll = self._size.y // 2
self._wrapped.clear()
for history_idx_pos, line in enumerate(self._history):
- self._add_wrapped_from_offset_history(history_idx_pos,
- FormattingString(
- line, raw=True))
+ self._add_wrapped_from_offset_history(
+ history_idx_pos, StylingString(line, store_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(keep_bookmark_at)
- def append_fmt(self, to_append: FormattingString) -> None:
- 'Wrap .append around FormattingString, update history dependents.'
+ def append_fmt(self, to_append: StylingString) -> None:
+ 'Wrap .append around StylingString, 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)
while len(visible_lines) < self._size.y - bool(add_scroll_info):
visible_lines.insert(0, _WrappedHistoryLine(
self._PADDING_HISTORY_IDX_POS,
- FormattingString('')))
+ StylingString('')))
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._size.x - len(scroll_info))
self._write(len(visible_lines),
- FormattingString(scroll_info).attrd('reverse'))
+ StylingString(scroll_info).attrd('reverse'))
self._lowest_read_history_idx_pos\
= max(self._lowest_read_history_idx_pos,
visible_lines[-1].history_idx_pos)
if not self._wrapped:
return
bookmark = _WrappedHistoryLine(self._BOOKMARK_HISTORY_IDX_POS,
- FormattingString('-' * self._size.x))
+ StylingString('-' * self._size.x))
lowest_read_wrapped_idx_neg\
= (self._wrapped_top_idx_neg
+ self._bottom_wrapped_for_history_idx_pos(
left=self.prefix[:],
right=content)
self._write(self._size.y,
- FormattingString(to_write[:x_cursor])
- + FormattingString(to_write[x_cursor]).attrd('reverse')
- + FormattingString(to_write[x_cursor + 1:]))
+ StylingString(to_write[:x_cursor])
+ + StylingString(to_write[x_cursor]).attrd('reverse')
+ + StylingString(to_write[x_cursor + 1:]))
def _archive_prompt(self) -> None:
self.append(self.input_buffer)
def __init__(self,
idx: int,
- write: Callable[[int, str | FormattingString], None],
+ write: Callable[[int, str | StylingString], None],
len_to_term: Callable[[str], int],
maxlen_log: int,
**kwargs
'Log date of today if it has not been logged yet.'
if today != self._last_today:
self._last_today = today
- self.log(FormattingString(today))
+ self.log(StylingString(today))
- def log(self, msg: FormattingString) -> None:
+ def log(self, msg: StylingString) -> None:
'Append msg to .history.'
self.history.append_fmt(msg)
def _write_w_attrs(self, text: str, attrs: tuple[str, ...]) -> None:
pass
- 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)
+ def write(self, y: int, text: str | StylingString) -> None:
+ 'Write line of text at y, enacting StylingString directives.'
self._cursor_y = y
attrs: tuple[str, ...] = tuple()
len_written = 0
- for attrs, part in text.parts_w_attrs():
+ for attrs, part in (StylingString(text) if isinstance(text, str)
+ else text).parts_w_attrs():
len_written += self.len_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 | FormattingString,
+ msg: str | StylingString,
formatting_tags=tuple(),
prefix_char: Optional[str] = None,
**kwargs
) -> Optional[tuple[tuple[int, ...], str]]:
'Write with timestamp, prefix to what window ._log_target_wins offers.'
if isinstance(msg, str):
- msg = FormattingString(msg)
+ msg = StylingString(msg)
if prefix_char is None:
prefix_char = LOG_PREFIX_DEFAULT
now = str(datetime.now())
today, time = now[:10], now[11:19]
- msg = FormattingString(f'{prefix_char}{LOG_FMT_SEP}{time} ') + msg
+ msg = StylingString(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()))