fe75236fad
- /me <action> sends CTCP ACTION - /slap <nick> slaps with a large trout - /q <nick> sets query target, /q clears it - /topic to view/set channel topic - Tab completion for nicks (cycling, : suffix at line start) - Nick list tracking via NAMES/JOIN/PART/QUIT/NICK - Bold formatting for joins, parts, quits, mode changes - Incoming CTCP ACTION displayed as * nick action - Updated README with all commands and features
1356 lines
37 KiB
C
1356 lines
37 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 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: <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, "332") == 0 && params) {
|
|
/* RPL_TOPIC: <nick> <channel> :<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: <nick> = <channel> :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 <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));
|
|
|
|
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;
|
|
}
|