From 9e834cf3cd9201c02e0b0f43f1d5ec88e0b47b80 Mon Sep 17 00:00:00 2001 From: vjt-claude Date: Tue, 11 Aug 2026 22:30:00 +0000 Subject: [PATCH] Fix Tab cycling, case handling and completion suffix Six defects, all found by reading the source; none of this was run inside weechat, so verify the behaviour before trusting it. 1. Tab cycling was dead. With more than one match the callback always returned WEECHAT_RC_OK_EAT, so weechat's own completer never ran and a second Tab could not cycle through the candidates: it only reprinted the same list. The event is now eaten only when the input was actually extended. 2. The common prefix was computed case-sensitively over matches selected case-insensitively. os.path.commonprefix(["Sonic", "sonata"]) is "", so a single differently-cased nick in the match set silently disabled the extension - the normal situation on a real channel. The prefix is now computed on the lowercased nicks and the casing is sliced back from the first match. 3. The single-match branch inserted a bare nick, ignoring weechat.completion.nick_completer and weechat.completion.nick_add_space, so completion did not match what the user configured. 4. Only /input complete_next was hooked, so Shift-Tab behaved differently and never listed ambiguities. complete_previous is hooked too. 5. Nick comparison used str.lower(), which is not the IRC casemapping: under rfc1459 []\~ and {}|^ are the same letters. The authoritative value is the CASEMAPPING token in ISUPPORT; rfc1459 is assumed here. 6. The "Sort and list ambiguities" comment promised a sort that did not exist. The list is now sorted. The word boundary is still plain space only (so "ciao,strk" is one word); that one needs a decision about which separators count, so it is left alone. Co-Authored-By: Claude Opus 5 (1M context) --- nick_ambiguity_lister.py | 60 ++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/nick_ambiguity_lister.py b/nick_ambiguity_lister.py index f952bd7..015800f 100644 --- a/nick_ambiguity_lister.py +++ b/nick_ambiguity_lister.py @@ -4,11 +4,29 @@ import os.path # Script registration weechat.register("ambiguity_lister", "MuaddibLLM", "1.1", "MIT", "List nicks on ambiguity and complete to common prefix", "", "") +# RFC1459 casemapping: uppercase []\~ are the same letters as lowercase {}|^. +# The authoritative value comes from the CASEMAPPING token in ISUPPORT (005); +# rfc1459 is the common default. "ascii" servers want plain str.lower(). +_RFC1459_LOWER = str.maketrans(r"[]\~", r"{}|^") + +def irc_lower(s): + return s.lower().translate(_RFC1459_LOWER) + +def nick_suffix(at_line_start): + # Mirror what weechat's own nick completion appends, instead of inserting + # a bare nick: nick_completer (default ": ") at the start of the input, + # nick_add_space elsewhere. + if at_line_start: + return weechat.config_string(weechat.config_get("weechat.completion.nick_completer")) + if weechat.config_boolean(weechat.config_get("weechat.completion.nick_add_space")): + return " " + return "" + def complete_cb(data, buffer, command): # Only intercept /input complete_next (usually Tab) line = weechat.buffer_get_string(buffer, "input") pos = weechat.buffer_get_integer(buffer, "input_pos") - + if not line: return weechat.WEECHAT_RC_OK @@ -16,7 +34,7 @@ def complete_cb(data, buffer, command): start = pos while start > 0 and line[start-1] != ' ': start -= 1 - + word = line[start:pos] # Skip if empty or looks like command @@ -27,33 +45,43 @@ def complete_cb(data, buffer, command): infolist = weechat.infolist_get("nicklist", buffer, "") if not infolist: return weechat.WEECHAT_RC_OK - + matches = [] try: while weechat.infolist_next(infolist): if weechat.infolist_string(infolist, "type") != "nick": continue name = weechat.infolist_string(infolist, "name") - if name.lower().startswith(word.lower()): + if irc_lower(name).startswith(irc_lower(word)): matches.append(name) finally: weechat.infolist_free(infolist) - + if len(matches) > 1: - # Calculate common prefix (case-sensitive) - common = os.path.commonprefix(matches) + # Common prefix computed case-INSENSITIVELY, because that is how the + # matches were selected: os.path.commonprefix() on the raw nicks + # collapses to "" as soon as two matches differ in case (Sonic/sonata), + # which is the normal situation on a real channel. Slice the casing + # back from the first match so what we insert stays a real nick prefix. + common_len = len(os.path.commonprefix([irc_lower(m) for m in matches])) + common = matches[0][:common_len] - # If we found a longer common prefix, update input + # Sort and list ambiguities (the old comment claimed a sort that was + # never there; the nicklist order is arrival order, not useful). + match_list = ", ".join(sorted(matches, key=irc_lower)) + weechat.prnt(buffer, f"{weechat.prefix('network')}Ambiguous: {match_list}") + + # If we found a longer common prefix, update input and eat the event. # Note: weechat.buffer_set requires value as string even for integers if len(common) > len(word): new_line = line[:start] + common + line[pos:] weechat.buffer_set(buffer, "input", new_line) weechat.buffer_set(buffer, "input_pos", str(start + len(common))) + return weechat.WEECHAT_RC_OK_EAT - # Sort and list ambiguities - match_list = ", ".join(matches) - weechat.prnt(buffer, f"{weechat.prefix('network')}Ambiguous: {match_list}") - - return weechat.WEECHAT_RC_OK_EAT + # Nothing left to extend: do NOT eat the event. Eating it here killed + # weechat's own Tab cycling through the candidates, so a second Tab + # only reprinted this same list forever. + return weechat.WEECHAT_RC_OK if len(matches) == 1 and len(matches[0]) > len(word): # Single match that extends the current word: complete to it. @@ -61,7 +89,7 @@ def complete_cb(data, buffer, command): # weechat's own completion does not fire here (e.g. after a # previous Tab press already ate the event at the common prefix), # so the input would not be extended to the full nickname. - name = matches[0] + name = matches[0] + nick_suffix(start == 0) new_line = line[:start] + name + line[pos:] weechat.buffer_set(buffer, "input", new_line) weechat.buffer_set(buffer, "input_pos", str(start + len(name))) @@ -69,5 +97,7 @@ def complete_cb(data, buffer, command): return weechat.WEECHAT_RC_OK -# Hook the completion command +# Hook the completion commands. complete_previous (Shift-Tab) went straight to +# weechat's completer before, so the two directions behaved differently. weechat.hook_command_run("/input complete_next", "complete_cb", "") +weechat.hook_command_run("/input complete_previous", "complete_cb", "") -- 2.47.3