From 8ca2281a1c8e5369c3f0f562bbc80c76a7218ca9 Mon Sep 17 00:00:00 2001 From: Anders Holck Date: Fri, 1 May 2026 22:21:03 +0200 Subject: [PATCH] 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 --- main.c | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 7 deletions(-) diff --git a/main.c b/main.c index 29dbb7b..92e7958 100644 --- a/main.c +++ b/main.c @@ -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 \n"); } } else if (strcasecmp(cmd, "mode") == 0 && args) { - irc_send_raw("MODE %s", 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;