From 12a9127af84371f4cda04cfa45d09d5c280687c9 Mon Sep 17 00:00:00 2001 From: Anders Holck Date: Wed, 29 Apr 2026 12:26:29 +0200 Subject: [PATCH] Add /names, Ctrl-P/N scrollback, charset logger, terminal fixes - /names #channel to list users - Ctrl-P/Ctrl-N for page up/down in scrollback - Charset logger (irc.log) with hex dump and detection - chartest.c utility for inspecting key codes - Fix window redraw to align lines at bottom - Fix terminal cleanup via atexit (no more broken terminal) - Green prompt --- chartest.c | 32 ++++++++++ main.c | 169 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 chartest.c diff --git a/chartest.c b/chartest.c new file mode 100644 index 0000000..cc13adc --- /dev/null +++ b/chartest.c @@ -0,0 +1,32 @@ +#include +#include +#include + +int main(void) +{ + struct termios orig, raw; + tcgetattr(STDIN_FILENO, &orig); + raw = orig; + raw.c_lflag &= ~(ICANON | ECHO); + raw.c_cc[VMIN] = 1; + tcsetattr(STDIN_FILENO, TCSANOW, &raw); + + printf("Press keys (Ctrl-D to quit):\n"); + for (;;) { + unsigned char c; + if (read(STDIN_FILENO, &c, 1) <= 0 || c == 0x04) + break; + printf(" dec=%3d hex=0x%02X oct=%03o", c, c, c); + if (c >= 32 && c < 127) + printf(" char='%c'", c); + else if (c >= 0xC0) + printf(" (UTF-8 lead byte, %d-byte seq)", + c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4); + else if ((c & 0xC0) == 0x80) + printf(" (UTF-8 continuation)"); + printf("\n"); + } + + tcsetattr(STDIN_FILENO, TCSANOW, &orig); + return 0; +} diff --git a/main.c b/main.c index 5af2cda..0c49917 100644 --- a/main.c +++ b/main.c @@ -30,6 +30,7 @@ #define SCROLLBACK 500 static int current_level = WL_STATUS; +static int scroll_offset = 0; /* 0 = bottom (live), >0 = scrolled up */ static volatile sig_atomic_t got_sigint = 0; static volatile sig_atomic_t got_sigwinch = 0; @@ -50,6 +51,7 @@ 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 { @@ -134,24 +136,42 @@ static void buf_store(int level, const char *line) static void redraw_window(void) { get_term_size(); - int visible = term_rows - 2; /* scroll region height */ + int visible = term_rows - 2; int count = win_buf[current_level].count; - int show = count < visible ? count : visible; + + /* Clamp scroll offset */ + int max_scroll = count - visible; + if (max_scroll < 0) max_scroll = 0; + if (scroll_offset > max_scroll) scroll_offset = max_scroll; + + int show = count - scroll_offset; + if (show > visible) show = visible; + if (show < 0) show = 0; /* Clear scroll region */ - for (int i = 1; i <= term_rows - 2; i++) + for (int i = 1; i <= visible; i++) printf("\033[%d;1H\033[K", i); - /* Print last 'show' lines */ - int start_idx; - if (count < SCROLLBACK) - start_idx = count - show; - else - start_idx = (win_buf[current_level].head - show + SCROLLBACK) % SCROLLBACK; + /* Calculate start index in circular buffer */ + int end_pos = count - scroll_offset; /* logical end position */ + int start_pos = end_pos - show; /* logical start position */ + int buf_start; + if (count <= SCROLLBACK) + buf_start = start_pos; + else + buf_start = (win_buf[current_level].head - count + start_pos + SCROLLBACK) % SCROLLBACK; + + /* Print lines aligned to bottom of scroll region */ + int first_row = visible - show + 1; for (int i = 0; i < show; i++) { - int idx = (start_idx + i) % SCROLLBACK; - printf("\033[%d;1H%s", i + 1, win_buf[current_level].lines[idx]); + int idx; + if (count <= SCROLLBACK) + idx = buf_start + i; + else + idx = (buf_start + i) % SCROLLBACK; + printf("\033[%d;1H%s", first_row + i, + win_buf[current_level].lines[idx]); } draw_statusbar(); @@ -181,12 +201,11 @@ static void wprintf(int level, const char *fmt, ...) /* Only display if this is the current window */ if (level == current_level) { - /* - * Move to last line of scroll region, issue newline to - * scroll up, then print the text on the new blank line. - */ - printf("\033[%d;1H\n\033[K%s", term_rows - 2, timestamped); - draw_statusbar(); + if (scroll_offset == 0) { + printf("\033[%d;1H\n\033[K%s", term_rows - 2, timestamped); + draw_statusbar(); + } + /* If scrolled up, don't disturb the view */ } } @@ -368,8 +387,60 @@ static void update_my_prefix(const char *chan, const char *modestr, } } +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); + char converted[BUF_SIZE]; to_iso8859_1((unsigned char *)line, strlen(line), @@ -529,31 +600,30 @@ static void handle_line(char *line) } } } else if (strcmp(cmd, "353") == 0 && params) { - /* RPL_NAMREPLY: = :names... - * Check our prefix in the names list */ + /* RPL_NAMREPLY: = :names... */ char *colon = strchr(params, ':'); if (colon) { - /* Find channel name before the colon */ char *p = params; char *chan = NULL; char *sp; - /* Skip our nick */ sp = strchr(p, ' '); if (sp) p = sp + 1; - /* Skip = or @ or * */ sp = strchr(p, ' '); if (sp) p = sp + 1; - /* Channel */ sp = strchr(p, ' '); if (sp) { *sp = '\0'; chan = p; } if (chan) { + 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) { - colon++; - /* Find our nick in the list */ - char *names = colon; - char *tok = strtok(names, " "); + /* Check our prefix */ + char namecopy[512]; + snprintf(namecopy, sizeof(namecopy), "%s", names); + char *tok = strtok(namecopy, " "); while (tok) { const char *n = tok; char pf = '\0'; @@ -643,6 +713,10 @@ static void handle_input(char *line) 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, "quit") == 0) { irc_send_raw("QUIT :%s", args ? args : "See you later"); close(sock_fd); @@ -702,6 +776,15 @@ static void redraw_input(const char *input_line, size_t input_len, 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 [port]\n", prog); @@ -719,8 +802,12 @@ int main(int argc, char *argv[]) memset(win_chans, 0, sizeof(win_chans)); + logfp = fopen("irc.log", "a"); + if (logfp) + fprintf(logfp, "--- Session started ---\n"); + /* Set terminal to raw mode */ - struct termios orig_term, raw_term; + struct termios raw_term; tcgetattr(STDIN_FILENO, &orig_term); raw_term = orig_term; raw_term.c_lflag &= ~(ICANON | ECHO); @@ -728,6 +815,8 @@ int main(int argc, char *argv[]) raw_term.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSANOW, &raw_term); + atexit(cleanup); + signal(SIGINT, sigint_handler); signal(SIGWINCH, sigwinch_handler); @@ -859,6 +948,7 @@ int main(int argc, char *argv[]) int lvl = ch - '1'; if (lvl < WL_MAX) { current_level = lvl; + scroll_offset = 0; redraw_window(); redraw_input(input_line, input_len, input_pos); } @@ -913,6 +1003,24 @@ int main(int argc, char *argv[]) memcpy(yank_buf, input_line + input_pos, yank_len); input_len = input_pos; redraw_input(input_line, input_len, input_pos); + } 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); @@ -956,11 +1064,6 @@ int main(int argc, char *argv[]) } } - /* Restore terminal */ - printf("\033[1;%dr", term_rows); /* reset scroll region */ - printf("\033[%d;1H\033[K", term_rows - 1); /* clear status bar */ - printf("\033[%d;1H\033[K", term_rows); /* clear input line */ - tcsetattr(STDIN_FILENO, TCSANOW, &orig_term); close(sock_fd); return 0; }