about summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
-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);
 }