summary refs log tree commit diff
diff options
context:
space:
mode:
authorJune McEnroe2022-07-30 18:47:26 -0400
committerJune McEnroe2022-07-30 18:47:26 -0400
commit14a6486b9b2b9522982cde12c05b20931fe34999 (patch)
tree672349f06f9e998e6859a937230cc32787af2d9f
parentc0be6fe4b207fe8f57e8a9a97734302b15733831 (diff)
Switch to cache interfaces
-rw-r--r--Makefile1
-rw-r--r--README.714
-rw-r--r--chat.c2
-rw-r--r--chat.h16
-rw-r--r--command.c19
-rw-r--r--complete.c187
-rw-r--r--handle.c86
-rw-r--r--input.c13
-rw-r--r--window.c6
9 files changed, 75 insertions, 269 deletions
diff --git a/Makefile b/Makefile
index 39b15b2..e8ef63a 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,6 @@ OBJS += buffer.o
 OBJS += cache.o
 OBJS += chat.o
 OBJS += command.o
-OBJS += complete.o
 OBJS += config.o
 OBJS += edit.o
 OBJS += filter.o
diff --git a/README.7 b/README.7
index d10d5f6..64024ab 100644
--- a/README.7
+++ b/README.7
@@ -1,5 +1,5 @@
-.\" To view this file, run: man ./README.7
-.Dd March 31, 2022
+.\" To view this file: $ man ./README.7
+.Dd July 30, 2022
 .Dt README 7
 .Os "Causal Agency"
 .
@@ -9,7 +9,7 @@
 .
 .Sh DESCRIPTION
 .Xr catgirl 1
-is a TLS-only terminal IRC client.
+is a terminal IRC client.
 .
 .Pp
 Screenshot:
@@ -180,7 +180,7 @@ $ make -C scripts sandman
 .Ed
 .
 .Sh FILES
-.Bl -tag -width "complete.c" -compact
+.Bl -tag -width "command.c" -compact
 .It Pa chat.h
 global state and declarations
 .It Pa chat.c
@@ -201,8 +201,8 @@ command handling
 line wrapping
 .It Pa edit.c
 line editing
-.It Pa complete.c
-tab complete
+.It Pa cache.c
+ordered cache
 .It Pa url.c
 URL detection
 .It Pa filter.c
@@ -270,4 +270,4 @@ IRC bouncer:
 .%D June 19, 2020
 .Re
 .
