/*
 * bsdirc - a small IRC client for OpenBSD
 *
 * curses for the user interface, libtls for TLS, poll(2) for the event
 * loop and pledge(2)/unveil(2) to lock the process down.
 *
 * Copyright (c) 2026 Johan Tärnklint <johan@fegis.nu>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#ifndef __OpenBSD__
#define _GNU_SOURCE		/* wcwidth, explicit_bzero on Linux */
#endif
#define NCURSES_WIDECHAR 1	/* get_wch, addnwstr */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/utsname.h>

#include <ctype.h>
#include <curses.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <locale.h>
#include <netdb.h>
#include <poll.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <termios.h>
#include <time.h>
#include <tls.h>
#include <openssl/hmac.h>		/* Q CHALLENGEAUTH (QuakeNet) */
#include <openssl/sha.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>

#ifdef __OpenBSD__
#define HAVE_ASR		/* asynchronous DNS in libc, see asr_run(3) */
#include <readpassphrase.h>
#endif
#ifdef HAVE_ASR
#include <asr.h>
#endif

#ifndef __dead
#define __dead __attribute__((__noreturn__))
#endif

#define VERSION		"0.9"
#define SCROLLBACK	2000	/* lines per buffer */
#define MAXBUFS		128
#define MAXNETS		16
#define MAXAUTO		64	/* autojoin channels per network */
#define MAXQUERY	64	/* query windows kept per network */
#define MAXPARAMS	16
#define INPUTMAX	512	/* characters in the input line */
#define CHUNK		400	/* max bytes of text per PRIVMSG */
#define CONNTIMEOUT	30	/* seconds for connect + TLS */
#define PINGIDLE	120	/* silence before we ping the server */
#define PINGTIMEOUT	300	/* silence before we give up */
#define BACKOFFMIN	5
#define BACKOFFMAX	300
#define SQMAX		512	/* lines in the send queue */
#define OBUFMAX		65536	/* bytes waiting for the server, at most */
#define BACKLOGMAX	1000	/* /set backlog: at most this many lines */
#define BACKLOGREAD	(256 * 1024)	/* bytes read from the end of a log */
#define QBOT		"Q@CServe.quakenet.org"
#define QWAIT		15	/* seconds to wait for Q before joining anyway */
enum { Q_NONE, Q_CHALLENGE, Q_RESULT, Q_DONE };
#define CTCPMAX		3	/* CTCP replies ... */
#define CTCPWIN		10	/* ... per this many seconds */
#define SQBURST		5	/* this many lines may go out at once */
#define SQRATE		2000	/* ms per line after that */
#define KEYBATCH	16384	/* keys per read round */
#define PASTEMAX	1000	/* lines in a paste */
#define PASTEWAIT	150	/* ms without new lines before we ask */
#define HISTMAX		200	/* lines in the input history */
#define MAXIGNORE	64	/* /ignore masks */
#define KEEPNICK_ISON	30	/* seconds between ISON polls without MONITOR */
#define KEEPNICK_RETRY	5	/* at most one NICK attempt per this many seconds */
#define NICKLISTW	18	/* nick list width incl. separator */
#define WINLISTW	21	/* window list width incl. separator */

enum { LVL_NONE, LVL_EVENT, LVL_MSG, LVL_HILITE };
enum { B_MAIN, B_STATUS, B_CHAN, B_QUERY };
enum { ST_OFFLINE, ST_RESOLVING, ST_CONNECTING, ST_HANDSHAKE, ST_REGISTER,
       ST_ONLINE };

struct network;

struct member {
	char		 nick[64];
	int		 modes;		/* bit i = n->prefix_modes[i] */
	time_t		 spoke;		/* for tab completion */
	char		 uhost[128];	/* user@host if known, for /ignore */
};

/* a line in a window: text and style per character */
struct line {
	int		 n;
	int		 indent;	/* characters before the message text */
	wchar_t		*w;
	uint16_t	*a;
};

struct buffer {
	struct network	*net;		/* NULL for the main window */
	int		 kind;
	char		 name[64];
	struct line	*lines[SCROLLBACK];	/* ring buffer */
	int		 start, nlines;
	int		 scroll;	/* number of lines scrolled up */
	int		 unread, hilite;
	int		 joined;	/* we are in the channel right now */
	int		 rejoin;	/* were in it when the connection dropped */
	struct member	*mem;		/* nick list for channels */
	int		 nmem, memcap;
	int		 in_names;	/* receiving 353 right now */
	char		 topic[512];
	char		 cmodes[32];	/* channel modes, e.g. "ntl" (keys not kept) */
	char		 climit[16];	/* the +l limit */
	int		 mode_auto;	/* 1: our MODE query, 2: expect 329 next */
	long		 total;		/* lines ever added */
	long		 mark;		/* read marker: first unread line, -1 = none */
	long		 marknew;	/* unread lines when you came back */
	FILE		*log;		/* log file, opened on the first line */
	char		 logname[64];	/* the name the log was opened with */
	int		 logfail;
	int		 sorted;
};

struct network {
	/* from the config file */
	char		 name[32];
	char		 host[256];
	char		 port[8];
	int		 tls, insecure, sasl, autoconnect;
	int		 qauth;		/* log in to Q (QuakeNet) after connecting */
	int		 qhide;		/* ... and then hide the host with +x */
	int		 qstate;	/* Q_* */
	time_t		 qstart;
	char		 nick[64], user[64], realname[128], account[64];
	char		 bind[64];		/* local IP to connect from */
	char		 pass[256];
	int		 pass_prompted;	/* read via prompt: never saved */
	char		 autojoin[MAXAUTO][128];
	char		 queries[MAXQUERY][64];	/* from the config, opened at start */
	int		 nqueries;
	int		 nauto;

	/* runtime */
	int		 state;
	int		 sock;
	short		 want;
	int		 wantconn;	/* the user wants to be connected */
	struct tls	*ctx;
	struct addrinfo	*ai0, *ai;
	int		 lasterr;
	char		 mynick[64];
	char		 switchto[64];	/* switch window when this JOIN arrives */
	char		 rbuf[8192];
	size_t		 rlen;
	int		 rdiscard;	/* skip to the next \n (line too long) */
	char		*obuf;		/* data the server has not taken yet */
	size_t		 olen, ocap;
	short		 owant;		/* POLLIN/POLLOUT to continue writing */
	time_t		 oprogress;	/* last time the server took data */
	time_t		 ctcp_win;	/* CTCP replies: start of the window */
	int		 ctcp_n;	/* ... and replies sent in it */
	time_t		 started, last_rx, ping_sent, retry_at;
	int		 backoff;
	struct buffer	*status;
	char		 prefix_modes[16];	/* ISUPPORT PREFIX, e.g. "ov" */
	char		 prefix_chars[16];	/* ... and "@+" */
	char		 chanmodes[4][64];	/* ISUPPORT CHANMODES A,B,C,D */
	char		 capreq[128];		/* caps we want */
	int		 away;			/* the server says we are away */
	char		 laddr[64], raddr[64];	/* actual addresses when connected */

	/* keepnick: win back the configured nick when it becomes free */
	int		 has_monitor;		/* ISUPPORT MONITOR */
	int		 maxmodes;		/* ISUPPORT MODES: modes per line */
	int		 monitoring;		/* MONITOR + sent for the nick */
	int		 keepnick_off;		/* you chose another nick with /nick */
	time_t		 nick_try_at;		/* last NICK attempt */
	time_t		 ison_at;		/* last ISON (servers without MONITOR) */

	/* send queue: at most SQBURST lines at once, then one per SQRATE ms */
#ifdef HAVE_ASR
	struct asr_query *aq;			/* DNS lookup in progress */
	int		 dns_fd;
	short		 dns_events;
	long long	 dns_deadline;
#endif
	char		*sq[SQMAX];
	int		 sqhead, sqlen;
	double		 tokens;
	long long	 tok_ms;
};

struct msg {
	char	*nick;
	char	*uhost;		/* user@host from the prefix, or "" */
	char	*cmd;
	char	*params[MAXPARAMS];
	int	 n;
};

#define P(i)	((i) < m->n ? m->params[i] : "")

/* " [user@host]" after the nick in join/part/quit, if the server sent it */
#define UH_L(m)	(*(m)->uhost ? " [" : "")
#define UH_R(m)	(*(m)->uhost ? "]" : "")

static struct buffer	*bufs[MAXBUFS];
static int		 nbufs, cur;
static struct network	*nets[MAXNETS];
static int		 nnets;

static char		 sysname[32] = "unknown";	/* uname -s, for VERSION */
static char		 g_login[16] = "bsdirc";	/* default USER name, see main */
static char		 g_nick[64], g_user[64], g_realname[128], g_bind[64];
static char		 confpath[1024], confdir[1024];
static struct tls_config *cfg_verify, *cfg_insecure;

static wchar_t		 in[INPUTMAX];
static int		 inlen, incur;
static int		 running = 1, dirty = 1, curses_on;
static int		 show_nicklist = 1, show_winlist = 1;

/* paste protection */
static char	*paste[PASTEMAX];
static int	 paste_n, paste_ask, batch_more;
static long long paste_last;


/* tab completion */
static char		 tabc[256][64];
static const char	*tabsuffix = " ";
static int		 ntab, tabi = -1, tabstart;

static const char *commands[] = {
	"autoaway", "autojoin", "away", "ban", "bare", "clear", "close",
	"connect", "ctcp", "deop", "devoice", "disconnect", "help", "ignore",
	"invite", "join", "kb", "kick", "me", "mode", "msg", "names", "nick",
	"nicklist", "op", "part", "pushover", "query", "quit", "raw",
	"reconnect", "save", "server", "serverlist", "set", "topic", "unban",
	"unignore", "url", "voice", "whois", "win", "windows",
};

static void	net_fail(struct network *, const char *);
static void	push_notify(struct network *, const char *, const char *,
		    const char *);
static void	cmd_pushover(char *);
static void	backlog_load(struct buffer *);
static void	term_upgrade(void);
static int	theme_find(const char *);
static void	strip_codes(const char *, char *, size_t);
static void	url_scan(struct buffer *, const char *, const char *);
static void	bare_show(struct buffer *, const char *);
static void	bare_exit(void);
static void	q_start(struct network *);
static void	q_done(struct network *, int);
static void	q_notice(struct network *, const char *);
static long long now_ms(void);
static int	parse_hide(char *);
static const char *hide_words(int);
static void	log_close(struct buffer *);
static void	buf_add(struct buffer *, struct line *);
static struct buffer *sysbuf(void);
static void	host_mask(const char *, char *, size_t);
static int	save_config(void);
static int	valid_name(const char *);
static struct buffer *reply_buf(struct network *);
static void	irc_send(struct network *, const char *, ...)
		    __attribute__((__format__(__printf__, 2, 3)));

static void
fatal(const char *what)
{
	if (curses_on)
		endwin();
	err(1, "%s", what);
}

/* Zero and free a string (freezero is not available everywhere). */
static void
zfree(char *p)
{
	if (p != NULL) {
		explicit_bzero(p, strlen(p));
		free(p);
	}
}

/*
 * strftime() of a local time. localtime() returns NULL for times it
 * cannot represent, such as a huge time stamp from the server, and
 * strftime() must never see that.
 */
static void
fmt_time(char *buf, size_t sz, const char *fmt, time_t t)
{
	struct tm	*tm = localtime(&t);

	if (tm == NULL || strftime(buf, sz, fmt, tm) == 0)
		snprintf(buf, sz, "?");
}

/* ---------- UTF-8 ---------- */

/*
 * Next character from a UTF-8 string. Invalid bytes are read as latin1,
 * so people with old ISO-8859-1 clients still show up with å, ä and ö.
 */
static unsigned long
utf8_next(const unsigned char **pp)
{
	const unsigned char	*p = *pp;
	unsigned int		 c = *p, need = 0, i;
	unsigned long		 wc = c;

	if (c >= 0xc2 && c <= 0xdf) {
		need = 1;
		wc = c & 0x1f;
	} else if (c >= 0xe0 && c <= 0xef) {
		need = 2;
		wc = c & 0x0f;
	} else if (c >= 0xf0 && c <= 0xf4) {
		need = 3;
		wc = c & 0x07;
	}
	for (i = 1; i <= need; i++) {
		if ((p[i] & 0xc0) != 0x80)
			break;
		wc = (wc << 6) | (p[i] & 0x3f);
	}
	if (i <= need) {			/* broken sequence: latin1 */
		wc = c;
		need = 0;
	}
	*pp = p + need + 1;
	if (wc > 0x10ffff || (wc >= 0xd800 && wc <= 0xdfff))
		wc = 0xfffd;			/* invalid code point */
	return wc;
}

/*
 * Control characters, plus the bidirectional overrides and isolates,
 * which could make text from others look like something else.
 */
static int
is_ctrl(unsigned long wc)
{
	return wc < 0x20 || (wc >= 0x7f && wc < 0xa0) || wc == 0x061c ||
	    (wc >= 0x202a && wc <= 0x202e) || (wc >= 0x2066 && wc <= 0x2069);
}

/* Decode to wchar_t. Control characters are dropped, tab becomes a space. */
static size_t
utf8_decode(const char *s, wchar_t *out, size_t outmax)
{
	const unsigned char	*p = (const unsigned char *)s;
	unsigned long		 wc;
	size_t			 n = 0;

	while (*p && n + 1 < outmax) {
		wc = utf8_next(&p);
		if (wc == '\t')
			wc = ' ';
		if (is_ctrl(wc))
			continue;
		out[n++] = (wchar_t)wc;
	}
	out[n] = 0;
	return n;
}

static size_t
utf8_encode(const wchar_t *w, size_t wn, char *out, size_t outmax)
{
	size_t	n = 0, i;

	for (i = 0; i < wn && n + 5 < outmax; i++) {
		unsigned long c = (unsigned long)w[i];

		if (c < 0x80) {
			out[n++] = c;
		} else if (c < 0x800) {
			out[n++] = 0xc0 | (c >> 6);
			out[n++] = 0x80 | (c & 0x3f);
		} else if (c < 0x10000) {
			out[n++] = 0xe0 | (c >> 12);
			out[n++] = 0x80 | ((c >> 6) & 0x3f);
			out[n++] = 0x80 | (c & 0x3f);
		} else {
			out[n++] = 0xf0 | (c >> 18);
			out[n++] = 0x80 | ((c >> 12) & 0x3f);
			out[n++] = 0x80 | ((c >> 6) & 0x3f);
			out[n++] = 0x80 | (c & 0x3f);
		}
	}
	out[n] = 0;
	return n;
}

/* ---------- colours and styles ---------- */

/*
 * Every character on screen has a 16-bit style: foreground (5 bits),
 * background (5 bits), bold, underline and reverse. Colours 0-15 are the
 * terminal's 16 colours, DEF is the terminal's default colour and SOFTYEL
 * a muted yellow (khaki from the 256-colour palette, else plain yellow). The style
 * is stored next to the text, so text from others can never fake the
 * client's own colours.
 */
#define ST(fg, bg, fl)	((uint16_t)((fg) | ((bg) << 5) | (fl)))
#define ST_FG(s)	((s) & 0x1f)
#define ST_BG(s)	(((s) >> 5) & 0x1f)
#define F_BOLD		0x0400
#define F_UNDER		0x0800
#define F_REV		0x1000
#define DEF		16
#define SOFTYEL		17
#define SOFTGREY	18	/* light grey: 250 on 256 colours, else white */

enum { BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE,
       GREY, LRED, LGREEN, LYELLOW, LBLUE, LMAGENTA, LCYAN, LWHITE };

/* the theme */
#define S_DEFAULT	ST(DEF, DEF, 0)
#define S_TIME		ST(GREY, DEF, 0)
#define S_DIM		ST(GREY, DEF, 0)
#define S_EVPFX		ST(LBLUE, DEF, F_BOLD)	/* -!- */
#define S_ERRPFX	ST(LRED, DEF, F_BOLD)	/* -!- on errors and warnings */
#define S_EVENT		ST(DEF, DEF, 0)
#define S_JOIN		ST(LCYAN, DEF, F_BOLD)
#define S_PART		ST(CYAN, DEF, 0)
#define S_UHOST		ST(CYAN, DEF, 0)	/* user@host */
#define S_CHAN		ST(DEF, DEF, F_BOLD)
#define S_ACTION	ST(LMAGENTA, DEF, 0)
#define S_NOTICE	ST(LMAGENTA, DEF, F_BOLD)
#define S_HLTIME	ST(LWHITE, RED, F_BOLD)	/* time stamp: the margin marker */
#define S_WINHL		ST(LRED, DEF, F_BOLD)	/* window list: mentioned */
#define S_WINNEW	ST(LWHITE, DEF, F_BOLD)	/* window list: new messages */
#define S_WINNET	themes[theme].net	/* window list: connected network */
#define S_NICK		ST(DEF, DEF, 0)		/* everyone else, mode prefix too */
/*
 * The status and topic bars come in themes (/set theme NAME): text,
 * punctuation such as [ ], bold text, a window you were mentioned in,
 * and errors such as [offline].
 */
static const struct {
	const char	*name;
	uint16_t	 bar, dim, msg, hl, bad;
	uint16_t	 net;		/* a connected network in the window list */
} themes[] = {
	{ "default", ST(BLACK, SOFTYEL, 0), ST(BLUE, SOFTYEL, 0),
	    ST(BLACK, SOFTYEL, F_BOLD), ST(RED, SOFTYEL, F_BOLD),
	    ST(RED, SOFTYEL, F_BOLD),
	    ST(DEF, DEF, 0) },				/* muted yellow */
	{ "irssi", ST(LWHITE, BLUE, 0), ST(LCYAN, BLUE, 0),
	    ST(LWHITE, BLUE, F_BOLD), ST(LRED, BLUE, F_BOLD),
	    ST(LRED, BLUE, F_BOLD),
	    ST(DEF, DEF, 0) },				/* blue, like irssi */
	{ "bw", ST(BLACK, SOFTGREY, 0), ST(BLACK, SOFTGREY, 0),
	    ST(BLACK, SOFTGREY, F_BOLD), ST(BLACK, SOFTGREY, F_BOLD | F_UNDER),
	    ST(BLACK, SOFTGREY, F_BOLD | F_UNDER),
	    ST(DEF, DEF, 0) },				/* black on light grey */
};
static int	 theme;			/* index into themes[] */

#define S_BAR		themes[theme].bar
#define S_BARDIM	themes[theme].dim
#define S_BARMSG	themes[theme].msg
#define S_BARHL		themes[theme].hl
#define S_BARBAD	themes[theme].bad
#define S_PROMPT	ST(DEF, DEF, 0)	/* [#channel] before the input line */
#define S_SEP		ST(BLUE, DEF, 0)
#define S_MARK		ST(LRED, DEF, 0)	/* the read marker */
#define S_MARKTXT	ST(LRED, DEF, F_BOLD)


static int	 locale_fixed;		/* no UTF-8 locale given, we chose one */
static int	 use_colors = 1;	/* "colors no" in the config or NO_COLOR */
static int	 use_log = 1;		/* "log no" in the config */
static int	 backlog = 50;		/* lines from the log in a new window */
static int	 config_ready;		/* config loaded: changes may be saved */
/*
 * /ignore entries, for all networks. mask is a nick or nick!user@host
 * mask; host is "*!*@host" when the person's host was known, so that a
 * nick change does not get around the ignore.
 */
struct ignore {
	char	mask[128];	/* nick, nick!user@host mask, or #channel */
	char	host[160];
	int	hide;		/* #channel: HIDE_* events not shown there */
};
#define HIDE_JOINS	0x01
#define HIDE_PARTS	0x02
#define HIDE_QUITS	0x04
#define HIDE_NICKS	0x08
#define HIDE_ALL	0x0f
static struct ignore ignores[MAXIGNORE];
static int	 nignore;

/* away: manual with /away, automatic when tmux is detached */
static double	 autoaway = 10;		/* minutes after detach, 0 = off */
static char	 awaymsg[256] = "Away";
static int	 manual_away, auto_away;
static char	 push_token[64], push_user[64];	/* Pushover keys */
static int	 push_mode = -1;		/* PU_*; -1: not set (away) */
static time_t	 detached_at;		/* 0 = the tmux client is attached */
static volatile sig_atomic_t got_detach, got_attach;
static char	 tmux_sock[1024];	/* from $TMUX, empty = not inside tmux */
static int	 tmux_attached = -1;	/* -1 = not checked yet */
static int	 tmux_err;		/* errno from the last stat(), 0 = ok */
static int	 tmux_unveil_failed;
static time_t	 tmux_lastcheck;
static int	 colors_ok, nodefault;
static short	 pairmap[19][19];
static int	 nextpair = 1;

/* mIRC colour 0-15 -> terminal colour, anything else -> def */
static int
mirc_color(int c, int def)
{
	static const uint8_t map[16] = {
		LWHITE, BLACK, BLUE, GREEN, LRED, RED, MAGENTA, YELLOW,
		LYELLOW, LGREEN, CYAN, LCYAN, LBLUE, LMAGENTA, GREY, WHITE,
	};

	return c >= 0 && c < 16 ? map[c] : def;
}

/* The terminal's colour number for one of ours. */
static int
real_color(int c)
{
	if (c == SOFTYEL)
		return COLORS >= 256 ? 186 : YELLOW;	/* #d7d787 */
	if (c == SOFTGREY)
		return COLORS >= 256 ? 250 : WHITE;	/* #bcbcbc */
	return c;
}

static int
get_pair(int fg, int bg)
{
	int	maxp = COLOR_PAIRS < 256 ? COLOR_PAIRS : 256;

	if (fg == DEF && bg == DEF)
		return 0;
	if (pairmap[fg][bg] == 0) {
		if (nextpair >= maxp)
			return 0;
		init_pair(nextpair,
		    fg == DEF ? (nodefault ? WHITE : -1) : real_color(fg),
		    bg == DEF ? (nodefault ? BLACK : -1) : real_color(bg));
		pairmap[fg][bg] = nextpair++;
	}
	return pairmap[fg][bg];
}

static attr_t
style_attr(uint16_t s)
{
	attr_t	a = A_NORMAL;
	int	fg = ST_FG(s), bg = ST_BG(s);

	if (s & F_BOLD)
		a |= A_BOLD;
	if (s & F_UNDER)
		a |= A_UNDERLINE;
	if (s & F_REV)
		a |= A_REVERSE;
	if (!colors_ok)
		return a;
	if (fg > SOFTGREY || bg > SOFTGREY)
		fg = bg = DEF;
	if (COLORS < 16) {		/* bright colours become bold */
		if (fg == GREY)		/* bold black would be invisible */
			fg = WHITE;
		else if (fg < DEF && fg >= 8) {
			fg -= 8;
			a |= A_BOLD;
		}
		if (bg < DEF && bg >= 8)
			bg -= 8;
	}
	return a | COLOR_PAIR(get_pair(fg, bg));
}

/*
 * The line builder: text and style per character. Used both for lines
 * in the windows and for the status bar, the prompt and the nick list.
 */
#define LBMAX	4096
static wchar_t	 lb_w[LBMAX];
static uint16_t	 lb_a[LBMAX];
static int	 lb_n, lb_indent;

static void
lb_clear(void)
{
	lb_n = lb_indent = 0;
}

static void
lb_put(wchar_t c, uint16_t st)
{
	if (lb_n < LBMAX - 1) {
		lb_w[lb_n] = c;
		lb_a[lb_n++] = st;
	}
}

/*
 * Append text in style base. With irc, mIRC codes (bold, colour,
 * reverse, underline, reset) are applied relative to base; otherwise
 * they are dropped. Other control characters are always dropped.
 */
static void
lb_text(uint16_t base, const char *s, int irc)
{
	const unsigned char	*p = (const unsigned char *)s;
	uint16_t		 st = base;
	unsigned long		 wc;
	int			 fg, bg, i;

	while (*p) {
		switch (*p) {
		case 0x02:
			p++;
			if (irc)
				st ^= F_BOLD;
			continue;
		case 0x1f:
			p++;
			if (irc)
				st ^= F_UNDER;
			continue;
		case 0x16:
			p++;
			if (irc)
				st ^= F_REV;
			continue;
		case 0x0f:
			p++;
			st = base;
			continue;
		case 0x1d:			/* italic, strikethrough, monospace */
		case 0x1e:
		case 0x11:
			p++;
			continue;
		case 0x04:			/* hex colour ^Drrggbb[,rrggbb]: ignored */
			p++;
			for (i = 0; i < 6 && isxdigit(*p); i++)
				p++;
			if (*p == ',' && isxdigit(p[1]))
				for (p++, i = 0; i < 6 && isxdigit(*p); i++)
					p++;
			continue;
		case 0x03:			/* ^Cfg[,bg] */
			p++;
			fg = bg = -1;
			if (isdigit(*p)) {
				fg = *p++ - '0';
				if (isdigit(*p))
					fg = fg * 10 + (*p++ - '0');
				if (*p == ',' && isdigit(p[1])) {
					p++;
					bg = *p++ - '0';
					if (isdigit(*p))
						bg = bg * 10 + (*p++ - '0');
				}
			}
			if (!irc)
				continue;
			if (fg == -1)		/* just ^C: back to the default */
				st = (st & ~0x3ff) | (base & 0x3ff);
			else {
				st = (st & ~0x1f) | mirc_color(fg, ST_FG(base));
				if (bg != -1)
					st = (st & ~(0x1f << 5)) |
					    (mirc_color(bg, ST_BG(base)) << 5);
			}
			continue;
		}
		wc = utf8_next(&p);
		if (wc == '\t')
			wc = ' ';
		if (is_ctrl(wc))
			continue;
		lb_put((wchar_t)wc, st);
	}
}

static void
lb_stamp(void)
{
	char	tb[16];
	time_t	t = time(NULL);

	fmt_time(tb, sizeof(tb), "%H:%M ", t);
	lb_text(S_TIME, tb, 0);
	lb_indent = lb_n;
}

/* The time stamp on a line where you are mentioned: a red block. */
static void
lb_stamp_hl(void)
{
	char	tb[16];
	time_t	t = time(NULL);

	fmt_time(tb, sizeof(tb), "%H:%M", t);
	lb_text(S_HLTIME, tb, 0);
	lb_put(' ', S_DEFAULT);
	lb_indent = lb_n;
}

static int
is_nickch(wchar_t c)
{
	return iswalnum(c) || (c && wcschr(L"[]\\`_^{|}-", c) != NULL);
}

/* Mark your nick (as a whole word) in lb_w[start..] with style st. */
static void
lb_mark(int start, const char *nick, uint16_t st)
{
	wchar_t	w[64];
	int	len, i, j;

	if ((len = utf8_decode(nick, w, 64)) == 0)
		return;
	for (i = start; i + len <= lb_n; i++) {
		if (i > start && is_nickch(lb_w[i - 1]))
			continue;
		if (i + len < lb_n && is_nickch(lb_w[i + len]))
			continue;
		for (j = 0; j < len; j++)
			if (towlower(lb_w[i + j]) != towlower(w[j]))
				break;
		if (j == len)
			for (j = 0; j < len; j++)
				lb_a[i + j] = st;
	}
}

/* Draw lb at (y, x), at most maxw columns. Returns the width. */
static int	cw(wchar_t);

static int
lb_draw(int y, int x, int maxw)
{
	int	i, used = 0, c;

	move(y, x);
	for (i = 0; i < lb_n; i++) {
		c = cw(lb_w[i]);
		if (used + c > maxw)
			break;
		attrset(style_attr(lb_a[i]));
		addnwstr(&lb_w[i], 1);
		used += c;
	}
	attrset(A_NORMAL);
	return used;
}

/* ---------- small helpers ---------- */

static int
ischan(const char *s)
{
	return *s == '#' || *s == '&' || *s == '+' || *s == '!';
}

static char *
trim(char *s)
{
	char	*e;

	while (isspace((unsigned char)*s))
		s++;
	e = s + strlen(s);
	while (e > s && isspace((unsigned char)e[-1]))
		*--e = 0;
	return s;
}

/* Take the next word from *s and advance *s. Empty string at the end. */
static char *
word(char **s)
{
	char	*w, *p = *s;

	while (*p == ' ')
		p++;
	w = p;
	while (*p && *p != ' ')
		p++;
	if (*p)
		*p++ = 0;
	while (*p == ' ')
		p++;
	*s = p;
	return w;
}

/* Is s a numeric IPv4 or IPv6 address? (no DNS lookup) */
static int
valid_ip(const char *s)
{
	struct addrinfo	hints, *res;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_flags = AI_NUMERICHOST;
	if (getaddrinfo(s, NULL, &hints, &res) != 0)
		return 0;
	freeaddrinfo(res);
	return 1;
}

/* A whole number in [min, max] (min >= 0), or -1. */
static int
parse_int(const char *s, int min, int max)
{
	char	*e;
	long	 v;

	if (*s == 0)
		return -1;
	errno = 0;
	v = strtol(s, &e, 10);
	if (*e || errno || v < min || v > max)
		return -1;
	return (int)v;
}

static int
parse_bool(const char *v, int *out)
{
	if (!strcasecmp(v, "yes") || !strcasecmp(v, "on") ||
	    !strcasecmp(v, "true") || !strcmp(v, "1"))
		*out = 1;
	else if (!strcasecmp(v, "no") || !strcasecmp(v, "off") ||
	    !strcasecmp(v, "false") || !strcmp(v, "0"))
		*out = 0;
	else
		return -1;
	return 0;
}

static int
valid_port(const char *p)
{
	const char	*s;
	long		 v;

	if (*p == 0 || strlen(p) > 5)
		return 0;
	for (s = p; *s; s++)
		if (!isdigit((unsigned char)*s))
			return 0;
	v = strtol(p, NULL, 10);
	return v > 0 && v < 65536;
}

