#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "charset.h" static size_t display_cols(const char *buf, size_t bytes); #define BUF_SIZE 4096 #define IRC_MAX 512 /* Window levels: 0=status+msg, 1-7=channels */ #define WL_STATUS 0 #define WL_MSG 0 #define WL_CHAN 1 #define WL_MAX 9 #define MAX_CHAN_WINS 8 #define SCROLLBACK 500 static int current_level = WL_STATUS; static int scroll_offset = 0; /* 0 = bottom (live), >0 = scrolled up */ static int win_activity[WL_MAX]; /* activity flag per window */ static volatile sig_atomic_t got_sigint = 0; static volatile sig_atomic_t got_sigwinch = 0; static void sigint_handler(int sig) { (void)sig; got_sigint = 1; } static void sigwinch_handler(int sig) { (void)sig; got_sigwinch = 1; } static int sock_fd = -1; static char recv_buf[BUF_SIZE]; static size_t recv_len = 0; static char nick[64] = "kiro_user"; static int term_rows = 24, term_cols = 80; static struct termios orig_term; /* Per-window scrollback buffer */ static struct { char lines[SCROLLBACK][512]; int count; /* total lines stored (up to SCROLLBACK) */ int head; /* next write position (circular) */ } win_buf[WL_MAX]; /* Per-channel nick list */ #define MAX_NICKS 256 #define NICK_LEN 32 static struct { char name[128]; /* channel name */ char modes[64]; /* channel modes (e.g. "+nt") */ char my_prefix; /* '@', '+', or '\0' */ char nicks[MAX_NICKS][NICK_LEN]; int nick_count; } win_chans[MAX_CHAN_WINS]; /* 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); } /* AI translation config */ static struct { char type[32]; /* ollama, vllm, openai */ char host[256]; /* host:port */ int port; char key[256]; char model[128]; char target_lang[64]; char skip_langs[256]; int enabled; } ai_cfg; /* Pending translation pipe fds */ #define MAX_TRANSLATE 8 static struct { int fd; /* read end of pipe from child */ int level; /* window to display in */ pid_t pid; } translate_pending[MAX_TRANSLATE]; static int translate_count = 0; static void ai_config_load(void) { memset(&ai_cfg, 0, sizeof(ai_cfg)); char path[512]; snprintf(path, sizeof(path), "%s/.hircrc", getenv("HOME")); FILE *f = fopen(path, "r"); if (!f) { /* Create default config */ f = fopen(path, "w"); if (f) { fprintf(f, "# AI translation settings\n"); fprintf(f, "ai_type=\n"); fprintf(f, "ai_host=\n"); fprintf(f, "ai_key=\n"); fprintf(f, "ai_model=\n"); fprintf(f, "ai_target_lang=\n"); fprintf(f, "ai_skip_langs=\n"); fclose(f); } return; } char line[512]; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = '\0'; if (line[0] == '#' || line[0] == '\0') continue; char *eq = strchr(line, '='); if (!eq) continue; *eq++ = '\0'; if (strcmp(line, "ai_type") == 0) snprintf(ai_cfg.type, sizeof(ai_cfg.type), "%s", eq); else if (strcmp(line, "ai_host") == 0) { snprintf(ai_cfg.host, sizeof(ai_cfg.host), "%s", eq); char *colon = strrchr(ai_cfg.host, ':'); if (colon) { *colon = '\0'; ai_cfg.port = atoi(colon + 1); } } else if (strcmp(line, "ai_port") == 0) ai_cfg.port = atoi(eq); else if (strcmp(line, "ai_key") == 0) snprintf(ai_cfg.key, sizeof(ai_cfg.key), "%s", eq); else if (strcmp(line, "ai_model") == 0) snprintf(ai_cfg.model, sizeof(ai_cfg.model), "%s", eq); else if (strcmp(line, "ai_target_lang") == 0) 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); } fclose(f); /* Enable only if minimum config is present */ if (ai_cfg.host[0] && ai_cfg.port && ai_cfg.model[0] && ai_cfg.target_lang[0]) ai_cfg.enabled = 1; } static int needs_translation(const char *text) { if (!ai_cfg.enabled) return 0; /* Check for non-ASCII characters that suggest non-Latin script */ for (const unsigned char *p = (const unsigned char *)text; *p; p++) { if (*p >= 0xC0) { /* Multi-byte UTF-8: check if it's beyond Latin Extended (U+0250+) */ unsigned int cp = 0; if ((*p & 0xE0) == 0xC0 && p[1]) { cp = ((*p & 0x1F) << 6) | (p[1] & 0x3F); } else if ((*p & 0xF0) == 0xE0 && p[1] && p[2]) { cp = ((*p & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F); } else if ((*p & 0xF8) == 0xF0 && p[1] && p[2] && p[3]) { cp = ((*p & 0x07) << 18) | ((p[1] & 0x3F) << 12) | ((p[2] & 0x3F) << 6) | (p[3] & 0x3F); } /* Skip Latin Extended, keep Cyrillic, CJK, Arabic, etc. */ if (cp > 0x024F) return 1; } } return 0; } static void translate_async(const char *text, int level) { if (translate_count >= MAX_TRANSLATE) return; int pipefd[2]; if (pipe(pipefd) < 0) return; pid_t pid = fork(); if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return; } if (pid == 0) { /* Child: do HTTP request to AI API */ close(pipefd[0]); int s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) _exit(1); struct hostent *he = gethostbyname(ai_cfg.host); if (!he) _exit(1); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(ai_cfg.port); memcpy(&addr.sin_addr, he->h_addr, he->h_length); if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) _exit(1); /* Build JSON body */ char escaped[1024]; int ei = 0; for (int i = 0; text[i] && ei < (int)sizeof(escaped) - 2; i++) { if (text[i] == '"' || text[i] == '\\') escaped[ei++] = '\\'; if (text[i] == '\n') { escaped[ei++] = '\\'; escaped[ei++] = 'n'; } else escaped[ei++] = text[i]; } escaped[ei] = '\0'; char body[2048]; snprintf(body, sizeof(body), "{\"model\":\"%s\",\"messages\":[" "{\"role\":\"system\",\"content\":\"Translate to %s. Reply with only the translation, nothing else.\"}," "{\"role\":\"user\",\"content\":\"%s\"}" "],\"stream\":false}", ai_cfg.model, ai_cfg.target_lang, escaped); char req[4096]; int rlen = snprintf(req, sizeof(req), "POST /v1/chat/completions HTTP/1.1\r\n" "Host: %s:%d\r\n" "Content-Type: application/json\r\n" "Content-Length: %d\r\n" "%s%s%s" "Connection: close\r\n\r\n%s", ai_cfg.host, ai_cfg.port, (int)strlen(body), ai_cfg.key[0] ? "Authorization: Bearer " : "", ai_cfg.key[0] ? ai_cfg.key : "", ai_cfg.key[0] ? "\r\n" : "", body); if (write(s, req, rlen) < 0) _exit(1); /* Read response */ char resp[8192]; int total = 0; int n; while ((n = read(s, resp + total, sizeof(resp) - total - 1)) > 0) total += n; resp[total] = '\0'; close(s); /* Extract content from JSON response */ char *content = strstr(resp, "\"content\":"); if (content) { content += 10; while (*content == ' ' || *content == '\t') content++; if (*content == '"') { content++; char result[1024]; int ri = 0; while (*content && *content != '"' && ri < (int)sizeof(result) - 1) { if (*content == '\\' && content[1]) { content++; if (*content == 'n') result[ri++] = ' '; else result[ri++] = *content; } else { result[ri++] = *content; } content++; } result[ri] = '\0'; (void)!write(pipefd[1], result, ri); } } close(pipefd[1]); _exit(0); } /* Parent */ close(pipefd[1]); translate_pending[translate_count].fd = pipefd[0]; translate_pending[translate_count].level = level; translate_pending[translate_count].pid = pid; translate_count++; } /* Get the active channel for current window, or "" */ static const char *current_channel(void) { if (current_level >= WL_CHAN && current_level < WL_MAX) { int idx = current_level - WL_CHAN; if (idx < MAX_CHAN_WINS) return win_chans[idx].name; } return ""; } static int get_term_size(void) { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { term_rows = ws.ws_row; term_cols = ws.ws_col; return 0; } return -1; } static void draw_statusbar(void) { get_term_size(); char bar[512]; const char *chan = ""; char prefix_str[4] = ""; const char *cmodes = ""; if (current_level >= WL_CHAN && current_level < WL_MAX) { int idx = current_level - WL_CHAN; chan = win_chans[idx].name; cmodes = win_chans[idx].modes; if (win_chans[idx].my_prefix) snprintf(prefix_str, sizeof(prefix_str), "%c", win_chans[idx].my_prefix); } else { chan = query_target[0] ? query_target : "(status)"; } snprintf(bar, sizeof(bar), " [%d:%s] %s%s %s%s%s", current_level + 1, current_level == WL_STATUS ? "status" : chan, prefix_str, nick, cmodes[0] ? "[" : "", cmodes, cmodes[0] ? "]" : ""); /* Append activity indicator */ char act[64] = ""; int apos = 0; for (int i = 0; i < WL_MAX; i++) { if (win_activity[i] && i != current_level) { if (apos == 0) apos += snprintf(act + apos, sizeof(act) - apos, " (Act: "); else apos += snprintf(act + apos, sizeof(act) - apos, ","); apos += snprintf(act + apos, sizeof(act) - apos, "%d", i + 1); } } if (apos > 0) snprintf(act + apos, sizeof(act) - apos, ")"); size_t blen = strlen(bar); snprintf(bar + blen, sizeof(bar) - blen, "%s", act); /* Status bar on second-to-last row */ printf("\033[%d;1H\033[7m%-*.*s\033[0m", term_rows - 1, term_cols, term_cols, bar); /* Move cursor to input line */ printf("\033[%d;1H", term_rows); fflush(stdout); } /* Store a line in a window's scrollback */ 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; if (win_buf[level].count < SCROLLBACK) win_buf[level].count++; } /* Redraw the current window's scrollback */ static void redraw_window(void) { get_term_size(); printf("\033[1;%dr", term_rows - 2); /* ensure scroll region is set */ int visible = term_rows - 2; int count = win_buf[current_level].count; /* Clamp scroll offset */ int max_scroll = count - visible; if (max_scroll < 0) max_scroll = 0; if (scroll_offset > max_scroll) scroll_offset = max_scroll; /* Clear scroll region */ for (int i = 1; i <= visible; i++) printf("\033[%d;1H\033[K", i); /* Figure out how many lines fit, accounting for wrapping */ int end_pos = count - scroll_offset; int rows_used = 0; int start_pos = end_pos; while (start_pos > 0 && rows_used < visible) { int li = start_pos - 1; int buf_idx; if (count <= SCROLLBACK) buf_idx = li; else 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--; } 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 li = start_pos + i; int buf_idx; if (count <= SCROLLBACK) buf_idx = li; else 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(); } static void wprintf(int level, const char *fmt, ...) { va_list ap; char msg[512]; char timestamped[512]; time_t now = time(NULL); struct tm *tm = localtime(&now); va_start(ap, fmt); vsnprintf(msg, sizeof(msg), fmt, ap); va_end(ap); /* Add timestamp */ snprintf(timestamped, sizeof(timestamped), "%02d:%02d %.500s", tm->tm_hour, tm->tm_min, msg); /* Remove trailing newline for storage */ size_t len = strlen(timestamped); if (len > 0 && timestamped[len-1] == '\n') timestamped[len-1] = '\0'; buf_store(level, timestamped); /* Only display if this is the current window */ if (level == current_level) { if (scroll_offset == 0) { printf("\033[%d;1H\n\033[K%s", term_rows - 2, timestamped); draw_statusbar(); } } else { /* Mark activity on other window */ win_activity[level] = 1; draw_statusbar(); } } static void die(const char *msg) { perror(msg); exit(1); } static void sock_write(const void *buf, size_t len) { ssize_t n = write(sock_fd, buf, len); (void)n; } static void irc_send_raw(const char *fmt, ...) { char buf[IRC_MAX]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf) - 2, fmt, ap); va_end(ap); size_t len = strlen(buf); buf[len] = '\r'; buf[len + 1] = '\n'; len += 2; sock_write(buf, len); } static void irc_send_converted(const char *prefix, const unsigned char *text, size_t text_len) { char converted[IRC_MAX]; char line[IRC_MAX]; to_iso8859_1(text, text_len, converted, sizeof(converted) / 2); size_t len = (size_t)snprintf(line, sizeof(line) - 2, "%s%.200s", prefix, converted); if (len > sizeof(line) - 3) len = sizeof(line) - 3; line[len] = '\r'; line[len + 1] = '\n'; len += 2; sock_write(line, len); } static int irc_connect(const char *host, const char *port) { struct addrinfo hints, *res, *p; int fd = -1; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if (getaddrinfo(host, port, &hints, &res) != 0) die("getaddrinfo"); for (p = res; p; p = p->ai_next) { fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (fd < 0) continue; if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) break; close(fd); fd = -1; } freeaddrinfo(res); if (fd < 0) die("connect"); return fd; } /* Find window index for a channel, or -1 */ /* Nick list management */ static void nicklist_add(int idx, const char *n) { if (idx < 0 || idx >= MAX_CHAN_WINS) return; /* Skip prefix chars */ if (*n == '@' || *n == '+' || *n == '%') n++; if (!*n) return; /* Check if already present */ for (int i = 0; i < win_chans[idx].nick_count; i++) if (strcasecmp(win_chans[idx].nicks[i], n) == 0) return; if (win_chans[idx].nick_count < MAX_NICKS) snprintf(win_chans[idx].nicks[win_chans[idx].nick_count++], NICK_LEN, "%s", n); } static void nicklist_remove(int idx, const char *n) { if (idx < 0 || idx >= MAX_CHAN_WINS) return; for (int i = 0; i < win_chans[idx].nick_count; i++) { if (strcasecmp(win_chans[idx].nicks[i], n) == 0) { win_chans[idx].nick_count--; if (i < win_chans[idx].nick_count) memcpy(win_chans[idx].nicks[i], win_chans[idx].nicks[win_chans[idx].nick_count], NICK_LEN); return; } } } static void nicklist_rename(const char *old, const char *new_nick) { for (int i = 0; i < MAX_CHAN_WINS; i++) { for (int j = 0; j < win_chans[i].nick_count; j++) { if (strcasecmp(win_chans[i].nicks[j], old) == 0) { snprintf(win_chans[i].nicks[j], NICK_LEN, "%s", new_nick); break; } } } } static int chan_win_idx(const char *chan) { int i; for (i = 0; i < MAX_CHAN_WINS; i++) { if (strcasecmp(win_chans[i].name, chan) == 0) return i; } return -1; } /* Get level for a channel (find existing or assign to current window) */ static int chan_to_level(const char *chan) { int idx = chan_win_idx(chan); if (idx >= 0) return WL_CHAN + idx; /* Assign to current window if it's a channel window and empty */ if (current_level >= WL_CHAN) { idx = current_level - WL_CHAN; if (win_chans[idx].name[0] == '\0') { snprintf(win_chans[idx].name, sizeof(win_chans[idx].name), "%s", chan); return current_level; } } /* Find first empty slot */ int i; for (i = 0; i < MAX_CHAN_WINS; i++) { if (win_chans[i].name[0] == '\0') { snprintf(win_chans[i].name, sizeof(win_chans[i].name), "%s", chan); return WL_CHAN + i; } } return WL_CHAN; } /* Update our prefix for a channel based on MODE changes */ static void update_my_prefix(const char *chan, const char *modestr, const char *args) { int idx = chan_win_idx(chan); if (idx < 0) return; int adding = 1; const char *m = modestr; /* Simple parse: walk mode chars, consume args for o/v */ char arg_buf[512]; snprintf(arg_buf, sizeof(arg_buf), "%s", args ? args : ""); char *arg = arg_buf; while (*m) { if (*m == '+') { adding = 1; m++; continue; } if (*m == '-') { adding = 0; m++; continue; } /* Modes that take a parameter */ char *this_arg = NULL; if (*m == 'o' || *m == 'v' || *m == 'b' || *m == 'k' || *m == 'l') { this_arg = arg; char *sp = strchr(arg, ' '); if (sp) { *sp = '\0'; arg = sp + 1; } else arg = arg + strlen(arg); } if ((*m == 'o' || *m == 'v') && this_arg && strcasecmp(this_arg, nick) == 0) { if (*m == 'o') win_chans[idx].my_prefix = adding ? '@' : '\0'; else if (*m == 'v' && win_chans[idx].my_prefix != '@') win_chans[idx].my_prefix = adding ? '+' : '\0'; } /* Track channel modes (non-user modes) */ if (*m != 'o' && *m != 'v' && *m != 'b') { char *cm = win_chans[idx].modes; size_t clen = strlen(cm); if (adding) { /* Add if not present */ if (!strchr(cm, *m) && clen + 1 < sizeof(win_chans[idx].modes)) { if (clen == 0) { cm[0] = '+'; cm[1] = *m; cm[2] = '\0'; } else { cm[clen] = *m; cm[clen+1] = '\0'; } } } else { /* Remove */ char *p = strchr(cm, *m); if (p) memmove(p, p + 1, strlen(p)); /* Remove '+' if empty */ if (strcmp(cm, "+") == 0) cm[0] = '\0'; } } m++; } } static FILE *logfp = NULL; static void log_raw(const char *line, size_t len) { if (!logfp) return; time_t now = time(NULL); struct tm *tm = localtime(&now); fprintf(logfp, "[%02d:%02d:%02d] len=%zu charset=", tm->tm_hour, tm->tm_min, tm->tm_sec, len); /* Detect charset */ const unsigned char *u = (const unsigned char *)line; int has_high = 0, valid_utf8 = 1; for (size_t i = 0; i < len; i++) { if (u[i] >= 0x80) { has_high = 1; if ((u[i] & 0xE0) == 0xC0) { if (i+1 >= len || (u[i+1] & 0xC0) != 0x80) valid_utf8 = 0; else i += 1; } else if ((u[i] & 0xF0) == 0xE0) { if (i+2 >= len || (u[i+1] & 0xC0) != 0x80 || (u[i+2] & 0xC0) != 0x80) valid_utf8 = 0; else i += 2; } else if ((u[i] & 0xF8) == 0xF0) { if (i+3 >= len || (u[i+1] & 0xC0) != 0x80 || (u[i+2] & 0xC0) != 0x80 || (u[i+3] & 0xC0) != 0x80) valid_utf8 = 0; else i += 3; } else { valid_utf8 = 0; } } } if (!has_high) fprintf(logfp, "ASCII"); else if (valid_utf8) fprintf(logfp, "UTF-8"); else fprintf(logfp, "ISO-8859-1"); fprintf(logfp, "\n text: %s\n hex: ", line); for (size_t i = 0; i < len; i++) fprintf(logfp, "%02X ", u[i]); fprintf(logfp, "\n"); fflush(logfp); } static void handle_line(char *line) { size_t rawlen = strlen(line); log_raw(line, rawlen); /* Incoming text is displayed as-is (terminal is UTF-8). * No conversion needed for display. */ char *converted = line; if (strncmp(converted, "PING ", 5) == 0) { irc_send_raw("PONG %s", converted + 5); return; } char *prefix = NULL; char *cmd = converted; if (cmd[0] == ':') { prefix = cmd + 1; cmd = strchr(cmd, ' '); if (!cmd) return; *cmd++ = '\0'; } while (*cmd == ' ') cmd++; char sender[64] = ""; if (prefix) { char *bang = strchr(prefix, '!'); if (bang) { size_t nlen = (size_t)(bang - prefix); if (nlen >= sizeof(sender)) nlen = sizeof(sender) - 1; memcpy(sender, prefix, nlen); sender[nlen] = '\0'; } else { snprintf(sender, sizeof(sender), "%.63s", prefix); } } char *params = strchr(cmd, ' '); if (params) *params++ = '\0'; if (strcmp(cmd, "PRIVMSG") == 0 && params) { char *target = params; char *text = strchr(params, ':'); if (text) { char *sp = strchr(target, ' '); if (sp) *sp = '\0'; text++; if (text[0] == '\x01') { if (strncmp(text, "\x01VERSION\x01", 9) == 0) { struct utsname ut; uname(&ut); irc_send_raw("NOTICE %s :\x01VERSION " "Holck's Mirk, OS: %s %s %s" " :: This space available for rent\x01", sender, ut.sysname, ut.release, ut.machine); wprintf(WL_STATUS, "* CTCP VERSION from %s\n", sender); } else if (strncmp(text, "\x01" "ACTION ", 8) == 0) { /* /me action */ char *action = text + 8; char *end = strchr(action, '\x01'); if (end) *end = '\0'; if (strcasecmp(target, nick) == 0) { wprintf(WL_MSG, "* %s %s\n", sender, action); } else { int lvl = chan_to_level(target); wprintf(lvl, "* %s %s\n", sender, action); } } else { wprintf(WL_STATUS, "* CTCP from %s: %s\n", sender, text + 1); } } else if (strcasecmp(target, nick) == 0) { wprintf(WL_MSG, "<%s> %s\n", sender, text); pm_nick_add(sender); if (needs_translation(text)) translate_async(text, WL_MSG); } else { int lvl = chan_to_level(target); wprintf(lvl, "<%s> %s\n", sender, text); if (needs_translation(text)) translate_async(text, lvl); } } } else if (strcmp(cmd, "NOTICE") == 0 && params) { char *text = strchr(params, ':'); if (text) text++; else text = params; wprintf(WL_STATUS, "-%s- %s\n", sender[0] ? sender : "*", text); } else if (strcmp(cmd, "JOIN") == 0) { char *chan = params; if (chan && chan[0] == ':') chan++; int lvl = chan ? chan_to_level(chan) : WL_STATUS; if (chan) nicklist_add(chan_win_idx(chan), sender); wprintf(lvl, "\033[1m* %s has joined %s\033[0m\n", sender, chan ? chan : ""); draw_statusbar(); } else if (strcmp(cmd, "PART") == 0) { char *chan = params; char *reason = NULL; if (chan) { reason = strchr(chan, ':'); if (reason) { char *sp = strchr(chan, ' '); if (sp) *sp = '\0'; reason++; } } int lvl = chan ? chan_to_level(chan) : WL_STATUS; if (chan) nicklist_remove(chan_win_idx(chan), sender); wprintf(lvl, "\033[1m* %s has left %s (%s)\033[0m\n", sender, chan ? chan : "", reason ? reason : ""); /* If we parted, clear the window */ if (strcasecmp(sender, nick) == 0 && chan) { int idx = chan_win_idx(chan); if (idx >= 0) { win_chans[idx].name[0] = '\0'; win_chans[idx].modes[0] = '\0'; win_chans[idx].my_prefix = '\0'; win_chans[idx].nick_count = 0; draw_statusbar(); } } } else if (strcmp(cmd, "QUIT") == 0) { char *reason = params; if (reason && reason[0] == ':') reason++; /* Show quit in all active channel windows and remove nick */ for (int i = 0; i < MAX_CHAN_WINS; i++) { if (win_chans[i].name[0]) { nicklist_remove(i, sender); wprintf(WL_CHAN + i, "\033[1m* %s has quit (%s)\033[0m\n", sender, reason ? reason : ""); } } } else if (strcmp(cmd, "NICK") == 0) { char *newnick = params; if (newnick && newnick[0] == ':') newnick++; /* Update nick lists and show in all active channel windows */ if (newnick) nicklist_rename(sender, newnick); for (int i = 0; i < MAX_CHAN_WINS; i++) { if (win_chans[i].name[0]) wprintf(WL_CHAN + i, "* %s is now known as %s\n", sender, newnick ? newnick : ""); } if (strcasecmp(sender, nick) == 0 && newnick) { snprintf(nick, sizeof(nick), "%s", newnick); draw_statusbar(); } } else if (strcmp(cmd, "MODE") == 0 && params) { /* MODE #chan +modes [args] */ char *target = params; char *modestr = strchr(params, ' '); if (modestr) { *modestr++ = '\0'; char *modeargs = strchr(modestr, ' '); if (modeargs) *modeargs++ = '\0'; if (target[0] == '#' || target[0] == '&') { update_my_prefix(target, modestr, modeargs); int lvl = chan_to_level(target); wprintf(lvl, "\033[1m* %s sets mode %s %s\033[0m\n", sender[0] ? sender : "*", modestr, modeargs ? modeargs : ""); draw_statusbar(); } else { wprintf(WL_STATUS, "\033[1m* %s sets mode %s\033[0m\n", sender[0] ? sender : "*", modestr); } } } else if (strcmp(cmd, "324") == 0 && params) { /* RPL_CHANNELMODEIS: */ char *p = params; /* skip our nick */ char *sp = strchr(p, ' '); if (sp) { p = sp + 1; char *chan = p; sp = strchr(p, ' '); if (sp) { *sp = '\0'; char *modes = sp + 1; /* Strip trailing params */ sp = strchr(modes, ' '); if (sp) *sp = '\0'; int idx = chan_win_idx(chan); if (idx >= 0) { snprintf(win_chans[idx].modes, sizeof(win_chans[idx].modes), "%s", modes); draw_statusbar(); } } } } else if (strcmp(cmd, "311") == 0 && params) { /* RPL_WHOISUSER: * : */ char *p = params; char *sp = strchr(p, ' '); if (sp) p = sp + 1; /* skip our nick */ char *wnick = p; sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; } char *wuser = p; sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; } char *whost = p; sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; } /* skip the * */ sp = strchr(p, ':'); char *real = sp ? sp + 1 : p; wprintf(WL_STATUS, "*** %s is %s@%s (%s)\n", wnick, wuser, whost, real); } else if (strcmp(cmd, "319") == 0 && params) { /* RPL_WHOISCHANNELS: : */ char *p = params; char *sp = strchr(p, ' '); if (sp) p = sp + 1; char *wnick = p; sp = strchr(p, ' '); if (sp) *sp = '\0'; char *chans = strchr(params, ':'); if (chans) chans++; wprintf(WL_STATUS, "*** %s on channels: %s\n", wnick, chans ? chans : ""); } else if (strcmp(cmd, "312") == 0 && params) { /* RPL_WHOISSERVER: : */ char *p = params; char *sp = strchr(p, ' '); if (sp) p = sp + 1; char *wnick = p; sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; } char *server = p; sp = strchr(p, ' '); if (sp) *sp = '\0'; char *info = strchr(params, ':'); if (info) info++; wprintf(WL_STATUS, "*** %s on irc via server %s (%s)\n", wnick, server, info ? info : ""); } else if (strcmp(cmd, "317") == 0 && params) { /* RPL_WHOISIDLE: : */ char *p = params; char *sp = strchr(p, ' '); if (sp) p = sp + 1; char *wnick = p; sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; } int idle_secs = atoi(p); int mins = idle_secs / 60; int secs = idle_secs % 60; if (mins > 0) wprintf(WL_STATUS, "*** %s has been idle %d min %d sec\n", wnick, mins, secs); else wprintf(WL_STATUS, "*** %s has been idle %d sec\n", wnick, secs); } else if (strcmp(cmd, "320") == 0 && params) { /* RPL_WHOISSPECIAL: : */ char *p = params; char *sp = strchr(p, ' '); if (sp) p = sp + 1; char *wnick = p; sp = strchr(p, ' '); if (sp) *sp = '\0'; char *text = strchr(params, ':'); if (text) text++; wprintf(WL_STATUS, "*** %s %s\n", wnick, text ? text : ""); } else if (strcmp(cmd, "318") == 0 && params) { /* RPL_ENDOFWHOIS — suppress or show subtle */ wprintf(WL_STATUS, "*** End of WHOIS\n"); } else if (strcmp(cmd, "332") == 0 && params) { /* RPL_TOPIC: : */ char *p = params; char *sp = strchr(p, ' '); if (sp) { p = sp + 1; /* skip our nick */ char *chan = p; char *topic = strchr(p, ':'); if (topic) { sp = strchr(chan, ' '); if (sp) *sp = '\0'; topic++; int lvl = chan_to_level(chan); wprintf(lvl, "* Topic for %s: %s\n", chan, topic); } } } else if (strcmp(cmd, "TOPIC") == 0 && params) { /* :nick TOPIC #channel :new topic */ char *chan = params; char *topic = strchr(params, ':'); if (topic) { char *sp = strchr(chan, ' '); if (sp) *sp = '\0'; topic++; 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: : */ 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: = :names... */ char *colon = strchr(params, ':'); if (colon) { char *p = params; char *chan = NULL; char *sp; sp = strchr(p, ' '); if (sp) p = sp + 1; sp = strchr(p, ' '); if (sp) p = sp + 1; sp = strchr(p, ' '); if (sp) { *sp = '\0'; chan = p; } if (chan) { char *names = colon + 1; int lvl = chan_to_level(chan); wprintf(lvl, "[%s] %s\n", chan, names); int idx = chan_win_idx(chan); if (idx >= 0) { char namecopy[512]; snprintf(namecopy, sizeof(namecopy), "%s", names); char *tok = strtok(namecopy, " "); while (tok) { const char *n = tok; char pf = '\0'; if (*n == '@' || *n == '+') { pf = *n; n++; } nicklist_add(idx, n); if (strcasecmp(n, nick) == 0) { win_chans[idx].my_prefix = pf; draw_statusbar(); } tok = strtok(NULL, " "); } } } } } else { if (params) { char *text = strchr(params, ':'); if (text) text++; else text = params; wprintf(WL_STATUS, "[%s] %s\n", cmd, text); } else { wprintf(WL_STATUS, "[%s]\n", cmd); } } } static void process_recv(void) { char *crlf; while ((crlf = strstr(recv_buf, "\r\n")) != NULL) { *crlf = '\0'; handle_line(recv_buf); size_t line_len = (size_t)(crlf - recv_buf) + 2; recv_len -= line_len; memmove(recv_buf, crlf + 2, recv_len); recv_buf[recv_len] = '\0'; } } static void handle_input(char *line) { size_t len = strlen(line); if (len > 0 && line[len-1] == '\n') line[--len] = '\0'; if (len == 0) return; if (line[0] == '/') { char *cmd = line + 1; char *args = strchr(cmd, ' '); if (args) *args++ = '\0'; if (strcasecmp(cmd, "join") == 0 && args) { /* Assign channel to current window if it's a chan window */ chan_to_level(args); irc_send_raw("JOIN %s", args); } else if (strcasecmp(cmd, "part") == 0) { const char *chan = args ? args : current_channel(); if (chan[0]) irc_send_raw("PART %s", chan); } else if (strcasecmp(cmd, "msg") == 0 && args) { char *target = args; char *text = strchr(args, ' '); if (text) { *text++ = '\0'; char pfx[IRC_MAX]; snprintf(pfx, sizeof(pfx), "PRIVMSG %s :", target); irc_send_converted(pfx, (unsigned char *)text, strlen(text)); wprintf(WL_MSG, "-> %s: %s\n", target, text); } } else if (strcasecmp(cmd, "q") == 0) { 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); irc_send_raw("NICK %s", args); draw_statusbar(); } else if (strcasecmp(cmd, "me") == 0 && args) { const char *chan = current_channel(); if (chan[0]) { irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", chan, args); wprintf(current_level, "* %s %s\n", nick, args); } else if (query_target[0]) { irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", query_target, args); wprintf(WL_MSG, "* %s %s\n", nick, args); } } else if (strcasecmp(cmd, "slap") == 0 && args) { const char *chan = current_channel(); char slap[256]; snprintf(slap, sizeof(slap), "slaps %s around a bit with a large trout", args); if (chan[0]) { irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", chan, slap); wprintf(current_level, "* %s %s\n", nick, slap); } else if (query_target[0]) { 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 \n"); } } else if (strcasecmp(cmd, "mode") == 0 && args) { irc_send_raw("MODE %s", args); } else if (strcasecmp(cmd, "whois") == 0 && args) { if (strchr(args, ' ')) irc_send_raw("WHOIS %s", args); else irc_send_raw("WHOIS %s", args); } else if (strcasecmp(cmd, "wii") == 0 && args) { irc_send_raw("WHOIS %s %s", args, args); } else if (strcasecmp(cmd, "names") == 0) { const char *chan = args ? args : current_channel(); if (chan[0]) irc_send_raw("NAMES %s", chan); } else if (strcasecmp(cmd, "topic") == 0) { if (args && strchr(args, ' ')) { /* /topic #channel new topic */ char *chan = args; char *text = strchr(args, ' '); *text++ = '\0'; irc_send_raw("TOPIC %s :%s", chan, text); } else { /* /topic or /topic #channel — query topic */ const char *chan = args ? args : current_channel(); if (chan[0]) irc_send_raw("TOPIC %s", chan); } } else if (strcasecmp(cmd, "quit") == 0) { irc_send_raw("QUIT :%s", args ? args : "See you later"); close(sock_fd); exit(0); } else if (strcasecmp(cmd, "raw") == 0 && args) { irc_send_raw("%s", args); } else { wprintf(current_level, "Unknown command: /%s\n", cmd); } } else { const char *chan = current_channel(); /* If on status/msg window with a query target, send there */ if (chan[0] == '\0' && query_target[0]) { char pfx[IRC_MAX]; snprintf(pfx, sizeof(pfx), "PRIVMSG %s :", query_target); irc_send_converted(pfx, (unsigned char *)line, len); wprintf(WL_MSG, "-> %s: %s\n", query_target, line); return; } if (chan[0] == '\0') { wprintf(current_level, "* Not in a channel on this window.\n"); return; } char pfx[IRC_MAX]; snprintf(pfx, sizeof(pfx), "PRIVMSG %s :", chan); irc_send_converted(pfx, (unsigned char *)line, len); wprintf(current_level, "<%s> %s\n", nick, line); } } /* Count display columns for a UTF-8 byte string */ static size_t display_cols(const char *buf, size_t bytes) { size_t cols = 0; size_t i = 0; while (i < bytes) { unsigned char c = (unsigned char)buf[i]; if (c < 0x80) { i++; cols++; } else if ((c & 0xE0) == 0xC0) { i += 2; cols++; } else if ((c & 0xF0) == 0xE0) { i += 3; cols++; } else if ((c & 0xF8) == 0xF0) { i += 4; cols++; } else { i++; cols++; } } return cols; } /* Return number of bytes in the UTF-8 char ending before pos */ static size_t utf8_back(const char *buf, size_t pos) { size_t n = 1; while (n < pos && n < 4 && ((unsigned char)buf[pos - n] & 0xC0) == 0x80) n++; return n; } static void redraw_input(const char *input_line, size_t input_len, size_t input_pos) { int avail = term_cols - 2; /* columns available after "> " */ size_t total_cols = display_cols(input_line, input_len); size_t cpos_cols = display_cols(input_line, input_pos); printf("\033[%d;1H\033[K\033[32m>\033[0m ", term_rows); if ((int)total_cols <= avail) { /* Fits entirely */ fwrite(input_line, 1, input_len, stdout); printf("\033[%d;%dH", term_rows, (int)(cpos_cols + 3)); } else { /* Scroll horizontally: show a window around cursor */ size_t win_start_bytes = 0; size_t win_start_cols = 0; /* If cursor is past the visible area, shift window */ if ((int)cpos_cols >= avail) { /* Move window so cursor is near the right edge */ size_t target_cols = cpos_cols - (size_t)(avail - 1); size_t i = 0, cols = 0; while (i < input_len && cols < target_cols) { unsigned char c = (unsigned char)input_line[i]; if (c < 0x80) i++; else if ((c & 0xE0) == 0xC0) i += 2; else if ((c & 0xF0) == 0xE0) i += 3; else if ((c & 0xF8) == 0xF0) i += 4; else i++; cols++; } win_start_bytes = i; win_start_cols = cols; } /* Write characters that fit in avail columns */ size_t i = win_start_bytes; int cols_written = 0; while (i < input_len && cols_written < avail) { unsigned char c = (unsigned char)input_line[i]; size_t clen = 1; if (c >= 0x80) { if ((c & 0xE0) == 0xC0) clen = 2; else if ((c & 0xF0) == 0xE0) clen = 3; else if ((c & 0xF8) == 0xF0) clen = 4; } fwrite(input_line + i, 1, clen, stdout); i += clen; cols_written++; } int cursor_col = (int)(cpos_cols - win_start_cols + 3); printf("\033[%d;%dH", term_rows, cursor_col); } fflush(stdout); } static void cleanup(void) { printf("\033[1;%dr", term_rows); /* reset scroll region */ printf("\033[%d;1H\033[K", term_rows - 1); printf("\033[%d;1H\033[K\n", term_rows); tcsetattr(STDIN_FILENO, TCSANOW, &orig_term); if (logfp) fclose(logfp); } static void usage(const char *prog) { fprintf(stderr, "Usage: %s [port]\n", prog); exit(1); } int main(int argc, char *argv[]) { const char *host, *port = "6667"; if (argc < 3) usage(argv[0]); snprintf(nick, sizeof(nick), "%s", argv[1]); host = argv[2]; if (argc >= 4) port = argv[3]; memset(win_chans, 0, sizeof(win_chans)); ai_config_load(); logfp = fopen("irc.log", "a"); if (logfp) fprintf(logfp, "--- Session started ---\n"); /* Set terminal to raw mode */ struct termios raw_term; tcgetattr(STDIN_FILENO, &orig_term); raw_term = orig_term; raw_term.c_lflag &= ~(ICANON | ECHO); raw_term.c_cc[VMIN] = 1; raw_term.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSANOW, &raw_term); atexit(cleanup); signal(SIGINT, sigint_handler); signal(SIGWINCH, sigwinch_handler); /* Set up scrolling region (leave last 2 rows for status + input) */ get_term_size(); printf("\033[2J\033[H"); /* clear screen */ printf("\033[1;%dr", term_rows - 2); printf("\033[1;1H"); /* cursor to top of scroll region */ draw_statusbar(); wprintf(WL_STATUS, "Connecting to %s:%s as %s...\n", host, port, nick); sock_fd = irc_connect(host, port); wprintf(WL_STATUS, "Connected.\n"); irc_send_raw("NICK %s", nick); const char *user = getenv("USER"); if (!user) user = nick; const char *realname = nick; struct passwd *pw = getpwuid(getuid()); if (pw && pw->pw_gecos && pw->pw_gecos[0]) { /* GECOS may have commas; use only first field */ static char gecos[128]; snprintf(gecos, sizeof(gecos), "%s", pw->pw_gecos); char *comma = strchr(gecos, ','); if (comma) *comma = '\0'; realname = gecos; } irc_send_raw("USER %s 0 * :%s", user, realname); draw_statusbar(); fd_set fds; char input_line[BUF_SIZE]; size_t input_pos = 0; /* cursor position */ size_t input_len = 0; /* total length */ char yank_buf[BUF_SIZE] = ""; size_t yank_len = 0; int esc_pending = 0; int esc_bracket = 0; /* got ESC [ */ /* Command history */ #define HIST_SIZE 50 char history[HIST_SIZE][BUF_SIZE]; int hist_count = 0; int hist_pos = 0; /* current browse position */ 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); FD_SET(sock_fd, &fds); FD_SET(STDIN_FILENO, &fds); int maxfd = sock_fd > STDIN_FILENO ? sock_fd : STDIN_FILENO; /* Add translation pipes to select */ for (int ti = 0; ti < translate_count; ti++) { FD_SET(translate_pending[ti].fd, &fds); if (translate_pending[ti].fd > maxfd) maxfd = translate_pending[ti].fd; } struct timeval tv; tv.tv_sec = 0; tv.tv_usec = esc_pending ? 50000 : 500000; int ret = select(maxfd + 1, &fds, NULL, NULL, &tv); if (ret < 0) { if (errno == EINTR) continue; die("select"); } /* Check translation results */ for (int ti = 0; ti < translate_count; ) { if (FD_ISSET(translate_pending[ti].fd, &fds)) { char tbuf[1024]; int n = read(translate_pending[ti].fd, tbuf, sizeof(tbuf) - 1); close(translate_pending[ti].fd); waitpid(translate_pending[ti].pid, NULL, 0); if (n > 0) { tbuf[n] = '\0'; /* Display in italic */ wprintf(translate_pending[ti].level, " \033[3m%s\033[0m\n", tbuf); } translate_count--; if (ti < translate_count) translate_pending[ti] = translate_pending[translate_count]; } else { ti++; } } if (ret == 0 && esc_pending) { esc_pending = 0; } if (got_sigint) { got_sigint = 0; printf("\033[%d;1H\033[KWanna quit? [y/N] ", term_rows); fflush(stdout); char qbuf[16]; size_t qpos = 0; int quit = 0; for (;;) { unsigned char qch; ssize_t r = read(STDIN_FILENO, &qch, 1); if (r <= 0) break; if (qch == '\r' || qch == '\n') { quit = (qpos > 0 && (qbuf[0] == 'y' || qbuf[0] == 'Y')); break; } else if ((qch == 127 || qch == 0x08) && qpos > 0) { qpos--; printf("\b \b"); fflush(stdout); } else if (qch >= 32 && qpos < sizeof(qbuf) - 1) { qbuf[qpos++] = qch; ssize_t w = write(STDOUT_FILENO, &qch, 1); (void)w; } } if (quit) { irc_send_raw("QUIT :Leaving"); break; } redraw_input(input_line, input_len, input_pos); continue; } if (got_sigwinch) { got_sigwinch = 0; get_term_size(); printf("\033[1;%dr", term_rows - 2); redraw_window(); redraw_input(input_line, input_len, input_pos); continue; } if (FD_ISSET(sock_fd, &fds)) { ssize_t n = read(sock_fd, recv_buf + recv_len, sizeof(recv_buf) - recv_len - 1); if (n <= 0) { printf("* Disconnected from server.\n"); break; } recv_len += (size_t)n; recv_buf[recv_len] = '\0'; process_recv(); } if (FD_ISSET(STDIN_FILENO, &fds)) { unsigned char ch; ssize_t n = read(STDIN_FILENO, &ch, 1); if (n <= 0) { irc_send_raw("QUIT :EOF"); break; } if (esc_pending) { esc_pending = 0; if (ch == '[') { esc_bracket = 1; continue; } if (ch >= '1' && ch <= '9') { int lvl = ch - '1'; if (lvl < WL_MAX) { current_level = lvl; win_activity[lvl] = 0; scroll_offset = 0; redraw_window(); redraw_input(input_line, input_len, input_pos); } continue; } } if (esc_bracket) { esc_bracket = 0; if (ch == 'A') { /* Arrow up: previous history */ if (hist_pos > 0) { hist_pos--; input_len = strlen(history[hist_pos]); memcpy(input_line, history[hist_pos], input_len); input_pos = input_len; redraw_input(input_line, input_len, input_pos); } } else if (ch == 'B') { /* Arrow down: next history */ if (hist_pos < hist_count - 1) { hist_pos++; input_len = strlen(history[hist_pos]); memcpy(input_line, history[hist_pos], input_len); input_pos = input_len; redraw_input(input_line, input_len, input_pos); } else { hist_pos = hist_count; input_len = 0; input_pos = 0; redraw_input(input_line, input_len, input_pos); } } continue; } /* Reset tab completion on any non-tab key */ if (ch != 0x09) tab_idx = -1; if (ch == 0x1B) { esc_pending = 1; } else if (ch == '\r' || ch == '\n') { printf("\033[%d;1H\033[K", term_rows); input_line[input_len] = '\0'; if (input_len > 0) { /* Save to history */ if (hist_count < HIST_SIZE) { memcpy(history[hist_count], input_line, input_len + 1); hist_count++; } else { memmove(history[0], history[1], (HIST_SIZE-1) * BUF_SIZE); memcpy(history[HIST_SIZE-1], input_line, input_len + 1); } hist_pos = hist_count; handle_input(input_line); } input_pos = 0; input_len = 0; printf("\033[%d;1H\033[32m>\033[0m ", term_rows); fflush(stdout); } else if (ch == 0x01) { /* Ctrl-A: beginning of line */ input_pos = 0; redraw_input(input_line, input_len, input_pos); } else if (ch == 0x05) { /* Ctrl-E: end of line */ input_pos = input_len; redraw_input(input_line, input_len, input_pos); } else if (ch == 0x15) { /* Ctrl-U: kill to beginning */ if (input_pos > 0) { yank_len = input_pos; memcpy(yank_buf, input_line, yank_len); memmove(input_line, input_line + input_pos, input_len - input_pos); input_len -= input_pos; input_pos = 0; redraw_input(input_line, input_len, input_pos); } } else if (ch == 0x19) { /* Ctrl-Y: yank (paste) */ if (yank_len > 0 && input_len + yank_len < sizeof(input_line) - 1) { memmove(input_line + input_pos + yank_len, input_line + input_pos, input_len - input_pos); memcpy(input_line + input_pos, yank_buf, yank_len); input_len += yank_len; input_pos += yank_len; redraw_input(input_line, input_len, input_pos); } } else if (ch == 0x0B) { /* Ctrl-K: kill to end */ yank_len = input_len - input_pos; memcpy(yank_buf, input_line + input_pos, yank_len); input_len = input_pos; redraw_input(input_line, input_len, input_pos); } else if (ch == 0x09) { /* Tab: nick completion */ char (*nlist)[NICK_LEN] = NULL; int ncount = 0; 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++; } int found = 0; for (int tries = 0; tries < ncount; tries++) { int ni = (tab_idx + tries) % ncount; if (tab_prefix_len == 0 || strncasecmp(nlist[ni], input_line + tab_start, tab_prefix_len) == 0) { tab_idx = ni; const char *compl = nlist[ni]; size_t clen = strlen(compl); char suffix[4] = ""; if (tab_start == 0 && current_level >= WL_CHAN) strcpy(suffix, ": "); else strcpy(suffix, " "); size_t slen = strlen(suffix); 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 + tab_end, tail); memcpy(input_line + tab_start, compl, clen); memcpy(input_line + tab_start + clen, suffix, slen); tab_end = tab_start + clen + slen; input_pos = tab_end; redraw_input(input_line, input_len, input_pos); tab_idx++; found = 1; break; } } if (!found) tab_idx = -1; } continue; /* don't reset tab state */ } else if (ch == 0x10) { /* Ctrl-P: page up in scrollback */ int page = term_rows - 3; if (page < 1) page = 1; scroll_offset += page; int max_scroll = win_buf[current_level].count - (term_rows - 2); if (max_scroll < 0) max_scroll = 0; if (scroll_offset > max_scroll) scroll_offset = max_scroll; redraw_window(); redraw_input(input_line, input_len, input_pos); } else if (ch == 0x0E) { /* Ctrl-N: page down in scrollback */ int page = term_rows - 3; if (page < 1) page = 1; scroll_offset -= page; if (scroll_offset < 0) scroll_offset = 0; redraw_window(); redraw_input(input_line, input_len, input_pos); } else if (ch == 127 || ch == 0x08) { if (input_pos > 0) { size_t clen = utf8_back(input_line, input_pos); memmove(input_line + input_pos - clen, input_line + input_pos, input_len - input_pos); input_pos -= clen; input_len -= clen; redraw_input(input_line, input_len, input_pos); } } else if (ch == 0x04) { irc_send_raw("QUIT :EOF"); break; } else if (ch >= 32 && input_len < sizeof(input_line) - 1) { /* For UTF-8 lead bytes, figure out how many * bytes to expect and read them all */ size_t seq_len = 1; if ((ch & 0xE0) == 0xC0) seq_len = 2; else if ((ch & 0xF0) == 0xE0) seq_len = 3; else if ((ch & 0xF8) == 0xF0) seq_len = 4; unsigned char seq[4]; seq[0] = ch; for (size_t si = 1; si < seq_len; si++) { unsigned char cb; ssize_t r = read(STDIN_FILENO, &cb, 1); if (r <= 0) break; seq[si] = cb; } if (input_len + seq_len < sizeof(input_line) - 1) { memmove(input_line + input_pos + seq_len, input_line + input_pos, input_len - input_pos); memcpy(input_line + input_pos, seq, seq_len); input_pos += seq_len; input_len += seq_len; redraw_input(input_line, input_len, input_pos); } } } } close(sock_fd); return 0; }