home · contact · privacy
Add most basic nickname autocompletion. master
authorChristian Heller <c.heller@plomlompom.de>
Tue, 9 Dec 2025 17:31:39 +0000 (18:31 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Tue, 9 Dec 2025 17:31:39 +0000 (18:31 +0100)
src/ircplom/client_tui.py
src/ircplom/tui_base.py
src/tests/lib/enter_misc
src/tests/test.test
src/tests/tui_draw.test

index 323e29db2f293814c5c24d4b1d1eb01e439ed12c..18c9be6e811f1f73bde05d42e739ad9d5143fb29 100644 (file)
@@ -431,13 +431,24 @@ class _ClientWindowsManager:
                              else _QueryWindow),
                     get_nick_data=lambda: (self.db.users['me'].nick
                                            if 'me' in self.db.users.keys()
                              else _QueryWindow),
                     get_nick_data=lambda: (self.db.users['me'].nick
                                            if 'me' in self.db.users.keys()
-                                           else '?'))
+                                           else '?'),
+                    get_completables=lambda: self._get_chat_usernames(title))
         else:
         else:
-            win = self._tui_new_window(path_logs=self._path_logs, win_cls=(
-                _ServerWindow if scope == _LogScope.SERVER else _DebugWindow))
+            win = self._tui_new_window(
+                    path_logs=self._path_logs,
+                    win_cls=(_ServerWindow if scope == _LogScope.SERVER
+                             else _DebugWindow))
         self.windows += [win]
         return win
 
         self.windows += [win]
         return win
 
+    def _get_chat_usernames(self, title: str) -> tuple[str, ...]:
+        if not self.db.is_chan_name(title):
+            return (title,)
+        if title not in self.db.channels.keys():  # ChannelWindow may call for
+            return tuple()                        # channel we're atm not in
+        return tuple(sorted([self.db.users[user_id].nick
+                             for user_id in self.db.channels[title].user_ids]))
+
     def windows_for(self, scope: _LogScope, id_='') -> list[_ClientWindow]:
         'Return client windows of scope, and additional potential identifier.'
         ret = []
     def windows_for(self, scope: _LogScope, id_='') -> list[_ClientWindow]:
         'Return client windows of scope, and additional potential identifier.'
         ret = []
index a44d4f79ea658c8cf8eaa2a8a68addc9ff8d2ad6..07d51c319f0bd6fd9a451201322f1ab2db70a81c 100644 (file)
@@ -52,6 +52,7 @@ _KEYBINDINGS = {
     'esc:91:49:59:51:68': ('window', 'left'),
     'esc:91:49:59:51:67': ('window', 'right'),
     'KEY_F1': ('window.paste',),
     'esc:91:49:59:51:68': ('window', 'left'),
     'esc:91:49:59:51:67': ('window', 'right'),
     'KEY_F1': ('window.paste',),
+    'KEY_TAB': ('window.prompt.autocomplete',),
 }
 CMD_SHORTCUTS: dict[str, str] = {}
 
 }
 CMD_SHORTCUTS: dict[str, str] = {}
 
@@ -506,7 +507,11 @@ class PromptWidget(_ScrollableWidget):
     _input_buffer_unsafe: str
     _cursor_x: int
 
     _input_buffer_unsafe: str
     _cursor_x: int
 
-    def __init__(self, **kwargs) -> None:
+    def __init__(self,
+                 get_completables: Callable[[], tuple[str, ...]] = tuple,
+                 **kwargs
+                 ) -> None:
+        self._get_completables = get_completables
         super().__init__(**kwargs)
         self._reset_buffer('')
 
         super().__init__(**kwargs)
         self._reset_buffer('')
 
@@ -599,6 +604,21 @@ class PromptWidget(_ScrollableWidget):
             self._archive_prompt()  # empties .input_buffer, thus use to_return
         return to_return
 
             self._archive_prompt()  # empties .input_buffer, thus use to_return
         return to_return
 
+    def cmd__autocomplete(self) -> None:
+        'Insert completion of current (space-separated) word if available.'
+        start_x = self._cursor_x
+        while start_x > 0 and self.input_buffer[start_x - 1] != ' ':
+            start_x -= 1
+        word_start = self.input_buffer[start_x:self._cursor_x]
+        options = tuple(word for word in self._get_completables()
+                        if word.startswith(word_start))
+        if len(options) == 1:
+            to_insert = options[0][len(word_start):]
+            if self.input_buffer[self._cursor_x:
+                                 self._cursor_x + 1] not in ('', ' '):
+                to_insert += ' '
+            self.insert(to_insert)
+
 
 class _StatusLine(_WidgetAtom):
 
 
 class _StatusLine(_WidgetAtom):
 
