Files
holckmirk/main.c
T
anders f4324ef2b4 Fix scrollback wrap bug, add /clear, add TCP keepalive
- Fix off-by-one in redraw_window circular buffer indexing (<=  to <)
  that caused stale content after 500 lines filled
- Add /clear command to reset current window scrollback
- Add TCP keepalive and application-level ping timeout to detect
  dead connections
2026-05-18 08:40:12 +02:00

2288 lines
66 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 <sys/wait.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/utsname.h>
#include <termios.h>
#include <pwd.h>
#include <signal.h>
#include <netinet/tcp.h>
#include "charset.h"
static size_t display_cols(const char *buf, size_t bytes);
#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;
static time_t last_recv = 0; /* time of last data from server */
static int ping_sent = 0; /* we sent a client PING, awaiting reply */
/* 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] = "";
static int translate_public = 0; /* /trans toggle: echo translations to channel */
static int translate_enabled = 1; /* master toggle for translation */
static int irc_colors = 1; /* display IRC colour codes as ANSI */
static char rent_msg[256] = "This space available for rent";
static int log_enabled = 0;
/* Track nicks who sent us private messages */
#define MAX_PM_NICKS 32
static char pm_nicks[MAX_PM_NICKS][NICK_LEN];
static int pm_nick_count = 0;
static void pm_nick_add(const char *n)
{
/* Move to front if already present */
for (int i = 0; i < pm_nick_count; i++) {
if (strcasecmp(pm_nicks[i], n) == 0) {
if (i == 0) return;
char tmp[NICK_LEN];
memcpy(tmp, pm_nicks[i], NICK_LEN);
memmove(pm_nicks[1], pm_nicks[0], i * NICK_LEN);
memcpy(pm_nicks[0], tmp, NICK_LEN);
return;
}
}
/* Insert at front */
if (pm_nick_count < MAX_PM_NICKS)
pm_nick_count++;
memmove(pm_nicks[1], pm_nicks[0], (pm_nick_count - 1) * NICK_LEN);
snprintf(pm_nicks[0], NICK_LEN, "%s", n);
}
/* AI translation config */
static struct {
char type[32]; /* ollama, vllm, openai */
char host[256]; /* host:port */
int port;
char key[256];
char model[128];
char target_lang[64];
char skip_langs[256];
int enabled;
} ai_cfg;
/* Pending translation pipe fds */
#define MAX_TRANSLATE 8
static struct {
int fd; /* read end of pipe from child */
int level; /* window to display in */
pid_t pid;
char target[128]; /* channel/nick to echo translation to */
char original[512]; /* original text for similarity check */
} translate_pending[MAX_TRANSLATE];
static int translate_count = 0;
static void ai_config_load(void)
{
memset(&ai_cfg, 0, sizeof(ai_cfg));
char path[512];
snprintf(path, sizeof(path), "%s/.hircrc", getenv("HOME"));
FILE *f = fopen(path, "r");
if (!f) {
/* Create default config */
f = fopen(path, "w");
if (f) {
fprintf(f, "# AI translation settings\n");
fprintf(f, "ai_type=\n");
fprintf(f, "ai_host=\n");
fprintf(f, "ai_key=\n");
fprintf(f, "ai_model=\n");
fprintf(f, "ai_target_lang=\n");
fprintf(f, "ai_skip_langs=\n");
fprintf(f, "translate=on\n");
fprintf(f, "irc_colors=1\n");
fclose(f);
}
return;
}
char line[512];
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = '\0';
if (line[0] == '#' || line[0] == '\0') continue;
char *eq = strchr(line, '=');
if (!eq) continue;
*eq++ = '\0';
if (strcmp(line, "ai_type") == 0)
snprintf(ai_cfg.type, sizeof(ai_cfg.type), "%s", eq);
else if (strcmp(line, "ai_host") == 0) {
snprintf(ai_cfg.host, sizeof(ai_cfg.host), "%s", eq);
char *colon = strrchr(ai_cfg.host, ':');
if (colon) {
*colon = '\0';
ai_cfg.port = atoi(colon + 1);
}
}
else if (strcmp(line, "ai_port") == 0)
ai_cfg.port = atoi(eq);
else if (strcmp(line, "ai_key") == 0)
snprintf(ai_cfg.key, sizeof(ai_cfg.key), "%s", eq);
else if (strcmp(line, "ai_model") == 0)
snprintf(ai_cfg.model, sizeof(ai_cfg.model), "%s", eq);
else if (strcmp(line, "ai_target_lang") == 0)
snprintf(ai_cfg.target_lang, sizeof(ai_cfg.target_lang), "%s", eq);
else if (strcmp(line, "ai_skip_langs") == 0)
snprintf(ai_cfg.skip_langs, sizeof(ai_cfg.skip_langs), "%s", eq);
else if (strcmp(line, "translate") == 0) {
if (strcmp(eq, "off") == 0) { translate_enabled = 0; translate_public = 0; }
else if (strcmp(eq, "public") == 0) { translate_enabled = 1; translate_public = 1; }
else { translate_enabled = 1; translate_public = 0; }
}
else if (strcmp(line, "irc_colors") == 0)
irc_colors = atoi(eq);
else if (strcmp(line, "rent") == 0)
snprintf(rent_msg, sizeof(rent_msg), "%s", eq);
else if (strcmp(line, "log") == 0)
log_enabled = (strcmp(eq, "true") == 0 || atoi(eq));
}
fclose(f);
/* Enable only if minimum config is present */
if (ai_cfg.host[0] && ai_cfg.port && ai_cfg.model[0] && ai_cfg.target_lang[0])
ai_cfg.enabled = 1;
}
static void config_save(void)
{
char path[512];
snprintf(path, sizeof(path), "%s/.hircrc", getenv("HOME"));
FILE *f = fopen(path, "w");
if (!f) return;
fprintf(f, "# AI translation settings\n");
fprintf(f, "ai_type=%s\n", ai_cfg.type);
if (ai_cfg.port)
fprintf(f, "ai_host=%s:%d\n", ai_cfg.host, ai_cfg.port);
else
fprintf(f, "ai_host=%s\n", ai_cfg.host);
fprintf(f, "ai_key=%s\n", ai_cfg.key);
fprintf(f, "ai_model=%s\n", ai_cfg.model);
fprintf(f, "ai_target_lang=%s\n", ai_cfg.target_lang);
fprintf(f, "ai_skip_langs=%s\n", ai_cfg.skip_langs);
fprintf(f, "translate=%s\n",
!translate_enabled ? "off" : translate_public ? "public" : "on");
fprintf(f, "irc_colors=%d\n", irc_colors);
fclose(f);
}
/* Base64 decode: returns decoded length, or -1 if not valid base64 */
static int base64_decode(const char *in, size_t in_len, char *out, size_t out_size)
{
static const int T[256] = {
['A']=0,['B']=1,['C']=2,['D']=3,['E']=4,['F']=5,['G']=6,['H']=7,
['I']=8,['J']=9,['K']=10,['L']=11,['M']=12,['N']=13,['O']=14,['P']=15,
['Q']=16,['R']=17,['S']=18,['T']=19,['U']=20,['V']=21,['W']=22,['X']=23,
['Y']=24,['Z']=25,['a']=26,['b']=27,['c']=28,['d']=29,['e']=30,['f']=31,
['g']=32,['h']=33,['i']=34,['j']=35,['k']=36,['l']=37,['m']=38,['n']=39,
['o']=40,['p']=41,['q']=42,['r']=43,['s']=44,['t']=45,['u']=46,['v']=47,
['w']=48,['x']=49,['y']=50,['z']=51,['0']=52,['1']=53,['2']=54,['3']=55,
['4']=56,['5']=57,['6']=58,['7']=59,['8']=60,['9']=61,['+']=62,['/']=63,
};
/* Strip trailing padding for length calc */
size_t pad = 0;
while (in_len > 0 && in[in_len - 1] == '=') { pad++; in_len--; }
if (in_len < 4) return -1;
size_t out_len = in_len * 3 / 4;
if (out_len >= out_size) return -1;
size_t o = 0;
for (size_t i = 0; i < in_len; i += 4) {
int n = (int)(in_len - i);
if (n < 2) break;
unsigned int a = T[(unsigned char)in[i]];
unsigned int b = T[(unsigned char)in[i+1]];
out[o++] = (a << 2) | (b >> 4);
if (n > 2 && i + 2 < in_len + pad) {
unsigned int c = (i+2 < in_len) ? T[(unsigned char)in[i+2]] : 0;
out[o++] = (b << 4) | (c >> 2);
if (n > 3 && i + 3 < in_len + pad) {
unsigned int d = (i+3 < in_len) ? T[(unsigned char)in[i+3]] : 0;
out[o++] = (c << 6) | d;
}
}
}
/* Adjust for padding */
if (pad == 1 && o > 0) o--;
if (pad == 2 && o > 0) o--;
out[o] = '\0';
return (int)o;
}
/* Check if text looks like base64 and decode it. Returns 1 if decoded. */
static int try_base64_decode(const char *text, char *out, size_t out_size)
{
size_t len = strlen(text);
if (len < 8 || len > 1000) return 0;
/* Must be valid base64 chars only */
size_t i;
for (i = 0; i < len; i++) {
char c = text[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '+' || c == '/' || c == '='))
return 0;
}
/* Length must be multiple of 4 */
if (len % 4 != 0) return 0;
int n = base64_decode(text, len, out, out_size);
if (n < 4) return 0;
/* Check that result looks like text (mostly printable) */
int printable = 0;
for (int j = 0; j < n; j++) {
unsigned char c = (unsigned char)out[j];
if ((c >= 0x20 && c <= 0x7e) || c == '\n' || c == '\r' || c == '\t' || c >= 0x80)
printable++;
}
if (printable * 100 / n < 80) return 0;
return 1;
}
static int needs_translation(const char *text)
{
if (!ai_cfg.enabled || !translate_enabled) return 0;
/* Skip very short messages */
int words = 0;
for (const char *p = text; *p; p++)
if (*p == ' ') words++;
if (words < 1 && strlen(text) < 6) return 0;
/* Skip messages that are just URLs */
if (strncmp(text, "http://", 7) == 0 || strncmp(text, "https://", 8) == 0) {
/* If no space after URL, it's just a link */
const char *sp = strchr(text, ' ');
if (!sp) return 0;
}
/* Pre-filter: if text contains common words from skip languages, skip */
static const struct { const char *lang; const char *words[33]; } lang_words[] = {
{"swedish", {"jag", "och", "att", "det", "inte", "var",
"som", "med", "har", "den", "kan",
"ska", "till", "eller", "men",
"ett", "en", "ta", "vad", "hur",
"dig", "du", "vi", "de", "sig",
"hade", "sedan", "bara",
"\xc3\xa4r", "\xc3\xa5", "f\xc3\xb6r",
"p\xc3\xa5",
NULL}},
{"english", {"the", "and", "that", "this", "with",
"have", "was", "are", "you", "not",
"from", "but", "for", "can", NULL}},
{NULL, {NULL}}
};
char lower[1024];
snprintf(lower, sizeof(lower), "%s", text);
for (char *p = lower; *p; p++)
if (*p >= 'A' && *p <= 'Z') *p += 32;
int hits = 0;
for (int i = 0; lang_words[i].lang; i++) {
if (!strstr(ai_cfg.skip_langs, lang_words[i].lang)) continue;
for (int j = 0; lang_words[i].words[j]; j++) {
const char *w = lang_words[i].words[j];
size_t wlen = strlen(w);
char *p = lower;
while ((p = strstr(p, w)) != NULL) {
/* Check word boundaries (non-alpha on both sides) */
int before_ok = (p == lower) ||
!(*(p-1) >= 'a' && *(p-1) <= 'z');
int after_ok = !p[wlen] ||
!(p[wlen] >= 'a' && p[wlen] <= 'z');
if (before_ok && after_ok) { hits++; break; }
p++;
}
}
}
int threshold = (words < 4) ? 1 : 2;
if (hits >= threshold) return 0;
return 1;
}
static void translate_async(const char *text, int level, const char *target)
{
if (translate_count >= MAX_TRANSLATE) return;
int pipefd[2];
if (pipe(pipefd) < 0) return;
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return;
}
if (pid == 0) {
/* Child: do HTTP request to AI API */
close(pipefd[0]);
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) _exit(1);
struct hostent *he = gethostbyname(ai_cfg.host);
if (!he) _exit(1);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(ai_cfg.port);
memcpy(&addr.sin_addr, he->h_addr, he->h_length);
if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0)
_exit(1);
/* Build JSON body */
char escaped[1024];
int ei = 0;
for (int i = 0; text[i] && ei < (int)sizeof(escaped) - 2; i++) {
if (text[i] == '"' || text[i] == '\\') escaped[ei++] = '\\';
if (text[i] == '\n') { escaped[ei++] = '\\'; escaped[ei++] = 'n'; }
else escaped[ei++] = text[i];
}
escaped[ei] = '\0';
char body[2048];
snprintf(body, sizeof(body),
"{\"model\":\"%s\",\"messages\":["
"{\"role\":\"system\",\"content\":\"Translate the message to both %s and %s. Format: %s translation|||%s translation. Nothing else.\"},"
"{\"role\":\"user\",\"content\":\"%s\"}"
"],\"stream\":false,\"tool_choice\":\"none\"}",
ai_cfg.model, ai_cfg.target_lang, ai_cfg.skip_langs,
ai_cfg.target_lang, ai_cfg.skip_langs, escaped);
char req[4096];
int rlen = snprintf(req, sizeof(req),
"POST /v1/chat/completions HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"%s%s%s"
"Connection: close\r\n\r\n%s",
ai_cfg.host, ai_cfg.port,
(int)strlen(body),
ai_cfg.key[0] ? "Authorization: Bearer " : "",
ai_cfg.key[0] ? ai_cfg.key : "",
ai_cfg.key[0] ? "\r\n" : "",
body);
if (write(s, req, rlen) < 0) _exit(1);
/* Read response */
char resp[8192];
int total = 0;
int n;
while ((n = read(s, resp + total, sizeof(resp) - total - 1)) > 0)
total += n;
resp[total] = '\0';
close(s);
/* Extract content from JSON response */
char *content = strstr(resp, "\"content\":");
if (content) {
content += 10;
while (*content == ' ' || *content == '\t') content++;
if (*content == '"') {
content++;
char result[1024];
int ri = 0;
while (*content && *content != '"' && ri < (int)sizeof(result) - 1) {
if (*content == '\\' && content[1]) {
content++;
if (*content == 'n') result[ri++] = ' ';
else result[ri++] = *content;
} else {
result[ri++] = *content;
}
content++;
}
result[ri] = '\0';
(void)!write(pipefd[1], result, ri);
}
}
close(pipefd[1]);
_exit(0);
}
/* Parent */
close(pipefd[1]);
translate_pending[translate_count].fd = pipefd[0];
translate_pending[translate_count].level = level;
translate_pending[translate_count].pid = pid;
snprintf(translate_pending[translate_count].target,
sizeof(translate_pending[translate_count].target),
"%s", target ? target : "");
snprintf(translate_pending[translate_count].original,
sizeof(translate_pending[translate_count].original),
"%s", text);
translate_count++;
}
/* 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 = query_target[0] ? query_target : "(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);
/* Right-align timestamp */
time_t now = time(NULL);
struct tm *tm = localtime(&now);
char rhs[64];
snprintf(rhs, sizeof(rhs), "[Holck Mirk - %02d%02d %02d:%02d]",
tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min);
size_t bar_len = strlen(bar);
size_t rhs_len = strlen(rhs);
if (bar_len + rhs_len + 1 < (size_t)term_cols) {
size_t pad = term_cols - rhs_len - 1;
while (bar_len < pad)
bar[bar_len++] = ' ';
memcpy(bar + pad, rhs, rhs_len);
bar[pad + rhs_len] = ' ';
bar[term_cols] = '\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 */
/* mIRC colour code to ANSI */
static const char *mirc_to_ansi[] = {
"\033[97m", /* 0: white */
"\033[30m", /* 1: black */
"\033[34m", /* 2: blue */
"\033[32m", /* 3: green */
"\033[31m", /* 4: red */
"\033[33m", /* 5: brown */
"\033[35m", /* 6: purple */
"\033[33m", /* 7: orange */
"\033[93m", /* 8: yellow */
"\033[92m", /* 9: light green */
"\033[36m", /* 10: cyan */
"\033[96m", /* 11: light cyan */
"\033[94m", /* 12: light blue */
"\033[95m", /* 13: pink */
"\033[90m", /* 14: grey */
"\033[37m", /* 15: light grey */
};
static size_t irc_color_process(char *out, size_t outsize, const char *in)
{
size_t o = 0;
for (const char *p = in; *p && o < outsize - 10; p++) {
if (*p == '\x03') {
/* mIRC colour: ^C[fg[,bg]] */
p++;
if (irc_colors && *p >= '0' && *p <= '9') {
int fg = *p - '0';
p++;
if (*p >= '0' && *p <= '9') { fg = fg * 10 + (*p - '0'); p++; }
if (*p == ',') {
p++;
if (*p >= '0' && *p <= '9') p++;
if (*p >= '0' && *p <= '9') p++;
}
if (fg < 16) {
const char *a = mirc_to_ansi[fg];
size_t alen = strlen(a);
if (o + alen < outsize) { memcpy(out + o, a, alen); o += alen; }
}
} else if (!irc_colors) {
/* Strip: skip digits and comma */
if (*p >= '0' && *p <= '9') p++;
if (*p >= '0' && *p <= '9') p++;
if (*p == ',') {
p++;
if (*p >= '0' && *p <= '9') p++;
if (*p >= '0' && *p <= '9') p++;
}
}
p--; /* loop will p++ */
} else if (*p == '\x02') {
/* Bold */
if (irc_colors) {
const char *a = "\033[1m";
memcpy(out + o, a, 4); o += 4;
}
} else if (*p == '\x1F') {
/* Underline */
if (irc_colors) {
const char *a = "\033[4m";
memcpy(out + o, a, 4); o += 4;
}
} else if (*p == '\x0F') {
/* Reset */
if (irc_colors) {
const char *a = "\033[0m";
memcpy(out + o, a, 4); o += 4;
}
} else if (*p == '\x1D') {
/* Italic */
if (irc_colors) {
const char *a = "\033[3m";
memcpy(out + o, a, 4); o += 4;
}
} else {
out[o++] = *p;
}
}
/* Reset at end if colours were used */
if (irc_colors && o > 0) {
const char *a = "\033[0m";
size_t alen = strlen(a);
if (o + alen < outsize) { memcpy(out + o, a, alen); o += alen; }
}
out[o] = '\0';
return o;
}
static void buf_store(int level, const char *line){
if (level < 0 || level >= WL_MAX) return;
char processed[512];
irc_color_process(processed, sizeof(processed), line);
snprintf(win_buf[level].lines[win_buf[level].head], 512, "%s", processed);
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;
/* Clear scroll region */
for (int i = 1; i <= visible; i++)
printf("\033[%d;1H\033[K", i);
/* Figure out how many lines fit, accounting for wrapping */
int end_pos = count - scroll_offset;
int rows_used = 0;
int start_pos = end_pos;
while (start_pos > 0 && rows_used < visible) {
int li = start_pos - 1;
int buf_idx;
if (count < SCROLLBACK)
buf_idx = li;
else
buf_idx = (win_buf[current_level].head - count + li + SCROLLBACK) % SCROLLBACK;
int line_cols = (int)display_cols(win_buf[current_level].lines[buf_idx],
strlen(win_buf[current_level].lines[buf_idx]));
int line_rows = line_cols / term_cols + 1;
if (rows_used + line_rows > visible) break;
rows_used += line_rows;
start_pos--;
}
int show = end_pos - start_pos;
/* Print lines from top, letting terminal wrap naturally */
int row = visible - rows_used + 1;
printf("\033[%d;1H", row);
for (int i = 0; i < show; i++) {
int li = start_pos + i;
int buf_idx;
if (count < SCROLLBACK)
buf_idx = li;
else
buf_idx = (win_buf[current_level].head - count + li + SCROLLBACK) % SCROLLBACK;
printf("\033[K%s\n", win_buf[current_level].lines[buf_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");
/* Enable TCP keepalive to detect dead connections */
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one));
int idle = 60;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
int intvl = 15;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
int cnt = 4;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
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 int nicklist_remove(int idx, const char *n)
{
if (idx < 0 || idx >= MAX_CHAN_WINS) return 0;
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 1;
}
}
return 0;
}
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)
{
(void)len;
if (!logfp) return;
time_t now = time(NULL);
struct tm *tm = localtime(&now);
fprintf(logfp, "[%02d:%02d:%02d] %s\n",
tm->tm_hour, tm->tm_min, tm->tm_sec, line);
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"
" :: %s\x01",
sender, ut.sysname,
ut.release, ut.machine, rent_msg);
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);
pm_nick_add(sender);
char b64dec[1024];
if (try_base64_decode(text, b64dec, sizeof(b64dec))) {
if (needs_translation(b64dec))
translate_async(b64dec, WL_MSG, NULL);
else
wprintf(WL_MSG, " \033[3m[b64: %s]\033[0m\n", b64dec);
} else if (needs_translation(text))
translate_async(text, WL_MSG, NULL);
} else {
int lvl = chan_to_level(target);
wprintf(lvl, "<%s> %s\n", sender, text);
char b64dec2[1024];
if (try_base64_decode(text, b64dec2, sizeof(b64dec2))) {
if (needs_translation(b64dec2))
translate_async(b64dec2, lvl, translate_public ? target : NULL);
else
wprintf(lvl, " \033[3m[b64: %s]\033[0m\n", b64dec2);
} else if (needs_translation(text))
translate_async(text, lvl, translate_public ? target : NULL);
}
}
} 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 only in channels where the user was present */
for (int i = 0; i < MAX_CHAN_WINS; i++) {
if (win_chans[i].name[0]) {
if (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 only in channels where user is present */
for (int i = 0; i < MAX_CHAN_WINS; i++) {
if (win_chans[i].name[0]) {
int found = 0;
for (int j = 0; j < win_chans[i].nick_count; j++) {
if (strcasecmp(win_chans[i].nicks[j], sender) == 0) {
if (newnick)
snprintf(win_chans[i].nicks[j], NICK_LEN, "%s", newnick);
found = 1;
break;
}
}
if (found)
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, "311") == 0 && params) {
/* RPL_WHOISUSER: <me> <nick> <user> <host> * :<realname> */
char *p = params;
char *sp = strchr(p, ' '); if (sp) p = sp + 1; /* skip our nick */
char *wnick = p;
sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; }
char *wuser = p;
sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; }
char *whost = p;
sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; }
/* skip the * */
sp = strchr(p, ':'); char *real = sp ? sp + 1 : p;
wprintf(WL_STATUS, "*** %s is %s@%s (%s)\n", wnick, wuser, whost, real);
} else if (strcmp(cmd, "319") == 0 && params) {
/* RPL_WHOISCHANNELS: <me> <nick> :<channels> */
char *chans = strchr(params, ':');
if (chans) chans++;
char *p = params;
char *sp = strchr(p, ' '); if (sp) p = sp + 1;
char *wnick = p;
sp = strchr(p, ' '); if (sp) *sp = '\0';
wprintf(WL_STATUS, "*** %s on channels: %s\n", wnick, chans ? chans : "");
} else if (strcmp(cmd, "312") == 0 && params) {
/* RPL_WHOISSERVER: <me> <nick> <server> :<info> */
char *info = strchr(params, ':');
if (info) info++;
char *p = params;
char *sp = strchr(p, ' '); if (sp) p = sp + 1;
char *wnick = p;
sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; }
char *server = p;
sp = strchr(p, ' '); if (sp) *sp = '\0';
wprintf(WL_STATUS, "*** %s on irc via server %s (%s)\n",
wnick, server, info ? info : "");
} else if (strcmp(cmd, "317") == 0 && params) {
/* RPL_WHOISIDLE: <me> <nick> <idle> <signon> :<text> */
char *p = params;
char *sp = strchr(p, ' '); if (sp) p = sp + 1;
char *wnick = p;
sp = strchr(p, ' '); if (sp) { *sp = '\0'; p = sp + 1; }
int idle_secs = atoi(p);
int mins = idle_secs / 60;
int secs = idle_secs % 60;
if (mins > 0)
wprintf(WL_STATUS, "*** %s has been idle %d min %d sec\n",
wnick, mins, secs);
else
wprintf(WL_STATUS, "*** %s has been idle %d sec\n", wnick, secs);
} else if (strcmp(cmd, "320") == 0 && params) {
/* RPL_WHOISSPECIAL: <me> <nick> :<text> */
char *p = params;
char *sp = strchr(p, ' '); if (sp) p = sp + 1;
char *wnick = p;
sp = strchr(p, ' '); if (sp) *sp = '\0';
char *text = strchr(params, ':');
if (text) text++;
wprintf(WL_STATUS, "*** %s %s\n", wnick, text ? text : "");
} else if (strcmp(cmd, "313") == 0 && params) {
/* RPL_WHOISOPERATOR: <me> <nick> :<text> */
char *text = strchr(params, ':');
if (text) text++;
char *p = params;
char *sp = strchr(p, ' '); if (sp) p = sp + 1;
char *wnick = p;
sp = strchr(p, ' '); if (sp) *sp = '\0';
wprintf(WL_STATUS, "*** %s %s\n", wnick, text ? text : "is an IRC Operator");
} else if (strcmp(cmd, "318") == 0 && params) {
/* RPL_ENDOFWHOIS — suppress or show subtle */
wprintf(WL_STATUS, "*** End of WHOIS\n");
} 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, "404") == 0 || strcmp(cmd, "482") == 0 ||
strcmp(cmd, "473") == 0 || strcmp(cmd, "474") == 0 ||
strcmp(cmd, "475") == 0) && params) {
/* Channel error numerics: <nick> <channel> :<message> */
char *p = params;
char *sp = strchr(p, ' ');
if (sp) {
p = sp + 1;
char *chan = p;
char *text = strchr(p, ':');
if (text) {
sp = strchr(chan, ' ');
if (sp) *sp = '\0';
text++;
int idx = chan_win_idx(chan);
int lvl = idx >= 0 ? WL_CHAN + idx : WL_STATUS;
wprintf(lvl, "* %s: %s\n", chan, text);
}
}
} 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);
irc_send_raw("MODE %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 || strcasecmp(cmd, "w") == 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);
/* Ensure target is in PM list (but don't reorder) */
int found = 0;
for (int i = 0; i < pm_nick_count; i++)
if (strcasecmp(pm_nicks[i], target) == 0) { found = 1; break; }
if (!found && pm_nick_count < MAX_PM_NICKS)
snprintf(pm_nicks[pm_nick_count++], NICK_LEN, "%s", target);
}
} 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);
draw_statusbar();
} else {
if (query_target[0])
wprintf(WL_MSG, "* No longer talking to %s\n", query_target);
query_target[0] = '\0';
draw_statusbar();
}
} 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, "trans") == 0) {
if (!translate_enabled) {
translate_enabled = 1;
translate_public = 0;
wprintf(current_level, "* Translation ON (local only)\n");
} else if (!translate_public) {
translate_public = 1;
wprintf(current_level, "* Translation ON (public echo)\n");
} else {
translate_enabled = 0;
translate_public = 0;
wprintf(current_level, "* Translation OFF\n");
}
config_save();
} else if (strcasecmp(cmd, "colors") == 0) {
irc_colors = !irc_colors;
wprintf(current_level, "* IRC colours %s\n",
irc_colors ? "ON" : "OFF (stripped)");
config_save();
} else if (strcasecmp(cmd, "ctcp") == 0 && args) {
char *target = args;
char *ctcp_cmd = strchr(args, ' ');
if (ctcp_cmd) {
*ctcp_cmd++ = '\0';
/* Uppercase the CTCP command */
for (char *p = ctcp_cmd; *p && *p != ' '; p++)
*p = (*p >= 'a' && *p <= 'z') ? *p - 32 : *p;
irc_send_raw("PRIVMSG %s :\x01%s\x01", target, ctcp_cmd);
wprintf(WL_STATUS, "* CTCP %s sent to %s\n", ctcp_cmd, target);
} else {
wprintf(current_level, "Usage: /ctcp <nick> <command>\n");
}
} else if (strcasecmp(cmd, "mode") == 0 && args) {
/* Replace * with current channel */
if (args[0] == '*') {
const char *chan = current_channel();
if (chan[0]) {
char modebuf[IRC_MAX];
snprintf(modebuf, sizeof(modebuf), "%s%s", chan, args + 1);
irc_send_raw("MODE %s", modebuf);
}
} else {
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 if (strcasecmp(cmd, "clear") == 0) {
win_buf[current_level].count = 0;
win_buf[current_level].head = 0;
scroll_offset = 0;
redraw_window();
} 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 (skips ANSI escapes) */
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];
/* Skip ANSI escape sequences */
if (c == 0x1B && i + 1 < bytes && buf[i+1] == '[') {
i += 2;
while (i < bytes && !((unsigned char)buf[i] >= 0x40 &&
(unsigned char)buf[i] <= 0x7E))
i++;
if (i < bytes) i++; /* skip final byte */
continue;
}
if (c < 0x80) { i++; cols++; }
else if ((c & 0xE0) == 0xC0) {
/* 2-byte: U+0080..U+07FF — all single width */
i += 2; cols++;
} else if ((c & 0xF0) == 0xE0) {
/* 3-byte: U+0800..U+FFFF — CJK ranges are double width */
unsigned int cp = ((c & 0x0F) << 12);
if (i + 1 < bytes) cp |= ((unsigned char)buf[i+1] & 0x3F) << 6;
if (i + 2 < bytes) cp |= ((unsigned char)buf[i+2] & 0x3F);
i += 3;
/* Wide: CJK Unified, Katakana, Hiragana, Hangul, fullwidth, etc. */
if ((cp >= 0x1100 && cp <= 0x115F) ||
(cp >= 0x2E80 && cp <= 0xA4CF && cp != 0x303F) ||
(cp >= 0xAC00 && cp <= 0xD7A3) ||
(cp >= 0xF900 && cp <= 0xFAFF) ||
(cp >= 0xFE10 && cp <= 0xFE6F) ||
(cp >= 0xFF01 && cp <= 0xFF60) ||
(cp >= 0xFFE0 && cp <= 0xFFE6))
cols += 2;
else
cols++;
} else if ((c & 0xF8) == 0xF0) {
/* 4-byte: U+10000..U+10FFFF — most are double width (emoji etc) */
i += 4; cols += 2;
} 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));
ai_config_load();
logfp = log_enabled ? fopen("irc.log", "a") : NULL;
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);
last_recv = time(NULL);
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 esc_bracket = 0; /* got ESC [ */
/* Command history */
#define HIST_SIZE 50
char history[HIST_SIZE][BUF_SIZE];
int hist_count = 0;
int hist_pos = 0; /* current browse position */
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 */
size_t tab_end = 0; /* byte position where last completion ends */
for (;;) {
FD_ZERO(&fds);
FD_SET(sock_fd, &fds);
FD_SET(STDIN_FILENO, &fds);
int maxfd = sock_fd > STDIN_FILENO ? sock_fd : STDIN_FILENO;
/* Add translation pipes to select */
for (int ti = 0; ti < translate_count; ti++) {
FD_SET(translate_pending[ti].fd, &fds);
if (translate_pending[ti].fd > maxfd)
maxfd = translate_pending[ti].fd;
}
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500000;
int ret = select(maxfd + 1, &fds, NULL, NULL, &tv);
if (ret < 0) {
if (errno == EINTR) continue;
die("select");
}
/* Redraw status bar every minute for clock update */
{
static int last_min = -1;
time_t now = time(NULL);
int cur_min = localtime(&now)->tm_min;
if (cur_min != last_min) {
last_min = cur_min;
draw_statusbar();
redraw_input(input_line, input_len, input_pos);
}
}
/* Detect dead connection: no data for 240s → send PING,
no reply after 60 more seconds → disconnect */
if (last_recv > 0) {
time_t elapsed = time(NULL) - last_recv;
if (!ping_sent && elapsed >= 240) {
irc_send_raw("PING :keepalive");
ping_sent = 1;
} else if (ping_sent && elapsed >= 300) {
wprintf(WL_STATUS,
"* No response from server (timeout)\n");
break;
}
}
/* Check translation results */
for (int ti = 0; ti < translate_count; ) {
if (FD_ISSET(translate_pending[ti].fd, &fds)) {
char tbuf[1024];
int n = read(translate_pending[ti].fd, tbuf, sizeof(tbuf) - 1);
close(translate_pending[ti].fd);
waitpid(translate_pending[ti].pid, NULL, 0);
if (n > 0) {
tbuf[n] = '\0';
/* Strip trailing whitespace */
while (n > 0 && (tbuf[n-1] == '\n' || tbuf[n-1] == '\r' || tbuf[n-1] == ' '))
tbuf[--n] = '\0';
if (n == 0) goto skip_translate;
/* Parse "english|||swedish" format */
char *sep = strstr(tbuf, "|||");
char *english = tbuf;
char *skip_part = NULL;
if (sep) {
*sep = '\0';
skip_part = sep + 3;
/* Strip leading space */
while (*skip_part == ' ') skip_part++;
}
/* Compare skip_part to original — if similar, suppress */
int suppress = 0;
if (skip_part && translate_pending[ti].original[0]) {
/* Simple word overlap: count matching words */
char orig_lower[512], skip_lower[512];
snprintf(orig_lower, sizeof(orig_lower), "%s",
translate_pending[ti].original);
snprintf(skip_lower, sizeof(skip_lower), "%s", skip_part);
for (char *p = orig_lower; *p; p++)
if (*p >= 'A' && *p <= 'Z') *p += 32;
for (char *p = skip_lower; *p; p++)
if (*p >= 'A' && *p <= 'Z') *p += 32;
/* Count words in original that appear in skip translation */
int total = 0, matches = 0;
char *tok = strtok(orig_lower, " ,.!?:;");
while (tok) {
if (strlen(tok) > 2) {
total++;
if (strstr(skip_lower, tok)) matches++;
}
tok = strtok(NULL, " ,.!?:;");
}
if (total > 0 && matches * 100 / total >= 50)
suppress = 1;
}
/* Also check old SKIP response */
if (strcasecmp(english, "SKIP") == 0 ||
strncasecmp(english, "SKIP", 4) == 0 ||
strncasecmp(english, "I cannot", 8) == 0 ||
strncasecmp(english, "I can't", 7) == 0)
suppress = 1;
/* Suppress if English translation matches original */
if (!suppress && translate_pending[ti].original[0]) {
char orig_l[512], eng_l[1024];
snprintf(orig_l, sizeof(orig_l), "%s",
translate_pending[ti].original);
snprintf(eng_l, sizeof(eng_l), "%s", english);
for (char *p = orig_l; *p; p++)
if (*p >= 'A' && *p <= 'Z') *p += 32;
for (char *p = eng_l; *p; p++)
if (*p >= 'A' && *p <= 'Z') *p += 32;
int total = 0, matches = 0;
char orig_copy[512];
snprintf(orig_copy, sizeof(orig_copy), "%s", orig_l);
char *tok = strtok(orig_copy, " ,.!?:;");
while (tok) {
if (strlen(tok) > 2) {
total++;
if (strstr(eng_l, tok)) matches++;
}
tok = strtok(NULL, " ,.!?:;");
}
if (total > 0 && matches * 100 / total >= 50)
suppress = 1;
}
if (!suppress && english[0]) {
/* Strip trailing whitespace from english */
size_t elen = strlen(english);
while (elen > 0 && (english[elen-1] == ' ' ||
english[elen-1] == '\n'))
english[--elen] = '\0';
wprintf(translate_pending[ti].level,
" \033[3m%s\033[0m\n", english);
if (translate_pending[ti].target[0]) {
char pfx[IRC_MAX];
snprintf(pfx, sizeof(pfx), "PRIVMSG %s :",
translate_pending[ti].target);
irc_send_converted(pfx,
(unsigned char *)english, strlen(english));
wprintf(translate_pending[ti].level,
"<%s> %s\n", nick, english);
}
}
}
skip_translate:
translate_count--;
if (ti < translate_count)
translate_pending[ti] = translate_pending[translate_count];
} else {
ti++;
}
}
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';
last_recv = time(NULL);
ping_sent = 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 == '[') {
esc_bracket = 1;
continue;
}
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;
}
}
if (esc_bracket) {
esc_bracket = 0;
if (ch == 'A') {
/* Arrow up: previous history */
if (hist_pos > 0) {
hist_pos--;
input_len = strlen(history[hist_pos]);
memcpy(input_line, history[hist_pos], input_len);
input_pos = input_len;
redraw_input(input_line, input_len, input_pos);
}
} else if (ch == 'B') {
/* Arrow down: next history */
if (hist_pos < hist_count - 1) {
hist_pos++;
input_len = strlen(history[hist_pos]);
memcpy(input_line, history[hist_pos], input_len);
input_pos = input_len;
redraw_input(input_line, input_len, input_pos);
} else {
hist_pos = hist_count;
input_len = 0;
input_pos = 0;
redraw_input(input_line, input_len, input_pos);
}
} else if (ch == 'C') {
/* Arrow right */
if (input_pos < input_len) {
unsigned char c = input_line[input_pos];
size_t clen = 1;
if ((c & 0xE0) == 0xC0) clen = 2;
else if ((c & 0xF0) == 0xE0) clen = 3;
else if ((c & 0xF8) == 0xF0) clen = 4;
input_pos += clen;
if (input_pos > input_len) input_pos = input_len;
redraw_input(input_line, input_len, input_pos);
}
} else if (ch == 'D') {
/* Arrow left */
if (input_pos > 0) {
input_pos -= utf8_back(input_line, input_pos);
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) {
/* Save to history */
if (hist_count < HIST_SIZE) {
memcpy(history[hist_count], input_line, input_len + 1);
hist_count++;
} else {
memmove(history[0], history[1], (HIST_SIZE-1) * BUF_SIZE);
memcpy(history[HIST_SIZE-1], input_line, input_len + 1);
}
hist_pos = hist_count;
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 == 0x0C) {
/* Ctrl-L: redraw screen */
get_term_size();
printf("\033[2J\033[H");
printf("\033[1;%dr", term_rows - 2);
redraw_window();
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 */
char (*nlist)[NICK_LEN] = NULL;
int ncount = 0;
/* Check if completing after /msg or /w */
int after_msg = 0;
input_line[input_len] = '\0';
if (strncasecmp(input_line, "/msg ", 5) == 0 ||
strncasecmp(input_line, "/w ", 3) == 0) {
after_msg = 1;
}
/* On window 1 with empty input, auto-insert /msg <nick> */
if (current_level == WL_STATUS && input_len == 0 && pm_nick_count > 0) {
after_msg = 1;
memcpy(input_line, "/msg ", 5);
input_len = 5;
input_pos = 5;
tab_start = 5;
tab_prefix_len = 0;
tab_end = 5;
tab_idx = 0;
}
if (after_msg) {
nlist = pm_nicks;
ncount = pm_nick_count;
} else if (current_level >= WL_CHAN) {
int cidx = current_level - WL_CHAN;
nlist = win_chans[cidx].nicks;
ncount = win_chans[cidx].nick_count;
} else {
nlist = pm_nicks;
ncount = pm_nick_count;
}
if (ncount > 0) {
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_end = input_pos;
tab_idx = 0;
} else {
input_pos = tab_end;
tab_idx++;
}
int found = 0;
for (int tries = 0; tries < ncount; tries++) {
int ni = (tab_idx + tries) % ncount;
if (tab_prefix_len == 0 ||
strncasecmp(nlist[ni],
input_line + tab_start,
tab_prefix_len) == 0) {
tab_idx = ni;
const char *compl = nlist[ni];
size_t clen = strlen(compl);
char suffix[4] = "";
if (tab_start == 0 && current_level >= WL_CHAN)
strcpy(suffix, ": ");
else
strcpy(suffix, " ");
size_t slen = strlen(suffix);
size_t tail = input_len - tab_end;
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 + tab_end, tail);
memcpy(input_line + tab_start, compl, clen);
memcpy(input_line + tab_start + clen, suffix, slen);
tab_end = tab_start + clen + slen;
input_pos = tab_end;
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;
}