sirc

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

sirc.c (76414B)


      1 /*
      2  * sirc.c — multi-server terminal IRC client
      3  *
      4  * Usage:   sirc [options]
      5  *
      6  * Config   (~/.sircrc):
      7  *   # Default server (applied when no [server] block precedes it)
      8  *   nick      = mynick
      9  *   ignore    = badbot,spammer
     10  *
     11  *   [server]
     12  *   host      = irc.libera.chat
     13  *   port      = 6697
     14  *   nick      = mynick
     15  *   channel   = #python,#linux
     16  *   tls       = true
     17  *   sasl_user = mynick
     18  *   sasl_pass = hunter2
     19  *
     20  *   [server]
     21  *   host      = irc.oftc.net
     22  *   port      = 6697
     23  *   tls       = true
     24  *   channel   = #debian
     25  *
     26  * CLI adds one server on top of whatever the config defines:
     27  *   --host / --port / --nick / --channel / --tls / --no-tls
     28  *   --sasl-user / --sasl-pass / --config
     29  *
     30  * Commands:
     31  *   /join #chan          join channel (on current server)
     32  *   /part [#chan]        leave channel
     33  *   /cycle [#chan]       part and rejoin
     34  *   /nick NAME           change nick
     35  *   /msg NICK TEXT       private message
     36  *   /me TEXT             action
     37  *   /notice TGT TEXT     send notice
     38  *   /ctcp NICK CMD       CTCP request
     39  *   /names [#chan]       list users
     40  *   /topic [text]        get/set topic
     41  *   /whois NICK          whois lookup
     42  *   /who [target]        who query
     43  *   /list [pattern]      list channels on server
     44  *   /away [msg]          set away
     45  *   /back                clear away
     46  *   /invite NICK [chan]  invite to channel
     47  *   /kick NICK [reason]  kick from channel
     48  *   /mode [target] [m]   get/set mode
     49  *   /server HOST [port]  connect to new server
     50  *   /raw <line>          send raw IRC line (alias /quote)
     51  *   /ignore [nick]       ignore nick (no arg = list)
     52  *   /unignore NICK       unignore nick
     53  *   /clear               clear scrollback
     54  *   /connect             reconnect current server
     55  *   /quit [msg]          disconnect and exit
     56  *   /help                show help
     57  *
     58  * Keys:
     59  *   Tab          nick completion (cycle)
     60  *   Ctrl+N/P     next/prev channel (skips server headers)
     61  *   PgUp/PgDn    scroll chat
     62  *   Ctrl+W       delete word left
     63  *   Up/Down      input history
     64  */
     65 
     66 #define _POSIX_C_SOURCE 200809L
     67 #define _DEFAULT_SOURCE
     68 #define _GNU_SOURCE
     69 
     70 #include <string.h>
     71 #include <stdio.h>
     72 #include <stdlib.h>
     73 #include <stdarg.h>
     74 #include <ctype.h>
     75 #include <time.h>
     76 #include <errno.h>
     77 #include <unistd.h>
     78 #include <fcntl.h>
     79 #include <sys/socket.h>
     80 #include <netdb.h>
     81 #include <pthread.h>
     82 #include <signal.h>
     83 #include <netinet/in.h>
     84 #include <arpa/inet.h>
     85 #include <ncurses.h>
     86 #include <openssl/ssl.h>
     87 #include <openssl/err.h>
     88 
     89 /* ── constants ─────────────────────────────────────────────────────────────── */
     90 
     91 #define MAX_NICK         64
     92 #define MAX_CHAN         64
     93 #define MAX_HOST         256
     94 #define MAX_LINE         512
     95 #define MAX_INPUT        480
     96 #define MAX_HISTORY      500
     97 #define MAX_SERVERS      8
     98 #define MAX_CHANS_TOTAL  128   /* across all servers */
     99 #define MAX_USERS        512
    100 #define MAX_IGNORE       64
    101 #define MAX_HIST_LINES   256
    102 #define MAX_AUTOJOIN     16
    103 #define PING_INTERVAL    90
    104 #define CHAN_W           18
    105 #define USER_W           16
    106 #define RECONNECT_DELAY  5
    107 
    108 /* colour pair ids */
    109 #define C_BORDER        1
    110 #define C_HEADER        2
    111 #define C_ME            4
    112 #define C_JOIN          5
    113 #define C_MENTION       6
    114 #define C_STATUS        7
    115 #define C_INPUT         8
    116 #define C_CHAN_SEL      9
    117 #define C_CHAN          10
    118 #define C_UNREAD        11
    119 #define C_MENTION_CHAN  12
    120 #define C_URL           13
    121 #define C_ACTION        14
    122 #define C_SERVER_HDR    15   /* server name row in channel list */
    123 #define C_NICK_BASE     16
    124 #define C_NICK_COUNT    8
    125 
    126 /* line flags */
    127 #define F_MSG     0
    128 #define F_ME      1
    129 #define F_MENTION 2
    130 #define F_JOIN    3
    131 #define F_STATUS  4
    132 #define F_ACTION  5
    133 
    134 /* ── data structures ───────────────────────────────────────────────────────── */
    135 
    136 typedef struct {
    137     char ts[8];
    138     int  flag;
    139     char text[MAX_LINE];
    140 } Line;
    141 
    142 typedef struct {
    143     char  name[MAX_CHAN];
    144     int   srv;             /* index of owning server */
    145     Line  lines[MAX_HISTORY];
    146     int   line_head;
    147     int   line_count;
    148     char  users[MAX_USERS][MAX_NICK];
    149     int   user_count;
    150     int   unread;
    151     int   mention;
    152     int   scroll;
    153     char  topic[MAX_LINE];
    154 } Channel;
    155 
    156 typedef struct {
    157     char text[MAX_INPUT];
    158 } HistEntry;
    159 
    160 /* IRC event types — each carries a server index so the UI routes correctly */
    161 typedef enum {
    162     EV_CONNECTED, EV_STATUS, EV_ERROR, EV_SERVER_TEXT,
    163     EV_PRIVMSG, EV_JOIN, EV_PART, EV_QUIT_MSG,
    164     EV_NICK_CHANGE, EV_NAMES, EV_KICK, EV_RAW,
    165     EV_RECONNECT, EV_TOPIC
    166 } EvType;
    167 
    168 typedef struct {
    169     EvType type;
    170     int    srv;            /* server index */
    171     char   nick[MAX_NICK];
    172     char   chan[MAX_CHAN];
    173     char   text[MAX_LINE];
    174     char   extra[MAX_NICK];
    175 } Event;
    176 
    177 /* per-server state */
    178 typedef struct {
    179     /* config */
    180     char  host[MAX_HOST];
    181     int   port;
    182     char  nick[MAX_NICK];
    183     char  autojoin[MAX_AUTOJOIN][MAX_CHAN];
    184     int   autojoin_count;
    185     int   use_tls;
    186     char  sasl_user[MAX_NICK];
    187     char  sasl_pass[256];
    188 
    189     /* runtime */
    190     int   connected;
    191     volatile int net_stop;
    192     pthread_t    net_tid;
    193     pthread_mutex_t send_lock;
    194     int   sock;
    195     SSL  *ssl;
    196 
    197     int   reconnect_pending;
    198     pthread_t reconnect_tid;
    199 
    200     /* per-server event pipe */
    201     int   evpipe[2];
    202 } Server;
    203 
    204 /* ── globals ───────────────────────────────────────────────────────────────── */
    205 
    206 static Server   g_srv[MAX_SERVERS];
    207 static int      g_srv_count  = 0;
    208 static SSL_CTX *g_ssl_ctx    = NULL;
    209 
    210 /* channels (flat array, each has a .srv index) */
    211 static Channel  g_chans[MAX_CHANS_TOTAL];
    212 static int      g_chan_count = 0;
    213 static int      g_active     = 0;   /* index into g_chans; -1 = no channels */
    214 
    215 /* global ignore list */
    216 static char  g_ignore[MAX_IGNORE][MAX_NICK];
    217 static int   g_ignore_count = 0;
    218 
    219 /* input */
    220 static char  g_input[MAX_INPUT];
    221 static int   g_input_len    = 0;
    222 static int   g_input_cur    = 0;
    223 static HistEntry g_hist[MAX_HIST_LINES];
    224 static int   g_hist_count   = 0;
    225 static int   g_hist_pos     = -1;
    226 
    227 /* tab completion */
    228 static char  g_tab_matches[MAX_USERS][MAX_NICK];
    229 static int   g_tab_count      = 0;
    230 static int   g_tab_idx        = 0;
    231 static int   g_tab_word_start = 0;
    232 static int   g_tab_active     = 0;
    233 
    234 /* single event pipe the UI polls — all servers write here */
    235 static int g_evpipe[2];
    236 
    237 /* curses windows */
    238 static WINDOW *win_chan   = NULL;
    239 static WINDOW *win_chat   = NULL;
    240 static WINDOW *win_users  = NULL;
    241 static WINDOW *win_input  = NULL;
    242 static WINDOW *win_status = NULL;
    243 
    244 /* ── utility ───────────────────────────────────────────────────────────────── */
    245 
    246 static void str_upper(char *dst, const char *src, int n) {
    247     int i;
    248     for (i = 0; i < n-1 && src[i]; i++)
    249         dst[i] = toupper((unsigned char)src[i]);
    250     dst[i] = '\0';
    251 }
    252 
    253 static int str_icase_starts(const char *hay, const char *needle) {
    254     while (*needle) {
    255         if (tolower((unsigned char)*hay) != tolower((unsigned char)*needle))
    256             return 0;
    257         hay++; needle++;
    258     }
    259     return 1;
    260 }
    261 
    262 static int nick_colour(const char *nick) {
    263     unsigned int h = 0;
    264     for (; *nick; nick++) h = (h * 31 + (unsigned char)*nick) & 0xFFFF;
    265     return C_NICK_BASE + (h % C_NICK_COUNT);
    266 }
    267 
    268 static void timestamp(char *buf, int n) {
    269     time_t t = time(NULL);
    270     strftime(buf, n, "%H:%M", localtime(&t));
    271 }
    272 
    273 /* ── channel management ────────────────────────────────────────────────────── */
    274 
    275 static int chan_find(int srv, const char *name) {
    276     for (int i = 0; i < g_chan_count; i++)
    277         if (g_chans[i].srv == srv && strcasecmp(g_chans[i].name, name) == 0)
    278             return i;
    279     return -1;
    280 }
    281 
    282 static int chan_add(int srv, const char *name) {
    283     int i = chan_find(srv, name);
    284     if (i >= 0) return i;
    285     if (g_chan_count >= MAX_CHANS_TOTAL) return 0;
    286     i = g_chan_count++;
    287     memset(&g_chans[i], 0, sizeof(Channel));
    288     strncpy(g_chans[i].name, name, MAX_CHAN-1);
    289     g_chans[i].srv = srv;
    290     return i;
    291 }
    292 
    293 static void chan_remove(int idx) {
    294     if (idx < 0 || idx >= g_chan_count) return;
    295     for (int i = idx; i < g_chan_count-1; i++)
    296         g_chans[i] = g_chans[i+1];
    297     g_chan_count--;
    298     if (g_active >= g_chan_count) g_active = g_chan_count - 1;
    299     if (g_active < 0) g_active = 0;
    300 }
    301 
    302 static Line *chan_line(Channel *ch, int idx) {
    303     if (idx < 0 || idx >= ch->line_count) return NULL;
    304     int real = (ch->line_head - ch->line_count + idx + MAX_HISTORY) % MAX_HISTORY;
    305     return &ch->lines[real];
    306 }
    307 
    308 /* strip IRC formatting: \x02 bold, \x03 colour, \x0f reset,
    309    \x1d italic, \x1f underline, \x1e strikethrough, \x11 monospace */
    310 static void strip_irc_fmt(const char *in, char *out, int outlen) {
    311     int j = 0;
    312     for (int i = 0; in[i] && j < outlen-1; i++) {
    313         unsigned char c = (unsigned char)in[i];
    314         if (c==0x02||c==0x0f||c==0x1d||c==0x1f||c==0x1e||c==0x11)
    315             continue;
    316         if (c == 0x03) {          /* colour: \x03[fg[,bg]] — skip digits */
    317             i++;
    318             if (in[i] && isdigit((unsigned char)in[i])) i++;
    319             if (in[i] && isdigit((unsigned char)in[i])) i++;
    320             if (in[i] == ',') {
    321                 i++;
    322                 if (in[i] && isdigit((unsigned char)in[i])) i++;
    323                 if (in[i] && isdigit((unsigned char)in[i])) i++;
    324             }
    325             i--;
    326             continue;
    327         }
    328         out[j++] = (char)c;
    329     }
    330     out[j] = '\0';
    331 }
    332 
    333 static void chan_addline(Channel *ch, int flag, const char *text) {
    334     char ts[8]; timestamp(ts, sizeof(ts));
    335     Line *l = &ch->lines[ch->line_head % MAX_HISTORY];
    336     strncpy(l->ts, ts, sizeof(l->ts)-1);
    337     strip_irc_fmt(text, l->text, MAX_LINE);
    338     l->flag = flag;
    339     ch->line_head = (ch->line_head + 1) % MAX_HISTORY;
    340     if (ch->line_count < MAX_HISTORY) ch->line_count++;
    341     if (flag != F_STATUS) ch->unread++;
    342 }
    343 
    344 /* get the status channel for a server (create if needed) */
    345 static int srv_status_chan(int srv) {
    346     return chan_add(srv, "*status*");
    347 }
    348 
    349 static void srv_status_msg(int srv, const char *msg) {
    350     int ci = srv_status_chan(srv);
    351     chan_addline(&g_chans[ci], F_STATUS, msg);
    352 }
    353 
    354 static void srv_status_fmt(int srv, const char *fmt, ...) {
    355     char buf[MAX_LINE];
    356     va_list ap; va_start(ap, fmt);
    357     vsnprintf(buf, sizeof(buf), fmt, ap);
    358     va_end(ap);
    359     srv_status_msg(srv, buf);
    360 }
    361 
    362 /* Store user as "<prefix><nick>" where prefix is @, +, ~, &, % or space.
    363    Lookup is always by bare nick (skip leading mode char). */
    364 static int user_find(Channel *ch, const char *nick) {
    365     for (int i = 0; i < ch->user_count; i++) {
    366         const char *stored = ch->users[i];
    367         /* skip stored mode prefix */
    368         if (*stored == '@' || *stored == '+' || *stored == '~' ||
    369             *stored == '&' || *stored == '%' || *stored == ' ')
    370             stored++;
    371         if (strcasecmp(stored, nick) == 0) return i;
    372     }
    373     return -1;
    374 }
    375 
    376 static void chan_adduser(Channel *ch, char mode, const char *nick) {
    377     int idx = user_find(ch, nick);
    378     char entry[MAX_NICK];
    379     entry[0] = mode ? mode : ' ';
    380     strncpy(entry+1, nick, MAX_NICK-2);
    381     if (idx >= 0) {
    382         /* update mode in place */
    383         strncpy(ch->users[idx], entry, MAX_NICK-1);
    384         return;
    385     }
    386     if (ch->user_count >= MAX_USERS) return;
    387     strncpy(ch->users[ch->user_count++], entry, MAX_NICK-1);
    388 }
    389 
    390 static void chan_removeuser(Channel *ch, const char *nick) {
    391     int idx = user_find(ch, nick);
    392     if (idx < 0) return;
    393     for (int j = idx; j < ch->user_count-1; j++)
    394         memcpy(ch->users[j], ch->users[j+1], MAX_NICK);
    395     ch->user_count--;
    396 }
    397 
    398 static int mode_rank(char c) {
    399     /* lower = higher privilege */
    400     switch (c) {
    401         case '~': return 0;   /* founder/owner */
    402         case '&': return 1;   /* protected */
    403         case '@': return 2;   /* op */
    404         case '%': return 3;   /* halfop */
    405         case '+': return 4;   /* voice */
    406         default:  return 5;   /* regular */
    407     }
    408 }
    409 
    410 static int user_cmp(const void *a, const void *b) {
    411     const char *sa = (const char *)a;
    412     const char *sb = (const char *)b;
    413     int ra = mode_rank(*sa);
    414     int rb = mode_rank(*sb);
    415     if (ra != rb) return ra - rb;
    416     /* same rank: sort by nick (skip mode prefix) */
    417     const char *na = (*sa == ' ' || mode_rank(*sa) < 5) ? sa+1 : sa;
    418     const char *nb = (*sb == ' ' || mode_rank(*sb) < 5) ? sb+1 : sb;
    419     return strcasecmp(na, nb);
    420 }
    421 
    422 static void chan_sort_users(Channel *ch) {
    423     qsort(ch->users, ch->user_count, MAX_NICK, user_cmp);
    424 }
    425 
    426 /* next/prev channel index that is NOT a *status* channel (unless it's the
    427    only kind), skipping nothing — just a linear wrap */
    428 static int chan_next(int cur, int dir) {
    429     if (g_chan_count <= 1) return cur;
    430     return (cur + dir + g_chan_count) % g_chan_count;
    431 }
    432 
    433 /* ── ignore list ───────────────────────────────────────────────────────────── */
    434 
    435 static int is_ignored(const char *nick) {
    436     for (int i = 0; i < g_ignore_count; i++)
    437         if (strcasecmp(g_ignore[i], nick) == 0) return 1;
    438     return 0;
    439 }
    440 
    441 static void ignore_add(const char *nick) {
    442     if (is_ignored(nick) || g_ignore_count >= MAX_IGNORE) return;
    443     strncpy(g_ignore[g_ignore_count++], nick, MAX_NICK-1);
    444 }
    445 
    446 static void ignore_remove(const char *nick) {
    447     for (int i = 0; i < g_ignore_count; i++) {
    448         if (strcasecmp(g_ignore[i], nick) == 0) {
    449             for (int j = i; j < g_ignore_count-1; j++)
    450                 memcpy(g_ignore[j], g_ignore[j+1], MAX_NICK);
    451             g_ignore_count--;
    452             return;
    453         }
    454     }
    455 }
    456 
    457 /* ── event pipe ────────────────────────────────────────────────────────────── */
    458 
    459 static void ev_push(const Event *ev) {
    460     write(g_evpipe[1], ev, sizeof(Event));
    461 }
    462 
    463 static void ev_simple(EvType type, int srv, const char *nick,
    464                        const char *chan, const char *text) {
    465     Event ev; memset(&ev, 0, sizeof(ev));
    466     ev.type = type; ev.srv = srv;
    467     if (nick) strncpy(ev.nick, nick, MAX_NICK-1);
    468     if (chan) strncpy(ev.chan, chan,  MAX_CHAN-1);
    469     if (text) strncpy(ev.text, text, MAX_LINE-1);
    470     ev_push(&ev);
    471 }
    472 
    473 /* ── network send ──────────────────────────────────────────────────────────── */
    474 
    475 static int srv_send_raw(int si, const char *line) {
    476     char buf[MAX_LINE];
    477     int n = snprintf(buf, sizeof(buf), "%s\r\n", line);
    478     if (n <= 0 || n >= (int)sizeof(buf)) return -1;
    479     Server *s = &g_srv[si];
    480     pthread_mutex_lock(&s->send_lock);
    481     int r;
    482     if (s->use_tls && s->ssl)
    483         r = SSL_write(s->ssl, buf, n);
    484     else
    485         r = send(s->sock, buf, n, 0);
    486     pthread_mutex_unlock(&s->send_lock);
    487     return r > 0 ? 0 : -1;
    488 }
    489 
    490 static void srv_sendf(int si, const char *fmt, ...) {
    491     char buf[MAX_LINE];
    492     va_list ap; va_start(ap, fmt);
    493     vsnprintf(buf, sizeof(buf), fmt, ap);
    494     va_end(ap);
    495     srv_send_raw(si, buf);
    496 }
    497 
    498 /* ── IRC parser ────────────────────────────────────────────────────────────── */
    499 
    500 typedef struct {
    501     char prefix[MAX_LINE];
    502     char cmd[32];
    503     char params[16][MAX_LINE];
    504     int  nparams;
    505     char trail[MAX_LINE];
    506 } IrcMsg;
    507 
    508 static void nick_from_prefix(const char *prefix, char *nick, int n) {
    509     const char *bang = strchr(prefix, '!');
    510     if (bang) {
    511         int len = (int)(bang - prefix);
    512         if (len >= n) len = n-1;
    513         memcpy(nick, prefix, len); nick[len] = '\0';
    514     } else {
    515         strncpy(nick, prefix, n-1); nick[n-1] = '\0';
    516     }
    517 }
    518 
    519 static void parse_irc(const char *raw, IrcMsg *m) {
    520     memset(m, 0, sizeof(*m));
    521     const char *p = raw;
    522     if (*p == ':') {
    523         p++;
    524         const char *end = strchr(p, ' '); if (!end) return;
    525         int len = (int)(end-p); if (len>=(int)sizeof(m->prefix)) len=sizeof(m->prefix)-1;
    526         memcpy(m->prefix, p, len); p = end+1;
    527     }
    528     {
    529         const char *end = strchr(p, ' ');
    530         int len = end ? (int)(end-p) : (int)strlen(p);
    531         if (len>=(int)sizeof(m->cmd)) len=sizeof(m->cmd)-1;
    532         memcpy(m->cmd, p, len); str_upper(m->cmd, m->cmd, sizeof(m->cmd));
    533         if (!end) return; p = end+1;
    534     }
    535     while (*p) {
    536         if (*p == ':') { strncpy(m->trail, p+1, MAX_LINE-1); break; }
    537         const char *end = strchr(p, ' ');
    538         int len = end ? (int)(end-p) : (int)strlen(p);
    539         if (m->nparams < 16) {
    540             if (len>=MAX_LINE) len=MAX_LINE-1;
    541             memcpy(m->params[m->nparams], p, len); m->nparams++;
    542         }
    543         if (!end) break; p = end+1;
    544     }
    545 }
    546 
    547 /* ── base64 for SASL ───────────────────────────────────────────────────────── */
    548 
    549 static const char b64tab[] =
    550     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    551 
    552 static void base64_encode(const unsigned char *in, int inlen, char *out) {
    553     int i=0, j=0;
    554     while (i < inlen) {
    555         unsigned int a=i<inlen?(unsigned char)in[i++]:0;
    556         unsigned int b=i<inlen?(unsigned char)in[i++]:0;
    557         unsigned int c=i<inlen?(unsigned char)in[i++]:0;
    558         unsigned int n=(a<<16)|(b<<8)|c;
    559         out[j++]=b64tab[(n>>18)&63]; out[j++]=b64tab[(n>>12)&63];
    560         out[j++]=(i-2<=inlen)?b64tab[(n>>6)&63]:'=';
    561         out[j++]=(i-1<=inlen)?b64tab[n&63]:'=';
    562     }
    563     out[j]='\0';
    564 }
    565 
    566 /* ── IRC line handler (net thread — uses si to tag every event) ────────────── */
    567 
    568 static void handle_irc_line(int si, const char *raw) {
    569     IrcMsg m; parse_irc(raw, &m);
    570     char nick[MAX_NICK]=""; nick_from_prefix(m.prefix, nick, sizeof(nick));
    571     Server *s = &g_srv[si];
    572 
    573     if (strcmp(m.cmd,"PING")==0) {
    574         srv_sendf(si, "PONG :%s", m.trail[0]?m.trail:m.params[0]);
    575         return;
    576     }
    577 
    578     /* SASL */
    579     if (strcmp(m.cmd,"CAP")==0) {
    580         char sub[16]="";
    581         if (m.nparams>=2) str_upper(sub, m.params[1], sizeof(sub));
    582         if (strcmp(sub,"ACK")==0 && strstr(m.trail,"sasl")) {
    583             srv_send_raw(si, "AUTHENTICATE PLAIN");
    584         } else if (strcmp(sub,"NAK")==0) {
    585             ev_simple(EV_ERROR, si, NULL, NULL, "Server rejected SASL CAP.");
    586             srv_sendf(si, "NICK %s", s->nick);
    587             srv_sendf(si, "USER %s 0 * :circ", s->nick);
    588         }
    589         return;
    590     }
    591     if (strcmp(m.cmd,"AUTHENTICATE")==0 && strcmp(m.trail,"+")==0) {
    592         char payload[512];
    593         int plen=snprintf(payload,sizeof(payload),"%s%c%s%c%s",
    594                           s->sasl_user,0,s->sasl_user,0,s->sasl_pass);
    595         char enc[700]; base64_encode((unsigned char*)payload,plen,enc);
    596         srv_sendf(si, "AUTHENTICATE %s", enc);
    597         return;
    598     }
    599     if (strcmp(m.cmd,"903")==0) {
    600         ev_simple(EV_STATUS, si, NULL, NULL, "SASL authentication successful.");
    601         srv_send_raw(si, "CAP END");
    602         srv_sendf(si, "NICK %s", s->nick);
    603         srv_sendf(si, "USER %s 0 * :circ", s->nick);
    604         return;
    605     }
    606     if (strcmp(m.cmd,"902")==0||strcmp(m.cmd,"904")==0||
    607         strcmp(m.cmd,"905")==0||strcmp(m.cmd,"906")==0) {
    608         char buf[MAX_LINE];
    609         snprintf(buf,sizeof(buf),"SASL failed (%s): %s",m.cmd,m.trail);
    610         ev_simple(EV_ERROR, si, NULL, NULL, buf);
    611         srv_send_raw(si, "CAP END");
    612         srv_sendf(si, "NICK %s", s->nick);
    613         srv_sendf(si, "USER %s 0 * :circ", s->nick);
    614         return;
    615     }
    616 
    617     /* 001 */
    618     if (strcmp(m.cmd,"001")==0) {
    619         s->connected=1;
    620         ev_simple(EV_CONNECTED, si, NULL, NULL, m.trail);
    621         ev_simple(EV_SERVER_TEXT, si, NULL, NULL, m.trail);
    622         return;
    623     }
    624 
    625     /* PRIVMSG */
    626     if (strcmp(m.cmd,"PRIVMSG")==0) {
    627         Event ev; memset(&ev,0,sizeof(ev));
    628         ev.type=EV_PRIVMSG; ev.srv=si;
    629         strncpy(ev.nick, nick, MAX_NICK-1);
    630         strncpy(ev.chan, m.nparams>0?m.params[0]:"", MAX_CHAN-1);
    631         strncpy(ev.text, m.trail, MAX_LINE-1);
    632         ev_push(&ev); return;
    633     }
    634 
    635     /* JOIN */
    636     if (strcmp(m.cmd,"JOIN")==0) {
    637         const char *ch=m.trail[0]?m.trail:(m.nparams>0?m.params[0]:"");
    638         ev_simple(EV_JOIN, si, nick, ch, NULL); return;
    639     }
    640 
    641     /* PART */
    642     if (strcmp(m.cmd,"PART")==0) {
    643         ev_simple(EV_PART, si, nick, m.nparams>0?m.params[0]:"", m.trail);
    644         return;
    645     }
    646 
    647     /* QUIT */
    648     if (strcmp(m.cmd,"QUIT")==0) {
    649         ev_simple(EV_QUIT_MSG, si, nick, NULL, m.trail); return;
    650     }
    651 
    652     /* NICK */
    653     if (strcmp(m.cmd,"NICK")==0) {
    654         Event ev; memset(&ev,0,sizeof(ev));
    655         ev.type=EV_NICK_CHANGE; ev.srv=si;
    656         strncpy(ev.nick, nick, MAX_NICK-1);
    657         const char *nn=m.trail[0]?m.trail:(m.nparams>0?m.params[0]:"");
    658         strncpy(ev.extra, nn, MAX_NICK-1);
    659         ev_push(&ev); return;
    660     }
    661 
    662     /* 353 NAMES */
    663     if (strcmp(m.cmd,"353")==0) {
    664         const char *ch=m.nparams>=3?m.params[2]:"";
    665         char tmp[MAX_LINE]; strncpy(tmp,m.trail,MAX_LINE-1);
    666         char *tok=strtok(tmp," ");
    667         while (tok) {
    668             Event ev; memset(&ev,0,sizeof(ev));
    669             ev.type=EV_NAMES; ev.srv=si;
    670             strncpy(ev.chan, ch, MAX_CHAN-1);
    671             /* ev.extra[0] = mode prefix char (or 0 if none) */
    672             const char *n=tok;
    673             if (*n=='@'||*n=='+'||*n=='~'||*n=='&'||*n=='%') {
    674                 ev.extra[0]=*n; n++;
    675             }
    676             strncpy(ev.nick, n, MAX_NICK-1);
    677             ev_push(&ev);
    678             tok=strtok(NULL," ");
    679         }
    680         return;
    681     }
    682 
    683     /* KICK */
    684     if (strcmp(m.cmd,"KICK")==0) {
    685         Event ev; memset(&ev,0,sizeof(ev));
    686         ev.type=EV_KICK; ev.srv=si;
    687         strncpy(ev.nick,  nick, MAX_NICK-1);
    688         strncpy(ev.chan,  m.nparams>0?m.params[0]:"", MAX_CHAN-1);
    689         strncpy(ev.extra, m.nparams>1?m.params[1]:"", MAX_NICK-1);
    690         strncpy(ev.text,  m.trail, MAX_LINE-1);
    691         ev_push(&ev); return;
    692     }
    693 
    694     /* 433 nick in use */
    695     if (strcmp(m.cmd,"433")==0) {
    696         ev_simple(EV_ERROR, si, NULL, NULL, "Nickname already in use.");
    697         return;
    698     }
    699 
    700     /* server text */
    701     if (strcmp(m.cmd,"002")==0||strcmp(m.cmd,"003")==0||strcmp(m.cmd,"004")==0||
    702         strcmp(m.cmd,"375")==0||strcmp(m.cmd,"372")==0||strcmp(m.cmd,"376")==0) {
    703         ev_simple(EV_SERVER_TEXT, si, NULL, NULL,
    704                   m.trail[0]?m.trail:m.params[m.nparams>1?1:0]);
    705         return;
    706     }
    707 
    708     /* MODE */
    709     if (strcmp(m.cmd,"MODE")==0) {
    710         char buf[MAX_LINE];
    711         snprintf(buf,sizeof(buf),"MODE %s %s",m.params[0],m.trail);
    712         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    713     }
    714 
    715     /* TOPIC */
    716     if (strcmp(m.cmd,"331")==0) {
    717         const char *ch=m.nparams>1?m.params[1]:m.params[0];
    718         char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] No topic set",ch);
    719         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    720     }
    721     if (strcmp(m.cmd,"332")==0) {
    722         const char *ch=m.nparams>1?m.params[1]:m.params[0];
    723         char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] Topic: %s",ch,m.trail);
    724         ev_simple(EV_STATUS, si, NULL, NULL, buf);
    725         ev_simple(EV_TOPIC,  si, NULL, ch, m.trail);
    726         return;
    727     }
    728     if (strcmp(m.cmd,"333")==0) {
    729         const char *ch=m.nparams>1?m.params[1]:"";
    730         const char *setter=m.nparams>2?m.params[2]:"";
    731         char sn[MAX_NICK]; strncpy(sn,setter,MAX_NICK-1);
    732         char *bang=strchr(sn,'!'); if(bang)*bang='\0';
    733         char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] Topic set by %s",ch,sn);
    734         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    735     }
    736     if (strcmp(m.cmd,"TOPIC")==0) {
    737         const char *ch=m.nparams>0?m.params[0]:"";
    738         char buf[MAX_LINE];
    739         snprintf(buf,sizeof(buf),"[%s] %s changed topic to: %s",ch,nick,m.trail);
    740         ev_simple(EV_STATUS,      si, NULL, NULL, buf);
    741         ev_simple(EV_SERVER_TEXT, si, NULL, ch,   buf);
    742         ev_simple(EV_TOPIC,       si, NULL, ch,   m.trail);
    743         return;
    744     }
    745 
    746     /* WHOIS */
    747     if (strcmp(m.cmd,"311")==0) {
    748         char buf[MAX_LINE];
    749         snprintf(buf,sizeof(buf),"[whois] %s (%s@%s): %s",
    750                  m.nparams>1?m.params[1]:"",m.nparams>2?m.params[2]:"",
    751                  m.nparams>3?m.params[3]:"",m.trail);
    752         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    753     }
    754     if (strcmp(m.cmd,"312")==0) {
    755         char buf[MAX_LINE];
    756         snprintf(buf,sizeof(buf),"[whois] server: %s (%s)",
    757                  m.nparams>2?m.params[2]:"",m.trail);
    758         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    759     }
    760     if (strcmp(m.cmd,"313")==0) {
    761         char buf[MAX_LINE];
    762         snprintf(buf,sizeof(buf),"[whois] %s is an IRC operator",
    763                  m.nparams>1?m.params[1]:"");
    764         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    765     }
    766     if (strcmp(m.cmd,"317")==0) {
    767         int idle=m.nparams>2?atoi(m.params[2]):0;
    768         char buf[MAX_LINE];
    769         snprintf(buf,sizeof(buf),"[whois] idle: %dm%ds",idle/60,idle%60);
    770         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    771     }
    772     if (strcmp(m.cmd,"318")==0) {
    773         ev_simple(EV_STATUS, si, NULL, NULL, "[whois] end"); return;
    774     }
    775     if (strcmp(m.cmd,"319")==0) {
    776         char buf[MAX_LINE];
    777         snprintf(buf,sizeof(buf),"[whois] channels: %s",m.trail);
    778         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    779     }
    780     if (strcmp(m.cmd,"307")==0||strcmp(m.cmd,"330")==0) {
    781         char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[whois] %s",m.trail);
    782         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    783     }
    784 
    785     /* WHO */
    786     if (strcmp(m.cmd,"352")==0) {
    787         char buf[MAX_LINE];
    788         snprintf(buf,sizeof(buf),"[who] %-16s %s!%s@%s",
    789                  m.nparams>1?m.params[1]:"*",
    790                  m.nparams>6?m.params[6]:"",
    791                  m.nparams>2?m.params[2]:"",
    792                  m.nparams>3?m.params[3]:"");
    793         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    794     }
    795     if (strcmp(m.cmd,"315")==0) {
    796         ev_simple(EV_STATUS, si, NULL, NULL, "[who] end"); return;
    797     }
    798 
    799     /* LIST */
    800     if (strcmp(m.cmd,"321")==0) {
    801         ev_simple(EV_STATUS, si, NULL, NULL, "[list] Channel          Users  Topic");
    802         return;
    803     }
    804     if (strcmp(m.cmd,"322")==0) {
    805         char buf[MAX_LINE];
    806         snprintf(buf,sizeof(buf),"[list] %-20s %-6s %s",
    807                  m.nparams>1?m.params[1]:"",
    808                  m.nparams>2?m.params[2]:"",m.trail);
    809         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    810     }
    811     if (strcmp(m.cmd,"323")==0) {
    812         ev_simple(EV_STATUS, si, NULL, NULL, "[list] end"); return;
    813     }
    814 
    815     if (strcmp(m.cmd,"366")==0) return;
    816 
    817     /* AWAY */
    818     if (strcmp(m.cmd,"301")==0) {
    819         char buf[MAX_LINE];
    820         snprintf(buf,sizeof(buf),"[away] %s is away: %s",
    821                  m.nparams>1?m.params[1]:"",m.trail);
    822         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    823     }
    824     if (strcmp(m.cmd,"305")==0) {
    825         ev_simple(EV_STATUS, si, NULL, NULL, "You are no longer marked as away.");
    826         return;
    827     }
    828     if (strcmp(m.cmd,"306")==0) {
    829         ev_simple(EV_STATUS, si, NULL, NULL, "You have been marked as away.");
    830         return;
    831     }
    832 
    833     /* INVITE */
    834     if (strcmp(m.cmd,"INVITE")==0) {
    835         char buf[MAX_LINE];
    836         snprintf(buf,sizeof(buf),"** %s invites you to %s",nick,
    837                  m.trail[0]?m.trail:m.params[m.nparams>1?1:0]);
    838         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    839     }
    840     if (strcmp(m.cmd,"341")==0) {
    841         char buf[MAX_LINE];
    842         snprintf(buf,sizeof(buf),"Invited %s to %s",
    843                  m.nparams>1?m.params[1]:"",m.nparams>2?m.params[2]:m.trail);
    844         ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
    845     }
    846 
    847     /* NOTICE */
    848     if (strcmp(m.cmd,"NOTICE")==0) {
    849         const char *target=m.nparams>0?m.params[0]:"";
    850         char buf[MAX_LINE];
    851         snprintf(buf,sizeof(buf),"--%s-- %s",nick[0]?nick:"server",m.trail);
    852         if (target[0]=='#') {
    853             Event ev2; memset(&ev2,0,sizeof(ev2));
    854             ev2.type=EV_SERVER_TEXT; ev2.srv=si;
    855             strncpy(ev2.chan, target, MAX_CHAN-1);
    856             strncpy(ev2.text, buf, MAX_LINE-1);
    857             ev_push(&ev2);
    858         } else {
    859             ev_simple(EV_STATUS, si, NULL, NULL, buf);
    860         }
    861         return;
    862     }
    863 
    864     ev_simple(EV_RAW, si, NULL, NULL, raw);
    865 }
    866 
    867 /* ── network thread ────────────────────────────────────────────────────────── */
    868 
    869 static void srv_close(int si) {
    870     Server *s=&g_srv[si];
    871     if (s->use_tls && s->ssl) { SSL_shutdown(s->ssl); SSL_free(s->ssl); s->ssl=NULL; }
    872     if (s->sock>=0) { close(s->sock); s->sock=-1; }
    873     s->connected=0;
    874 }
    875 
    876 static int srv_connect(int si) {
    877     Server *s=&g_srv[si];
    878     char portstr[8]; snprintf(portstr,sizeof(portstr),"%d",s->port);
    879     struct addrinfo hints,*res,*r;
    880     memset(&hints,0,sizeof(hints));
    881     hints.ai_family=AF_UNSPEC; hints.ai_socktype=SOCK_STREAM;
    882     if (getaddrinfo(s->host,portstr,&hints,&res)!=0) return -1;
    883     s->sock=-1;
    884     for (r=res; r; r=r->ai_next) {
    885         int fd=socket(r->ai_family,r->ai_socktype,r->ai_protocol);
    886         if (fd<0) continue;
    887         int flags=fcntl(fd,F_GETFL,0);
    888         fcntl(fd,F_SETFL,flags|O_NONBLOCK);
    889         int rc=connect(fd,r->ai_addr,r->ai_addrlen);
    890         if (rc==0||errno==EINPROGRESS) {
    891             fd_set wfds; FD_ZERO(&wfds); FD_SET(fd,&wfds);
    892             struct timeval tv={15,0};
    893             if (select(fd+1,NULL,&wfds,NULL,&tv)>0) {
    894                 int err=0; socklen_t el=sizeof(err);
    895                 getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&el);
    896                 if (err==0) { s->sock=fd; break; }
    897             }
    898         }
    899         close(fd);
    900     }
    901     freeaddrinfo(res);
    902     if (s->sock<0) return -1;
    903     int flags=fcntl(s->sock,F_GETFL,0);
    904     fcntl(s->sock,F_SETFL,flags&~O_NONBLOCK);
    905 
    906     if (s->use_tls) {
    907         if (!g_ssl_ctx) {
    908             SSL_library_init(); SSL_load_error_strings();
    909             g_ssl_ctx=SSL_CTX_new(TLS_client_method());
    910             if (!g_ssl_ctx) return -1;
    911             SSL_CTX_set_verify(g_ssl_ctx,SSL_VERIFY_PEER,NULL);
    912             SSL_CTX_set_default_verify_paths(g_ssl_ctx);
    913         }
    914         s->ssl=SSL_new(g_ssl_ctx);
    915         SSL_set_fd(s->ssl,s->sock);
    916         SSL_set_tlsext_host_name(s->ssl,s->host);
    917         if (SSL_connect(s->ssl)<=0) {
    918             SSL_free(s->ssl); s->ssl=NULL; return -1;
    919         }
    920     }
    921     return 0;
    922 }
    923 
    924 typedef struct { int si; } NetArg;
    925 
    926 static void *net_thread(void *arg) {
    927     NetArg *na=(NetArg*)arg; int si=na->si; free(na);
    928     Server *s=&g_srv[si];
    929 
    930     char buf[MAX_LINE];
    931     snprintf(buf,sizeof(buf),"Connecting to %s:%d%s...",
    932              s->host,s->port,s->use_tls?" (TLS)":"");
    933     ev_simple(EV_STATUS, si, NULL, NULL, buf);
    934 
    935     if (srv_connect(si)<0) {
    936         ev_simple(EV_ERROR, si, NULL, NULL, "Connection failed.");
    937         ev_simple(EV_RECONNECT, si, NULL, NULL, NULL);
    938         return NULL;
    939     }
    940 
    941     int sasl_pending=(s->sasl_user[0]&&s->sasl_pass[0])?1:0;
    942     if (sasl_pending)
    943         srv_send_raw(si,"CAP REQ :sasl");
    944     else {
    945         srv_sendf(si,"NICK %s",s->nick);
    946         srv_sendf(si,"USER %s 0 * :circ",s->nick);
    947     }
    948 
    949     char readbuf[8192], linebuf[MAX_LINE*4];
    950     int linelen=0;
    951     time_t last_ping=time(NULL);
    952 
    953     while (!s->net_stop) {
    954         if (time(NULL)-last_ping>PING_INTERVAL) {
    955             srv_sendf(si,"PING :%s",s->host);
    956             last_ping=time(NULL);
    957         }
    958         fd_set rfds; FD_ZERO(&rfds); FD_SET(s->sock,&rfds);
    959         struct timeval tv={5,0};
    960         int sel=select(s->sock+1,&rfds,NULL,NULL,&tv);
    961         if (sel==0) continue;
    962         if (sel<0) { ev_simple(EV_ERROR,si,NULL,NULL,"Select error."); break; }
    963 
    964         int n;
    965         if (s->use_tls && s->ssl)
    966             n=SSL_read(s->ssl,readbuf,sizeof(readbuf)-1);
    967         else
    968             n=recv(s->sock,readbuf,sizeof(readbuf)-1,0);
    969         if (n<=0) { ev_simple(EV_ERROR,si,NULL,NULL,"Server closed connection."); break; }
    970         readbuf[n]='\0';
    971 
    972         for (int i=0; i<n; i++) {
    973             char c=readbuf[i];
    974             if (c=='\n') {
    975                 if (linelen>0 && linebuf[linelen-1]=='\r') linelen--;
    976                 linebuf[linelen]='\0';
    977                 if (linelen>0) handle_irc_line(si,linebuf);
    978                 linelen=0;
    979             } else if (linelen<(int)sizeof(linebuf)-1) {
    980                 linebuf[linelen++]=c;
    981             }
    982         }
    983         last_ping=time(NULL);
    984     }
    985 
    986     srv_close(si);
    987     ev_simple(EV_RECONNECT, si, NULL, NULL, NULL);
    988     return NULL;
    989 }
    990 
    991 /* ── reconnect ─────────────────────────────────────────────────────────────── */
    992 
    993 static void start_net_thread(int si);
    994 
    995 typedef struct { int si; } ReconArg;
    996 static void *reconnect_thread(void *arg) {
    997     ReconArg *ra=(ReconArg*)arg; int si=ra->si; free(ra);
    998     sleep(RECONNECT_DELAY);
    999     g_srv[si].reconnect_pending=0;
   1000     start_net_thread(si);
   1001     return NULL;
   1002 }
   1003 
   1004 static void start_net_thread(int si) {
   1005     g_srv[si].net_stop=0;
   1006     NetArg *na=malloc(sizeof(NetArg)); na->si=si;
   1007     pthread_create(&g_srv[si].net_tid, NULL, net_thread, na);
   1008 }
   1009 
   1010 static void schedule_reconnect(int si) {
   1011     if (g_srv[si].reconnect_pending) return;
   1012     g_srv[si].reconnect_pending=1;
   1013     srv_status_fmt(si,"Reconnecting in %ds...",RECONNECT_DELAY);
   1014     ReconArg *ra=malloc(sizeof(ReconArg)); ra->si=si;
   1015     pthread_create(&g_srv[si].reconnect_tid, NULL, reconnect_thread, ra);
   1016     pthread_detach(g_srv[si].reconnect_tid);
   1017 }
   1018 
   1019 /* ── config file ───────────────────────────────────────────────────────────── */
   1020 
   1021 static int cfg_bool(const char *v) {
   1022     return strcasecmp(v,"true")==0||strcasecmp(v,"yes")==0||
   1023            strcasecmp(v,"1")==0||strcasecmp(v,"on")==0;
   1024 }
   1025 
   1026 /* add a server entry with defaults; returns its index */
   1027 static int srv_alloc(void) {
   1028     if (g_srv_count>=MAX_SERVERS) return -1;
   1029     int si=g_srv_count++;
   1030     Server *s=&g_srv[si];
   1031     memset(s,0,sizeof(Server));
   1032     strncpy(s->host,"irc.libera.chat",MAX_HOST-1);
   1033     s->port=6697;
   1034     strncpy(s->nick,"sirc_user",MAX_NICK-1);
   1035     s->use_tls=1;
   1036     s->sock=-1;
   1037     pthread_mutex_init(&s->send_lock,NULL);
   1038     return si;
   1039 }
   1040 
   1041 static void srv_add_autojoin(int si, const char *chanlist) {
   1042     char tmp[512]; strncpy(tmp,chanlist,511);
   1043     char *tok=strtok(tmp,",");
   1044     while (tok) {
   1045         while (*tok==' ') tok++;
   1046         if (*tok && g_srv[si].autojoin_count<MAX_AUTOJOIN) {
   1047             char ch[MAX_CHAN]; strncpy(ch,tok,MAX_CHAN-1);
   1048             if (ch[0]!='#') { memmove(ch+1,ch,strlen(ch)+1); ch[0]='#'; }
   1049             strncpy(g_srv[si].autojoin[g_srv[si].autojoin_count++],ch,MAX_CHAN-1);
   1050         }
   1051         tok=strtok(NULL,",");
   1052     }
   1053 }
   1054 
   1055 static void load_config(const char *path) {
   1056     char candidates[2][MAX_HOST];
   1057     const char *home=getenv("HOME");
   1058     if (home) {
   1059         snprintf(candidates[0],MAX_HOST,"%s/.ircrc",home);
   1060         snprintf(candidates[1],MAX_HOST,"%s/.config/irc/ircrc",home);
   1061     }
   1062     FILE *f=NULL;
   1063     if (path) f=fopen(path,"r");
   1064     else for (int i=0;i<2&&!f;i++) f=fopen(candidates[i],"r");
   1065     if (!f) return;
   1066 
   1067     /* defaults that apply before any [server] block */
   1068     char def_nick[MAX_NICK]="sirc_user";
   1069 
   1070     /* current server being parsed; -1 = not inside a [server] block */
   1071     int cur_si=-1;
   1072 
   1073     char line[512];
   1074     while (fgets(line,sizeof(line),f)) {
   1075         line[strcspn(line,"\r\n")]='\0';
   1076         char *p=line;
   1077         while (*p==' '||*p=='\t') p++;
   1078         if (!*p||*p=='#') continue;
   1079 
   1080         /* [server] section header */
   1081         if (strcmp(p,"[server]")==0) {
   1082             cur_si=srv_alloc();
   1083             if (cur_si>=0) strncpy(g_srv[cur_si].nick,def_nick,MAX_NICK-1);
   1084             continue;
   1085         }
   1086 
   1087         char *eq=strchr(p,'='); if (!eq) continue;
   1088         *eq='\0';
   1089         char *key=p, *val=eq+1;
   1090         char *ke=key+strlen(key)-1;
   1091         while (ke>key&&(*ke==' '||*ke=='\t')) *ke--='\0';
   1092         while (*val==' '||*val=='\t') val++;
   1093         char *ve=val+strlen(val)-1;
   1094         while (ve>val&&(*ve==' '||*ve=='\t')) *ve--='\0';
   1095 
   1096         if (cur_si<0) {
   1097             /* global defaults */
   1098             if (strcmp(key,"nick")==0) strncpy(def_nick,val,MAX_NICK-1);
   1099             else if (strcmp(key,"ignore")==0) {
   1100                 char tmp[512]; strncpy(tmp,val,511);
   1101                 char *tok=strtok(tmp,",");
   1102                 while (tok) { while(*tok==' ')tok++; if(*tok)ignore_add(tok); tok=strtok(NULL,","); }
   1103             }
   1104         } else {
   1105             Server *s=&g_srv[cur_si];
   1106             if (strcmp(key,"host")==0)       strncpy(s->host,val,MAX_HOST-1);
   1107             else if (strcmp(key,"port")==0)  s->port=atoi(val);
   1108             else if (strcmp(key,"nick")==0)  strncpy(s->nick,val,MAX_NICK-1);
   1109             else if (strcmp(key,"tls")==0)   s->use_tls=cfg_bool(val);
   1110             else if (strcmp(key,"sasl_user")==0) strncpy(s->sasl_user,val,MAX_NICK-1);
   1111             else if (strcmp(key,"sasl_pass")==0) strncpy(s->sasl_pass,val,255);
   1112             else if (strcmp(key,"channel")==0)   srv_add_autojoin(cur_si,val);
   1113         }
   1114     }
   1115     fclose(f);
   1116 }
   1117 
   1118 /* ── URL detection ─────────────────────────────────────────────────────────── */
   1119 
   1120 static int find_url(const char *str, int start, int *len) {
   1121     for (int i=start; str[i]; i++) {
   1122         if (strncmp(str+i,"http://",7)==0||strncmp(str+i,"https://",8)==0||
   1123             strncmp(str+i,"www.",4)==0) {
   1124             int j=i;
   1125             while (str[j]&&str[j]!=' '&&str[j]!='\t'&&
   1126                    str[j]!='"'&&str[j]!='\''&&str[j]!='<') j++;
   1127             *len=j-i; return i;
   1128         }
   1129     }
   1130     return -1;
   1131 }
   1132 
   1133 /* ── draw routines ─────────────────────────────────────────────────────────── */
   1134 
   1135 static void draw_status_bar(void) {
   1136     int H,W; getmaxyx(stdscr,H,W); (void)H;
   1137     werase(win_status);
   1138     wbkgd(win_status,COLOR_PAIR(C_HEADER));
   1139     Channel *ch=&g_chans[g_active];
   1140     /* topic starts after the channel panel, truncated on the right */
   1141     int avail = W - CHAN_W - 1;
   1142     if (avail < 1) avail = 1;
   1143     char row[512];
   1144     if (ch->topic[0])
   1145         snprintf(row, sizeof(row), " %.*s", avail-1, ch->topic);
   1146     else
   1147         row[0]='\0';
   1148     int l=strlen(row);
   1149     while (l < avail) row[l++]=' ';
   1150     row[avail]='\0';
   1151     mvwaddnstr(win_status, 0, CHAN_W, row, avail);
   1152     wnoutrefresh(win_status);
   1153 }
   1154 
   1155 static void draw_channels(void) {
   1156     int H,W; getmaxyx(win_chan,H,W); (void)W;
   1157     werase(win_chan);
   1158     wattron(win_chan,COLOR_PAIR(C_BORDER)|A_BOLD);
   1159     mvwaddnstr(win_chan,0,0," CHANNELS",CHAN_W-1);
   1160     wattroff(win_chan,COLOR_PAIR(C_BORDER)|A_BOLD);
   1161 
   1162     int row=2;
   1163     for (int si=0; si<g_srv_count && row<H-1; si++) {
   1164         /* server header: full hostname, ~ prefix if disconnected */
   1165         char hdr[CHAN_W];
   1166         const char *conn_mark = g_srv[si].connected ? "" : "~";
   1167         snprintf(hdr,sizeof(hdr),"%s%.*s",conn_mark,(int)(CHAN_W-2),g_srv[si].host);
   1168         int hl=strlen(hdr);
   1169         while (hl<CHAN_W-1) hdr[hl++]=' '; hdr[CHAN_W-1]='\0';
   1170         wattron(win_chan,COLOR_PAIR(C_SERVER_HDR)|A_BOLD);
   1171         mvwaddnstr(win_chan,row,0,hdr,CHAN_W-1);
   1172         wattroff(win_chan,COLOR_PAIR(C_SERVER_HDR)|A_BOLD);
   1173         row++;
   1174 
   1175         /* show *status* first, then regular channels */
   1176         for (int pass=0; pass<2 && row<H-1; pass++) {
   1177             for (int ci=0; ci<g_chan_count && row<H-1; ci++) {
   1178                 if (g_chans[ci].srv!=si) continue;
   1179                 int is_status=(strcmp(g_chans[ci].name,"*status*")==0);
   1180                 if (pass==0 && !is_status) continue;  /* pass 0: status only */
   1181                 if (pass==1 &&  is_status) continue;  /* pass 1: channels only */
   1182                 Channel *ch=&g_chans[ci];
   1183                 int attr; const char *pfx;
   1184                 if (ci==g_active) {
   1185                     attr=COLOR_PAIR(C_CHAN_SEL)|A_BOLD; pfx="> ";
   1186                 } else if (is_status) {
   1187                     attr=COLOR_PAIR(C_STATUS)|A_DIM; pfx="  ";
   1188                 } else if (ch->mention) {
   1189                     attr=COLOR_PAIR(C_MENTION_CHAN)|A_BOLD; pfx="! ";
   1190                 } else if (ch->unread) {
   1191                     attr=COLOR_PAIR(C_UNREAD); pfx="+ ";
   1192                 } else {
   1193                     attr=COLOR_PAIR(C_CHAN); pfx="  ";
   1194                 }
   1195                 char label[CHAN_W+3];
   1196                 snprintf(label,sizeof(label)," %s%.*s",pfx,CHAN_W-4,ch->name);
   1197                 int ll=strlen(label);
   1198                 while (ll<CHAN_W-1) label[ll++]=' '; label[CHAN_W-1]='\0';
   1199                 wattron(win_chan,attr);
   1200                 mvwaddnstr(win_chan,row,0,label,CHAN_W-1);
   1201                 wattroff(win_chan,attr);
   1202                 row++;
   1203             }
   1204         }
   1205     }
   1206 
   1207     /* bottom: active channel name */
   1208     char hint[CHAN_W];
   1209     snprintf(hint,sizeof(hint),"%.*s",CHAN_W-1,g_chans[g_active].name);
   1210     int hl=strlen(hint);
   1211     while (hl<CHAN_W-1) hint[hl++]=' '; hint[CHAN_W-1]='\0';
   1212     wattron(win_chan,COLOR_PAIR(C_STATUS)|A_DIM);
   1213     mvwaddnstr(win_chan,H-1,0,hint,CHAN_W-1);
   1214     wattroff(win_chan,COLOR_PAIR(C_STATUS)|A_DIM);
   1215 
   1216     wnoutrefresh(win_chan);
   1217 }
   1218 
   1219 static void draw_chat(void) {
   1220     int chat_h,chat_w; getmaxyx(win_chat,chat_h,chat_w);
   1221     werase(win_chat);
   1222     Channel *ch=&g_chans[g_active];
   1223     if (ch->line_count==0) { wnoutrefresh(win_chat); return; }
   1224 
   1225     typedef struct { int flag; char text[MAX_LINE]; int has_url; } FlatLine;
   1226     static FlatLine flat[MAX_HISTORY*4];
   1227     int flat_count=0;
   1228 
   1229     for (int li=0; li<ch->line_count && flat_count<(int)(sizeof(flat)/sizeof(flat[0])); li++) {
   1230         Line *ln=chan_line(ch,li); if (!ln) continue;
   1231         char full[MAX_LINE];
   1232         snprintf(full,sizeof(full),"%s %s",ln->ts,ln->text);
   1233         int plen=strlen(ln->ts)+1;
   1234         int avail=chat_w-plen; if(avail<4)avail=4;
   1235         const char *p=full; int is_first=1;
   1236         while (*p && flat_count<(int)(sizeof(flat)/sizeof(flat[0]))) {
   1237             int w=is_first?chat_w:avail;
   1238             FlatLine *fl=&flat[flat_count++];
   1239             fl->flag=ln->flag;
   1240             fl->has_url=(find_url(ln->text,0,&(int){0})>=0);
   1241             if ((int)strlen(p)<=w) {
   1242                 if (!is_first) { memset(fl->text,' ',plen); strncpy(fl->text+plen,p,MAX_LINE-plen-1); }
   1243                 else strncpy(fl->text,p,MAX_LINE-1);
   1244                 break;
   1245             }
   1246             int cut=w;
   1247             while (cut>0&&p[cut]!=' ') cut--;
   1248             if (cut==0) cut=w;
   1249             if (!is_first) { memset(fl->text,' ',plen); memcpy(fl->text+plen,p,cut); fl->text[plen+cut]='\0'; }
   1250             else { memcpy(fl->text,p,cut); fl->text[cut]='\0'; }
   1251             is_first=0; p+=cut; while(*p==' ')p++;
   1252         }
   1253     }
   1254 
   1255     int max_scroll=flat_count>chat_h?flat_count-chat_h:0;
   1256     if (ch->scroll>max_scroll) ch->scroll=max_scroll;
   1257     if (ch->scroll<0) ch->scroll=0;
   1258     int vis_start=flat_count-chat_h-ch->scroll;
   1259     if (vis_start<0) vis_start=0;
   1260     int vis_end=vis_start+chat_h;
   1261     if (vis_end>flat_count) vis_end=flat_count;
   1262 
   1263     for (int i=vis_start; i<vis_end; i++) {
   1264         int row=i-vis_start;
   1265         FlatLine *fl=&flat[i];
   1266         if (fl->flag==F_MSG) {
   1267             wattron(win_chat,COLOR_PAIR(C_STATUS));
   1268             mvwaddnstr(win_chat,row,0,fl->text,chat_w-1);
   1269             wattroff(win_chat,COLOR_PAIR(C_STATUS));
   1270             const char *lt=strchr(fl->text,'<');
   1271             const char *gt=lt?strchr(lt,'>'):NULL;
   1272             if (lt&&gt) {
   1273                 int ns=(int)(lt-fl->text), nl=(int)(gt-lt+1);
   1274                 char nn[MAX_NICK]; int nlen=nl-2; if(nlen>=MAX_NICK)nlen=MAX_NICK-1;
   1275                 memcpy(nn,lt+1,nlen); nn[nlen]='\0';
   1276                 mvwchgat(win_chat,row,ns,nl,A_BOLD,nick_colour(nn),NULL);
   1277             }
   1278         } else {
   1279             int attr;
   1280             switch(fl->flag) {
   1281                 case F_ME:      attr=COLOR_PAIR(C_ME)|A_BOLD;    break;
   1282                 case F_MENTION: attr=COLOR_PAIR(C_MENTION)|A_BOLD; break;
   1283                 case F_JOIN:    attr=COLOR_PAIR(C_JOIN);          break;
   1284                 case F_ACTION:  attr=COLOR_PAIR(C_ACTION)|A_ITALIC; break;
   1285                 default:        attr=COLOR_PAIR(C_STATUS);        break;
   1286             }
   1287             wattron(win_chat,attr);
   1288             mvwaddnstr(win_chat,row,0,fl->text,chat_w-1);
   1289             wattroff(win_chat,attr);
   1290         }
   1291         if (fl->has_url) {
   1292             int ustart=0,ulen,us;
   1293             while ((us=find_url(fl->text,ustart,&ulen))>=0) {
   1294                 if (us>=chat_w-1) break;
   1295                 int ue=us+ulen; if(ue>chat_w-2)ue=chat_w-2;
   1296                 mvwchgat(win_chat,row,us,ue-us,A_UNDERLINE,C_URL,NULL);
   1297                 ustart=us+ulen;
   1298             }
   1299         }
   1300     }
   1301 
   1302     if (ch->scroll>0) {
   1303         char tag[16]; snprintf(tag,sizeof(tag),"^%d",ch->scroll);
   1304         wattron(win_chat,COLOR_PAIR(C_STATUS)|A_DIM);
   1305         mvwaddstr(win_chat,0,chat_w-(int)strlen(tag)-1,tag);
   1306         wattroff(win_chat,COLOR_PAIR(C_STATUS)|A_DIM);
   1307     }
   1308     wnoutrefresh(win_chat);
   1309 }
   1310 
   1311 static void draw_users(void) {
   1312     int H,W; getmaxyx(win_users,H,W); (void)W;
   1313     werase(win_users);
   1314     Channel *ch=&g_chans[g_active];
   1315     char hdr[32]; snprintf(hdr,sizeof(hdr)," USERS (%d)",ch->user_count);
   1316     wattron(win_users,COLOR_PAIR(C_BORDER)|A_BOLD);
   1317     mvwaddnstr(win_users,0,1,hdr,USER_W-2);
   1318     wattroff(win_users,COLOR_PAIR(C_BORDER)|A_BOLD);
   1319     for (int i=0; i<ch->user_count; i++) {
   1320         int y=i+2; if(y>=H-1) break;
   1321         const char *entry = ch->users[i];
   1322         char   mode  = entry[0];
   1323         const char *bare = entry+1;   /* nick without prefix */
   1324         /* colour the mode symbol */
   1325         if (mode=='@'||mode=='~'||mode=='&') {
   1326             wattron(win_users, COLOR_PAIR(C_MENTION)|A_BOLD);
   1327             mvwaddnstr(win_users,y,1,&mode,1);
   1328             wattroff(win_users, COLOR_PAIR(C_MENTION)|A_BOLD);
   1329         } else if (mode=='+'||mode=='%') {
   1330             wattron(win_users, COLOR_PAIR(C_ME)|A_BOLD);
   1331             mvwaddnstr(win_users,y,1,&mode,1);
   1332             wattroff(win_users, COLOR_PAIR(C_ME)|A_BOLD);
   1333         } else {
   1334             mvwaddch(win_users,y,1,' ');
   1335         }
   1336         /* nick in normal white */
   1337         wattron(win_users,COLOR_PAIR(C_CHAN));
   1338         mvwaddnstr(win_users,y,2,bare,USER_W-3);
   1339         wattroff(win_users,COLOR_PAIR(C_CHAN));
   1340     }
   1341     wnoutrefresh(win_users);
   1342 }
   1343 
   1344 static void draw_input(void) {
   1345     int iw; getmaxyx(win_input,(int){0},iw);
   1346     werase(win_input);
   1347     wbkgd(win_input,COLOR_PAIR(C_INPUT));
   1348 
   1349     /* draw "> " prompt */
   1350     wattron(win_input, COLOR_PAIR(C_BORDER)|A_BOLD);
   1351     mvwaddstr(win_input, 0, 0, "> ");
   1352     wattroff(win_input, COLOR_PAIR(C_BORDER)|A_BOLD);
   1353 
   1354     /* available width after prompt */
   1355     int pfx = 2;
   1356     int avail = iw - pfx - 1;
   1357     if (avail < 1) avail = 1;
   1358 
   1359     /* scroll text so cursor stays visible */
   1360     int disp = 0, cx = g_input_cur;
   1361     if (g_input_cur > avail) {
   1362         disp = g_input_cur - avail;
   1363         cx   = avail;
   1364     }
   1365 
   1366     wattron(win_input, COLOR_PAIR(C_INPUT));
   1367     mvwaddnstr(win_input, 0, pfx, g_input + disp, avail);
   1368     wattroff(win_input, COLOR_PAIR(C_INPUT));
   1369 
   1370     /* place terminal cursor — curs_set(2) gives blinking block on most terms */
   1371     wmove(win_input, 0, pfx + cx);
   1372     wnoutrefresh(win_input);
   1373 }
   1374 
   1375 static void draw_all(void) {
   1376     draw_status_bar(); draw_channels(); draw_chat();
   1377     draw_users(); draw_input(); doupdate();
   1378 }
   1379 
   1380 /* ── window setup ──────────────────────────────────────────────────────────── */
   1381 
   1382 static void init_colors(void) {
   1383     start_color(); use_default_colors();
   1384     init_pair(C_BORDER,      COLOR_CYAN,    -1);
   1385     init_pair(C_HEADER,      COLOR_BLACK,   COLOR_CYAN);
   1386     init_pair(C_ME,          COLOR_YELLOW,  -1);
   1387     init_pair(C_JOIN,        COLOR_BLACK+8, -1);
   1388     init_pair(C_MENTION,     COLOR_RED,     -1);
   1389     init_pair(C_STATUS,      COLOR_WHITE,   -1);
   1390     init_pair(C_INPUT,       COLOR_WHITE,   COLOR_BLACK);
   1391     init_pair(C_CHAN_SEL,    COLOR_BLACK,   COLOR_WHITE);
   1392     init_pair(C_CHAN,        COLOR_WHITE,   -1);
   1393     init_pair(C_UNREAD,      COLOR_YELLOW,  -1);
   1394     init_pair(C_MENTION_CHAN,COLOR_RED,     -1);
   1395     init_pair(C_URL,         COLOR_BLUE,    -1);
   1396     init_pair(C_ACTION,      COLOR_MAGENTA, -1);
   1397     init_pair(C_SERVER_HDR,  COLOR_CYAN,    -1);
   1398     static const int nick_cols[C_NICK_COUNT]={
   1399         COLOR_GREEN,COLOR_CYAN,COLOR_MAGENTA,COLOR_YELLOW,
   1400         COLOR_WHITE,COLOR_RED, COLOR_BLUE,   COLOR_GREEN
   1401     };
   1402     for (int i=0;i<C_NICK_COUNT;i++)
   1403         init_pair(C_NICK_BASE+i,nick_cols[i],-1);
   1404 }
   1405 
   1406 static void build_windows(void) {
   1407     int H,W; getmaxyx(stdscr,H,W);
   1408     int chat_w=W-CHAN_W-USER_W, chat_h=H-3;
   1409     if(win_chan)   delwin(win_chan);
   1410     if(win_chat)   delwin(win_chat);
   1411     if(win_users)  delwin(win_users);
   1412     if(win_input)  delwin(win_input);
   1413     if(win_status) delwin(win_status);
   1414     win_status=newwin(1,     W,      0,  0);
   1415     win_chan   =newwin(H,     CHAN_W, 0,  0);
   1416     win_chat   =newwin(chat_h,chat_w,1,  CHAN_W);
   1417     win_users  =newwin(H,     USER_W,0,  CHAN_W+chat_w);
   1418     win_input  =newwin(1,     chat_w,H-1,CHAN_W);
   1419 }
   1420 
   1421 /* ── event handler ─────────────────────────────────────────────────────────── */
   1422 
   1423 static void do_rejoin_channels(int si) {
   1424     int any=0;
   1425     for (int i=0;i<g_chan_count;i++) {
   1426         if (g_chans[i].srv==si && g_chans[i].name[0]=='#') {
   1427             srv_sendf(si,"JOIN %s",g_chans[i].name); any=1;
   1428         }
   1429     }
   1430     if (!any) {
   1431         for (int j=0;j<g_srv[si].autojoin_count;j++)
   1432             srv_sendf(si,"JOIN %s",g_srv[si].autojoin[j]);
   1433     }
   1434 }
   1435 
   1436 static void handle_event(const Event *ev) {
   1437     int si=ev->srv;
   1438     Server *s=&g_srv[si];
   1439 
   1440     switch (ev->type) {
   1441 
   1442     case EV_CONNECTED:
   1443         srv_status_msg(si,"Connected.");
   1444         do_rejoin_channels(si);
   1445         break;
   1446 
   1447     case EV_STATUS:
   1448         srv_status_msg(si,ev->text);
   1449         break;
   1450 
   1451     case EV_ERROR:
   1452         srv_status_fmt(si,"[ERR] %s",ev->text);
   1453         break;
   1454 
   1455     case EV_RECONNECT:
   1456         if (!s->connected) schedule_reconnect(si);
   1457         break;
   1458 
   1459     case EV_SERVER_TEXT:
   1460         if (ev->chan[0]) {
   1461             int ci=chan_find(si,ev->chan);
   1462             if (ci<0) ci=chan_add(si,ev->chan);
   1463             chan_addline(&g_chans[ci],F_STATUS,ev->text);
   1464             if (ci==g_active) g_chans[ci].unread=0;
   1465         } else {
   1466             srv_status_msg(si,ev->text);
   1467         }
   1468         break;
   1469 
   1470     case EV_RAW:
   1471         srv_status_msg(si,ev->text);
   1472         break;
   1473 
   1474     case EV_PRIVMSG: {
   1475         if (is_ignored(ev->nick)) break;
   1476         const char *target=ev->chan, *text=ev->text;
   1477         const char *dest=(target[0]=='#')?target:ev->nick;
   1478         int ci=chan_find(si,dest); if(ci<0) ci=chan_add(si,dest);
   1479 
   1480         if (text[0]=='\x01'&&strncmp(text+1,"ACTION",6)==0) {
   1481             const char *act=text+8;
   1482             char msg[MAX_LINE];
   1483             int alen=strlen(act); if(alen>0&&act[alen-1]=='\x01')alen--;
   1484             snprintf(msg,sizeof(msg),"* %s %.*s",ev->nick,alen,act);
   1485             int flag=(strcasestr(msg,s->nick)!=NULL)?F_MENTION:F_ACTION;
   1486             chan_addline(&g_chans[ci],flag,msg);
   1487             if(ci!=g_active&&flag==F_MENTION) g_chans[ci].mention=1;
   1488             if(ci==g_active) g_chans[ci].unread=0;
   1489             break;
   1490         }
   1491         char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<%s> %s",ev->nick,text);
   1492         int flag;
   1493         if (strcmp(ev->nick,s->nick)==0) flag=F_ME;
   1494         else if (strcasestr(text,s->nick)!=NULL) flag=F_MENTION;
   1495         else flag=F_MSG;
   1496         chan_addline(&g_chans[ci],flag,msg);
   1497         if(ci!=g_active&&flag==F_MENTION) g_chans[ci].mention=1;
   1498         if(ci==g_active) g_chans[ci].unread=0;
   1499         break;
   1500     }
   1501 
   1502     case EV_JOIN: {
   1503         int ci=chan_find(si,ev->chan); if(ci<0) ci=chan_add(si,ev->chan);
   1504         if (strcmp(ev->nick,s->nick)==0) {
   1505             g_chans[ci].user_count=0;
   1506             g_active=ci;
   1507             g_chans[ci].unread=0; g_chans[ci].mention=0;
   1508             srv_status_fmt(si,"Joined %s",ev->chan);
   1509         } else {
   1510             char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"-> %s joined",ev->nick);
   1511             chan_addline(&g_chans[ci],F_JOIN,msg);
   1512             chan_adduser(&g_chans[ci], 0, ev->nick);
   1513         }
   1514         break;
   1515     }
   1516 
   1517     case EV_PART: {
   1518         int ci=chan_find(si,ev->chan); if(ci<0) break;
   1519         char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<- %s parted (%s)",ev->nick,ev->text);
   1520         chan_addline(&g_chans[ci],F_JOIN,msg);
   1521         if (strcmp(ev->nick,s->nick)==0) chan_remove(ci);
   1522         else chan_removeuser(&g_chans[ci],ev->nick);
   1523         break;
   1524     }
   1525 
   1526     case EV_QUIT_MSG:
   1527         for (int i=0;i<g_chan_count;i++) {
   1528             if (g_chans[i].srv!=si) continue;
   1529             int found=0;
   1530             for (int j=0;j<g_chans[i].user_count;j++)
   1531                 if (strcasecmp(g_chans[i].users[j],ev->nick)==0){found=1;break;}
   1532             if (found) {
   1533                 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<- %s quit (%s)",ev->nick,ev->text);
   1534                 chan_addline(&g_chans[i],F_JOIN,msg);
   1535                 chan_removeuser(&g_chans[i],ev->nick);
   1536             }
   1537         }
   1538         break;
   1539 
   1540     case EV_NICK_CHANGE:
   1541         if (strcmp(ev->nick,s->nick)==0) {
   1542             strncpy(s->nick,ev->extra,MAX_NICK-1);
   1543             srv_status_fmt(si,"You are now known as %s",s->nick);
   1544         }
   1545         for (int i=0;i<g_chan_count;i++) {
   1546             if (g_chans[i].srv!=si) continue;
   1547             for (int j=0;j<g_chans[i].user_count;j++) {
   1548                 /* skip stored mode prefix when comparing */
   1549                 const char *sn=g_chans[i].users[j];
   1550                 if (*sn=='@'||*sn=='+'||*sn=='~'||*sn=='&'||*sn=='%'||*sn==' ') sn++;
   1551                 if (strcasecmp(sn,ev->nick)==0) {
   1552                     char mode=g_chans[i].users[j][0];
   1553                     g_chans[i].users[j][0]=mode;
   1554                     strncpy(g_chans[i].users[j]+1,ev->extra,MAX_NICK-2);
   1555                     char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"~ %s is now %s",ev->nick,ev->extra);
   1556                     chan_addline(&g_chans[i],F_JOIN,msg);
   1557                     break;
   1558                 }
   1559             }
   1560         }
   1561         break;
   1562 
   1563     case EV_NAMES: {
   1564         int ci=chan_find(si,ev->chan); if(ci<0) break;
   1565         chan_adduser(&g_chans[ci], ev->extra[0], ev->nick);
   1566         chan_sort_users(&g_chans[ci]);
   1567         break;
   1568     }
   1569 
   1570     case EV_KICK: {
   1571         int ci=chan_find(si,ev->chan); if(ci<0) break;
   1572         char msg[MAX_LINE];
   1573         snprintf(msg,sizeof(msg),"X %s was kicked by %s (%s)",ev->extra,ev->nick,ev->text);
   1574         chan_addline(&g_chans[ci],F_JOIN,msg);
   1575         chan_removeuser(&g_chans[ci],ev->extra);
   1576         break;
   1577     }
   1578 
   1579     case EV_TOPIC: {
   1580         int ci=chan_find(si,ev->chan); if(ci<0) break;
   1581         strip_irc_fmt(ev->text, g_chans[ci].topic, MAX_LINE);
   1582         break;
   1583     }
   1584 
   1585     }
   1586 }
   1587 
   1588 /* ── tab completion ────────────────────────────────────────────────────────── */
   1589 
   1590 static void tab_reset(void) { g_tab_active=0; g_tab_count=0; g_tab_idx=0; }
   1591 
   1592 static void tab_complete(void) {
   1593     if (g_active<0||g_active>=g_chan_count) return;
   1594     Channel *ch=&g_chans[g_active];
   1595     if (!g_tab_active) {
   1596         int ws=g_input_cur;
   1597         while (ws>0&&g_input[ws-1]!=' '&&g_input[ws-1]!='\t') ws--;
   1598         char prefix[MAX_NICK]; int plen=g_input_cur-ws;
   1599         if (plen<=0) return;
   1600         strncpy(prefix,g_input+ws,plen); prefix[plen]='\0';
   1601         char *pp=prefix;
   1602         while(*pp=='@'||*pp=='+'||*pp=='~'||*pp=='&'||*pp=='%') pp++;
   1603         if (!*pp) return;
   1604         g_tab_count=0;
   1605         for (int i=0;i<ch->user_count;i++) {
   1606             const char *bare=ch->users[i]+1;  /* skip mode prefix */
   1607             if (str_icase_starts(bare,pp))
   1608                 strncpy(g_tab_matches[g_tab_count++],bare,MAX_NICK-1);
   1609         }
   1610         if (!g_tab_count) return;
   1611         g_tab_idx=0; g_tab_word_start=ws; g_tab_active=1;
   1612     } else {
   1613         g_tab_idx=(g_tab_idx+1)%g_tab_count;
   1614     }
   1615     const char *nick=g_tab_matches[g_tab_idx];
   1616     char repl[MAX_NICK+3];
   1617     snprintf(repl,sizeof(repl),g_tab_word_start==0?"%s: ":"%s ",nick);
   1618     int rlen=strlen(repl);
   1619     char newbuf[MAX_INPUT];
   1620     int tail=g_input_len-g_input_cur; if(tail<0)tail=0;
   1621     memcpy(newbuf,g_input,g_tab_word_start);
   1622     memcpy(newbuf+g_tab_word_start,repl,rlen);
   1623     memcpy(newbuf+g_tab_word_start+rlen,g_input+g_input_cur,tail);
   1624     int newlen=g_tab_word_start+rlen+tail;
   1625     if(newlen>=MAX_INPUT)newlen=MAX_INPUT-1;
   1626     memcpy(g_input,newbuf,newlen); g_input[newlen]='\0';
   1627     g_input_len=newlen; g_input_cur=g_tab_word_start+rlen;
   1628 }
   1629 
   1630 /* ── command processing ────────────────────────────────────────────────────── */
   1631 
   1632 /* current server index = server of the active channel */
   1633 static int active_srv(void) {
   1634     if (g_active>=0&&g_active<g_chan_count) return g_chans[g_active].srv;
   1635     return 0;
   1636 }
   1637 
   1638 static void process_input(const char *text) {
   1639     int si=active_srv();
   1640     Server *s=&g_srv[si];
   1641 
   1642     if (text[0]!='/') {
   1643         if (g_active<0) { srv_status_msg(si,"Use /join #channel first."); return; }
   1644         const char *cn=g_chans[g_active].name;
   1645         if (cn[0]=='*') { srv_status_msg(si,"Use /join #channel first."); return; }
   1646         if (!s->connected) { srv_status_msg(si,"[not connected]"); return; }
   1647         srv_sendf(si,"PRIVMSG %s :%s",cn,text);
   1648         char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<%s> %s",s->nick,text);
   1649         chan_addline(&g_chans[g_active],F_ME,msg);
   1650         g_chans[g_active].unread=0;
   1651         return;
   1652     }
   1653 
   1654     char cmd[64],arg[MAX_INPUT];
   1655     cmd[0]='\0'; arg[0]='\0';
   1656     const char *sp=strchr(text+1,' ');
   1657     if (sp) {
   1658         int cl=(int)(sp-text); if(cl>=(int)sizeof(cmd))cl=sizeof(cmd)-1;
   1659         memcpy(cmd,text,cl); cmd[cl]='\0';
   1660         const char *a=sp+1; while(*a==' ')a++;
   1661         strncpy(arg,a,MAX_INPUT-1);
   1662     } else strncpy(cmd,text,sizeof(cmd)-1);
   1663     for (char *c=cmd;*c;c++) *c=tolower((unsigned char)*c);
   1664 
   1665     if (strcmp(cmd,"/join")==0) {
   1666         char ch[MAX_CHAN]; strncpy(ch,arg[0]?arg:g_srv[si].autojoin_count?g_srv[si].autojoin[0]:"#general",MAX_CHAN-1);
   1667         if(ch[0]!='#'){memmove(ch+1,ch,strlen(ch)+1);ch[0]='#';}
   1668         chan_add(si,ch);
   1669         if(s->connected) srv_sendf(si,"JOIN %s",ch);
   1670         else srv_status_msg(si,"[not connected]");
   1671 
   1672     } else if (strcmp(cmd,"/part")==0) {
   1673         const char *ch=arg[0]?arg:g_chans[g_active].name;
   1674         if(s->connected) srv_sendf(si,"PART %s :bye",ch);
   1675         else srv_status_msg(si,"[not connected]");
   1676 
   1677     } else if (strcmp(cmd,"/cycle")==0) {
   1678         const char *ch=arg[0]?arg:g_chans[g_active].name;
   1679         if(ch[0]!='#'){srv_status_msg(si,"Not in a channel.");return;}
   1680         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1681         srv_sendf(si,"PART %s :cycling",ch); srv_sendf(si,"JOIN %s",ch);
   1682 
   1683     } else if (strcmp(cmd,"/nick")==0) {
   1684         if(!arg[0]) {
   1685             srv_status_fmt(si,"Your nick is: %s",s->nick);
   1686         } else {
   1687             if(s->connected) srv_sendf(si,"NICK %s",arg);
   1688             else strncpy(s->nick,arg,MAX_NICK-1);
   1689             srv_status_fmt(si,"Nick change requested: %s -> %s",s->nick,arg);
   1690         }
   1691 
   1692     } else if (strcmp(cmd,"/msg")==0) {
   1693         char tgt[MAX_NICK],msg[MAX_LINE];
   1694         if(sscanf(arg,"%63s %[^\n]",tgt,msg)==2) {
   1695             chan_add(si,tgt);
   1696             if(s->connected) srv_sendf(si,"PRIVMSG %s :%s",tgt,msg);
   1697             int ci=chan_find(si,tgt);
   1698             if(ci>=0){ char full[MAX_LINE]; snprintf(full,sizeof(full),"<%s> %s",s->nick,msg); chan_addline(&g_chans[ci],F_ME,full); }
   1699         }
   1700 
   1701     } else if (strcmp(cmd,"/me")==0) {
   1702         const char *cn=g_chans[g_active].name;
   1703         if(cn[0]=='*'){srv_status_msg(si,"Not in a channel.");return;}
   1704         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1705         srv_sendf(si,"PRIVMSG %s :\x01""ACTION %s\x01",cn,arg);
   1706         char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"* %s %s",s->nick,arg);
   1707         chan_addline(&g_chans[g_active],F_ACTION,msg);
   1708 
   1709     } else if (strcmp(cmd,"/notice")==0) {
   1710         char tgt[MAX_NICK],msg[MAX_LINE];
   1711         if(sscanf(arg,"%63s %[^\n]",tgt,msg)==2) {
   1712             if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1713             srv_sendf(si,"NOTICE %s :%s",tgt,msg);
   1714             srv_status_fmt(si,"->%s<- %s",tgt,msg);
   1715         } else srv_status_msg(si,"Usage: /notice <target> <message>");
   1716 
   1717     } else if (strcmp(cmd,"/ctcp")==0) {
   1718         char tgt[MAX_NICK],ctcp[MAX_LINE];
   1719         if(sscanf(arg,"%63s %[^\n]",tgt,ctcp)>=1) {
   1720             if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1721             for(char *c=ctcp;*c&&*c!=' ';c++) *c=toupper((unsigned char)*c);
   1722             srv_sendf(si,"PRIVMSG %s :\x01%s\x01",tgt,ctcp);
   1723             srv_status_fmt(si,"[ctcp] %s -> %s",ctcp,tgt);
   1724         } else srv_status_msg(si,"Usage: /ctcp <nick> <command>");
   1725 
   1726     } else if (strcmp(cmd,"/names")==0) {
   1727         const char *ch=arg[0]?arg:g_chans[g_active].name;
   1728         if(ch[0]=='*'){srv_status_msg(si,"Not in a channel.");return;}
   1729         if(s->connected) srv_sendf(si,"NAMES %s",ch);
   1730         else {
   1731             int ci=chan_find(si,ch); if(ci<0){srv_status_msg(si,"Channel not found.");return;}
   1732             srv_status_fmt(si,"[names] %s (%d users):",ch,g_chans[ci].user_count);
   1733             char line[MAX_LINE]; line[0]='\0';
   1734             for(int i=0;i<g_chans[ci].user_count;i++){
   1735                 if(strlen(line)+strlen(g_chans[ci].users[i])+2>60){
   1736                     srv_status_fmt(si,"[names]  %s",line); line[0]='\0';
   1737                 }
   1738                 if(line[0]) strncat(line," ",MAX_LINE-1);
   1739                 strncat(line,g_chans[ci].users[i],MAX_LINE-1);
   1740             }
   1741             if(line[0]) srv_status_fmt(si,"[names]  %s",line);
   1742         }
   1743 
   1744     } else if (strcmp(cmd,"/topic")==0) {
   1745         const char *ch=g_chans[g_active].name;
   1746         if(ch[0]=='*'){srv_status_msg(si,"Not in a channel.");return;}
   1747         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1748         if(arg[0]) srv_sendf(si,"TOPIC %s :%s",ch,arg);
   1749         else srv_sendf(si,"TOPIC %s",ch);
   1750 
   1751     } else if (strcmp(cmd,"/whois")==0) {
   1752         if(!arg[0]){srv_status_msg(si,"Usage: /whois <nick>");return;}
   1753         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1754         srv_sendf(si,"WHOIS %s",arg);
   1755 
   1756     } else if (strcmp(cmd,"/who")==0) {
   1757         const char *tgt=arg[0]?arg:g_chans[g_active].name;
   1758         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1759         srv_sendf(si,"WHO %s",tgt);
   1760 
   1761     } else if (strcmp(cmd,"/list")==0) {
   1762         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1763         srv_status_msg(si,"[list] Requesting channel list...");
   1764         if(arg[0]) srv_sendf(si,"LIST %s",arg); else srv_send_raw(si,"LIST");
   1765 
   1766     } else if (strcmp(cmd,"/away")==0) {
   1767         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1768         if(arg[0]) srv_sendf(si,"AWAY :%s",arg); else srv_send_raw(si,"AWAY");
   1769 
   1770     } else if (strcmp(cmd,"/back")==0) {
   1771         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1772         srv_send_raw(si,"AWAY");
   1773 
   1774     } else if (strcmp(cmd,"/invite")==0) {
   1775         char tgt[MAX_NICK],ch[MAX_CHAN]; ch[0]='\0';
   1776         if(sscanf(arg,"%63s %63s",tgt,ch)<1){srv_status_msg(si,"Usage: /invite <nick> [#chan]");return;}
   1777         if(!ch[0]) strncpy(ch,g_chans[g_active].name,MAX_CHAN-1);
   1778         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1779         srv_sendf(si,"INVITE %s %s",tgt,ch);
   1780 
   1781     } else if (strcmp(cmd,"/kick")==0) {
   1782         char tgt[MAX_NICK],reason[MAX_LINE]; reason[0]='\0';
   1783         if(sscanf(arg,"%63s %[^\n]",tgt,reason)<1){srv_status_msg(si,"Usage: /kick <nick> [reason]");return;}
   1784         const char *ch=g_chans[g_active].name;
   1785         if(ch[0]!='#'){srv_status_msg(si,"Not in a channel.");return;}
   1786         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1787         if(reason[0]) srv_sendf(si,"KICK %s %s :%s",ch,tgt,reason);
   1788         else srv_sendf(si,"KICK %s %s",ch,tgt);
   1789 
   1790     } else if (strcmp(cmd,"/mode")==0) {
   1791         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1792         if(!arg[0]) srv_sendf(si,"MODE %s",g_chans[g_active].name);
   1793         else srv_sendf(si,"MODE %s",arg);
   1794 
   1795     } else if (strcmp(cmd,"/raw")==0||strcmp(cmd,"/quote")==0) {
   1796         if(!arg[0]){srv_status_msg(si,"Usage: /raw <irc line>");return;}
   1797         if(!s->connected){srv_status_msg(si,"[not connected]");return;}
   1798         srv_send_raw(si,arg);
   1799         srv_status_fmt(si,">> %s",arg);
   1800 
   1801     } else if (strcmp(cmd,"/server")==0) {
   1802         /* /server host [port] — add a new server connection */
   1803         char newhost[MAX_HOST]; int newport=6697;
   1804         if(sscanf(arg,"%255s %d",newhost,&newport)<1){srv_status_msg(si,"Usage: /server <host> [port]");return;}
   1805         int nsi=srv_alloc();
   1806         if(nsi<0){srv_status_msg(si,"Max servers reached.");return;}
   1807         strncpy(g_srv[nsi].host,newhost,MAX_HOST-1);
   1808         g_srv[nsi].port=newport;
   1809         /* inherit nick from current server */
   1810         strncpy(g_srv[nsi].nick,s->nick,MAX_NICK-1);
   1811         chan_add(nsi,"*status*");
   1812         start_net_thread(nsi);
   1813         srv_status_fmt(nsi,"Connecting to %s:%d...",newhost,newport);
   1814 
   1815     } else if (strcmp(cmd,"/connect")==0) {
   1816         g_srv[si].net_stop=1;
   1817         start_net_thread(si);
   1818 
   1819     } else if (strcmp(cmd,"/ignore")==0) {
   1820         if(!arg[0]) {
   1821             if(!g_ignore_count){srv_status_msg(si,"Ignore list is empty.");return;}
   1822             srv_status_msg(si,"Ignored nicks:");
   1823             for(int i=0;i<g_ignore_count;i++) srv_status_fmt(si,"  %s",g_ignore[i]);
   1824             return;
   1825         }
   1826         ignore_add(arg); srv_status_fmt(si,"Ignoring %s",arg);
   1827 
   1828     } else if (strcmp(cmd,"/unignore")==0) {
   1829         if(is_ignored(arg)){ignore_remove(arg);srv_status_fmt(si,"Unignored %s",arg);}
   1830         else srv_status_fmt(si,"%s is not ignored",arg);
   1831 
   1832     } else if (strcmp(cmd,"/clear")==0) {
   1833         g_chans[g_active].line_count=0;
   1834         g_chans[g_active].line_head=0;
   1835         g_chans[g_active].scroll=0;
   1836 
   1837     } else if (strcmp(cmd,"/quit")==0) {
   1838         for (int i=0;i<g_srv_count;i++)
   1839             if(g_srv[i].connected) srv_sendf(i,"QUIT :%s",arg[0]?arg:"Goodbye");
   1840         endwin(); exit(0);
   1841 
   1842     } else if (strcmp(cmd,"/help")==0) {
   1843         static const char *h[]={
   1844             "Commands:",
   1845             "  /join #chan          join channel",
   1846             "  /part [#chan]        leave channel",
   1847             "  /cycle [#chan]       part and rejoin",
   1848             "  /nick NAME           change nick",
   1849             "  /msg NICK TEXT       private message",
   1850             "  /me TEXT             action message",
   1851             "  /notice TGT TEXT     send notice",
   1852             "  /ctcp NICK CMD       CTCP request",
   1853             "  /names [#chan]       list users",
   1854             "  /topic [text]        get/set topic",
   1855             "  /whois NICK          whois lookup",
   1856             "  /who [target]        who query",
   1857             "  /list [pattern]      list channels",
   1858             "  /away [msg]          set away",
   1859             "  /back                clear away",
   1860             "  /invite NICK [chan]  invite to channel",
   1861             "  /kick NICK [reason]  kick from channel",
   1862             "  /mode [target] [m]   get/set mode",
   1863             "  /server HOST [port]  connect to new server",
   1864             "  /raw <line>          send raw IRC line",
   1865             "  /ignore [nick]       ignore (no arg = list)",
   1866             "  /unignore NICK       unignore nick",
   1867             "  /clear               clear scrollback",
   1868             "  /connect             reconnect current server",
   1869             "  /quit [msg]          quit",
   1870             "Keys:",
   1871             "  Tab                  nick completion",
   1872             "  Ctrl+N/P             next/prev channel",
   1873             "  PgUp/PgDn            scroll chat",
   1874             "  Ctrl+W               delete word left",
   1875             "  Up/Down              input history",
   1876             NULL
   1877         };
   1878         for(int i=0;h[i];i++) srv_status_msg(si,h[i]);
   1879 
   1880     } else {
   1881         srv_status_fmt(si,"Unknown command: %s  (/help for list)",cmd);
   1882     }
   1883 }
   1884 
   1885 /* ── history ───────────────────────────────────────────────────────────────── */
   1886 
   1887 static void hist_push(const char *text) {
   1888     if(g_hist_count<MAX_HIST_LINES) strncpy(g_hist[g_hist_count++].text,text,MAX_INPUT-1);
   1889     else { memmove(g_hist,g_hist+1,(MAX_HIST_LINES-1)*sizeof(HistEntry)); strncpy(g_hist[MAX_HIST_LINES-1].text,text,MAX_INPUT-1); }
   1890 }
   1891 
   1892 /* ── key handler ───────────────────────────────────────────────────────────── */
   1893 
   1894 static void handle_key(int key) {
   1895     if (key==KEY_RESIZE) { clear(); build_windows(); return; }
   1896     if (key=='\t') { tab_complete(); return; }
   1897     tab_reset();
   1898 
   1899     Channel *ch=&g_chans[g_active];
   1900 
   1901     if (key==14) { g_active=chan_next(g_active,1); g_chans[g_active].unread=0; g_chans[g_active].mention=0; return; }
   1902     if (key==16) { g_active=chan_next(g_active,-1); g_chans[g_active].unread=0; g_chans[g_active].mention=0; return; }
   1903 
   1904     if (key==KEY_PPAGE) { int ch2; getmaxyx(win_chat,ch2,(int){0}); ch->scroll+=ch2/2; return; }
   1905     if (key==KEY_NPAGE) { int ch2; getmaxyx(win_chat,ch2,(int){0}); ch->scroll-=ch2/2; if(ch->scroll<0)ch->scroll=0; return; }
   1906 
   1907     if (key=='\n'||key=='\r'||key==KEY_ENTER) {
   1908         if(g_input_len>0) {
   1909             g_input[g_input_len]='\0';
   1910             char *p=g_input; while(*p==' ')p++;
   1911             char *e=g_input+g_input_len-1; while(e>p&&*e==' ')*e--='\0';
   1912             if(*p) { hist_push(p); g_hist_pos=-1; process_input(p); }
   1913         }
   1914         g_input[0]='\0'; g_input_len=0; g_input_cur=0; return;
   1915     }
   1916 
   1917     if (key==127||key==KEY_BACKSPACE||key==8) {
   1918         if(g_input_cur>0){ memmove(g_input+g_input_cur-1,g_input+g_input_cur,g_input_len-g_input_cur+1); g_input_cur--; g_input_len--; }
   1919         return;
   1920     }
   1921 
   1922     if (key==23) { /* Ctrl+W */
   1923         int cur=g_input_cur;
   1924         while(cur>0&&g_input[cur-1]==' ') cur--;
   1925         while(cur>0&&g_input[cur-1]!=' ') cur--;
   1926         int del=g_input_cur-cur;
   1927         memmove(g_input+cur,g_input+g_input_cur,g_input_len-g_input_cur+1);
   1928         g_input_len-=del; g_input_cur=cur; return;
   1929     }
   1930 
   1931     if (key==KEY_DC) { if(g_input_cur<g_input_len){ memmove(g_input+g_input_cur,g_input+g_input_cur+1,g_input_len-g_input_cur); g_input_len--; } return; }
   1932 
   1933     if (key==KEY_LEFT  &&g_input_cur>0)           { g_input_cur--; return; }
   1934     if (key==KEY_RIGHT &&g_input_cur<g_input_len) { g_input_cur++; return; }
   1935     if (key==KEY_HOME) { g_input_cur=0; return; }
   1936     if (key==KEY_END)  { g_input_cur=g_input_len; return; }
   1937 
   1938     if (key==KEY_UP) {
   1939         if(g_hist_count>0){ if(g_hist_pos<g_hist_count-1)g_hist_pos++; const char *t=g_hist[g_hist_count-1-g_hist_pos].text; strncpy(g_input,t,MAX_INPUT-1); g_input_len=strlen(g_input); g_input_cur=g_input_len; }
   1940         return;
   1941     }
   1942     if (key==KEY_DOWN) {
   1943         if(g_hist_pos>0){ g_hist_pos--; const char *t=g_hist[g_hist_count-1-g_hist_pos].text; strncpy(g_input,t,MAX_INPUT-1); g_input_len=strlen(g_input); g_input_cur=g_input_len; }
   1944         else { g_hist_pos=-1; g_input[0]='\0'; g_input_len=0; g_input_cur=0; }
   1945         return;
   1946     }
   1947 
   1948     if (key>=32&&key<256) {
   1949         if(g_input_len<MAX_INPUT-1){ memmove(g_input+g_input_cur+1,g_input+g_input_cur,g_input_len-g_input_cur+1); g_input[g_input_cur++]=(char)key; g_input_len++; }
   1950         return;
   1951     }
   1952 }
   1953 
   1954 /* ── main ──────────────────────────────────────────────────────────────────── */
   1955 
   1956 static void usage(const char *prog) {
   1957     fprintf(stderr,
   1958         "Usage: %s [options]\n"
   1959         "  --host HOST        server hostname\n"
   1960         "  --port PORT        port (default 6697)\n"
   1961         "  --nick NICK        nickname\n"
   1962         "  --channel CHAN     channel(s) to join, comma-separated\n"
   1963         "  --tls / --no-tls   TLS on/off (default on)\n"
   1964         "  --sasl-user USER   SASL username\n"
   1965         "  --sasl-pass PASS   SASL password\n"
   1966         "  --config FILE      config file\n"
   1967         "\n"
   1968         "Multiple servers via ~/.ircrc [server] blocks.\n", prog);
   1969 }
   1970 
   1971 int main(int argc, char **argv) {
   1972     const char *config_path=NULL;
   1973     for(int i=1;i<argc-1;i++) if(strcmp(argv[i],"--config")==0){config_path=argv[i+1];break;}
   1974     load_config(config_path);
   1975 
   1976     /* CLI server (only if --host given, or if no servers loaded yet) */
   1977     const char *cli_host=NULL;
   1978     int cli_port=-1; int cli_tls=-1;
   1979     const char *cli_nick=NULL, *cli_chan=NULL, *cli_su=NULL, *cli_sp=NULL;
   1980     for(int i=1;i<argc;i++) {
   1981         if      (strcmp(argv[i],"--host")==0     &&i+1<argc) cli_host=argv[++i];
   1982         else if (strcmp(argv[i],"--port")==0     &&i+1<argc) cli_port=atoi(argv[++i]);
   1983         else if (strcmp(argv[i],"--nick")==0     &&i+1<argc) cli_nick=argv[++i];
   1984         else if (strcmp(argv[i],"--channel")==0  &&i+1<argc) cli_chan=argv[++i];
   1985         else if (strcmp(argv[i],"--tls")==0)      cli_tls=1;
   1986         else if (strcmp(argv[i],"--no-tls")==0)   cli_tls=0;
   1987         else if (strcmp(argv[i],"--sasl-user")==0&&i+1<argc) cli_su=argv[++i];
   1988         else if (strcmp(argv[i],"--sasl-pass")==0&&i+1<argc) cli_sp=argv[++i];
   1989         else if (strcmp(argv[i],"--config")==0)   i++;
   1990         else if (strcmp(argv[i],"--help")==0)     { usage(argv[0]); return 0; }
   1991     }
   1992 
   1993     /* if CLI gave a host, either override first server or add new one */
   1994     if (cli_host) {
   1995         int si;
   1996         if (g_srv_count==0) si=srv_alloc();
   1997         else si=0; /* override first */
   1998         strncpy(g_srv[si].host,cli_host,MAX_HOST-1);
   1999         if(cli_port>0) g_srv[si].port=cli_port;
   2000         if(cli_nick)   strncpy(g_srv[si].nick,cli_nick,MAX_NICK-1);
   2001         if(cli_tls>=0) g_srv[si].use_tls=cli_tls;
   2002         if(cli_su)     strncpy(g_srv[si].sasl_user,cli_su,MAX_NICK-1);
   2003         if(cli_sp)     strncpy(g_srv[si].sasl_pass,cli_sp,255);
   2004         if(cli_chan)   srv_add_autojoin(si,cli_chan);
   2005     } else if (g_srv_count==0) {
   2006         /* no config, no --host: add default server */
   2007         srv_alloc();
   2008     }
   2009 
   2010     /* create status channels */
   2011     for(int i=0;i<g_srv_count;i++) chan_add(i,"*status*");
   2012 
   2013     /* event pipe */
   2014     if(pipe(g_evpipe)<0){perror("pipe");return 1;}
   2015     fcntl(g_evpipe[1],F_SETFL,O_NONBLOCK);
   2016     fcntl(g_evpipe[0],F_SETFL,O_NONBLOCK);
   2017     signal(SIGPIPE,SIG_IGN);
   2018 
   2019     initscr(); raw(); noecho();
   2020     keypad(stdscr,TRUE); nodelay(stdscr,TRUE); curs_set(2);
   2021     init_colors(); build_windows();
   2022 
   2023     /* start all server threads */
   2024     for(int i=0;i<g_srv_count;i++) start_net_thread(i);
   2025 
   2026     while(1) {
   2027         Event ev;
   2028         while(read(g_evpipe[0],&ev,sizeof(Event))==(ssize_t)sizeof(Event))
   2029             handle_event(&ev);
   2030         draw_all();
   2031         int key=getch();
   2032         if(key!=ERR) handle_key(key);
   2033         struct timespec ts={0,20000000};
   2034         nanosleep(&ts,NULL);
   2035     }
   2036     endwin(); return 0;
   2037 }