home · contact · privacy
Refactor prompt handling into own class.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 1 Jun 2025 14:26:46 +0000 (16:26 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 1 Jun 2025 14:26:46 +0000 (16:26 +0200)
ircplom.py

index cc75822689933377e5eb32d7f586af1352e533d6..cb1dfb3417c8804e7844990500e25443b592f5f3 100755 (executable)
@@ -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.'