#!/usr/bin/env python3
'Attempt at an IRC client.'
from queue import SimpleQueue
+from sys import argv
from ircplom.events import ExceptionEvent, QuitEvent
from ircplom.client import ClientsDb, ClientEvent, NewClientEvent
-from ircplom.tui_base import Terminal, TuiEvent
+from ircplom.tui_base import (BaseTui, Terminal, TerminalInterface,
+ TestTerminal, TestTui, TuiEvent)
from ircplom.client_tui import ClientTui
-def main_loop() -> None:
+def main_loop(cls_term: type[TerminalInterface], cls_tui: type[BaseTui]
+ ) -> None:
'Main execution code / loop.'
q_events: SimpleQueue = SimpleQueue()
clients_db: ClientsDb = {}
try:
- with Terminal(_q_out=q_events).setup() as term:
- tui = ClientTui(_q_out=q_events, term=term)
+ with cls_term(_q_out=q_events).setup() as term:
+ tui = cls_tui(_q_out=q_events, term=term)
while True:
event = q_events.get()
if isinstance(event, QuitEvent):
if __name__ == '__main__':
- main_loop()
+ if len(argv) > 1 and argv[1] == 'test':
+ main_loop(TestTerminal, TestTui)
+ print('test finished')
+ else:
+ main_loop(Terminal, ClientTui)
from contextlib import contextmanager
from datetime import datetime
from inspect import _empty as inspect_empty, signature, stack
+from queue import SimpleQueue, Empty as QueueEmpty
from signal import SIGWINCH, signal
from typing import (Callable, Generator, Iterator, NamedTuple, Optional,
Sequence)
'To affect TUI.'
+class TerminalInterface(ABC):
+ 'What BaseTui expects from a Terminal.'
+ size: _YX
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
+
+ @abstractmethod
+ @contextmanager
+ def setup(self) -> Generator:
+ 'Combine multiple contexts into one and run keypress loop.'
+
+ @abstractmethod
+ def calc_geometry(self) -> None:
+ '(Re-)calculate .size..'
+
+ @abstractmethod
+ def flush(self) -> None:
+ 'Flush terminal.'
+
+ @abstractmethod
+ def wrap(self, line: str) -> list[str]:
+ 'Wrap line to list of lines fitting into terminal width.'
+
+ @abstractmethod
+ def write(self,
+ msg: str = '',
+ start_y: Optional[int] = None,
+ attribute: Optional[str] = None,
+ padding: bool = True
+ ) -> None:
+ 'Print to terminal, with position, padding to line end, attributes.'
+
+ @abstractmethod
+ def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]:
+ pass
+
+
class BaseTui(QueueMixin):
'Base for graphical user interface elements.'
- def __init__(self, term: 'Terminal', **kwargs) -> None:
+ def __init__(self, term: TerminalInterface, **kwargs) -> None:
super().__init__(**kwargs)
self._term = term
self._window_idx = 0
return None
-class Terminal(QueueMixin):
+class Terminal(QueueMixin, TerminalInterface):
'Abstraction of terminal interface.'
- size: _YX
_cursor_yx_: _YX
def __init__(self, **kwargs) -> None:
@contextmanager
def setup(self) -> Generator:
- 'Combine multiple contexts into one and run keypress loop.'
print(self._blessed.clear, end='')
with (self._blessed.raw(),
self._blessed.fullscreen(),
self._cursor_yx_ = yx
def calc_geometry(self) -> None:
- '(Re-)calculate .size..'
self.size = _YX(self._blessed.height, self._blessed.width)
def flush(self) -> None:
- 'Flush terminal.'
print('', end='', flush=True)
def wrap(self, line: str) -> list[str]:
- 'Wrap line to list of lines fitting into terminal width.'
return self._blessed.wrap(line, width=self.size.x,
subsequent_indent=' '*4)
attribute: Optional[str] = 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!
yield (TuiEvent.affector('handle_keyboard_event'
).kw(typed_in=to_yield) if to_yield
else None)
+
+
+PLAYBOOK: tuple[str, ...] = (
+ '<',
+ '>/help',
+ '<commands available in this window:',
+ '< /help ',
+ '< /list ',
+ '< /prompt_enter ',
+ '< /quit ',
+ '< /window TOWARDS',
+ '< /window.history.scroll DIRECTION',
+ '< /window.paste ',
+ '< /window.prompt.backspace ',
+ '< /window.prompt.move_cursor DIRECTION',
+ '< /window.prompt.scroll DIRECTION',
+ '>/list',
+ '<windows available via /window:',
+ '< 0) :start',
+ '>/quit',
+ '<'
+)
+
+
+class TestTerminal(QueueMixin, TerminalInterface):
+ 'Collects keypresses from string queue, otherwise mostly dummy.'
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
+ self._q_keypresses: SimpleQueue = SimpleQueue()
+
+ @contextmanager
+ def setup(self) -> Generator:
+ with Loop(iterator=self._get_keypresses(), _q_out=self._q_out):
+ yield self
+
+ def flush(self) -> None:
+ pass
+
+ def calc_geometry(self) -> None:
+ self.size = _YX(0, 0)
+
+ def wrap(self, line: str) -> list[str]:
+ return []
+
+ def write(self,
+ msg: str = '',
+ start_y: Optional[int] = None,
+ attribute: Optional[str] = None,
+ padding: bool = True
+ ) -> None:
+ pass
+
+ def _get_keypresses(self) -> Iterator[Optional[TuiEvent]]:
+ while True:
+ try:
+ to_yield = self._q_keypresses.get(timeout=0.1)
+ except QueueEmpty:
+ break
+ yield TuiEvent.affector('handle_keyboard_event'
+ ).kw(typed_in=to_yield)
+
+
+class TestTui(BaseTui):
+ 'Collects keypresses via TestTerminal from PLAYBOOK, compares log results.'
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
+ self._test_idx = 0
+ assert isinstance(self._term, TestTerminal)
+ self._q_keypresses = self._term._q_keypresses
+ self._log('')
+
+ def _log(self, msg: str, **kwargs) -> None:
+ entry = PLAYBOOK[self._test_idx]
+ direction, expected = entry[0], entry[1:]
+ assert direction == '<'
+ assert expected == msg, (self._test_idx, expected, msg)
+ super()._log(msg, **kwargs)
+ while True:
+ self._test_idx += 1
+ entry = PLAYBOOK[self._test_idx]
+ direction, msg = entry[0], entry[1:]
+ if direction == '>':
+ for c in msg:
+ self._q_keypresses.put(c)
+ self._q_keypresses.put('KEY_ENTER')
+ else:
+ break