from queue import SimpleQueue, Empty as QueueEmpty
from pathlib import Path
from time import sleep
+from textwrap import wrap
from typing import Callable, Generator, Iterator, Optional
from ircplom.events import Event, Loop, QueueMixin
from ircplom.client import IrcConnection, IrcConnSetup
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._q_keypresses: SimpleQueue = SimpleQueue()
+ self._screen: list[list[tuple[tuple[str, ...], str]]] = []
@contextmanager
def setup(self) -> Generator:
pass
def set_size_hide_cursor(self) -> None:
- self.size = TerminalInterface.__annotations__['size'](0, 0)
+ self.size = TerminalInterface.__annotations__['size'](24, 80)
+ self._screen.clear()
+ for _ in range(self.size.y):
+ line: list[tuple[tuple[str, ...], str]] = []
+ self._screen += [line]
+ for _ in range(self.size.x):
+ line += [(tuple(), ' ')]
def wrap(self, line: str) -> list[str]:
- return []
-
- def write(self,
- msg: str = '',
- start_y: Optional[int] = None,
- attributes: str = '',
- padding: bool = True
- ) -> None:
- pass
+ return wrap(line, width=self.size.x, subsequent_indent=' '*4)
+
+ def _length_to_terminal(self, text: str) -> int:
+ return len(text)
+
+ def _write_w_attrs(self, text: str, attrs: tuple[str, ...]) -> None:
+ for i, c in enumerate(text):
+ self._screen[self._cursor_yx.y][self._cursor_yx.x + i] = (attrs, c)
+
+ def _truncate(self, text: str, size: int) -> str:
+ return text[:size]
def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]:
while True:
yield TuiEvent.affector('handle_keyboard_event'
).kw(typed_in=to_yield)
+ def assert_screen_line(self, y: int, x: int, text: str, attrs_str: str
+ ) -> None:
+ 'Assert test screen at (y,x) shows text, with attrs of attrs_str set.'
+ assert len(text) + x <= self.size.x
+ assert 0 <= y < self.size.y
+ for idx, cell_expected in enumerate(
+ (self._attrs_tuple_from_str(attrs_str), c) for c in text):
+ cell_found = self._screen[y][x + idx]
+ info = (x + idx, 'EXPECTED/FOUND',
+ cell_expected, cell_found,
+ text, ''.join(t[1] for t in self._screen[y][x:]))
+ assert cell_expected == cell_found, info
+
class _FakeIrcConnection(IrcConnection):
_CHAR_WIN_ID_SEP = ','
_TOK_REPEAT = 'repeat'
_TOK_WAIT = 'wait'
+_TOK_TUI = 'TUI'
class _Playbook:
put_keypress: Optional[Callable] = None
+ assert_screen_line: Optional[Callable] = None
def __init__(self, path: Path, get_client: Callable, verbose: bool
) -> None:
client = self._get_client(int(context[1:]))
assert isinstance(client.conn, _FakeIrcConnection), client.conn
client.conn.put_server_msg(msg)
+ elif context == _TOK_TUI:
+ assert self.assert_screen_line is not None
+ position, attrs_str, msg = msg.split('.', maxsplit=2)
+ y, x = ((int(item) for item in position.split(','))
+ if ',' in position
+ else (int(position), 0))
+ self.assert_screen_line(y, x, msg, attrs_str)
elif context == _TOK_WAIT:
assert msg.isdigit()
sleep(int(msg))
get_client=lambda idx: self._clients[idx])
super().__init__(**kwargs)
assert isinstance(self._term, TestTerminal)
+ self._playbook.assert_screen_line = self._term.assert_screen_line
self._playbook.put_keypress = self._term._q_keypresses.put
self._playbook.ensure_has_started() # if .__init__ didn't yet by log()
- def _switch_window(self, idx: int) -> None:
- self._window_idx = idx
-
@classmethod
def on_file(cls, path_test: Path, verbose: bool):
'Return cls with ._path_test set.'
def log(self, msg: str, **kwargs) -> tuple[tuple[int, ...], str]:
win_ids, logged_msg = super().log(msg, **kwargs)
+ # usually whatever called log would call .redraw_affected directly
+ # after, but for TUI display tests on the effects of .log that might
+ # follow directly in the playbook (i.e. will run before .log actually
+ # finishes, signaling to caller they should call .redraw_affected) we
+ # want this available immediately
+ self.redraw_affected()
fmt, time_str, msg_sans_time = logged_msg.split(' ', maxsplit=2)
msg_sans_time = fmt + ' ' + msg_sans_time
assert len(time_str) == 8
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
+ self._cursor_yx = _YX(0, 0)
@abstractmethod
@contextmanager
def wrap(self, line: str) -> list[str]:
'Wrap line to list of lines fitting into terminal width.'
+ @staticmethod
+ def _attrs_tuple_from_str(attrs_str: str) -> tuple[str, ...]:
+ return tuple(attr for attr in attrs_str.split(',') if attr)
+
+ @abstractmethod
+ def _length_to_terminal(self, text: str) -> int:
+ pass
+
@abstractmethod
+ def _write_w_attrs(self, text: str, attrs: tuple[str, ...]) -> None:
+ pass
+
+ @abstractmethod
+ def _truncate(self, text: str, size: int) -> str:
+ pass
+
def write(self,
msg: str = '',
start_y: Optional[int] = None,
padding: bool = True
) -> None:
'Print to terminal, with position, padding to line end, attributes.'
+ if start_y is not None:
+ self._cursor_yx = _YX(start_y, 0)
+ # ._blessed.length can slow down things notably: only use where needed!
+ end_x = self._cursor_yx.x + self._length_to_terminal(msg)
+ len_padding = self.size.x - end_x
+ if len_padding < 0:
+ msg = self._truncate(msg, self.size.x - self._cursor_yx.x)
+ elif padding:
+ msg += ' ' * len_padding
+ end_x = self.size.x
+ self._write_w_attrs(msg, self._attrs_tuple_from_str(attributes))
+ self._cursor_yx = _YX(self._cursor_yx.y, end_x)
@abstractmethod
def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]:
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._blessed = BlessedTerminal()
- self._cursor_yx = _YX(0, 0)
@contextmanager
def setup(self) -> Generator:
return self._blessed.wrap(line, width=self.size.x,
subsequent_indent=' '*4)
- def write(self,
- msg: str = '',
- start_y: Optional[int] = None,
- attributes: str = '',
- padding: bool = True
- ) -> None:
- if start_y is not None:
- self._cursor_yx = _YX(start_y, 0)
- # ._blessed.length can slow down things notably: only use where needed!
- end_x = self._cursor_yx.x + (len(msg) if msg.isascii()
- else self._blessed.length(msg))
- len_padding = self.size.x - end_x
- if len_padding < 0:
- msg = self._blessed.truncate(msg, self.size.x - self._cursor_yx.x)
- elif padding:
- msg += ' ' * len_padding
- end_x = self.size.x
- for attr in [attr for attr in attributes.split(',') if attr]:
- msg = getattr(self._blessed, attr)(msg)
- print(msg, end='')
- self._cursor_yx = _YX(self._cursor_yx.y, end_x)
+ def _length_to_terminal(self, text: str) -> int:
+ return len(text) if text.isascii() else self._blessed.length(text)
+
+ def _write_w_attrs(self, text: str, attrs: tuple[str, ...]) -> None:
+ for attr in attrs:
+ text = getattr(self._blessed, attr)(text)
+ print(text, end='')
+
+ def _truncate(self, text: str, size: int) -> str:
+ return self._blessed.truncate(text, size)
def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]:
'''Loop through keypresses from terminal, expand blessed's handling.
--- /dev/null
+TUI 0.on_black.
+TUI 1.on_black.
+TUI 2.on_black.
+TUI 3.on_black.
+TUI 4.on_black.
+TUI 5.on_black.
+TUI 6.on_black.
+TUI 7.on_black.
+TUI 8.on_black.
+TUI 9.on_black.
+TUI 10.on_black.
+TUI 11.on_black.
+TUI 12.on_black.
+TUI 13.on_black.
+TUI 14.on_black.
+TUI 15.on_black.
+TUI 16.on_black.
+TUI 17.on_black.
+TUI 18.on_black.
+TUI 19.on_black.
+TUI 20.on_black.
+TUI 21.on_black.
+TUI 22..:start)=====================================================================([0]
+
+TUI 23,0..>
+TUI 23,2.reverse.
+TUI 23,3..
+# nothing happening on empty command input
+>
+TUI 0.on_black.
+TUI 21.on_black.
+TUI 22..:start)=====================================================================([0]
+
+TUI 23,2.reverse.
+
+# non-empty command input starts log at bottom, with date above it
+> foo
+0 .!# invalid prompt command: not prefixed by /
+
+TUI 0.on_black.
+TUI 20,0.on_black.20
+TUI 20,4.on_black.-
+TUI 20,7.on_black.-
+TUI 20,10.on_black.
+TUI 21,0.on_black,bold,bright_red,bright_cyan..!#
+TUI 21,13.on_black,bold,bright_red,bright_cyan.invalid prompt command: not prefixed by /
+TUI 22..:start)=====================================================================([0]
+TUI 23,0..>
+TUI 23,2.reverse.
+TUI 23,3..
+
+# further inputs grow log upwards
+> /foo
+0 .!# invalid prompt command: /foo unknown
+> /foo
+0 .!# invalid prompt command: /foo unknown
+
+TUI 17.on_black.
+TUI 18,0.on_black.20
+TUI 19,13.on_black,bold,bright_red,bright_cyan.invalid prompt command: not prefixed by /
+TUI 20,13.on_black,bold,bright_red,bright_cyan.invalid prompt command: /foo unknown
+TUI 21,13.on_black,bold,bright_red,bright_cyan.invalid prompt command: /foo unknown
+
+# check wrapping
+> /foo_0123456789_0123456789_01234567
+0 .!# invalid prompt command: /foo_0123456789_0123456789_01234567 unknown
+TUI 21,13.on_black,bold,bright_red,bright_cyan.invalid prompt command: /foo_0123456789_0123456789_01234567 unknown
+> /foo_0123456789_0123456789_012345678
+0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+TUI 20,13.on_black,bold,bright_red,bright_cyan.invalid prompt command: /foo_0123456789_0123456789_012345678
+TUI 21,0.on_black,bold,bright_red,bright_cyan. unknown
+# # check scrolling
+# > /window.prompt.scroll up
+# > /foo_0123456789_0123456789_012345678
+# 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # TUI 21.on_black.
+# # > /foo_0123456789_0123456789_012345678
+# # 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # 0 .!# invalid prompt command: /foo_0123456789_0123456789_012345678 unknown
+# # > /foo_0123456789_0123456789_012345678
+# # TUI 0.on_black.
+> /quit
+0 ..<