static int
valid_netname(const char *s)
{
	if (*s == 0 || strlen(s) >= sizeof(((struct network *)0)->name))
		return 0;
	for (; *s; s++)
		if (!isalnum((unsigned char)*s) && *s != '-' && *s != '_')
			return 0;
	return 1;
}

static const char *
net_nick(struct network *n)
{
	return n->nick[0] ? n->nick : g_nick;
}

static const char *
net_user(struct network *n)
{
	return n->user[0] ? n->user : (g_user[0] ? g_user : g_login);
}

static const char *
net_realname(struct network *n)
{
	return n->realname[0] ? n->realname :
	    (g_realname[0] ? g_realname : net_nick(n));
}

static const char *
net_account(struct network *n)
{
	return n->account[0] ? n->account : net_nick(n);
}

static const char *
state_name(int st)
{
	switch (st) {
	case ST_OFFLINE:	return "offline";
	case ST_RESOLVING:	return "resolving";
	case ST_CONNECTING:	return "connecting";
	case ST_HANDSHAKE:	return "tls";
	case ST_REGISTER:	return "registering";
	default:		return "online";
	}
}

/* ---------- buffers ---------- */

static struct buffer *
buf_find(struct network *n, const char *name)
{
	int	i;

	for (i = 0; i < nbufs; i++)
		if (bufs[i]->net == n && bufs[i]->kind >= B_CHAN &&
		    strcasecmp(bufs[i]->name, name) == 0)
			return bufs[i];
	return NULL;
}

static struct buffer *
buf_new(struct network *n, const char *name, int kind)
{
	struct buffer	*b;
	int		 i, pos;

	if (nbufs == MAXBUFS)
		return n ? n->status : sysbuf();
	if ((b = calloc(1, sizeof(*b))) == NULL)
		fatal("calloc");
	b->net = n;
	b->kind = kind;
	b->mark = -1;
	snprintf(b->name, sizeof(b->name), "%s", name);

	/*
	 * Keep windows grouped per network, in the same order as in the
	 * window list: put the new one after the network's last window.
	 */
	pos = nbufs;
	if (n != NULL)
		for (i = 0; i < nbufs; i++)
			if (bufs[i]->net == n)
				pos = i + 1;
	memmove(&bufs[pos + 1], &bufs[pos], (nbufs - pos) * sizeof(bufs[0]));
	bufs[pos] = b;
	nbufs++;
	if (nbufs > 1 && pos <= cur)
		cur++;
	backlog_load(b);
	if (kind == B_QUERY && n != NULL && config_ready)
		save_config();		/* the query is there after a restart */
	dirty = 1;
	return b;
}

/* Nicks and channels: not empty, no control characters, spaces or commas. */
static int
valid_name(const char *s)
{
	if (*s == 0)
		return 0;
	for (; *s; s++)
		if ((unsigned char)*s <= 0x20 || *s == ',' || *s == 0x7f)
			return 0;
	return 1;
}

static struct buffer *
buf_get(struct network *n, const char *name)
{
	struct buffer	*b;

	if (!valid_name(name))		/* broken or malicious name */
		return n->status;
	if ((b = buf_find(n, name)) != NULL)
		return b;
	return buf_new(n, name, ischan(name) ? B_CHAN : B_QUERY);
}

static int
buf_index(struct buffer *b)
{
	int	i;

	for (i = 0; i < nbufs; i++)
		if (bufs[i] == b)
			return i;
	return 0;
}

static struct line *
buf_line(struct buffer *b, int i)
{
	return b->lines[(b->start + i) % SCROLLBACK];
}

static void
buf_clear(struct buffer *b)
{
	int	i;

	for (i = 0; i < b->nlines; i++)
		free(buf_line(b, i));
	b->start = b->nlines = b->scroll = 0;
	b->mark = -1;
	dirty = 1;
}

/* ---------- nick list ---------- */

static struct network	*sortnet;

static int
prefix_rank(struct network *n, int modes)
{
	int	i, l = strlen(n->prefix_chars);

	for (i = 0; i < l; i++)
		if (modes & (1 << i))
			return i;
	return 15;
}

static char
prefix_char(struct network *n, int modes)
{
	int	r = prefix_rank(n, modes);

	return r < (int)strlen(n->prefix_chars) ? n->prefix_chars[r] : ' ';
}

/* "@+kalle" -> modes, and *nick points to "kalle" */
static int
strip_prefix(struct network *n, const char **nick)
{
	const char	*p;
	int		 modes = 0;

	while (**nick && (p = strchr(n->prefix_chars, **nick)) != NULL) {
		modes |= 1 << (p - n->prefix_chars);
		(*nick)++;
	}
	return modes;
}

static struct member *
mem_find(struct buffer *b, const char *nick)
{
	int	i;

	for (i = 0; i < b->nmem; i++)
		if (strcasecmp(b->mem[i].nick, nick) == 0)
			return &b->mem[i];
	return NULL;
}

static void
mem_add(struct buffer *b, const char *nick, int modes)
{
	struct member	*m;

	if (!valid_name(nick))
		return;
	if ((m = mem_find(b, nick)) != NULL) {
		m->modes |= modes;
	} else {
		if (b->nmem == b->memcap) {
			int newcap = b->memcap ? b->memcap * 2 : 64;

			if ((m = reallocarray(b->mem, newcap, sizeof(*m))) == NULL)
				fatal("reallocarray");
			b->mem = m;
			b->memcap = newcap;
		}
		m = &b->mem[b->nmem++];
		snprintf(m->nick, sizeof(m->nick), "%s", nick);
		m->modes = modes;
		m->spoke = 0;
		m->uhost[0] = 0;
	}
	b->sorted = 0;
	dirty = 1;
}

static int
mem_del(struct buffer *b, const char *nick)
{
	struct member	*m;

	if ((m = mem_find(b, nick)) == NULL)
		return 0;
	*m = b->mem[--b->nmem];
	b->sorted = 0;
	dirty = 1;
	return 1;
}

static void
mem_clear(struct buffer *b)
{
	b->nmem = 0;
	b->in_names = 0;
	dirty = 1;
}

static int
mem_cmp(const void *a, const void *b)
{
	const struct member *x = a, *y = b;
	int ra = prefix_rank(sortnet, x->modes), rb = prefix_rank(sortnet, y->modes);

	if (ra != rb)
		return ra - rb;
	return strcasecmp(x->nick, y->nick);
}

static void
mem_sort(struct buffer *b)
{
	if (b->sorted || b->nmem == 0)
		return;
	sortnet = b->net;
	qsort(b->mem, b->nmem, sizeof(b->mem[0]), mem_cmp);
	b->sorted = 1;
}

/* "@" or "" to show in front of the nick in messages */
static const char *
nick_prefix(struct buffer *b, const char *nick)
{
	static char	 s[2];
	struct member	*m;

	s[0] = s[1] = 0;
	if (b->kind == B_CHAN && (m = mem_find(b, nick)) != NULL &&
	    (s[0] = prefix_char(b->net, m->modes)) == ' ')
		s[0] = 0;
	return s;
}

static void
buf_close(int i)
{
	int	query;

	if (i < 0 || i >= nbufs)
		return;
	query = bufs[i]->kind == B_QUERY && bufs[i]->net != NULL;
	buf_clear(bufs[i]);
	log_close(bufs[i]);
	free(bufs[i]->mem);
	free(bufs[i]);
	memmove(&bufs[i], &bufs[i + 1], (nbufs - i - 1) * sizeof(bufs[0]));
	nbufs--;
	if (cur >= i)
		cur--;
	if (cur < 0)
		cur = 0;
	if (query && config_ready)
		save_config();		/* closed queries do not come back */
	dirty = 1;
}

/*
 * The window for bsdirc's own messages: the main window while there are
 * no networks, otherwise the first network's status window.
 */
static struct buffer *
sysbuf(void)
{
	int	i;

	for (i = 0; i < nbufs; i++)
		if (bufs[i]->kind == B_MAIN || bufs[i]->kind == B_STATUS)
			return bufs[i];
	return bufs[0];
}

/*
 * The main window only exists while there are no networks. Once a
 * network exists, move its lines to the first status window and remove it.
 */
static void
main_merge(void)
{
	struct buffer	*m = NULL, *st = NULL;
	int		 i, mi = -1, wascur;

	for (i = 0; i < nbufs; i++) {
		if (bufs[i]->kind == B_MAIN) {
			m = bufs[i];
			mi = i;
		} else if (bufs[i]->kind == B_STATUS && st == NULL)
			st = bufs[i];
	}
	if (m == NULL || st == NULL)
		return;
	for (i = 0; i < m->nlines; i++)
		buf_add(st, buf_line(m, i));
	m->nlines = m->start = 0;	/* the lines now belong to st */
	wascur = cur == mi;
	buf_close(mi);
	if (wascur)
		cur = buf_index(st);
	dirty = 1;
}

static void
buf_switch(int i)
{
	if (i < 0 || i >= nbufs)
		return;
	/* leaving a channel or query sets the read marker where you were */
	if (i != cur && cur < nbufs && bufs[cur]->kind >= B_CHAN)
		bufs[cur]->mark = bufs[cur]->total;
	cur = i;
	/*
	 * Back in the window: if nothing arrived while you were away no
	 * marker is needed. Otherwise the count is frozen, so lines that
	 * arrive while you are looking are not counted as unread.
	 */
	if (bufs[i]->mark >= 0) {
		if (bufs[i]->mark >= bufs[i]->total)
			bufs[i]->mark = -1;
		else
			bufs[i]->marknew = bufs[i]->total - bufs[i]->mark;
	}
	bufs[i]->unread = bufs[i]->hilite = 0;
	dirty = 1;
}

static void
buf_add(struct buffer *b, struct line *l)
{
	b->total++;
	if (b->nlines < SCROLLBACK) {
		b->lines[(b->start + b->nlines) % SCROLLBACK] = l;
		b->nlines++;
	} else {
		free(b->lines[b->start]);
		b->lines[b->start] = l;
		b->start = (b->start + 1) % SCROLLBACK;
	}
}

static void
buf_label(struct buffer *b, char *out, size_t sz)
{
	if (b->kind == B_MAIN)
		snprintf(out, sz, "bsdirc");
	else if (b->kind == B_STATUS)
		snprintf(out, sz, "%s", b->net->name);
	else
		snprintf(out, sz, "%s/%s", b->net->name, b->name);
}

/* ---------- logging ---------- */

/* A name usable as a file name: lower case, no / or leading dot. */
static void
log_fname(const char *src, char *out, size_t sz)
{
	size_t	i;

	for (i = 0; src[i] && i + 1 < sz; i++) {
		unsigned char c = src[i];

		out[i] = c == '/' || c == '\\' || c < 0x20 ? '_' : tolower(c);
	}
	out[i] = 0;
	if (out[0] == 0 && sz >= 2) {		/* empty name */
		out[0] = '_';
		out[1] = 0;
	} else if (out[0] == '.')
		out[0] = '_';
}

static void
log_close(struct buffer *b)
{
	if (b->log != NULL) {
		fclose(b->log);
		b->log = NULL;
	}
}

/* Open ~/.bsdirc/logs/<network>/<channel>.log for appending (mode 0600). */
static int
log_open(struct buffer *b)
{
	char	path[1400], net[64], name[64];
	int	fd;

	log_fname(b->net->name, net, sizeof(net));
	log_fname(b->name, name, sizeof(name));
	snprintf(path, sizeof(path), "%s/logs", confdir);
	if (mkdir(path, 0700) == -1 && errno != EEXIST)
		return -1;
	snprintf(path, sizeof(path), "%s/logs/%s", confdir, net);
	if (mkdir(path, 0700) == -1 && errno != EEXIST)
		return -1;
	snprintf(path, sizeof(path), "%s/logs/%s/%s.log", confdir, net, name);
	if ((fd = open(path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC,
	    0600)) == -1)
		return -1;
	if ((b->log = fdopen(fd, "a")) == NULL) {
		close(fd);
		return -1;
	}
	setvbuf(b->log, NULL, _IOLBF, 0);
	snprintf(b->logname, sizeof(b->logname), "%s", b->name);
	return 0;
}

/* Write the line in the line builder to the window's log, with date. */
static void
log_line(struct buffer *b)
{
	char	out[LBMAX * 4 + 1], ts[32];
	time_t	t = time(NULL);
	int	skip;

	if (!use_log || b->logfail || b->net == NULL ||
	    (b->kind != B_CHAN && b->kind != B_QUERY))
		return;
	if (b->log != NULL && strcmp(b->logname, b->name) != 0)
		log_close(b);			/* the query was renamed */
	if (b->log == NULL && log_open(b) == -1) {
		b->logfail = 1;
		return;
	}
	skip = lb_n >= 6 ? 6 : 0;		/* "HH:MM " is already in ts */
	utf8_encode(lb_w + skip, lb_n - skip, out, sizeof(out));
	fmt_time(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", t);
	fprintf(b->log, "%s %s\n", ts, out);
}

/* Store what is in the line builder as a new line in b. */
static void
lb_store(struct buffer *b)
{
	struct line	*l;

	if ((l = malloc(sizeof(*l) +
	    (lb_n + 1) * (sizeof(wchar_t) + sizeof(uint16_t)))) == NULL)
		fatal("malloc");
	l->n = lb_n;
	l->indent = lb_indent;
	l->w = (wchar_t *)(l + 1);
	l->a = (uint16_t *)(l->w + lb_n + 1);
	wmemcpy(l->w, lb_w, lb_n);
	l->w[lb_n] = 0;
	memcpy(l->a, lb_a, lb_n * sizeof(uint16_t));
	l->a[lb_n] = 0;
	buf_add(b, l);
}

/* Store the line in the line builder in b, log it, and mark activity. */
static void
lb_commit(struct buffer *b, int lvl)
{
	log_line(b);
	lb_store(b);
	if (b->scroll > 0)		/* keep the view when scrolled up */
		b->scroll++;
	if (b != bufs[cur]) {
		if (lvl >= LVL_MSG)
			b->unread = 1;
		if (lvl >= LVL_HILITE) {
			b->hilite = 1;
			if (curses_on)
				beep();
		}
	}
	dirty = 1;
}

/*
 * A new channel or query window: show the last lines of its log, so a
 * restart does not lose what was said. Only the end of the file is
 * read. The lines are grey, go through the same filter as everything
 * else, and count neither as new messages nor as highlights.
 */
static void
backlog_load(struct buffer *b)
{
	static wchar_t	 sw[LBMAX];	/* the line builder, put back after */
	static uint16_t	 sa[LBMAX];
	char		 path[1400], net[64], name[64], sep[96], *buf, *p, *e;
	char		 last[17];
	char		**lines = NULL;
	struct stat	 st;
	off_t		 off;
	ssize_t		 r;
	size_t		 len, got = 0;
	int		 fd, n = 0, cap = 0, i, sn = lb_n, si = lb_indent;

	if (!use_log || backlog <= 0 || b->net == NULL ||
	    (b->kind != B_CHAN && b->kind != B_QUERY))
		return;
	log_fname(b->net->name, net, sizeof(net));
	log_fname(b->name, name, sizeof(name));
	snprintf(path, sizeof(path), "%s/logs/%s/%s.log", confdir, net, name);
	if ((fd = open(path, O_RDONLY | O_CLOEXEC)) == -1)
		return;
	if (fstat(fd, &st) == -1 || st.st_size <= 0) {
		close(fd);
		return;
	}
	len = st.st_size > BACKLOGREAD ? BACKLOGREAD : (size_t)st.st_size;
	off = st.st_size - len;
	if ((buf = malloc(len + 1)) == NULL)
		fatal("malloc");
	while (got < len && (r = pread(fd, buf + got, len - got,
	    off + got)) > 0)
		got += r;
	close(fd);
	buf[got] = 0;

	/* the lines, the first one skipped if it was cut in the middle */
	p = buf;
	if (off > 0 && (p = strchr(buf, '\n')) != NULL)
		p++;
	for (; p != NULL && *p; p = e) {
		if ((e = strchr(p, '\n')) != NULL)
			*e++ = 0;
		/* "2026-11-05 10:01:22 <nick> text" */
		if (strlen(p) < 21 || p[4] != '-' || p[10] != ' ' ||
		    p[13] != ':' || p[19] != ' ')
			continue;
		if (n == cap) {
			cap = cap ? cap * 2 : 256;
			if ((lines = reallocarray(lines, cap,
			    sizeof(*lines))) == NULL)
				fatal("reallocarray");
		}
		lines[n++] = p;
	}

	if (n > 0) {
		wmemcpy(sw, lb_w, sn);
		memcpy(sa, lb_a, sn * sizeof(sa[0]));
		for (i = n > backlog ? n - backlog : 0; i < n; i++) {
			char hm[7];

			snprintf(hm, sizeof(hm), "%.5s ", lines[i] + 11);
			lb_clear();
			lb_text(S_DIM, hm, 0);
			lb_indent = lb_n;
			lb_text(S_DIM, lines[i] + 20, 0);
			lb_store(b);
		}
		snprintf(last, sizeof(last), "%.16s", lines[n - 1]);
		snprintf(sep, sizeof(sep), "─── backlog until "
		    "%s ───", last);
		lb_clear();
		lb_text(S_DIM, sep, 0);
		lb_store(b);
		wmemcpy(lb_w, sw, sn);
		memcpy(lb_a, sa, sn * sizeof(sa[0]));
		lb_n = sn;
		lb_indent = si;
	}
	free(lines);
	free(buf);
}

/*
 * Ordinary lines. "-!- ..." becomes an event line with a coloured prefix
 * (red on errors), and mIRC colours in the text are shown.
 */
static void
vbprint(struct buffer *b, int lvl, const char *fmt, va_list ap)
{
	char	raw[2048];

	vsnprintf(raw, sizeof(raw), fmt, ap);
	lb_clear();
	lb_stamp();
	if (strncmp(raw, "-!- ", 4) == 0) {
		lb_text(lvl >= LVL_HILITE ? S_ERRPFX : S_EVPFX, "-!-", 0);
		lb_indent = lb_n + 1;
		lb_text(S_EVENT, raw + 3, 1);
	} else
		lb_text(S_DEFAULT, raw, 1);
	lb_commit(b, lvl);
}

/*
 * A line in several styles: eprint(b, lvl, style, "text", style, "text", ..., -1)
 */
static void
eprint(struct buffer *b, int lvl, ...)
{
	va_list		 ap;
	int		 st, first = 1;
	const char	*s;

	lb_clear();
	lb_stamp();
	va_start(ap, lvl);
	while ((st = va_arg(ap, int)) >= 0) {
		s = va_arg(ap, const char *);
		lb_text((uint16_t)st, s ? s : "", 1);
		if (first) {		/* indent after "-!- " */
			lb_indent = lb_n;
			first = 0;
		}
	}
	va_end(ap);
	lb_commit(b, lvl);
}

enum { M_MSG, M_ACTION, M_NOTICE };

/*
 * A message from nick. own: you wrote it. mention: someone mentioned
 * your nick, so the time stamp becomes a red block in the margin.
 */
static void
print_msg(struct buffer *b, int lvl, int kind, const char *nick,
    const char *pfx, const char *text, int own, int mention)
{
	uint16_t	ns, ts;

	mention = mention && !own;	/* only the red time stamp marks it */
	ns = S_NICK;
	ts = kind == M_ACTION ? S_ACTION : S_DEFAULT;
	lb_clear();
	if (mention)
		lb_stamp_hl();
	else
		lb_stamp();
	switch (kind) {
	case M_MSG:
		lb_text(S_DIM, "<", 0);
		if (pfx && *pfx)
			lb_text(ns, pfx, 0);	/* @ in the nick's colour */
		lb_text(ns, nick, 0);
		lb_text(S_DIM, ">", 0);
		break;
	case M_ACTION:
		lb_text(S_ACTION, " * ", 0);
		lb_text(ns, nick, 0);
		break;
	case M_NOTICE:
		lb_text(S_NOTICE, "-", 0);
		lb_text(S_NOTICE, nick, 0);
		lb_text(S_NOTICE, "-", 0);
		break;
	}
	lb_put(' ', S_DEFAULT);
	lb_indent = lb_n;
	lb_text(ts, text, 1);
	url_scan(b, nick, text);
	lb_commit(b, lvl);
}

static void __attribute__((__format__(__printf__, 3, 4)))
bprint(struct buffer *b, int lvl, const char *fmt, ...)
{
	va_list	ap;

	va_start(ap, fmt);
	vbprint(b, lvl, fmt, ap);
	va_end(ap);
}

/* Print in the network's status window, and in the current window if it belongs to the network. */
static void __attribute__((__format__(__printf__, 3, 4)))
nprint(struct network *n, int lvl, const char *fmt, ...)
{
	va_list	ap;

	va_start(ap, fmt);
	vbprint(n->status, lvl, fmt, ap);
	va_end(ap);
	if (bufs[cur] != n->status && bufs[cur]->net == n) {
		va_start(ap, fmt);
		vbprint(bufs[cur], LVL_NONE, fmt, ap);
		va_end(ap);
	}
}

/* Print in the window the user is looking at. */
static void __attribute__((__format__(__printf__, 1, 2)))
uprint(const char *fmt, ...)
{
	va_list	ap;

	va_start(ap, fmt);
	vbprint(bufs[cur], LVL_NONE, fmt, ap);
	va_end(ap);
}

/* ---------- networks: structure ---------- */

static struct network *
net_find(const char *name)
{
	int	i;

	for (i = 0; i < nnets; i++)
		if (strcasecmp(nets[i]->name, name) == 0)
			return nets[i];
	return NULL;
}

static struct network *
net_new(const char *name)
{
	struct network	*n;

	if (nnets == MAXNETS)
		return NULL;
	if ((n = calloc(1, sizeof(*n))) == NULL)
		fatal("calloc");
	snprintf(n->name, sizeof(n->name), "%s", name);
	snprintf(n->port, sizeof(n->port), "6667");
	n->tls = -1;			/* not given: decided by the port */
	n->sock = -1;
	n->want = POLLIN;
	n->backoff = BACKOFFMIN;
	n->qhide = 1;
	snprintf(n->prefix_modes, sizeof(n->prefix_modes), "ov");
	snprintf(n->prefix_chars, sizeof(n->prefix_chars), "@+");
	snprintf(n->chanmodes[0], sizeof(n->chanmodes[0]), "beI");
	snprintf(n->chanmodes[1], sizeof(n->chanmodes[1]), "k");
	snprintf(n->chanmodes[2], sizeof(n->chanmodes[2]), "l");
	n->status = buf_new(n, name, B_STATUS);
	nets[nnets++] = n;
	return n;
}

/* The network of the current window, otherwise the first connected one. */
static struct network *
curnet(void)
{
	int	i;

	if (bufs[cur]->net != NULL)
		return bufs[cur]->net;
	for (i = 0; i < nnets; i++)
		if (nets[i]->state == ST_ONLINE)
			return nets[i];
	return nnets ? nets[0] : NULL;
}

static int
auto_find(struct network *n, const char *chan)
{
	char	tmp[128], *p;
	int	i;

	for (i = 0; i < n->nauto; i++) {
		snprintf(tmp, sizeof(tmp), "%s", n->autojoin[i]);
		if ((p = strchr(tmp, ' ')) != NULL)
			*p = 0;
		if (strcasecmp(tmp, chan) == 0)
			return i;
	}
	return -1;
}

static int
auto_add(struct network *n, const char *entry)
{
	char	chan[128], *p;
	int	i;

	snprintf(chan, sizeof(chan), "%s", entry);
	if ((p = strchr(chan, ' ')) != NULL)
		*p = 0;
	if (!ischan(chan))
		return -1;
	if ((i = auto_find(n, chan)) == -1) {
		if (n->nauto == MAXAUTO)
			return -1;
		i = n->nauto++;
	}
	snprintf(n->autojoin[i], sizeof(n->autojoin[i]), "%s", entry);
	return 0;
}

static int
auto_del(struct network *n, const char *chan)
{
	int	i;

	if ((i = auto_find(n, chan)) == -1)
		return -1;
	memmove(n->autojoin[i], n->autojoin[i + 1],
	    (n->nauto - i - 1) * sizeof(n->autojoin[0]));
	n->nauto--;
	return 0;
}

/*
 * Create the windows for the autojoin channels up front, in the order of
 * the config file. That way every channel always gets the same window
 * number, whatever order the server answers in, and a channel you could
 * not join keeps its place (shown as not joined) instead of the windows
 * after it moving up.
 */
static void
auto_windows(struct network *n)
{
	char	chan[128], *p;
	int	i;

	for (i = 0; i < n->nauto; i++) {
		snprintf(chan, sizeof(chan), "%s", n->autojoin[i]);
		if ((p = strchr(chan, ' ')) != NULL)
			*p = 0;
		if (ischan(chan))
			buf_get(n, chan);
	}
	/* then the query windows that were open, each with its backlog */
	for (i = 0; i < n->nqueries; i++)
		buf_get(n, n->queries[i]);
	n->nqueries = 0;
}

/* ---------- config file ---------- */

/*
 * Format (INI style):
 *
 *	nick _empty
 *	realname Johan
 *
 *	[libera]
 *	host irc.libera.chat
 *	port 6697
 *	tls yes
 *	sasl yes
 *	password secret
 *	autoconnect yes
 *	join #runbsd
 *	join #secret key
 *
 * Lines starting with # (after whitespace) are comments.
 */
static int
net_set(struct network *n, const char *key, const char *val)
{
	int	b;

	if (strcmp(val, "-") == 0)
		val = "";
#define STR(f)	snprintf(n->f, sizeof(n->f), "%s", val)
	if (!strcmp(key, "host"))
		STR(host);
	else if (!strcmp(key, "port")) {
		if (!valid_port(val))
			return -1;
		STR(port);
	} else if (!strcmp(key, "nick"))
		STR(nick);
	else if (!strcmp(key, "user"))
		STR(user);
	else if (!strcmp(key, "realname"))
		STR(realname);
	else if (!strcmp(key, "account"))
		STR(account);
	else if (!strcmp(key, "bind")) {
		if (*val && !valid_ip(val))
			return -1;
		STR(bind);
	}
	else if (!strcmp(key, "password")) {
		STR(pass);
		n->pass_prompted = 0;
	} else if (!strcmp(key, "join")) {
		return auto_add(n, val);
	} else if (!strcmp(key, "query")) {
		/* a query window that was open when bsdirc last saved */
		if (!valid_name(val) || ischan(val) || n->nqueries == MAXQUERY)
			return -1;
		snprintf(n->queries[n->nqueries++], sizeof(n->queries[0]),
		    "%s", val);
	} else if (!strcmp(key, "tls") || !strcmp(key, "insecure") ||
	    !strcmp(key, "sasl") || !strcmp(key, "autoconnect") ||
	    !strcmp(key, "qauth") || !strcmp(key, "qhide")) {
		if (parse_bool(val, &b) == -1)
			return -1;
		if (!strcmp(key, "tls"))
			n->tls = b;
		else if (!strcmp(key, "insecure"))
			n->insecure = b;
		else if (!strcmp(key, "sasl"))
			n->sasl = b;
		else if (!strcmp(key, "qauth"))
			n->qauth = b;
		else if (!strcmp(key, "qhide"))
			n->qhide = b;
		else
			n->autoconnect = b;
	} else
		return -1;
#undef STR
	return 0;
}

static void
load_config(const char *path)
{
	FILE		*f;
	struct stat	 st;
	struct network	*n = NULL;
	char		 line[1024], *s, *key, *val, *e;
	int		 lineno = 0, haspass = 0, i;

	if ((f = fopen(path, "r")) == NULL) {
		if (errno == ENOENT)
			return;
		err(1, "%s", path);
	}
	while (fgets(line, sizeof(line), f) != NULL) {
		lineno++;
		s = trim(line);
		if (*s == 0 || *s == '#' || *s == ';')
			continue;
		if (*s == '[') {
			if ((e = strchr(s, ']')) == NULL)
				errx(1, "%s:%d: missing ]", path, lineno);
			*e = 0;
			s = trim(s + 1);
			if (!valid_netname(s))
				errx(1, "%s:%d: bad network name", path, lineno);
			if (net_find(s) != NULL)
				errx(1, "%s:%d: duplicate network %s",
				    path, lineno, s);
			if ((n = net_new(s)) == NULL)
				errx(1, "%s:%d: too many networks", path, lineno);
			continue;
		}
		key = s;
		while (*s && !isspace((unsigned char)*s))
			s++;
		if (*s)
			*s++ = 0;
		val = trim(s);
		if (n == NULL) {
			if (!strcmp(key, "nick"))
				snprintf(g_nick, sizeof(g_nick), "%s", val);
			else if (!strcmp(key, "user"))
				snprintf(g_user, sizeof(g_user), "%s", val);
			else if (!strcmp(key, "realname"))
				snprintf(g_realname, sizeof(g_realname), "%s", val);
			else if (!strcmp(key, "bind")) {
				if (!valid_ip(val))
					warnx("%s:%d: bind needs an IP address", path,
					    lineno);
				else
					snprintf(g_bind, sizeof(g_bind), "%s", val);
			} else if (!strcmp(key, "autoaway")) {
				char *ep;

				autoaway = strtod(val, &ep);
				if (*ep || autoaway < 0) {
					warnx("%s:%d: autoaway minutes", path, lineno);
					autoaway = 10;
				}
			} else if (!strcmp(key, "awaymsg"))
				snprintf(awaymsg, sizeof(awaymsg), "%s", val);
			else if (!strcmp(key, "ignore")) {
				/* ignore MASK [*!*@host] */
				char *v2 = val, *mk = word(&v2), *hk;
				int bits;

				if (nignore < MAXIGNORE && ischan(mk)) {
					/* ignore #channel joins parts ... */
					if ((bits = parse_hide(v2)) == -1)
						warnx("%s:%d: ignore #channel "
						    "joins|parts|quits|nicks",
						    path, lineno);
					else {
						memset(&ignores[nignore], 0,
						    sizeof(ignores[0]));
						snprintf(ignores[nignore].mask,
						    sizeof(ignores[0].mask),
						    "%s", mk);
						ignores[nignore++].hide = bits;
					}
					continue;
				}
				hk = word(&v2);
				if (nignore < MAXIGNORE && *mk) {
					snprintf(ignores[nignore].mask,
					    sizeof(ignores[0].mask), "%s", mk);
					snprintf(ignores[nignore].host,
					    sizeof(ignores[0].host), "%s", hk);
					nignore++;
				}
			} else if (!strcmp(key, "log")) {
				if (parse_bool(val, &use_log) == -1)
					warnx("%s:%d: log yes/no", path, lineno);
			} else if (!strcmp(key, "pushover_token"))
				snprintf(push_token, sizeof(push_token), "%s", val);
			else if (!strcmp(key, "pushover_user"))
				snprintf(push_user, sizeof(push_user), "%s", val);
			else if (!strcmp(key, "pushover")) {
				if (!strcmp(val, "off"))
					push_mode = 0;
				else if (!strcmp(val, "away"))
					push_mode = 1;
				else if (!strcmp(val, "always"))
					push_mode = 2;
				else
					warnx("%s:%d: pushover away|always|off",
					    path, lineno);
			} else if (!strcmp(key, "backlog")) {
				int v = parse_int(val, 0, BACKLOGMAX);

				if (v == -1)
					warnx("%s:%d: backlog 0-%d", path, lineno,
					    BACKLOGMAX);
				else
					backlog = v;
			} else if (!strcmp(key, "theme")) {
				int t = theme_find(val);

				if (t == -1)
					warnx("%s:%d: unknown theme %s", path,
					    lineno, val);
				else
					theme = t;
			} else if (!strcmp(key, "colors")) {
				if (parse_bool(val, &use_colors) == -1)
					warnx("%s:%d: colors yes/no", path, lineno);
			} else
				warnx("%s:%d: unknown global setting %s",
				    path, lineno, key);
		} else if (net_set(n, key, val) == -1)
			warnx("%s:%d: bad setting: %s %s", path, lineno, key, val);
	}
	if (ferror(f))
		err(1, "%s", path);

	for (i = 0; i < nnets; i++) {
		/* no "tls" line: TLS only on port 6697 */
		if (nets[i]->tls == -1)
			nets[i]->tls = strcmp(nets[i]->port, "6697") == 0;
		if (nets[i]->pass[0])
			haspass = 1;
		auto_windows(nets[i]);
	}
	if (push_token[0])
		haspass = 1;
	if (haspass && fstat(fileno(f), &st) == 0 && (st.st_mode & 077))
		bprint(sysbuf(), LVL_HILITE, "-!- WARNING: %s contains passwords "
		    "but is readable by others, run: chmod 600 %s", path, path);
	fclose(f);
}

static int
save_config(void)
{
	char		 tmp[1100];
	FILE		*f;
	int		 fd, i, j;
	struct network	*n;

	snprintf(tmp, sizeof(tmp), "%s.tmp", confpath);
	if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600)) == -1 ||
	    (f = fdopen(fd, "w")) == NULL) {
		uprint("-!- Could not save %s: %s", tmp, strerror(errno));
		if (fd != -1)
			close(fd);
		return -1;
	}
	fprintf(f, "# bsdirc configuration, rewritten by bsdirc " VERSION "\n");
	if (g_nick[0])
		fprintf(f, "nick %s\n", g_nick);
	if (g_user[0])
		fprintf(f, "user %s\n", g_user);
	if (g_realname[0])
		fprintf(f, "realname %s\n", g_realname);
	if (g_bind[0])
		fprintf(f, "bind %s\n", g_bind);
	if (!use_colors)
		fprintf(f, "colors no\n");
	if (theme != 0)
		fprintf(f, "theme %s\n", themes[theme].name);
	if (backlog != 50)
		fprintf(f, "backlog %d\n", backlog);
	if (!use_log)
		fprintf(f, "log no\n");
	for (i = 0; i < nignore; i++)
		if (ignores[i].hide)
			fprintf(f, "ignore %s %s\n", ignores[i].mask,
			    hide_words(ignores[i].hide));
		else
			fprintf(f, "ignore %s%s%s\n", ignores[i].mask,
			    ignores[i].host[0] ? " " : "", ignores[i].host);
	fprintf(f, "autoaway %g\n", autoaway);
	fprintf(f, "awaymsg %s\n", awaymsg);
	if (push_token[0])
		fprintf(f, "pushover_token %s\n", push_token);
	if (push_user[0])
		fprintf(f, "pushover_user %s\n", push_user);
	if (push_mode != -1)
		fprintf(f, "pushover %s\n", push_mode == 0 ? "off" :
		    push_mode == 1 ? "away" : "always");

	for (i = 0; i < nnets; i++) {
		n = nets[i];
		fprintf(f, "\n[%s]\n", n->name);
		fprintf(f, "host %s\n", n->host);
		fprintf(f, "port %s\n", n->port);
		fprintf(f, "tls %s\n", n->tls ? "yes" : "no");
		if (n->insecure)
			fprintf(f, "insecure yes\n");
		if (n->nick[0])
			fprintf(f, "nick %s\n", n->nick);
		if (n->user[0])
			fprintf(f, "user %s\n", n->user);
		if (n->realname[0])
			fprintf(f, "realname %s\n", n->realname);
		fprintf(f, "sasl %s\n", n->sasl ? "yes" : "no");
		if (n->qauth)
			fprintf(f, "qauth yes\n");
		if (!n->qhide)
			fprintf(f, "qhide no\n");
		if (n->account[0])
			fprintf(f, "account %s\n", n->account);
		if (n->bind[0])
			fprintf(f, "bind %s\n", n->bind);
		if (n->pass[0] && !n->pass_prompted)
			fprintf(f, "password %s\n", n->pass);
		fprintf(f, "autoconnect %s\n", n->autoconnect ? "yes" : "no");
		for (j = 0; j < n->nauto; j++)
			fprintf(f, "join %s\n", n->autojoin[j]);
		for (j = 0; j < nbufs; j++)	/* open queries come back at start */
			if (bufs[j]->net == n && bufs[j]->kind == B_QUERY)
				fprintf(f, "query %s\n", bufs[j]->name);
	}
	if (fflush(f) == EOF || fsync(fd) == -1) {
		uprint("-!- Could not save %s: %s", tmp, strerror(errno));
		fclose(f);
		unlink(tmp);
		return -1;
	}
	fclose(f);
	if (rename(tmp, confpath) == -1) {
		uprint("-!- Could not save %s: %s", confpath, strerror(errno));
		unlink(tmp);
		return -1;
	}
	uprint("-!- Saved %s", confpath);
	return 0;
}

