#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "charset.h" #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] = ""; /* 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 = "(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; 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 */ int buf_start; if (count <= SCROLLBACK) buf_start = start_pos; else buf_start = (win_buf[current_level].head - count + start_pos + SCROLLBACK) % SCROLLBACK; /* Print lines aligned to bottom of scroll region */ int first_row = visible - show + 1; for (int i = 0; i < show; i++) { int idx; if (count <= SCROLLBACK) idx = buf_start + i; else idx = (buf_start + i) % SCROLLBACK; printf("\033[%d;1H%s", first_row + i, win_buf[current_level].lines[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); } else { int lvl = chan_to_level(target); wprintf(lvl, "<%s> %s\n", sender, text); } } } 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, "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, "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); } else { if (query_target[0]) wprintf(WL_MSG, "* No longer talking to %s\n", query_target); query_target[0] = '\0'; } } 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, "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)); 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 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 */ for (;;) { FD_ZERO(&fds); FD_SET(sock_fd, &fds); FD_SET(STDIN_FILENO, &fds); int maxfd = sock_fd > STDIN_FILENO ? sock_fd : STDIN_FILENO; 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"); } 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 >= '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; } } /* 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) 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 */ int cidx = -1; if (current_level >= WL_CHAN) cidx = current_level - WL_CHAN; if (cidx >= 0 && win_chans[cidx].nick_count > 0) { /* Find word start */ 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_idx = 0; } else { 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; if (tab_prefix_len == 0 || strncasecmp(win_chans[cidx].nicks[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]; size_t clen = strlen(compl); /* Add ": " if at start of line */ char suffix[4] = ""; if (tab_start == 0) strcpy(suffix, ": "); else strcpy(suffix, " "); size_t slen = strlen(suffix); size_t tail = input_len - input_pos; 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); memcpy(input_line + tab_start, compl, clen); memcpy(input_line + tab_start + clen, suffix, slen); input_pos = tab_start + clen + slen; 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; }