home · contact · privacy
Split too-long chat message into truncated first part and continuation prompt proposa... master
authorPlom Heller <plom@plomlompom.com>
Sun, 8 Mar 2026 04:28:27 +0000 (05:28 +0100)
committerPlom Heller <plom@plomlompom.com>
Sun, 8 Mar 2026 04:28:27 +0000 (05:28 +0100)
src/ircplom/client.py
src/ircplom/client_tui.py
src/ircplom/irc_conn.py
src/ircplom/testing.py
src/run.py
src/tests/linelen.test [new file with mode: 0644]

index b5b6343897c99881c07026e3f20bd4a09e6fcd59..2e47246025b273247c519a88b4120168891c91dc 100644 (file)
@@ -155,10 +155,15 @@ class IrcConnection(BaseIrcConnection, _ClientIdMixin):
     'Parent extended to work with Client.'
     hostname: InitVar[str]  # needed by BaseIrcConnection, but not desired as
     port: InitVar[int]      # dataclass fields, only for __post_init__ call
+    len_userprefix: InitVar[Callable]
 
-    def __post_init__(self, hostname, port, **kwargs) -> None:
-        super().__init__(hostname=hostname, port=port, _q_out=self._q_out,
-                         **kwargs)
+    def __post_init__(self, hostname, port, len_userprefix, **kwargs) -> None:
+        super().__init__(
+                hostname=hostname,
+                port=port,
+                len_userprefix=len_userprefix,
+                _q_out=self._q_out,
+                **kwargs)
 
     def _make_recv_event(self, msg: IrcMessage) -> 'ClientEvent':
         return ClientEvent.affector('handle_msg', client_id=self.client_id
@@ -715,8 +720,11 @@ class Client(ABC, ClientQueueMixin):
         def connect(self) -> None:
             try:
                 self.conn = self._cls_conn(
-                    hostname=self.db.hostname, port=self.db.port,
-                    _q_out=self._q_out, client_id=self.client_id)
+                    hostname=self.db.hostname,
+                    port=self.db.port,
+                    len_userprefix=self._len_userprefix,
+                    _q_out=self._q_out,
+                    client_id=self.client_id)
             except IrcConnException as e:
                 self._client_trigger('_on_connecting_exception', e=e)
             # pylint: disable=broad-exception-caught
@@ -728,6 +736,11 @@ class Client(ABC, ClientQueueMixin):
         # Do this in a thread, not to block flow of other (e.g. TUI) events.
         Thread(target=connect, daemon=True, args=(self,)).start()
 
+    def _len_userprefix(self) -> int:
+        if 'me' in self.db.users.keys():
+            return len(f' :{self.db.users["me"]} ')
+        return 0
+
     @property
     def _retry_connect_on(self) -> bool:
         return self._retry_connect_in_s > self._RETRY_CONNECT_IN_S_OFF
index 56f2ca72ce57950c3020d9457c22c49763238f3c..4ab012d1664ba764cac2f2120f70df9b44f50f4b 100644 (file)
@@ -37,6 +37,8 @@ LOG_FMT_ATTRS[_LOG_PREFIX_SERVER] = ('bright_yellow',)
 _PATH_LOGS = Path.home().joinpath('.local', 'share', 'ircplom', 'logs')
 _PATH_CONFIG = Path.home().joinpath('.config', 'ircplom', 'ircplom.toml')
 
+_PROMPT_CUT_ELL = '…'
+
 _DbLinked = DbLinked['_TuiClientDb']
 
 
@@ -55,8 +57,14 @@ class _ClientWindow(Window, ClientQueueMixin):
     _logpath_prefix = '!'
     _title: str
 
-    def __init__(self, path_logs: Optional[Path], **kwargs) -> None:
+    def __init__(
+            self,
+            path_logs: Optional[Path],
+            privmsg_lenfail: Callable[[str, str], bool],
+            **kwargs
+            ) -> None:
         self._path_logs = path_logs
+        self._privmsg_lenfail = privmsg_lenfail
         super().__init__(**kwargs)
 
     @property
@@ -107,9 +115,20 @@ class _ClientWindow(Window, ClientQueueMixin):
         'Attempt joining a channel.'
         self._client_trigger('cmd__join', chan_name=channel)
 
