From 6a36cbcb8259c5257b3f832b9def4414e6d3415b Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 17 May 2026 14:21:34 +0800 Subject: [PATCH] input: Tab completes @mentions in INSERT mode (UX-9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing @al in INSERT mode now resolves to @alice and appends a trailing space so the next word starts cleanly. Algorithm: 1. walk back from end-of-input until '@' or ' ' is seen 2. '@' counts as a mention start only when at start-of-input or preceded by a space (avoids matching e.g. email@host) 3. case-insensitive strncasecmp against current g_room usernames 4. first hit wins; the search ignores the local user when the prefix is empty (so a lone "@" defaults to the first *other* member, matching the typical "ping someone" intent) If the buffer is too short to hold "@ ", the completion is a no-op rather than silently truncating the match. Standard chat-client behaviour — much less typing for @mentions. --- src/input.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/input.c b/src/input.c index d564ba6..266c7a5 100644 --- a/src/input.c +++ b/src/input.c @@ -11,6 +11,7 @@ #include #include #include +#include /* strncasecmp */ #include #include #include @@ -322,6 +323,53 @@ static bool handle_key(client_t *client, unsigned char key, char *input) { tui_render_input(client, input); } return true; + } else if (key == 9) { /* Tab: complete @mention */ + /* Walk back from end to find the start of the trailing + * "@…" token (an '@' not preceded by an alphanumeric). + * If found, scan g_room for the first case-insensitive + * username prefix-match (cycling past self) and replace + * the token. */ + size_t in_len = strlen(input); + ssize_t at_idx = -1; + for (ssize_t i = (ssize_t)in_len - 1; i >= 0; i--) { + unsigned char c = (unsigned char)input[i]; + if (c == '@') { + if (i == 0 || input[i - 1] == ' ') at_idx = i; + break; + } + if (c == ' ') break; + } + if (at_idx >= 0) { + const char *prefix = input + at_idx + 1; + size_t plen = strlen(prefix); + char match[MAX_USERNAME_LEN] = ""; + pthread_rwlock_rdlock(&g_room->lock); + for (int i = 0; i < g_room->client_count; i++) { + const char *uname = g_room->clients[i]->username; + if (plen == 0 + ? strcmp(uname, client->username) != 0 + : strncasecmp(uname, prefix, plen) == 0) { + strncpy(match, uname, sizeof(match) - 1); + match[sizeof(match) - 1] = '\0'; + break; + } + } + pthread_rwlock_unlock(&g_room->lock); + if (match[0] != '\0') { + /* Replace "@" with "@ " (trailing + * space so the next word starts cleanly). */ + size_t avail = MAX_MESSAGE_LEN - 1 + - (size_t)at_idx - 1; + size_t mlen = strlen(match); + if (mlen + 1 <= avail) { + input[at_idx + 1] = '\0'; + strncat(input, match, avail); + strncat(input, " ", 1); + tui_render_input(client, input); + } + } + } + return true; } break;