home · contact · privacy
Improve log messaging, differentiate target reach.
authorChristian Heller <c.heller@plomlompom.de>
Sat, 26 Jul 2025 15:23:12 +0000 (17:23 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 26 Jul 2025 15:23:12 +0000 (17:23 +0200)
ircplom/irc_conn.py
ircplom/tui.py

index 192c0965a1bbf6035a8cb43e212a207706179363..de4b33ab8fe31a5ba22f82c3195e3cbc270f25b6 100644 (file)
@@ -11,6 +11,10 @@ from ircplom.events import (
         AffectiveEvent, ExceptionEvent, Loop, PayloadMixin, QueueMixin)
 
 
+ClientsDb = dict[UUID, 'Client']
+
+CHAT_GLOB = '*'
+
 _TIMEOUT_RECV_LOOP = 0.1
 _TIMEOUT_CONNECT = 5
 _CONN_RECV_BUFSIZE = 1024
@@ -23,8 +27,6 @@ _IRCSPEC_TAG_ESCAPES = ((r'\:', ';'),
                         (r'\r', '\r'),
                         (r'\\', '\\'))
 
-ClientsDb = dict[UUID, 'Client']
-
 
 class IrcMessage:
     'Properly structured representation of IRC message as per IRCv3 spec.'
@@ -149,6 +151,7 @@ class ClientEvent(AffectiveEvent, ClientIdMixin):
 class _ConnectedEvent(ClientEvent):
 
     def affect(self, target: 'Client') -> None:
+        target.log(msg='# connected to server', chat=CHAT_GLOB)
         target.send(IrcMessage(verb='USER', params=(getuser(), '0', '*',
                                                     target.realname)))
         target.send(IrcMessage(verb='NICK', params=(target.nickname,)))
@@ -159,8 +162,8 @@ class InitReconnectEvent(ClientEvent):
 
     def affect(self, target: 'Client') -> None:
         if target.assumed_open:
-            target.log('ALERT: Reconnect called, but still seem connected, '
-                       'so nothing to do.')
+            target.log('# ALERT: reconnection called, but still seem '
+                       'connected, so nothing to do.')
         else:
             target.start_connecting()
 
@@ -169,8 +172,8 @@ class SendEvent(ClientEvent, PayloadMixin):
     'To trigger sending of payload to server.'
     payload: IrcMessage
 
-    def affect(self, target: 'Client') -> None:
-        target.send(self.payload)
+    def affect(self, target: 'Client', chat: str = '') -> None:
+        target.send(msg=self.payload, chat=chat)
 
 
 class ClientQueueMixin(QueueMixin):
@@ -184,15 +187,17 @@ class ClientQueueMixin(QueueMixin):
 
 class Client(ABC, ClientQueueMixin):
     'Abstracts socket connection, loop over it, and handling messages from it.'
+    nick_confirmed: bool
+    nickname: str
 
     def __init__(self, hostname: str, nickname: str, realname: str, **kwargs
                  ) -> None:
         super().__init__(**kwargs)
-        self.id_ = uuid4()
         self._hostname = hostname
         self._socket: Optional[socket] = None
-        self.assumed_open = False
         self._recv_loop: Optional[Loop] = None
+        self.id_ = uuid4()
+        self.assumed_open = False
         self.realname = realname
         self.update_login(nick_confirmed=False, nickname=nickname)
         self.start_connecting()
@@ -203,12 +208,12 @@ class Client(ABC, ClientQueueMixin):
         def connect(self) -> None:
             try:
                 self._socket = socket()
-                self.log(f'Connecting to {self._hostname} …')
+                self.log(f'# connecting to server {self._hostname} …')
                 self._socket.settimeout(_TIMEOUT_CONNECT)
                 try:
                     self._socket.connect((self._hostname, _PORT))
                 except (TimeoutError, socket_gaierror) as e:
-                    self.log(f'ALERT: {e}')
+                    self.log(f'ALERT: {e}')
                     return
                 self._socket.settimeout(_TIMEOUT_RECV_LOOP)
                 self.assumed_open = True
@@ -222,24 +227,37 @@ class Client(ABC, ClientQueueMixin):
 
     @abstractmethod
     def log(self, msg: str, chat: str = '') -> None:
-        'Write msg into log, whatever shape that may have.'
+        'Write msg into log of chat, whatever shape that may have.
+
+        Messages to chat=CHAT_GLOB are meant to appear in all widgets mapped to
+        the client, those to chat="" only in the initial connection window.
+        '
 
-    def send(self, msg: IrcMessage) -> None:
+    def send(self, msg: IrcMessage, chat: str = '') -> None:
         'Send line-separator-delimited message over socket.'
         if not (self._socket and self.assumed_open):
-            self.log('ALERT: cannot send, assuming connection closed.')
+            self.log('# ALERT: cannot send, connection seems closed')
             return
         self._socket.sendall(msg.raw.encode('utf-8') + _IRCSPEC_LINE_SEPARATOR)
-        self.log(f'->: {msg.raw}')
+        self.log(msg=f'->: {msg.raw}', chat=chat)
 
     def update_login(self, nick_confirmed: bool, nickname: str = '') -> None:
         'Manage .nickname, .nick_confirmed – useful for subclass extension.'
-        if nickname:
+        first_run = not hasattr(self, 'nickname')
+        prefix = '# nickname'
+        if first_run or (nickname and nickname != self.nickname):
+            verb = 'set' if first_run else f'changed from "{self.nickname}'
             self.nickname = nickname
-        self.nick_confirmed = nick_confirmed
+            self.log(msg=f'{prefix} {verb} to {nickname}', chat=CHAT_GLOB)
+        if first_run or nick_confirmed != self.nick_confirmed:
+            self.nick_confirmed = nick_confirmed
+            if not first_run:
+                self.log(
+                    msg=f'{prefix} {"" if nick_confirmed else "un"}confirmed')
 
     def close(self) -> None:
         'Close both recv Loop and socket.'
+        self.log(msg='# disconnected from server', chat=CHAT_GLOB)
         self.assumed_open = False
         self.update_login(nick_confirmed=False)
         if self._recv_loop:
index 0e3df99d0ba16020af2e94d07631357e174cb137..eb28de7328441be1294df6339fa52be7ab83adb3 100644 (file)
@@ -13,7 +13,7 @@ from blessed import Terminal as BlessedTerminal
 from ircplom.events import (
         AffectiveEvent, Loop, PayloadMixin, QueueMixin, QuitEvent)
 from ircplom.irc_conn import (
-        IrcMessage, Client, ClientIdMixin, ClientQueueMixin,
+        CHAT_GLOB, IrcMessage, Client, ClientIdMixin, ClientQueueMixin,
         InitReconnectEvent, NewClientEvent, SendEvent)
 
 _MIN_HEIGHT = 4
@@ -394,7 +394,7 @@ class _KeyboardEvent(TuiEvent, PayloadMixin):
         elif len(self.payload) == 1:
             target.window.prompt.insert(self.payload)
         else:
-            target.log(f'ALERT: unknown keyboard input: {self.payload}')
+            target.log(f'ALERT: unknown keyboard input: {self.payload}')
         super().affect(target)
 
 
@@ -430,29 +430,36 @@ class Tui(QueueMixin):
         'Currently selected _Window.'
         return self.windows[self._window_idx]
 
+    def _new_client_window(self, client_id: UUID, chat: str = ''
+                           ) -> '_ClientWindow':
+        new_idx = len(self.windows)
+        win = _ClientWindow(idx=new_idx, term=self.term, q_out=self._q_out,
+                            client_id=client_id, chat=chat)
+        self.windows += [win]
+        self._switch_window(new_idx)
+        return win
+
     def client_wins(self, client_id: UUID) -> list['_ClientWindow']:
-        'All _ClientWindows matching client_id.'
-        return [win for win in self.windows
+        'All _ClientWindows matching client_id; if none, create one.'
+        wins = [win for win in self.windows
                 if isinstance(win, _ClientWindow)
                 and win.client_id == client_id]  # pylint: disable=no-member
+        if not wins:
+            wins = [self._new_client_window(client_id=client_id)]
+        return wins
 
     def client_win(self, client_id: UUID, chat: str = '') -> '_ClientWindow':
         '''That _ClientWindow matching client_id and chat; create if none.
 
-        In case of creation, also switch to window, and if not client's first
-        window, copy prompt prefix from client's first window.
+        In case of creation, copy prompt prefix from client's first window.
         '''
         client_wins = self.client_wins(client_id)
         candidates = [win for win in client_wins if win.chat == chat]
         if candidates:
             return candidates[0]
-        new_idx = len(self.windows)
-        win = _ClientWindow(idx=new_idx, term=self.term, q_out=self._q_out,
-                            client_id=client_id, chat=chat)
+        win = self._new_client_window(client_id=client_id, chat=chat)
         if client_wins:
             win.prompt.prefix = client_wins[0].prompt.prefix
-        self.windows += [win]
-        self._switch_window(new_idx)
         return win
 
     def log(self, msg: str) -> None:
@@ -500,7 +507,7 @@ class Tui(QueueMixin):
         else:
             alert = 'not prefixed by /'
         if alert:
-            self.log(f'invalid prompt command: {alert}')
+            self.log(f'# ALERT: invalid prompt command: {alert}')
 
     def cmd__quit(self) -> None:
         'Trigger program exit.'
@@ -649,8 +656,8 @@ class _ClientWindow(_Window, ClientQueueMixin):
 
     def cmd__disconnect(self, quit_msg: str = 'ircplom says bye') -> None:
         'Send QUIT command to server.'
-        self._cput(SendEvent, payload=IrcMessage(verb='QUIT',
-                                                 params=(quit_msg,)))
+        self._cput(SendEvent,
+                   payload=IrcMessage(verb='QUIT', params=(quit_msg,)))
 
     def cmd__reconnect(self) -> None:
         'Attempt reconnection.'
@@ -658,8 +665,8 @@ class _ClientWindow(_Window, ClientQueueMixin):
 
     def cmd__nick(self, new_nick: str) -> None:
         'Attempt nickname change.'
-        self._cput(SendEvent, payload=IrcMessage(verb='NICK',
-                                                 params=(new_nick,)))
+        self._cput(SendEvent,
+                   payload=IrcMessage(verb='NICK', params=(new_nick,)))
 
 
 class _ClientWindowEvent(TuiEvent, ClientIdMixin):
@@ -673,7 +680,12 @@ class _ClientLogEvent(_ClientWindowEvent, PayloadMixin):
     payload: str
 
     def affect(self, target: Tui) -> None:
-        target.client_win(self.client_id, self.chat).log.append(self.payload)
+        if self.chat == CHAT_GLOB:
+            for win in target.client_wins(self.client_id):
+                win.log.append(self.payload)
+        else:
+            target.client_win(self.client_id, self.chat
+                              ).log.append(self.payload)
         super().affect(target)