summary refs log tree commit diff
diff options
context:
space:
mode:
authorCurtis McEnroe2018-08-11 19:30:30 -0400
committerCurtis McEnroe2018-08-11 20:02:03 -0400
commita281f89592cf5531ebf53863768fccd054de252c (patch)
tree502042617c3909ed5143d3e51562a7314666b250
parent461bfca37855b68ab7dab1a89d154bd483716840 (diff)
Rework UI code for multi-channel
Tags are now permanently assigned (and I'm betting on never needing more
than 256 of them) and the UI maps tags to a linked list of views for
easy reordering and removal. Currently, views can only be added. Views
don't have a topic window until they need one. All UI code wants to be
functional reactive.

Beeping is temporarily removed until message priorities (status,
message, ping) can be added to the UI. At that point spawning
notify-send should also be possible. Priorities will also help with
unnecessary markers, which will not appear for status messages.

The tab system is now used to send QUIT and NICK messages to all the
relevant tags. Verbose output now goes to its own tag, and sending to
it sends raw IRC.

IRC colors are now listed in chat.h and handler functions for numeric
replies have real names. The color algorithm now uses a real hash
function for hopefully better results. QUIT, PART and KICK messages are
scanned for URLs.
-rw-r--r--chat.c12
-rw-r--r--chat.h32
-rw-r--r--handle.c115
-rw-r--r--input.c24
-rw-r--r--irc.c10
-rw-r--r--tab.c55
-rw-r--r--tag.c45
-rw-r--r--ui.c351
8 files changed, 334 insertions, 310 deletions
diff --git a/chat.c b/chat.c
index ed33fbd..5542c82 100644
--- a/chat.c
+++ b/chat.c
@@ -60,7 +60,7 @@ static union {
 
 void spawn(char *const argv[]) {
 	if (fds.pipe.events) {
-		uiLog(TAG_DEFAULT, L"spawn: existing pipe");
+		uiLog(TAG_STATUS, L"spawn: existing pipe");
 		return;
 	}
 
@@ -93,7 +93,7 @@ static void pipeRead(void) {
 	if (len) {
 		buf[len] = '\0';
 		len = strcspn(buf, "\n");
-		uiFmt(TAG_DEFAULT, "%.*s", (int)len, buf);
+		uiFmt(TAG_STATUS, "spawn: %.*s", (int)len, buf);
 	} else {
 		close(fds.pipe.fd);
 		fds.pipe.events = 0;
@@ -124,15 +124,15 @@ static void sigchld(int sig) {
 	pid_t pid = wait(&status);
 	if (pid < 0) err(EX_OSERR, "wait");
 	if (WIFEXITED(status) && WEXITSTATUS(status)) {
-		uiFmt(TAG_DEFAULT, "spawn: exit %d", WEXITSTATUS(status));
+		uiFmt(TAG_STATUS, "spawn: exit %d", WEXITSTATUS(status));
 	} else if (WIFSIGNALED(status)) {
-		uiFmt(TAG_DEFAULT, "spawn: signal %d", WTERMSIG(status));
+		uiFmt(TAG_STATUS, "spawn: signal %d", WTERMSIG(status));
 	}
 }
 
 static void sigint(int sig) {
 	(void)sig;
-	input(TAG_DEFAULT, "/quit");
+	input(TAG_STATUS, "/quit");
 	uiExit();
 	exit(EX_OK);
 }
@@ -182,7 +182,7 @@ int main(int argc, char *argv[]) {
 	inputTab();
 
 	uiInit();
-	uiLog(TAG_DEFAULT, L"Traveling...");
+	uiLog(TAG_STATUS, L"Traveling...");
 	uiDraw();
 
 	fds.irc.fd = ircConnect(host, port, pass, webirc);
diff --git a/chat.h b/chat.h
index f900172..be1e05b 100644
--- a/chat.h
+++ b/chat.h
@@ -42,13 +42,30 @@ struct Tag {
 };
 
 enum { TAGS_LEN = 256 };
-const struct Tag TAG_ALL;
-const struct Tag TAG_DEFAULT;
+const struct Tag TAG_NONE;
+const struct Tag TAG_STATUS;
+const struct Tag TAG_VERBOSE;
 struct Tag tagFor(const char *name);
-struct Tag tagName(const char *name);
-struct Tag tagNum(size_t num);
 
 enum {
+	IRC_WHITE,
+	IRC_BLACK,
+	IRC_BLUE,
+	IRC_GREEN,
+	IRC_RED,
+	IRC_BROWN,
+	IRC_MAGENTA,
+	IRC_ORANGE,
+	IRC_YELLOW,
+	IRC_LIGHT_GREEN,
+	IRC_CYAN,
+	IRC_LIGHT_CYAN,
+	IRC_LIGHT_BLUE,
+	IRC_PINK,
+	IRC_GRAY,
+	IRC_LIGHT_GRAY,
+};
+enum {
 	IRC_BOLD      = 002,
 	IRC_COLOR     = 003,
 	IRC_REVERSE   = 026,
@@ -72,9 +89,9 @@ void uiInit(void);
 void uiHide(void);
 void uiExit(void);
 void uiDraw(void);
-void uiBeep(void);
 void uiRead(void);
-void uiFocus(struct Tag tag);
+void uiViewTag(struct Tag tag);
+void uiViewNum(int num);
 void uiTopic(struct Tag tag, const char *topic);
 void uiLog(struct Tag tag, const wchar_t *line);
 void uiFmt(struct Tag tag, const wchar_t *format, ...);
@@ -115,8 +132,9 @@ const wchar_t *editTail(void);
 
 void tabTouch(struct Tag tag, const char *word);
 void tabRemove(struct Tag tag, const char *word);
+void tabReplace(struct Tag tag, const char *prev, const char *next);
 void tabClear(struct Tag tag);
-void tabReplace(const char *prev, const char *next);
+struct Tag tabTag(const char *word);
 const char *tabNext(struct Tag tag, const char *prefix);
 void tabAccept(void);
 void tabReject(void);
diff --git a/handle.c b/handle.c
index 29b354c..4d2d4b1 100644
--- a/handle.c
+++ b/handle.c
@@ -23,14 +23,17 @@
 
 #include "chat.h"
 
-static int color(const char *s) {
-	if (!s) return 0;
-	int x = 0;
-	for (; s[0]; ++s) {
-		x ^= s[0];
+// Adapted from <https://github.com/cbreeden/fxhash/blob/master/lib.rs>.
+static int color(const char *str) {
+	if (!str) return IRC_GRAY;
+	uint32_t hash = 0;
+	for (; str[0]; ++str) {
+		hash = (hash << 5) | (hash >> 27);
+		hash ^= str[0];
+		hash *= 0x27220A95;
 	}
-	x &= 15;
-	return (x == 1) ? 0 : x;
+	hash &= IRC_LIGHT_GRAY;
+	return (hash == IRC_BLACK) ? IRC_GRAY : hash;
 }
 
 static char *paramField(char **params) {
@@ -90,44 +93,41 @@ static void handlePing(char *prefix, char *params) {
 	ircFmt("PONG %s\r\n", params);
 }
 
-static void handle432(char *prefix, char *params) {
+static void handleReplyErroneousNickname(char *prefix, char *params) {
 	char *mesg;
 	shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg);
-	uiLog(TAG_DEFAULT, L"You can't use that name here");
-	uiFmt(TAG_DEFAULT, "Sheriff says, \"%s\"", mesg);
-	uiLog(TAG_DEFAULT, L"Type /nick <name> to choose a new one");
+	uiLog(TAG_STATUS, L"You can't use that name here");
+	uiFmt(TAG_STATUS, "Sheriff says, \"%s\"", mesg);
+	uiLog(TAG_STATUS, L"Type /nick <name> to choose a new one");
 }
 
-static void handle001(char *prefix, char *params) {
+static void handleReplyWelcome(char *prefix, char *params) {
 	char *nick;
 	shift(prefix, NULL, NULL, NULL, params, 1, 0, &nick);
 	if (strcmp(nick, self.nick)) selfNick(nick);
-	tabTouch(TAG_DEFAULT, self.nick);
+	tabTouch(TAG_STATUS, self.nick);
 	if (self.join) ircFmt("JOIN %s\r\n", self.join);
-	uiLog(TAG_DEFAULT, L"You have arrived");
+	uiLog(TAG_STATUS, L"You have arrived");
 }
 
-static void handle372(char *prefix, char *params) {
+static void handleReplyMOTD(char *prefix, char *params) {
 	char *mesg;
 	shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg);
 	if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2];
-	uiFmt(TAG_DEFAULT, "%s", mesg);
+	urlScan(TAG_STATUS, mesg);
+	uiFmt(TAG_STATUS, "%s", mesg);
 }
 
 static void handleJoin(char *prefix, char *params) {
 	char *nick, *user, *chan;
 	shift(prefix, &nick, &user, NULL, params, 1, 0, &chan);
 	struct Tag tag = tagFor(chan);
-	if (isSelf(nick, user)) {
-		tabTouch(TAG_DEFAULT, chan);
-		uiFocus(tag);
-	} else {
-		tabTouch(tag, nick);
-	}
+	tabTouch(tag, nick);
 	uiFmt(
 		tag, "\3%d%s\3 arrives in \3%d%s\3",
 		color(user), nick, color(chan), chan
 	);
+	if (isSelf(nick, user)) uiViewTag(tag);
 }
 
 static void handlePart(char *prefix, char *params) {
@@ -136,6 +136,7 @@ static void handlePart(char *prefix, char *params) {
 	struct Tag tag = tagFor(chan);
 	(void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick));
 	if (mesg) {
+		urlScan(tag, mesg);
 		uiFmt(
 			tag, "\3%d%s\3 leaves \3%d%s\3, \"%s\"",
 			color(user), nick, color(chan), chan, mesg
@@ -154,6 +155,7 @@ static void handleKick(char *prefix, char *params) {
 	struct Tag tag = tagFor(chan);
 	(void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick));
 	if (mesg) {
+		urlScan(tag, mesg);
 		uiFmt(
 			tag, "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3, \"%s\"",
 			color(user), nick, color(kick), kick, color(chan), chan, mesg
@@ -169,20 +171,23 @@ static void handleKick(char *prefix, char *params) {
 static void handleQuit(char *prefix, char *params) {
 	char *nick, *user, *mesg;
 	shift(prefix, &nick, &user, NULL, params, 0, 1, &mesg);
-	// TODO: Send to tags where nick is in tab.
-	tabRemove(TAG_ALL, nick);
-	if (mesg) {
-		char *quot = (mesg[0] == '"') ? "" : "\"";
-		uiFmt(
-			TAG_DEFAULT, "\3%d%s\3 leaves, %s%s%s",
-			color(user), nick, quot, mesg, quot
-		);
-	} else {
-		uiFmt(TAG_DEFAULT, "\3%d%s\3 leaves", color(user), nick);
+	char *quot = (mesg && mesg[0] == '"') ? "" : "\"";
+	struct Tag tag;
+	while (TAG_NONE.id != (tag = tabTag(nick)).id) {
+		tabRemove(tag, nick);
+		if (mesg) {
+			urlScan(tag, mesg);
+			uiFmt(
+				tag, "\3%d%s\3 leaves, %s%s%s",
+				color(user), nick, quot, mesg, quot
+			);
+		} else {
+			uiFmt(tag, "\3%d%s\3 leaves", color(user), nick);
+		}
 	}
 }
 
-static void handle332(char *prefix, char *params) {
+static void handleReplyTopic(char *prefix, char *params) {
 	char *chan, *topic;
 	shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &chan, &topic);
 	struct Tag tag = tagFor(chan);
@@ -207,7 +212,7 @@ static void handleTopic(char *prefix, char *params) {
 	);
 }
 
-static void handle366(char *prefix, char *params) {
+static void handleReplyEndOfNames(char *prefix, char *params) {
 	char *chan;
 	shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan);
 	ircFmt("WHO %s\r\n", chan);
@@ -219,14 +224,14 @@ static struct {
 	size_t len;
 } who;
 
-static void handle352(char *prefix, char *params) {
+static void handleReplyWho(char *prefix, char *params) {
 	char *chan, *user, *nick;
 	shift(
 		prefix, NULL, NULL, NULL,
 		params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick
 	);
 	struct Tag tag = tagFor(chan);
-	if (!isSelf(nick, user)) tabTouch(tag, nick);
+	tabTouch(tag, nick);
 	size_t cap = sizeof(who.buf) - who.len;
 	int len = snprintf(
 		&who.buf[who.len], cap,
@@ -236,7 +241,7 @@ static void handle352(char *prefix, char *params) {
 	if ((size_t)len < cap) who.len += len;
 }
 
-static void handle315(char *prefix, char *params) {
+static void handleReplyEndOfWho(char *prefix, char *params) {
 	char *chan;
 	shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan);
 	struct Tag tag = tagFor(chan);
@@ -251,12 +256,14 @@ static void handleNick(char *prefix, char *params) {
 	char *prev, *user, *next;
 	shift(prefix, &prev, &user, NULL, params, 1, 0, &next);
 	if (isSelf(prev, user)) selfNick(next);
-	// TODO: Send to tags where prev is in tab.
-	tabReplace(prev, next);
-	uiFmt(
-		TAG_DEFAULT, "\3%d%s\3 is now known as \3%d%s\3",
-		color(user), prev, color(user), next
-	);
+	struct Tag tag;
+	while (TAG_NONE.id != (tag = tabTag(prev)).id) {
+		tabReplace(tag, prev, next);
+		uiFmt(
+			tag, "\3%d%s\3 is now known as \3%d%s\3",
+			color(user), prev, color(user), next
+		);
+	}
 }
 
 static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) {
@@ -288,13 +295,13 @@ static void handlePrivmsg(char *prefix, char *params) {
 		tag, "%c\3%d%c%s%c\17 %s",
 		ping["\17\26"], color(user), self["<("], nick, self[">)"], mesg
 	);
-	if (ping) uiBeep();
+	// TODO: always be beeping.
 }
 
 static void handleNotice(char *prefix, char *params) {
 	char *nick, *user, *chan, *mesg;
 	shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg);
-	struct Tag tag = TAG_DEFAULT;
+	struct Tag tag = TAG_STATUS;
 	if (user) tag = (strcmp(chan, self.nick) ? tagFor(chan) : tagFor(nick));
 	if (!isSelf(nick, user)) tabTouch(tag, nick);
 	urlScan(tag, mesg);
@@ -308,15 +315,15 @@ static const struct {
 	const char *command;
 	Handler handler;
 } HANDLERS[] = {
-	{ "001", handle001 },
-	{ "315", handle315 },
-	{ "332", handle332 },
-	{ "352", handle352 },
-	{ "366", handle366 },
-	{ "372", handle372 },
-	{ "375", handle372 },
-	{ "432", handle432 },
-	{ "433", handle432 },
+	{ "001", handleReplyWelcome },
+	{ "315", handleReplyEndOfWho },
+	{ "332", handleReplyTopic },
+	{ "352", handleReplyWho },
+	{ "366", handleReplyEndOfNames },
+	{ "372", handleReplyMOTD },
+	{ "375", handleReplyMOTD },
+	{ "432", handleReplyErroneousNickname },
+	{ "433", handleReplyErroneousNickname },
 	{ "JOIN", handleJoin },
 	{ "KICK", handleKick },
 	{ "NICK", handleNick },
diff --git a/input.c b/input.c
index f4e3106..cb7575f 100644
--- a/input.c
+++ b/input.c
@@ -24,7 +24,6 @@
 #include "chat.h"
 
 static void privmsg(struct Tag tag, bool action, const char *mesg) {
-	if (tag.id == TAG_DEFAULT.id) return;
 	char *line;
 	int send;
 	asprintf(
@@ -50,7 +49,7 @@ static void inputNick(struct Tag tag, char *params) {
 	if (nick) {
 		ircFmt("NICK %s\r\n", nick);
 	} else {
-		uiLog(TAG_DEFAULT, L"/nick requires a name");
+		uiLog(TAG_STATUS, L"/nick requires a name");
 	}
 }
 
@@ -60,7 +59,7 @@ static void inputJoin(struct Tag tag, char *params) {
 	if (chan) {
 		ircFmt("JOIN %s\r\n", chan);
 	} else {
-		uiLog(TAG_DEFAULT, L"/join requires a channel");
+		uiLog(TAG_STATUS, L"/join requires a channel");
 	}
 }
 
@@ -104,9 +103,12 @@ static void inputOpen(struct Tag tag, char *params) {
 static void inputView(struct Tag tag, char *params) {
 	char *view = strsep(&params, " ");
 	if (!view) return;
-	size_t num = strtoul(view, &view, 0);
-	tag = (view[0] ? tagName(view) : tagNum(num));
-	if (tag.name) uiFocus(tag);
+	int num = strtol(view, &view, 0);
+	if (view[0]) {
+		uiViewTag(tagFor(view));
+	} else {
+		uiViewNum(num);
+	}
 }
 
 static const struct {
@@ -128,7 +130,11 @@ static const size_t COMMANDS_LEN = sizeof(COMMANDS) / sizeof(COMMANDS[0]);
 
 void input(struct Tag tag, char *input) {
 	if (input[0] != '/') {
-		privmsg(tag, false, input);
+		if (tag.id == TAG_VERBOSE.id) {
+			ircFmt("%s\r\n", input);
+		} else if (tag.id != TAG_STATUS.id) {
+			privmsg(tag, false, input);
+		}
 		return;
 	}
 	char *command = strsep(&input, " ");
@@ -138,11 +144,11 @@ void input(struct Tag tag, char *input) {
 		COMMANDS[i].handler(tag, input);
 		return;
 	}
-	uiFmt(TAG_DEFAULT, "%s isn't a recognized command", command);
+	uiFmt(TAG_STATUS, "%s isn't a recognized command", command);
 }
 
 void inputTab(void) {
 	for (size_t i = 0; i < COMMANDS_LEN; ++i) {
-		tabTouch(TAG_DEFAULT, COMMANDS[i].command);
+		tabTouch(TAG_NONE, COMMANDS[i].command);
 	}
 }
diff --git a/irc.c b/irc.c
index 579f23b..0a131b2 100644
--- a/irc.c
+++ b/irc.c
@@ -106,15 +106,15 @@ void ircFmt(const char *format, ...) {
 	int len = vasprintf(&buf, format, ap);
 	va_end(ap);
 	if (!buf) err(EX_OSERR, "vasprintf");
-	if (self.verbose) uiFmt(tagFor("(irc)"), "\00314<<<\3 %.*s", len - 2, buf);
+	if (self.verbose) uiFmt(TAG_VERBOSE, "\3%d<<<\3 %.*s", IRC_WHITE, len - 2, buf);
 	ircWrite(buf, len);
 	free(buf);
 }
 
-static char buf[4096];
-static size_t len;
-
 void ircRead(void) {
+	static char buf[4096];
+	static size_t len;
+
 	ssize_t read = tls_read(client, &buf[len], sizeof(buf) - len);
 	if (read < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client));
 	if (!read) {
@@ -126,7 +126,7 @@ void ircRead(void) {
 	char *crlf, *line = buf;
 	while ((crlf = strnstr(line, "\r\n", &buf[len] - line))) {
 		crlf[0] = '\0';
-		if (self.verbose) uiFmt(tagFor("(irc)"), "\00314>>>\3 %s", line);
+		if (self.verbose) uiFmt(TAG_VERBOSE, "\3%d>>>\3 %s", IRC_GRAY, line);
 		handle(line);
 		line = &crlf[2];
 	}
diff --git a/tab.c b/tab.c
index c0a96a4..87e4f02 100644
--- a/tab.c
+++ b/tab.c
@@ -22,7 +22,7 @@
 #include "chat.h"
 
 static struct Entry {
-	size_t tag;
+	struct Tag tag;
 	char *word;
 	struct Entry *prev;
 	struct Entry *next;
@@ -49,7 +49,7 @@ static void touch(struct Entry *entry) {
 
 void tabTouch(struct Tag tag, const char *word) {
 	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (entry->tag != tag.id) continue;
+		if (entry->tag.id != tag.id) continue;
 		if (strcmp(entry->word, word)) continue;
 		touch(entry);
 		return;
@@ -58,30 +58,28 @@ void tabTouch(struct Tag tag, const char *word) {
 	struct Entry *entry = malloc(sizeof(*entry));
 	if (!entry) err(EX_OSERR, "malloc");
 
-	entry->tag = tag.id;
+	entry->tag = tag;
 	entry->word = strdup(word);
 	if (!entry->word) err(EX_OSERR, "strdup");
 
 	prepend(entry);
 }
 
-void tabReplace(const char *prev, const char *next) {
-	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (strcmp(entry->word, prev)) continue;
-		free(entry->word);
-		entry->word = strdup(next);
-		if (!entry->word) err(EX_OSERR, "strdup");
-	}
+void tabReplace(struct Tag tag, const char *prev, const char *next) {
+	tabTouch(tag, prev);
+	free(head->word);
+	head->word = strdup(next);
+	if (!head->word) err(EX_OSERR, "strdup");
 }
 
-static struct Entry *match;
+static struct Entry *iter;
 
 void tabRemove(struct Tag tag, const char *word) {
 	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (tag.id != TAG_ALL.id && entry->tag != tag.id) continue;
+		if (entry->tag.id != tag.id) continue;
 		if (strcmp(entry->word, word)) continue;
+		if (iter == entry) iter = entry->prev;
 		unlink(entry);
-		if (match == entry) match = entry->prev;
 		free(entry->word);
 		free(entry);
 		return;
@@ -90,33 +88,44 @@ void tabRemove(struct Tag tag, const char *word) {
 
 void tabClear(struct Tag tag) {
 	for (struct Entry *entry = head; entry; entry = entry->next) {
-		if (entry->tag != tag.id) continue;
+		if (entry->tag.id != tag.id) continue;
+		if (iter == entry) iter = entry->prev;
 		unlink(entry);
-		if (match == entry) match = entry->prev;
 		free(entry->word);
 		free(entry);
 	}
 }
 
+struct Tag tabTag(const char *word) {
+	struct Entry *start = (iter ? iter->next : head);
+	for (struct Entry *entry = start; entry; entry = entry->next) {
+		if (strcmp(entry->word, word)) continue;
+		iter = entry;
+		return entry->tag;
+	}
+	iter = NULL;
+	return TAG_NONE;
+}
+
 const char *tabNext(struct Tag tag, const char *prefix) {
 	size_t len = strlen(prefix);
-	struct Entry *start = (match ? match->next : head);
+	struct Entry *start = (iter ? iter->next : head);
 	for (struct Entry *entry = start; entry; entry = entry->next) {
-		if (entry->tag != TAG_DEFAULT.id && entry->tag != tag.id) continue;
+		if (entry->tag.id != TAG_NONE.id && entry->tag.id != tag.id) continue;
 		if (strncasecmp(entry->word, prefix, len)) continue;
-		match = entry;
+		iter = entry;
 		return entry->word;
 	}
-	if (!match) return NULL;
-	match = NULL;
+	if (!iter) return NULL;
+	iter = NULL;
 	return tabNext(tag, prefix);
 }
 
 void tabAccept(void) {
-	if (match) touch(match);
-	match = NULL;
+	if (iter) touch(iter);
+	iter = NULL;
 }
 
 void tabReject(void) {
-	match = NULL;
+	iter = NULL;
 }
diff --git a/tag.c b/tag.c
index 014e84c..397c191 100644
--- a/tag.c
+++ b/tag.c
@@ -21,57 +21,30 @@
 
 #include "chat.h"
 
-const struct Tag TAG_ALL = { (size_t)-1, NULL };
-const struct Tag TAG_DEFAULT = { 0, "(status)" };
+const struct Tag TAG_NONE    = { 0, "" };
+const struct Tag TAG_STATUS  = { 1, "(status)" };
+const struct Tag TAG_VERBOSE = { 2, "(irc)" };
 
 static struct {
 	char *name[TAGS_LEN];
 	size_t len;
-	size_t gap;
 } tags = {
-	.name = { "(status)" },
-	.len = 1,
-	.gap = 1,
+	.name = { "", "(status)", "(irc)" },
+	.len = 3,
 };
 
 static struct Tag Tag(size_t id) {
 	return (struct Tag) { id, tags.name[id] };
 }
 
-struct Tag tagName(const char *name) {
+struct Tag tagFor(const char *name) {
 	for (size_t id = 0; id < tags.len; ++id) {
-		if (!tags.name[id] || strcmp(tags.name[id], name)) continue;
+		if (strcmp(tags.name[id], name)) continue;
 		return Tag(id);
 	}
-	return TAG_ALL;
-}
-
-struct Tag tagNum(size_t num) {
-	if (num < tags.gap) return Tag(num);
-	num -= tags.gap;
-	for (size_t id = tags.gap; id < tags.len; ++id) {
-		if (!tags.name[id]) continue;
-		if (!num--) return Tag(id);
-	}
-	return TAG_ALL;
-}
-
-struct Tag tagFor(const char *name) {
-	struct Tag tag = tagName(name);
-	if (tag.name) return tag;
-
-	size_t id = tags.gap;
+	if (tags.len == TAGS_LEN) return TAG_STATUS;
+	size_t id = tags.len++;
 	tags.name[id] = strdup(name);
 	if (!tags.name[id]) err(EX_OSERR, "strdup");
-
-	if (tags.gap == tags.len) {
-		tags.gap++;
-		tags.len++;
-	} else {
-		for (tags.gap++; tags.gap < tags.len; ++tags.gap) {
-			if (!tags.name[tags.gap]) break;
-		}
-	}
-
 	return Tag(id);
 }
diff --git a/ui.c b/ui.c
index 844e777..748235c 100644
--- a/ui.c
+++ b/ui.c
@@ -21,13 +21,16 @@
 #include <locale.h>
 #include <stdarg.h>
 #include <stdbool.h>
-#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sysexits.h>
 #include <wchar.h>
 #include <wctype.h>
 
+#ifndef A_ITALIC
+#define A_ITALIC A_NORMAL
+#endif
+
 #include "chat.h"
 #undef uiFmt
 
@@ -35,20 +38,6 @@
 #define MAX(a, b) ((a) > (b) ? (a) : (b))
 
 #define CTRL(c)   ((c) & 037)
-#define UNCTRL(c) ((c) + '@')
-
-#ifndef A_ITALIC
-#define A_ITALIC A_NORMAL
-#endif
-
-static void focusEnable(void) {
-	printf("\33[?1004h");
-	fflush(stdout);
-}
-static void focusDisable(void) {
-	printf("\33[?1004l");
-	fflush(stdout);
-}
 
 static void colorInit(void) {
 	start_color();
@@ -81,52 +70,83 @@ static short pair8(short pair) {
 	return (pair & 0x70) >> 1 | (pair & 0x07);
 }
 
-static const int TOPIC_COLS = 512;
-static const int INPUT_COLS = 512;
-static const int LOG_LINES = 100;
+static const int LOG_LINES   = 256;
+static const int TOPIC_COLS  = 512;
+static const int INPUT_COLS  = 512;
+
+static struct View {
+	struct Tag tag;
+	WINDOW *topic;
+	WINDOW *log;
+	int scroll;
+	bool mark;
+	struct View *prev;
+	struct View *next;
+} *viewHead, *viewTail;
+
+static void viewAppend(struct View *view) {
+	if (viewTail) viewTail->next = view;
+	view->prev = viewTail;
+	view->next = NULL;
+	viewTail = view;
+	if (!viewHead) viewHead = view;
+}
 
+static int logHeight(const struct View *view) {
+	return LINES - (view->topic ? 2 : 0) - 2;
+}
+static int lastLogLine(void) {
+	return LOG_LINES - 1;
+}
 static int lastLine(void) {
 	return LINES - 1;
 }
 static int lastCol(void) {
 	return COLS - 1;
 }
-static int logHeight(void) {
-	return LINES - 4;
-}
-
-struct View {
-	WINDOW *topic;
-	WINDOW *log;
-	int scroll;
-	bool mark;
-};
 
 static struct {
 	bool hide;
 	WINDOW *input;
-	struct Tag tag;
-	struct View views[TAGS_LEN];
-	size_t len;
+	struct View *view;
+	struct View *tags[TAGS_LEN];
 } ui;
 
+static struct View *viewTag(struct Tag tag) {
+	struct View *view = ui.tags[tag.id];
+	if (view) return view;
+
+	view = calloc(1, sizeof(*view));
+	if (!view) err(EX_OSERR, "calloc");
+
+	view->tag = tag;
+	view->log = newpad(LOG_LINES, COLS);
+	wsetscrreg(view->log, 0, lastLogLine());
+	scrollok(view->log, true);
+	wmove(view->log, lastLogLine() - logHeight(view), 0);
+	view->scroll = LOG_LINES;
+
+	viewAppend(view);
+	ui.tags[tag.id] = view;
+	return view;
+}
+
 void uiInit(void) {
 	setlocale(LC_CTYPE, "");
 	initscr();
 	cbreak();
 	noecho();
 
-	focusEnable();
 	colorInit();
-
-	ui.tag = TAG_DEFAULT;
+	termMode(TERM_FOCUS, true);
 
 	ui.input = newpad(2, INPUT_COLS);
 	mvwhline(ui.input, 0, 0, ACS_HLINE, INPUT_COLS);
 	wmove(ui.input, 1, 0);
-
 	keypad(ui.input, true);
 	nodelay(ui.input, true);
+
+	ui.view = viewTag(TAG_STATUS);
 }
 
 void uiHide(void) {
@@ -136,54 +156,34 @@ void uiHide(void) {
 
 void uiExit(void) {
 	uiHide();
-	focusDisable();
+	termMode(TERM_FOCUS, false);
 	printf(
 		"This program is AGPLv3 free software!\n"
 		"The source is available at <" SOURCE_URL ">.\n"
 	);
 }
 
-static struct View *uiView(struct Tag tag) {
-	struct View *view = &ui.views[tag.id];
-	if (view->log) return view;
-
-	view->topic = newpad(2, TOPIC_COLS);
-	mvwhline(view->topic, 1, 0, ACS_HLINE, TOPIC_COLS);
-
-	view->log = newpad(LOG_LINES, COLS);
-	wsetscrreg(view->log, 0, LOG_LINES - 1);
-	scrollok(view->log, true);
-	wmove(view->log, LOG_LINES - logHeight() - 1, 0);
-
-	view->scroll = LOG_LINES;
-	view->mark = false;
-
-	if (tag.id >= ui.len) ui.len = tag.id + 1;
-	return view;
-}
-
 static void uiResize(void) {
-	for (size_t i = 0; i < ui.len; ++i) {
-		struct View *view = &ui.views[i];
-		if (!view->log) continue;
+	for (struct View *view = viewHead; view; view = view->next) {
 		wresize(view->log, LOG_LINES, COLS);
-		wmove(view->log, LOG_LINES - 1, COLS - 1);
+		wmove(view->log, lastLogLine(), lastCol());
 	}
 }
 
 void uiDraw(void) {
 	if (ui.hide) return;
-	struct View *view = uiView(ui.tag);
-	pnoutrefresh(
-		view->topic,
-		0, 0,
-		0, 0,
-		1, lastCol()
-	);
+	if (ui.view->topic) {
+		pnoutrefresh(
+			ui.view->topic,
+			0, 0,
+			0, 0,
+			1, lastCol()
+		);
+	}
 	pnoutrefresh(
-		view->log,
-		view->scroll - logHeight(), 0,
-		2, 0,
+		ui.view->log,
+		ui.view->scroll - logHeight(ui.view), 0,
+		(ui.view->topic ? 2 : 0), 0,
 		lastLine() - 2, lastCol()
 	);
 	int _, x;
@@ -201,37 +201,51 @@ static void uiRedraw(void) {
 	clearok(curscr, true);
 }
 
-void uiFocus(struct Tag tag) {
-	struct View *view = uiView(ui.tag);
-	view->mark = true;
-	view = uiView(tag);
-	view->mark = false;
-	touchwin(view->topic);
+static void uiView(struct View *view) {
+	if (view->topic) touchwin(view->topic);
 	touchwin(view->log);
-	ui.tag = tag;
+	view->mark = false;
+	ui.view->mark = true;
+	ui.view = view;
 }
 
-void uiBeep(void) {
-	beep(); // always be beeping
+void uiViewTag(struct Tag tag) {
+	uiView(viewTag(tag));
 }
 
-static const short IRC_COLORS[16] = {
-	8 + COLOR_WHITE,   // white
-	0 + COLOR_BLACK,   // black
-	0 + COLOR_BLUE,    // blue
-	0 + COLOR_GREEN,   // green
-	8 + COLOR_RED,     // red
-	0 + COLOR_RED,     // brown
-	0 + COLOR_MAGENTA, // magenta
-	0 + COLOR_YELLOW,  // orange
-	8 + COLOR_YELLOW,  // yellow
-	8 + COLOR_GREEN,   // light green
-	0 + COLOR_CYAN,    // cyan
-	8 + COLOR_CYAN,    // light cyan
-	8 + COLOR_BLUE,    // light blue
-	8 + COLOR_MAGENTA, // pink
-	8 + COLOR_BLACK,   // gray
-	0 + COLOR_WHITE,   // light gray
+void uiViewNum(int num) {
+	if (num < 0) {
+		for (struct View *view = viewTail; view; view = view->prev) {
+			if (++num) continue;
+			uiView(view);
+			break;
+		}
+	} else {
+		for (struct View *view = viewHead; view; view = view->next) {
+			if (num--) continue;
+			uiView(view);
+			break;
+		}
+	}
+}
+
+static const short IRC_COLORS[] = {
+	[IRC_WHITE]       = 8 + COLOR_WHITE,
+	[IRC_BLACK]       = 0 + COLOR_BLACK,
+	[IRC_BLUE]        = 0 + COLOR_BLUE,
+	[IRC_GREEN]       = 0 + COLOR_GREEN,
+	[IRC_RED]         = 8 + COLOR_RED,
+	[IRC_BROWN]       = 0 + COLOR_RED,
+	[IRC_MAGENTA]     = 0 + COLOR_MAGENTA,
+	[IRC_ORANGE]      = 0 + COLOR_YELLOW,
+	[IRC_YELLOW]      = 8 + COLOR_YELLOW,
+	[IRC_LIGHT_GREEN] = 8 + COLOR_GREEN,
+	[IRC_CYAN]        = 0 + COLOR_CYAN,
+	[IRC_LIGHT_CYAN]  = 8 + COLOR_CYAN,
+	[IRC_LIGHT_BLUE]  = 8 + COLOR_BLUE,
+	[IRC_PINK]        = 8 + COLOR_MAGENTA,
+	[IRC_GRAY]        = 8 + COLOR_BLACK,
+	[IRC_LIGHT_GRAY]  = 0 + COLOR_WHITE,
 };
 
 static const wchar_t *parseColor(short *pair, const wchar_t *str) {
@@ -310,9 +324,13 @@ static void addIRC(WINDOW *win, const wchar_t *str) {
 }
 
 void uiTopic(struct Tag tag, const char *topic) {
+	struct View *view = viewTag(tag);
+	if (!view->topic) {
+		view->topic = newpad(2, TOPIC_COLS);
+		mvwhline(view->topic, 1, 0, ACS_HLINE, TOPIC_COLS);
+	}
 	wchar_t *wcs = ambstowcs(topic);
 	if (!wcs) err(EX_DATAERR, "ambstowcs");
-	struct View *view = uiView(tag);
 	wmove(view->topic, 0, 0);
 	addIRC(view->topic, wcs);
 	wclrtoeol(view->topic);
@@ -320,7 +338,7 @@ void uiTopic(struct Tag tag, const char *topic) {
 }
 
 void uiLog(struct Tag tag, const wchar_t *line) {
-	struct View *view = uiView(tag);
+	struct View *view = viewTag(tag);
 	waddch(view->log, '\n');
 	if (view->mark) {
 		waddch(view->log, '\n');
@@ -330,93 +348,89 @@ void uiLog(struct Tag tag, const wchar_t *line) {
 }
 
 void uiFmt(struct Tag tag, const wchar_t *format, ...) {
-	wchar_t *buf;
+	wchar_t *wcs;
 	va_list ap;
 	va_start(ap, format);
-	vaswprintf(&buf, format, ap);
+	vaswprintf(&wcs, format, ap);
 	va_end(ap);
-	if (!buf) err(EX_OSERR, "vaswprintf");
-	uiLog(tag, buf);
-	free(buf);
+	if (!wcs) err(EX_OSERR, "vaswprintf");
+	uiLog(tag, wcs);
+	free(wcs);
 }
 
-static void logUp(void) {
-	struct View *view = uiView(ui.tag);
-	if (view->scroll == logHeight()) return;
-	if (view->scroll == LOG_LINES) view->mark = true;
-	view->scroll = MAX(view->scroll - logHeight() / 2, logHeight());
+static void logPageUp(void) {
+	int height = logHeight(ui.view);
+	if (ui.view->scroll == height) return;
+	if (ui.view->scroll == LOG_LINES) ui.view->mark = true;
+	ui.view->scroll = MAX(ui.view->scroll - height / 2, height);
 }
-static void logDown(void) {
-	struct View *view = uiView(ui.tag);
-	if (view->scroll == LOG_LINES) return;
-	view->scroll = MIN(view->scroll + logHeight() / 2, LOG_LINES);
-	if (view->scroll == LOG_LINES) view->mark = false;
+static void logPageDown(void) {
+	if (ui.view->scroll == LOG_LINES) return;
+	ui.view->scroll = MIN(ui.view->scroll + logHeight(ui.view) / 2, LOG_LINES);
+	if (ui.view->scroll == LOG_LINES) ui.view->mark = false;
 }
 
 static bool keyChar(wchar_t ch) {
-	static bool esc, csi;
-	if (ch == L'\33') {
-		esc = true;
-		return false;
-	}
-	if (esc && ch == L'[') {
-		esc = false;
-		csi = true;
-		return false;
+	if (iswascii(ch)) {
+		enum TermEvent event = termEvent((char)ch);
+		switch (event) {
+			break; case TERM_FOCUS_IN:  ui.view->mark = false;
+			break; case TERM_FOCUS_OUT: ui.view->mark = true;
+			break; default: {}
+		}
+		if (event) return false;
 	}
-	if (csi) {
-		if (ch == L'O') uiView(ui.tag)->mark = true;
-		if (ch == L'I') uiView(ui.tag)->mark = false;
-		csi = false;
+
+	static bool meta;
+	if (ch == L'\33') {
+		meta = true;
 		return false;
 	}
-	if (ch == L'\177') ch = L'\b';
 
-	bool update = true;
-	if (esc) {
+	if (meta) {
+		bool update = true;
 		switch (ch) {
-			break; case L'b':  edit(ui.tag, EDIT_BACK_WORD, 0);
-			break; case L'f':  edit(ui.tag, EDIT_FORE_WORD, 0);
-			break; case L'\b': edit(ui.tag, EDIT_KILL_BACK_WORD, 0);
-			break; case L'd':  edit(ui.tag, EDIT_KILL_FORE_WORD, 0);
+			break; case L'b':  edit(ui.view->tag, EDIT_BACK_WORD, 0);
+			break; case L'f':  edit(ui.view->tag, EDIT_FORE_WORD, 0);
+			break; case L'\b': edit(ui.view->tag, EDIT_KILL_BACK_WORD, 0);
+			break; case L'd':  edit(ui.view->tag, EDIT_KILL_FORE_WORD, 0);
 			break; default: {
 				update = false;
-				if (ch >= L'0' && ch <= L'9') {
-					struct Tag tag = tagNum(ch - L'0');
-					if (tag.name) uiFocus(tag);
-				}
+				if (ch < L'0' || ch > L'9') break;
+				uiViewNum(ch - L'0');
 			}
 		}
-		esc = false;
+		meta = false;
 		return update;
 	}
 
+	if (ch == L'\177') ch = L'\b';
 	switch (ch) {
 		break; case CTRL(L'L'): uiRedraw(); return false;
 
-		break; case CTRL(L'A'): edit(ui.tag, EDIT_HOME, 0);
-		break; case CTRL(L'B'): edit(ui.tag, EDIT_LEFT, 0);
-		break; case CTRL(L'D'): edit(ui.tag, EDIT_DELETE, 0);
-		break; case CTRL(L'E'): edit(ui.tag, EDIT_END, 0);
-		break; case CTRL(L'F'): edit(ui.tag, EDIT_RIGHT, 0);
-		break; case CTRL(L'K'): edit(ui.tag, EDIT_KILL_LINE, 0);
-		break; case CTRL(L'W'): edit(ui.tag, EDIT_KILL_BACK_WORD, 0);
-
-		break; case CTRL(L'C'): edit(ui.tag, EDIT_INSERT, IRC_COLOR);
-		break; case CTRL(L'N'): edit(ui.tag, EDIT_INSERT, IRC_RESET);
-		break; case CTRL(L'O'): edit(ui.tag, EDIT_INSERT, IRC_BOLD);
-		break; case CTRL(L'R'): edit(ui.tag, EDIT_INSERT, IRC_COLOR);
-		break; case CTRL(L'T'): edit(ui.tag, EDIT_INSERT, IRC_ITALIC);
-		break; case CTRL(L'U'): edit(ui.tag, EDIT_INSERT, IRC_UNDERLINE);
-		break; case CTRL(L'V'): edit(ui.tag, EDIT_INSERT, IRC_REVERSE);
-
-		break; case L'\b': edit(ui.tag, EDIT_BACKSPACE, 0);
-		break; case L'\t': edit(ui.tag, EDIT_COMPLETE, 0);
-		break; case L'\n': edit(ui.tag, EDIT_ENTER, 0);
+		break; case CTRL(L'A'): edit(ui.view->tag, EDIT_HOME, 0);
+		break; case CTRL(L'B'): edit(ui.view->tag, EDIT_LEFT, 0);
+		break; case CTRL(L'D'): edit(ui.view->tag, EDIT_DELETE, 0);
+		break; case CTRL(L'E'): edit(ui.view->tag, EDIT_END, 0);
+		break; case CTRL(L'F'): edit(ui.view->tag, EDIT_RIGHT, 0);
+		break; case CTRL(L'K'): edit(ui.view->tag, EDIT_KILL_LINE, 0);
+		break; case CTRL(L'W'): edit(ui.view->tag, EDIT_KILL_BACK_WORD, 0);
+
+		break; case CTRL(L'C'): edit(ui.view->tag, EDIT_INSERT, IRC_COLOR);
+		break; case CTRL(L'N'): edit(ui.view->tag, EDIT_INSERT, IRC_RESET);
+		break; case CTRL(L'O'): edit(ui.view->tag, EDIT_INSERT, IRC_BOLD);
+		break; case CTRL(L'R'): edit(ui.view->tag, EDIT_INSERT, IRC_COLOR);
+		break; case CTRL(L'T'): edit(ui.view->tag, EDIT_INSERT, IRC_ITALIC);
+		break; case CTRL(L'U'): edit(ui.view->tag, EDIT_INSERT, IRC_UNDERLINE);
+		break; case CTRL(L'V'): edit(ui.view->tag, EDIT_INSERT, IRC_REVERSE);
+
+		break; case L'\b': edit(ui.view->tag, EDIT_BACKSPACE, 0);
+		break; case L'\t': edit(ui.view->tag, EDIT_COMPLETE, 0);
+		break; case L'\n': edit(ui.view->tag, EDIT_ENTER, 0);
 
 		break; default: {
 			if (!iswprint(ch)) return false;
-			edit(ui.tag, EDIT_INSERT, ch);
+			edit(ui.view->tag, EDIT_INSERT, ch);
 		}
 	}
 	return true;
@@ -425,15 +439,15 @@ static bool keyChar(wchar_t ch) {
 static bool keyCode(wchar_t ch) {
 	switch (ch) {
 		break; case KEY_RESIZE:    uiResize(); return false;
-		break; case KEY_PPAGE:     logUp(); return false;
-		break; case KEY_NPAGE:     logDown(); return false;
-		break; case KEY_LEFT:      edit(ui.tag, EDIT_LEFT, ch);
-		break; case KEY_RIGHT:     edit(ui.tag, EDIT_RIGHT, ch);
-		break; case KEY_HOME:      edit(ui.tag, EDIT_HOME, ch);
-		break; case KEY_END:       edit(ui.tag, EDIT_END, ch);
-		break; case KEY_DC:        edit(ui.tag, EDIT_DELETE, ch);
-		break; case KEY_BACKSPACE: edit(ui.tag, EDIT_BACKSPACE, ch);
-		break; case KEY_ENTER:     edit(ui.tag, EDIT_ENTER, ch);
+		break; case KEY_PPAGE:     logPageUp(); return false;
+		break; case KEY_NPAGE:     logPageDown(); return false;
+		break; case KEY_LEFT:      edit(ui.view->tag, EDIT_LEFT, 0);
+		break; case KEY_RIGHT:     edit(ui.view->tag, EDIT_RIGHT, 0);
+		break; case KEY_HOME:      edit(ui.view->tag, EDIT_HOME, 0);
+		break; case KEY_END:       edit(ui.view->tag, EDIT_END, 0);
+		break; case KEY_DC:        edit(ui.view->tag, EDIT_DELETE, 0);
+		break; case KEY_BACKSPACE: edit(ui.view->tag, EDIT_BACKSPACE, 0);
+		break; case KEY_ENTER:     edit(ui.view->tag, EDIT_ENTER, 0);
 	}
 	return true;
 }
@@ -453,14 +467,11 @@ void uiRead(void) {
 	}
 	if (!update) return;
 
+	int y, x;
 	wmove(ui.input, 1, 0);
 	addIRC(ui.input, editHead());
-
-	int y, x;
 	getyx(ui.input, y, x);
-
 	addIRC(ui.input, editTail());
-
 	wclrtoeol(ui.input);
 	wmove(ui.input, y, x);
 }