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
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.'
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):
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.'
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)
'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)
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
'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
'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] = []
_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]] = []
_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
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)
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()
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):
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):