/* ---------- networks: connecting ---------- */

/*
 * Write as much of the output buffer as the server takes now, without
 * waiting. The rest goes out from the poll loop when the socket is
 * writable again, so a slow or stalled server never freezes the client.
 */
static void
conn_flush(struct network *n)
{
	ssize_t	r;

	while (n->olen > 0 && n->sock != -1) {
		if (n->ctx != NULL)
			r = tls_write(n->ctx, n->obuf, n->olen);
		else
			r = write(n->sock, n->obuf, n->olen);
		if (n->ctx != NULL &&
		    (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT)) {
			n->owant = r == TLS_WANT_POLLIN ? POLLIN : POLLOUT;
			return;
		}
		if (n->ctx == NULL && r == -1 &&
		    (errno == EAGAIN || errno == EINTR)) {
			n->owant = POLLOUT;
			return;
		}
		if (r <= 0) {
			net_fail(n, n->ctx ? tls_error(n->ctx) : strerror(errno));
			return;
		}
		/* the buffer may hold passwords: wipe what has been sent */
		memmove(n->obuf, n->obuf + r, n->olen - r);
		n->olen -= r;
		explicit_bzero(n->obuf + n->olen, r);
		n->oprogress = time(NULL);
	}
	n->owant = 0;
}

/* Queue data for the server and send what can be sent right away. */
static void
conn_write(struct network *n, const char *p, size_t len)
{
	char	*nb;
	size_t	 cap;

	if (n->sock == -1)
		return;
	if (n->olen + len > OBUFMAX) {
		net_fail(n, "the server is not taking our data");
		return;
	}
	if (n->olen + len > n->ocap) {
		cap = n->ocap ? n->ocap : 1024;
		while (cap < n->olen + len)
			cap *= 2;
		/* not realloc: the old buffer must be wiped, not just freed */
		if ((nb = malloc(cap)) == NULL)
			fatal("malloc");
		if (n->olen)
			memcpy(nb, n->obuf, n->olen);
		if (n->obuf != NULL) {
			explicit_bzero(n->obuf, n->ocap);
			free(n->obuf);
		}
		n->obuf = nb;
		n->ocap = cap;
	}
	if (n->olen == 0)
		n->oprogress = time(NULL);
	memcpy(n->obuf + n->olen, p, len);
	n->olen += len;
	conn_flush(n);
}

/*
 * Before closing on purpose (/quit, /disconnect): give what is left,
 * such as QUIT, up to ms milliseconds to go out.
 */
static void
conn_drain(struct network *n, int ms)
{
	struct pollfd	pfd;
	long long	end = now_ms() + ms;
	int		w;

	while (n->olen > 0 && n->sock != -1 && (w = end - now_ms()) > 0) {
		pfd.fd = n->sock;
		pfd.events = n->owant ? n->owant : POLLOUT;
		if (poll(&pfd, 1, w) <= 0)
			break;
		conn_flush(n);
	}
}

static long long
now_ms(void)
{
	struct timespec	ts;

	clock_gettime(CLOCK_MONOTONIC, &ts);
	return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

static void
sq_refill(struct network *n)
{
	long long	t = now_ms();

	n->tokens += (double)(t - n->tok_ms) / SQRATE;
	if (n->tokens > SQBURST)
		n->tokens = SQBURST;
	n->tok_ms = t;
}

/* Send as much of the queue as the bucket allows. */
static void
sq_flush(struct network *n)
{
	char	*l;

	sq_refill(n);
	/* nothing new while the server is behind: lines wait in the queue */
	while (n->sqlen > 0 && n->tokens >= 1 && n->sock != -1 &&
	    n->olen == 0) {
		l = n->sq[n->sqhead];
		n->sq[n->sqhead] = NULL;
		n->sqhead = (n->sqhead + 1) % SQMAX;
		n->sqlen--;
		n->tokens -= 1;
		conn_write(n, l, strlen(l));
		zfree(l);
		dirty = 1;
	}
}

static void
sq_clear(struct network *n)
{
	char	*l;

	while (n->sqlen > 0) {
		l = n->sq[n->sqhead];
		n->sqhead = (n->sqhead + 1) % SQMAX;
		n->sqlen--;
		zfree(l);
	}
	n->sqhead = 0;
}

/* ms until the next queued line may be sent, or -1 if the queue is empty */
static int
sq_wait(struct network *n)
{
	if (n->sqlen == 0 || n->sock == -1)
		return -1;
	sq_refill(n);
	if (n->tokens >= 1)
		return 0;
	return (int)((1 - n->tokens) * SQRATE) + 1;
}

/*
 * Send a line to the server. During registration, and for PING, PONG
 * and QUIT, the line goes out directly; everything else goes through the
 * send queue so that a long paste does not cause "Excess Flood".
 */
static void
irc_send(struct network *n, const char *fmt, ...)
{
	char	buf[514], *l;
	va_list	ap;
	int	len, urgent, i;

	if (n == NULL || n->state < ST_REGISTER) {
		uprint("-!- %s: not connected", n ? n->name : "bsdirc");
		return;
	}
	va_start(ap, fmt);
	len = vsnprintf(buf, 511, fmt, ap);
	va_end(ap);
	if (len < 0)
		return;
	if (len > 510)
		len = 510;
	for (i = 0; i < len; i++)	/* one line, whatever the arguments hold */
		if (buf[i] == '\r' || buf[i] == '\n')
			buf[i] = ' ';
	buf[len++] = '\r';
	buf[len++] = '\n';
	buf[len] = 0;

	urgent = n->state != ST_ONLINE || strncmp(buf, "PONG ", 5) == 0 ||
	    strncmp(buf, "PING ", 5) == 0 || strncmp(buf, "QUIT", 4) == 0;
	if (urgent && n->sqlen == 0) {
		sq_refill(n);
		if (n->tokens >= 1)
			n->tokens -= 1;
		conn_write(n, buf, len);
		return;
	}
	if (urgent) {			/* skip the queue */
		conn_write(n, buf, len);
		return;
	}
	if (n->sqlen == SQMAX) {
		uprint("-!- %s: send queue full, line dropped", n->name);
		return;
	}
	if ((l = strdup(buf)) == NULL)
		fatal("strdup");
	n->sq[(n->sqhead + n->sqlen) % SQMAX] = l;
	n->sqlen++;
	sq_flush(n);
	explicit_bzero(buf, sizeof(buf));
}

static void
net_cleanup(struct network *n)
{
	int	i;

	sq_clear(n);
	n->away = 0;
	n->has_monitor = n->monitoring = 0;
	n->keepnick_off = 0;
	n->nick_try_at = n->ison_at = 0;
#ifdef HAVE_ASR
	if (n->aq != NULL) {
		asr_abort(n->aq);
		n->aq = NULL;
	}
#endif

	if (n->ctx != NULL) {
		tls_close(n->ctx);
		tls_free(n->ctx);
		n->ctx = NULL;
	}
	if (n->sock != -1) {
		close(n->sock);
		n->sock = -1;
	}
	if (n->obuf != NULL) {
		explicit_bzero(n->obuf, n->ocap);
		free(n->obuf);
		n->obuf = NULL;
	}
	n->olen = n->ocap = 0;
	n->owant = 0;
	n->qstate = Q_NONE;
	if (n->ai0 != NULL) {
		freeaddrinfo(n->ai0);
		n->ai0 = n->ai = NULL;
	}
	for (i = 0; i < nbufs; i++)
		if (bufs[i]->net == n) {
			if (bufs[i]->joined)
				bufs[i]->rejoin = 1;
			bufs[i]->joined = 0;
			mem_clear(bufs[i]);
		}
	n->state = ST_OFFLINE;
	n->rlen = 0;
	n->want = POLLIN;
	n->ping_sent = 0;
	dirty = 1;
}

/* The connection died or could not be established. */
static void
net_fail(struct network *n, const char *why)
{
	if (n->state == ST_OFFLINE && n->sock == -1)
		return;
	nprint(n, LVL_HILITE, "-!- Disconnected from %s: %s",
	    n->name, why ? why : "unknown error");
	net_cleanup(n);
	if (n->wantconn) {
		n->retry_at = time(NULL) + n->backoff;
		nprint(n, LVL_NONE, "-!- Reconnecting in %d seconds "
		    "(/disconnect %s to stop)", n->backoff, n->name);
		n->backoff *= 2;
		if (n->backoff > BACKOFFMAX)
			n->backoff = BACKOFFMAX;
	}
}

static void
net_register(struct network *n)
{
	n->state = ST_REGISTER;
	n->want = POLLIN;
	n->tokens = SQBURST;
	n->tok_ms = now_ms();
	n->last_rx = time(NULL);
	snprintf(n->mynick, sizeof(n->mynick), "%s", net_nick(n));
	nprint(n, LVL_EVENT, "-!- Logging in to %s as %s",
	    n->host, n->mynick);
	n->capreq[0] = 0;
	irc_send(n, "CAP LS 302");
	if (n->state >= ST_REGISTER)
		irc_send(n, "NICK %s", n->mynick);
	if (n->state >= ST_REGISTER)
		irc_send(n, "USER %s 0 * :%s", net_user(n), net_realname(n));
}

static void
net_handshake(struct network *n)
{
	int	r = tls_handshake(n->ctx);

	if (r == TLS_WANT_POLLIN)
		n->want = POLLIN;
	else if (r == TLS_WANT_POLLOUT)
		n->want = POLLOUT;
	else if (r == -1)
		net_fail(n, tls_error(n->ctx));
	else
		net_register(n);
}

static void
net_tcp_ok(struct network *n)
{
	struct sockaddr_storage	ss;
	socklen_t		len;

	/* which address did we connect from and to? (numeric, no DNS) */
	n->laddr[0] = n->raddr[0] = 0;
	len = sizeof(ss);
	if (getsockname(n->sock, (struct sockaddr *)&ss, &len) == 0)
		getnameinfo((struct sockaddr *)&ss, len, n->laddr,
		    sizeof(n->laddr), NULL, 0, NI_NUMERICHOST);
	len = sizeof(ss);
	if (getpeername(n->sock, (struct sockaddr *)&ss, &len) == 0)
		getnameinfo((struct sockaddr *)&ss, len, n->raddr,
		    sizeof(n->raddr), NULL, 0, NI_NUMERICHOST);
	nprint(n, LVL_EVENT, "-!- Connected to %s [%s] from %s", n->host,
	    n->raddr, n->laddr[0] ? n->laddr : "?");

	freeaddrinfo(n->ai0);
	n->ai0 = n->ai = NULL;
	if (!n->tls) {
		net_register(n);
		return;
	}
	if ((n->ctx = tls_client()) == NULL) {
		net_fail(n, "tls_client failed");
		return;
	}
	if (tls_configure(n->ctx, n->insecure ? cfg_insecure : cfg_verify) == -1 ||
	    tls_connect_socket(n->ctx, n->sock, n->host) == -1) {
		net_fail(n, tls_error(n->ctx));
		return;
	}
	n->state = ST_HANDSHAKE;
	net_handshake(n);
}

/* Try the addresses in order with non-blocking connect. */
static void
net_try_addr(struct network *n)
{
	struct addrinfo	*ai, hints, *la;
	const char	*bindip = n->bind[0] ? n->bind : g_bind;
	int		 s, skipped = 0;

	for (; (ai = n->ai) != NULL; n->ai = ai->ai_next) {
		la = NULL;
		if (*bindip) {
			/* only bind to addresses of the same family (v4/v6) */
			memset(&hints, 0, sizeof(hints));
			hints.ai_family = ai->ai_family;
			hints.ai_socktype = SOCK_STREAM;
			hints.ai_flags = AI_NUMERICHOST;
			if (getaddrinfo(bindip, NULL, &hints, &la) != 0) {
				skipped = 1;
				continue;
			}
		}
		if ((s = socket(ai->ai_family, ai->ai_socktype,
		    ai->ai_protocol)) == -1) {
			n->lasterr = errno;
			if (la)
				freeaddrinfo(la);
			continue;
		}
		if (la != NULL) {
			if (bind(s, la->ai_addr, la->ai_addrlen) == -1) {
				n->lasterr = errno;
				freeaddrinfo(la);
				close(s);
				continue;
			}
			freeaddrinfo(la);
		}
		if (fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK) == -1) {
			n->lasterr = errno;
			close(s);
			continue;
		}
		n->sock = s;
		if (connect(s, ai->ai_addr, ai->ai_addrlen) == 0) {
			net_tcp_ok(n);
			return;
		}
		if (errno == EINPROGRESS) {
			n->state = ST_CONNECTING;
			n->want = POLLOUT;
			return;
		}
		n->lasterr = errno;
		close(s);
		n->sock = -1;
	}
	n->state = ST_CONNECTING;	/* so that net_fail does not ignore us */
	if (skipped && n->lasterr == 0) {
		nprint(n, LVL_HILITE, "-!- %s has no address of the same type "
		    "(IPv4/IPv6) as bind %s", n->host, bindip);
		net_fail(n, "no usable address for bind");
		return;
	}
	net_fail(n, strerror(n->lasterr ? n->lasterr : ECONNREFUSED));
}

#ifdef HAVE_ASR
/*
 * Drive the DNS lookup forward. asr_run() returns 0 when it needs to
 * wait on ar_fd (or ar_timeout ms), and 1 when the answer is ready.
 */
static void
dns_step(struct network *n)
{
	struct asr_result	ar;

	if (n->aq == NULL)
		return;
	if (asr_run(n->aq, &ar) == 0) {
		n->dns_fd = ar.ar_fd;
		n->dns_events = ar.ar_cond == ASR_WANT_READ ? POLLIN : POLLOUT;
		n->dns_deadline = now_ms() + ar.ar_timeout;
		return;
	}
	n->aq = NULL;			/* asr has freed the query */
	if (ar.ar_gai_errno != 0) {
		net_fail(n, gai_strerror(ar.ar_gai_errno));
		return;
	}
	n->ai0 = n->ai = ar.ar_addrinfo;
	net_try_addr(n);
}
#endif

static void
net_connect(struct network *n)
{
	struct addrinfo	hints;
#ifndef HAVE_ASR
	int		e;
#endif

	if (n->state != ST_OFFLINE)
		return;
	n->retry_at = 0;
	if (n->host[0] == 0) {
		bprint(n->status, LVL_HILITE, "-!- %s has no host, "
		    "use /server set %s host ...", n->name, n->name);
		n->wantconn = 0;
		return;
	}
	if (n->sasl && n->pass[0] == 0)
		nprint(n, LVL_HILITE, "-!- %s: sasl is on but no password is "
		    "set, connecting without SASL", n->name);
	else if (n->sasl && !n->tls)
		nprint(n, LVL_HILITE, "-!- %s: SASL without TLS, the password "
		    "is sent unencrypted (/server set %s tls yes)", n->name,
		    n->name);
	nprint(n, LVL_EVENT, "-!- Connecting to %s port %s%s...",
	    n->host, n->port, n->tls ? " (TLS)" : "");

	n->started = time(NULL);
	n->lasterr = 0;
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_NUMERICSERV;
#ifdef HAVE_ASR
	/* asynchronous: the answer arrives via the poll loop, the UI stays alive */
	if ((n->aq = getaddrinfo_async(n->host, n->port, &hints, NULL)) == NULL) {
		n->state = ST_RESOLVING;
		net_fail(n, strerror(errno));
		return;
	}
	n->state = ST_RESOLVING;
	dns_step(n);
#else
	n->state = ST_RESOLVING;
	if ((e = getaddrinfo(n->host, n->port, &hints, &n->ai0)) != 0) {
		net_fail(n, gai_strerror(e));
		return;
	}
	n->ai = n->ai0;
	net_try_addr(n);
#endif
}

/* The socket became writable during ST_CONNECTING. */
static void
net_connect_done(struct network *n)
{
	int		e = 0;
	socklen_t	len = sizeof(e);

	if (getsockopt(n->sock, SOL_SOCKET, SO_ERROR, &e, &len) == -1)
		e = errno;
	if (e == 0) {
		net_tcp_ok(n);
		return;
	}
	n->lasterr = e;
	close(n->sock);
	n->sock = -1;
	n->ai = n->ai->ai_next;
	net_try_addr(n);
}

static void
net_disconnect(struct network *n, const char *msg)
{
	n->wantconn = 0;
	n->retry_at = 0;
	if (n->state >= ST_REGISTER && msg && *msg)
		irc_send(n, "QUIT :%s", msg);
	else if (n->state >= ST_REGISTER)
		irc_send(n, "QUIT");
	conn_drain(n, 2000);
	if (n->state != ST_OFFLINE || n->sock != -1)
		net_fail(n, "disconnected");
	n->backoff = BACKOFFMIN;
}

/* ---------- the IRC protocol ---------- */

static void
join_params(struct msg *m, int from, char *out, size_t sz)
{
	size_t	n = 0;
	int	i;

	out[0] = 0;
	for (i = from; i < m->n && n < sz; i++)
		n += snprintf(out + n, sz - n, "%s%s",
		    i > from ? " " : "", m->params[i]);
}

static int
parse(char *s, struct msg *m)
{
	char	*p;

	memset(m, 0, sizeof(*m));
	m->nick = "";
	m->uhost = "";
	if (*s == '@') {			/* IRCv3 tags: skip */
		if ((p = strchr(s, ' ')) == NULL)
			return -1;
		s = p + 1;
	}
	while (*s == ' ')
		s++;
	if (*s == ':') {
		m->nick = ++s;
		if ((p = strchr(s, ' ')) == NULL)
			return -1;
		*p = 0;
		s = p + 1;
		if ((p = strchr(m->nick, '!')) != NULL) {
			*p = 0;
			m->uhost = p + 1;
		}
		while (*s == ' ')
			s++;
	}
	m->cmd = s;
	if ((p = strchr(s, ' ')) == NULL)
		return *s ? 0 : -1;
	*p = 0;
	s = p + 1;
	while (*s && m->n < MAXPARAMS) {
		while (*s == ' ')
			s++;
		if (*s == 0)
			break;
		if (*s == ':') {
			m->params[m->n++] = s + 1;
			break;
		}
		m->params[m->n++] = s;
		if ((p = strchr(s, ' ')) == NULL)
			break;
		*p = 0;
		s = p + 1;
	}
	return 0;
}

static void
b64(const unsigned char *src, size_t len, char *o)
{
	static const char t[] =
	    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	size_t	i;

	for (i = 0; i + 2 < len; i += 3) {
		*o++ = t[src[i] >> 2];
		*o++ = t[((src[i] & 3) << 4) | (src[i + 1] >> 4)];
		*o++ = t[((src[i + 1] & 15) << 2) | (src[i + 2] >> 6)];
		*o++ = t[src[i + 2] & 63];
	}
	if (i < len) {
		*o++ = t[src[i] >> 2];
		if (i + 1 < len) {
			*o++ = t[((src[i] & 3) << 4) | (src[i + 1] >> 4)];
			*o++ = t[(src[i + 1] & 15) << 2];
		} else {
			*o++ = t[(src[i] & 3) << 4];
			*o++ = '=';
		}
		*o++ = '=';
	}
	*o = 0;
}

/* SASL PLAIN: base64(account \0 account \0 password) */
static void
sasl_send(struct network *n)
{
	unsigned char	 raw[297];
	char		 enc[400];
	const char	*acct = net_account(n);
	size_t		 al = strlen(acct), pl = strlen(n->pass), len = 0;

	if (2 * al + pl + 2 > sizeof(raw)) {
		nprint(n, LVL_HILITE, "-!- SASL credentials too long");
		irc_send(n, "AUTHENTICATE *");
		return;
	}
	memcpy(raw, acct, al);
	len = al;
	raw[len++] = 0;
	memcpy(raw + len, acct, al);
	len += al;
	raw[len++] = 0;
	memcpy(raw + len, n->pass, pl);
	len += pl;
	b64(raw, len, enc);
	irc_send(n, "AUTHENTICATE %s", enc);
	explicit_bzero(raw, sizeof(raw));
	explicit_bzero(enc, sizeof(enc));
}

/* Collect channels without a key into "JOIN #a,#b,#c" (at most ~400 bytes). */
static void
join_add(struct network *n, char *list, size_t sz, const char *chan)
{
	size_t	l = strlen(list);

	if (l > 0 && l + strlen(chan) + 2 > 400) {
		irc_send(n, "JOIN %s", list);
		list[0] = 0;
		l = 0;
	}
	snprintf(list + l, sz - l, "%s%s", l ? "," : "", chan);
}

static void
do_autojoin(struct network *n)
{
	char	list[512];
	int	i;

	list[0] = 0;
	for (i = 0; i < n->nauto && n->state == ST_ONLINE; i++) {
		if (strchr(n->autojoin[i], ' ') != NULL)	/* with a key */
			irc_send(n, "JOIN %s", n->autojoin[i]);
		else
			join_add(n, list, sizeof(list), n->autojoin[i]);
	}
	/* channels we were in when the connection dropped */
	for (i = 0; i < nbufs && n->state == ST_ONLINE; i++)
		if (bufs[i]->net == n && bufs[i]->rejoin) {
			bufs[i]->rejoin = 0;
			if (auto_find(n, bufs[i]->name) == -1)
				join_add(n, list, sizeof(list), bufs[i]->name);
		}
	if (list[0] && n->state == ST_ONLINE)
		irc_send(n, "JOIN %s", list);
}

/* Is the word tok in the space-separated list? ("sasl=PLAIN" counts) */
static int
has_token(const char *list, const char *tok)
{
	size_t	l = strlen(tok);

	while (*list) {
		while (*list == ' ')
			list++;
		if (strncasecmp(list, tok, l) == 0 &&
		    (list[l] == 0 || list[l] == ' ' || list[l] == '='))
			return 1;
		while (*list && *list != ' ')
			list++;
	}
	return 0;
}

static void
handle_cap(struct network *n, struct msg *m)
{
	const char	*sub = P(1), *caps;
	int		 more;
	size_t		 l;

	if (strcmp(sub, "LS") == 0) {
		more = strcmp(P(2), "*") == 0;
		caps = more ? P(3) : P(2);
		l = strlen(n->capreq);
		if (has_token(caps, "multi-prefix") &&
		    !has_token(n->capreq, "multi-prefix"))
			l += snprintf(n->capreq + l, sizeof(n->capreq) - l,
			    "%smulti-prefix", l ? " " : "");
		if (has_token(caps, "userhost-in-names") &&
		    !has_token(n->capreq, "userhost-in-names") &&
		    l < sizeof(n->capreq))
			l += snprintf(n->capreq + l, sizeof(n->capreq) - l,
			    "%suserhost-in-names", l ? " " : "");
		if (n->sasl && n->pass[0] && has_token(caps, "sasl") &&
		    !has_token(n->capreq, "sasl") && l < sizeof(n->capreq))
			snprintf(n->capreq + l, sizeof(n->capreq) - l,
			    "%ssasl", l ? " " : "");
		if (more)
			return;
		if (n->sasl && n->pass[0] && !has_token(n->capreq, "sasl"))
			bprint(n->status, LVL_HILITE,
			    "-!- Server does not support SASL");
		if (n->capreq[0])
			irc_send(n, "CAP REQ :%s", n->capreq);
		else
			irc_send(n, "CAP END");
	} else if (strcmp(sub, "ACK") == 0) {
		if (has_token(P(2), "sasl"))
			irc_send(n, "AUTHENTICATE PLAIN");
		else
			irc_send(n, "CAP END");
	} else if (strcmp(sub, "NAK") == 0) {
		bprint(n->status, LVL_HILITE, "-!- Server refused caps: %s", P(2));
		irc_send(n, "CAP END");
	}
}

