home · contact · privacy
Refactor TUI commands into their respective widgets.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 4 Jun 2025 00:01:17 +0000 (02:01 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 4 Jun 2025 00:01:17 +0000 (02:01 +0200)
ircplom.py

index 1a50b5c247473e34a23333487dd6922470a3a895..e527214f20d6f2da88a92bcae217c6dd3e80a286 100755 (executable)
@@ -24,12 +24,12 @@ TIMEOUT_LOOP = 0.1
 INPUT_PROMPT = '> '
 
 KEYBINDINGS = {
-    'KEY_BACKSPACE': ('prompt_backspace',),
+    'KEY_BACKSPACE': ('window.prompt.backspace',),
     'KEY_ENTER': ('prompt_enter',),
-    'KEY_UP': ('prompt_scroll', 'up'),
-    'KEY_DOWN': ('prompt_scroll', 'down'),
-    'KEY_PGUP': ('log_scroll', 'up'),
-    'KEY_PGDOWN': ('log_scroll', 'down'),
+    'KEY_UP': ('window.prompt.scroll', 'up'),
+    'KEY_DOWN': ('window.prompt.scroll', 'down'),
+    'KEY_PGUP': ('window.log.scroll', 'up'),
+    'KEY_PGDOWN': ('window.log.scroll', 'down'),
     '[91, 49, 59, 51, 68]': ('window', 'left'),
     '[91, 49, 59, 51, 67]': ('window', 'right'),
 }
@@ -399,9 +399,9 @@ class ScrollableWidget(Widget):
     def _scroll(self, up=True) -> None:
         pass
 
-    def scroll(self, up=True) -> None:
+    def cmd__scroll(self, direction: str) -> None:
         'Scroll through stored content/history.'
-        self._scroll(up)
+        self._scroll(up=direction == 'up')
         self.draw()
 
 
@@ -445,7 +445,7 @@ class PromptWidget(ScrollableWidget):
         self._input_buffer = self._history[self._history_idx][:]
         self.draw()
 
-    def backspace(self) -> None:
+    def cmd__backspace(self) -> None:
         'Truncate current content by one character, if possible.'
         self._input_buffer = self._input_buffer[:-1]
         self._history_idx = 0
@@ -565,6 +565,21 @@ class TuiLoop(Loop):
         self._term.flush()
         super().__init__(*args, **kwargs)
 
+    def _cmd_name_to_cmd(self, cmd_name: str) -> Optional[Callable]:
+        cmd_parent = self
+        while True:
+            cmd_name_toks = cmd_name.split('.', maxsplit=1)
+            if len(cmd_name_toks) == 1:
+                break
+            if not hasattr(cmd_parent, cmd_name_toks[0]):
+                return None
+            cmd_parent = getattr(cmd_parent, cmd_name_toks[0])
+            cmd_name = cmd_name_toks[1]
+        cmd_name = f'cmd__{cmd_name}'
+        if not hasattr(cmd_parent, cmd_name):
+            return None
+        return getattr(cmd_parent, cmd_name)
+
     def process_main(self, event: Event) -> bool:
         if not super().process_main(event):
             return False
@@ -574,26 +589,29 @@ class TuiLoop(Loop):
                 self._windows[1].log.append(f'<- {event.payload.raw}')
             elif event.type_ == 'SEND':
                 self._windows[1].log.append(f'-> {event.payload.raw}')
-            self._window.log.draw()
+            self.window.log.draw()
         elif event.type_ == 'KEYBINDING':
-            getattr(self, f'_cmd__{event.payload[0]}')(*event.payload[1:])
+            cmd = self._cmd_name_to_cmd(event.payload[0])
+            assert cmd is not None
+            cmd(*event.payload[1:])
         elif event.type_ == 'INPUT_CHAR':
-            self._window.prompt.append(event.payload)
+            self.window.prompt.append(event.payload)
         elif event.type_ == 'SIGWINCH':
             self._calc_and_draw_all()
         # elif event.type_ == 'DEBUG':
         #     from traceback import format_exception
         #     for line in '\n'.join(format_exception(event.payload)
         #                           ).split('\n'):
-        #         self._window.log.append(f'DEBUG {line}')
-        #     self._window.log.draw()
+        #         self.window.log.append(f'DEBUG {line}')
+        #     self.window.log.draw()
         else:
             return True
         self._term.flush()
         return True
 
     @property
-    def _window(self) -> Window:
+    def window(self) -> Window:
+        'Currently selected Window.'
         return self._windows[self._window_idx]
 
     def _calc_and_draw_all(self) -> None:
@@ -605,30 +623,31 @@ class TuiLoop(Loop):
         self._term.write_yx(YX(y_prompt, 0), INPUT_PROMPT)
         for window in self._windows:
             window.set_geometry((y_separator, y_prompt))
-        self._window.draw()
+        self.window.draw()
 
-    def _cmd__prompt_backspace(self) -> None:
-        self._window.prompt.backspace()
+    def cmd__disconnect(self, quit_msg: str = 'ircplom says bye') -> None:
+        'Send QUIT command to server.'
+        self.broadcast('SEND', IrcMessage('QUIT', [quit_msg]))
 
-    def _cmd__prompt_enter(self) -> None:
-        to_parse = self._window.prompt.enter()
+    def cmd__prompt_enter(self) -> None:
+        'Get prompt content from .window.prompt.enter, parse to & run command.'
+        to_parse = self.window.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
+            cmd_name = toks[0]
+            cmd = self._cmd_name_to_cmd(cmd_name)
+            if cmd and cmd.__name__ != stack()[0].function:
+                params = signature(cmd).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()
+                    alert = cmd()
                 elif len(toks) > 1 and params\
                         and n_args_min <= len(toks[1].split()):
                     args = []
@@ -636,25 +655,18 @@ class TuiLoop(Loop):
                         toks = toks[1].split(maxsplit=1)
                         args += [toks[0]]
                         n_args_max -= 1
-                    alert = method(*args)
+                    alert = cmd(*args)
         else:
             alert = 'not prefixed by /'
         if alert:
             self.broadcast('ALERT', f'invalid prompt command: {alert}')
 
-    def _cmd__prompt_scroll(self, direction: str) -> None:
-        self._window.prompt.scroll(up=direction == 'up')
-
-    def _cmd__log_scroll(self, direction: str) -> None:
-        self._window.log.scroll(up=direction == 'up')
-
-    def _cmd__disconnect(self, quit_msg: str = 'ircplom says bye') -> None:
-        self.broadcast('SEND', IrcMessage('QUIT', [quit_msg]))
-
-    def _cmd__quit(self) -> None:
+    def cmd__quit(self) -> None:
+        'Send QUIT to all threads.'
         self.broadcast('QUIT')
 
-    def _cmd__window(self, towards: str) -> Optional[str]:
+    def cmd__window(self, towards: str) -> Optional[str]:
+        'Switch window selection.'
         n_windows = len(self._windows)
         if n_windows < 2:
             return 'no alternate window to move into'
@@ -670,7 +682,7 @@ class TuiLoop(Loop):
             if not 0 <= window_idx < n_windows:
                 return f'unavailable window idx: {window_idx}'
         self._window_idx = window_idx
-        self._window.draw()
+        self.window.draw()
         return None