-.\" To view this file, run: man ./README.7
+.\" To view this file: $ man ./README.7
diff --git a/chat.c b/chat.c
index 2d35771..39b1a93 100644
--- a/chat.c
+++ b/chat.c
@@ -374,7 +374,7 @@ int main(int argc, char *argv[]) {
 	set(&network.name, host);
 	set(&self.nick, "*");
 
-	inputCompleteAdd();
+	inputCache();
 
 	ircConfig(insecure, trust, cert, priv);
 
diff --git a/chat.h b/chat.h
index b7de9ac..ea4ece3 100644
--- a/chat.h
+++ b/chat.h
@@ -292,7 +292,7 @@ const char *commandIsPrivmsg(uint id, const char *input);
 const char *commandIsNotice(uint id, const char *input);
 const char *commandIsAction(uint id, const char *input);
 size_t commandWillSplit(uint id, const char *input);
-void commandCompleteAdd(void);
+void commandCache(void);
 
 enum Heat {
 	Ice,
@@ -334,7 +334,7 @@ void inputWait(void);
 void inputUpdate(void);
 bool inputPending(uint id);
 void inputRead(void);
-void inputCompleteAdd(void);
+void inputCache(void);
 int inputSave(FILE *file);
 void inputLoad(FILE *file, size_t version);
 
@@ -412,18 +412,6 @@ void cacheReject(struct Cursor *curs);
 void cacheRemove(uint id, const char *key);
 void cacheClear(uint id);
 
-const char *complete(uint id, const char *prefix);
-const char *completeSubstr(uint id, const char *substr);
-void completeAccept(void);
-void completeReject(void);
-void completeAdd(uint id, const char *str, enum Color color);
-void completeTouch(uint id, const char *str, enum Color color);
-void completeReplace(uint id, const char *old, const char *new);
-void completeRemove(uint id, const char *str);
-void completeClear(uint id);
-uint completeID(const char *str);
-enum Color completeColor(uint id, const char *str);
-
 extern struct Util urlOpenUtil;
 extern struct Util urlCopyUtil;
 void urlScan(uint id, const char *nick, const char *mesg);
diff --git a/command.c b/command.c
index dcd0e22..f5d9dbf 100644
--- a/command.c
+++ b/command.c
@@ -139,7 +139,7 @@ static void commandMsg(uint id, char *params) {
 	char *nick = strsep(&params, " ");
 	uint msg = idFor(nick);
 	if (idColors[msg] == Default) {
-		idColors[msg] = completeColor(id, nick);
+		idColors[msg] = cacheColor(id, nick);
 	}
 	if (params) {
 		splitMessage("PRIVMSG", msg, params);
@@ -380,7 +380,7 @@ static void commandQuery(uint id, char *params) {
 	if (!params) return;
 	uint query = idFor(params);
 	if (idColors[query] == Default) {
-		idColors[query] = completeColor(id, params);
+		idColors[query] = cacheColor(id, params);
 	}
 	windowShow(windowFor(query));
 }
@@ -396,10 +396,11 @@ static void commandWindow(uint id, char *params) {
 			windowShow(windowFor(id));
 			return;
 		}
-		for (const char *match; (match = completeSubstr(None, params));) {
+		struct Cursor curs = {0};
+		for (const char *match; (match = cacheSubstr(&curs, None, params));) {
 			id = idFind(match);
 			if (!id) continue;
-			completeAccept();
+			cacheAccept(&curs);
 			windowShow(windowFor(id));
 			break;
 		}
@@ -669,11 +670,11 @@ void command(uint id, char *input) {
 		return;
 	}
 
+	struct Cursor curs = {0};
 	const char *cmd = strsep(&input, " ");
-	const char *unique = complete(None, cmd);
-	if (unique && !complete(None, cmd)) {
+	const char *unique = cachePrefix(&curs, None, cmd);
+	if (unique && !cachePrefix(&curs, None, cmd)) {
 		cmd = unique;
-		completeReject();
 	}
 
 	const struct Handler *handler = bsearch(
@@ -700,9 +701,9 @@ void command(uint id, char *input) {
 	handler->fn(id, input);
 }
 
-void commandCompleteAdd(void) {
+void commandCache(void) {
 	for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) {
 		if (!commandAvailable(&Commands[i])) continue;
-		completeAdd(None, Commands[i].cmd, Default);
+		cacheInsert(false, None, Commands[i].cmd);
 	}
 }
diff --git a/complete.c b/complete.c
deleted file mode 100644
index e8ea3fe..0000000
--- a/complete.c
+++ /dev/null
@@ -1,187 +0,0 @@
-/* Copyright (C) 2020  June McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- *
- * Additional permission under GNU GPL version 3 section 7:
- *
- * If you modify this Program, or any covered work, by linking or
- * combining it with OpenSSL (or a modified version of that library),
- * containing parts covered by the terms of the OpenSSL License and the
- * original SSLeay license, the licensors of this Program grant you
- * additional permission to convey the resulting work. Corresponding
- * Source for a non-source form of such a combination shall include the
- * source code for the parts of OpenSSL used as well as that of the
- * covered work.
- */
-
-#include <err.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-
-#include "chat.h"
-
-struct Node {
-	uint id;
-	char *str;
-	enum Color color;
-	struct Node *prev;
-	struct Node *next;
-};
-
-static struct Node *alloc(uint id, const char *str, enum Color color) {
-	struct Node *node = malloc(sizeof(*node));
-	if (!node) err(EX_OSERR, "malloc");
-	node->id = id;
-	node->str = strdup(str);
-	node->color = color;
-	node->prev = NULL;
-	node->next = NULL;
-	if (!node->str) err(EX_OSERR, "strdup");
-	return node;
-}
-
-static struct Node *head;
-static struct Node *tail;
-
-static struct Node *detach(struct Node *node) {
-	if (node->prev) node->prev->next = node->next;
-	if (node->next) node->next->prev = node->prev;
-	if (head == node) head = node->next;
-	if (tail == node) tail = node->prev;
-	node->prev = NULL;
-	node->next = NULL;
-	return node;
-}
-
-static struct Node *prepend(struct Node *node) {
-	node->prev = NULL;
-	node->next = head;
-	if (head) head->prev = node;
-	head = node;
-	tail = (tail ?: node);
-	return node;
-}
-
-static struct Node *append(struct Node *node) {
-	node->next = NULL;
-	node->prev = tail;
-	if (tail) tail->next = node;
-	tail = node;
-	head = (head ?: node);
-	return node;
-}
-
-static struct Node *find(uint id, const char *str) {
-	for (struct Node *node = head; node; node = node->next) {
-		if (node->id != id) continue;
-		if (strcmp(node->str, str)) continue;
-		return node;
-	}
-	return NULL;
-}
-
-void completeAdd(uint id, const char *str, enum Color color) {
-	if (!find(id, str)) append(alloc(id, str, color));
-}
-
-void completeTouch(uint id, const char *str, enum Color color) {
-	struct Node *node = find(id, str);
-	if (node) node->color = color;
-	prepend(node ? detach(node) : alloc(id, str, color));
-}
-
-enum Color completeColor(uint id, const char *str) {
-	struct Node *node = find(id, str);
-	return (node ? node->color : Default);
-}
-
-static struct Node *match;
-
-const char *complete(uint id, const char *prefix) {
-	for (match = (match ? match->next : head); match; match = match->next) {
-		if (match->id && match->id != id) continue;
-		if (strncasecmp(match->str, prefix, strlen(prefix))) continue;
-		return match->str;
-	}
-	return NULL;
-}
-
-const char *completeSubstr(uint id, const char *substr) {
-	for (match = (match ? match->next : head); match; match = match->next) {
-		if (match->id && match->id != id) continue;
-		if (!strstr(match->str, substr)) continue;
-		return match->str;
-	}
-	return NULL;
-}
-
-void completeAccept(void) {
-	if (match) prepend(detach(match));
-	match = NULL;
-}
-
-void completeReject(void) {
-	match = NULL;
-}
-
-static struct Node *iter;
-
-uint completeID(const char *str) {
-	for (iter = (iter ? iter->next : head); iter; iter = iter->next) {
-		if (iter->id && !strcmp(iter->str, str)) return iter->id;
-	}
-	return None;
-}
-
-void completeReplace(uint id, const char *old, const char *new) {
-	struct Node *next = NULL;
-	for (struct Node *node = head; node; node = next) {
-		next = node->next;
-		if (id && node->id != id) continue;
-		if (strcmp(node->str, old)) continue;
-		free(node->str);
-		node->str = strdup(new);
-		prepend(detach(node));
-		if (!node->str) err(EX_OSERR, "strdup");
-	}
-}
-
-void completeRemove(uint id, const char *str) {
-	struct Node *next = NULL;
-	for (struct Node *node = head; node; node = next) {
-		next = node->next;
-		if (id && node->id != id) continue;
-		if (strcmp(node->str, str)) continue;
-		if (match == node) match = NULL;
-		if (iter == node) iter = NULL;
-		detach(node);
-		free(node->str);
-		free(node);
-	}
-}
-
-void completeClear(uint id) {
-	struct Node *next = NULL;
-	for (struct Node *node = head; node; node = next) {
-		next = node->next;
-		if (node->id != id) continue;
-		if (match == node) match = NULL;
-		if (iter == node) iter = NULL;
-		detach(node);
-		free(node->str);
-		free(node);
-	}
-}
diff --git a/handle.c b/handle.c
index d98a72c..8034957 100644
--- a/handle.c
+++ b/handle.c
@@ -266,7 +266,7 @@ static void handleErrorSASLFail(struct Message *msg) {
 static void handleReplyWelcome(struct Message *msg) {
 	require(msg, false, 1);
 	set(&self.nick, msg->params[0]);
-	completeTouch(Network, self.nick, Default);
+	cacheInsert(true, Network, self.nick);
 	if (self.mode) ircFormat("MODE %s %s\r\n", self.nick, self.mode);
 	if (self.join) {
 		uint count = 1;
@@ -278,7 +278,7 @@ static void handleReplyWelcome(struct Message *msg) {
 		replies[ReplyTopicAuto] += count;
 		replies[ReplyNamesAuto] += count;
 	}
-	commandCompleteAdd();
+	commandCache();
 	handleReplyGeneric(msg);
 }
 
@@ -372,13 +372,13 @@ static void handleJoin(struct Message *msg) {
 			set(&self.host, msg->host);
 		}
 		idColors[id] = hash(msg->params[0]);
-		completeTouch(None, msg->params[0], idColors[id]);
+		cacheInsertColor(true, None, msg->params[0], idColors[id]);
 		if (replies[ReplyJoin]) {
 			windowShow(windowFor(id));
 			replies[ReplyJoin]--;
 		}
 	}
-	completeTouch(id, msg->nick, hash(msg->user));
+	cacheInsertColor(true, id, msg->nick, hash(msg->user));
 	if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) {
 		msg->params[2] = NULL;
 	}
@@ -410,9 +410,9 @@ static void handlePart(struct Message *msg) {
 	require(msg, true, 1);
 	uint id = idFor(msg->params[0]);
 	if (!strcmp(msg->nick, self.nick)) {
-		completeClear(id);
+		cacheClear(id);
 	}
-	completeRemove(id, msg->nick);
+	cacheRemove(id, msg->nick);
 	enum Heat heat = filterCheck(Cold, id, msg);
 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
 	uiFormat(
@@ -432,14 +432,14 @@ static void handleKick(struct Message *msg) {
 	require(msg, true, 2);
 	uint id = idFor(msg->params[0]);
 	bool kicked = !strcmp(msg->params[1], self.nick);
-	completeTouch(id, msg->nick, hash(msg->user));
+	cacheInsertColor(true, id, msg->nick, hash(msg->user));
 	urlScan(id, msg->nick, msg->params[2]);
 	uiFormat(
 		id, (kicked ? Hot : Cold), tagTime(msg),
 		"%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s",
 		(kicked ? "\26" : ""),
 		hash(msg->user), msg->nick,
-		completeColor(id, msg->params[1]), msg->params[1],
+		cacheColor(id, msg->params[1]), msg->params[1],
 		hash(msg->params[0]), msg->params[0],
 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
 	);
@@ -448,8 +448,8 @@ static void handleKick(struct Message *msg) {
 		msg->nick, msg->params[1], msg->params[0],
 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
 	);
-	completeRemove(id, msg->params[1]);
-	if (kicked) completeClear(id);
+	cacheRemove(id, msg->params[1]);
+	if (kicked) cacheClear(id);
 }
 
 static void handleNick(struct Message *msg) {
@@ -458,7 +458,8 @@ static void handleNick(struct Message *msg) {
 		set(&self.nick, msg->params[0]);
 		inputUpdate();
 	}
-	for (uint id; (id = completeID(msg->nick));) {
+	struct Cursor curs = {0};
+	for (uint id; (id = cacheID(&curs, msg->nick));) {
 		if (!strcmp(idNames[id], msg->nick)) {
 			set(&idNames[id], msg->params[0]);
 		}
@@ -473,12 +474,13 @@ static void handleNick(struct Message *msg) {
 			msg->nick, msg->params[0]
 		);
 	}
-	completeReplace(None, msg->nick, msg->params[0]);
+	cacheReplace(true, msg->nick, msg->params[0]);
 }
 
 static void handleSetname(struct Message *msg) {
 	require(msg, true, 1);
-	for (uint id; (id = completeID(msg->nick));) {
+	struct Cursor curs = {0};
+	for (uint id; (id = cacheID(&curs, msg->nick));) {
 		uiFormat(
 			id, filterCheck(Cold, id, msg), tagTime(msg),
 			"\3%02d%s\3\tis now known as \3%02d%s\3 (%s\17)",
@@ -490,7 +492,8 @@ static void handleSetname(struct Message *msg) {
 
 static void handleQuit(struct Message *msg) {
 	require(msg, true, 0);
-	for (uint id; (id = completeID(msg->nick));) {
+	struct Cursor curs = {0};
+	for (uint id; (id = cacheID(&curs, msg->nick));) {
 		enum Heat heat = filterCheck(Cold, id, msg);
 		if (heat > Ice) urlScan(id, msg->nick, msg->params[0]);
 		uiFormat(
@@ -506,7 +509,7 @@ static void handleQuit(struct Message *msg) {
 			(msg->params[0] ? ": " : ""), (msg->params[0] ?: "")
 		);
 	}
-	completeRemove(None, msg->nick);
+	cacheRemove(None, msg->nick);
 }
 
 static void handleInvite(struct Message *msg) {
@@ -552,7 +555,7 @@ static void handleErrorUserOnChannel(struct Message *msg) {
 	uiFormat(
 		id, Warm, tagTime(msg),
 		"\3%02d%s\3 is already in \3%02d%s\3",
-		completeColor(id, msg->params[1]), msg->params[1],
+		cacheColor(id, msg->params[1]), msg->params[1],
 		hash(msg->params[2]), msg->params[2]
 	);
 }
@@ -568,7 +571,7 @@ static void handleReplyNames(struct Message *msg) {
 		char *nick = &prefixes[strspn(prefixes, network.prefixes)];
 		char *user = strsep(&name, "@");
 		enum Color color = (user ? hash(user) : Default);
-		completeAdd(id, nick, color);
+		cacheInsertColor(false, id, nick, color);
 		if (!replies[ReplyNames] && !replies[ReplyNamesAuto]) continue;
 		ptr = seprintf(
 			ptr, end, "%s\3%02d%s\3", (ptr > buf ? ", " : ""), color, prefixes
@@ -635,23 +638,24 @@ static void handleReplyNoTopic(struct Message *msg) {
 	);
 }
 
-static void topicComplete(uint id, const char *topic) {
+static void topicCache(uint id, const char *topic) {
 	char buf[512];
-	const char *prev = complete(id, "/topic ");
+	struct Cursor curs = {0};
+	const char *prev = cachePrefix(&curs, id, "/topic ");
 	if (prev) {
 		snprintf(buf, sizeof(buf), "%s", prev);
-		completeRemove(id, buf);
+		cacheRemove(id, buf);
 	}
 	if (topic) {
 		snprintf(buf, sizeof(buf), "/topic %s", topic);
-		completeAdd(id, buf, Default);
+		cacheInsert(false, id, buf);
 	}
 }
 
 static void handleReplyTopic(struct Message *msg) {
 	require(msg, false, 3);
 	uint id = idFor(msg->params[1]);
-	topicComplete(id, msg->params[2]);
+	topicCache(id, msg->params[2]);
 	if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return;
 	urlScan(id, NULL, msg->params[2]);
 	uiFormat(
@@ -702,7 +706,7 @@ static void handleTopic(struct Message *msg) {
 	require(msg, true, 2);
 	uint id = idFor(msg->params[0]);
 	if (!msg->params[1][0]) {
-		topicComplete(id, NULL);
+		topicCache(id, NULL);
 		uiFormat(
 			id, Warm, tagTime(msg),
 			"\3%02d%s\3\tremoves the sign in \3%02d%s\3",
@@ -715,8 +719,8 @@ static void handleTopic(struct Message *msg) {
 		return;
 	}
 
-	const char *prev = complete(id, "/topic ");
-	completeReject();
+	struct Cursor curs = {0};
+	const char *prev = cachePrefix(&curs, id, "/topic ");
 	if (prev) {
 		prev += 7;
 	} else {
@@ -766,7 +770,7 @@ log:
 		id, tagTime(msg), "%s places a new sign in %s: %s",
 		msg->nick, msg->params[0], msg->params[1]
 	);
-	topicComplete(id, msg->params[1]);
+	topicCache(id, msg->params[1]);
 	urlScan(id, msg->nick, msg->params[1]);
 }
 
@@ -893,7 +897,7 @@ static void handleMode(struct Message *msg) {
 				id, Cold, tagTime(msg),
 				"\3%02d%s\3\t%s \3%02d%c%s\3 %s%s in \3%02d%s\3",
 				hash(msg->user), msg->nick, verb,
-				completeColor(id, nick), prefix, nick,
+				cacheColor(id, nick), prefix, nick,
 				mode, name, hash(msg->params[0]), msg->params[0]
 			);
 			logFormat(
@@ -1031,7 +1035,7 @@ static void handleReplyBanList(struct Message *msg) {
 			id, Warm, tagTime(msg),
 			"Banned from \3%02d%s\3 since %s by \3%02d%s\3: %s",
 			hash(msg->params[1]), msg->params[1],
-			since, completeColor(id, msg->params[3]), msg->params[3],
+			since, cacheColor(id, msg->params[3]), msg->params[3],
 			msg->params[2]
 		);
 	} else {
@@ -1054,7 +1058,7 @@ static void onList(const char *list, struct Message *msg) {
 			id, Warm, tagTime(msg),
 			"On the \3%02d%s\3 %s list since %s by \3%02d%s\3: %s",
 			hash(msg->params[1]), msg->params[1], list,
-			since, completeColor(id, msg->params[3]), msg->params[3],
+			since, cacheColor(id, msg->params[3]), msg->params[3],
 			msg->params[2]
 		);
 	} else {
@@ -1087,7 +1091,7 @@ static void handleReplyList(struct Message *msg) {
 
 static void handleReplyWhoisUser(struct Message *msg) {
 	require(msg, false, 6);
-	completeTouch(Network, msg->params[1], hash(msg->params[2]));
+	cacheInsertColor(true, Network, msg->params[1], hash(msg->params[2]));
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\tis %s!%s@%s (%s\17)",
@@ -1102,7 +1106,7 @@ static void handleReplyWhoisServer(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\t%s connected to %s (%s)",
-		completeColor(Network, msg->params[1]), msg->params[1],
+		cacheColor(Network, msg->params[1]), msg->params[1],
 		(replies[ReplyWhowas] ? "was" : "is"), msg->params[2], msg->params[3]
 	);
 }
@@ -1126,7 +1130,7 @@ static void handleReplyWhoisIdle(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\tis idle for %lu %s%s%s%s",
-		completeColor(Network, msg->params[1]), msg->params[1],
+		cacheColor(Network, msg->params[1]), msg->params[1],
 		idle, unit, (idle != 1 ? "s" : ""),
 		(msg->params[3] ? ", signed on " : ""), (msg->params[3] ? signon : "")
 	);
@@ -1148,7 +1152,7 @@ static void handleReplyWhoisChannels(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\tis in %s",
-		completeColor(Network, msg->params[1]), msg->params[1], buf
+		cacheColor(Network, msg->params[1]), msg->params[1], buf
 	);
 }
 
@@ -1162,7 +1166,7 @@ static void handleReplyWhoisGeneric(struct Message *msg) {
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\t%s%s%s",
-		completeColor(Network, msg->params[1]), msg->params[1],
+		cacheColor(Network, msg->params[1]), msg->params[1],
 		msg->params[2], (msg->params[3] ? " " : ""), (msg->params[3] ?: "")
 	);
 }
@@ -1170,13 +1174,13 @@ static void handleReplyWhoisGeneric(struct Message *msg) {
 static void handleReplyEndOfWhois(struct Message *msg) {
 	require(msg, false, 2);
 	if (strcmp(msg->params[1], self.nick)) {
-		completeRemove(Network, msg->params[1]);
+		cacheRemove(Network, msg->params[1]);
 	}
 }
 
 static void handleReplyWhowasUser(struct Message *msg) {
 	require(msg, false, 6);
-	completeTouch(Network, msg->params[1], hash(msg->params[2]));
+	cacheInsertColor(true, Network, msg->params[1], hash(msg->params[2]));
 	uiFormat(
 		Network, Warm, tagTime(msg),
 		"\3%02d%s\3\twas %s!%s@%s (%s)",
@@ -1188,7 +1192,7 @@ static void handleReplyWhowasUser(struct Message *msg) {
 static void handleReplyEndOfWhowas(struct Message *msg) {
 	require(msg, false, 2);
 	if (strcmp(msg->params[1], self.nick)) {
-		completeRemove(Network, msg->params[1]);
+		cacheRemove(Network, msg->params[1]);
 	}
 }
 
@@ -1196,7 +1200,7 @@ static void handleReplyAway(struct Message *msg) {
 	require(msg, false, 3);
 	// Might be part of a WHOIS response.
 	uint id;
-	if (completeColor(Network, msg->params[1]) != Default) {
+	if (cacheColor(Network, msg->params[1]) != Default) {
 		id = Network;
 	} else {
 		id = idFor(msg->params[1]);
@@ -1204,7 +1208,7 @@ static void handleReplyAway(struct Message *msg) {
 	uiFormat(
 		id, (id == Network ? Warm : Cold), tagTime(msg),
 		"\3%02d%s\3\tis away: %s",
-		completeColor(id, msg->params[1]), msg->params[1], msg->params[2]
+		cacheColor(id, msg->params[1]), msg->params[1], msg->params[2]
 	);
 	logFormat(
 		id, tagTime(msg), "%s is away: %s",
@@ -1275,7 +1279,7 @@ static char *colorMentions(char *ptr, char *end, uint id, const char *msg) {
 
 		size_t len = strcspn(msg, ",:<> ");
 		char *p = seprintf(ptr, end, "%.*s", (int)len, msg);
-		enum Color color = completeColor(id, ptr);
+		enum Color color = cacheColor(id, ptr);
 		if (color != Default) {
 			ptr = seprintf(ptr, end, "\3%02d%.*s\3", color, (int)len, msg);
 		} else {
@@ -1315,7 +1319,7 @@ static void handlePrivmsg(struct Message *msg) {
 	heat = filterCheck(heat, id, msg);
 	if (heat > Warm && !mine && !query) highlight = true;
 	if (!notice && !mine && heat > Ice) {
-		completeTouch(id, msg->nick, hash(msg->user));
+		cacheInsertColor(true, id, msg->nick, hash(msg->user));
 	}
 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
 
diff --git a/input.c b/input.c
index 87a4eb9..739d889 100644
--- a/input.c
+++ b/input.c
@@ -261,12 +261,12 @@ static const struct {
 	{ L"\\wave", L"ヾ(^∇^)" },
 };
 
-void inputCompleteAdd(void) {
+void inputCache(void) {
 	char mbs[256];
 	for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) {
 		size_t n = wcstombs(mbs, Macros[i].name, sizeof(mbs));
 		assert(n != (size_t)-1);
-		completeAdd(None, mbs, Default);
+		cacheInsert(false, None, mbs);
 	}
 }
 
@@ -296,15 +296,16 @@ static struct {
 	size_t pos;
 	size_t len;
 	bool suffix;
+	struct Cursor curs;
 } tab;
 
 static void tabAccept(void) {
-	completeAccept();
+	cacheAccept(&tab.curs);
 	tab.len = 0;
 }
 
 static void tabReject(void) {
-	completeReject();
+	cacheReject(&tab.curs);
 	tab.len = 0;
 }
 
@@ -332,9 +333,9 @@ static int tabComplete(struct Edit *e, uint id) {
 		tab.suffix = true;
 	}
 
-	const char *comp = complete(id, tab.pre);
+	const char *comp = cachePrefix(&tab.curs, id, tab.pre);
 	if (!comp) {
-		comp = complete(id, tab.pre);
+		comp = cachePrefix(&tab.curs, id, tab.pre);
 		tab.suffix ^= true;
 	}
 	if (!comp) {
diff --git a/window.c b/window.c
index ee0911f..72a3571 100644
--- a/window.c
+++ b/window.c
@@ -93,7 +93,7 @@ static struct Window *windowRemove(uint num) {
 }
 
 static void windowFree(struct Window *window) {
-	completeRemove(None, idNames[window->id]);
+	cacheRemove(None, idNames[window->id]);
 	bufferFree(window->buffer);
 	free(window);
 }
@@ -118,7 +118,7 @@ uint windowFor(uint id) {
 		window->thresh = windowThreshold;
 	}
 	window->buffer = bufferAlloc();
-	completeAdd(None, idNames[id], idColors[id]);
+	cacheInsertColor(false, None, idNames[id], idColors[id]);
 
 	return windowPush(window);
 }
@@ -477,7 +477,7 @@ void windowClose(uint num) {
 	if (num >= count) return;
 	if (windows[num]->id == Network) return;
 	struct Window *window = windowRemove(num);
-	completeClear(window->id);
+	cacheClear(window->id);
 	windowFree(window);
 	if (swap >= num) swap--;
 	if (show == num) {