/*
 * keepnick: if the server gave us a fallback nick (jt_ instead of jt),
 * win the configured nick back as soon as it is free. Nothing happens
 * when you already have it, or after you chose another nick with /nick.
 */
static int
chasing(struct network *n)
{
	return n->state == ST_ONLINE && !n->keepnick_off &&
	    strcasecmp(n->mynick, net_nick(n)) != 0;
}

static void
keepnick_try(struct network *n)
{
	time_t	now = time(NULL);

	if (!chasing(n) || now - n->nick_try_at < KEEPNICK_RETRY)
		return;
	n->nick_try_at = now;
	irc_send(n, "NICK %s", net_nick(n));
}

/* Stop watching the nick, e.g. when we got it or gave up the chase. */
static void
keepnick_stop(struct network *n)
{
	if (n->monitoring && n->state == ST_ONLINE)
		irc_send(n, "MONITOR - %s", net_nick(n));
	n->monitoring = 0;
}

/* Once a second: start MONITOR, or poll with ISON on older servers. */
static void
keepnick_tick(struct network *n, time_t now)
{
	if (!chasing(n)) {
		if (n->monitoring)
			keepnick_stop(n);
		return;
	}
	if (n->has_monitor) {
		if (!n->monitoring) {	/* the server tells us when it is free */
			n->monitoring = 1;
			irc_send(n, "MONITOR + %s", net_nick(n));
		}
	} else if (now - n->ison_at >= KEEPNICK_ISON) {
		n->ison_at = now;
		irc_send(n, "ISON %s", net_nick(n));
	}
}

/* ISUPPORT (005): PREFIX and CHANMODES */
static void
handle_isupport(struct network *n, struct msg *m)
{
	const char	*s, *e;
	int		 i, k;
	size_t		 l;

	for (i = 1; i < m->n - 1; i++) {
		s = m->params[i];
		if (strcmp(s, "MONITOR") == 0 || strncmp(s, "MONITOR=", 8) == 0)
			n->has_monitor = 1;
		if (strncmp(s, "MODES=", 6) == 0 && atoi(s + 6) > 0)
			n->maxmodes = atoi(s + 6);
		if (strncmp(s, "PREFIX=(", 8) == 0 &&
		    (e = strchr(s + 8, ')')) != NULL) {
			l = e - (s + 8);
			if (l >= sizeof(n->prefix_modes) || strlen(e + 1) != l)
				continue;
			memcpy(n->prefix_modes, s + 8, l);
			n->prefix_modes[l] = 0;
			snprintf(n->prefix_chars, sizeof(n->prefix_chars), "%s", e + 1);
		} else if (strncmp(s, "CHANMODES=", 10) == 0) {
			s += 10;
			for (k = 0; k < 4; k++) {
				e = strchr(s, ',');
				l = e ? (size_t)(e - s) : strlen(s);
				if (l >= sizeof(n->chanmodes[k]))
					l = sizeof(n->chanmodes[k]) - 1;
				memcpy(n->chanmodes[k], s, l);
				n->chanmodes[k][l] = 0;
				if (e == NULL)
					break;
				s = e + 1;
			}
		}
	}
}

/* Add or remove a channel mode letter (lists like +b are not kept). */
static void
cmode_set(struct buffer *b, char c, int add, const char *param)
{
	char	*p = strchr(b->cmodes, c);
	size_t	 l = strlen(b->cmodes);

	if (add && p == NULL && l + 1 < sizeof(b->cmodes)) {
		b->cmodes[l] = c;
		b->cmodes[l + 1] = 0;
	} else if (!add && p != NULL)
		memmove(p, p + 1, strlen(p));
	if (c == 'l')
		snprintf(b->climit, sizeof(b->climit), "%s", add ? param : "");
	dirty = 1;
}

/*
 * Apply a mode string to channel b: prefix modes (+o, +v ...) update the
 * nick list, other modes the channel's mode letters. CHANMODES from 005
 * tells which modes take a parameter: A lists (+b), B always (+k),
 * C only when set (+l), D never (+n).
 */
static void
modes_apply(struct network *n, struct buffer *b, const char *s,
    char **params, int nparams)
{
	struct member	*mb;
	const char	*p, *arg;
	int		 add = 1, ai = 0;

#define NEXTARG()	(ai < nparams ? params[ai++] : "")
	for (; *s; s++) {
		if (*s == '+') {
			add = 1;
		} else if (*s == '-') {
			add = 0;
		} else if ((p = strchr(n->prefix_modes, *s)) != NULL) {
			if ((mb = mem_find(b, NEXTARG())) != NULL) {
				if (add)
					mb->modes |= 1 << (p - n->prefix_modes);
				else
					mb->modes &= ~(1 << (p - n->prefix_modes));
				b->sorted = 0;
				dirty = 1;
			}
		} else if (strchr(n->chanmodes[0], *s)) {
			(void)NEXTARG();		/* list mode: not kept */
		} else if (strchr(n->chanmodes[1], *s)) {
			arg = NEXTARG();
			cmode_set(b, *s, add, arg);
		} else if (strchr(n->chanmodes[2], *s)) {
			arg = add ? NEXTARG() : "";
			cmode_set(b, *s, add, arg);
		} else
			cmode_set(b, *s, add, "");
	}
#undef NEXTARG
}

/* Update the nick list and modes after MODE #channel +o-v nick nick */
static void
apply_modes(struct network *n, struct buffer *b, struct msg *m)
{
	modes_apply(n, b, P(1), m->params + 2, m->n > 2 ? m->n - 2 : 0);
}

static void
names_summary(struct buffer *b)
{
	int	i, ops = 0, half = 0, voice = 0, normal = 0;
	char	c;

	for (i = 0; i < b->nmem; i++) {
		c = prefix_char(b->net, b->mem[i].modes);
		if (c == '~' || c == '&' || c == '@')
			ops++;
		else if (c == '%')
			half++;
		else if (c == '+')
			voice++;
		else
			normal++;
	}
	bprint(b, LVL_EVENT, "-!- %s: Total of %d nicks [%d ops, %d halfops, "
	    "%d voices, %d normal]", b->name, b->nmem, ops, half, voice, normal);
}

/* Is nick mentioned as a whole word in text? ("johan" in "johansson" is not) */
static int
mentions(const char *text, const char *nick)
{
	int	save = lb_n, found = 0, i;

	/* borrow the line builder: decode the text and search with the same rule as lb_mark */
	lb_clear();
	lb_text(S_DEFAULT, text, 0);
	lb_mark(0, nick, F_REV);
	for (i = 0; i < lb_n; i++)
		if (lb_a[i] == F_REV)
			found = 1;
	lb_n = save;
	return found;
}

/*
 * CTCP. Requests to you (VERSION, PING, TIME) are answered and shown in
 * the network's status window. Replies to your /ctcp are shown where you
 * are, PING with the round-trip time.
 */
static void
handle_ctcp(struct network *n, const char *nick, char *text, int reply)
{
	char		*arg, tb[64];
	struct buffer	*b = reply_buf(n);
	long long	 sent;
	time_t		 t;

	if ((arg = strchr(text, ' ')) != NULL)
		*arg++ = 0;
	else
		arg = "";

	if (reply) {
		if (strcasecmp(text, "PING") == 0 &&
		    (sent = strtoll(arg, NULL, 10)) > 0) {
			snprintf(tb, sizeof(tb), "%.3f s",
			    (now_ms() - sent) / 1000.0);
			arg = tb;
		}
		eprint(b, LVL_NONE, S_EVPFX, "-!- ", S_EVENT, "CTCP ",
		    S_CHAN, text, S_EVENT, " reply from ", S_JOIN, nick,
		    S_EVENT, *arg ? ": " : "", S_EVENT, arg, -1);
		return;
	}

	/*
	 * At most CTCPMAX replies per CTCPWIN seconds, and none while the
	 * send queue is backed up, so a CTCP flood cannot fill the queue
	 * and delay what you write.
	 */
	t = time(NULL);
	if (t - n->ctcp_win >= CTCPWIN) {
		n->ctcp_win = t;
		n->ctcp_n = 0;
	}
	if (n->ctcp_n >= CTCPMAX || n->sqlen > 2) {
		if (n->ctcp_n++ == CTCPMAX)
			bprint(n->status, LVL_EVENT, "-!- CTCP flood, not "
			    "answering for a while");
		return;
	}
	n->ctcp_n++;
	eprint(n->status, LVL_EVENT, S_EVPFX, "-!- ", S_JOIN, nick,
	    S_EVENT, " requested CTCP ", S_CHAN, text, S_EVENT,
	    *arg ? " " : "", S_EVENT, arg, -1);
	if (strcasecmp(text, "VERSION") == 0)
		irc_send(n, "NOTICE %s :\001VERSION bsdirc " VERSION
		    " - running on %s\001", nick, sysname);
	else if (strcasecmp(text, "PING") == 0 && strlen(arg) < 64)
		irc_send(n, "NOTICE %s :\001PING %s\001", nick, arg);
	else if (strcasecmp(text, "TIME") == 0) {
		t = time(NULL);
		fmt_time(tb, sizeof(tb), "%a %b %d %H:%M:%S %Y", t);
		irc_send(n, "NOTICE %s :\001TIME %s\001", nick, tb);
	}
}

/* ---------- Q on QuakeNet ---------- */

static void
hex_encode(const unsigned char *src, size_t len, char *out)
{
	static const char	hex[] = "0123456789abcdef";
	size_t			i;

	for (i = 0; i < len; i++) {
		out[2 * i] = hex[src[i] >> 4];
		out[2 * i + 1] = hex[src[i] & 15];
	}
	out[2 * len] = 0;
}

/*
 * CHALLENGEAUTH, from QuakeNet's documentation:
 *   key      = SHA256(lower(user) ":" SHA256(password[0..9]))   (hex)
 *   response = HMAC-SHA256(key, challenge)                       (hex)
 * The user name is lowercased the RFC 1459 way ([]\^ become {}|~), and Q
 * only uses the first 10 characters of the password. The password
 * itself never goes over the wire.
 */
static void
q_response(const char *user, const char *pass, const char *challenge,
    char *out)
{
	unsigned char	 h[SHA256_DIGEST_LENGTH];
	unsigned int	 hl = sizeof(h);
	char		 phex[2 * SHA256_DIGEST_LENGTH + 1];
	char		 khex[2 * SHA256_DIGEST_LENGTH + 1];
	char		 buf[256];
	size_t		 i, pl = strlen(pass);

	if (pl > 10)
		pl = 10;
	SHA256((const unsigned char *)pass, pl, h);
	hex_encode(h, sizeof(h), phex);

	for (i = 0; user[i] && i < 64; i++) {
		unsigned char c = user[i];

		buf[i] = c == '[' ? '{' : c == ']' ? '}' : c == '\\' ? '|' :
		    c == '^' ? '~' : tolower(c);
	}
	snprintf(buf + i, sizeof(buf) - i, ":%s", phex);
	SHA256((const unsigned char *)buf, strlen(buf), h);
	hex_encode(h, sizeof(h), khex);

	HMAC(EVP_sha256(), khex, strlen(khex),
	    (const unsigned char *)challenge, strlen(challenge), h, &hl);
	hex_encode(h, hl, out);

	explicit_bzero(h, sizeof(h));
	explicit_bzero(phex, sizeof(phex));
	explicit_bzero(khex, sizeof(khex));
	explicit_bzero(buf, sizeof(buf));
}

/* After 001: ask Q for a challenge. Autojoin waits for the answer. */
static void
q_start(struct network *n)
{
	n->qstate = Q_CHALLENGE;
	n->qstart = time(NULL);
	bprint(n->status, LVL_EVENT, "-!- Logging in to Q as %s...",
	    net_account(n));
	irc_send(n, "PRIVMSG " QBOT " :CHALLENGE");
}

/* Logged in (ok) or not: hide the host if we can, then join. */
static void
q_done(struct network *n, int ok)
{
	n->qstate = Q_DONE;
	if (ok && n->qhide) {
		bprint(n->status, LVL_EVENT, "-!- Logged in to Q as %s, "
		    "hiding your host (+x)", net_account(n));
		irc_send(n, "MODE %s +x", n->mynick);
	} else if (ok)
		bprint(n->status, LVL_EVENT, "-!- Logged in to Q as %s",
		    net_account(n));
	do_autojoin(n);
}

/* A notice from Q while we are logging in. */
static void
q_notice(struct network *n, const char *text)
{
	char	 chal[128], resp[2 * SHA256_DIGEST_LENGTH + 1];
	const char *e;
	size_t	 l;

	if (n->qstate == Q_CHALLENGE && strncmp(text, "CHALLENGE ", 10) == 0) {
		/* CHALLENGE 3afabede5c2859fd821e315f889d9a6c HMAC-MD5 ... */
		text += 10;
		l = strcspn(text, " ");
		e = text + l;
		if (l == 0 || l >= sizeof(chal) || strstr(e, "HMAC-SHA-256") ==
		    NULL) {
			bprint(n->status, LVL_HILITE, "-!- Q does not offer "
			    "HMAC-SHA-256, not logging in");
			q_done(n, 0);
			return;
		}
		memcpy(chal, text, l);
		chal[l] = 0;
		q_response(net_account(n), n->pass, chal, resp);
		irc_send(n, "PRIVMSG " QBOT " :CHALLENGEAUTH %s %s HMAC-SHA-256",
		    net_account(n), resp);
		explicit_bzero(resp, sizeof(resp));
		n->qstate = Q_RESULT;
		return;
	}
	if (n->qstate != Q_RESULT)
		return;
	if (strstr(text, "You are now logged in as") != NULL)
		q_done(n, 1);
	else if (strstr(text, "incorrect") != NULL ||
	    strstr(text, "Too many") != NULL ||
	    strstr(text, "suspended") != NULL) {
		bprint(n->status, LVL_HILITE, "-!- Q login failed: %s", text);
		q_done(n, 0);
	}
}

static void
handle_msg(struct network *n, struct msg *m)
{
	int		 notice = m->cmd[0] == 'N', action = 0, lvl, mention = 0;
	char		*target = P(0), *text = P(1);
	struct buffer	*b;
	size_t		 len;

	if (text[0] == '\001') {		/* CTCP */
		text++;
		len = strlen(text);
		if (len > 0 && text[len - 1] == '\001')
			text[--len] = 0;
		if (strncmp(text, "ACTION ", 7) == 0) {
			text += 7;
			action = 1;
		} else {
			handle_ctcp(n, m->nick, text, notice);
			return;
		}
	}

	if (notice && n->qstate != Q_NONE && n->qstate != Q_DONE &&
	    strcasecmp(m->nick, "Q") == 0 && strcasecmp(m->uhost,
	    "TheQBot@CServe.quakenet.org") == 0)
		q_notice(n, text);

	if (ischan(target)) {
		struct member *mb;

		b = buf_get(n, target);
		mention = mentions(text, n->mynick);
		lvl = mention ? LVL_HILITE : LVL_MSG;
		if ((mb = mem_find(b, m->nick)) != NULL)
			mb->spoke = time(NULL);
	} else if (n->state != ST_ONLINE || strchr(m->nick, '.') != NULL ||
	    !*m->nick) {
		b = n->status;			/* server notices */
		lvl = LVL_EVENT;
	} else if (notice) {			/* e.g. NickServ */
		b = bufs[cur]->net == n ? bufs[cur] : n->status;
		lvl = LVL_MSG;
	} else {
		b = buf_get(n, m->nick);	/* query */
		lvl = LVL_HILITE;
	}

	print_msg(b, lvl, action ? M_ACTION : notice ? M_NOTICE : M_MSG,
	    m->nick, nick_prefix(b, m->nick), text, 0, mention);
	if (mention)
		push_notify(n, target, m->nick, text);
	else if (b->kind == B_QUERY && !notice)
		push_notify(n, NULL, m->nick, text);
}

/* The window where replies to your commands are shown. */
static struct buffer *
reply_buf(struct network *n)
{
	return bufs[cur]->net == n ? bufs[cur] : n->status;
}

static void
fmt_duration(long secs, char *out, size_t sz)
{
	long	d = secs / 86400, h = secs / 3600 % 24, mi = secs / 60 % 60;

	if (d)
		snprintf(out, sz, "%ldd %ldh %ldm %lds", d, h, mi, secs % 60);
	else if (h)
		snprintf(out, sz, "%ldh %ldm %lds", h, mi, secs % 60);
	else if (mi)
		snprintf(out, sz, "%ldm %lds", mi, secs % 60);
	else
		snprintf(out, sz, "%lds", secs % 60);
}

static void
whois_field(struct buffer *b, const char *label, const char *val)
{
	char	l[16];

	/* "-!- server   : value", continuation lines below the value */
	snprintf(l, sizeof(l), " %-9s: ", label);
	lb_clear();
	lb_stamp();
	lb_text(S_EVPFX, "-!-", 0);
	lb_text(S_DIM, l, 0);
	lb_indent = lb_n;
	lb_text(S_EVENT, val, 1);
	lb_commit(b, LVL_NONE);
}

/*
 * Replies (numerics) to things you did: WHOIS, errors like "No such
 * nick", channel errors and lists. They are shown in the window you are
 * in, or in the channel's window when the reply concerns a channel.
 * Returns 0 if the numeric was not handled here.
 */
static int
handle_reply(struct network *n, struct msg *m)
{
	static const char *chanreplies[] = {
		"324", "329", "346", "347", "348", "349", "367", "368",
		"403", "404", "405", "442", "467", "471", "473", "474",
		"475", "476", "477", "478", "482", "728", "729",
	};
	static const char *userreplies[] = {
		"301", "305", "306", "341", "352", "315", "314", "369",
		"401", "402", "406", "407", "421", "432", "433", "437",
		"441", "443", "461", "462", "481", "491", "501", "502",
		"276", "307", "320", "335", "338", "378", "379", "671",
	};
	const char	*c = m->cmd;
	struct buffer	*b = reply_buf(n), *cb;
	char		 tmp[1024], t1[64], t2[64];
	time_t		 when;
	size_t		 i;

	/* keepnick: MONITOR (730-734) and ISON (303) */
	if (strcmp(c, "731") == 0) {		/* nick(s) went offline: free */
		char *list = P(1), *w;

		while ((w = strsep(&list, ",")) != NULL)
			if (strcasecmp(w, net_nick(n)) == 0)
				keepnick_try(n);
		return 1;
	}
	if (strcmp(c, "730") == 0 || strcmp(c, "732") == 0 ||
	    strcmp(c, "733") == 0)
		return 1;			/* online, list: nothing to show */
	if (strcmp(c, "734") == 0) {		/* MONITOR list full: use ISON */
		n->has_monitor = n->monitoring = 0;
		return 1;
	}
	if (strcmp(c, "303") == 0 && chasing(n)) {
		/* :srv 303 me :nicks that are online */
		char *list = P(1), *w;
		int online = 0;

		while (*(w = word(&list)))
			if (strcasecmp(w, net_nick(n)) == 0)
				online = 1;
		if (!online)
			keepnick_try(n);
		return 1;
	}
	if ((strcmp(c, "433") == 0 || strcmp(c, "437") == 0) && chasing(n) &&
	    time(NULL) - n->nick_try_at < 10)
		return 1;			/* our own attempt: stay quiet */
	if (strcmp(c, "302") == 0) {
		/* :srv 302 me :troll=+user@host other*=-user@host */
		char *list = P(1), *w, *eq, *uh, hm[160];
		int j, used = 0;

		while (*(w = word(&list))) {
			if ((eq = strchr(w, '=')) == NULL)
				continue;
			*eq = 0;
			uh = eq + 1;
			if (*uh == '+' || *uh == '-')
				uh++;
			if (eq > w && eq[-1] == '*')	/* IRC operator */
				eq[-1] = 0;
			host_mask(uh, hm, sizeof(hm));
			for (j = 0; j < nignore; j++)
				if (strcasecmp(ignores[j].mask, w) == 0 &&
				    !ignores[j].host[0] && hm[0]) {
					snprintf(ignores[j].host,
					    sizeof(ignores[j].host), "%s", hm);
					bprint(b, LVL_NONE, "-!- Also ignoring %s "
					    "(the host of %s)", hm, w);
					used = 1;
				}
		}
		if (used)
			save_config();
		else {
			join_params(m, 1, tmp, sizeof(tmp));
			bprint(b, LVL_NONE, "-!- %s", tmp);
		}
		return 1;
	}
	if (strcmp(c, "305") == 0 || strcmp(c, "306") == 0) {
		n->away = c[2] == '6';
		dirty = 1;
	}
	if (strcmp(c, "311") == 0 || strcmp(c, "314") == 0) {
		/* :srv 311 me nick user host * :realname */
		snprintf(tmp, sizeof(tmp), "%s@%s", P(2), P(3));
		eprint(b, LVL_NONE, S_EVPFX, "-!- ", S_JOIN, P(1), S_DIM, " [",
		    S_UHOST, tmp, S_DIM, "]", S_EVENT,
		    c[2] == '4' ? " was" : "", -1);
		whois_field(b, "ircname", P(5));
		return 1;
	}
	if (strcmp(c, "312") == 0) {
		snprintf(tmp, sizeof(tmp), "%s [%s]", P(2), P(3));
		whois_field(b, "server", tmp);
		return 1;
	}
	if (strcmp(c, "319") == 0) {
		whois_field(b, "channels", P(2));
		return 1;
	}
	if (strcmp(c, "330") == 0) {
		whois_field(b, "account", P(2));
		return 1;
	}
	if (strcmp(c, "313") == 0) {
		whois_field(b, "operator", P(2));
		return 1;
	}
	if (strcmp(c, "317") == 0) {
		/* :srv 317 me nick seconds signon :seconds idle, signon time */
		fmt_duration(strtol(P(2), NULL, 10), t1, sizeof(t1));
		when = (time_t)strtoll(P(3), NULL, 10);
		if (when > 0)
			fmt_time(t2, sizeof(t2), "%Y-%m-%d %H:%M", when);
		else
			t2[0] = 0;
		snprintf(tmp, sizeof(tmp), "%s%s%s%s", t1,
		    *t2 ? " [signon: " : "", t2, *t2 ? "]" : "");
		whois_field(b, "idle", tmp);
		return 1;
	}
	if (strcmp(c, "318") == 0 || strcmp(c, "369") == 0) {
		eprint(b, LVL_NONE, S_EVPFX, "-!- ", S_DIM,
		    c[1] == '1' ? "End of WHOIS" : "End of WHOWAS", -1);
		return 1;
	}
	if (strcmp(c, "301") == 0) {		/* away (in whois or on /msg) */
		snprintf(tmp, sizeof(tmp), "%s is away: %s", P(1), P(2));
		whois_field(b, "away", tmp);
		return 1;
	}
	if (strcmp(c, "671") == 0 || strcmp(c, "276") == 0 ||
	    strcmp(c, "338") == 0 || strcmp(c, "378") == 0 ||
	    strcmp(c, "307") == 0 || strcmp(c, "320") == 0 ||
	    strcmp(c, "335") == 0 || strcmp(c, "379") == 0) {
		join_params(m, 2, tmp, sizeof(tmp));
		whois_field(b, c[0] == '6' ? "secure" : c[2] == '6' ?
		    "certfp" : "info", tmp);
		return 1;
	}
	if (strcmp(c, "324") == 0) {		/* the channel's modes */
		if ((cb = buf_find(n, P(1))) == NULL)
			cb = b;
		else {
			cb->cmodes[0] = cb->climit[0] = 0;
			modes_apply(n, cb, P(2), m->params + 3,
			    m->n > 3 ? m->n - 3 : 0);
			if (cb->mode_auto == 1) {	/* our query on join */
				cb->mode_auto = 2;
				return 1;
			}
		}
		join_params(m, 2, tmp, sizeof(tmp));
		eprint(cb, LVL_NONE, S_EVPFX, "-!- ", S_EVENT, "Mode for ",
		    S_CHAN, P(1), S_EVENT, ": ", S_EVENT, tmp, -1);
		return 1;
	}
	if (strcmp(c, "329") == 0) {		/* when the channel was created */
		if ((cb = buf_find(n, P(1))) == NULL)
			cb = b;
		else if (cb->mode_auto) {	/* follows our query on join */
			cb->mode_auto = 0;
			return 1;
		}
		when = (time_t)strtoll(P(2), NULL, 10);
		fmt_time(t2, sizeof(t2), "%Y-%m-%d %H:%M", when);
		eprint(cb, LVL_NONE, S_EVPFX, "-!- ", S_EVENT, "Channel ",
		    S_CHAN, P(1), S_EVENT, " created ", S_EVENT, t2, -1);
		return 1;
	}
	for (i = 0; i < sizeof(chanreplies) / sizeof(chanreplies[0]); i++)
		if (strcmp(c, chanreplies[i]) == 0) {
			if ((cb = buf_find(n, P(1))) == NULL)
				cb = b;
			join_params(m, 1, tmp, sizeof(tmp));
			bprint(cb, c[0] == '4' ? LVL_MSG : LVL_NONE, "-!- %s", tmp);
			return 1;
		}
	for (i = 0; i < sizeof(userreplies) / sizeof(userreplies[0]); i++)
		if (strcmp(c, userreplies[i]) == 0) {
			join_params(m, 1, tmp, sizeof(tmp));
			bprint(b, LVL_NONE, "-!- %s", tmp);
			return 1;
		}
	return 0;
}

/* Case-insensitive glob match with * and ?. */
static int
glob_match(const char *p, const char *s)
{
	const char	*star = NULL, *ss = NULL;

	while (*s) {
		if (*p == '*') {
			star = p++;
			ss = s;
		} else if (*p == '?' || tolower((unsigned char)*p) ==
		    tolower((unsigned char)*s)) {
			p++;
			s++;
		} else if (star != NULL) {
			p = star + 1;
			s = ++ss;
		} else
			return 0;
	}
	while (*p == '*')
		p++;
	return *p == 0;
}

/*
 * Is the sender ignored? A mask without ! or @ matches the nick only,
 * otherwise it is matched against nick!user@host. Servers are never
 * ignored.
 */
static int
is_ignored(struct msg *m)
{
	char	full[256];
	int	i;

	if (*m->nick == 0 || nignore == 0 ||
	    (*m->uhost == 0 && strchr(m->nick, '.') != NULL))
		return 0;
	snprintf(full, sizeof(full), "%s!%s", m->nick, m->uhost);
	for (i = 0; i < nignore; i++) {
		if (ignores[i].hide)		/* a #channel entry */
			continue;
		if (strpbrk(ignores[i].mask, "!@") != NULL) {
			if (glob_match(ignores[i].mask, full))
				return 1;
		} else if (glob_match(ignores[i].mask, m->nick))
			return 1;
		if (ignores[i].host[0] && *m->uhost &&
		    glob_match(ignores[i].host, full))
			return 1;
	}
	return 0;
}

/* Is this kind of event hidden in chan? (/ignore #channel joins ...) */
static int
chan_hides(const char *chan, int what)
{
	int	i;

	for (i = 0; i < nignore; i++)
		if ((ignores[i].hide & what) &&
		    glob_match(ignores[i].mask, chan))
			return 1;
	return 0;
}

/* "joins parts" -> HIDE_JOINS|HIDE_PARTS; nothing means all; -1 on error */
static int
parse_hide(char *s)
{
	static const struct { const char *name; int bit; } hw[] = {
		{ "join", HIDE_JOINS }, { "joins", HIDE_JOINS },
		{ "part", HIDE_PARTS }, { "parts", HIDE_PARTS },
		{ "quit", HIDE_QUITS }, { "quits", HIDE_QUITS },
		{ "nick", HIDE_NICKS }, { "nicks", HIDE_NICKS },
		{ "all", HIDE_ALL },
	};
	char	*w;
	int	 bits = 0;
	size_t	 i;

	while (*(w = word(&s)) != 0) {
		for (i = 0; i < sizeof(hw) / sizeof(hw[0]); i++)
			if (strcasecmp(w, hw[i].name) == 0)
				break;
		if (i == sizeof(hw) / sizeof(hw[0]))
			return -1;
		bits |= hw[i].bit;
	}
	return bits ? bits : HIDE_ALL;
}

/* "joins parts quits" for the list and the config file */
static const char *
hide_words(int bits)
{
	static char	buf[64];

	buf[0] = 0;
	if (bits & HIDE_JOINS)
		strlcat(buf, " joins", sizeof(buf));
	if (bits & HIDE_PARTS)
		strlcat(buf, " parts", sizeof(buf));
	if (bits & HIDE_QUITS)
		strlcat(buf, " quits", sizeof(buf));
	if (bits & HIDE_NICKS)
		strlcat(buf, " nicks", sizeof(buf));
	return buf + 1;
}

/* Remember nick's user@host in every channel of the network it is in. */
static void
mem_seen(struct network *n, const char *nick, const char *uhost)
{
	struct member	*mb;
	int		 i;

	if (*uhost == 0 || strchr(uhost, '@') == NULL)
		return;
	for (i = 0; i < nbufs; i++)
		if (bufs[i]->net == n && bufs[i]->kind == B_CHAN &&
		    (mb = mem_find(bufs[i], nick)) != NULL)
			snprintf(mb->uhost, sizeof(mb->uhost), "%s", uhost);
}

/* nick's user@host from the nick lists, or NULL if unknown. */
static const char *
find_uhost(struct network *n, const char *nick)
{
	struct member	*mb;
	int		 i;

	for (i = 0; i < nbufs; i++)
		if (bufs[i]->net == n && bufs[i]->kind == B_CHAN &&
		    (mb = mem_find(bufs[i], nick)) != NULL && mb->uhost[0])
			return mb->uhost;
	return NULL;
}

