f32d248d71
- All messages sent to LLM (not just non-Latin) - LLM detects language and returns SKIP for understood languages - /trans toggles echoing translations publicly to channel - PM nicks ordered most-recent-first for tab completion - tool_choice:none to fix vLLM/litellm 400 error - Updated README with /trans command
1804 lines
50 KiB
C
1804 lines
50 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 "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;
|
|
|
|
/* 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 */
|
|
|
|
/* 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 */
|
|
} 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");
|
|
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);
|
|
}
|
|
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 int needs_translation(const char *text)
|
|
{
|
|
if (!ai_cfg.enabled) return 0;
|
|
/* Skip very short messages (nicks, URLs, single words like "ok") */
|
|
int words = 0;
|
|
for (const char *p = text; *p; p++)
|
|
if (*p == ' ') words++;
|
|
if (words < 1 && strlen(text) < 6) 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\":\"You are a translator for an IRC chat. The user understands %s. If the message is in any of those languages or a mix of them, respond with exactly SKIP. Only translate messages in other languages to %s. Respond with only the translation or SKIP.\"},"
|
|
"{\"role\":\"user\",\"content\":\"%s\"}"
|
|
"],\"stream\":false,\"tool_choice\":\"none\"}",
|
|
ai_cfg.model, ai_cfg.skip_langs, ai_cfg.target_lang, 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 : "");
|
|
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);
|
|
|
|
/* Status bar on second-to-last row */
|
|
printf("\033[%d;1H\033[7m%-*.*s\033[0m",
|
|
term_rows - 1, term_cols, term_cols, bar);
|
|
/* Move cursor to input line */
|
|
printf("\033[%d;1H", term_rows);
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* Store a line in a window's scrollback */
|
|
static void buf_store(int level, const char *line){
|
|
if (level < 0 || level >= WL_MAX) return;
|
|
snprintf(win_buf[level].lines[win_buf[level].head], 512, "%s", line);
|
|
win_buf[level].head = (win_buf[level].head + 1) % SCROLLBACK;
|
|
if (win_buf[level].count < SCROLLBACK)
|
|
win_buf[level].count++;
|
|
}
|
|
|
|
/* Redraw the current window's scrollback */
|
|
static void redraw_window(void)
|
|
{
|
|
get_term_size();
|
|
printf("\033[1;%dr", term_rows - 2); /* ensure scroll region is set */
|
|
int visible = term_rows - 2;
|
|
int count = win_buf[current_level].count;
|
|
|
|
/* Clamp scroll offset */
|
|
int max_scroll = count - visible;
|
|
if (max_scroll < 0) max_scroll = 0;
|
|
if (scroll_offset > max_scroll) scroll_offset = max_scroll;
|
|
|
|
/* 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");
|
|
|
|
return fd;
|
|
}
|
|
|
|
/* Find window index for a channel, or -1 */
|
|
/* Nick list management */
|
|
static void nicklist_add(int idx, const char *n)
|
|
{
|
|
if (idx < 0 || idx >= MAX_CHAN_WINS) return;
|
|
/* Skip prefix chars */
|
|
if (*n == '@' || *n == '+' || *n == '%') n++;
|
|
if (!*n) return;
|
|
/* Check if already present */
|
|
for (int i = 0; i < win_chans[idx].nick_count; i++)
|
|
if (strcasecmp(win_chans[idx].nicks[i], n) == 0) return;
|
|
if (win_chans[idx].nick_count < MAX_NICKS)
|
|
snprintf(win_chans[idx].nicks[win_chans[idx].nick_count++],
|
|
NICK_LEN, "%s", n);
|
|
}
|
|
|
|
static void nicklist_remove(int idx, const char *n)
|
|
{
|
|
if (idx < 0 || idx >= MAX_CHAN_WINS) return;
|
|
for (int i = 0; i < win_chans[idx].nick_count; i++) {
|
|
if (strcasecmp(win_chans[idx].nicks[i], n) == 0) {
|
|
win_chans[idx].nick_count--;
|
|
if (i < win_chans[idx].nick_count)
|
|
memcpy(win_chans[idx].nicks[i],
|
|
win_chans[idx].nicks[win_chans[idx].nick_count],
|
|
NICK_LEN);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void nicklist_rename(const char *old, const char *new_nick)
|
|
{
|
|
for (int i = 0; i < MAX_CHAN_WINS; i++) {
|
|
for (int j = 0; j < win_chans[i].nick_count; j++) {
|
|
if (strcasecmp(win_chans[i].nicks[j], old) == 0) {
|
|
snprintf(win_chans[i].nicks[j], NICK_LEN, "%s", new_nick);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int chan_win_idx(const char *chan)
|
|
{
|
|
int i;
|
|
for (i = 0; i < MAX_CHAN_WINS; i++) {
|
|
if (strcasecmp(win_chans[i].name, chan) == 0)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Get level for a channel (find existing or assign to current window) */
|
|
static int chan_to_level(const char *chan)
|
|
{
|
|
int idx = chan_win_idx(chan);
|
|
if (idx >= 0)
|
|
return WL_CHAN + idx;
|
|
|
|
/* Assign to current window if it's a channel window and empty */
|
|
if (current_level >= WL_CHAN) {
|
|
idx = current_level - WL_CHAN;
|
|
if (win_chans[idx].name[0] == '\0') {
|
|
snprintf(win_chans[idx].name,
|
|
sizeof(win_chans[idx].name), "%s", chan);
|
|
return current_level;
|
|
}
|
|
}
|
|
|
|
/* Find first empty slot */
|
|
int i;
|
|
for (i = 0; i < MAX_CHAN_WINS; i++) {
|
|
if (win_chans[i].name[0] == '\0') {
|
|
snprintf(win_chans[i].name,
|
|
sizeof(win_chans[i].name), "%s", chan);
|
|
return WL_CHAN + i;
|
|
}
|
|
}
|
|
return WL_CHAN;
|
|
}
|
|
|
|
/* Update our prefix for a channel based on MODE changes */
|
|
static void update_my_prefix(const char *chan, const char *modestr,
|
|
const char *args)
|
|
{
|
|
int idx = chan_win_idx(chan);
|
|
if (idx < 0) return;
|
|
|
|
int adding = 1;
|
|
const char *m = modestr;
|
|
/* Simple parse: walk mode chars, consume args for o/v */
|
|
char arg_buf[512];
|
|
snprintf(arg_buf, sizeof(arg_buf), "%s", args ? args : "");
|
|
char *arg = arg_buf;
|
|
|
|
while (*m) {
|
|
if (*m == '+') { adding = 1; m++; continue; }
|
|
if (*m == '-') { adding = 0; m++; continue; }
|
|
|
|
/* Modes that take a parameter */
|
|
char *this_arg = NULL;
|
|
if (*m == 'o' || *m == 'v' || *m == 'b' ||
|
|
*m == 'k' || *m == 'l') {
|
|
this_arg = arg;
|
|
char *sp = strchr(arg, ' ');
|
|
if (sp) { *sp = '\0'; arg = sp + 1; }
|
|
else arg = arg + strlen(arg);
|
|
}
|
|
|
|
if ((*m == 'o' || *m == 'v') && this_arg &&
|
|
strcasecmp(this_arg, nick) == 0) {
|
|
if (*m == 'o')
|
|
win_chans[idx].my_prefix = adding ? '@' : '\0';
|
|
else if (*m == 'v' && win_chans[idx].my_prefix != '@')
|
|
win_chans[idx].my_prefix = adding ? '+' : '\0';
|
|
}
|
|
|
|
/* Track channel modes (non-user modes) */
|
|
if (*m != 'o' && *m != 'v' && *m != 'b') {
|
|
char *cm = win_chans[idx].modes;
|
|
size_t clen = strlen(cm);
|
|
if (adding) {
|
|
/* Add if not present */
|
|
if (!strchr(cm, *m) &&
|
|
clen + 1 < sizeof(win_chans[idx].modes)) {
|
|
if (clen == 0) {
|
|
cm[0] = '+';
|
|
cm[1] = *m;
|
|
cm[2] = '\0';
|
|
} else {
|
|
cm[clen] = *m;
|
|
cm[clen+1] = '\0';
|
|
}
|
|
}
|
|
} else {
|
|
/* Remove */
|
|
char *p = strchr(cm, *m);
|
|
if (p) memmove(p, p + 1, strlen(p));
|
|
/* Remove '+' if empty */
|
|
if (strcmp(cm, "+") == 0) cm[0] = '\0';
|
|
}
|
|
}
|
|
m++;
|
|
}
|
|
}
|
|
|
|
static FILE *logfp = NULL;
|
|
|
|
static void log_raw(const char *line, size_t len)
|
|
{
|
|
if (!logfp) return;
|
|
time_t now = time(NULL);
|
|
struct tm *tm = localtime(&now);
|
|
fprintf(logfp, "[%02d:%02d:%02d] len=%zu charset=",
|
|
tm->tm_hour, tm->tm_min, tm->tm_sec, len);
|
|
|
|
/* Detect charset */
|
|
const unsigned char *u = (const unsigned char *)line;
|
|
int has_high = 0, valid_utf8 = 1;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (u[i] >= 0x80) {
|
|
has_high = 1;
|
|
if ((u[i] & 0xE0) == 0xC0) {
|
|
if (i+1 >= len || (u[i+1] & 0xC0) != 0x80)
|
|
valid_utf8 = 0;
|
|
else i += 1;
|
|
} else if ((u[i] & 0xF0) == 0xE0) {
|
|
if (i+2 >= len || (u[i+1] & 0xC0) != 0x80 ||
|
|
(u[i+2] & 0xC0) != 0x80)
|
|
valid_utf8 = 0;
|
|
else i += 2;
|
|
} else if ((u[i] & 0xF8) == 0xF0) {
|
|
if (i+3 >= len || (u[i+1] & 0xC0) != 0x80 ||
|
|
(u[i+2] & 0xC0) != 0x80 || (u[i+3] & 0xC0) != 0x80)
|
|
valid_utf8 = 0;
|
|
else i += 3;
|
|
} else {
|
|
valid_utf8 = 0;
|
|
}
|
|
}
|
|
}
|
|
if (!has_high)
|
|
fprintf(logfp, "ASCII");
|
|
else if (valid_utf8)
|
|
fprintf(logfp, "UTF-8");
|
|
else
|
|
fprintf(logfp, "ISO-8859-1");
|
|
|
|
fprintf(logfp, "\n text: %s\n hex: ", line);
|
|
for (size_t i = 0; i < len; i++)
|
|
fprintf(logfp, "%02X ", u[i]);
|
|
fprintf(logfp, "\n");
|
|
fflush(logfp);
|
|
}
|
|
|
|
static void handle_line(char *line)
|
|
{
|
|
size_t rawlen = strlen(line);
|
|
log_raw(line, rawlen);
|
|
|
|
/* Incoming text is displayed as-is (terminal is UTF-8).
|
|
* No conversion needed for display. */
|
|
char *converted = line;
|
|
|
|
if (strncmp(converted, "PING ", 5) == 0) {
|
|
irc_send_raw("PONG %s", converted + 5);
|
|
return;
|
|
}
|
|
|
|
char *prefix = NULL;
|
|
char *cmd = converted;
|
|
|
|
if (cmd[0] == ':') {
|
|
prefix = cmd + 1;
|
|
cmd = strchr(cmd, ' ');
|
|
if (!cmd) return;
|
|
*cmd++ = '\0';
|
|
}
|
|
|
|
while (*cmd == ' ') cmd++;
|
|
|
|
char sender[64] = "";
|
|
if (prefix) {
|
|
char *bang = strchr(prefix, '!');
|
|
if (bang) {
|
|
size_t nlen = (size_t)(bang - prefix);
|
|
if (nlen >= sizeof(sender)) nlen = sizeof(sender) - 1;
|
|
memcpy(sender, prefix, nlen);
|
|
sender[nlen] = '\0';
|
|
} else {
|
|
snprintf(sender, sizeof(sender), "%.63s", prefix);
|
|
}
|
|
}
|
|
|
|
char *params = strchr(cmd, ' ');
|
|
if (params) *params++ = '\0';
|
|
|
|
if (strcmp(cmd, "PRIVMSG") == 0 && params) {
|
|
char *target = params;
|
|
char *text = strchr(params, ':');
|
|
if (text) {
|
|
char *sp = strchr(target, ' ');
|
|
if (sp) *sp = '\0';
|
|
text++;
|
|
if (text[0] == '\x01') {
|
|
if (strncmp(text, "\x01VERSION\x01", 9) == 0) {
|
|
struct utsname ut;
|
|
uname(&ut);
|
|
irc_send_raw("NOTICE %s :\x01VERSION "
|
|
"Holck's Mirk, OS: %s %s %s"
|
|
" :: This space available for rent\x01",
|
|
sender, ut.sysname,
|
|
ut.release, ut.machine);
|
|
wprintf(WL_STATUS, "* CTCP VERSION from %s\n", sender);
|
|
} else if (strncmp(text, "\x01" "ACTION ", 8) == 0) {
|
|
/* /me action */
|
|
char *action = text + 8;
|
|
char *end = strchr(action, '\x01');
|
|
if (end) *end = '\0';
|
|
if (strcasecmp(target, nick) == 0) {
|
|
wprintf(WL_MSG, "* %s %s\n", sender, action);
|
|
} else {
|
|
int lvl = chan_to_level(target);
|
|
wprintf(lvl, "* %s %s\n", sender, action);
|
|
}
|
|
} else {
|
|
wprintf(WL_STATUS, "* CTCP from %s: %s\n",
|
|
sender, text + 1);
|
|
}
|
|
} else if (strcasecmp(target, nick) == 0) {
|
|
wprintf(WL_MSG, "<%s> %s\n", sender, text);
|
|
pm_nick_add(sender);
|
|
if (needs_translation(text))
|
|
translate_async(text, WL_MSG, NULL);
|
|
} else {
|
|
int lvl = chan_to_level(target);
|
|
wprintf(lvl, "<%s> %s\n", sender, text);
|
|
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 in all active channel windows and remove nick */
|
|
for (int i = 0; i < MAX_CHAN_WINS; i++) {
|
|
if (win_chans[i].name[0]) {
|
|
nicklist_remove(i, sender);
|
|
wprintf(WL_CHAN + i, "\033[1m* %s has quit (%s)\033[0m\n",
|
|
sender, reason ? reason : "");
|
|
}
|
|
}
|
|
} else if (strcmp(cmd, "NICK") == 0) {
|
|
char *newnick = params;
|
|
if (newnick && newnick[0] == ':') newnick++;
|
|
/* Update nick lists and show in all active channel windows */
|
|
if (newnick) nicklist_rename(sender, newnick);
|
|
for (int i = 0; i < MAX_CHAN_WINS; i++) {
|
|
if (win_chans[i].name[0])
|
|
wprintf(WL_CHAN + i, "* %s is now known as %s\n",
|
|
sender, newnick ? newnick : "");
|
|
}
|
|
if (strcasecmp(sender, nick) == 0 && newnick) {
|
|
snprintf(nick, sizeof(nick), "%s", newnick);
|
|
draw_statusbar();
|
|
}
|
|
} else if (strcmp(cmd, "MODE") == 0 && params) {
|
|
/* MODE #chan +modes [args] */
|
|
char *target = params;
|
|
char *modestr = strchr(params, ' ');
|
|
if (modestr) {
|
|
*modestr++ = '\0';
|
|
char *modeargs = strchr(modestr, ' ');
|
|
if (modeargs) *modeargs++ = '\0';
|
|
|
|
if (target[0] == '#' || target[0] == '&') {
|
|
update_my_prefix(target, modestr, modeargs);
|
|
int lvl = chan_to_level(target);
|
|
wprintf(lvl, "\033[1m* %s sets mode %s %s\033[0m\n",
|
|
sender[0] ? sender : "*", modestr,
|
|
modeargs ? modeargs : "");
|
|
draw_statusbar();
|
|
} else {
|
|
wprintf(WL_STATUS, "\033[1m* %s sets mode %s\033[0m\n",
|
|
sender[0] ? sender : "*", modestr);
|
|
}
|
|
}
|
|
} else if (strcmp(cmd, "324") == 0 && params) {
|
|
/* RPL_CHANNELMODEIS: <nick> <channel> <modes> */
|
|
char *p = params;
|
|
/* skip our nick */
|
|
char *sp = strchr(p, ' ');
|
|
if (sp) {
|
|
p = sp + 1;
|
|
char *chan = p;
|
|
sp = strchr(p, ' ');
|
|
if (sp) {
|
|
*sp = '\0';
|
|
char *modes = sp + 1;
|
|
/* Strip trailing params */
|
|
sp = strchr(modes, ' ');
|
|
if (sp) *sp = '\0';
|
|
int idx = chan_win_idx(chan);
|
|
if (idx >= 0) {
|
|
snprintf(win_chans[idx].modes,
|
|
sizeof(win_chans[idx].modes),
|
|
"%s", modes);
|
|
draw_statusbar();
|
|
}
|
|
}
|
|
}
|
|
} else if (strcmp(cmd, "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 *p = params;
|
|
char *sp = strchr(p, ' '); if (sp) p = sp + 1;
|
|
char *wnick = p;
|
|
sp = strchr(p, ' '); if (sp) *sp = '\0';
|
|
char *chans = strchr(params, ':');
|
|
if (chans) chans++;
|
|
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 *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';
|
|
char *info = strchr(params, ':');
|
|
if (info) info++;
|
|
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, "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);
|
|
} else if (strcasecmp(cmd, "part") == 0) {
|
|
const char *chan = args ? args : current_channel();
|
|
if (chan[0])
|
|
irc_send_raw("PART %s", chan);
|
|
} else if (strcasecmp(cmd, "msg") == 0 && args) {
|
|
char *target = args;
|
|
char *text = strchr(args, ' ');
|
|
if (text) {
|
|
*text++ = '\0';
|
|
char pfx[IRC_MAX];
|
|
snprintf(pfx, sizeof(pfx),
|
|
"PRIVMSG %s :", target);
|
|
irc_send_converted(pfx,
|
|
(unsigned char *)text,
|
|
strlen(text));
|
|
wprintf(WL_MSG, "-> %s: %s\n", target, text);
|
|
}
|
|
} else if (strcasecmp(cmd, "q") == 0) {
|
|
if (args && args[0]) {
|
|
snprintf(query_target, sizeof(query_target), "%s", args);
|
|
wprintf(WL_MSG, "* Now talking to %s\n", query_target);
|
|
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) {
|
|
translate_public = !translate_public;
|
|
wprintf(current_level, "* Translation echo %s\n",
|
|
translate_public ? "ON" : "OFF");
|
|
} 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) {
|
|
irc_send_raw("MODE %s", args);
|
|
} else if (strcasecmp(cmd, "whois") == 0 && args) {
|
|
if (strchr(args, ' '))
|
|
irc_send_raw("WHOIS %s", args);
|
|
else
|
|
irc_send_raw("WHOIS %s", args);
|
|
} else if (strcasecmp(cmd, "wii") == 0 && args) {
|
|
irc_send_raw("WHOIS %s %s", args, args);
|
|
} else if (strcasecmp(cmd, "names") == 0) {
|
|
const char *chan = args ? args : current_channel();
|
|
if (chan[0])
|
|
irc_send_raw("NAMES %s", chan);
|
|
} else if (strcasecmp(cmd, "topic") == 0) {
|
|
if (args && strchr(args, ' ')) {
|
|
/* /topic #channel new topic */
|
|
char *chan = args;
|
|
char *text = strchr(args, ' ');
|
|
*text++ = '\0';
|
|
irc_send_raw("TOPIC %s :%s", chan, text);
|
|
} else {
|
|
/* /topic or /topic #channel — query topic */
|
|
const char *chan = args ? args : current_channel();
|
|
if (chan[0])
|
|
irc_send_raw("TOPIC %s", chan);
|
|
}
|
|
} else if (strcasecmp(cmd, "quit") == 0) {
|
|
irc_send_raw("QUIT :%s", args ? args : "See you later");
|
|
close(sock_fd);
|
|
exit(0);
|
|
} else if (strcasecmp(cmd, "raw") == 0 && args) {
|
|
irc_send_raw("%s", args);
|
|
} else {
|
|
wprintf(current_level, "Unknown command: /%s\n", cmd);
|
|
}
|
|
} else {
|
|
const char *chan = current_channel();
|
|
/* If on status/msg window with a query target, send there */
|
|
if (chan[0] == '\0' && query_target[0]) {
|
|
char pfx[IRC_MAX];
|
|
snprintf(pfx, sizeof(pfx), "PRIVMSG %s :", query_target);
|
|
irc_send_converted(pfx, (unsigned char *)line, len);
|
|
wprintf(WL_MSG, "-> %s: %s\n", query_target, line);
|
|
return;
|
|
}
|
|
if (chan[0] == '\0') {
|
|
wprintf(current_level,
|
|
"* Not in a channel on this window.\n");
|
|
return;
|
|
}
|
|
char pfx[IRC_MAX];
|
|
snprintf(pfx, sizeof(pfx), "PRIVMSG %s :", chan);
|
|
irc_send_converted(pfx, (unsigned char *)line, len);
|
|
wprintf(current_level, "<%s> %s\n", nick, line);
|
|
}
|
|
}
|
|
|
|
/* Count display columns for a UTF-8 byte string */
|
|
static size_t display_cols(const char *buf, size_t bytes)
|
|
{
|
|
size_t cols = 0;
|
|
size_t i = 0;
|
|
while (i < bytes) {
|
|
unsigned char c = (unsigned char)buf[i];
|
|
if (c < 0x80) { i++; cols++; }
|
|
else if ((c & 0xE0) == 0xC0) { i += 2; cols++; }
|
|
else if ((c & 0xF0) == 0xE0) { i += 3; cols++; }
|
|
else if ((c & 0xF8) == 0xF0) { i += 4; cols++; }
|
|
else { i++; cols++; }
|
|
}
|
|
return cols;
|
|
}
|
|
|
|
/* Return number of bytes in the UTF-8 char ending before pos */
|
|
static size_t utf8_back(const char *buf, size_t pos)
|
|
{
|
|
size_t n = 1;
|
|
while (n < pos && n < 4 &&
|
|
((unsigned char)buf[pos - n] & 0xC0) == 0x80)
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
static void redraw_input(const char *input_line, size_t input_len,
|
|
size_t input_pos)
|
|
{
|
|
int avail = term_cols - 2; /* columns available after "> " */
|
|
size_t total_cols = display_cols(input_line, input_len);
|
|
size_t cpos_cols = display_cols(input_line, input_pos);
|
|
|
|
printf("\033[%d;1H\033[K\033[32m>\033[0m ", term_rows);
|
|
|
|
if ((int)total_cols <= avail) {
|
|
/* Fits entirely */
|
|
fwrite(input_line, 1, input_len, stdout);
|
|
printf("\033[%d;%dH", term_rows, (int)(cpos_cols + 3));
|
|
} else {
|
|
/* Scroll horizontally: show a window around cursor */
|
|
size_t win_start_bytes = 0;
|
|
size_t win_start_cols = 0;
|
|
|
|
/* If cursor is past the visible area, shift window */
|
|
if ((int)cpos_cols >= avail) {
|
|
/* Move window so cursor is near the right edge */
|
|
size_t target_cols = cpos_cols - (size_t)(avail - 1);
|
|
size_t i = 0, cols = 0;
|
|
while (i < input_len && cols < target_cols) {
|
|
unsigned char c = (unsigned char)input_line[i];
|
|
if (c < 0x80) i++;
|
|
else if ((c & 0xE0) == 0xC0) i += 2;
|
|
else if ((c & 0xF0) == 0xE0) i += 3;
|
|
else if ((c & 0xF8) == 0xF0) i += 4;
|
|
else i++;
|
|
cols++;
|
|
}
|
|
win_start_bytes = i;
|
|
win_start_cols = cols;
|
|
}
|
|
|
|
/* Write characters that fit in avail columns */
|
|
size_t i = win_start_bytes;
|
|
int cols_written = 0;
|
|
while (i < input_len && cols_written < avail) {
|
|
unsigned char c = (unsigned char)input_line[i];
|
|
size_t clen = 1;
|
|
if (c >= 0x80) {
|
|
if ((c & 0xE0) == 0xC0) clen = 2;
|
|
else if ((c & 0xF0) == 0xE0) clen = 3;
|
|
else if ((c & 0xF8) == 0xF0) clen = 4;
|
|
}
|
|
fwrite(input_line + i, 1, clen, stdout);
|
|
i += clen;
|
|
cols_written++;
|
|
}
|
|
|
|
int cursor_col = (int)(cpos_cols - win_start_cols + 3);
|
|
printf("\033[%d;%dH", term_rows, cursor_col);
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void cleanup(void)
|
|
{
|
|
printf("\033[1;%dr", term_rows); /* reset scroll region */
|
|
printf("\033[%d;1H\033[K", term_rows - 1);
|
|
printf("\033[%d;1H\033[K\n", term_rows);
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &orig_term);
|
|
if (logfp) fclose(logfp);
|
|
}
|
|
|
|
static void usage(const char *prog)
|
|
{
|
|
fprintf(stderr, "Usage: %s <nick> <server> [port]\n", prog);
|
|
exit(1);
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
const char *host, *port = "6667";
|
|
|
|
if (argc < 3) usage(argv[0]);
|
|
snprintf(nick, sizeof(nick), "%s", argv[1]);
|
|
host = argv[2];
|
|
if (argc >= 4) port = argv[3];
|
|
|
|
memset(win_chans, 0, sizeof(win_chans));
|
|
|
|
ai_config_load();
|
|
|
|
logfp = fopen("irc.log", "a");
|
|
if (logfp)
|
|
fprintf(logfp, "--- Session started ---\n");
|
|
|
|
/* Set terminal to raw mode */
|
|
struct termios raw_term;
|
|
tcgetattr(STDIN_FILENO, &orig_term);
|
|
raw_term = orig_term;
|
|
raw_term.c_lflag &= ~(ICANON | ECHO);
|
|
raw_term.c_cc[VMIN] = 1;
|
|
raw_term.c_cc[VTIME] = 0;
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &raw_term);
|
|
|
|
atexit(cleanup);
|
|
|
|
signal(SIGINT, sigint_handler);
|
|
signal(SIGWINCH, sigwinch_handler);
|
|
|
|
/* Set up scrolling region (leave last 2 rows for status + input) */
|
|
get_term_size();
|
|
printf("\033[2J\033[H"); /* clear screen */
|
|
printf("\033[1;%dr", term_rows - 2);
|
|
printf("\033[1;1H"); /* cursor to top of scroll region */
|
|
draw_statusbar();
|
|
|
|
wprintf(WL_STATUS, "Connecting to %s:%s as %s...\n", host, port, nick);
|
|
|
|
sock_fd = irc_connect(host, port);
|
|
|
|
wprintf(WL_STATUS, "Connected.\n");
|
|
|
|
irc_send_raw("NICK %s", nick);
|
|
const char *user = getenv("USER");
|
|
if (!user) user = nick;
|
|
const char *realname = nick;
|
|
struct passwd *pw = getpwuid(getuid());
|
|
if (pw && pw->pw_gecos && pw->pw_gecos[0]) {
|
|
/* GECOS may have commas; use only first field */
|
|
static char gecos[128];
|
|
snprintf(gecos, sizeof(gecos), "%s", pw->pw_gecos);
|
|
char *comma = strchr(gecos, ',');
|
|
if (comma) *comma = '\0';
|
|
realname = gecos;
|
|
}
|
|
irc_send_raw("USER %s 0 * :%s", user, realname);
|
|
|
|
draw_statusbar();
|
|
|
|
fd_set fds;
|
|
char input_line[BUF_SIZE];
|
|
size_t input_pos = 0; /* cursor position */
|
|
size_t input_len = 0; /* total length */
|
|
char yank_buf[BUF_SIZE] = "";
|
|
size_t yank_len = 0;
|
|
int esc_pending = 0;
|
|
int 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 = esc_pending ? 50000 : 500000;
|
|
|
|
int ret = select(maxfd + 1, &fds, NULL, NULL, &tv);
|
|
if (ret < 0) {
|
|
if (errno == EINTR) continue;
|
|
die("select");
|
|
}
|
|
|
|
/* 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';
|
|
/* Skip if LLM says it's already in a skip language */
|
|
if (strcasecmp(tbuf, "SKIP") == 0 ||
|
|
strncasecmp(tbuf, "SKIP", 4) == 0) {
|
|
/* do nothing */
|
|
} else {
|
|
wprintf(translate_pending[ti].level,
|
|
" \033[3m%s\033[0m\n", tbuf);
|
|
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 *)tbuf, strlen(tbuf));
|
|
wprintf(translate_pending[ti].level,
|
|
"<%s> %s\n", nick, tbuf);
|
|
}
|
|
}
|
|
}
|
|
translate_count--;
|
|
if (ti < translate_count)
|
|
translate_pending[ti] = translate_pending[translate_count];
|
|
} else {
|
|
ti++;
|
|
}
|
|
}
|
|
|
|
if (ret == 0 && esc_pending) {
|
|
esc_pending = 0;
|
|
}
|
|
|
|
if (got_sigint) {
|
|
got_sigint = 0;
|
|
printf("\033[%d;1H\033[KWanna quit? [y/N] ",
|
|
term_rows);
|
|
fflush(stdout);
|
|
char qbuf[16];
|
|
size_t qpos = 0;
|
|
int quit = 0;
|
|
for (;;) {
|
|
unsigned char qch;
|
|
ssize_t r = read(STDIN_FILENO, &qch, 1);
|
|
if (r <= 0) break;
|
|
if (qch == '\r' || qch == '\n') {
|
|
quit = (qpos > 0 &&
|
|
(qbuf[0] == 'y' || qbuf[0] == 'Y'));
|
|
break;
|
|
} else if ((qch == 127 || qch == 0x08) && qpos > 0) {
|
|
qpos--;
|
|
printf("\b \b");
|
|
fflush(stdout);
|
|
} else if (qch >= 32 && qpos < sizeof(qbuf) - 1) {
|
|
qbuf[qpos++] = qch;
|
|
ssize_t w = write(STDOUT_FILENO, &qch, 1);
|
|
(void)w;
|
|
}
|
|
}
|
|
if (quit) {
|
|
irc_send_raw("QUIT :Leaving");
|
|
break;
|
|
}
|
|
redraw_input(input_line, input_len, input_pos);
|
|
continue;
|
|
}
|
|
|
|
if (got_sigwinch) {
|
|
got_sigwinch = 0;
|
|
get_term_size();
|
|
printf("\033[1;%dr", term_rows - 2);
|
|
redraw_window();
|
|
redraw_input(input_line, input_len, input_pos);
|
|
continue;
|
|
}
|
|
|
|
if (FD_ISSET(sock_fd, &fds)) {
|
|
ssize_t n = read(sock_fd, recv_buf + recv_len,
|
|
sizeof(recv_buf) - recv_len - 1);
|
|
if (n <= 0) {
|
|
printf("* Disconnected from server.\n");
|
|
break;
|
|
}
|
|
recv_len += (size_t)n;
|
|
recv_buf[recv_len] = '\0';
|
|
process_recv();
|
|
}
|
|
|
|
if (FD_ISSET(STDIN_FILENO, &fds)) {
|
|
unsigned char ch;
|
|
ssize_t n = read(STDIN_FILENO, &ch, 1);
|
|
if (n <= 0) {
|
|
irc_send_raw("QUIT :EOF");
|
|
break;
|
|
}
|
|
|
|
if (esc_pending) {
|
|
esc_pending = 0;
|
|
if (ch == '[') {
|
|
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);
|
|
}
|
|
}
|
|
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 == 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;
|
|
|
|
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;
|
|
}
|