+    def _privmsg(self, target: str, msg: str, as_privmsg: bool) -> None:
+        full_msg = msg[:]
+        while self._privmsg_lenfail(target, msg):
+            msg = msg[:-(1 + len(_PROMPT_CUT_ELL))] + _PROMPT_CUT_ELL
+        if len(full_msg) > len(msg):
+            self.prompt.input_buffer = (
+                    (int(as_privmsg) * f'/privmsg {target} ')
+                    + _PROMPT_CUT_ELL
+                    + full_msg[len(msg) - len(_PROMPT_CUT_ELL):])
+        self._client_trigger('cmd__privmsg', chat_target=target, msg=msg)
+
     def cmd__privmsg(self, target: str, msg: str) -> None:
         'Send chat message msg to target.'
-        self._client_trigger('cmd__privmsg', chat_target=target, msg=msg)
+        self._privmsg(target, msg, as_privmsg=True)
 
     def cmd__raw(self, verb: str, params_str: str = '') -> None:
         'Send raw command, with direct input of params string.'
@@ -156,8 +175,7 @@ class _ChatWindow(_ClientWindow):
     _title_separator = '/'
     _logpath_prefix = ''
 
-    def __init__(self, title: str, get_nick_data: Callable, **kwargs
-                 ) -> None:
+    def __init__(self, title: str, get_nick_data: Callable, **kwargs) -> None:
         self._title = title
         self._get_nick_data = get_nick_data
         super().__init__(**kwargs)
@@ -170,7 +188,7 @@ class _ChatWindow(_ClientWindow):
 
     def cmd__chat(self, msg: str) -> None:
         'PRIVMSG to target identified by .chatname.'
-        self.cmd__privmsg(target=self.chatname, msg=msg)
+        self._privmsg(self.chatname, msg, as_privmsg=False)
 
 
 class _ChannelWindow(_ChatWindow):
@@ -435,33 +453,45 @@ class _ClientWindowsManager:
                  tui_log: Callable,
                  tui_new_window: Callable,
                  path_logs: Optional[Path],
-                 to_highlight: tuple[str, ...]
+                 to_highlight: tuple[str, ...],
                  ) -> None:
         self._tui_log = tui_log
         self._tui_new_window = tui_new_window
         self._path_logs = path_logs
         self._to_highlight = to_highlight
+        self._privmsg_lenfail = lambda target, msg: False
         self.db = _TuiClientDb()
         self.windows: list[_ClientWindow] = []
 
     def _new_win(self, scope: _LogScope, title: str = '') -> _ClientWindow:
+        kwargs = {'path_logs': self._path_logs,
+                  # pylint: disable=unnecessary-lambda  # to forward updated
+                  'privmsg_lenfail': lambda a, b: self._privmsg_lenfail(a, b)}
         if scope == _LogScope.CHAT:
             win = self._tui_new_window(
-                    title=title, path_logs=self._path_logs,
+                    title=title,
                     win_cls=(_ChannelWindow if self.db.is_chan_name(title)
                              else _QueryWindow),
                     get_nick_data=lambda: (self.db.users['me'].nick
                                            if 'me' in self.db.users.keys()
                                            else '?'),
-                    get_completables=lambda: self._get_chat_usernames(title))
+                    get_completables=lambda: self._get_chat_usernames(title),
+                    **kwargs)
         else:
             win = self._tui_new_window(
-                    path_logs=self._path_logs,
                     win_cls=(_ServerWindow if scope == _LogScope.SERVER
-                             else _DebugWindow))
+                             else _DebugWindow),
+                    **kwargs)
         self.windows += [win]
         return win
 
+    def set_privmsg_lenfail(
+            self,
+            privmsg_lenfail: Callable[[str, str], bool]
+            ) -> None:
+        "For setting privmsg-too-long tester from connection once it's set up."
+        self._privmsg_lenfail = privmsg_lenfail
+
     def _get_chat_usernames(self, title: str) -> tuple[str, ...]:
         if not self.db.is_chan_name(title):
             return (title,)
@@ -560,6 +590,19 @@ class ClientKnowingTui(Client):
         self._tui_trigger('for_client_do', client_id=self.client_id,
                           todo=todo, **kwargs)
 
