Files
holckmirk/main.c
T
anders 71f6699aa9 Add window levels, status bar, UTF-8 input, charset conversion
- Isolated window levels with 500-line scrollback per window
  - Window 1: status + private messages
  - Windows 2-9: channels
- Status bar showing window, channel, nick prefix, channel modes
- Automatic charset conversion (UTF-8/UTF-16 -> ISO-8859-1 on wire)
- UTF-8 aware input editing with Ctrl-A/E/U/K/Y
- CTCP VERSION reply with real OS info from uname()
- Real name from passwd GECOS field
- SIGWINCH handling for terminal resize
- Ctrl-C quit confirmation (y + Enter)
- /whois, /wii, /mode, /quit with default reason
- Green prompt, timestamps on all messages
2026-04-29 12:03:57 +02:00

967 lines
25 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/ioctl.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/utsname.h>
#include <termios.h>
#include <pwd.h>
#include <signal.h>
#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: <nick> <channel> <modes> */
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: <nick> = <channel> :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 <nick> <server> [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;
}