Fix tab cycling, add /ctcp, PM tab completion, query in status bar

- Fix tab completion cycling (track tab_end properly)
- Tab on window 1 cycles through nicks who PM'd you
- /ctcp <nick> <command> sends CTCP requests
- Query target shown in status bar on window 1
- Channel errors (404 etc) routed to channel window
- Long lines wrap properly on window redraw
This commit is contained in:
2026-04-30 08:26:05 +02:00
parent fe75236fad
commit 58a058a783
+102 -36
View File
@@ -18,6 +18,8 @@
#include "charset.h"
static size_t display_cols(const char *buf, size_t bytes);
#define BUF_SIZE 4096
#define IRC_MAX 512
@@ -75,6 +77,19 @@ static struct {
/* Query target (for /q) */
static char query_target[64] = "";
/* Track nicks who sent us private messages */
#define MAX_PM_NICKS 32
static char pm_nicks[MAX_PM_NICKS][NICK_LEN];
static int pm_nick_count = 0;
static void pm_nick_add(const char *n)
{
for (int i = 0; i < pm_nick_count; i++)
if (strcasecmp(pm_nicks[i], n) == 0) return;
if (pm_nick_count < MAX_PM_NICKS)
snprintf(pm_nicks[pm_nick_count++], NICK_LEN, "%s", n);
}
/* Get the active channel for current window, or "" */
static const char *current_channel(void)
{
@@ -113,7 +128,7 @@ static void draw_statusbar(void)
snprintf(prefix_str, sizeof(prefix_str), "%c",
win_chans[idx].my_prefix);
} else {
chan = "(status)";
chan = query_target[0] ? query_target : "(status)";
}
snprintf(bar, sizeof(bar), " [%d:%s] %s%s %s%s%s",
@@ -149,8 +164,7 @@ static void draw_statusbar(void)
}
/* Store a line in a window's scrollback */
static void buf_store(int level, const char *line)
{
static void buf_store(int level, const char *line){
if (level < 0 || level >= WL_MAX) return;
snprintf(win_buf[level].lines[win_buf[level].head], 512, "%s", line);
win_buf[level].head = (win_buf[level].head + 1) % SCROLLBACK;
@@ -171,34 +185,43 @@ static void redraw_window(void)
if (max_scroll < 0) max_scroll = 0;
if (scroll_offset > max_scroll) scroll_offset = max_scroll;
int show = count - scroll_offset;
if (show > visible) show = visible;
if (show < 0) show = 0;
/* Clear scroll region */
for (int i = 1; i <= visible; i++)
printf("\033[%d;1H\033[K", i);
/* Calculate start index in circular buffer */
int end_pos = count - scroll_offset; /* logical end position */
int start_pos = end_pos - show; /* logical start position */
/* Figure out how many lines fit, accounting for wrapping */
int end_pos = count - scroll_offset;
int rows_used = 0;
int start_pos = end_pos;
int buf_start;
while (start_pos > 0 && rows_used < visible) {
int li = start_pos - 1;
int buf_idx;
if (count <= SCROLLBACK)
buf_start = start_pos;
buf_idx = li;
else
buf_start = (win_buf[current_level].head - count + start_pos + SCROLLBACK) % SCROLLBACK;
buf_idx = (win_buf[current_level].head - count + li + SCROLLBACK) % SCROLLBACK;
int line_cols = (int)display_cols(win_buf[current_level].lines[buf_idx],
strlen(win_buf[current_level].lines[buf_idx]));
int line_rows = line_cols / term_cols + 1;
if (rows_used + line_rows > visible) break;
rows_used += line_rows;
start_pos--;
}
/* Print lines aligned to bottom of scroll region */
int first_row = visible - show + 1;
int show = end_pos - start_pos;
/* Print lines from top, letting terminal wrap naturally */
int row = visible - rows_used + 1;
printf("\033[%d;1H", row);
for (int i = 0; i < show; i++) {
int idx;
int li = start_pos + i;
int buf_idx;
if (count <= SCROLLBACK)
idx = buf_start + i;
buf_idx = li;
else
idx = (buf_start + i) % SCROLLBACK;
printf("\033[%d;1H%s", first_row + i,
win_buf[current_level].lines[idx]);
buf_idx = (win_buf[current_level].head - count + li + SCROLLBACK) % SCROLLBACK;
printf("\033[K%s\n", win_buf[current_level].lines[buf_idx]);
}
draw_statusbar();
@@ -584,6 +607,7 @@ static void handle_line(char *line)
}
} else if (strcasecmp(target, nick) == 0) {
wprintf(WL_MSG, "<%s> %s\n", sender, text);
pm_nick_add(sender);
} else {
int lvl = chan_to_level(target);
wprintf(lvl, "<%s> %s\n", sender, text);
@@ -724,6 +748,25 @@ static void handle_line(char *line)
int lvl = chan_to_level(chan);
wprintf(lvl, "* %s changed topic to: %s\n", sender, topic);
}
} else if ((strcmp(cmd, "404") == 0 || strcmp(cmd, "482") == 0 ||
strcmp(cmd, "473") == 0 || strcmp(cmd, "474") == 0 ||
strcmp(cmd, "475") == 0) && params) {
/* Channel error numerics: <nick> <channel> :<message> */
char *p = params;
char *sp = strchr(p, ' ');
if (sp) {
p = sp + 1;
char *chan = p;
char *text = strchr(p, ':');
if (text) {
sp = strchr(chan, ' ');
if (sp) *sp = '\0';
text++;
int idx = chan_win_idx(chan);
int lvl = idx >= 0 ? WL_CHAN + idx : WL_STATUS;
wprintf(lvl, "* %s: %s\n", chan, text);
}
}
} else if (strcmp(cmd, "353") == 0 && params) {
/* RPL_NAMREPLY: <nick> = <channel> :names... */
char *colon = strchr(params, ':');
@@ -828,10 +871,12 @@ static void handle_input(char *line)
if (args && args[0]) {
snprintf(query_target, sizeof(query_target), "%s", args);
wprintf(WL_MSG, "* Now talking to %s\n", query_target);
draw_statusbar();
} else {
if (query_target[0])
wprintf(WL_MSG, "* No longer talking to %s\n", query_target);
query_target[0] = '\0';
draw_statusbar();
}
} else if (strcasecmp(cmd, "nick") == 0 && args) {
snprintf(nick, sizeof(nick), "%s", args);
@@ -858,6 +903,19 @@ static void handle_input(char *line)
irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", query_target, slap);
wprintf(WL_MSG, "* %s %s\n", nick, slap);
}
} else if (strcasecmp(cmd, "ctcp") == 0 && args) {
char *target = args;
char *ctcp_cmd = strchr(args, ' ');
if (ctcp_cmd) {
*ctcp_cmd++ = '\0';
/* Uppercase the CTCP command */
for (char *p = ctcp_cmd; *p && *p != ' '; p++)
*p = (*p >= 'a' && *p <= 'z') ? *p - 32 : *p;
irc_send_raw("PRIVMSG %s :\x01%s\x01", target, ctcp_cmd);
wprintf(WL_STATUS, "* CTCP %s sent to %s\n", ctcp_cmd, target);
} else {
wprintf(current_level, "Usage: /ctcp <nick> <command>\n");
}
} else if (strcasecmp(cmd, "mode") == 0 && args) {
irc_send_raw("MODE %s", args);
} else if (strcasecmp(cmd, "whois") == 0 && args) {
@@ -1083,6 +1141,7 @@ int main(int argc, char *argv[])
int tab_idx = -1; /* current tab completion index */
size_t tab_prefix_len = 0; /* length of prefix being completed */
size_t tab_start = 0; /* byte position where completion word starts */
size_t tab_end = 0; /* byte position where last completion ends */
for (;;) {
FD_ZERO(&fds);
@@ -1235,51 +1294,58 @@ int main(int argc, char *argv[])
redraw_input(input_line, input_len, input_pos);
} else if (ch == 0x09) {
/* Tab: nick completion */
int cidx = -1;
if (current_level >= WL_CHAN)
cidx = current_level - WL_CHAN;
char (*nlist)[NICK_LEN] = NULL;
int ncount = 0;
if (cidx >= 0 && win_chans[cidx].nick_count > 0) {
/* Find word start */
if (current_level >= WL_CHAN) {
int cidx = current_level - WL_CHAN;
nlist = win_chans[cidx].nicks;
ncount = win_chans[cidx].nick_count;
} else {
nlist = pm_nicks;
ncount = pm_nick_count;
}
if (ncount > 0) {
if (tab_idx < 0) {
tab_start = input_pos;
while (tab_start > 0 &&
input_line[tab_start-1] != ' ')
tab_start--;
tab_prefix_len = input_pos - tab_start;
tab_end = input_pos;
tab_idx = 0;
} else {
input_pos = tab_end;
tab_idx++;
}
/* Find next matching nick */
int found = 0;
for (int tries = 0; tries < win_chans[cidx].nick_count; tries++) {
int ni = (tab_idx + tries) % win_chans[cidx].nick_count;
for (int tries = 0; tries < ncount; tries++) {
int ni = (tab_idx + tries) % ncount;
if (tab_prefix_len == 0 ||
strncasecmp(win_chans[cidx].nicks[ni],
strncasecmp(nlist[ni],
input_line + tab_start,
tab_prefix_len) == 0) {
tab_idx = ni;
/* Replace from tab_start to input_pos */
const char *compl = win_chans[cidx].nicks[ni];
const char *compl = nlist[ni];
size_t clen = strlen(compl);
/* Add ": " if at start of line */
char suffix[4] = "";
if (tab_start == 0)
if (tab_start == 0 && current_level >= WL_CHAN)
strcpy(suffix, ": ");
else
strcpy(suffix, " ");
size_t slen = strlen(suffix);
size_t tail = input_len - input_pos;
size_t tail = input_len - tab_end;
input_len = tab_start + clen + slen + tail;
if (input_len >= sizeof(input_line) - 1)
input_len = sizeof(input_line) - 1;
memmove(input_line + tab_start + clen + slen,
input_line + input_pos, tail);
input_line + tab_end, tail);
memcpy(input_line + tab_start, compl, clen);
memcpy(input_line + tab_start + clen, suffix, slen);
input_pos = tab_start + clen + slen;
tab_end = tab_start + clen + slen;
input_pos = tab_end;
redraw_input(input_line, input_len, input_pos);
tab_idx++;
found = 1;