#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 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; /* 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 window state */ static struct { char name[128]; /* channel name */ char modes[64]; /* channel modes (e.g. "+nt") */ char my_prefix; /* '@', '+', or '\0' */ } win_chans[MAX_CHAN_WINS]; /* 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] ? "]" : ""); /* 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(); int visible = term_rows - 2; /* scroll region height */ int count = win_buf[current_level].count; int show = count < visible ? count : visible; /* Clear scroll region */ for (int i = 1; i <= term_rows - 2; i++) printf("\033[%d;1H\033[K", i); /* Print last 'show' lines */ int start_idx; if (count < SCROLLBACK) start_idx = count - show; else start_idx = (win_buf[current_level].head - show + SCROLLBACK) % SCROLLBACK; for (int i = 0; i < show; i++) { int idx = (start_idx + i) % SCROLLBACK; printf("\033[%d;1H%s", i + 1, 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) { /* * Move to last line of scroll region, issue newline to * scroll up, then print the text on the new blank line. */ printf("\033[%d;1H\n\033[K%s", term_rows - 2, timestamped); 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 */ 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 void handle_line(char *line) { char converted[BUF_SIZE]; to_iso8859_1((unsigned char *)line, strlen(line), converted, sizeof(converted)); 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 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; wprintf(lvl, "* %s has joined %s\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; wprintf(lvl, "* %s has left %s (%s)\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'; draw_statusbar(); } } } else if (strcmp(cmd, "QUIT") == 0) { char *reason = params; if (reason && reason[0] == ':') reason++; wprintf(WL_STATUS, "* %s has quit (%s)\n", sender, reason ? reason : ""); } else if (strcmp(cmd, "NICK") == 0) { char *newnick = params; if (newnick && newnick[0] == ':') newnick++; wprintf(WL_STATUS, "* %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, "* %s sets mode %s %s\n", sender[0] ? sender : "*", modestr, modeargs ? modeargs : ""); draw_statusbar(); } else { wprintf(WL_STATUS, "* %s sets mode %s\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, "353") == 0 && params) { /* RPL_NAMREPLY: = :names... * Check our prefix in the names list */ char *colon = strchr(params, ':'); if (colon) { /* Find channel name before the colon */ char *p = params; char *chan = NULL; char *sp; /* Skip our nick */ sp = strchr(p, ' '); if (sp) p = sp + 1; /* Skip = or @ or * */ sp = strchr(p, ' '); if (sp) p = sp + 1; /* Channel */ sp = strchr(p, ' '); if (sp) { *sp = '\0'; chan = p; } if (chan) { int idx = chan_win_idx(chan); if (idx >= 0) { colon++; /* Find our nick in the list */ char *names = colon; char *tok = strtok(names, " "); while (tok) { const char *n = tok; char pf = '\0'; if (*n == '@' || *n == '+') { pf = *n; n++; } if (strcasecmp(n, nick) == 0) { win_chans[idx].my_prefix = pf; draw_statusbar(); break; } 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, "nick") == 0 && args) { snprintf(nick, sizeof(nick), "%s", args); irc_send_raw("NICK %s", args); draw_statusbar(); } 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, "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 (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) { size_t cpos_cols = display_cols(input_line, input_pos); printf("\033[%d;1H\033[K\033[32m>\033[0m ", term_rows); fwrite(input_line, 1, input_len, stdout); printf("\033[%d;%dH", term_rows, (int)(cpos_cols + 3)); fflush(stdout); } 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)); /* Set terminal to raw mode */ struct termios orig_term, 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); 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; 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; redraw_window(); redraw_input(input_line, input_len, input_pos); } continue; } } 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 == 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); } } } } /* Restore terminal */ printf("\033[1;%dr", term_rows); /* reset scroll region */ printf("\033[%d;1H\033[K", term_rows - 1); /* clear status bar */ printf("\033[%d;1H\033[K", term_rows); /* clear input line */ tcsetattr(STDIN_FILENO, TCSANOW, &orig_term); close(sock_fd); return 0; }