home · contact · privacy
Refactor interaction between TUI and Connection. master
authorChristian Heller <c.heller@plomlompom.de>
Sat, 12 Jul 2025 20:02:09 +0000 (22:02 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 12 Jul 2025 20:02:09 +0000 (22:02 +0200)
ircplom.py
ircplom/irc_conn.py
ircplom/tui.py

index aeff1c65947ee89b0ee098a8e48cf12ca56366b7..3fcceb6b7a8e742aa794e3e7de63c0df9c7b2188 100755 (executable)
@@ -8,7 +8,7 @@ from ircplom.tui import Terminal
 def main_loop() -> None:
     'Main execution code / loop.'
     q_to_main: EventQueue = EventQueue()
-    connections: list[IrcConnection] = []
+    connections: set[IrcConnection] = set()
     try:
         with Terminal().context(q_to_main) as term:
             while True:
@@ -19,10 +19,10 @@ def main_loop() -> None:
                 if isinstance(event, ExceptionEvent):
                     raise event.payload
                 if isinstance(event, InitConnectEvent):
-                    connections += [IrcConnection(q_to_main, len(connections),
-                                                  *event.payload)]
+                    connections.add(IrcConnection(q_to_main=q_to_main,
+                                                  *event.payload))
                 elif isinstance(event, ConnEvent):
-                    connections[event.conn_idx].handle(event)
+                    event.conn.handle(event)
     finally:
         for conn in connections:
             conn.close()
index ae2649bdb1e65bb64cd669d57d9536367d247d64..289ecfd1c58fd5066ef61cd247e9acd2eeeff2b8 100644 (file)
@@ -5,8 +5,7 @@ from socket import socket, gaierror as socket_gaierror
 from threading import Thread
 from typing import Callable, Iterator, NamedTuple, Optional, Self
 # ourselves
-from ircplom.events import (BroadcastMixin, Event, EventQueue, Loop,
-                            PayloadMixin)
+from ircplom.events import BroadcastMixin, Event, Loop, PayloadMixin
 
 
 TIMEOUT_LOOP = 0.1
@@ -123,14 +122,6 @@ class _IrcMessage:
         return self._raw
 
 
-class _ConnIdxMixin:
-    'Collects a Connection ID at .conn_idx.'
-
-    def __init__(self, conn_idx: int, **kwargs) -> None:
-        super().__init__(**kwargs)
-        self.conn_idx = conn_idx
-
-
 @dataclass
 class LoginNames:
     'Collects the names needed on server connect for USER, NICK commands.'
@@ -140,18 +131,21 @@ class LoginNames:
     nick_confirmed: bool = False
 
 
+class ConnMixin:
+    'Collects an IrcConnection at .conn.'
+
+    def __init__(self, conn: 'IrcConnection', **kwargs) -> None:
+        super().__init__(**kwargs)
+        self.conn = conn
+
+
 class InitConnectEvent(Event, PayloadMixin):
     'Event to trigger connection, with payload (host, LoginNames).'
     payload: tuple[str, LoginNames]
 
 
-class ConnEvent(Event, _ConnIdxMixin):
-    'Event with .conn_idx.'
-
-
-class InitConnWindowEvent(ConnEvent, PayloadMixin):
-    'Event to trigger TUI making ConnectionWindow.'
-    payload: LoginNames
+class ConnEvent(Event, ConnMixin):
+    'Event with .conn.'
 
 
 class _ConnectedEvent(ConnEvent):
@@ -177,68 +171,62 @@ class NickSetEvent(ConnEvent):
 
 class _SendEvent(ConnEvent, PayloadMixin):
     'Event to trigger sending of payload to server.'
-    payload: '_IrcMessage'
-
-
-class BroadcastConnMixin(BroadcastMixin, _ConnIdxMixin):
-    'Provides .broadcast_conn on classes that have .conn_idx defined.'
+    payload: _IrcMessage
 
-    def broadcast_conn[E: ConnEvent](self,
-                                     event_class: type[E],
-                                     *args, **kwargs
-                                     ) -> None:
-        'Broadcast event subclassing ConnEvent, with connection ID.'
-        self.broadcast(event_class, conn_idx=self.conn_idx, *args, **kwargs)
 
-    def send(self, verb: str, parameters: tuple[str, ...]) -> None:
-        'Broadcast _SendEvent for _IrcMessage(verb, parameters).'
-        self.broadcast_conn(_SendEvent, _IrcMessage(verb, parameters))
-
-
-class IrcConnection(BroadcastConnMixin):
+class IrcConnection(BroadcastMixin):
     'Abstracts socket connection, loop over it, and handling messages from it.'
 
     def __init__(self,
-                 q_to_main: EventQueue,
-                 idx: int,
                  hostname: str,
                  login: LoginNames,
+                 **kwargs
                  ) -> None:
-        super().__init__(conn_idx=idx, q_to_main=q_to_main)
+        super().__init__(**kwargs)
         self._hostname = hostname
-        self._login = login
+        self.login = login
         self._socket: Optional[socket] = None
         self._assumed_open = False
         self._recv_loop: Optional[_RecvLoop] = None
-        self.broadcast_conn(InitConnWindowEvent, self._login)
         self._start_connecting()
 
     def _start_connecting(self) -> None:
 
         def connect(self) -> None:
             self._socket = socket()
-            self.broadcast_conn(LogConnEvent,
-                                f'Connecting to {self._hostname} …')
+            self.broadcast(LogConnEvent,
+                           f'Connecting to {self._hostname} …')
             self._socket.settimeout(_TIMEOUT_CONNECT)
             try:
                 self._socket.connect((self._hostname, _PORT))
             except (TimeoutError, socket_gaierror) as e:
-                self.broadcast_conn(LogConnEvent, f'ALERT: {e}')
+                self.broadcast(LogConnEvent, f'ALERT: {e}')
                 return
             self._socket.settimeout(TIMEOUT_LOOP)
             self._assumed_open = True
-            self._recv_loop = _RecvLoop(self,
+            self._recv_loop = _RecvLoop(conn=self,
                                         q_to_main=self._q_to_main,
                                         bonus_iterator=self._read_lines())
-            self.broadcast_conn(_ConnectedEvent)
+            self.broadcast(_ConnectedEvent)
 
         Thread(target=connect, daemon=True, args=(self,)).start()
 
+    def broadcast[E: Event](self,
+                            event_class: type[E],
+                            *args, **kwargs
+                            ) -> None:
+        'Broadcast event subclassing ConnEvent, with self as its .conn.'
+        super().broadcast(event_class, conn=self, *args, **kwargs)
+
+    def send(self, verb: str, parameters: tuple[str, ...]) -> None:
+        'Broadcast _SendEvent for _IrcMessage(verb, parameters).'
+        self.broadcast(_SendEvent, _IrcMessage(verb, parameters))
+
     def update_login(self, **kwargs) -> None:
-        'Adapt ._login attributes to kwargs, broadcast NickSetEvent.'
+        'Adapt .login attributes to kwargs, broadcast NickSetEvent.'
         for key, val in kwargs.items():
-            setattr(self._login, key, val)
-        self.broadcast_conn(NickSetEvent)
+            setattr(self.login, key, val)
+        self.broadcast(NickSetEvent)
 
     def close(self) -> None:
         'Close both RecvLoop and socket.'
@@ -285,9 +273,8 @@ class IrcConnection(BroadcastConnMixin):
     def _write_line(self, line: str) -> None:
         'Send line-separator-delimited message over socket.'
         if not (self._socket and self._assumed_open):
-            self.broadcast_conn(LogConnEvent,
-                                'ALERT: cannot send, assuming connection '
-                                'closed')
+            self.broadcast(LogConnEvent,
+                           'ALERT: cannot send, assuming connection closed.')
             return
         self._socket.sendall(line.encode('utf-8') + _IRCSPEC_LINE_SEPARATOR)
 
@@ -295,39 +282,35 @@ class IrcConnection(BroadcastConnMixin):
         'Process connection-directed Event into further steps.'
         if isinstance(event, InitReconnectEvent):
             if self._assumed_open:
-                self.broadcast_conn(LogConnEvent,
-                                    'ALERT: Reconnect called, but still seem '
-                                    'connected, so nothing to do.')
+                self.broadcast(LogConnEvent,
+                               'ALERT: Reconnect called, but still seem '
+                               'connected, so nothing to do.')
             else:
                 self._start_connecting()
         elif isinstance(event, _ConnectedEvent):
             # self.send('CAP', ('LS', '302'))
-            self.send('USER', (self._login.user, '0', '*', self._login.real))
-            self.send('NICK', (self._login.nick,))
+            self.send('USER', (self.login.user, '0', '*', self.login.real))
+            self.send('NICK', (self.login.nick,))
             # self.send('CAP', ('LIST',))
             # self.send('CAP', ('END',))
         elif isinstance(event, _DisconnectedEvent):
             self.close()
         elif isinstance(event, _SendEvent):
-            self.broadcast_conn(LogConnEvent, f'->: {event.payload.raw}')
+            self.broadcast(LogConnEvent, f'->: {event.payload.raw}')
             self._write_line(event.payload.raw)
 
 
-class _RecvLoop(Loop):
+class _RecvLoop(Loop, ConnMixin):
     'Loop to react on messages from server.'
 
-    def __init__(self, conn: IrcConnection, **kwargs) -> None:
-        super().__init__(**kwargs)
-        self._conn = conn
-
     def process_bonus(self, yielded: str) -> None:
         msg = _IrcMessage.from_raw(yielded)
-        self._conn.broadcast_conn(LogConnEvent, f'<-: {msg.raw}')
+        self.conn.broadcast(LogConnEvent, f'<-: {msg.raw}')
         if msg.verb == 'PING':
-            self._conn.send('PONG', (msg.parameters[0],))
+            self.conn.send('PONG', (msg.parameters[0],))
         elif msg.verb == 'ERROR'\
                 and msg.parameters[0].startswith('Closing link:'):
-            self._conn.broadcast_conn(_DisconnectedEvent)
+            self.conn.broadcast(_DisconnectedEvent)
         elif msg.verb in {'001', 'NICK'}:
-            self._conn.update_login(nick=msg.parameters[0],
-                                    nick_confirmed=True)
+            self.conn.update_login(nick=msg.parameters[0],
+                                   nick_confirmed=True)
index 3c1a8021de733ceaa9a33fcc91aa18aa091ef71b..d05db647419a73e28302799e1926c039b01b8d92 100644 (file)
@@ -13,9 +13,8 @@ from blessed import Terminal as BlessedTerminal
 from ircplom.events import (BroadcastMixin, Event, EventQueue, Loop,
                             PayloadMixin, QuitEvent)
 from ircplom.irc_conn import (
-        BroadcastConnMixin, ConnEvent, InitConnectEvent, InitConnWindowEvent,
-        InitReconnectEvent, LoginNames, LogConnEvent, NickSetEvent,
-        TIMEOUT_LOOP)
+        ConnEvent, ConnMixin, InitConnectEvent, InitReconnectEvent,
+        LoginNames, LogConnEvent, NickSetEvent, TIMEOUT_LOOP)
 
 _MIN_HEIGHT = 4
 _MIN_WIDTH = 32
@@ -73,8 +72,8 @@ class _Widget(ABC):
     'Defines most basic TUI object API.'
 
     @abstractmethod
-    def __init__(self, *args, **kwargs) -> None:
-        super().__init__(*args, **kwargs)
+    def __init__(self, **kwargs) -> None:
+        super().__init__(**kwargs)
         self.tainted = True
         self._drawable = False
 
@@ -98,8 +97,8 @@ class _ScrollableWidget(_Widget, ABC):
     'Defines some API shared between _PromptWidget and _LogWidget.'
     _history_idx: int
 
-    def __init__(self, write: Callable[..., None], *args, **kwargs) -> None:
-        super().__init__(*args, **kwargs)
+    def __init__(self, write: Callable[..., None], **kwargs) -> None:
+        super().__init__(**kwargs)
         self._write = write
         self._history: list[str] = []
 
@@ -121,9 +120,9 @@ class _LogWidget(_ScrollableWidget):
     _view_size: _YX
     _y_pgscroll: int
 
-    def __init__(self, wrap: Callable[[str], list[str]], *args, **kwargs
+    def __init__(self, wrap: Callable[[str], list[str]], **kwargs
                  ) -> None:
-        super().__init__(*args, **kwargs)
+        super().__init__(**kwargs)
         self._wrap = wrap
         self._wrapped_idx = self._history_idx = -1
         self._wrapped: list[tuple[Optional[int], str]] = []
@@ -201,8 +200,8 @@ class _PromptWidget(_ScrollableWidget):
     _input_buffer_unsafe: str
     _cursor_x: int
 
-    def __init__(self, *args, **kwargs) -> None:
-        super().__init__(*args, **kwargs)
+    def __init__(self, **kwargs) -> None:
+        super().__init__(**kwargs)
         self._reset_buffer('')
 
     @property
@@ -310,14 +309,13 @@ class _PromptWidget(_ScrollableWidget):
         return to_return
 
 
-class _ConnectionPromptWidget(_PromptWidget):
+class _ConnectionPromptWidget(_PromptWidget, ConnMixin):
     'PromptWidget with attributes, methods for dealing with an IrcConnection.'
-    login: LoginNames
 
     @property
     def _prompt(self) -> str:
-        return ((' ' if self.login.nick_confirmed else '?')
-                + self.login.nick
+        return ((' ' if self.conn.login.nick_confirmed else '?')
+                + self.conn.login.nick
                 + super()._prompt)
 
 
@@ -330,8 +328,9 @@ class _Window(_Widget):
         super().__init__(**kwargs)
         self.idx = idx
         self._term = term
-        self.log = _LogWidget(self._term.wrap, self._term.write)
-        self.prompt = self.__annotations__['prompt'](self._term.write)
+        self.log = _LogWidget(wrap=self._term.wrap, write=self._term.write)
+        self.prompt = self.__annotations__['prompt'](write=self._term.write,
+                                                     **kwargs)
         if hasattr(self._term, 'size'):
             self.set_geometry()
 
@@ -383,25 +382,21 @@ class _Window(_Widget):
             widget.draw()
 
 
-class _ConnectionWindow(_Window, BroadcastConnMixin):
+class _ConnectionWindow(_Window, ConnMixin):
     'Window with attributes and methods for dealing with an IrcConnection.'
     prompt: _ConnectionPromptWidget
 
-    def __init__(self, login: LoginNames, **kwargs) -> None:
-        super().__init__(**kwargs)
-        self.prompt.login = login
-
     def cmd__disconnect(self, quit_msg: str = 'ircplom says bye') -> None:
         'Send QUIT command to server.'
-        self.send('QUIT', (quit_msg, ))
+        self.conn.send('QUIT', (quit_msg, ))
 
     def cmd__reconnect(self) -> None:
         'Attempt reconnection.'
-        self.broadcast_conn(InitReconnectEvent)
+        self.conn.broadcast(InitReconnectEvent)
 
     def cmd__nick(self, new_nick: str) -> None:
         'Attempt nickname change.'
-        self.send('NICK', (new_nick, ))
+        self.conn.send('NICK', (new_nick, ))
 
 
 class _KeyboardLoop(Loop, BroadcastMixin):
@@ -472,17 +467,18 @@ class _TuiLoop(Loop, BroadcastMixin):
             cmd = self._cmd_name_to_cmd(event.payload[0])
             assert cmd is not None
             cmd(*event.payload[1:])
-        elif isinstance(event, InitConnWindowEvent):
-            conn_win = _ConnectionWindow(q_to_main=self._q_to_main,
-                                         conn_idx=event.conn_idx,
-                                         idx=len(self._windows),
-                                         term=self._term,
-                                         login=event.payload)
-            self._windows += [conn_win]
-            self._conn_windows += [conn_win]
-            self._switch_window(conn_win.idx)
         elif isinstance(event, ConnEvent):
-            conn_win = self._conn_windows[event.conn_idx]
+            matching_wins = [cw for cw in self._conn_windows
+                             if cw.conn == event.conn]
+            if matching_wins:
+                conn_win = matching_wins[0]
+            else:
+                conn_win = _ConnectionWindow(idx=len(self._windows),
+                                             conn=event.conn,
+                                             term=self._term)
+                self._windows += [conn_win]
+                self._conn_windows += [conn_win]
+                self._switch_window(conn_win.idx)
             if isinstance(event, LogConnEvent):
                 conn_win.log.append(event.payload)
             elif isinstance(event, NickSetEvent):