IRC colour support, /colors toggle, persistent config, ESC timeout fix, Ctrl-L redraw

- mIRC colour/bold/underline/italic codes converted to ANSI (or stripped)
- /colors toggles display vs strip, saved to ~/.hircrc
- /trans state saved to ~/.hircrc on toggle
- ESC key stays pending indefinitely (no 50ms timeout)
- Ctrl-L redraws screen
- /mode * expands to current channel
- PM tab: incoming PMs prioritized, sent targets don't reorder list
This commit is contained in:
2026-05-01 22:21:03 +02:00
parent a1c33e346d
commit 8ca2281a1c
+155 -6
View File
@@ -79,6 +79,7 @@ static struct {
static char query_target[64] = "";
static int translate_public = 0; /* /trans toggle: echo translations to channel */
static int translate_enabled = 1; /* master toggle for translation */
static int irc_colors = 1; /* display IRC colour codes as ANSI */
/* Track nicks who sent us private messages */
#define MAX_PM_NICKS 32
@@ -146,6 +147,8 @@ static void ai_config_load(void)
fprintf(f, "ai_model=\n");
fprintf(f, "ai_target_lang=\n");
fprintf(f, "ai_skip_langs=\n");
fprintf(f, "translate=on\n");
fprintf(f, "irc_colors=1\n");
fclose(f);
}
return;
@@ -178,6 +181,13 @@ static void ai_config_load(void)
snprintf(ai_cfg.target_lang, sizeof(ai_cfg.target_lang), "%s", eq);
else if (strcmp(line, "ai_skip_langs") == 0)
snprintf(ai_cfg.skip_langs, sizeof(ai_cfg.skip_langs), "%s", eq);
else if (strcmp(line, "translate") == 0) {
if (strcmp(eq, "off") == 0) { translate_enabled = 0; translate_public = 0; }
else if (strcmp(eq, "public") == 0) { translate_enabled = 1; translate_public = 1; }
else { translate_enabled = 1; translate_public = 0; }
}
else if (strcmp(line, "irc_colors") == 0)
irc_colors = atoi(eq);
}
fclose(f);
@@ -186,6 +196,28 @@ static void ai_config_load(void)
ai_cfg.enabled = 1;
}
static void config_save(void)
{
char path[512];
snprintf(path, sizeof(path), "%s/.hircrc", getenv("HOME"));
FILE *f = fopen(path, "w");
if (!f) return;
fprintf(f, "# AI translation settings\n");
fprintf(f, "ai_type=%s\n", ai_cfg.type);
if (ai_cfg.port)
fprintf(f, "ai_host=%s:%d\n", ai_cfg.host, ai_cfg.port);
else
fprintf(f, "ai_host=%s\n", ai_cfg.host);
fprintf(f, "ai_key=%s\n", ai_cfg.key);
fprintf(f, "ai_model=%s\n", ai_cfg.model);
fprintf(f, "ai_target_lang=%s\n", ai_cfg.target_lang);
fprintf(f, "ai_skip_langs=%s\n", ai_cfg.skip_langs);
fprintf(f, "translate=%s\n",
!translate_enabled ? "off" : translate_public ? "public" : "on");
fprintf(f, "irc_colors=%d\n", irc_colors);
fclose(f);
}
static int needs_translation(const char *text)
{
if (!ai_cfg.enabled || !translate_enabled) return 0;
@@ -438,9 +470,101 @@ static void draw_statusbar(void)
}
/* Store a line in a window's scrollback */
/* mIRC colour code to ANSI */
static const char *mirc_to_ansi[] = {
"\033[97m", /* 0: white */
"\033[30m", /* 1: black */
"\033[34m", /* 2: blue */
"\033[32m", /* 3: green */
"\033[31m", /* 4: red */
"\033[33m", /* 5: brown */
"\033[35m", /* 6: purple */
"\033[33m", /* 7: orange */
"\033[93m", /* 8: yellow */
"\033[92m", /* 9: light green */
"\033[36m", /* 10: cyan */
"\033[96m", /* 11: light cyan */
"\033[94m", /* 12: light blue */
"\033[95m", /* 13: pink */
"\033[90m", /* 14: grey */
"\033[37m", /* 15: light grey */
};
static size_t irc_color_process(char *out, size_t outsize, const char *in)
{
size_t o = 0;
for (const char *p = in; *p && o < outsize - 10; p++) {
if (*p == '\x03') {
/* mIRC colour: ^C[fg[,bg]] */
p++;
if (irc_colors && *p >= '0' && *p <= '9') {
int fg = *p - '0';
p++;
if (*p >= '0' && *p <= '9') { fg = fg * 10 + (*p - '0'); p++; }
if (*p == ',') {
p++;
if (*p >= '0' && *p <= '9') p++;
if (*p >= '0' && *p <= '9') p++;
}
if (fg < 16) {
const char *a = mirc_to_ansi[fg];
size_t alen = strlen(a);
if (o + alen < outsize) { memcpy(out + o, a, alen); o += alen; }
}
} else if (!irc_colors) {
/* Strip: skip digits and comma */
if (*p >= '0' && *p <= '9') p++;
if (*p >= '0' && *p <= '9') p++;
if (*p == ',') {
p++;
if (*p >= '0' && *p <= '9') p++;
if (*p >= '0' && *p <= '9') p++;
}
}
p--; /* loop will p++ */
} else if (*p == '\x02') {
/* Bold */
if (irc_colors) {
const char *a = "\033[1m";
memcpy(out + o, a, 4); o += 4;
}
} else if (*p == '\x1F') {
/* Underline */
if (irc_colors) {
const char *a = "\033[4m";
memcpy(out + o, a, 4); o += 4;
}
} else if (*p == '\x0F') {
/* Reset */
if (irc_colors) {
const char *a = "\033[0m";
memcpy(out + o, a, 4); o += 4;
}
} else if (*p == '\x1D') {
/* Italic */
if (irc_colors) {
const char *a = "\033[3m";
memcpy(out + o, a, 4); o += 4;
}
} else {
out[o++] = *p;
}
}
/* Reset at end if colours were used */
if (irc_colors && o > 0) {
const char *a = "\033[0m";
size_t alen = strlen(a);
if (o + alen < outsize) { memcpy(out + o, a, alen); o += alen; }
}
out[o] = '\0';
return o;
}
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);
char processed[512];
irc_color_process(processed, sizeof(processed), line);
snprintf(win_buf[level].lines[win_buf[level].head], 512, "%s", processed);
win_buf[level].head = (win_buf[level].head + 1) % SCROLLBACK;
if (win_buf[level].count < SCROLLBACK)
win_buf[level].count++;
@@ -1213,6 +1337,12 @@ static void handle_input(char *line)
(unsigned char *)text,
strlen(text));
wprintf(WL_MSG, "-> %s: %s\n", target, text);
/* Ensure target is in PM list (but don't reorder) */
int found = 0;
for (int i = 0; i < pm_nick_count; i++)
if (strcasecmp(pm_nicks[i], target) == 0) { found = 1; break; }
if (!found && pm_nick_count < MAX_PM_NICKS)
snprintf(pm_nicks[pm_nick_count++], NICK_LEN, "%s", target);
}
} else if (strcasecmp(cmd, "q") == 0) {
if (args && args[0]) {
@@ -1263,6 +1393,12 @@ static void handle_input(char *line)
translate_public = 0;
wprintf(current_level, "* Translation OFF\n");
}
config_save();
} else if (strcasecmp(cmd, "colors") == 0) {
irc_colors = !irc_colors;
wprintf(current_level, "* IRC colours %s\n",
irc_colors ? "ON" : "OFF (stripped)");
config_save();
} else if (strcasecmp(cmd, "ctcp") == 0 && args) {
char *target = args;
char *ctcp_cmd = strchr(args, ' ');
@@ -1277,7 +1413,17 @@ static void handle_input(char *line)
wprintf(current_level, "Usage: /ctcp <nick> <command>\n");
}
} else if (strcasecmp(cmd, "mode") == 0 && args) {
/* Replace * with current channel */
if (args[0] == '*') {
const char *chan = current_channel();
if (chan[0]) {
char modebuf[IRC_MAX];
snprintf(modebuf, sizeof(modebuf), "%s%s", chan, args + 1);
irc_send_raw("MODE %s", modebuf);
}
} else {
irc_send_raw("MODE %s", args);
}
} else if (strcasecmp(cmd, "whois") == 0 && args) {
if (strchr(args, ' '))
irc_send_raw("WHOIS %s", args);
@@ -1528,7 +1674,7 @@ int main(int argc, char *argv[])
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = esc_pending ? 50000 : 500000;
tv.tv_usec = 500000;
int ret = select(maxfd + 1, &fds, NULL, NULL, &tv);
if (ret < 0) {
@@ -1647,10 +1793,6 @@ int main(int argc, char *argv[])
}
}
if (ret == 0 && esc_pending) {
esc_pending = 0;
}
if (got_sigint) {
got_sigint = 0;
printf("\033[%d;1H\033[KWanna quit? [y/N] ",
@@ -1834,6 +1976,13 @@ int main(int argc, char *argv[])
input_pos += yank_len;
redraw_input(input_line, input_len, input_pos);
}
} else if (ch == 0x0C) {
/* Ctrl-L: redraw screen */
get_term_size();
printf("\033[2J\033[H");
printf("\033[1;%dr", term_rows - 2);
redraw_window();
redraw_input(input_line, input_len, input_pos);
} else if (ch == 0x0B) {
/* Ctrl-K: kill to end */
yank_len = input_len - input_pos;