+    def _on_connected(self) -> None:
+        self._client_tui_trigger('set_privmsg_lenfail',
+                                 privmsg_lenfail=self._privmsg_lenfail)
+        super()._on_connected()
+
+    def _privmsg_lenfail(self, target: str, msg: str) -> bool:
+        if self.conn:
+            too_long = self.conn.make_sendable(
+                    IrcMessage('PRIVMSG', (target, msg)), only_test=True)
+            assert isinstance(too_long, bool)
+            return too_long
+        return False
+
     def send_w_params_tuple(self, verb: str, params: tuple[str, ...]) -> None:
         'Helper for ClientWindow to trigger .send, for it can only do kwargs.'
         self.send(verb, *params)
index 8e1d94d009a0c0498a07d43177790620625b39e3..7db21b3933b6d4d464caf1280da07f95735c3ff3 100644 (file)
@@ -186,7 +186,14 @@ class BaseIrcConnection(QueueMixin, ABC):
     _recv_buffer_linesep: bytes
     _recv_last_ping_check: datetime
 
-    def __init__(self, hostname: str, port: int, **kwargs) -> None:
+    def __init__(
+            self,
+            hostname: str,
+            port: int,
+            len_userprefix: Callable[[], int],
+            **kwargs
+            ) -> None:
+        self._len_userprefix = len_userprefix
         super().__init__(**kwargs)
         self.ssl = port == PORT_SSL
         self._set_up_socket(hostname, port)
@@ -211,15 +218,21 @@ class BaseIrcConnection(QueueMixin, ABC):
         self._recv_loop.stop()
         self._socket.close()
 
-    def _make_sendable(self, msg: IrcMessage) -> bytes:
+    def make_sendable(self, msg: IrcMessage, only_test=False) -> bytes | bool:
+        'Format msg into sendable, test for proper length.'
         to_send = msg.raw.encode('utf-8') + _IRCSPEC_LINE_SEPARATOR
-        if len(to_send) > _LEN_MAX_MESSAGE:
+        too_long = self._len_userprefix() + len(to_send) > _LEN_MAX_MESSAGE
+        if only_test:
+            return too_long
+        if too_long:
             raise SendFail('message too long')
         return to_send
 
     def send(self, msg: IrcMessage) -> None:
         'Send line-separator-delimited message over socket.'
-        self._socket.sendall(self._make_sendable(msg))
+        sendable = self.make_sendable(msg)
+        assert isinstance(sendable, bytes)
+        self._socket.sendall(sendable)
 
     @abstractmethod
     def _make_recv_event(self, msg: IrcMessage) -> Event:
index 53b06c98bd129f70d2a320f122f02865208e3cef..cddd6e23b6e1421ccfffdd8e28431844d5cc01ec 100644 (file)
@@ -13,7 +13,6 @@ from ircplom.client_tui import ClientKnowingTui, ClientTui
 from ircplom.irc_conn import ERR_STR_TIMEOUT, IrcConnException, IrcMessage
 from ircplom.tui_base import StylingString, TerminalInterface, TuiEvent
 
-
 PATH_TESTS = Path('tests')
 _FAKE_TIMEOUT_PORTS_BEYOND = 10000
 
@@ -147,7 +146,7 @@ class _FakeIrcConnection(IrcConnection):
         self._recv_loop.stop()
 
     def send(self, msg: IrcMessage) -> None:
-        self._make_sendable(msg)
+        self.make_sendable(msg)
 
     def _process_recv(self) -> Iterator[Optional[Event]]:
         while True:
@@ -498,8 +497,10 @@ class TestingClientTui(ClientTui):
             assert path_config.is_file()
             self._path_config = path_config
         self._clients = []
-        self._playbook = _Playbook(path=self._path_test, verbose=self._verbose,
-                                   get_client=lambda idx: self._clients[idx])
+        self._playbook = _Playbook(
+                path=self._path_test,
+                verbose=self._verbose,
+                get_client=lambda idx: self._clients[idx])
         super().__init__(**kwargs)
         assert isinstance(self._term, TestTerminal)
         self._playbook.assert_screen_line = self._term.assert_screen_line
index 336203d90c97fb612d31ca1ba8bdd8c93ebc3816..6e64d5dadaaba5e8c2954fc504c5431e5172cf77 100755 (executable)
@@ -18,8 +18,10 @@ except ModuleNotFoundError as e:
     dependency_hint(e)
 
 
