f4324ef2b4
- 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
2288 lines
66 KiB
C
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;
|
|
}
|