/* "*!*@host" from "user@host". */
static void
host_mask(const char *uhost, char *out, size_t sz)
{
	const char	*at = strchr(uhost, '@');

	if (at == NULL || at[1] == 0)
		out[0] = 0;
	else
		snprintf(out, sz, "*!*@%s", at + 1);
}

static void
handle_line(struct network *n, char *line)
{
	struct msg	 mm, *m = &mm;
	struct buffer	*b;
	const char	*cmd;
	char		 tmp[1024];
	int		 me, ign;

	if (parse(line, m) == -1 || *m->cmd == 0)
		return;
	cmd = m->cmd;
	me = strcasecmp(m->nick, n->mynick) == 0;
	mem_seen(n, m->nick, m->uhost);

	/*
	 * Ignored: drop messages, notices, CTCP and invites entirely (no
	 * CTCP replies, no query windows). Joins, parts, quits and nick
	 * changes still update the nick list but are not shown.
	 */
	ign = !me && is_ignored(m);
	if (ign && (strcmp(cmd, "PRIVMSG") == 0 || strcmp(cmd, "NOTICE") == 0 ||
	    strcmp(cmd, "INVITE") == 0))
		return;

	if (strcmp(cmd, "PING") == 0) {
		irc_send(n, "PONG :%s", P(0));
	} else if (strcmp(cmd, "PONG") == 0) {
		/* reply to our keepalive */
	} else if (strcmp(cmd, "CAP") == 0) {
		handle_cap(n, m);
	} else if (strcmp(cmd, "AUTHENTICATE") == 0) {
		/* only while logging in, and only if we asked for SASL */
		if (strcmp(P(0), "+") == 0 && n->state == ST_REGISTER &&
		    n->sasl && n->pass[0] && has_token(n->capreq, "sasl"))
			sasl_send(n);
	} else if (strcmp(cmd, "903") == 0) {
		bprint(n->status, LVL_EVENT, "-!- SASL authentication successful");
		irc_send(n, "CAP END");
	} else if (strcmp(cmd, "902") == 0 || strcmp(cmd, "904") == 0 ||
	    strcmp(cmd, "905") == 0 || strcmp(cmd, "906") == 0) {
		bprint(n->status, LVL_HILITE, "-!- SASL failed: %s", P(1));
		irc_send(n, "CAP END");
	} else if (strcmp(cmd, "001") == 0) {
		n->state = ST_ONLINE;
		n->backoff = BACKOFFMIN;
		n->tokens = SQBURST;		/* registration does not count */
		n->tok_ms = now_ms();
		snprintf(n->mynick, sizeof(n->mynick), "%s", P(0));
		bprint(n->status, LVL_EVENT, "-!- %s", P(1));
		if (n->qauth && n->pass[0])
			q_start(n);		/* joins once logged in to Q */
		else
			do_autojoin(n);
		if (manual_away || auto_away)	/* was away before the reconnect */
			irc_send(n, "AWAY :%s", awaymsg);
	} else if ((strcmp(cmd, "433") == 0 || strcmp(cmd, "432") == 0) &&
	    n->state != ST_ONLINE) {
		/*
		 * Nick in use (433) or invalid (432) during registration.
		 * Append _ while the nick is short, otherwise replace the last
		 * character with a digit, so we do not loop on the server's NICKLEN.
		 */
		size_t l = strlen(n->mynick);

		bprint(n->status, LVL_EVENT, "-!- %s: %s", P(1), P(2));
		if (cmd[2] == '2' || l == 0)
			snprintf(n->mynick, sizeof(n->mynick), "guest%ld",
			    (long)getpid() % 10000);
		else if (l < 9)
			snprintf(n->mynick + l, sizeof(n->mynick) - l, "_");
		else if (isdigit((unsigned char)n->mynick[l - 1]) &&
		    n->mynick[l - 1] != '9')
			n->mynick[l - 1]++;
		else
			n->mynick[l - 1] = '0';
		bprint(n->status, LVL_EVENT, "-!- Trying nick %s", n->mynick);
		irc_send(n, "NICK %s", n->mynick);
	} else if (strcmp(cmd, "PRIVMSG") == 0 || strcmp(cmd, "NOTICE") == 0) {
		handle_msg(n, m);
	} else if (strcmp(cmd, "JOIN") == 0) {
		b = buf_get(n, P(0));
		if (b->kind != B_CHAN)		/* buffer table full */
			b = n->status;
		else if (me) {
			b->joined = 1;
			b->topic[0] = 0;
			b->cmodes[0] = b->climit[0] = 0;
			b->mode_auto = 1;	/* ask quietly for the modes */
			irc_send(n, "MODE %s", P(0));
			b->rejoin = 0;
			mem_clear(b);
			if (strcasecmp(n->switchto, P(0)) == 0) {
				buf_switch(buf_index(b));
				n->switchto[0] = 0;
			}
		}
		if (b->kind == B_CHAN) {
			mem_add(b, m->nick, 0);
			mem_seen(n, m->nick, m->uhost);
		}
		if (!ign && (me || !chan_hides(P(0), HIDE_JOINS)))
			eprint(b, LVL_EVENT, S_EVPFX, "-!- ", S_JOIN, m->nick,
			    S_DIM, UH_L(m), S_UHOST, m->uhost, S_DIM, UH_R(m),
			    S_EVENT, " has joined ", S_CHAN, P(0), -1);
	} else if (strcmp(cmd, "PART") == 0) {
		if ((b = buf_find(n, P(0))) == NULL)
			b = n->status;
		if (me) {
			b->joined = 0;
			mem_clear(b);
		} else
			mem_del(b, m->nick);
		if (!ign && (me || !chan_hides(P(0), HIDE_PARTS)))
			eprint(b, LVL_EVENT, S_EVPFX, "-!- ", S_PART, m->nick,
			    S_DIM, UH_L(m), S_UHOST, m->uhost, S_DIM, UH_R(m),
			    S_EVENT, " has left ", S_CHAN, P(0), S_DIM, " [",
			    S_EVENT, P(1), S_DIM, "]", -1);
	} else if (strcmp(cmd, "KICK") == 0) {
		if ((b = buf_find(n, P(0))) == NULL)
			b = n->status;
		if (strcasecmp(P(1), n->mynick) == 0) {
			b->joined = 0;
			mem_clear(b);
			bprint(b, LVL_HILITE, "-!- You were kicked from %s by %s [%s]",
			    P(0), m->nick, P(2));
		} else {
			mem_del(b, P(1));
			bprint(b, LVL_EVENT, "-!- %s was kicked from %s by %s [%s]",
			    P(1), P(0), m->nick, P(2));
		}
	} else if (strcmp(cmd, "NICK") == 0) {
		struct member *mb;
		int i, shown = 0;

		if (!valid_name(P(0)))
			return;

		for (i = 0; i < nbufs; i++) {
			b = bufs[i];
			if (b->net != n)
				continue;
			if (b->kind == B_CHAN && (mb = mem_find(b, m->nick)) != NULL) {
				snprintf(mb->nick, sizeof(mb->nick), "%s", P(0));
				b->sorted = 0;
			} else if (b->kind == B_QUERY &&
			    strcasecmp(b->name, m->nick) == 0) {
				snprintf(b->name, sizeof(b->name), "%s", P(0));
				save_config();	/* the query has a new name */
			}
			else
				continue;
			if (!ign && (me || b->kind != B_CHAN ||
			    !chan_hides(b->name, HIDE_NICKS)))
				eprint(b, LVL_EVENT, S_EVPFX, "-!- ", S_PART,
				    m->nick, S_EVENT, " is now known as ", S_JOIN,
				    P(0), -1);
			shown = 1;
		}
		if (me || (!shown && !ign))
			eprint(n->status, LVL_EVENT, S_EVPFX, "-!- ", S_PART,
			    m->nick, S_EVENT, " is now known as ", S_JOIN, P(0), -1);
		if (me) {
			int was = chasing(n);

			snprintf(n->mynick, sizeof(n->mynick), "%s", P(0));
			if (was && !chasing(n)) {
				nprint(n, LVL_EVENT, "-!- keepnick: got your nick "
				    "%s back", n->mynick);
				keepnick_stop(n);
			}
		} else if (strcasecmp(m->nick, net_nick(n)) == 0)
			keepnick_try(n);	/* someone let go of our nick */
	} else if (strcmp(cmd, "QUIT") == 0) {
		int i, shown = 0;

		if (strcasecmp(m->nick, net_nick(n)) == 0)
			keepnick_try(n);	/* our nick just became free */

		for (i = 0; i < nbufs; i++) {
			b = bufs[i];
			if (b->net != n)
				continue;
			if (!((b->kind == B_CHAN && mem_del(b, m->nick)) ||
			    (b->kind == B_QUERY &&
			    strcasecmp(b->name, m->nick) == 0)))
				continue;
			shown = 1;		/* hidden or not: not in status */
			if (!ign && (b->kind != B_CHAN ||
			    !chan_hides(b->name, HIDE_QUITS))) {
				eprint(b, LVL_EVENT, S_EVPFX, "-!- ", S_PART, m->nick,
				    S_DIM, UH_L(m), S_UHOST, m->uhost, S_DIM, UH_R(m),
				    S_EVENT, " has quit ", S_DIM, "[", S_EVENT, P(0),
				    S_DIM, "]", -1);
			}
		}
		if (!shown && !ign)
			eprint(n->status, LVL_EVENT, S_EVPFX, "-!- ", S_PART,
			    m->nick, S_DIM, UH_L(m), S_UHOST, m->uhost, S_DIM,
			    UH_R(m), S_EVENT, " has quit ", S_DIM, "[", S_EVENT,
			    P(0), S_DIM, "]", -1);
	} else if (strcmp(cmd, "005") == 0) {
		handle_isupport(n, m);
		join_params(m, 1, tmp, sizeof(tmp));
		bprint(n->status, LVL_EVENT, "-!- %s", tmp);
	} else if (strcmp(cmd, "TOPIC") == 0) {
		if ((b = buf_find(n, P(0))) == NULL)
			b = n->status;
		else
			snprintf(b->topic, sizeof(b->topic), "%s", P(1));
		bprint(b, LVL_EVENT, "-!- %s changed the topic of %s to: %s",
		    m->nick, P(0), P(1));
	} else if (strcmp(cmd, "332") == 0) {
		if ((b = buf_find(n, P(1))) == NULL)
			b = n->status;
		else
			snprintf(b->topic, sizeof(b->topic), "%s", P(2));
		bprint(b, LVL_EVENT, "-!- Topic for %s: %s", P(1), P(2));
	} else if (strcmp(cmd, "331") == 0) {	/* no topic set */
		if ((b = buf_find(n, P(1))) != NULL)
			b->topic[0] = 0;
		dirty = 1;
	} else if (strcmp(cmd, "353") == 0) {
		/* :srv 353 me = #channel :@kalle +pelle johan */
		if ((b = buf_find(n, P(2))) == NULL || !b->joined) {
			bprint(n->status, LVL_EVENT, "-!- Users %s: %s", P(2), P(3));
		} else {
			char *list = P(3), *w;
			const char *nick;
			int modes;

			if (!b->in_names) {
				mem_clear(b);
				b->in_names = 1;
			}
			while (*(w = word(&list))) {
				char *bang;

				nick = w;
				modes = strip_prefix(n, &nick);
				/* userhost-in-names: "@nick!user@host" */
				if ((bang = strchr(nick, '!')) != NULL)
					*bang++ = 0;
				mem_add(b, nick, modes);
				if (bang != NULL)
					mem_seen(n, nick, bang);
			}
		}
	} else if (strcmp(cmd, "366") == 0) {
		if ((b = buf_find(n, P(1))) != NULL && b->in_names) {
			b->in_names = 0;
			names_summary(b);
		}
	} else if (strcmp(cmd, "333") == 0) {
		/* topic metadata: not interesting */
	} else if (strcmp(cmd, "MODE") == 0) {
		if (!ischan(P(0)) || (b = buf_find(n, P(0))) == NULL)
			b = n->status;
		else
			apply_modes(n, b, m);
		join_params(m, 1, tmp, sizeof(tmp));
		bprint(b, LVL_EVENT, "-!- mode/%s [%s] by %s",
		    P(0), tmp, m->nick);
	} else if (strcmp(cmd, "ERROR") == 0) {
		bprint(n->status, LVL_HILITE, "-!- ERROR: %s", P(0));
	} else if (isdigit((unsigned char)cmd[0])) {
		if (!handle_reply(n, m)) {
			join_params(m, 1, tmp, sizeof(tmp));
			bprint(n->status, LVL_EVENT, "-!- %s", tmp);
		}
	} else {
		join_params(m, 0, tmp, sizeof(tmp));
		bprint(n->status, LVL_EVENT, "-!- %s %s %s", m->nick, cmd, tmp);
	}
}

static void
net_read(struct network *n)
{
	ssize_t	 r;
	char	*nl, *p;
	size_t	 len;

	for (;;) {
		if (n->rlen >= sizeof(n->rbuf) - 1) {	/* line too long */
			n->rlen = 0;
			n->rdiscard = 1;	/* its tail is not a line either */
		}
		if (n->ctx != NULL) {
			r = tls_read(n->ctx, n->rbuf + n->rlen,
			    sizeof(n->rbuf) - 1 - n->rlen);
			if (r == TLS_WANT_POLLIN) {
				n->want = POLLIN;
				return;
			}
			if (r == TLS_WANT_POLLOUT) {
				n->want = POLLOUT;
				return;
			}
		} else {
			r = read(n->sock, n->rbuf + n->rlen,
			    sizeof(n->rbuf) - 1 - n->rlen);
			if (r == -1 && (errno == EAGAIN || errno == EINTR)) {
				n->want = POLLIN;
				return;
			}
		}
		if (r == 0) {
			net_fail(n, "connection closed by server");
			return;
		}
		if (r < 0) {
			net_fail(n, n->ctx ? tls_error(n->ctx) : strerror(errno));
			return;
		}
		n->want = POLLIN;
		n->last_rx = time(NULL);
		n->ping_sent = 0;
		n->rlen += r;

		while ((nl = memchr(n->rbuf, '\n', n->rlen)) != NULL) {
			len = nl - n->rbuf;
			*nl = 0;
			if (len > 0 && n->rbuf[len - 1] == '\r')
				n->rbuf[len - 1] = 0;
			/* a CR inside a line must never reach a reply or a file */
			for (p = n->rbuf; (p = strchr(p, '\r')) != NULL; )
				*p = ' ';
			if (n->rdiscard)
				n->rdiscard = 0;
			else
				handle_line(n, n->rbuf);
			if (n->sock == -1)
				return;
			n->rlen -= len + 1;
			memmove(n->rbuf, nl + 1, n->rlen);
		}
	}
}

/* Event on the network's socket. */
static void
net_event(struct network *n)
{
	switch (n->state) {
	case ST_CONNECTING:
		net_connect_done(n);
		break;
	case ST_HANDSHAKE:
		net_handshake(n);
		break;
	case ST_REGISTER:
	case ST_ONLINE:
		if (n->olen > 0)
			conn_flush(n);
		if (n->sock != -1)
			net_read(n);
		break;
	}
}

/* Runs once a second: timeouts, keepalive, reconnect. */
static void
net_tick(struct network *n, time_t now)
{
	if (n->state == ST_RESOLVING || n->state == ST_CONNECTING ||
	    n->state == ST_HANDSHAKE) {
		if (now - n->started > CONNTIMEOUT)
			net_fail(n, "connection timed out");
	} else if (n->state >= ST_REGISTER) {
		keepnick_tick(n, now);
		if (n->state < ST_REGISTER)
			return;
		if (now - n->last_rx > PINGTIMEOUT)
			net_fail(n, "ping timeout");
		else if ((n->qstate == Q_CHALLENGE || n->qstate == Q_RESULT) &&
		    now - n->qstart > QWAIT) {
			bprint(n->status, LVL_HILITE, "-!- Q did not answer, "
			    "joining without logging in");
			q_done(n, 0);
		} else if (n->olen > 0 && now - n->oprogress > PINGTIMEOUT)
			net_fail(n, "the server has not taken our data for "
			    "5 minutes");
		else if (now - n->last_rx > PINGIDLE && !n->ping_sent) {
			irc_send(n, "PING :bsdirc");
			n->ping_sent = now;
		}
	} else if (n->wantconn && n->retry_at && now >= n->retry_at)
		net_connect(n);
}

/* ---------- commands ---------- */

/*
 * "IDENTIFY secret" to NickServ or "AUTH user secret" to Q: such lines
 * are shown and logged as "IDENTIFY ****". Returns 1 and fills out.
 */
static int
mask_secret(const char *target, const char *text, char *out, size_t sz)
{
	static const char *const cmds[] = {
		"identify", "auth", "login", "register", "ghost", "recover",
		"release", "regain", "newpass", "setpass", "challengeauth",
	};
	size_t	 i, l = strcspn(text, " ");

	if (ischan(target) || text[l] == 0)
		return 0;
	for (i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++)
		if (strlen(cmds[i]) == l && strncasecmp(text, cmds[i], l) == 0)
			break;
	if (strncasecmp(text, "set password ", 13) == 0)
		l = 12;				/* "SET PASSWORD ****" */
	else if (i == sizeof(cmds) / sizeof(cmds[0]))
		return 0;
	snprintf(out, sz, "%.*s ****", (int)l, text);
	return 1;
}

static void
send_privmsg(struct network *n, const char *target, const char *text,
    int action)
{
	struct buffer	*b;
	size_t		 len = strlen(text), off = 0, c;
	char		 chunk[CHUNK + 1], shown[64];

	if (n == NULL || n->state != ST_ONLINE) {
		uprint("-!- Not connected");
		return;
	}
	b = buf_get(n, target);
	if (b == bufs[cur])		/* you are writing: you have read everything */
		b->mark = -1;
	while (off < len && n->state == ST_ONLINE) {
		c = len - off;
		if (c > CHUNK) {		/* do not split in the middle of a UTF-8 character */
			c = CHUNK;
			while (c > 0 && ((unsigned char)text[off + c] & 0xc0) == 0x80)
				c--;
			if (c == 0)
				c = CHUNK;
		}
		memcpy(chunk, text + off, c);
		chunk[c] = 0;
		if (action)
			irc_send(n, "PRIVMSG %s :\001ACTION %s\001", target, chunk);
		else
			irc_send(n, "PRIVMSG %s :%s", target, chunk);
		if (!action && off == 0 &&
		    mask_secret(target, chunk, shown, sizeof(shown)))
			print_msg(b, LVL_NONE, M_MSG, n->mynick,
			    nick_prefix(b, n->mynick), shown, 1, 0);
		else
			print_msg(b, LVL_NONE, action ? M_ACTION : M_MSG,
			    n->mynick, nick_prefix(b, n->mynick), chunk, 1, 0);
		off += c;
	}
}

static void
cmd_help(void)
{
	static const char *h[] = {
		"Networks:",
		"  /server                      list networks",
		"  /server add NAME HOST [PORT] [-tls|-notls] [-sasl|-qauth] [-auto]",
		"              [-insecure] [-bind IP]",
		"  /server set NAME KEY VALUE   (host port tls insecure sasl qauth qhide account",
		"              password nick user realname autoconnect bind)",
		"  /server del NAME",
		"  /connect [NAME]   /disconnect [NAME] [msg]   /reconnect [NAME]",
		"Examples:",
		"  /server add libera irc.libera.chat 6697 -sasl -auto",
		"  /server set libera password SECRET",
		"  /server set quakenet qauth yes    log in to Q (account + password)",
		"  /server set quakenet qhide no     ... but do not set +x",
		"  /server set quakenet tls yes      switch to TLS ...",
		"  /server set quakenet port 6697    ... on the TLS port",
		"  /reconnect quakenet               new settings take effect",
		"  /server set quakenet tls no       back to plain TCP (port 6667)",
		"  Values: yes/no for tls, insecure, sasl, autoconnect; \"-\" clears",
		"Autojoin (for the network of the current window):",
		"  /autojoin                    list",
		"  /autojoin add [#channel [key]]   /autojoin del [#channel]",
		"  /autojoin save               save all channels you are in now",
		"Channels and messages:",
		"  /join #channel [key]   /part [msg]   /msg nick text",
		"  /query nick   /me text   /nick new   /topic [text]   /names",
		"  (taken nick? bsdirc wins the config nick back when it is free)",
		"  /whois [nick] (no nick: yourself)   /ctcp nick version|ping|time",
		"  /away [msg]   /autoaway [minutes]  (tmux detach: window 1 at",
		"  once, away after the given minutes)",
		"  /ignore [nick|mask]  (nick: also its host)   /unignore nick|N",
		"  /ignore #channel [joins parts quits nicks]  hide those events",
		"Channel operator (current channel, or give #channel first):",
		"  /op /deop /voice /devoice nick [nick ...]",
		"  /kick nick [reason]   /kb nick [reason]  (ban *!*@host, then kick)",
		"  /ban nick|mask   /unban mask   /invite nick [#channel]",
		"  /mode [+-modes args]  (no args: show; outside a channel: your",
		"  user modes, e.g. /mode +i)",
		"Pushover (phone notices when you are mentioned or get a private",
		"message, at most one a minute per window):",
		"  /pushover token KEY   /pushover user KEY   /pushover test",
		"  /pushover away|always|off   (away: only while away, default)",
		"Other:",
		"  /win N   /windows   /close   /clear",
		"  /set theme default|irssi|bw   colours of the status and topic bars",
		"  /set backlog N   log lines shown in a new window (0 = off)",
		"  /url [N]   this window's last 5 links, or link N whole (clickable)",
		"  /url clear   forget this window's links",
		"  /bare, Alt-l   the window as plain text, for copying; any key back",
		"  /nicklist, /serverlist     show or hide the side panels",
		"  /raw LINE   /save   /quit [msg]",
		"  Unknown commands are sent to the server as is (/list, /who ...)",
		"Keys: Alt/Esc 1..0 and q..o windows 1-19, Alt-a activity,",
		"      ^N/^P next/previous window,",
		"      Tab/Shift-Tab complete nick, channel and command,",
		"      Alt-n nick list, Alt-s window list, Alt-l bare view,",
		"      PgUp/PgDn scroll,",
		"      Up/Down input history, Ctrl-K/Ctrl-C when pasting,",
		"      ^A/^E/^U/^W/^K edit the line, ^L redraw",
	};
	size_t i;

	for (i = 0; i < sizeof(h) / sizeof(h[0]); i++)
		uprint("-!- %s", h[i]);
}

static void
cmd_server(char *arg)
{
	struct network	*n;
	char		*sub = word(&arg), *name, *host, *opt, *key;
	int		 tlsopt = -1, portset = 0;
	int		 i;

	if (*sub == 0 || strcasecmp(sub, "list") == 0) {
		if (nnets == 0)
			uprint("-!- No networks. Try: /server add libera "
			    "irc.libera.chat 6697 -sasl -auto");
		for (i = 0; i < nnets; i++) {
			n = nets[i];
			uprint("-!- %-10s %s:%s%s%s%s%s  autojoin %d  [%s%s%s]",
			    n->name, n->host[0] ? n->host : "(no host)", n->port,
			    n->tls ? " tls" : "", n->insecure ? " insecure" : "",
			    n->sasl ? " sasl" : n->qauth ? " qauth" : "",
			    n->autoconnect ? " auto" : "",
			    n->nauto, state_name(n->state),
			    n->state == ST_ONLINE ? " as " : "",
			    n->state == ST_ONLINE ? n->mynick : "");
		}
		return;
	}

	if (strcasecmp(sub, "add") == 0) {
		name = word(&arg);
		host = word(&arg);
		if (!valid_netname(name) || *host == 0) {
			uprint("-!- usage: /server add NAME HOST [PORT] [-tls|-notls] "
			    "[-insecure] [-sasl|-qauth] [-auto] [-bind IP]");
			return;
		}
		if (net_find(name) != NULL) {
			uprint("-!- Network %s already exists", name);
			return;
		}
		if ((n = net_new(name)) == NULL) {
			uprint("-!- Too many networks");
			return;
		}
		snprintf(n->host, sizeof(n->host), "%s", host);
		while (*(opt = word(&arg))) {
			if (valid_port(opt)) {
				snprintf(n->port, sizeof(n->port), "%s", opt);
				portset = 1;
			} else if (strcmp(opt, "-notls") == 0)
				tlsopt = 0;
			else if (strcmp(opt, "-tls") == 0)
				tlsopt = 1;
			else if (strcmp(opt, "-insecure") == 0)
				n->insecure = 1;
			else if (strcmp(opt, "-sasl") == 0)
				n->sasl = 1;
			else if (strcmp(opt, "-qauth") == 0)
				n->qauth = 1;
			else if (strcmp(opt, "-auto") == 0)
				n->autoconnect = 1;
			else if (strcmp(opt, "-bind") == 0) {
				opt = word(&arg);
				if (valid_ip(opt))
					snprintf(n->bind, sizeof(n->bind), "%s", opt);
				else
					uprint("-!- -bind needs an IP address");
			}
			else
				uprint("-!- Ignoring unknown option %s", opt);
		}
		/*
		 * -tls/-notls wins. Otherwise the default is plain TCP, and TLS
		 * only on port 6697. Without a port: 6697 with -tls, else 6667.
		 */
		if (tlsopt != -1)
			n->tls = tlsopt;
		else
			n->tls = portset && strcmp(n->port, "6697") == 0;
		if (!portset)
			snprintf(n->port, sizeof(n->port), "%s",
			    n->tls ? "6697" : "6667");
		uprint("-!- Added %s (%s port %s, %s). Connect with /connect %s",
		    n->name, n->host, n->port, n->tls ? "TLS" : "no TLS", n->name);
		if (n->sasl)
			uprint("-!- Set the password: /server set %s password ...",
			    n->name);
		save_config();
		main_merge();
		return;
	}

	if (strcasecmp(sub, "del") == 0 || strcasecmp(sub, "rm") == 0) {
		if ((n = net_find(word(&arg))) == NULL) {
			uprint("-!- No such network");
			return;
		}
		net_disconnect(n, NULL);
		if (nnets == 1)			/* the last network: main window back */
			buf_new(NULL, "bsdirc", B_MAIN);
		for (i = nbufs - 1; i >= 0; i--)
			if (bufs[i]->net == n)
				buf_close(i);
		if (nnets == 1)
			cur = buf_index(sysbuf());
		for (i = 0; i < nnets; i++)
			if (nets[i] == n)
				break;
		memmove(&nets[i], &nets[i + 1], (nnets - i - 1) * sizeof(nets[0]));
		nnets--;
		uprint("-!- Removed network %s", n->name);
		explicit_bzero(n->pass, sizeof(n->pass));
		free(n);
		save_config();
		return;
	}

	if (strcasecmp(sub, "set") == 0) {
		if ((n = net_find(word(&arg))) == NULL) {
			uprint("-!- usage: /server set NAME KEY VALUE");
			return;
		}
		key = word(&arg);
		if (strcmp(key, "join") == 0 || net_set(n, key, arg) == -1) {
			uprint("-!- Bad setting: %s %s", key, arg);
			return;
		}
		if (strcmp(key, "password") == 0)
			uprint("-!- %s: password %s", n->name,
			    *arg && strcmp(arg, "-") ? "set" : "cleared");
		else
			uprint("-!- %s: %s = %s", n->name, key, arg);
		if (n->state != ST_OFFLINE && (strcmp(key, "host") == 0 ||
		    strcmp(key, "port") == 0 || strcmp(key, "tls") == 0))
			uprint("-!- Takes effect after /reconnect %s", n->name);
		save_config();
		return;
	}
	uprint("-!- usage: /server [list|add|set|del]");
}

static void
cmd_autojoin(char *arg)
{
	struct network	*n = curnet();
	struct buffer	*b = bufs[cur];
	char		*sub = word(&arg), entry[128];
	int		 i, cnt = 0;

	if (n == NULL) {
		uprint("-!- No networks, add one with /server add");
		return;
	}
	if (*sub == 0 || strcasecmp(sub, "list") == 0) {
		uprint("-!- Autojoin on %s: %d channel(s)", n->name, n->nauto);
		for (i = 0; i < n->nauto; i++)
			uprint("-!-   %s", n->autojoin[i]);
		return;
	}
	if (strcasecmp(sub, "add") == 0) {
		if (*arg == 0) {
			if (b->net != n || b->kind != B_CHAN) {
				uprint("-!- usage: /autojoin add #channel [key]");
				return;
			}
			snprintf(entry, sizeof(entry), "%s", b->name);
		} else
			snprintf(entry, sizeof(entry), "%s", arg);
		if (auto_add(n, entry) == -1) {
			uprint("-!- Could not add %s", entry);
			return;
		}
		uprint("-!- %s: added %s to autojoin", n->name, entry);
		auto_windows(n);
		save_config();
		return;
	}
	if (strcasecmp(sub, "del") == 0 || strcasecmp(sub, "rm") == 0) {
		if (*arg == 0 && b->net == n && b->kind == B_CHAN)
			arg = b->name;
		if (auto_del(n, word(&arg)) == -1) {
			uprint("-!- Not in autojoin");
			return;
		}
		uprint("-!- %s: removed from autojoin", n->name);
		save_config();
		return;
	}
	if (strcasecmp(sub, "save") == 0) {
		/* replace the list with the channels we are in right now, keep keys */
		char	old[MAXAUTO][128];
		int	nold = n->nauto, j;

		memcpy(old, n->autojoin, sizeof(old));
		n->nauto = 0;
		for (i = 0; i < nbufs; i++) {
			if (bufs[i]->net != n || !bufs[i]->joined)
				continue;
			snprintf(entry, sizeof(entry), "%s", bufs[i]->name);
			for (j = 0; j < nold; j++)	/* keep any key */
				if (strncasecmp(old[j], entry, strlen(entry)) == 0 &&
				    (old[j][strlen(entry)] == ' ' ||
				    old[j][strlen(entry)] == 0))
					snprintf(entry, sizeof(entry), "%s", old[j]);
			if (auto_add(n, entry) == 0)
				cnt++;
		}
		uprint("-!- %s: autojoin is now %d channel(s)", n->name, cnt);
		for (i = 0; i < n->nauto; i++)
			uprint("-!-   %s", n->autojoin[i]);
		save_config();
		return;
	}
	uprint("-!- usage: /autojoin [list|add|del|save]");
}

