From 3eb313d9b89c7a31e07b6dd3db71f7769075f275 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 1 Jun 2025 16:26:46 +0200 Subject: [PATCH] Refactor prompt handling into own class. --- ircplom.py | 116 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 43 deletions(-) diff --git a/ircplom.py b/ircplom.py index cc75822..cb1dfb3 100755 --- a/ircplom.py +++ b/ircplom.py @@ -20,7 +20,7 @@ NICKNAME = 'bar' REALNAME = 'debug debugger' TIMEOUT_CONNECT = 5 TIMEOUT_LOOP = 0.1 -INPUT_PROMPT = ':' +INPUT_PROMPT = '> ' KEYBINDINGS = { 'KEY_BACKSPACE': ('prompt_backspace',), @@ -433,12 +433,48 @@ class LogBuffer: return to_return + [self._wrapped[-1][1]] +class TuiPrompt: + 'Keyboard-controlled command input field.' + start_y: int + + def __init__(self, term: Terminal) -> None: + self._term = term + self._buffer = '' + + def append(self, char: str) -> None: + 'Append char to current content.' + self._buffer += char + self.draw() + + def backspace(self) -> None: + 'Truncate current content by one character, if possible.' + self._buffer = self._buffer[:-1] + self.draw() + + def clear(self) -> None: + 'Empty current content.' + self._buffer = '' + self.draw() + + def draw(self) -> None: + 'Print into screen..' + self._term.write_yx(YX(self.start_y, len(INPUT_PROMPT)), + f'{self._buffer}_') + + def enter(self) -> str: + 'Return current content while also clearing and then redrawing.' + to_return = self._buffer[:] + self.clear() + self.draw() + return to_return + + class TuiLoop(Loop): 'Loop for drawing/updating TUI.' def __init__(self, term: Terminal, *args, **kwargs) -> None: self._term = term - self._prompt = '' + self._prompt = TuiPrompt(self._term) self._logs = [LogBuffer(self._term.wrap) for i in range(2)] self._log_selected = 0 self._upscroll = 0 @@ -459,8 +495,7 @@ class TuiLoop(Loop): elif event.type_ == 'KEYBINDING': getattr(self, f'_cmd__{event.payload[0]}')(*event.payload[1:]) elif event.type_ == 'INPUT_CHAR': - self._prompt += event.payload - self._draw_prompt() + self._prompt.append(event.payload) elif event.type_ == 'SIGWINCH': self._calc_and_draw_all() # elif event.type_ == 'DEBUG': @@ -479,40 +514,39 @@ class TuiLoop(Loop): return self._logs[self._log_selected] def _cmd__prompt_backspace(self) -> None: - self._prompt = self._prompt[:-1] - self._draw_prompt() + self._prompt.backspace() def _cmd__prompt_enter(self) -> None: - if self._prompt: - alert: Optional[str] = None - if len(self._prompt) > 1 and self._prompt[0] == '/': - toks = self._prompt[1:].split(maxsplit=1) - method_name = f'_cmd__{toks[0]}' - alert = f'{toks[0]} unknown' - if hasattr(self, method_name)\ - and method_name != stack()[0].function: - method = getattr(self, method_name) - params = signature(method).parameters - n_args_max = len(params) - n_args_min = len([p for p in params.values() - if p.default == inspect_empty]) - alert = f'{toks[0]} needs {n_args_min} - {n_args_max} args' - if len(toks) == 1 and not n_args_min: - alert = method() - elif len(toks) > 1 and params\ - and n_args_min <= len(toks[1].split()): - args = [] - while len(toks) > 1 and n_args_max: - toks = toks[1].split(maxsplit=1) - args += [toks[0]] - n_args_max -= 1 - alert = method(*args) - else: - alert = 'not prefixed by /' - if alert: - self.broadcast('ALERT', f'invalid prompt command: {alert}') - self._prompt = '' - self._draw_prompt() + to_parse = self._prompt.enter() + if not to_parse: + return + alert: Optional[str] = None + if to_parse[0:1] == '/': + toks = to_parse[1:].split(maxsplit=1) + method_name = f'_cmd__{toks[0]}' + alert = f'{toks[0]} unknown' + if hasattr(self, method_name)\ + and method_name != stack()[0].function: + method = getattr(self, method_name) + params = signature(method).parameters + n_args_max = len(params) + n_args_min = len([p for p in params.values() + if p.default == inspect_empty]) + alert = f'{toks[0]} needs {n_args_min} - {n_args_max} args' + if len(toks) == 1 and not n_args_min: + alert = method() + elif len(toks) > 1 and params\ + and n_args_min <= len(toks[1].split()): + args = [] + while len(toks) > 1 and n_args_max: + toks = toks[1].split(maxsplit=1) + args += [toks[0]] + n_args_max -= 1 + alert = method(*args) + else: + alert = 'not prefixed by /' + if alert: + self.broadcast('ALERT', f'invalid prompt command: {alert}') def _cmd__scroll(self, direction: str) -> None: self._log.scroll(up=direction == 'up') @@ -537,26 +571,22 @@ class TuiLoop(Loop): def _calc_and_draw_all(self) -> None: self._term.clear() self._term.calc_geometry() - self._y_prompt = self._term.size.y - 1 + self._prompt.start_y = self._term.size.y - 1 self._y_separator = self._term.size.y - 2 for log in self._logs: log.apply_geometry(YX(self._y_separator, self._term.size.x)) self._draw_frame() self._draw_log() - self._draw_prompt() + self._prompt.draw() def _draw_frame(self) -> None: self._term.write_yx(YX(self._y_separator, 0), '=' * self._term.size.x) - self._term.write_yx(YX(self._y_prompt, 0), INPUT_PROMPT) + self._term.write_yx(YX(self._prompt.start_y, 0), INPUT_PROMPT) def _draw_log(self) -> None: for i, line in enumerate(self._log.wrapped): self._term.write_yx(YX(i, 0), line) - def _draw_prompt(self) -> None: - self._term.write_yx(YX(self._y_prompt, len(INPUT_PROMPT)), - f'{self._prompt}_') - class SocketRecvLoop(Loop): 'Loop receiving and translating socket messages towards main loop.' -- 2.30.2