-def main_loop(cls_term: type[TerminalInterface], cls_tui: type[BaseTui]
-              ) -> None:
+def main_loop(
+        cls_term: type[TerminalInterface],
+        cls_tui: type[BaseTui]
+        ) -> None:
     'Main execution code / loop.'
     q_events: SimpleQueue = SimpleQueue()
     clients_db: ClientsDb = {}
diff --git a/src/tests/linelen.test b/src/tests/linelen.test
new file mode 100644 (file)
index 0000000..4367eca
--- /dev/null
@@ -0,0 +1,40 @@
+insert ./lib/connect-to-usermode
+insert ./lib/join-empty
+
+× ×--------------------------
+
+insert connect-to-usermode
+insert join-empty [(WIN_ID)=3]
+> /window 3
+# NB: the short delay that follows here probably is due to processing the whole following prompt instruction as individual keypresses each triggering a prompt redraw …
+
+> 1234567 10 234567 20 234567 30 234567 40 234567 50 234567 60 234567 70 234567 80 234567 90 23456 100 23456 110 23456 120 23456 130 23456 140 23456 150 23456 160 23456 170 23456 180 23456 190 23456 200 23456 210 23456 220 23456 230 23456 240 23456 250 23456 260 23456 270 23456 280 23456 290 23456 300 23456 310 23456 320 23456 330 23456 340 23456 350 23456 360 23456 370 23456 380 23456 390 23456 400 23456 410 23456 420 23456 430 23456 440 23456 450 23456 460 23456 470 23456 480 23456 490 23456 500 23456 510 23456 520 23456 530 23456 540 23456 550 23456 560 23456 570 23456 580 23456 590 23456 600 23456 610 23456 620 23456 630 23456 640 23456 650 23456 660 23456 670 23456 680 23456 690 23456 700 23456 710 23456 720 23456 730 23456 740 23456 750 23456 760 23456 770 23456 780 23456 790 23456 800 23456 810 23456 820 23456 830 23456 840 23456 850 23456 860 23456 870 23456 880 23456 890 23456 900 23456 910 23456 920 23456 930 23456 940 23456 950 23456 960 23456 970 23456 980 23456 990 2345 1000 2345 1010 2345 1020 2345 1030 2345 1040 2345 1050 2345 1060 2345 1070 2345 1080 2345 1090 2345 1100 2345 1110 2345 1120 2345 1130 2345 1140 2345 1150 2345 1160 2345 1170 2345 1180 2345 1190
+log 1 > PRIVMSG #ch_win3 :1234567 10 234567 20 234567 30 234567 40 234567 50 234567 60 234567 70 234567 80 234567 90 23456 100 23456 110 23456 120 23456 130 23456 140 23456 150 23456 160 23456 170 23456 180 23456 190 23456 200 23456 210 23456 220 23456 230 23456 240 23456 250 23456 260 23456 270 23456 280 23456 290 23456 300 23456 310 23456 320 23456 330 23456 340 23456 350 23456 360 23456 370 23456 380 23456 390 23456 400 23456 410 23456 420 23456 430 23456 440 23456 450 23456 460 23456 …
+log 3 > [foo] 1234567 10 234567 20 234567 30 234567 40 234567 50 234567 60 234567 70 234567 80 234567 90 23456 100 23456 110 23456 120 23456 130 23456 140 23456 150 23456 160 23456 170 23456 180 23456 190 23456 200 23456 210 23456 220 23456 230 23456 240 23456 250 23456 260 23456 270 23456 280 23456 290 23456 300 23456 310 23456 320 23456 330 23456 340 23456 350 23456 360 23456 370 23456 380 23456 390 23456 400 23456 410 23456 420 23456 430 23456 440 23456 450 23456 460 23456 …
+line 23 0 on_black,bright_white [foo] §470 23456 480 23456 490 23456 500 23456 510 23456 520 23456 530 23456 5…>
+line 23 6 on_black,bright_white,reverse …§
+
+> just_enter
+log 1 > PRIVMSG #ch_win3 :…470 23456 480 23456 490 23456 500 23456 510 23456 520 23456 530 23456 540 23456 550 23456 560 23456 570 23456 580 23456 590 23456 600 23456 610 23456 620 23456 630 23456 640 23456 650 23456 660 23456 670 23456 680 23456 690 23456 700 23456 710 23456 720 23456 730 23456 740 23456 750 23456 760 23456 770 23456 780 23456 790 23456 800 23456 810 23456 820 23456 830 23456 840 23456 850 23456 860 23456 870 23456 880 23456 890 23456 900 23456 910 23456 920 23456 930 …
+log 3 > [foo] …470 23456 480 23456 490 23456 500 23456 510 23456 520 23456 530 23456 540 23456 550 23456 560 23456 570 23456 580 23456 590 23456 600 23456 610 23456 620 23456 630 23456 640 23456 650 23456 660 23456 670 23456 680 23456 690 23456 700 23456 710 23456 720 23456 730 23456 740 23456 750 23456 760 23456 770 23456 780 23456 790 23456 800 23456 810 23456 820 23456 830 23456 840 23456 850 23456 860 23456 870 23456 880 23456 890 23456 900 23456 910 23456 920 23456 930 …
+line 23 0 on_black,bright_white [foo] §23456 940 23456 950 23456 960 23456 970 23456 980 23456 990 2345 1000 2…>
+line 23 6 on_black,bright_white,reverse …§
+
+> just_enter
+log 1 > PRIVMSG #ch_win3 :…23456 940 23456 950 23456 960 23456 970 23456 980 23456 990 2345 1000 2345 1010 2345 1020 2345 1030 2345 1040 2345 1050 2345 1060 2345 1070 2345 1080 2345 1090 2345 1100 2345 1110 2345 1120 2345 1130 2345 1140 2345 1150 2345 1160 2345 1170 2345 1180 2345 1190
+log 3 > [foo] …23456 940 23456 950 23456 960 23456 970 23456 980 23456 990 2345 1000 2345 1010 2345 1020 2345 1030 2345 1040 2345 1050 2345 1060 2345 1070 2345 1080 2345 1090 2345 1100 2345 1110 2345 1120 2345 1130 2345 1140 2345 1150 2345 1160 2345 1170 2345 1180 2345 1190
+line 23 0 on_black,bright_white [foo] § §§
+line 23 6 on_black,bright_white,reverse  §
+
+> /window 2
+> /privmsg #ch_win3 1234567 10 234567 20 234567 30 234567 40 234567 50 234567 60 234567 70 234567 80 234567 90 23456 100 23456 110 23456 120 23456 130 23456 140 23456 150 23456 160 23456 170 23456 180 23456 190 23456 200 23456 210 23456 220 23456 230 23456 240 23456 250 23456 260 23456 270 23456 280 23456 290 23456 300 23456 310 23456 320 23456 330 23456 340 23456 350 23456 360 23456 370 23456 380 23456 390 23456 400 23456 410 23456 420 23456 430 23456 440 23456 450 23456 460 23456 470 23456 480 23456 490 23456 500
+log 1 > PRIVMSG #ch_win3 :1234567 10 234567 20 234567 30 234567 40 234567 50 234567 60 234567 70 234567 80 234567 90 23456 100 23456 110 23456 120 23456 130 23456 140 23456 150 23456 160 23456 170 23456 180 23456 190 23456 200 23456 210 23456 220 23456 230 23456 240 23456 250 23456 260 23456 270 23456 280 23456 290 23456 300 23456 310 23456 320 23456 330 23456 340 23456 350 23456 360 23456 370 23456 380 23456 390 23456 400 23456 410 23456 420 23456 430 23456 440 23456 450 23456 460 23456 …
+log 3 > [foo] 1234567 10 234567 20 234567 30 234567 40 234567 50 234567 60 234567 70 234567 80 234567 90 23456 100 23456 110 23456 120 23456 130 23456 140 23456 150 23456 160 23456 170 23456 180 23456 190 23456 200 23456 210 23456 220 23456 230 23456 240 23456 250 23456 260 23456 270 23456 280 23456 290 23456 300 23456 310 23456 320 23456 330 23456 340 23456 350 23456 360 23456 370 23456 380 23456 390 23456 400 23456 410 23456 420 23456 430 23456 440 23456 450 23456 460 23456 …
+line 23 0 on_black,bright_white > §privmsg #ch_win3 …470 23456 480 23456 490 23456 500
+line 23 2 on_black,bright_white,reverse /§
+
+> just_enter
+log 1 > PRIVMSG #ch_win3 :…470 23456 480 23456 490 23456 500
+log 3 > [foo] …470 23456 480 23456 490 23456 500
+line 23 0 on_black,bright_white > § §§
+line 23 2 on_black,bright_white,reverse  §