/* /names in a channel window: show the local list, irssi style */
static void
cmd_names(struct buffer *b)
{
	char	line[512];
	size_t	len = 0;
	int	i, per = COLS > 40 ? (COLS - 10) : 30;

	mem_sort(b);
	bprint(b, LVL_NONE, "-!- Users %s:", b->name);
	line[0] = 0;
	for (i = 0; i < b->nmem; i++) {
		len += snprintf(line + len, sizeof(line) - len, "[%c%-12s] ",
		    prefix_char(b->net, b->mem[i].modes), b->mem[i].nick);
		if ((int)len > per - 16 || len >= sizeof(line) - 80) {
			bprint(b, LVL_NONE, "%s", line);
			len = 0;
			line[0] = 0;
		}
	}
	if (len)
		bprint(b, LVL_NONE, "%s", line);
	names_summary(b);
}

/* Set away (msg) or remove it (msg == NULL) on all connected networks. */
static void
set_away(const char *msg)
{
	int	i;

	if (msg != NULL && msg != awaymsg)	/* do not copy onto itself */
		snprintf(awaymsg, sizeof(awaymsg), "%s", msg);
	for (i = 0; i < nnets; i++) {
		if (nets[i]->state != ST_ONLINE)
			continue;
		if (msg != NULL)
			irc_send(nets[i], "AWAY :%s", awaymsg);
		else
			irc_send(nets[i], "AWAY");
	}
	dirty = 1;
}

/* tmux hooks send SIGUSR1 on detach and SIGUSR2 on attach */
static void
sig_tmux(int sig)
{
	if (sig == SIGUSR1)
		got_detach = 1;
	else
		got_attach = 1;
}

/* Shared by both methods: new state for whether the session is attached. */
static void
tmux_set_attached(int att)
{
	if (att == tmux_attached)
		return;
	if (att)
		got_attach = 1;
	else
		got_detach = 1;		/* even if bsdirc was started detached */
	if (tmux_attached != -1 || !att)
		bprint(sysbuf(), LVL_NONE, "-!- tmux: %s", att ? "attached" :
		    "detached");
	tmux_attached = att;
}

/*
 * Is any tmux client attached? tmux sets the execute bit on its socket
 * while a client is attached and clears it when all have detached, so
 * stat() is enough. No hooks in tmux.conf are needed.
 */
static void
tmux_check(void)
{
	struct stat	st;
	int		att;

	if (tmux_sock[0] == 0)
		return;
	if (stat(tmux_sock, &st) == -1) {
		if (tmux_err != errno)
			bprint(sysbuf(), LVL_NONE, "-!- tmux: cannot stat %s: %s",
			    tmux_sock, strerror(errno));
		tmux_err = errno;
		return;
	}
	tmux_err = 0;
	att = (st.st_mode & S_IXUSR) != 0;
	tmux_set_attached(att);
}

static void
away_check(time_t now)
{

	if (got_detach) {
		got_detach = 0;
		detached_at = now;
		/*
		 * Like irssi: go to window 1, so that a highlight in the
		 * window you were looking at is not missed. It shows up in
		 * the Act list and the window list when you come back.
		 */
		buf_switch(0);
		if (autoaway > 0 && !manual_away)
			bprint(sysbuf(), LVL_NONE, "-!- Auto-away in %g min unless "
			    "you come back", autoaway);
	}
	if (got_attach) {
		got_attach = 0;
		detached_at = 0;
		if (auto_away) {
			auto_away = 0;
			set_away(NULL);
			bprint(sysbuf(), LVL_NONE, "-!- Welcome back, auto-away removed");
		}
	}
	if (detached_at && autoaway > 0 && !manual_away && !auto_away &&
	    now - detached_at >= (time_t)(autoaway * 60)) {
		auto_away = 1;
		set_away(awaymsg);
		bprint(sysbuf(), LVL_NONE, "-!- Auto-away: tmux detached for "
		    "%g min", autoaway);
	}
}

/* A ban mask for nick: *!*@host if the host is known, else nick!*@* */
static void
ban_mask(struct network *n, const char *w, char *out, size_t sz)
{
	const char	*uh;

	if (strpbrk(w, "!@*?") != NULL)		/* already a mask */
		snprintf(out, sz, "%s", w);
	else if ((uh = find_uhost(n, w)) != NULL && strchr(uh, '@'))
		host_mask(uh, out, sz);
	else
		snprintf(out, sz, "%s!*@*", w);
}

/* MODE #chan +ooo a b c, split by the server's MODES limit. */
static void
mode_batch(struct network *n, const char *chan, int add, char mode, char *arg)
{
	char	 flags[16], nicks[512], *w;
	int	 max = n->maxmodes > 0 ? n->maxmodes : 3, k = 0;

	if (max > 12)
		max = 12;
	nicks[0] = 0;
	for (;;) {
		w = word(&arg);
		if (*w) {
			if (nicks[0])
				strlcat(nicks, " ", sizeof(nicks));
			strlcat(nicks, w, sizeof(nicks));
			k++;
		}
		if (k > 0 && (k == max || *w == 0)) {
			memset(flags, mode, k);
			flags[k] = 0;
			irc_send(n, "MODE %s %c%s %s", chan, add ? '+' : '-',
			    flags, nicks);
			nicks[0] = 0;
			k = 0;
		}
		if (*w == 0)
			break;
	}
}

/*
 * Channel operator commands. They act on the current channel unless the
 * first argument is a channel. Returns 0 if cmd is not one of them.
 */
static int
cmd_chanop(struct buffer *b, struct network *n, const char *cmd, char *arg)
{
	static const struct { const char *name; char mode; int add; } nm[] = {
		{ "op", 'o', 1 }, { "deop", 'o', 0 },
		{ "voice", 'v', 1 }, { "devoice", 'v', 0 },
	};
	const char	*chan = NULL, *usage = NULL;
	char		*p = arg, *w, mask[256];
	size_t		 i;

	for (i = 0; i < sizeof(nm) / sizeof(nm[0]); i++)
		if (strcasecmp(cmd, nm[i].name) == 0)
			break;
	if (i == sizeof(nm) / sizeof(nm[0]) && strcasecmp(cmd, "kick") &&
	    strcasecmp(cmd, "k") && strcasecmp(cmd, "ban") &&
	    strcasecmp(cmd, "unban") && strcasecmp(cmd, "kb") &&
	    strcasecmp(cmd, "invite") && strcasecmp(cmd, "mode"))
		return 0;

	if (n == NULL || n->state < ST_REGISTER) {
		uprint("-!- Not connected");
		return 1;
	}
	/* explicit #channel first ("+i" is a mode here, not a +channel) */
	if (ischan(p) && *p != '+') {
		chan = word(&p);
	} else if (b->kind == B_CHAN)
		chan = b->name;

	if (strcasecmp(cmd, "mode") == 0) {
		if (chan == NULL) {		/* user modes: /mode +i */
			if (*p)
				irc_send(n, "MODE %s %s", n->mynick, p);
			else
				irc_send(n, "MODE %s", n->mynick);
		} else if (*p)
			irc_send(n, "MODE %s %s", chan, p);
		else
			irc_send(n, "MODE %s", chan);
		return 1;
	}
	if (strcasecmp(cmd, "invite") == 0) {
		w = word(&p);
		if (*p && ischan(p))		/* /invite nick #chan */
			chan = word(&p);
		if (*w == 0 || chan == NULL)
			usage = "/invite nick [#channel]";
		else
			irc_send(n, "INVITE %s %s", w, chan);
		goto out;
	}
	if (chan == NULL) {
		uprint("-!- Not in a channel; give the channel first, e.g. "
		    "/%s #channel ...", cmd);
		return 1;
	}
	if (i < sizeof(nm) / sizeof(nm[0])) {
		if (*p == 0)
			usage = nm[i].add ? "/op nick [nick ...]  (also "
			    "/deop /voice /devoice)" :
			    "/deop nick [nick ...]";
		else
			mode_batch(n, chan, nm[i].add, nm[i].mode, p);
		goto out;
	}
	w = word(&p);
	if (*w == 0) {
		usage = strcasecmp(cmd, "unban") == 0 ? "/unban mask" :
		    strcasecmp(cmd, "ban") == 0 ? "/ban nick|mask" :
		    "/kick nick [reason]  or  /kb nick [reason]";
		goto out;
	}
	if (strcasecmp(cmd, "unban") == 0) {
		irc_send(n, "MODE %s -b %s", chan, w);
		return 1;
	}
	if (strcasecmp(cmd, "ban") == 0 || strcasecmp(cmd, "kb") == 0) {
		ban_mask(n, w, mask, sizeof(mask));
		irc_send(n, "MODE %s +b %s", chan, mask);
		if (strcasecmp(cmd, "ban") == 0)
			return 1;
	}
	if (*p)
		irc_send(n, "KICK %s %s :%s", chan, w, p);
	else
		irc_send(n, "KICK %s %s", chan, w);
	return 1;
out:
	if (usage != NULL)
		uprint("-!- usage: %s", usage);
	return 1;
}

/* ---------- links and the bare view ---------- */

#define URLMAX	200	/* links kept, all windows together */
#define URLSHOW	5	/* ... and listed by /url */

static struct {
	char	*url;
	char	 who[64];
	char	 net[32];		/* network and window it came from */
	char	 where[64];
	time_t	 t;
} urls[URLMAX];
static int	nurls;

static int		bare;		/* the bare view is showing */
static struct termios	bare_tio;	/* the terminal as curses left it */

/*
 * Remember http:// and https:// links in a message for /url. The text
 * goes through the same decoder as the screen, so a link can never
 * carry control characters into the bare view.
 */
static void
url_scan(struct buffer *b, const char *nick, const char *text)
{
	char		 plain[1024], raw[1024], *p, *e, *u;
	wchar_t		 w[1024];
	size_t		 l, wn;

	strip_codes(text, plain, sizeof(plain));
	for (p = plain; *p; p = e) {
		if (strncasecmp(p, "http://", 7) != 0 &&
		    strncasecmp(p, "https://", 8) != 0) {
			e = p + 1;
			continue;
		}
		for (e = p; *e && !isspace((unsigned char)*e) &&
		    strchr("<>\"\001", *e) == NULL; e++)
			;
		l = e - p;
		/* "(see https://x.org/a)." - not the closing punctuation */
		while (l > 8 && strchr(".,;:!?'", p[l - 1]) != NULL)
			l--;
		if (l > 8 && p[l - 1] == ')' && memchr(p, '(', l) == NULL)
			l--;
		if (l >= sizeof(raw) || l <= 8)
			continue;
		memcpy(raw, p, l);
		raw[l] = 0;
		wn = utf8_decode(raw, w, sizeof(w) / sizeof(w[0]));
		utf8_encode(w, wn, raw, sizeof(raw));
		if ((u = strdup(raw)) == NULL)
			fatal("strdup");
		if (nurls == URLMAX) {
			free(urls[0].url);
			memmove(&urls[0], &urls[1], (URLMAX - 1) * sizeof(urls[0]));
			nurls--;
		}
		urls[nurls].url = u;
		snprintf(urls[nurls].who, sizeof(urls[0].who), "%s", nick);
		snprintf(urls[nurls].net, sizeof(urls[0].net), "%s",
		    b->net ? b->net->name : "");
		snprintf(urls[nurls].where, sizeof(urls[0].where), "%s", b->name);
		urls[nurls].t = time(NULL);
		nurls++;
	}
}

/*
 * The bare view, as in WeeChat: leave curses and print the window's
 * last lines (or one link) as plain text, so the terminal wraps them
 * itself. Links are then whole on the screen: clickable, and copied
 * without the indent or the nick list's border. The network keeps
 * running in the poll loop; any key goes back.
 */
static void
bare_show(struct buffer *b, const char *only)
{
	struct termios	 t;
	struct line	*l;
	char		 out[LBMAX * 4 + 1];
	int		 i, n, first;

	if (bare)
		return;
	endwin();
	curses_on = 0;
	if (tcgetattr(STDIN_FILENO, &bare_tio) == 0) {
		t = bare_tio;
		t.c_lflag &= ~(ICANON | ECHO);
		t.c_cc[VMIN] = 1;
		t.c_cc[VTIME] = 0;
		tcsetattr(STDIN_FILENO, TCSANOW, &t);
	}
	printf("\n");
	if (only != NULL)
		printf("%s\n", only);
	else {
		n = b->total < SCROLLBACK ? b->total : SCROLLBACK;
		first = n > LINES - 2 ? n - (LINES - 2) : 0;
		for (i = first; i < n; i++) {
			l = buf_line(b, i);
			utf8_encode(l->w, l->n, out, sizeof(out));
			printf("%s\n", out);
		}
	}
	printf("\n-- press any key to return to bsdirc --");
	fflush(stdout);
	bare = 1;
}

static void
bare_exit(void)
{
	struct pollfd	kp = { STDIN_FILENO, POLLIN, 0 };
	char		junk[64];

	/* swallow the rest of the key (an arrow is several bytes) */
	while (poll(&kp, 1, 50) == 1 && (kp.revents & POLLIN) &&
	    read(STDIN_FILENO, junk, sizeof(junk)) > 0)
		;
	tcsetattr(STDIN_FILENO, TCSANOW, &bare_tio);
	printf("\n");
	fflush(stdout);
	bare = 0;
	curses_on = 1;
	clearok(curscr, TRUE);
	refresh();
	dirty = 1;
}

/* Was link i posted in window b? */
static int
url_here(int i, struct buffer *b)
{
	return strcmp(urls[i].net, b->net ? b->net->name : "") == 0 &&
	    strcasecmp(urls[i].where, b->name) == 0;
}

/* The n-th newest link in window b (1 = newest), or -1. */
static int
url_nth(struct buffer *b, int n)
{
	int	i;

	for (i = nurls - 1; i >= 0; i--)
		if (url_here(i, b) && --n == 0)
			return i;
	return -1;
}

/*
 * Links are kept per window: /url lists the last URLSHOW links posted
 * in the window you are in, /url N shows one of them alone, unwrapped,
 * and /url clear forgets this window's links.
 */
static void
cmd_url(char *arg)
{
	struct buffer	*b = bufs[cur];
	char		 tb[16];
	int		 i, j, k, here = 0;

	for (i = 0; i < nurls; i++)
		here += url_here(i, b);

	if (strcasecmp(arg, "clear") == 0) {
		for (i = j = 0; i < nurls; i++) {
			if (url_here(i, b))
				free(urls[i].url);
			else
				urls[j++] = urls[i];
		}
		nurls = j;
		uprint("-!- %d link%s in %s cleared", here, here == 1 ? "" : "s",
		    b->name);
		return;
	}
	if (*arg == 0) {
		if (here == 0) {
			uprint("-!- No links in %s yet", b->name);
			return;
		}
		for (k = 1; k <= URLSHOW && (i = url_nth(b, k)) != -1; k++) {
			fmt_time(tb, sizeof(tb), "%H:%M", urls[i].t);
			uprint("-!- %d: [%s] %s: %s", k, tb, urls[i].who,
			    urls[i].url);
		}
		uprint("-!- /url N shows link N alone, whole and clickable"
		    "%s; /url clear forgets them", here > URLSHOW ?
		    " (older ones too)" : "");
		return;
	}
	k = atoi(arg);
	if (k < 1 || (i = url_nth(b, k)) == -1) {
		uprint("-!- usage: /url [N|clear]  (%s has %d link%s, 1 is "
		    "the newest)", b->name, here, here == 1 ? "" : "s");
		return;
	}
	bare_show(b, urls[i].url);
}

/* Index of the theme called name, or -1. */
static int
theme_find(const char *name)
{
	size_t	i;

	for (i = 0; i < sizeof(themes) / sizeof(themes[0]); i++)
		if (strcasecmp(themes[i].name, name) == 0)
			return (int)i;
	return -1;
}

/* /set [theme [NAME]] */
static void
cmd_set(char *arg)
{
	char	*key = word(&arg), *val = word(&arg), names[128];
	size_t	 i;
	int	 t;

	names[0] = 0;
	for (i = 0; i < sizeof(themes) / sizeof(themes[0]); i++) {
		if (i)
			strlcat(names, ", ", sizeof(names));
		strlcat(names, themes[i].name, sizeof(names));
	}
	if (*key == 0) {
		uprint("-!- theme = %s  (%s)", themes[theme].name, names);
		uprint("-!- backlog = %d  (lines from the log in a new "
		    "window, 0 = off)", backlog);
		return;
	}
	if (strcasecmp(key, "backlog") == 0) {
		if (*val == 0) {
			uprint("-!- backlog = %d", backlog);
			return;
		}
		if ((t = parse_int(val, 0, BACKLOGMAX)) == -1) {
			uprint("-!- usage: /set backlog 0-%d", BACKLOGMAX);
			return;
		}
		backlog = t;
		save_config();
		uprint("-!- backlog = %d%s", backlog, backlog ? "" : " (off)");
		return;
	}
	if (strcasecmp(key, "theme") == 0 && *val == 0) {
		uprint("-!- theme = %s  (%s)", themes[theme].name, names);
		return;
	}
	if (strcasecmp(key, "theme") != 0) {
		uprint("-!- Unknown setting %s; settings: theme, backlog", key);
		return;
	}
	if ((t = theme_find(val)) == -1) {
		uprint("-!- No theme %s; themes: %s", val, names);
		return;
	}
	theme = t;
	save_config();
	if (curses_on)
		clearok(curscr, TRUE);
	dirty = 1;
	uprint("-!- theme = %s", themes[theme].name);
}

static void
do_input(char *s)
{
	struct buffer	*b = bufs[cur];
	struct network	*n = curnet();
	char		*cmd, *arg, *p, *w;
	int		 i;

	if (*s == 0)
		return;
	if (s[0] != '/' || s[1] == '/') {	/* "//text" sends "/text" */
		if (s[0] == '/')
			s++;
		if (b->kind < B_CHAN) {
			uprint("-!- No channel or query here, use /join or /query");
			return;
		}
		send_privmsg(b->net, b->name, s, 0);
		return;
	}

	cmd = s + 1;
	if ((arg = strchr(cmd, ' ')) != NULL) {
		*arg++ = 0;
		while (*arg == ' ')
			arg++;
	} else
		arg = cmd + strlen(cmd);

	if (cmd_chanop(b, n, cmd, arg))
		return;
	if (strcasecmp(cmd, "join") == 0 || strcasecmp(cmd, "j") == 0) {
		if (*arg == 0 || n == NULL)
			return;
		p = arg;
		w = word(&p);
		if (!ischan(w)) {
			uprint("-!- usage: /join #channel [key]");
			return;
		}
		snprintf(n->switchto, sizeof(n->switchto), "%s", w);
		if (*p)
			irc_send(n, "JOIN %s %s", w, p);
		else
			irc_send(n, "JOIN %s", w);
	} else if (strcasecmp(cmd, "part") == 0) {
		if (b->kind == B_CHAN && *arg)
			irc_send(b->net, "PART %s :%s", b->name, arg);
		else if (b->kind == B_CHAN)
			irc_send(b->net, "PART %s", b->name);
	} else if (strcasecmp(cmd, "msg") == 0) {
		if ((p = strchr(arg, ' ')) == NULL) {
			uprint("-!- usage: /msg nick text");
			return;
		}
		*p++ = 0;
		send_privmsg(n, arg, p, 0);
	} else if (strcasecmp(cmd, "query") == 0 || strcasecmp(cmd, "q") == 0) {
		/* /query nick [text]: open the window, and send text if given */
		w = word(&arg);
		if (*w == 0 || n == NULL) {
			uprint("-!- usage: /query nick [text]");
			return;
		}
		buf_switch(buf_index(buf_get(n, w)));
		if (*arg)
			send_privmsg(n, w, arg, 0);
	} else if (strcasecmp(cmd, "me") == 0) {
		if (b->kind >= B_CHAN && *arg)
			send_privmsg(b->net, b->name, arg, 1);
	} else if (strcasecmp(cmd, "nick") == 0) {
		if (*arg && n != NULL) {
			/* keepnick follows your choice: chase only the config nick */
			n->keepnick_off = strcasecmp(arg, net_nick(n)) != 0;
			if (n->keepnick_off)
				keepnick_stop(n);
			if (n->state >= ST_REGISTER)
				irc_send(n, "NICK %s", arg);
			else
				uprint("-!- Not connected; to change the saved "
				    "nick use /server set %s nick %s", n->name, arg);
		}
	} else if (strcasecmp(cmd, "topic") == 0) {
		if (b->kind != B_CHAN)
			return;
		if (*arg)
			irc_send(b->net, "TOPIC %s :%s", b->name, arg);
		else
			irc_send(b->net, "TOPIC %s", b->name);
	} else if (strcasecmp(cmd, "names") == 0) {
		if (*arg == 0 && b->kind == B_CHAN && b->joined && b->nmem)
			cmd_names(b);
		else if (*arg || b->kind == B_CHAN)
			irc_send(n, "NAMES %s", *arg ? arg : b->name);
	} else if (strcasecmp(cmd, "nicklist") == 0) {
		show_nicklist = !show_nicklist;
		dirty = 1;
	} else if (strcasecmp(cmd, "serverlist") == 0 ||
	    strcasecmp(cmd, "winlist") == 0) {
		show_winlist = !show_winlist;
		dirty = 1;
	} else if (strcasecmp(cmd, "windows") == 0) {
		char lbl[128];

		for (i = 0; i < nbufs; i++) {
			buf_label(bufs[i], lbl, sizeof(lbl));
			uprint("-!- %2d: %s%s", i + 1, lbl,
			    bufs[i]->kind == B_CHAN && !bufs[i]->joined ?
			    " (not joined)" : "");
		}
	} else if (strcasecmp(cmd, "win") == 0 || strcasecmp(cmd, "w") == 0) {
		buf_switch(atoi(arg) - 1);
	} else if (strcasecmp(cmd, "close") == 0 || strcasecmp(cmd, "wc") == 0) {
		if (b->kind < B_CHAN) {
			uprint("-!- Status windows can't be closed");
			return;
		}
		if (b->joined && b->net->state == ST_ONLINE)
			irc_send(b->net, "PART %s", b->name);
		buf_close(cur);
	} else if (strcasecmp(cmd, "clear") == 0) {
		buf_clear(b);
	} else if (strcasecmp(cmd, "server") == 0) {
		cmd_server(arg);
	} else if (strcasecmp(cmd, "autojoin") == 0 || strcasecmp(cmd, "aj") == 0) {
		cmd_autojoin(arg);
	} else if (strcasecmp(cmd, "connect") == 0) {
		if (*arg && net_find(arg) == NULL) {
			uprint("-!- No network %s, add it with /server add", arg);
			return;
		}
		if ((n = *arg ? net_find(arg) : curnet()) == NULL) {
			uprint("-!- No networks, add one with /server add");
			return;
		}
		if (n->state != ST_OFFLINE) {
			uprint("-!- %s is already %s", n->name, state_name(n->state));
			return;
		}
		n->wantconn = 1;
		n->backoff = BACKOFFMIN;
		buf_switch(buf_index(n->status));
		net_connect(n);
	} else if (strcasecmp(cmd, "disconnect") == 0) {
		char whole[512];

		/* /disconnect [network] [message] */
		snprintf(whole, sizeof(whole), "%s", arg);
		w = word(&arg);
		if (*w && net_find(w) != NULL)
			n = net_find(w);
		else
			arg = whole;
		if (n != NULL)
			net_disconnect(n, arg);
	} else if (strcasecmp(cmd, "reconnect") == 0) {
		if ((n = *arg ? net_find(arg) : curnet()) == NULL) {
			uprint("-!- No such network");
			return;
		}
		net_disconnect(n, NULL);
		n->wantconn = 1;
		net_connect(n);
	} else if (strcasecmp(cmd, "save") == 0) {
		save_config();
	} else if (strcasecmp(cmd, "quit") == 0) {
		for (i = 0; i < nnets; i++)
			net_disconnect(nets[i], *arg ? arg : NULL);
		running = 0;
	} else if (strcasecmp(cmd, "raw") == 0 || strcasecmp(cmd, "quote") == 0) {
		if (*arg)
			irc_send(n, "%s", arg);
	} else if (strcasecmp(cmd, "ignore") == 0) {
		/*
		 * /ignore lists. /ignore nick ignores the nick and its host
		 * (*!*@host), so changing nick does not help. /ignore with a
		 * nick!user@host mask ignores just that mask.
		 */
		const char *uh;
		struct ignore *ig;

		w = word(&arg);
		if (*w == 0) {
			if (nignore == 0)
				uprint("-!- Ignore list is empty");
			for (i = 0; i < nignore; i++)
				if (ignores[i].hide)
					uprint("-!- Ignoring %d: %s in %s", i + 1,
					    hide_words(ignores[i].hide),
					    ignores[i].mask);
				else
					uprint("-!- Ignoring %d: %s%s%s%s", i + 1,
					    ignores[i].mask,
					    ignores[i].host[0] ? " (" : "",
					    ignores[i].host,
					    ignores[i].host[0] ? ")" : "");
			return;
		}
		if (ischan(w)) {
			/* /ignore #channel [joins] [parts] [quits] [nicks] */
			int bits = parse_hide(arg);

			if (bits == -1 || strlen(w) >= sizeof(ignores[0].mask)) {
				uprint("-!- usage: /ignore #channel [joins] "
				    "[parts] [quits] [nicks]  (none: all four)");
				return;
			}
			for (i = 0; i < nignore; i++)
				if (strcasecmp(ignores[i].mask, w) == 0)
					break;
			if (i == nignore) {
				if (nignore == MAXIGNORE) {
					uprint("-!- Ignore list is full");
					return;
				}
				memset(&ignores[nignore], 0, sizeof(ignores[0]));
				snprintf(ignores[nignore].mask,
				    sizeof(ignores[0].mask), "%s", w);
				nignore++;
			}
			ignores[i].hide = bits;
			uprint("-!- Hiding %s in %s (/unignore %s to show "
			    "them again)", hide_words(bits), w, w);
			save_config();
			return;
		}
		if (strlen(w) >= sizeof(ignores[0].mask)) {
			uprint("-!- Mask too long");
			return;
		}
		for (i = 0; i < nignore; i++)
			if (strcasecmp(ignores[i].mask, w) == 0) {
				uprint("-!- Already ignoring %s", w);
				return;
			}
		if (nignore == MAXIGNORE) {
			uprint("-!- Ignore list is full");
			return;
		}
		ig = &ignores[nignore++];
		memset(ig, 0, sizeof(*ig));
		snprintf(ig->mask, sizeof(ig->mask), "%s", w);
		if (strpbrk(w, "!@*?") != NULL) {
			uprint("-!- Ignoring %s", w);
		} else if (n != NULL && (uh = find_uhost(n, w)) != NULL) {
			host_mask(uh, ig->host, sizeof(ig->host));
			uprint("-!- Ignoring %s and %s", w, ig->host);
		} else if (n != NULL && n->state == ST_ONLINE) {
			/* host unknown: ask the server, 302 fills it in */
			uprint("-!- Ignoring %s, looking up the host...", w);
			irc_send(n, "USERHOST %s", w);
		} else
			uprint("-!- Ignoring %s (host unknown, nick only)", w);
		save_config();
	} else if (strcasecmp(cmd, "unignore") == 0) {
		/* /unignore nick, mask or N (number from /ignore) */
		w = word(&arg);
		for (i = 0; i < nignore; i++)
			if (strcasecmp(ignores[i].mask, w) == 0 ||
			    strcasecmp(ignores[i].host, w) == 0)
				break;
		if (i == nignore && atoi(w) >= 1 && atoi(w) <= nignore)
			i = atoi(w) - 1;
		if (*w == 0 || i == nignore) {
			uprint("-!- usage: /unignore nick|MASK|N (see /ignore)");
			return;
		}
		uprint("-!- No longer ignoring %s%s%s", ignores[i].mask,
		    ignores[i].host[0] ? " and " : "", ignores[i].host);
		memmove(&ignores[i], &ignores[i + 1],
		    (nignore - i - 1) * sizeof(ignores[0]));
		nignore--;
		save_config();
	} else if (strcasecmp(cmd, "autoaway") == 0) {
		/* /autoaway shows the state, /autoaway N sets minutes (0 = off) */
		char tb[32], *ep;
		double v;
		time_t when;

		if (*arg) {
			v = strtod(arg, &ep);
			if (*ep || v < 0) {
				uprint("-!- usage: /autoaway [minutes, 0 = off]");
				return;
			}
			autoaway = v;
			save_config();
		}
		uprint("-!- autoaway: %g min%s, message: %s", autoaway,
		    autoaway > 0 ? "" : " (off)", awaymsg);
		if (tmux_sock[0] == 0)
			uprint("-!- tmux: not running inside tmux ($TMUX is not "
			    "set), auto-away is inactive");
		else {
			uprint("-!- tmux: socket %s", tmux_sock);
			if (tmux_unveil_failed)
				uprint("-!- tmux: unveil failed: %s",
				    strerror(tmux_unveil_failed));
			if (tmux_err)
				uprint("-!- tmux: stat failed: %s",
				    strerror(tmux_err));
			uprint("-!- tmux: client %s", tmux_attached == 1 ?
			    "attached" : tmux_attached == 0 ? "detached" :
			    "not checked yet");
		}
		if (detached_at && autoaway > 0 && !auto_away) {
			when = detached_at + (time_t)(autoaway * 60);
			fmt_time(tb, sizeof(tb), "%H:%M:%S", when);
			uprint("-!- away will be set at %s", tb);
		}
		uprint("-!- status: %s", manual_away ? "away (manual)" :
		    auto_away ? "away (auto)" : "not away");
	} else if (strcasecmp(cmd, "away") == 0) {
		/* /away text sets away on all networks, /away without text removes it */
		if (*arg) {
			manual_away = 1;
			auto_away = 0;
			set_away(arg);
		} else {
			manual_away = auto_away = 0;
			set_away(NULL);
		}
	} else if (strcasecmp(cmd, "whois") == 0 || strcasecmp(cmd, "wi") == 0) {
		/* no nick: whois yourself */
		if (n == NULL)
			return;
		irc_send(n, "WHOIS %s", *arg ? arg : n->mynick);
	} else if (strcasecmp(cmd, "ctcp") == 0) {
		/* /ctcp nick COMMAND [arguments] */
		char *target = word(&arg), *what = word(&arg);

		if (*target == 0 || *what == 0) {
			uprint("-!- usage: /ctcp nick version|ping|time [args]");
			return;
		}
		for (p = what; *p; p++)
			*p = toupper((unsigned char)*p);
		if (strcmp(what, "PING") == 0)	/* the time in ms, the reply gives the lag */
			irc_send(n, "PRIVMSG %s :\001PING %lld\001", target,
			    now_ms());
		else if (*arg)
			irc_send(n, "PRIVMSG %s :\001%s %s\001", target, what, arg);
		else
			irc_send(n, "PRIVMSG %s :\001%s\001", target, what);
		eprint(bufs[cur], LVL_NONE, S_EVPFX, "-!- ", S_EVENT, "CTCP ",
		    S_CHAN, what, S_EVENT, " sent to ", S_JOIN, target, -1);
	} else if (strcasecmp(cmd, "set") == 0) {
		cmd_set(arg);
	} else if (strcasecmp(cmd, "url") == 0) {
		cmd_url(arg);
	} else if (strcasecmp(cmd, "bare") == 0) {
		bare_show(b, NULL);
	} else if (strcasecmp(cmd, "pushover") == 0) {
		cmd_pushover(arg);
	} else if (strcasecmp(cmd, "help") == 0) {
		cmd_help();
	} else {
		for (p = cmd; *p; p++)
			*p = toupper((unsigned char)*p);
		if (*arg)
			irc_send(n, "%s %s", cmd, arg);
		else
			irc_send(n, "%s", cmd);
	}
}

