cio

a simple irc client
git clone git://kloet.net/cio
Download | Log | Files | Refs | README | LICENSE

cio.c (23270B)


      1 /* See LICENSE file for copyright and license details.
      2  *
      3  * cio is driven by a synchronous select(2) loop. It multiplexes stdin, the
      4  * network socket, and SIGWINCH for terminal resizing. Each channel's
      5  * scrollback is stored in an exponentially-reallocating heap buffer.
      6  *
      7  * Channels are organized in a fixed-size array. The '0' channel is reserved
      8  * for server messages (status), while others are created dynamically upon
      9  * JOIN or PRIVMSG. IRC protocol parsing is handled via a string tokenizer,
     10  * dispatching commands through a lookup table in scmd().
     11  *
     12  * UI rendering is handled by ncurses and UTF-8 is supported by ncursesw.
     13  *
     14  * To understand the lifecycle start reading main().
     15  */
     16 #include <assert.h>
     17 #include <ctype.h>
     18 #include <errno.h>
     19 #include <limits.h>
     20 #include <locale.h>
     21 #include <signal.h>
     22 #include <stdarg.h>
     23 #include <stdio.h>
     24 #include <stdlib.h>
     25 #include <string.h>
     26 #include <time.h>
     27 #include <wchar.h>
     28 #include <wctype.h>
     29 
     30 #include <arpa/inet.h>
     31 #include <netdb.h>
     32 #include <netinet/in.h>
     33 #include <netinet/tcp.h>
     34 #include <sys/ioctl.h>
     35 #include <sys/select.h>
     36 #include <sys/socket.h>
     37 #include <sys/types.h>
     38 #include <unistd.h>
     39 
     40 #include <curses.h>
     41 #include <openssl/evp.h>
     42 #include <openssl/ssl.h>
     43 #include <openssl/x509v3.h>
     44 
     45 #undef  CTRL
     46 #define CTRL(x)    ((x) & 037)
     47 #define IS_CONT(b) (((b) & 0xC0) == 0x80)
     48 #define GET_ARG(i) ((argc > (i)) ? argv[i] : "")
     49 
     50 #define SCROLL     15
     51 #define INDENT     23
     52 #define TABSTOP    8
     53 #define DATEFMT    "%H:%M"
     54 #define PFMT       "  %-12s < %s"
     55 #define PFMTHIGH   "> %-12s < %s"
     56 #define SRV        "irc.oftc.net"
     57 #define PORT       "6697"
     58 
     59 typedef enum {
     60 	CMD_UNKNOWN, NOTICE, PRIVMSG, PING, PONG, PART, JOIN, QUIT, NICK, CAP,
     61 	AUTHENTICATE, RPL_TOPIC, RPL_TOPICWHOTIME, RPL_NAMREPLY,
     62 	RPL_ENDOFNAMES, ERR_NICKNAMEINUSE, ERR_CHANNELISFULL,
     63 	ERR_INVITEONLYCHAN, ERR_BADCHANNELKEY, ERR_NOCHANMODES, SASL_OK,
     64 	SASL_ERR
     65 } IrcCmd;
     66 
     67 struct {
     68 	const char *name;
     69 	IrcCmd id;
     70 } cmd_map[] = {
     71 	{ "NOTICE", NOTICE }, { "PRIVMSG", PRIVMSG }, { "PING", PING },
     72 	{ "PONG", PONG }, { "PART", PART }, { "JOIN", JOIN },
     73 	{ "QUIT", QUIT }, { "NICK", NICK}, { "CAP", CAP },
     74 	{ "AUTHENTICATE", AUTHENTICATE }, { "332", RPL_TOPIC },
     75 	{ "333", RPL_TOPICWHOTIME }, { "353", RPL_NAMREPLY },
     76 	{ "366", RPL_ENDOFNAMES }, { "477", ERR_NOCHANMODES },
     77 	{ "433", ERR_NICKNAMEINUSE }, { "471", ERR_CHANNELISFULL },
     78 	{ "473", ERR_INVITEONLYCHAN }, { "475", ERR_BADCHANNELKEY },
     79 	{ "903", SASL_OK }, { "904", SASL_ERR }
     80 };
     81 
     82 enum {
     83 	ChanLen		= 64,
     84 	NickLen		= 64,
     85 	LineLen		= 512,
     86 	MaxChans	= 16,
     87 	MaxParams	= 15,
     88 	BufSz		= 2048,
     89 	LogSz		= 4096,
     90 	PingDelay	= 6,
     91 	UtfSz		= 4,
     92 	RuneInvalid	= 0xFFFD,
     93 };
     94 
     95 static struct {
     96 	int x, y;
     97 	WINDOW *sw, *mw, *iw;
     98 } scr;
     99 
    100 struct VisLine {
    101     size_t len, offset; /* Offset from Chan->buf */
    102     unsigned char cont;
    103 };
    104 
    105 static struct Chan {
    106 	char name[ChanLen];
    107 	char *buf, *eol;
    108 	int last_mday;      /* Tracks day of last message */
    109 	size_t sz;          /* Size of buf. */
    110 	unsigned char high; /* Nick highlight. */
    111 	unsigned char new;  /* New message. */
    112 	unsigned char join; /* Channel was 'j'-oined. */
    113 	struct VisLine *vis;
    114 	size_t vis_n;
    115 	size_t vis_sz;
    116 	int vis_off;        /* Scroll offset in visual lines */
    117 } chl[MaxChans];
    118 
    119 static struct {
    120 	char buf[BufSz];
    121 	size_t len;
    122 	size_t cu;
    123 	size_t shft;
    124 } inp;
    125 
    126 static int ssl = 1;
    127 static int sslverify = 1;
    128 static struct {
    129 	int fd;
    130 	SSL *ssl;
    131 	SSL_CTX *ctx;
    132 } srv;
    133 static char nick[NickLen];
    134 static char cert[PATH_MAX];
    135 static char key[512];
    136 static int quit, winchg;
    137 static int nch, ch; /* Current number of channels, and current channel. */
    138 static char outb[BufSz], *outp = outb; /* Output buffer. */
    139 static FILE *logfp;
    140 
    141 static void scmd(char *, char *, int, char **);
    142 static void tdrawbar(void);
    143 static void tdrawinput(void);
    144 static void tredraw(void);
    145 static void treset(void);
    146 
    147 static void
    148 die(const char *fmt, ...)
    149 {
    150 	va_list ap;
    151 
    152 	treset();
    153 	va_start(ap, fmt);
    154 	vfprintf(stderr, fmt, ap);
    155 	va_end(ap);
    156 	if (fmt[0] && fmt[strlen(fmt)-1] == ':')
    157 		fprintf(stderr, " %s", strerror(errno));
    158 	fputc('\n', stderr);
    159 	exit(1);
    160 }
    161 
    162 static int
    163 empty(const char *str) {
    164 	return (str == NULL || str[0] == '\0');
    165 }
    166 
    167 static void
    168 sndf(const char *fmt, ...)
    169 {
    170 	va_list vl;
    171 	size_t n, l = BufSz - (outp - outb);
    172 
    173 	if (l < 2)
    174 		return;
    175 	va_start(vl, fmt);
    176 	n = vsnprintf(outp, l - 2, fmt, vl);
    177 	va_end(vl);
    178 	outp += n > l - 2 ? l - 2 : n;
    179 	*outp++ = '\r';
    180 	*outp++ = '\n';
    181 }
    182 
    183 static int
    184 srd(void)
    185 {
    186 	static char l[BufSz], *p = l;
    187 	ssize_t rd;
    188 
    189 	if (p - l >= BufSz)
    190 		p = l; /* Input buffer overflow */
    191 	if (ssl)
    192 		rd = SSL_read(srv.ssl, p, BufSz - (p - l));
    193 	else
    194 		rd = read(srv.fd, p, BufSz - (p - l));
    195 	if (rd <= 0) return 0;
    196 	p += rd;
    197 	for (;;) { /* Cycle on all received lines. */
    198 		char *s = memchr(l, '\n', p - l);
    199 		if (!s) return 1;
    200 		if (s > l && s[-1] == '\r') s[-1] = 0;
    201 		*s++ = 0;
    202 		char *line = l, *token, *usr = NULL, *cmd;
    203 		int argc = 0;
    204 		char *argv[MaxParams];
    205 		if (*line == ':') {
    206 			usr = line + 1;
    207 			strsep(&line, " ");
    208 		}
    209 		cmd = strsep(&line, " ");
    210 		while (line && argc < MaxParams) {
    211 			if (*line == ':') {
    212 				argv[argc++] = line + 1;
    213 				break;
    214 			}
    215 			token = strsep(&line, " ");
    216 			if (*token) argv[argc++] = token;
    217 		}
    218 		if (cmd) scmd(usr, cmd, argc, argv);
    219 		memmove(l, s, p - s);
    220 		p -= s - l;
    221 	}
    222 }
    223 
    224 static void
    225 sinit(const char *init_nick, const char *user)
    226 {
    227 	sndf("CAP LS 302");
    228 	sndf("NICK %s", init_nick);
    229 	sndf("USER %s 8 * :%s", user, user);
    230 }
    231 
    232 static char *
    233 dial(const char *host, const char *service)
    234 {
    235 	struct addrinfo *res, *rp, hints = {
    236 		.ai_family = AF_UNSPEC,
    237 		.ai_socktype = SOCK_STREAM,
    238 		.ai_flags = AI_NUMERICSERV
    239 	};
    240 	int fd = -1;
    241 
    242 	if (getaddrinfo(host, service, &hints, &res))
    243 		return "getaddrinfo failed";
    244 	for (rp = res; rp; rp = rp->ai_next) {
    245 		fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
    246 		if (fd == -1)
    247 			continue;
    248 		if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
    249 			break;
    250 		close(fd);
    251 		fd = -1;
    252 	}
    253 	freeaddrinfo(res);
    254 	srv.fd = fd;
    255 	if (!fd) return "cannot connect to host";
    256 	if (!ssl) return 0; /* done */
    257 	SSL_load_error_strings();
    258 	SSL_library_init();
    259 	srv.ctx = SSL_CTX_new(TLS_client_method());
    260 	if (!srv.ctx) return "could not initialize ssl context";
    261 	if (sslverify && (SSL_CTX_set_verify(srv.ctx, SSL_VERIFY_PEER, NULL),
    262 	    (SSL_CTX_set_default_verify_paths(srv.ctx) != 1)))
    263 		return "could not load system trust store";
    264 	srv.ssl = SSL_new(srv.ctx);
    265 	if (!srv.ssl) return "could not create ssl object";
    266 	if (sslverify &&
    267 	    (SSL_set_hostflags(srv.ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS),
    268 	    !SSL_set1_host(srv.ssl, host) ||
    269 	    !SSL_set_tlsext_host_name(srv.ssl, host)))
    270 		return "could not set expected hostname";
    271 	if (!empty(cert) &&
    272 	    (SSL_use_certificate_chain_file(srv.ssl, cert) != 1 ||
    273 	     SSL_use_PrivateKey_file(srv.ssl, cert, SSL_FILETYPE_PEM) != 1))
    274 		return "failed to load certificate or private key";
    275 	SSL_set_fd(srv.ssl, srv.fd);
    276 	if (SSL_connect(srv.ssl) <= 0)
    277 		return "ssl handshake failed";
    278 	return 0;
    279 }
    280 
    281 static void
    282 hangup(void)
    283 {
    284 	if (srv.ssl) SSL_shutdown(srv.ssl);
    285 	SSL_free(srv.ssl);
    286 	if (srv.fd > 0) close(srv.fd);
    287 	SSL_CTX_free(srv.ctx);
    288 	memset(&srv, 0, sizeof(srv));
    289 }
    290 
    291 static inline int
    292 chfind(const char *name)
    293 {
    294 	int i;
    295 
    296 	assert(name);
    297 	for (i = nch - 1; i > 0; i--)
    298 		if (!strcasecmp(chl[i].name, name))
    299 			break;
    300 	return i;
    301 }
    302 
    303 static int
    304 chadd(const char *name, int joined)
    305 {
    306 	int n;
    307 
    308 	if (nch >= MaxChans || strlen(name) >= ChanLen)
    309 		return -1;
    310 	n = chfind(name);
    311 	if (n > 0) return n;
    312 	strlcpy(chl[nch].name, name, sizeof(chl[nch].name));
    313 	chl[nch].sz = LogSz;
    314 	chl[nch].buf = malloc(LogSz);
    315 	if (!chl[nch].buf)
    316 		die("cio: malloc:");
    317 	chl[nch].eol = chl[nch].buf;
    318 	chl[nch].join = joined;
    319 	chl[nch].last_mday = -1;
    320 	chl[nch].vis = NULL;
    321 	chl[nch].vis_n = 0;
    322 	chl[nch].vis_sz = 0;
    323 	chl[nch].vis_off = 0;
    324 	if (joined)
    325 		ch = nch;
    326 	nch++;
    327 	tdrawbar();
    328 	return nch - 1;
    329 }
    330 
    331 static int
    332 chdel(const char *name)
    333 {
    334 	int n = chfind(name);
    335 	if (n <= 0) return 0;
    336 	free(chl[n].buf);
    337 	free(chl[n].vis);
    338 
    339 	if (n < nch - 1) { /* Move trailing channels left by one */
    340 		size_t len = (nch - n - 1) * sizeof(struct Chan);
    341 		memmove(&chl[n], &chl[n + 1], len);
    342 	}
    343 	nch--;
    344 	if (ch > n || ch >= nch) /* Adjust current channel focus */
    345 		ch = (ch > 0) ? ch - 1 : 0;
    346 	tdrawbar();
    347 	return 1;
    348 }
    349 
    350 static void
    351 add_visline(struct Chan *c, size_t offset, size_t len, int cont)
    352 {
    353 	if (c->vis_n >= c->vis_sz) {
    354 		c->vis_sz = c->vis_sz ? c->vis_sz * 2 : 1024;
    355 		c->vis = realloc(c->vis, c->vis_sz * sizeof(*c->vis));
    356 		if (!c->vis) die("vis realloc:");
    357 	}
    358 	c->vis[c->vis_n].offset = offset;
    359 	c->vis[c->vis_n].len = len;
    360 	c->vis[c->vis_n].cont = cont;
    361 	c->vis_n++;
    362 }
    363 
    364 static char *
    365 pushl(struct Chan *c, char *p, char *e)
    366 {
    367 	size_t x = 0;
    368 	wchar_t wc;
    369 	int cl;
    370 	int cont = 0;
    371 	char *eol = memchr(p, '\n', e - p);
    372 	const char *offset = p;
    373 
    374 	if (!eol) eol = e;
    375 	mbtowc(NULL, NULL, 0);
    376 	while (p < eol) {
    377 		int n = mbtowc(&wc, p, eol - p);
    378 		if (n <= 0) { wc = L'?'; n = 1; }
    379 		if (iswcntrl(wc)) {
    380 			p += n;
    381 			continue;
    382 		}
    383 
    384 		cl = wcwidth(wc);
    385 		if (cl < 0) cl = 0;
    386 
    387 		if (x + cl >= (size_t)scr.x) {
    388 			add_visline(c, offset - c->buf, p - offset, cont);
    389 			offset = p;
    390 			cont = 1;
    391 			x = INDENT;
    392 		}
    393 		x += cl;
    394 		p += n;
    395 	}
    396 
    397 	if (p > offset)
    398 		add_visline(c, offset - c->buf, p - offset, cont);
    399 	return (eol < e) ? eol + 1 : e;
    400 }
    401 
    402 static void
    403 pushf(int cn, const char *fmt, ...)
    404 {
    405 	struct Chan *const c = &chl[cn];
    406 	size_t n, blen = c->eol - c->buf;
    407 	va_list vl;
    408 	time_t t;
    409 	char *s;
    410 	struct tm tm, gmtm;
    411 
    412 	if (blen + LineLen >= c->sz) {
    413 		c->sz *= 2;
    414 		c->buf = realloc(c->buf, c->sz);
    415 		if (!c->buf)
    416 			die("cio: realloc:");
    417 		c->eol = c->buf + blen;
    418 	}
    419 	t = time(0);
    420 	if (!localtime_r(&t, &tm))
    421 		die("cio: localtime failed");
    422 	if (c->last_mday == -1) {
    423 		c->last_mday = tm.tm_mday;
    424 	} else if (c->last_mday != tm.tm_mday) {
    425 		c->last_mday = tm.tm_mday;
    426 		pushf(cn, "--- Day changed to %04d-%02d-%02d Anno Domini ---",
    427 		    tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
    428 	}
    429 	n = strftime(c->eol, LineLen, DATEFMT, &tm);
    430 	if (!gmtime_r(&t, &gmtm))
    431 		die("cio: gmtime failed");
    432 	c->eol[n++] = ' ';
    433 	va_start(vl, fmt);
    434 	s = c->eol + n;
    435 	int wrote = vsnprintf(s, LineLen - n - 1, fmt, vl);
    436 	if (wrote >= LineLen - n - 1) wrote = LineLen - n - 2;
    437 	n += (wrote < 0) ? 0 : wrote; /* failed to append to buffer */
    438 	va_end(vl);
    439 	if (logfp) {
    440 		char isotm[32];
    441 		strftime(isotm, sizeof(isotm), "%Y-%m-%dT%H:%M:%SZ", &gmtm);
    442 		fprintf(logfp, "%-12.12s\t%s\t%s\n", c->name, isotm, s);
    443 		fflush(logfp);
    444 	}
    445 	c->eol += n;
    446 	*c->eol++ = '\n';
    447 	*c->eol = '\0';
    448 	if (c->vis_off == 0) {
    449 		char *p = c->eol - n - 1;
    450 		pushl(c, p, c->eol - 1);
    451 		tredraw();
    452 	}
    453 }
    454 
    455 static IrcCmd get_cmd_type(const char *cmd) {
    456 	for (size_t i = 0; i < sizeof(cmd_map)/sizeof(cmd_map[0]); i++)
    457 		if (!strcmp(cmd, cmd_map[i].name)) return cmd_map[i].id;
    458 	return CMD_UNKNOWN;
    459 }
    460 
    461 static void
    462 scmd(char *usr, char *cmd, int argc, char **argv)
    463 {
    464 	IrcCmd type = get_cmd_type(cmd);
    465 	int c = 0;
    466 
    467 	if (!usr) usr = "?";
    468 	else {
    469 		char *bang = strchr(usr, '!');
    470 		if (bang) *bang = '\0';
    471 	}
    472 	switch (type) {
    473 	case NOTICE: /* FALLTHROUGH */
    474 	case PRIVMSG:
    475 		if (argc < 2) break;
    476 		const char *chan;
    477 		if (strchr("&#!+.~", argv[0][0])) chan = argv[0];
    478 		else if (type == PRIVMSG) chan = usr;
    479 		else chan = NULL;
    480 		if (!chan) c = 0;
    481 		else if ((c = chadd(chan, 0)) < 0) break;
    482 		int high = (type == PRIVMSG && strcasestr(argv[1], nick));
    483 		pushf(c, high ? PFMTHIGH : PFMT, usr, argv[1]);
    484 		if (ch != c) {
    485 			chl[c].new = 1;
    486 			chl[c].high |= high;
    487 			tdrawbar();
    488 		}
    489 		break;
    490 	case PING:
    491 		if (argc >= 2)
    492 			sndf("PONG %s :%s", argv[0], argv[1]);
    493 		else if (argc == 1)
    494 			sndf("PONG :%s", argv[0]);
    495 		else
    496 			sndf("PONG :%s", chl[0]);
    497 		break;
    498 	case NICK:
    499 		if (argc < 1) break;
    500 		if (!strcasecmp(usr, nick)) {
    501 			strlcpy(nick, argv[0], sizeof(nick));
    502 			pushf(0, "-!- You are now known as %s", nick);
    503 		} else
    504 			pushf(0, "-!- %s is now known as %s", usr, argv[0]);
    505 		break;
    506 	case RPL_TOPIC:
    507 		if (argc >= 3)
    508 			pushf(chfind(argv[1]), "-!- Topic: %s", argv[2]);
    509 		break;
    510 	case RPL_NAMREPLY:
    511 		if (argc >= 4)
    512 			pushf(chfind(argv[2]), "-!- Names: %s", argv[3]);
    513 		break;
    514 	case ERR_NOCHANMODES: /* FALLTHROUGH */
    515 	case ERR_CHANNELISFULL:
    516 	case ERR_INVITEONLYCHAN:
    517 	case ERR_BADCHANNELKEY:
    518 		if (argc < 2) break;
    519 		chdel(argv[1]);
    520 		pushf(0, "-!- Cannot join %s (%s)", argv[1], cmd);
    521 		tredraw();
    522 		break;
    523 	case ERR_NICKNAMEINUSE:
    524 		strlcat(nick, "_", sizeof(nick));
    525 		sndf("NICK %s", nick);
    526 		break;
    527 	case CAP:
    528 		if (argc < 2) break;
    529 		int want = !empty(key) || !empty(cert);
    530 		int has = argc > 2 && strstr(argv[2], "sasl");
    531 		if (!strcmp(argv[1], "LS"))
    532 			sndf((want && has) ? "CAP REQ :sasl" : "CAP END");
    533 		else if (!strcmp(argv[1], "ACK") && has)
    534 			sndf("AUTHENTICATE %s",
    535 			    empty(cert) ? "PLAIN" : "EXTERNAL");
    536 		else if (!strcmp(argv[1], "NAK") || !strcmp(argv[1], "ACK"))
    537 			sndf("CAP END");
    538 		break;
    539 	case AUTHENTICATE:
    540 		if (argc < 1 || strcmp(argv[0], "+") != 0) break;
    541 		if (!empty(cert)) {
    542 			sndf("AUTHENTICATE +");
    543 		} else if (!empty(usr) && !empty(key)) {
    544 			unsigned char raw[512];
    545 			char b64[((sizeof(raw) + 2) / 3) * 4 + 1];
    546 			size_t ulen = strlen(usr);
    547 			size_t klen = strlen(key);
    548 			size_t rlen = ulen + klen + 2;
    549 			if (rlen > sizeof(raw)) break;
    550 			raw[0] = '\0';
    551 			memcpy(raw + 1, usr, ulen);
    552 			raw[1 + ulen] = '\0';
    553 			memcpy(raw + 2 + ulen, key, klen);
    554 			EVP_EncodeBlock((unsigned char *)b64, raw, (int)rlen);
    555 			sndf("AUTHENTICATE %s", b64);
    556 		}
    557 		break;
    558 	case SASL_OK: /* FALLTHROUGH */
    559 	case SASL_ERR:
    560 		sndf("CAP END");
    561 		pushf(0, "-!- SASL auth %s",
    562 		    (type == SASL_OK ? "successful" : "failed"));
    563 		break;
    564 	/* Ignored commands */
    565 	case PONG: /* FALLTHROUGH */
    566 	case JOIN:
    567 	case PART:
    568 	case QUIT:
    569 	case RPL_TOPICWHOTIME:
    570 	case RPL_ENDOFNAMES:
    571 		break;
    572 	default: {
    573 		if (!isdigit(cmd[0])) break;
    574 		char arg_buf[LineLen] = "";
    575 		/* Numerics usually have the client nick at argv[0] */
    576 		for (int i = 1; i < argc; i++) {
    577 			strlcat(arg_buf, argv[i], sizeof(arg_buf));
    578 			if (i < argc - 1)
    579 				strlcat(arg_buf, " ", sizeof(arg_buf));
    580 		}
    581 		pushf(0, "-!- %s", arg_buf);
    582 		break;
    583 		}
    584 	}
    585 }
    586 
    587 static void
    588 uparse(char *m)
    589 {
    590 	char *p = m;
    591 
    592 	if (!p[0] || (p[1] != ' ' && p[1] != 0)) {
    593 	pmsg:
    594 		if (ch == 0) return;
    595 		m += strspn(m, " ");
    596 		if (!*m) return;
    597 		pushf(ch, PFMT, nick, m);
    598 		sndf("PRIVMSG %s :%s", chl[ch].name, m);
    599 		return;
    600 	}
    601 	switch (*p) {
    602 	case 'j': /* Join channels. */
    603 		p += 1 + (p[1] == ' ');
    604 		for (char *t = strtok(p, " "); t; t = strtok(NULL, " ")) {
    605 			if (chadd(t, 1) < 0)
    606 				break;
    607 			if (strchr("&#!+", t[0]))
    608 				sndf("JOIN %s", t);
    609 		}
    610 		tredraw();
    611 		return;
    612 	case 'l': /* Leave channels. */
    613 		p += 1 + (p[1] == ' ');
    614 		if (!*p) {
    615 			if (ch == 0) return; /* Cannot leave server window */
    616 			static char buf[ChanLen];
    617 			strlcpy(buf, chl[ch].name, sizeof(buf));
    618 			p = buf;
    619 		}
    620 		for (char *t = strtok(p, " "); t; t = strtok(NULL, " "))
    621 			if (chdel(t) && strchr("&#!+", t[0]))
    622 				sndf("PART %s", t);
    623 		tredraw();
    624 		return;
    625 	case 'm': /* Private message. */
    626 		m = p + 1 + (p[1] == ' ');
    627 		p = strchr(m, ' ');
    628 		if (!p) return;
    629 		*p++ = 0;
    630 		sndf("PRIVMSG %s :%s", m, p);
    631 		return;
    632 	case 'r': /* Send raw. */
    633 		if (p[1])
    634 			sndf("%s", &p[2]);
    635 		return;
    636 	case 'q': /* Quit. */
    637 		quit = 1;
    638 		return;
    639 	default: /* Send on current channel. */
    640 		goto pmsg;
    641 	}
    642 }
    643 
    644 static void
    645 sigwinch(int sig)
    646 {
    647 	if (sig) winchg = 1;
    648 }
    649 
    650 static void
    651 tinit(void)
    652 {
    653 	setlocale(LC_ALL, "");
    654 	signal(SIGWINCH, sigwinch);
    655 	initscr();
    656 	raw();
    657 	noecho();
    658 	getmaxyx(stdscr, scr.y, scr.x);
    659 	if (scr.y < 4)
    660 		die("cio: screen too small");
    661 	scr.sw = newwin(1, scr.x, 0, 0);
    662 	scr.mw = newwin(scr.y - 2, scr.x, 1, 0);
    663 	scr.iw = newwin(1, scr.x, scr.y - 1, 0);
    664 	if (!scr.sw || !scr.mw || !scr.iw)
    665 		die("cio: cannot create windows");
    666 	keypad(scr.iw, 1);
    667 	scrollok(scr.mw, 1);
    668 	if (has_colors() == TRUE) {
    669 		start_color();
    670 		use_default_colors();
    671 		init_pair(1, COLOR_WHITE, COLOR_BLACK);
    672 		wbkgd(scr.sw, COLOR_PAIR(1));
    673 	}
    674 }
    675 
    676 static void
    677 tresize(void)
    678 {
    679 	struct winsize ws;
    680 
    681 	winchg = 0;
    682 	if (ioctl(0, TIOCGWINSZ, &ws) < 0)
    683 		die("cio: Ioctl (TIOCGWINSZ) failed:");
    684 	if (ws.ws_row <= 2) return;
    685 	resizeterm(scr.y = ws.ws_row, scr.x = ws.ws_col);
    686 	erase();
    687 	wnoutrefresh(stdscr);
    688 	wresize(scr.mw, scr.y - 2, scr.x);
    689 	wresize(scr.iw, 1, scr.x);
    690 	wresize(scr.sw, 1, scr.x);
    691 	mvwin(scr.iw, scr.y - 1, 0);
    692 	for (int i = 0; i < nch; i++){
    693 		chl[i].vis_n = 0;
    694 		char *p = chl[i].buf;
    695 		while (p && p < chl[i].eol)
    696 			p = pushl(&chl[i], p, chl[i].eol);
    697 	}
    698 	tredraw();
    699 	tdrawbar();
    700 	tdrawinput();
    701 	doupdate();
    702 }
    703 
    704 static void
    705 tredraw(void)
    706 {
    707 	struct Chan *c = &chl[ch];
    708 	int height = scr.y - 2;
    709 
    710 	wclear(scr.mw);
    711 	if (c->vis_n == 0) {
    712 		wnoutrefresh(scr.mw);
    713 		return;
    714 	}
    715 	if (c->vis_off > (int)c->vis_n - height)
    716 		c->vis_off = (int)c->vis_n - height;
    717 	if (c->vis_off < 0)
    718 		c->vis_off = 0;
    719 	int offset = (int)c->vis_n - height - c->vis_off;
    720 	if (offset < 0)
    721 		offset = 0;
    722 	for (int i = offset; i < (int)c->vis_n && i < offset + height; i++) {
    723 		if (c->vis[i].cont)
    724 			wmove(scr.mw, getcury(scr.mw), INDENT);
    725 		waddnstr(scr.mw, c->buf + c->vis[i].offset, c->vis[i].len);
    726 		waddch(scr.mw, '\n');
    727 	}
    728 	wnoutrefresh(scr.mw);
    729 }
    730 
    731 static void
    732 tdrawbar(void)
    733 {
    734 	size_t l;
    735 	int fst = ch;
    736 
    737 	for (l = 0; fst > 0 && l < scr.x / 2; fst--)
    738 		l += strlen(chl[fst].name) + 3;
    739 	werase(scr.sw);
    740 	for (l = 0; fst < nch && l < scr.x; fst++) {
    741 		char *p = chl[fst].name;
    742 		if (fst == ch)
    743 			wattron(scr.sw, A_BOLD);
    744 		waddch(scr.sw, '['), l++;
    745 		if (chl[fst].high)
    746 			waddch(scr.sw, '>'), l++;
    747 		else if (chl[fst].new)
    748 			waddch(scr.sw, '+'), l++;
    749 		for (; *p && l < scr.x; p++, l++)
    750 			waddch(scr.sw, *p);
    751 		if (l < scr.x - 1)
    752 			waddstr(scr.sw, "] "), l += 2;
    753 		if (fst == ch)
    754 			wattroff(scr.sw, A_BOLD);
    755 	}
    756 	wnoutrefresh(scr.sw);
    757 }
    758 
    759 static void
    760 tdrawinput(void)
    761 {
    762 	int v_cu = 0; /* visual cursor position */
    763 	size_t b_shft = 0;
    764 	int v_curr = 0;
    765 	wchar_t wc;
    766 	int n, cl;
    767 
    768 	mbtowc(NULL, NULL, 0);
    769 	for (size_t i = 0; i < inp.len; i += n) {
    770 		n = mbtowc(&wc, inp.buf + i, inp.len - i);
    771 		if (n <= 0) { n = 1; cl = 1; }
    772 		else { cl = (cl = wcwidth(wc)) < 0 ? 0 : cl; }
    773 		if (i < inp.cu) v_cu += cl;
    774 		if (v_curr < inp.shft) {
    775 			v_curr += cl;
    776 			b_shft = i + n;
    777 		}
    778 	}
    779 	if (v_cu < inp.shft)
    780 		inp.shft = (v_cu > scr.x / 2) ? v_cu - scr.x / 2 : 0;
    781 	else if (v_cu >= inp.shft + scr.x)
    782 	inp.shft = v_cu - scr.x / 2;
    783 	wmove(scr.iw, 0, 0);
    784 	waddnstr(scr.iw, inp.buf + b_shft, inp.len - b_shft);
    785 	wclrtoeol(scr.iw);
    786 	wmove(scr.iw, 0, v_cu - (int)inp.shft);
    787 	wnoutrefresh(scr.iw);
    788 }
    789 
    790 static size_t prev_char(size_t pos) {
    791     if (pos == 0) return 0;
    792     size_t i = pos - 1;
    793     while (i > 0 && IS_CONT(inp.buf[i])) i--;
    794     return i;
    795 }
    796 
    797 static size_t next_char(size_t pos) {
    798     if (pos >= inp.len) return inp.len;
    799     size_t i = pos + 1;
    800     while (i < inp.len && IS_CONT(inp.buf[i])) i++;
    801     return i;
    802 }
    803 
    804 static void buf_delete(size_t start, size_t end) {
    805     if (start >= end) return;
    806     memmove(&inp.buf[start], &inp.buf[end], inp.len - end);
    807     inp.len -= (end - start);
    808     inp.cu = start;
    809 }
    810 
    811 static void
    812 tgetch(void)
    813 {
    814 	wint_t wc;
    815 	int res = wget_wch(scr.iw, &wc);
    816 	char *p = &inp.buf[inp.cu]; /* Current cursor position */
    817 	size_t tail = inp.len - inp.cu; /* Count of chars after cursor */
    818 	size_t old;
    819 
    820 	switch (wc) {
    821 	case CTRL('n'): /* FALLTHROUGH */
    822 	case CTRL('p'):
    823 		ch = (ch + (wc == CTRL('n') ? 1 : -1) + nch) % nch;
    824 		chl[ch].high = chl[ch].new = 0;
    825 		tdrawbar();
    826 		tredraw();
    827 		return;
    828 	case KEY_PPAGE: /* FALLTHROUGH */
    829 	case KEY_NPAGE:
    830 		chl[ch].vis_off += (wc == KEY_PPAGE) ? SCROLL : -SCROLL;
    831 		tredraw();
    832 		return;
    833 	case CTRL('a'):
    834 		inp.cu = 0;
    835 		break;
    836 	case CTRL('e'):
    837 		inp.cu = inp.len;
    838 		break;
    839 	case CTRL('b'): /* FALLTHROUGH */
    840 	case KEY_LEFT:
    841 		inp.cu = prev_char(inp.cu);
    842 		break;
    843 	case CTRL('f'): /* FALLTHROUGH */
    844 	case KEY_RIGHT:
    845 		inp.cu = next_char(inp.cu);
    846 		break;
    847 	case CTRL('k'):
    848 		inp.len = inp.cu;
    849 		break;
    850 	case CTRL('u'):
    851 		buf_delete(0, inp.cu);
    852 		break;
    853 	case CTRL('d'):
    854 		buf_delete(inp.cu, next_char(inp.cu));
    855 		break;
    856 	case CTRL('h'): /* FALLTHROUGH */
    857 	case KEY_BACKSPACE:
    858 		if (inp.cu > 0) buf_delete(prev_char(inp.cu), inp.cu);
    859 		break;
    860 	case CTRL('w'):
    861 		old = inp.cu;
    862 		while (inp.cu > 0 && inp.buf[inp.cu - 1] == ' ') inp.cu--;
    863 		while (inp.cu > 0 && inp.buf[inp.cu - 1] != ' ')
    864 		    inp.cu = prev_char(inp.cu);
    865 		buf_delete(inp.cu, old);
    866 		break;
    867 	case '\n':
    868 		inp.buf[inp.len] = 0;
    869 		uparse(inp.buf);
    870 		inp.cu = inp.len = inp.shft = 0;
    871 		break;
    872 	default:
    873 		if (res == KEY_CODE_YES || (iswcntrl(wc))) return;
    874 		char mb[MB_LEN_MAX];
    875 		int n = wctomb(mb, (wchar_t)wc);
    876 		if (n <= 0 || inp.len + n >= BufSz - 1) return;
    877 		memmove(p + n, p, tail);
    878 		memcpy(p, mb, n);
    879 		inp.cu += n; inp.len += n;
    880 		break;
    881 	}
    882 	tdrawinput();
    883 }
    884 
    885 static void
    886 treset(void)
    887 {
    888 	if (scr.mw) delwin(scr.mw);
    889 	if (scr.sw) delwin(scr.sw);
    890 	if (scr.iw) delwin(scr.iw);
    891 	if (!isendwin()) endwin();
    892 }
    893 
    894 int
    895 main(int argc, char *argv[])
    896 {
    897 	const char *user = getenv("USER");
    898 	const char *ircnick = getenv("IRCNICK");
    899 	snprintf(key, sizeof(key), "%s", getenv("IRCPASS"));
    900 	const char *server = SRV;
    901 	const char *port = PORT;
    902 	const char *err;
    903 	int o, ping;
    904 
    905 	signal(SIGPIPE, SIG_IGN);
    906 	while ((o = getopt(argc, argv, "hvTVn:c:u:s:p:l:")) >= 0)
    907 		switch (o) {
    908 		case 'h':
    909 		usage:
    910 			die("usage: cio [-vTV] [-n nick] [-u user]"
    911 			    " [-s server]\n"
    912 			    "           [-p port] [-l logfile]"
    913 			    " [-c certificate]");
    914 			break;
    915 		case 'v':
    916 			die("cio-" VERSION);
    917 			break;
    918 		case 'l':
    919 			logfp = fopen(optarg, "a");
    920 			if (!logfp) die("cio: fopen: logfile:");
    921 			break;
    922 		case 'n':
    923 			if (strlcpy(nick, optarg, sizeof(nick)) >= NickLen)
    924 				goto usage;
    925 			break;
    926 		case 'T':
    927 			ssl = 0;
    928 			break;
    929 		case 'V':
    930 			sslverify = 0;
    931 			break;
    932 		case 'u':
    933 			user = optarg;
    934 			break;
    935 		case 's':
    936 			server = optarg;
    937 			break;
    938 		case 'p':
    939 			port = optarg;
    940 			break;
    941 		case 'c':
    942 			if (strlcpy(cert, optarg, sizeof(cert)) >= PATH_MAX)
    943 				goto usage;
    944 			break;
    945 		default:
    946 			goto usage;
    947 		}
    948 	if (!user)
    949 		user = "anonymous";
    950 	if (!nick[0] && ircnick)
    951 		strlcpy(nick, ircnick, sizeof(nick));
    952 	if (!nick[0])
    953 		strlcpy(nick, user, sizeof(nick));
    954 	if (!nick[0])
    955 		goto usage;
    956 	atexit(treset);
    957 	tinit();
    958 	err = dial(server, port);
    959 #ifdef __OpenBSD__
    960 	if (pledge("stdio tty", NULL) == -1)
    961 		die("pledge");
    962 #endif /* __OpenBSD__ */
    963 	if (err) die("cio: %s", err);
    964 	chadd(server, 0);
    965 	sinit(nick, user);
    966 	ping = 0;
    967 	while (!quit) {
    968 		struct timeval t = {.tv_sec = 5};
    969 		fd_set rfs, wfs;
    970 		if (winchg)
    971 			tresize();
    972 		FD_ZERO(&wfs);
    973 		FD_ZERO(&rfs);
    974 		FD_SET(0, &rfs);
    975 		FD_SET(srv.fd, &rfs);
    976 		if (outp != outb)
    977 			FD_SET(srv.fd, &wfs);
    978 		if (select(srv.fd + 1, &rfs, &wfs, 0, &t) < 0) {
    979 			if (errno == EINTR)
    980 				continue;
    981 			die("cio: select failed:");
    982 		}
    983 		if (FD_ISSET(srv.fd, &rfs)) {
    984 			if (!srd()) {
    985 				die("cio: connection closed by remote host");
    986 			}
    987 		}
    988 		if (FD_ISSET(srv.fd, &wfs)) {
    989 			size_t n = (size_t)(outp - outb);
    990 			ssize_t wr = ssl ? SSL_write(srv.ssl, outb, (int)n)
    991 			                 : write(srv.fd, outb, n);
    992 			if (wr <= 0) {
    993 				die("cio: write failed: connection lost");
    994 			}
    995 			outp -= wr;
    996 			memmove(outb, outb + wr, outp - outb);
    997 		}
    998 		if (FD_ISSET(0, &rfs))
    999 			tgetch();
   1000 		doupdate();
   1001 		if ((!FD_ISSET(srv.fd, &wfs))
   1002 		 && (!FD_ISSET(srv.fd, &rfs))
   1003 		 && (outp == outb)
   1004 		 && (++ping >= PingDelay)) {
   1005 			sndf("PING %s", server);
   1006 			ping = 0;
   1007 		}
   1008 	}
   1009 	hangup();
   1010 	while (nch--) {
   1011 		free(chl[nch].buf);
   1012 		free(chl[nch].vis);
   1013 	}
   1014 	exit(0);
   1015 }