@@ -645,6 +665,7 @@ class Window(_Widget):
                  write: Callable[[int, str | StylingString], None],
                  len_to_term: Callable[[str], int],
                  maxlen_log: int,
                  write: Callable[[int, str | StylingString], None],
                  len_to_term: Callable[[str], int],
                  maxlen_log: int,
+                 get_completables: Callable[[], tuple[str, ...]] = tuple,
                  **kwargs
                  ) -> None:
         super().__init__(**kwargs)
                  **kwargs
                  ) -> None:
         super().__init__(**kwargs)
@@ -653,7 +674,9 @@ class Window(_Widget):
         self.history = _HistoryWidget(maxlen_log=maxlen_log,
                                       len_to_term=len_to_term,
                                       write=self._write)
         self.history = _HistoryWidget(maxlen_log=maxlen_log,
                                       len_to_term=len_to_term,
                                       write=self._write)
-        self.prompt = self.__annotations__['prompt'](write=self._write)
+        self.prompt = self.__annotations__['prompt'](
+                write=self._write,
+                get_completables=get_completables)
 
     def ensure_date(self, today: str) -> None:
         'Log date of today if it has not been logged yet.'
 
     def ensure_date(self, today: str) -> None:
         'Log date of today if it has not been logged yet.'
index 096096074eaed66a5081b4bb70757a3db7f6298e..37c751e9201fab1de98b47ac4364c215234de0b3 100644 (file)
@@ -9,6 +9,7 @@ log 0 #   /quit
 log 0 #   /window TOWARDS
 log 0 #   /window.history.scroll DIRECTION
 log 0 #   /window.paste
 log 0 #   /window TOWARDS
 log 0 #   /window.history.scroll DIRECTION
 log 0 #   /window.paste
+log 0 #   /window.prompt.autocomplete
 log 0 #   /window.prompt.backspace
 log 0 #   /window.prompt.move_cursor DIRECTION
 log 0 #   /window.prompt.scroll DIRECTION
 log 0 #   /window.prompt.backspace
 log 0 #   /window.prompt.move_cursor DIRECTION
 log 0 #   /window.prompt.scroll DIRECTION
index b747218fde19c9cbc3760e246477ec3d561ed950..235dd5d97b18918ba6c06ca1109c5094009c78a5 100644 (file)
@@ -8,6 +8,8 @@ insert ./lib/disconnect
 # for: disconnect0, disconnect1
 insert ./lib/disconnect-to-stop-auto-reconnect
 insert ./lib/enter-list-start
 # for: disconnect0, disconnect1
 insert ./lib/disconnect-to-stop-auto-reconnect
 insert ./lib/enter-list-start
+insert ./lib/enter_misc
+# for: enter-help-win0
 insert ./lib/isupport-clear
 insert ./lib/join-empty
 # for: join-channel-0-cmd-to-list-residents, join-channel-1-end-of-names
 insert ./lib/isupport-clear
 insert ./lib/join-empty
 # for: join-channel-0-cmd-to-list-residents, join-channel-1-end-of-names
@@ -192,29 +194,24 @@ log 0 #   1) foo.bar.baz:debug
 log 0 #   2) foo.bar.baz:server
 log 0 #   3) foo.bar.baz/SaslServ
 > /window 1
 log 0 #   2) foo.bar.baz:server
 log 0 #   3) foo.bar.baz/SaslServ
 > /window 1
-> /help
-log 1 # commands available in this window:
-log 1 #   /connect HOST_PORT [NICKNAME_PW] [REALNAME_USERNAME]
+insert enter-help-win0 range=:3 [% log%0%=log%1%]
 log 1 #   /disconnect [QUIT_MSG]
 log 1 #   /disconnect [QUIT_MSG]
-log 1 #   /help
+insert enter-help-win0 range=3:4 [% log%0%=log%1%]
 log 1 #   /join CHANNEL
 log 1 #   /join CHANNEL
-log 1 #   /list
+insert enter-help-win0 range=4:5 [% log%0%=log%1%]
 log 1 #   /nick NEW_NICK
 log 1 #   /privmsg TARGET MSG
 log 1 #   /nick NEW_NICK
 log 1 #   /privmsg TARGET MSG
-log 1 #   /prompt_enter
-log 1 #   /quit
+insert enter-help-win0 range=5:7 [% log%0%=log%1%]
 log 1 #   /raw VERB [PARAMS_STR]
 log 1 #   /reconnect
 log 1 #   /raw VERB [PARAMS_STR]
 log 1 #   /reconnect