/* ---------- user interface ---------- */

static int
cw(wchar_t c)
{
	int	w = wcwidth(c);

	return w < 0 ? 1 : w;
}

static int
wwidth(const wchar_t *s, int n)
{
	int	i, w = 0;

	for (i = 0; i < n && s[i]; i++)
		w += cw(s[i]);
	return w;
}

/*
 * Wrap a line to width w. Lines break at spaces when possible, and
 * continuation lines are indented to l->indent (where the message
 * starts) so the text gets its own column. starts[] gets the start
 * index of each screen row; the number of rows is returned.
 */
static int	wstarts[LBMAX + 1];

static int
line_wrap(struct line *l, int w, int *indw)
{
	int	rows = 1, col = 0, rs = 0, lastsp = -1, i, c, brk, j;

	*indw = wwidth(l->w, l->indent);
	if (*indw > w / 2)
		*indw = 0;
	wstarts[0] = 0;
	for (i = 0; i < l->n; i++) {
		c = cw(l->w[i]);
		if (col + c > w && i > rs) {
			brk = lastsp > rs ? lastsp + 1 : i;
			while (brk < l->n && l->w[brk] == ' ')
				brk++;		/* no spaces at the start of a row */
			if (rows <= LBMAX)
				wstarts[rows] = brk;
			rows++;
			rs = brk;
			lastsp = -1;
			col = *indw;
			if (brk > i) {
				i = brk - 1;
				continue;
			}
			for (j = brk; j < i; j++)
				col += cw(l->w[j]);
		}
		if (l->w[i] == ' ' && i >= l->indent)
			lastsp = i;
		col += c;
	}
	return rows;
}

/* Draw line l with top screen row top; only rows in [minrow, maxrow]. */
static void
draw_line(int top, struct line *l, int x0, int w, int minrow, int maxrow)
{
	int	rows, indw, r, i, end, col, c, row;

	rows = line_wrap(l, w, &indw);
	for (r = 0; r < rows && r <= LBMAX; r++) {
		row = top + r;
		if (row < minrow || row > maxrow)
			continue;
		end = r + 1 < rows && r + 1 <= LBMAX ? wstarts[r + 1] : l->n;
		col = r ? indw : 0;
		move(row, x0 + col);
		for (i = wstarts[r]; i < end; i++) {
			c = cw(l->w[i]);
			if (col + c > w)
				break;
			attrset(style_attr(l->a[i]));
			addnwstr(&l->w[i], 1);
			col += c;
		}
	}
	attrset(A_NORMAL);
}

static int
put_fit(int y, int x, const wchar_t *s, int n, int maxw)
{
	int	i, used = 0, c;

	move(y, x);
	for (i = 0; i < n && s[i]; i++) {
		c = cw(s[i]);
		if (used + c > maxw)
			break;
		addnwstr(&s[i], 1);
		used += c;
	}
	return used;
}

/*
 * The window list: one group per network with its channels and queries
 * indented below. The current window is reversed, activity shows in colour.
 */
static void
draw_winlist(int lw, int y0, int rows)
{
	struct buffer	*wb;
	uint16_t	 st;
	int		 i, off = 0, y;
	char		 num[16];

	mvvline(y0, lw - 1, ACS_VLINE | style_attr(S_SEP), rows);

	if (nbufs > rows) {		/* scroll so the current window is visible */
		off = cur - rows / 2;
		if (off > nbufs - rows)
			off = nbufs - rows;
		if (off < 0)
			off = 0;
	}
	for (i = off, y = 0; i < nbufs && y < rows; i++, y++) {
		wb = bufs[i];
		/*
		 * Networks: blue when connected, grey when not. Channels and
		 * queries: grey while nothing new has happened, white when
		 * someone has written, red when you were mentioned or got a
		 * private message. Joins and parts do not count.
		 */
		if (wb->kind == B_STATUS)
			st = wb->net && wb->net->state == ST_ONLINE ?
			    S_WINNET : S_DIM;
		else if (wb->kind == B_MAIN)
			st = S_DEFAULT;
		else
			st = S_DIM;
		if (wb->hilite)
			st = S_WINHL;
		else if (wb->unread && wb->kind >= B_CHAN)
			st = S_WINNEW;
		if (i == cur)
			st |= F_REV;

		/* " 3  #channel": the window number (Alt/Esc + digit) first */
		lb_clear();
		snprintf(num, sizeof(num), "%2d ", i + 1);
		lb_text(i == cur ? (st & ~0x3ff) | (S_DIM & 0x3ff) : S_DIM, num, 0);
		if (wb->kind >= B_CHAN)
			lb_text(i == cur ? st : S_DEFAULT, " ", 0);
		lb_text(st, wb->kind == B_MAIN ? "bsdirc" : wb->name, 0);
		if (i == cur)		/* extend the highlight to the edge */
			while (lb_n < lw - 1)
				lb_put(' ', st);
		lb_draw(y0 + y, 0, lw - 1);
	}
}

/* The read marker: ───── 12 new ───── */
static void
draw_marker(int y, int x0, int w, long count)
{
	char	txt[32];
	int	len;

	mvhline(y, x0, ACS_HLINE | style_attr(S_MARK), w);
	len = snprintf(txt, sizeof(txt), " %ld new ", count);
	if (len < w) {
		lb_clear();
		lb_text(S_MARKTXT, txt, 0);
		lb_draw(y, x0 + (w - len) / 2, len);
	}
}

/* The top line: the channel's topic, or what the window is. */
static void
draw_topic(struct buffer *b, int w)
{
	struct network	*n = b->net;
	char		 tmp[600];

	lb_clear();
	lb_put(' ', S_BAR);
	switch (b->kind) {
	case B_MAIN:
		lb_text(S_BAR, "bsdirc " VERSION " - /help for commands", 0);
		break;
	case B_STATUS:
		if (n->state == ST_ONLINE || n->state == ST_REGISTER)
			snprintf(tmp, sizeof(tmp), "%s - %s [%s] port %s%s - "
			    "from %s", n->name, n->host, n->raddr, n->port,
			    n->tls ? " (TLS)" : "", n->laddr);
		else
			snprintf(tmp, sizeof(tmp), "%s - %s port %s%s%s%s - %s",
			    n->name, n->host[0] ? n->host : "(no host)", n->port,
			    n->tls ? " (TLS)" : "",
			    *(n->bind[0] ? n->bind : g_bind) ? " - bind " : "",
			    n->bind[0] ? n->bind : g_bind, state_name(n->state));
		lb_text(S_BAR, tmp, 0);
		break;
	case B_QUERY:
		snprintf(tmp, sizeof(tmp), "Query with %s on %s", b->name,
		    n->name);
		lb_text(S_BAR, tmp, 0);
		break;
	default:
		lb_text(S_BAR, b->topic[0] ? b->topic :
		    (b->joined ? "(no topic)" : "(not joined)"), 0);
		break;
	}
	mvhline(0, 0, ' ' | style_attr(S_BAR), w);
	lb_draw(0, 0, w);
}

static void
draw(void)
{
	struct buffer	*b = bufs[cur];
	struct network	*n = b->net;
	int		 h = LINES, w = COLS, y0, ch, y, i, r, top, indw;
	int		 pw, avail, start, x, nlw, tw, lw;
	long		 first;
	char		 sb[128], tb[8], lbl[128], pc[2];
	time_t		 t = time(NULL);

	attrset(A_NORMAL);
	erase();
	if (h < 3 || w < 10) {
		refresh();
		return;
	}

	/* topic bar at the top if there is room, then the chat area */
	y0 = h >= 6 ? 1 : 0;
	ch = h - 2 - y0;
	if (y0)
		draw_topic(b, w);

	/*
	 * The window list on the left and the nick list on the right (in
	 * channels), as long as the chat area stays at least 40 columns wide.
	 */
	nlw = show_nicklist && b->kind == B_CHAN && w - NICKLISTW >= 40 ?
	    NICKLISTW : 0;
	lw = show_winlist && w - WINLISTW - nlw >= 40 ? WINLISTW : 0;
	tw = w - lw - nlw;

	/* the chat area, from the bottom up */
	if (b->scroll > b->nlines - 1)
		b->scroll = b->nlines > 0 ? b->nlines - 1 : 0;
	y = y0 + ch - 1;
	first = b->total - b->nlines;		/* line number of line 0 */
	for (i = b->nlines - 1 - b->scroll; i >= 0 && y >= y0; i--) {
		struct line *l = buf_line(b, i);

		/* the read marker: between the last read and the first unread line */
		if (b->mark > first && first + i == b->mark - 1 &&
		    b->mark < b->total) {
			draw_marker(y, lw, tw, b->marknew);
			if (--y < y0)
				break;
		}
		r = line_wrap(l, tw, &indw);
		top = y - r + 1;
		draw_line(top, l, lw, tw, y0, y0 + ch - 1);
		y = top - 1;
	}

	if (lw)
		draw_winlist(lw, y0, ch);

	if (nlw) {
		mvvline(y0, lw + tw, ACS_VLINE | style_attr(S_SEP), ch);
		mem_sort(b);
		for (i = 0; i < b->nmem && i < ch; i++) {
			lb_clear();
			if (i + 1 == ch && b->nmem > ch) {	/* does not fit */
				snprintf(sb, sizeof(sb), " +%d more", b->nmem - i);
				lb_text(S_DIM, sb, 0);
			} else {
				pc[0] = prefix_char(n, b->mem[i].modes);
				pc[1] = 0;
				lb_text(S_NICK, pc, 0);
				lb_text(S_NICK, b->mem[i].nick, 0);
			}
			lb_draw(y0 + i, lw + tw + 1, nlw - 1);
		}
	}

	/* the status bar: [12:34] [nick] [3:libera/#runbsd] [Act: 2,4] */
	fmt_time(tb, sizeof(tb), "%H:%M", t);
	buf_label(b, lbl, sizeof(lbl));
	lb_clear();
	lb_text(S_BARDIM, "[", 0);
	lb_text(S_BAR, tb, 0);
	lb_text(S_BARDIM, "] [", 0);
	lb_text(S_BARMSG, n ? (n->state >= ST_REGISTER ? n->mynick :
	    net_nick(n)) : g_nick, 0);
	if (n != NULL && chasing(n)) {		/* keepnick at work */
		lb_text(S_BARDIM, " -> ", 0);
		lb_text(S_BAR, net_nick(n), 0);
	}
	lb_text(S_BARDIM, "] [", 0);
	snprintf(sb, sizeof(sb), "%d:", cur + 1);
	lb_text(S_BAR, sb, 0);
	lb_text(S_BAR, lbl, 0);
	if (b->kind == B_CHAN && b->joined && b->cmodes[0]) {
		if (strchr(b->cmodes, 'l') && b->climit[0])
			snprintf(sb, sizeof(sb), "(+%s %s)", b->cmodes,
			    b->climit);
		else
			snprintf(sb, sizeof(sb), "(+%s)", b->cmodes);
		lb_text(S_BARDIM, sb, 0);	/* [3:libera/#runbsd(+nt)] */
	}
	lb_text(S_BARDIM, "]", 0);
	if (n != NULL && n->state != ST_ONLINE) {
		lb_text(S_BARDIM, " [", 0);
		lb_text(S_BARBAD, state_name(n->state), 0);
		lb_text(S_BARDIM, "]", 0);
	}
	for (i = 0, x = 0; i < nnets; i++)	/* away on the current or any network */
		if ((n == NULL || nets[i] == n) && nets[i]->away)
			x = 1;
	if (x) {
		lb_text(S_BARDIM, " [", 0);
		lb_text(S_BARBAD, "away", 0);
		lb_text(S_BARDIM, "]", 0);
	}
	if (n != NULL && n->sqlen > 0) {	/* lines waiting in the send queue */
		snprintf(sb, sizeof(sb), "%d", n->sqlen);
		lb_text(S_BARDIM, " [Queue: ", 0);
		lb_text(S_BARMSG, sb, 0);
		lb_text(S_BARDIM, "]", 0);
	}
	for (i = 0, x = 0; i < nbufs; i++) {
		if (!bufs[i]->unread || i == cur)
			continue;
		lb_text(S_BARDIM, x++ ? "," : " [Act: ", 0);
		snprintf(sb, sizeof(sb), "%d", i + 1);
		lb_text(bufs[i]->hilite ? S_BARHL : S_BARMSG, sb, 0);
	}
	if (x)
		lb_text(S_BARDIM, "]", 0);
	if (b->scroll)
		lb_text(S_BARMSG, " [-- more --]", 0);
	if (paste_ask) {
		snprintf(sb, sizeof(sb), " [Paste %d lines: ^K send, ^C cancel]",
		    paste_n);
		lb_text(S_BARHL, sb, 0);
	}
	mvhline(h - 2, 0, ' ' | style_attr(S_BAR), w);
	lb_draw(h - 2, 0, w);

	/* the input line, scrolls horizontally around the cursor */
	lb_clear();
	lb_text(S_PROMPT, "[", 0);
	lb_text(S_PROMPT, b->kind >= B_CHAN ? b->name : lbl, 0);
	lb_text(S_PROMPT, "] ", 0);
	pw = lb_draw(h - 1, 0, w / 2);
	avail = w - pw - 1;
	start = 0;
	while (start < incur && wwidth(in + start, incur - start) > avail)
		start++;
	put_fit(h - 1, pw, in + start, inlen - start, w - pw);
	move(h - 1, pw + wwidth(in + start, incur - start));
	refresh();
}


static void	do_input(char *);
static void	hist_add(const wchar_t *, int);

/* ---------- input history ---------- */

static wchar_t	*hist[HISTMAX];
static int	 nhist, histpos;		/* histpos == nhist: not in the history */
static wchar_t	 draft[INPUTMAX];	/* what you typed before you started browsing */
static int	 draftlen;

/* "/msg NickServ IDENTIFY x", "/pushover token x" ...: not in history */
static int
hist_secret(const wchar_t *w, int n)
{
	char	 line[INPUTMAX * 4 + 1], *p = line, *cmd, *target, tmp[64];

	utf8_encode(w, n, line, sizeof(line));
	if (line[0] != '/')		/* typed in a query window */
		return bufs[cur]->kind == B_QUERY &&
		    mask_secret(bufs[cur]->name, line, tmp, sizeof(tmp));
	cmd = word(&p);
	if (strcasecmp(cmd, "/pushover") == 0)
		return strncasecmp(p, "token", 5) == 0;
	if (strcasecmp(cmd, "/msg") != 0 && strcasecmp(cmd, "/query") != 0 &&
	    strcasecmp(cmd, "/q") != 0)
		return 0;
	target = word(&p);
	return mask_secret(target, p, tmp, sizeof(tmp));
}

static void
hist_add(const wchar_t *w, int n)
{
	static const wchar_t	pw[] = L"password";
	wchar_t			*h;
	int			 i;

	histpos = nhist;
	if (n == 0)
		return;
	/* never store lines that may contain a password */
	for (i = 0; i + 8 <= n; i++)
		if (wmemcmp(w + i, pw, 8) == 0 && w[0] == '/')
			return;
	if (hist_secret(w, n))
		return;
	if (nhist > 0 && (int)wcslen(hist[nhist - 1]) == n &&
	    wmemcmp(hist[nhist - 1], w, n) == 0)
		return;				/* same as the previous one */
	if ((h = calloc(n + 1, sizeof(wchar_t))) == NULL)
		fatal("calloc");
	wmemcpy(h, w, n);
	if (nhist == HISTMAX) {
		free(hist[0]);
		memmove(hist, hist + 1, (HISTMAX - 1) * sizeof(hist[0]));
		nhist--;
	}
	hist[nhist++] = h;
	histpos = nhist;
}

/* Up (dir -1) or down (dir 1). */
static void
hist_move(int dir)
{
	int	np = histpos + dir;

	if (np < 0 || np > nhist)
		return;
	if (histpos == nhist) {			/* leaving the draft */
		wmemcpy(draft, in, inlen);
		draftlen = inlen;
	}
	histpos = np;
	if (histpos == nhist) {
		wmemcpy(in, draft, draftlen);
		inlen = draftlen;
	} else {
		inlen = wcslen(hist[histpos]);
		if (inlen > INPUTMAX - 1)
			inlen = INPUTMAX - 1;
		wmemcpy(in, hist[histpos], inlen);
	}
	incur = inlen;
}

static void
paste_run(void)
{
	int	i;

	for (i = 0; i < paste_n; i++) {
		do_input(paste[i]);
		zfree(paste[i]);
	}
	paste_n = paste_ask = 0;
	dirty = 1;
}

static void
paste_cancel(void)
{
	int	i;

	for (i = 0; i < paste_n; i++)
		zfree(paste[i]);
	uprint("-!- Paste of %d line(s) cancelled", paste_n);
	paste_n = paste_ask = 0;
}

/* The paste is complete: 1-2 lines are sent, more require confirmation. */
static void
paste_check(void)
{
	if (paste_n == 0 || paste_ask || now_ms() - paste_last < PASTEWAIT)
		return;
	if (paste_n < 3)
		paste_run();
	else {
		paste_ask = 1;
		uprint("-!- You are pasting %d lines. Ctrl-K sends them, "
		    "Ctrl-C cancels.", paste_n);
		if (curses_on)
			beep();
	}
}

static void
submit(void)
{
	char	out[INPUTMAX * 4 + 1];

	utf8_encode(in, inlen, out, sizeof(out));
	bufs[cur]->scroll = 0;
	if (paste_n > 0 || batch_more) {	/* part of a paste */
		if (paste_n < PASTEMAX && (paste[paste_n] = strdup(out)) != NULL)
			paste_n++;
		paste_last = now_ms();
	} else {
		hist_add(in, inlen);
		do_input(out);
	}
	inlen = incur = 0;
	explicit_bzero(out, sizeof(out));	/* may contain a password */
	dirty = 1;
}

static void
tab_add(const char *s, const char *prefix)
{
	int	i;

	if (ntab == (int)(sizeof(tabc) / sizeof(tabc[0])) ||
	    strncasecmp(s, prefix, strlen(prefix)) != 0)
		return;
	for (i = 0; i < ntab; i++)
		if (strcasecmp(tabc[i], s) == 0)
			return;
	snprintf(tabc[ntab++], sizeof(tabc[0]), "%s", s);
}

/* most recent speaker first, then alphabetical */
static int
spoke_cmp(const void *a, const void *b)
{
	const struct member *x = *(const struct member * const *)a;
	const struct member *y = *(const struct member * const *)b;

	if (x->spoke != y->spoke)
		return x->spoke > y->spoke ? -1 : 1;
	return strcasecmp(x->nick, y->nick);
}

/*
 * Tab: complete the word before the cursor. Nicks in channels (most
 * recent speaker first, "nick: " at the start of the line), #channels
 * and /commands. More presses cycle, Shift-Tab goes backwards.
 */
static void
tab_complete(int dir)
{
	struct buffer	 *b = bufs[cur];
	struct member	**v;
	char		  prefix[256], cand[80], *p;
	wchar_t		  wc[80];
	int		  ws, i, len;

	if (tabi < 0) {
		ws = incur;
		while (ws > 0 && in[ws - 1] != ' ')
			ws--;
		utf8_encode(in + ws, incur - ws, prefix, sizeof(prefix));
		ntab = 0;
		tabsuffix = " ";
		if (ws == 0 && prefix[0] == '/') {
			for (i = 0; i < (int)(sizeof(commands) / sizeof(commands[0])); i++) {
				snprintf(cand, sizeof(cand), "/%s", commands[i]);
				tab_add(cand, prefix);
			}
		} else if (ischan(prefix) && b->net != NULL) {
			for (i = 0; i < nbufs; i++)
				if (bufs[i]->net == b->net && bufs[i]->kind == B_CHAN)
					tab_add(bufs[i]->name, prefix);
			for (i = 0; i < b->net->nauto; i++) {
				snprintf(cand, sizeof(cand), "%s", b->net->autojoin[i]);
				if ((p = strchr(cand, ' ')) != NULL)
					*p = 0;
				tab_add(cand, prefix);
			}
		} else if (b->kind == B_CHAN && b->nmem > 0) {
			if ((v = reallocarray(NULL, b->nmem, sizeof(*v))) == NULL)
				fatal("reallocarray");
			for (i = 0; i < b->nmem; i++)
				v[i] = &b->mem[i];
			qsort(v, b->nmem, sizeof(*v), spoke_cmp);
			for (i = 0; i < b->nmem; i++)
				if (strcasecmp(v[i]->nick, b->net->mynick) != 0)
					tab_add(v[i]->nick, prefix);
			free(v);
			if (ws == 0)
				tabsuffix = ": ";
		} else if (b->kind == B_QUERY) {
			tab_add(b->name, prefix);
		}
		if (ntab == 0) {
			beep();
			return;
		}
		tabstart = ws;
		tabi = dir > 0 ? 0 : ntab - 1;
	} else
		tabi = (tabi + dir + ntab) % ntab;

	/* replace in[tabstart..incur) with the candidate */
	snprintf(cand, sizeof(cand), "%.63s%.2s", tabc[tabi], tabsuffix);
	len = utf8_decode(cand, wc, sizeof(wc) / sizeof(wc[0]));
	if (inlen - (incur - tabstart) + len >= INPUTMAX)
		return;
	wmemmove(in + tabstart + len, in + incur, inlen - incur);
	wmemcpy(in + tabstart, wc, len);
	inlen += len - (incur - tabstart);
	incur = tabstart + len;
}

static int	esc_pending;

/*
 * Alt key, or Esc followed by the key:
 *   1-9, 0      windows 1-10
 *   q w e r t y u i o   windows 11-19
 *   a  activity,  n  nick list,  s  window list
 * Returns 1 if the key was used.
 */
static int
meta_key(wint_t c)
{
	static const char	 row[] = "qwertyuio";
	const char		*p;
	int			 i;

	if (c >= '1' && c <= '9')
		buf_switch(c - '1');
	else if (c == '0')
		buf_switch(9);
	else if (c < 128 && c && (p = strchr(row, (int)c)) != NULL)
		buf_switch(10 + (p - row));
	else if (c == 'n')
		show_nicklist = !show_nicklist;
	else if (c == 's')
		show_winlist = !show_winlist;
	else if (c == 'l') {
		bare_show(bufs[cur], NULL);
		return 1;
	}
	else if (c == 'a') {			/* jump to activity */
		for (i = 0; i < nbufs; i++)
			if (bufs[i]->hilite) {
				buf_switch(i);
				return 1;
			}
		for (i = 0; i < nbufs; i++)
			if (bufs[i]->unread) {
				buf_switch(i);
				return 1;
			}
	} else
		return 0;
	dirty = 1;
	return 1;
}

static void
handle_key(int type, wint_t c)
{
	struct buffer	*b = bufs[cur];
	int		 step = (LINES - 2) / 2, i;

	if (step < 1)
		step = 1;
	dirty = 1;

	/* any other key ends an ongoing tab completion */
	if (type == KEY_CODE_YES && c == KEY_BTAB) {
		tab_complete(-1);
		return;
	}
	if (type != KEY_CODE_YES && c == '\t') {
		tab_complete(1);
		return;
	}
	tabi = -1;

	if (paste_ask && type != KEY_CODE_YES && (c == 11 || c == 3)) {
		if (c == 11)
			paste_run();
		else
			paste_cancel();
		return;
	}

	if (esc_pending) {
		esc_pending = 0;
		if (type != KEY_CODE_YES && meta_key(c))
			return;
	}

	if (type == KEY_CODE_YES) {
		switch (c) {
		case KEY_ENTER:
			submit();
			break;
		case KEY_BACKSPACE:
			goto backspace;
		case KEY_DC:
			if (incur < inlen) {
				wmemmove(in + incur, in + incur + 1, inlen - incur - 1);
				inlen--;
			}
			break;
		case KEY_UP:
			hist_move(-1);
			break;
		case KEY_DOWN:
			hist_move(1);
			break;
		case KEY_LEFT:
			if (incur > 0)
				incur--;
			break;
		case KEY_RIGHT:
			if (incur < inlen)
				incur++;
			break;
		case KEY_HOME:
			incur = 0;
			break;
		case KEY_END:
			incur = inlen;
			break;
		case KEY_PPAGE:
			b->scroll += step;
			break;
		case KEY_NPAGE:
			b->scroll = b->scroll > step ? b->scroll - step : 0;
			break;
		}
		return;
	}

	switch (c) {
	case '\r':
	case '\n':
		submit();
		break;
	case 127:
	case 8:
 backspace:
		if (incur > 0) {
			wmemmove(in + incur - 1, in + incur, inlen - incur);
			incur--;
			inlen--;
		}
		break;
	case 1:					/* ^A */
		incur = 0;
		break;
	case 5:					/* ^E */
		incur = inlen;
		break;
	case 11:				/* ^K */
		inlen = incur;
		break;
	case 21:				/* ^U */
		inlen = incur = 0;
		break;
	case 23:				/* ^W */
		i = incur;
		while (i > 0 && in[i - 1] == ' ')
			i--;
		while (i > 0 && in[i - 1] != ' ')
			i--;
		wmemmove(in + i, in + incur, inlen - incur);
		inlen -= incur - i;
		incur = i;
		break;
	case 12:				/* ^L */
		clearok(curscr, TRUE);
		break;
	case 14:				/* ^N */
		buf_switch((cur + 1) % nbufs);
		break;
	case 16:				/* ^P */
		buf_switch((cur + nbufs - 1) % nbufs);
		break;
	case 27:
		/*
		 * Esc works as a meta key, just like in irssi: Alt-1 and Esc
		 * followed by 1 (even with a pause in between) do the same.
		 */
		esc_pending = 1;
		break;
	default:
		if (!is_ctrl((unsigned long)c) && inlen < INPUTMAX - 1) {
			wmemmove(in + incur + 1, in + incur, inlen - incur);
			in[incur++] = (wchar_t)c;
			inlen++;
		}
		break;
	}
}

/* ---------- main ---------- */

static void
prompt_passwords(void)
{
	struct network	*n;
	char		 q[128];
	int		 i;

	for (i = 0; i < nnets; i++) {
		n = nets[i];
		if (!n->autoconnect || (!n->sasl && !n->qauth) || n->pass[0])
			continue;
#ifdef __OpenBSD__
		snprintf(q, sizeof(q), "%s password for %s: ",
		    n->qauth ? "Q" : "SASL", n->name);
		if (readpassphrase(q, n->pass, sizeof(n->pass),
		    RPP_REQUIRE_TTY) == NULL)
			err(1, "readpassphrase");
		n->pass_prompted = 1;
#else
		(void)q;
		warnx("%s: sasl without password; set it in %s",
		    n->name, confpath);
#endif
	}
}

/*
 * Pushover notifications (https://pushover.net/api). When someone
 * mentions you or writes to you in private, a POST goes to
 * api.pushover.net over TLS. It runs in the same poll loop as the IRC
 * connections, without fork, using the CA certificates loaded before
 * unveil, so pledge needs nothing new. One request at a time, the rest
 * wait in a small queue.
 */
#define PUSH_HOST	"api.pushover.net"
#define PUSH_PORT	"443"
#define PUSH_PATH	"/1/messages.json"
#define PUSH_QMAX	8
#define PUSH_COOLDOWN	60	/* seconds between notices per window */
#define PUSH_TIMEOUT	20	/* seconds for one request */
#define PUSH_CAP	10	/* at most this many notices ... */
#define PUSH_CAPWIN	600	/* ... per this many seconds */

enum { PU_OFF, PU_AWAY, PU_ALWAYS };
enum { PS_IDLE, PS_DNS, PS_CONNECT, PS_HANDSHAKE, PS_WRITE, PS_READ };

