From e2a5ddf9a0a6d0c35dae3f013b2e4e1f260efe71 Mon Sep 17 00:00:00 2001 From: Anders Holck Date: Wed, 29 Apr 2026 11:44:36 +0200 Subject: [PATCH] first commit --- Makefile | 19 ++ README.md | 81 +++++ charset.c | 139 ++++++++ charset.h | 15 + main.c | 948 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1202 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 charset.c create mode 100644 charset.h create mode 100644 main.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab67847 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +CC = gcc +CFLAGS = -Wall -Wextra -pedantic -O2 +LDFLAGS = +TARGET = irc +SRCS = main.c charset.c +OBJS = $(SRCS:.c=.o) + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJS) $(TARGET) + +.PHONY: all clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..1250779 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Mirk — Holck's IRC Client + +A lightweight terminal IRC client written in C with automatic charset conversion and ircII-style window levels. + +## Features + +- **Automatic charset conversion** — detects UTF-8, UTF-16 (BOM), and ISO-8859-1 input; always sends ISO-8859-1 on the wire +- **Window levels** — isolated windows with independent 500-line scrollback: + - Window 1: Status (server messages, numerics, notices) + - Window 2: Private messages + - Windows 3–9: Channels +- **Status bar** — shows current window, channel, nick prefix (@/+), and channel modes +- **UTF-8 terminal support** — full multi-byte input editing +- **CTCP VERSION** reply with OS info +- **SIGWINCH** handling (terminal resize) +- **Ident** — works with system identd on port 113 +- **Real name** from passwd GECOS field + +## Building + +``` +make +``` + +Requires only a C compiler and POSIX headers. No external dependencies. + +## Usage + +``` +./irc [port] +``` + +Port defaults to 6667. + +## Key Bindings + +| Key | Action | +|-----|--------| +| ESC+1–9 | Switch window | +| Ctrl-A | Beginning of line | +| Ctrl-E | End of line | +| Ctrl-U | Kill to beginning (yank buffer) | +| Ctrl-K | Kill to end (yank buffer) | +| Ctrl-Y | Yank (paste) | +| Ctrl-D | Quit (EOF) | +| Ctrl-C | Quit prompt (Y/N, default N) | + +## Commands + +| Command | Description | +|---------|-------------| +| `/join #channel` | Join channel (assigned to current window) | +| `/part [#channel]` | Part channel (defaults to current) | +| `/msg ` | Send private message | +| `/nick ` | Change nickname | +| `/mode ` | Set mode | +| `/whois ` | WHOIS query | +| `/wii ` | Extended WHOIS (queries remote server) | +| `/quit [reason]` | Quit (default: "See you later") | +| `/raw ` | Send raw IRC command | + +Typing text without a `/` prefix sends to the channel on the current window. + +## Window Workflow + +1. Press ESC+3 to switch to window 3 +2. `/join #channel` — the channel is bound to that window +3. Press ESC+4, `/join #other` — second channel on window 4 +4. ESC+1 to check status, ESC+2 for private messages + +Each window maintains its own scrollback. Switching redraws the full history. + +## CTCP VERSION Reply + +``` +Holck's Mirk, OS: Linux 6.x.x x86_64 :: This space available for rent +``` + +## License + +Public domain. diff --git a/charset.c b/charset.c new file mode 100644 index 0000000..cd8a428 --- /dev/null +++ b/charset.c @@ -0,0 +1,139 @@ +#include "charset.h" +#include + +static int is_utf16_le(const unsigned char *in, size_t len) +{ + return len >= 2 && in[0] == 0xFF && in[1] == 0xFE; +} + +static int is_utf16_be(const unsigned char *in, size_t len) +{ + return len >= 2 && in[0] == 0xFE && in[1] == 0xFF; +} + +static int is_utf8(const unsigned char *in, size_t len) +{ + size_t i; + int has_multibyte = 0; + + for (i = 0; i < len; i++) { + if (in[i] < 0x80) + continue; + has_multibyte = 1; + if ((in[i] & 0xE0) == 0xC0) { + if (i + 1 >= len || (in[i+1] & 0xC0) != 0x80) + return 0; + i += 1; + } else if ((in[i] & 0xF0) == 0xE0) { + if (i + 2 >= len || (in[i+1] & 0xC0) != 0x80 || + (in[i+2] & 0xC0) != 0x80) + return 0; + i += 2; + } else if ((in[i] & 0xF8) == 0xF0) { + if (i + 3 >= len || (in[i+1] & 0xC0) != 0x80 || + (in[i+2] & 0xC0) != 0x80 || (in[i+3] & 0xC0) != 0x80) + return 0; + i += 3; + } else { + return 0; + } + } + return has_multibyte; +} + +static int utf8_to_codepoint(const unsigned char *in, size_t remaining, + unsigned int *cp) +{ + if (in[0] < 0x80) { + *cp = in[0]; + return 1; + } else if ((in[0] & 0xE0) == 0xC0) { + if (remaining < 2) return 0; + *cp = ((in[0] & 0x1F) << 6) | (in[1] & 0x3F); + return 2; + } else if ((in[0] & 0xF0) == 0xE0) { + if (remaining < 3) return 0; + *cp = ((in[0] & 0x0F) << 12) | ((in[1] & 0x3F) << 6) | + (in[2] & 0x3F); + return 3; + } else if ((in[0] & 0xF8) == 0xF0) { + if (remaining < 4) return 0; + *cp = ((in[0] & 0x07) << 18) | ((in[1] & 0x3F) << 12) | + ((in[2] & 0x3F) << 6) | (in[3] & 0x3F); + return 4; + } + return 0; +} + +static int utf16le_to_iso8859_1(const unsigned char *in, size_t in_len, + char *out, size_t out_size) +{ + size_t i, o = 0; + + for (i = 2; i + 1 < in_len && o + 1 < out_size; i += 2) { + unsigned int cp = in[i] | (in[i+1] << 8); + out[o++] = cp <= 0xFF ? (char)cp : '?'; + } + out[o] = '\0'; + return (int)o; +} + +static int utf16be_to_iso8859_1(const unsigned char *in, size_t in_len, + char *out, size_t out_size) +{ + size_t i, o = 0; + + for (i = 2; i + 1 < in_len && o + 1 < out_size; i += 2) { + unsigned int cp = (in[i] << 8) | in[i+1]; + out[o++] = cp <= 0xFF ? (char)cp : '?'; + } + out[o] = '\0'; + return (int)o; +} + +static int utf8_to_iso8859_1(const unsigned char *in, size_t in_len, + char *out, size_t out_size) +{ + size_t i = 0, o = 0; + + while (i < in_len && o + 1 < out_size) { + unsigned int cp; + int consumed = utf8_to_codepoint(in + i, in_len - i, &cp); + if (consumed == 0) { + out[o++] = '?'; + i++; + continue; + } + out[o++] = cp <= 0xFF ? (char)cp : '?'; + i += consumed; + } + out[o] = '\0'; + return (int)o; +} + +int to_iso8859_1(const unsigned char *in, size_t in_len, + char *out, size_t out_size) +{ + if (!in || !out || out_size == 0) + return 0; + + if (in_len == 0) { + out[0] = '\0'; + return 0; + } + + if (is_utf16_le(in, in_len)) + return utf16le_to_iso8859_1(in, in_len, out, out_size); + + if (is_utf16_be(in, in_len)) + return utf16be_to_iso8859_1(in, in_len, out, out_size); + + if (is_utf8(in, in_len)) + return utf8_to_iso8859_1(in, in_len, out, out_size); + + /* Already ISO-8859-1 / ASCII — passthrough */ + size_t copy = in_len < out_size - 1 ? in_len : out_size - 1; + memcpy(out, in, copy); + out[copy] = '\0'; + return (int)copy; +} diff --git a/charset.h b/charset.h new file mode 100644 index 0000000..59569b7 --- /dev/null +++ b/charset.h @@ -0,0 +1,15 @@ +#ifndef CHARSET_H +#define CHARSET_H + +#include + +/* + * Convert input (UTF-8, UTF-16 with BOM, or ISO-8859-1) to ISO-8859-1. + * Unmappable characters (> U+00FF) are replaced with '?'. + * Returns number of bytes written (excluding null terminator). + * Output is always null-terminated. + */ +int to_iso8859_1(const unsigned char *in, size_t in_len, + char *out, size_t out_size); + +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..e26f032 --- /dev/null +++ b/main.c @@ -0,0 +1,948 @@ +#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, 1=msg, 2-8=channels */ +#define WL_STATUS 0 +#define WL_MSG 1 +#define WL_CHAN 2 +#define WL_MAX 9 +#define MAX_CHAN_WINS 7 +#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; + 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 if (current_level == WL_MSG) { + chan = "(messages)"; + } else { + chan = "(status)"; + } + + snprintf(bar, sizeof(bar), " [%d:%s] %s%s %s%s%s", + current_level + 1, + current_level == WL_STATUS ? "status" : + current_level == WL_MSG ? "msg" : 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> ", 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); + unsigned char ans; + ssize_t r = read(STDIN_FILENO, &ans, 1); + if (r > 0 && (ans == 'y' || ans == 'Y')) { + 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(); + } + 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> ", 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; +}