-log 1 #   /window TOWARDS
+insert enter-help-win0 range=7:8 [% log%0%=log%1%]
 log 1 #   /window.disconnect [QUIT_MSG]
 log 1 #   /window.disconnect [QUIT_MSG]
-log 1 #   /window.history.scroll DIRECTION
+insert enter-help-win0 range=8:9 [% log%0%=log%1%]
 log 1 #   /window.join CHANNEL
 log 1 #   /window.nick NEW_NICK
 log 1 #   /window.join CHANNEL
 log 1 #   /window.nick NEW_NICK
-log 1 #   /window.paste
+insert enter-help-win0 range=9:10 [% log%0%=log%1%]
 log 1 #   /window.privmsg TARGET MSG
 log 1 #   /window.privmsg TARGET MSG
-log 1 #   /window.prompt.backspace
-log 1 #   /window.prompt.move_cursor DIRECTION
-log 1 #   /window.prompt.scroll DIRECTION
+insert enter-help-win0 range=10: [% log%0%=log%1%]
 log 1 #   /window.raw VERB [PARAMS_STR]
 log 1 #   /window.reconnect
 
 log 1 #   /window.raw VERB [PARAMS_STR]
 log 1 #   /window.reconnect
 
index 6947ed6d453aae5c3caf37d96bf09a114fc9c391..727d753cd594869d361c865b14d69f7242ca96cc 100644 (file)
@@ -102,17 +102,17 @@ insert line-tui-log bump=8 [% ?=%%/quit§§]
 insert line-tui-log bump=9 [% ?=%%/window%TOWARDS§§]
 insert line-tui-log bump=10 [% ?=%%/window.history.scroll%DIRECTION§§]
 insert line-tui-log bump=11 [% ?=%%/window.paste]
 insert line-tui-log bump=9 [% ?=%%/window%TOWARDS§§]
 insert line-tui-log bump=10 [% ?=%%/window.history.scroll%DIRECTION§§]
 insert line-tui-log bump=11 [% ?=%%/window.paste]
-insert line-tui-log bump=12 [% ?=%%/window.prompt.backspace§§]
-insert line-tui-log bump=13 [% ?=%%/window.prompt.move_cursor%DIRECTION§§]
-insert line-tui-log bump=14 [% ?=%%/window.prompt.scroll%DIRECTION§§]
-insert line-invalid-prompt-command-unknown bump=15 [(CMD)=0]
-insert line-invalid-prompt-command-unknown bump=16 [(CMD)=1]
-insert line-invalid-prompt-command-unknown bump=17 [(CMD)=2]
-insert line-invalid-prompt-command-unknown bump=18 [(CMD)=3]
-insert line-invalid-prompt-command-unknown bump=19 [(CMD)=4]
-insert line-invalid-prompt-command-unknown bump=20 [(CMD)=5]
-insert line-invalid-prompt-command-unknown bump=21 [(CMD)=6]
-insert line-invalid-prompt-command-unknown bump=22 [(CMD)=7]
+insert line-tui-log bump=12 [% ?=%%/window.prompt.autocomplete§§]
+insert line-tui-log bump=13 [% ?=%%/window.prompt.backspace§§]
+insert line-tui-log bump=14 [% ?=%%/window.prompt.move_cursor%DIRECTION§§]
+insert line-tui-log bump=15 [% ?=%%/window.prompt.scroll%DIRECTION§§]
+insert line-invalid-prompt-command-unknown bump=16 [(CMD)=0]
+insert line-invalid-prompt-command-unknown bump=17 [(CMD)=1]
+insert line-invalid-prompt-command-unknown bump=18 [(CMD)=2]
+insert line-invalid-prompt-command-unknown bump=19 [(CMD)=3]
+insert line-invalid-prompt-command-unknown bump=20 [(CMD)=4]
+insert line-invalid-prompt-command-unknown bump=21 [(CMD)=5]
+insert line-invalid-prompt-command-unknown bump=22 [(CMD)=6]
 insert line-invalid-prompt-command-unknown bump=23 [(CMD)=foo_0123456789_0123456789_0123456789]
 insert line-invalid-prompt-command bump=24 [(MSG)=/foo_0123456789_0123456789_0123456789_§§]
 insert line-bright-red-bold bump=25 [% ?=%%%unknown]
 insert line-invalid-prompt-command-unknown bump=23 [(CMD)=foo_0123456789_0123456789_0123456789]
 insert line-invalid-prompt-command bump=24 [(MSG)=/foo_0123456789_0123456789_0123456789_§§]
 insert line-bright-red-bold bump=25 [% ?=%%%unknown]