static struct {
	int		 state;
	char		*q[PUSH_QMAX];		/* request bodies */
	int		 qtest[PUSH_QMAX];	/* from /pushover test */
	int		 qn;
	int		 sock;
	short		 want;
	struct tls	*ctx;
	struct addrinfo	*ai0, *ai;
	char		*out;			/* the whole HTTP request */
	size_t		 olen, ooff;
	char		 in[256];
	size_t		 ilen;
	long long	 deadline;		/* give up at (ms) */
	int		 test;			/* report the result */
#ifdef HAVE_ASR
	struct asr_query *aq;
	int		 dns_fd;
	short		 dns_events;
	long long	 dns_deadline;
#endif
} pu = { .sock = -1 };

static struct {
	char	 key[128];
	time_t	 at;
} push_seen[32];

static const char *
push_mode_name(void)
{
	int	m = push_mode == -1 ? PU_AWAY : push_mode;

	return m == PU_OFF ? "off" : m == PU_AWAY ? "away" : "always";
}

/* Append key=value to buf, the value URL-encoded. */
static void
url_add(char *buf, size_t sz, const char *key, const char *s)
{
	static const char hex[] = "0123456789ABCDEF";
	size_t		  l = strlen(buf);
	unsigned char	  c;

	if (l && l + 1 < sz)
		buf[l++] = '&';
	while (*key && l + 1 < sz)
		buf[l++] = *key++;
	if (l + 1 < sz)
		buf[l++] = '=';
	for (; (c = *s) != 0 && l + 3 < sz; s++) {
		if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
			buf[l++] = c;
		else {
			buf[l++] = '%';
			buf[l++] = hex[c >> 4];
			buf[l++] = hex[c & 15];
		}
	}
	buf[l] = 0;
}

/* Copy text without IRC colour and formatting codes. */
static void
strip_codes(const char *s, char *out, size_t sz)
{
	size_t	l = 0;

	for (; *s && l + 1 < sz; s++) {
		if (*s == '\003') {		/* ^C fg[,bg] */
			if (isdigit((unsigned char)s[1])) {
				s++;
				if (isdigit((unsigned char)s[1]))
					s++;
				if (s[1] == ',' && isdigit((unsigned char)s[2])) {
					s += 2;
					if (isdigit((unsigned char)s[1]))
						s++;
				}
			}
			continue;
		}
		if (strchr("\002\017\026\035\036\037", *s) != NULL)
			continue;
		out[l++] = *s;
	}
	out[l] = 0;
}

static void
push_close(void)
{
#ifdef HAVE_ASR
	if (pu.aq != NULL) {
		asr_abort(pu.aq);
		pu.aq = NULL;
	}
#endif
	if (pu.ctx != NULL) {
		(void)tls_close(pu.ctx);	/* non-blocking: best effort */
		tls_free(pu.ctx);
		pu.ctx = NULL;
	}
	if (pu.sock != -1) {
		close(pu.sock);
		pu.sock = -1;
	}
	if (pu.ai0 != NULL) {
		freeaddrinfo(pu.ai0);
		pu.ai0 = pu.ai = NULL;
	}
	if (pu.out != NULL) {
		zfree(pu.out);		/* it holds the token */
		pu.out = NULL;
	}
	pu.state = PS_IDLE;
}

static void	push_start(void);

/* The request is over: report errors, start the next one. */
static void
push_done(const char *err)
{
	if (err != NULL)
		bprint(sysbuf(), LVL_HILITE, "-!- Pushover: %s", err);
	else if (pu.test)
		bprint(sysbuf(), LVL_NONE, "-!- Pushover: test notice sent");
	pu.test = 0;
	push_close();
	push_start();
}

/* Check the status line: "HTTP/1.1 200 OK" */
static void
push_reply(void)
{
	char	*e, msg[160];
	int	 code;

	pu.in[pu.ilen] = 0;
	pu.in[strcspn(pu.in, "\r\n")] = 0;
	if (strncmp(pu.in, "HTTP/", 5) != 0 || (e = strchr(pu.in, ' ')) == NULL) {
		push_done("bad reply from " PUSH_HOST);
		return;
	}
	code = atoi(e + 1);
	if (code == 200) {
		push_done(NULL);
		return;
	}
	snprintf(msg, sizeof(msg), "%s from " PUSH_HOST "%s", e + 1,
	    code == 400 ? " (check the token and user key)" : "");
	push_done(msg);
}

/* TLS handshake, write the request, read the status line. */
static void
push_io(void)
{
	ssize_t	r;

	if (pu.state == PS_HANDSHAKE) {
		r = tls_handshake(pu.ctx);
		if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) {
			pu.want = r == TLS_WANT_POLLIN ? POLLIN : POLLOUT;
			return;
		}
		if (r == -1) {
			push_done(tls_error(pu.ctx));
			return;
		}
		pu.state = PS_WRITE;
	}
	while (pu.state == PS_WRITE) {
		r = tls_write(pu.ctx, pu.out + pu.ooff, pu.olen - pu.ooff);
		if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) {
			pu.want = r == TLS_WANT_POLLIN ? POLLIN : POLLOUT;
			return;
		}
		if (r == -1) {
			push_done(tls_error(pu.ctx));
			return;
		}
		if ((pu.ooff += r) == pu.olen) {
			pu.state = PS_READ;
			pu.ilen = 0;
		}
	}
	while (pu.state == PS_READ) {
		r = tls_read(pu.ctx, pu.in + pu.ilen,
		    sizeof(pu.in) - 1 - pu.ilen);
		if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) {
			pu.want = r == TLS_WANT_POLLIN ? POLLIN : POLLOUT;
			return;
		}
		if (r == -1) {
			push_done(tls_error(pu.ctx));
			return;
		}
		pu.ilen += r;
		/* the status line is all we need */
		if (r == 0 || memchr(pu.in, '\n', pu.ilen) != NULL ||
		    pu.ilen == sizeof(pu.in) - 1) {
			push_reply();
			return;
		}
	}
}

static void
push_tcp_ok(void)
{
	freeaddrinfo(pu.ai0);
	pu.ai0 = pu.ai = NULL;
	if ((pu.ctx = tls_client()) == NULL) {
		push_done("tls_client failed");
		return;
	}
	if (tls_configure(pu.ctx, cfg_verify) == -1 ||
	    tls_connect_socket(pu.ctx, pu.sock, PUSH_HOST) == -1) {
		push_done(tls_error(pu.ctx));
		return;
	}
	pu.state = PS_HANDSHAKE;
	push_io();
}

/* Try the addresses in order with non-blocking connect. */
static void
push_try_addr(int e)
{
	struct addrinfo	*ai;
	int		 s;

	for (; (ai = pu.ai) != NULL; pu.ai = ai->ai_next) {
		if ((s = socket(ai->ai_family, ai->ai_socktype,
		    ai->ai_protocol)) == -1) {
			e = errno;
			continue;
		}
		if (fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK) == -1) {
			e = errno;
			close(s);
			continue;
		}
		pu.sock = s;
		if (connect(s, ai->ai_addr, ai->ai_addrlen) == 0) {
			push_tcp_ok();
			return;
		}
		if (errno == EINPROGRESS) {
			pu.state = PS_CONNECT;
			pu.want = POLLOUT;
			return;
		}
		e = errno;
		close(s);
		pu.sock = -1;
	}
	push_done(strerror(e ? e : ECONNREFUSED));
}

static void
push_connect_done(void)
{
	int		e = 0;
	socklen_t	len = sizeof(e);

	if (getsockopt(pu.sock, SOL_SOCKET, SO_ERROR, &e, &len) == -1)
		e = errno;
	if (e == 0) {
		push_tcp_ok();
		return;
	}
	close(pu.sock);
	pu.sock = -1;
	pu.ai = pu.ai->ai_next;
	push_try_addr(e);
}

#ifdef HAVE_ASR
static void
push_dns_step(void)
{
	struct asr_result	ar;

	if (asr_run(pu.aq, &ar) == 0) {
		pu.dns_fd = ar.ar_fd;
		pu.dns_events = ar.ar_cond == ASR_WANT_READ ? POLLIN : POLLOUT;
		pu.dns_deadline = now_ms() + ar.ar_timeout;
		return;
	}
	pu.aq = NULL;			/* asr has freed the query */
	if (ar.ar_gai_errno != 0) {
		push_done(gai_strerror(ar.ar_gai_errno));
		return;
	}
	pu.ai0 = pu.ai = ar.ar_addrinfo;
	push_try_addr(0);
}
#endif

/* Take the next body from the queue and start a request. */
static void
push_start(void)
{
	struct addrinfo	 hints;
	char		*body;
	size_t		 blen;
	int		 len;
#ifndef HAVE_ASR
	int		 e;
#endif

	if (pu.state != PS_IDLE || pu.qn == 0)
		return;
	body = pu.q[0];
	pu.test = pu.qtest[0];
	pu.qn--;
	memmove(pu.q, pu.q + 1, pu.qn * sizeof(pu.q[0]));
	memmove(pu.qtest, pu.qtest + 1, pu.qn * sizeof(pu.qtest[0]));
	blen = strlen(body);
	len = asprintf(&pu.out, "POST " PUSH_PATH " HTTP/1.1\r\n"
	    "Host: " PUSH_HOST "\r\n"
	    "User-Agent: bsdirc/" VERSION "\r\n"
	    "Content-Type: application/x-www-form-urlencoded\r\n"
	    "Content-Length: %zu\r\n"
	    "Connection: close\r\n\r\n%s", blen, body);
	zfree(body);			/* it holds the token */
	if (len == -1) {
		pu.out = NULL;
		pu.test = 0;
		return;
	}
	pu.olen = len;
	pu.ooff = 0;
	pu.deadline = now_ms() + PUSH_TIMEOUT * 1000LL;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_NUMERICSERV;
	pu.state = PS_DNS;
#ifdef HAVE_ASR
	if ((pu.aq = getaddrinfo_async(PUSH_HOST, PUSH_PORT, &hints,
	    NULL)) == NULL) {
		push_done(strerror(errno));
		return;
	}
	push_dns_step();
#else
	if ((e = getaddrinfo(PUSH_HOST, PUSH_PORT, &hints, &pu.ai0)) != 0) {
		push_done(gai_strerror(e));
		return;
	}
	pu.ai = pu.ai0;
	push_try_addr(0);
#endif
}

/* Queue a notice. test: from /pushover test, reports the result. */
static void
push_send(const char *title, const char *text, int test)
{
	char	body[2048], t[256], m[1024];

	if (!push_token[0] || !push_user[0]) {
		if (test)
			uprint("-!- Pushover: set the keys first with "
			    "/pushover token KEY and /pushover user KEY");
		return;
	}
	if (pu.qn == PUSH_QMAX) {
		if (test)
			uprint("-!- Pushover: queue full");
		return;
	}
	strip_codes(title, t, sizeof(t));
	strip_codes(text, m, sizeof(m));
	body[0] = 0;
	url_add(body, sizeof(body), "token", push_token);
	url_add(body, sizeof(body), "user", push_user);
	url_add(body, sizeof(body), "title", t);
	url_add(body, sizeof(body), "message", m);
	pu.q[pu.qn] = strdup(body);
	explicit_bzero(body, sizeof(body));
	if (pu.q[pu.qn] == NULL)
		return;
	pu.qtest[pu.qn++] = test;
	push_start();
}

/*
 * Someone mentioned you in chan, or wrote to you in private when chan
 * is NULL. Sent when the mode allows it, at most once a minute per
 * window, so that a busy conversation does not flood the phone.
 */
static void
push_notify(struct network *n, const char *chan, const char *nick,
    const char *text)
{
	char	 key[128], title[200], msg[1024];
	time_t	 now = time(NULL);
	int	 i, mode = push_mode == -1 ? PU_AWAY : push_mode, slot = 0;
	int	 nseen = (int)(sizeof(push_seen) / sizeof(push_seen[0]));

	static time_t	 win;
	static int	 sent;

	if (!push_token[0] || !push_user[0] || mode == PU_OFF ||
	    (mode == PU_AWAY && !manual_away && !auto_away))
		return;
	snprintf(key, sizeof(key), "%s/%s", n->name, chan ? chan : nick);
	for (i = 0; i < nseen; i++) {
		if (strcasecmp(push_seen[i].key, key) == 0) {
			if (now - push_seen[i].at < PUSH_COOLDOWN)
				return;
			slot = i;
			break;
		}
		if (push_seen[i].at < push_seen[slot].at)
			slot = i;		/* else reuse the oldest slot */
	}
	snprintf(push_seen[slot].key, sizeof(push_seen[slot].key), "%s", key);
	push_seen[slot].at = now;

	/* many nicks at once (a flood) must not empty your Pushover quota */
	if (now - win >= PUSH_CAPWIN) {
		win = now;
		sent = 0;
	}
	if (sent >= PUSH_CAP) {
		if (sent++ == PUSH_CAP)
			bprint(sysbuf(), LVL_HILITE, "-!- Pushover: more than "
			    "%d notices in %d minutes, pausing", PUSH_CAP,
			    PUSH_CAPWIN / 60);
		return;
	}
	sent++;

	if (chan != NULL) {
		snprintf(title, sizeof(title), "%s (%s)", chan, n->name);
		snprintf(msg, sizeof(msg), "<%s> %s", nick, text);
	} else {
		snprintf(title, sizeof(title), "Private from %s (%s)", nick,
		    n->name);
		snprintf(msg, sizeof(msg), "%s", text);
	}
	push_send(title, msg, 0);
}

/* The poll(2) entry for the request in progress; 0 if there is none. */
static int
push_pollfd(struct pollfd *p, int *tmo)
{
	long long	 d = pu.deadline, w;

	if (pu.state == PS_IDLE)
		return 0;
#ifdef HAVE_ASR
	if (pu.aq != NULL) {
		p->fd = pu.dns_fd;
		p->events = pu.dns_events;
		if (pu.dns_deadline < d)
			d = pu.dns_deadline;
	} else
#endif
	{
		p->fd = pu.sock;
		p->events = pu.want;
	}
	p->revents = 0;
	w = d - now_ms();
	if (w < *tmo)
		*tmo = w < 0 ? 0 : (int)w;
	return 1;
}

static void
push_event(short revents)
{
	if (pu.state == PS_IDLE)
		return;
	if (now_ms() >= pu.deadline) {
		push_done("timeout talking to " PUSH_HOST);
		return;
	}
#ifdef HAVE_ASR
	if (pu.aq != NULL) {
		if (revents || now_ms() >= pu.dns_deadline)
			push_dns_step();
		return;
	}
#endif
	if (revents == 0)
		return;
	if (pu.state == PS_CONNECT)
		push_connect_done();
	else
		push_io();
}

static void
push_status(void)
{
	uprint("-!- Pushover: %s, token %s, user key %s", push_mode_name(),
	    push_token[0] ? "set" : "not set",
	    push_user[0] ? "set" : "not set");
}

/* /pushover [away|always|off|test|token KEY|user KEY] */
static void
cmd_pushover(char *arg)
{
	char	*w = word(&arg), *k;
	int	 tok;

	if (*w == 0) {
		push_status();
		uprint("-!- away: only while you are away (/away, or tmux "
		    "detached long enough for auto-away)");
		return;
	}
	if (strcasecmp(w, "test") == 0) {
		push_send("bsdirc", "Test notice from bsdirc", 1);
		return;
	}
	if (strcasecmp(w, "token") == 0 || strcasecmp(w, "user") == 0) {
		tok = strcasecmp(w, "token") == 0;
		k = word(&arg);
		if (*k == 0 || strlen(k) >= sizeof(push_token)) {
			uprint("-!- usage: /pushover %s KEY", w);
			return;
		}
		snprintf(tok ? push_token : push_user, sizeof(push_token),
		    "%s", k);
		explicit_bzero(k, strlen(k));
	} else if (strcasecmp(w, "away") == 0)
		push_mode = PU_AWAY;
	else if (strcasecmp(w, "always") == 0)
		push_mode = PU_ALWAYS;
	else if (strcasecmp(w, "off") == 0)
		push_mode = PU_OFF;
	else {
		uprint("-!- usage: /pushover [away|always|off|test|"
		    "token KEY|user KEY]");
		return;
	}
	save_config();
	push_status();
}

static void __dead
usage(void)
{
	fprintf(stderr, "usage: bsdirc [-c config]\n");
	exit(1);
}

int
main(int argc, char *argv[])
{
	struct pollfd	 pfd[2 + MAXNETS];
	struct network	*map[2 + MAXNETS];
	const char	*home, *e;
	char		 tmp[1024];
	int		 ch, r, i, np, lastmin = -1, tmo, w, nk, pi;
	static int	 ktype[KEYBATCH];
	static wint_t	 kchar[KEYBATCH];
	wint_t		 wc;
	time_t		 t, lasttick = 0;
	struct rlimit	 rl = { 0, 0 };
	uint8_t		*ca;
	size_t		 calen;
	struct tm	*tm;

	/* a core dump would contain SASL passwords: no core dumps */
	if (setrlimit(RLIMIT_CORE, &rl) == -1)
		err(1, "setrlimit");

	/*
	 * Writing to a socket the server has closed raises SIGPIPE, which
	 * kills the process. We want EPIPE and a reconnect instead.
	 */
	signal(SIGPIPE, SIG_IGN);
	{
		struct sigaction sa;

		memset(&sa, 0, sizeof(sa));
		sa.sa_handler = sig_tmux;	/* no SA_RESTART: poll wakes up */
		sigemptyset(&sa.sa_mask);
		sigaction(SIGUSR1, &sa, NULL);
		sigaction(SIGUSR2, &sa, NULL);
	}

	setlocale(LC_ALL, "");
	tzset();
	{
		struct utsname u;	/* before pledge */

		if (uname(&u) == 0)
			snprintf(sysname, sizeof(sysname), "%s", u.sysname);
	}

	if ((home = getenv("HOME")) == NULL)
		errx(1, "HOME not set");
	snprintf(confpath, sizeof(confpath), "%s/.bsdirc/config", home);

	while ((ch = getopt(argc, argv, "c:")) != -1) {
		switch (ch) {
		case 'c':
			snprintf(confpath, sizeof(confpath), "%s", optarg);
			break;
		default:
			usage();
		}
	}
	if (argc != optind)
		usage();

	/* the config file's directory; create ~/.bsdirc if needed */
	snprintf(tmp, sizeof(tmp), "%s", confpath);
	snprintf(confdir, sizeof(confdir), "%s", dirname(tmp));
	if (mkdir(confdir, 0700) == -1 && errno != EEXIST)
		err(1, "mkdir %s", confdir);

	/* $TMUX = "/tmp/tmux-1000/default,1234,0": the socket before the first comma */
	if ((e = getenv("TMUX")) != NULL && *e == '/') {
		snprintf(tmux_sock, sizeof(tmux_sock), "%s", e);
		tmux_sock[strcspn(tmux_sock, ",")] = 0;
	}

	if ((e = getenv("USER")) == NULL)
		e = "guest";
	snprintf(g_nick, sizeof(g_nick), "%s", e);

	/*
	 * The user name sent with USER, unless "user" is set: the login
	 * name, as irssi does. Not the nick: servers such as QuakeNet
	 * refuse anything but lowercase letters and digits ("_empty").
	 */
	{
		struct passwd	*pw = getpwuid(getuid());
		const char	*ln = pw != NULL ? pw->pw_name : getenv("USER");
		size_t		 k = 0;

		for (; ln != NULL && *ln && k < 10; ln++)
			if (isalnum((unsigned char)*ln) && !((unsigned char)*ln & 0x80))
				g_login[k++] = tolower((unsigned char)*ln);
		if (k > 0)
			g_login[k] = 0;
	}

	/*
	 * Started from cron or a bare tmux there is often no LANG or
	 * LC_CTYPE, and then å, ä and ö break. IRC is UTF-8: pick a UTF-8
	 * locale ourselves (en_US.UTF-8 always exists on OpenBSD).
	 */
	if (MB_CUR_MAX == 1 &&
	    (setlocale(LC_CTYPE, "en_US.UTF-8") != NULL ||
	    setlocale(LC_CTYPE, "C.UTF-8") != NULL))
		locale_fixed = 1;

#ifdef __OpenBSD__
	if (pledge("stdio rpath wpath cpath inet dns tty unveil", NULL) == -1)
		err(1, "pledge");
#endif

	buf_new(NULL, "bsdirc", B_MAIN);	/* until there is a network */
	bprint(sysbuf(), LVL_NONE, "-!- bsdirc " VERSION " - /help for commands");
	if (locale_fixed)
		bprint(sysbuf(), LVL_NONE, "-!- No UTF-8 locale was set (LANG/"
		    "LC_CTYPE), using %s", setlocale(LC_CTYPE, NULL));
	else if (MB_CUR_MAX == 1)
		bprint(sysbuf(), LVL_HILITE, "-!- No UTF-8 locale: å, ä and ö "
		    "will not work. Set LC_CTYPE=en_US.UTF-8");
	load_config(confpath);
	config_ready = 1;
	bprint(sysbuf(), LVL_NONE, "-!- Config: %s (%d network(s))",
	    confpath, nnets);
	if (tmux_sock[0])
		bprint(sysbuf(), LVL_NONE, "-!- tmux: %s, auto-away after %g min "
		    "detached (/autoaway for status)", tmux_sock, autoaway);
	else
		bprint(sysbuf(), LVL_NONE, "-!- Not inside tmux, auto-away "
		    "inactive");
	if (nnets == 0) {
		bprint(sysbuf(), LVL_NONE, "-!- No networks yet. Example:");
		bprint(sysbuf(), LVL_NONE, "-!-   /server add libera "
		    "irc.libera.chat 6697 -sasl -auto");
		bprint(sysbuf(), LVL_NONE, "-!-   /server set libera password ...");
		bprint(sysbuf(), LVL_NONE, "-!-   /connect libera");
	}
	main_merge();		/* networks exist: no main window needed */

	/*
	 * Otherwise libtls reads the CA file only at the handshake, when
	 * unveil has already closed the file system. Load it into memory now.
	 */
	if ((cfg_verify = tls_config_new()) == NULL ||
	    (cfg_insecure = tls_config_new()) == NULL)
		errx(1, "tls_config_new");
	if ((ca = tls_load_file(tls_default_ca_cert_file(), &calen, NULL)) == NULL)
		errx(1, "%s: could not load CA certificates",
		    tls_default_ca_cert_file());
	if (tls_config_set_ca_mem(cfg_verify, ca, calen) == -1)
		errx(1, "tls_config_set_ca_mem: %s", tls_config_error(cfg_verify));
	tls_unload_file(ca, calen);
	tls_config_insecure_noverifycert(cfg_insecure);
	tls_config_insecure_noverifyname(cfg_insecure);

	prompt_passwords();

	term_upgrade();
	initscr();
	curses_on = 1;
	raw();
	noecho();
	nonl();
	keypad(stdscr, TRUE);
	nodelay(stdscr, TRUE);
	set_escdelay(25);
	if (getenv("NO_COLOR") != NULL)		/* https://no-color.org */
		use_colors = 0;
	if (use_colors && has_colors() && start_color() == OK && COLORS >= 8) {
		colors_ok = 1;
		if (use_default_colors() == ERR)
			nodefault = 1;
	}

	/*
	 * Terminfo, CA certificates, time zone and config are loaded. From
	 * here on we may only touch the config directory and resolve/connect
	 * to servers. rpath is still needed because rename(2) requires it,
	 * but unveil leaves only ~/.bsdirc and resolv.conf/hosts visible.
	 */
#ifdef __OpenBSD__
	if (unveil(confdir, "rwc") == -1 ||
	    unveil("/etc/resolv.conf", "r") == -1 ||
	    unveil("/etc/hosts", "r") == -1)
		fatal("unveil");
	/* the tmux socket: only stat() to see whether a client is attached */
	if (tmux_sock[0] && unveil(tmux_sock, "r") == -1)
		tmux_unveil_failed = errno;
	if (unveil(NULL, NULL) == -1)
		fatal("unveil");
	if (pledge("stdio rpath wpath cpath inet dns tty", NULL) == -1)
		fatal("pledge");
#endif

	for (i = 0; i < nnets; i++)
		if (nets[i]->autoconnect) {
			nets[i]->wantconn = 1;
			net_connect(nets[i]);
		}

	while (running) {
		if (dirty && !bare) {
			draw();
			dirty = 0;
		}
		pfd[0].fd = STDIN_FILENO;
		pfd[0].events = POLLIN;
		pfd[0].revents = 0;
		np = 1;
		for (i = 0; i < nnets; i++) {
#ifdef HAVE_ASR
			if (nets[i]->aq != NULL) {	/* waiting for DNS */
				pfd[np].fd = nets[i]->dns_fd;
				pfd[np].events = nets[i]->dns_events;
				pfd[np].revents = 0;
				map[np++] = nets[i];
				continue;
			}
#endif
			if (nets[i]->sock == -1)
				continue;
			pfd[np].fd = nets[i]->sock;
			pfd[np].events = nets[i]->want |
			    (nets[i]->olen > 0 ? nets[i]->owant : 0);
			pfd[np].revents = 0;
			map[np++] = nets[i];
		}

		/* wake up in time for the send queue and the paste window */
		tmo = 1000;
		for (i = 0; i < nnets; i++)
			if ((w = sq_wait(nets[i])) >= 0 && w < tmo)
				tmo = w;
#ifdef HAVE_ASR
		for (i = 0; i < nnets; i++)
			if (nets[i]->aq != NULL) {
				w = (int)(nets[i]->dns_deadline - now_ms());
				if (w < tmo)
					tmo = w < 0 ? 0 : w;
			}
#endif
		if (paste_n > 0 && !paste_ask) {
			w = PASTEWAIT - (int)(now_ms() - paste_last);
			if (w < tmo)
				tmo = w < 0 ? 0 : w;
		}

		pi = -1;			/* a Pushover request in progress */
		if (push_pollfd(&pfd[np], &tmo)) {
			map[np] = NULL;
			pi = np++;
		}

		r = poll(pfd, np, tmo);
		if (r == -1) {
			if (errno != EINTR)
				fatal("poll");
			for (i = 0; i < np; i++)
				pfd[i].revents = 0;
			pfd[0].revents = POLLIN;	/* SIGWINCH -> KEY_RESIZE */
		}

		if (pfd[0].revents & (POLLHUP | POLLERR))
			running = 0;
		if (bare && (pfd[0].revents & POLLIN)) {
			struct pollfd	kp = { STDIN_FILENO, POLLIN, 0 };

			/* a real key, not just SIGWINCH waking poll up */
			if (poll(&kp, 1, 0) == 1 && (kp.revents & POLLIN)) {
				char k;

				if (read(STDIN_FILENO, &k, 1) == 1)
					bare_exit();
			}
		} else if (pfd[0].revents & POLLIN) {
			/*
			 * Read everything that is waiting first. If more keys follow
			 * an Enter in the same round, it is a paste.
			 */
			nk = 0;
			while (nk < KEYBATCH && (r = get_wch(&wc)) != ERR) {
				ktype[nk] = r;
				kchar[nk++] = wc;
			}
			for (i = 0; i < nk; i++) {
				batch_more = i + 1 < nk;
				handle_key(ktype[i], kchar[i]);
			}
			batch_more = 0;
		}

		/* a network may have been removed by /server del while handling keys */
		if (pi != -1)
			push_event(pfd[pi].revents);
		for (i = 1; i < np; i++) {
			int j, alive = 0;

			if (i == pi)
				continue;
			for (j = 0; j < nnets; j++)
				if (nets[j] == map[i])
					alive = 1;
#ifdef HAVE_ASR
			if (alive && map[i]->aq != NULL) {
				if (pfd[i].revents || now_ms() >= map[i]->dns_deadline)
					dns_step(map[i]);
				continue;
			}
#endif
			if (alive && pfd[i].revents && map[i]->sock == pfd[i].fd)
				net_event(map[i]);
		}

		for (i = 0; i < nnets; i++)
			if (nets[i]->sqlen > 0)
				sq_flush(nets[i]);
		paste_check();
		away_check(time(NULL));

		t = time(NULL);
		if (t != lasttick) {
			lasttick = t;
			if (t - tmux_lastcheck >= 5) {
				tmux_lastcheck = t;
				tmux_check();
			}
			for (i = 0; i < nnets; i++)
				net_tick(nets[i], t);
			tm = localtime(&t);
			if (tm != NULL && tm->tm_min != lastmin) {	/* the clock in the status bar */
				lastmin = tm->tm_min;
				dirty = 1;
			}
		}
	}

	for (i = 0; i < nnets; i++)
		net_cleanup(nets[i]);
	for (i = 0; i < nbufs; i++)
		log_close(bufs[i]);
	endwin();
	return 0;
}

/*
 * Last in the file on purpose: <term.h> defines macros such as "lines"
 * that would clash with names used above.
 */
#include <term.h>

/*
 * Inside tmux every pane can show 256 colours, whatever the outer
 * terminal is. But a tmux started from cron or with an old config
 * says TERM=screen (8 colours), and then the theme looks wrong. In
 * that case use tmux-256color or screen-256color, if terminfo has it.
 */
static void
term_upgrade(void)
{
	static const char *const better[] = {
		"tmux-256color", "screen-256color",
	};
	const char	*t = getenv("TERM");
	size_t		 i;
	int		 err;

	if (getenv("TMUX") == NULL || t == NULL ||
	    (strcmp(t, "screen") != 0 && strcmp(t, "tmux") != 0))
		return;
	for (i = 0; i < sizeof(better) / sizeof(better[0]); i++) {
		if (setupterm(better[i], STDOUT_FILENO, &err) == OK) {
			del_curterm(cur_term);
			/* print first: setenv() may free the old value t */
			bprint(sysbuf(), LVL_NONE, "-!- tmux: TERM was %s, "
			    "using %s for 256 colours", t, better[i]);
			setenv("TERM", better[i], 1);
			return;
		}
	}
}