@@ -237,32 +237,32 @@ insert lines-status-prompt-start [(FOCUS_X)=77 (FOCUS_STR)=0 X123456789X12345678
 insert enter-help-win0
 insert lines-empty range=:20
 insert history_0 range=:1 bump=20
 insert enter-help-win0
 insert lines-empty range=:20
 insert history_0 range=:1 bump=20
-insert line-scrolldown bump=21 [% XXXXXX=[14]%v]
-insert lines-status-prompt-start [(FOCUS_X)=72 (FOCUS_STR)=(0:12) X123456789X123456789X=============(§§§§§§§§]
+insert line-scrolldown bump=21 [% XXXXXX=[15]%v]
+insert lines-status-prompt-start [(FOCUS_X)=72 (FOCUS_STR)=(0:13) X123456789X123456789X=============(§§§§§§§§]
 
 # check scroll-down on newer history longer than half a screen width does not fully land at bottom
 > /window.history.scroll down
 insert lines-empty range=:9
 insert history_0 range=:12 bump=9
 
 # check scroll-down on newer history longer than half a screen width does not fully land at bottom
 > /window.history.scroll down
 insert lines-empty range=:9
 insert history_0 range=:12 bump=9
-insert line-scrolldown bump=21 [% XXXXXX=[3]%vv]
-insert lines-status-prompt-start [(FOCUS_X)=73 (FOCUS_STR)=(0:3) X123456789X123456789X==============(§§§§§§§]
+insert line-scrolldown bump=21 [% XXXXXX=[4]%vv]
+insert lines-status-prompt-start [(FOCUS_X)=73 (FOCUS_STR)=(0:4) X123456789X123456789X==============(§§§§§§§]
 
 # check previous scroll-down not hitting bottom be fully reversible
 > /window.history.scroll up
 insert lines-empty range=:20
 insert history_0 range=:1 bump=20
 
 # check previous scroll-down not hitting bottom be fully reversible
 > /window.history.scroll up
 insert lines-empty range=:20
 insert history_0 range=:1 bump=20
-insert line-scrolldown bump=21 [% XXXXXX=[14]%v]
-insert lines-status-prompt-start [(FOCUS_X)=73 (FOCUS_STR)=(0:3) X123456789X123456789X==============(§§§§§§§]
+insert line-scrolldown bump=21 [% XXXXXX=[15]%v]
+insert lines-status-prompt-start [(FOCUS_X)=73 (FOCUS_STR)=(0:4) X123456789X123456789X==============(§§§§§§§]
 > /window.history.scroll down
 insert lines-empty range=:9
 insert history_0 range=:12 bump=9
 > /window.history.scroll down
 insert lines-empty range=:9
 insert history_0 range=:12 bump=9
-insert line-scrolldown bump=21 [% XXXXXX=[3]%vv]
-insert lines-status-prompt-start [(FOCUS_X)=73 (FOCUS_STR)=(0:3) X123456789X123456789X==============(§§§§§§§]
+insert line-scrolldown bump=21 [% XXXXXX=[4]%vv]
+insert lines-status-prompt-start [(FOCUS_X)=73 (FOCUS_STR)=(0:4) X123456789X123456789X==============(§§§§§§§]
 
 # scroll to bottom, check history still growing up even beyond upper fold
 > /window.history.scroll down
 
 # scroll to bottom, check history still growing up even beyond upper fold
 > /window.history.scroll down
-insert lines-empty range=:7
-insert history_0 range=:15 bump=7
+insert lines-empty range=:6
+insert history_0 range=:16 bump=6
 insert lines-status-prompt-start [(FOCUS_X)=77 (FOCUS_STR)=0 X123456789X123456789X==================(§§§]
 insert enter-unknown [?=0]
 insert enter-unknown [?=1]
 insert lines-status-prompt-start [(FOCUS_X)=77 (FOCUS_STR)=0 X123456789X123456789X==================(§§§]
 insert enter-unknown [?=0]
 insert enter-unknown [?=1]
@@ -271,7 +271,6 @@ insert enter-unknown [?=3]
 insert enter-unknown [?=4]
 insert enter-unknown [?=5]
 insert enter-unknown [?=6]
 insert enter-unknown [?=4]
 insert enter-unknown [?=5]
 insert enter-unknown [?=6]
-insert enter-unknown [?=7]
 insert history_0 range=1:23 bump=0
 insert lines-status-prompt-start [(FOCUS_X)=77 (FOCUS_STR)=0 X123456789X123456789X==================(§§§]
 
 insert history_0 range=1:23 bump=0
 insert lines-status-prompt-start [(FOCUS_X)=77 (FOCUS_STR)=0 X123456789X123456789X==================(§§§]