summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Makefile2
-rw-r--r--chat.c1
-rw-r--r--chat.h25
-rw-r--r--edit.c452
-rw-r--r--edit.h91
-rw-r--r--ui.c88
6 files changed, 358 insertions, 301 deletions
diff --git a/Makefile b/Makefile
index 83ead95..9e6392b 100644
--- a/Makefile
+++ b/Makefile
@@ -32,7 +32,7 @@ all: catgirl
 catgirl: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
 
-${OBJS}: chat.h
+${OBJS}: chat.h edit.h
 
 tags: *.[ch]
 	ctags -w *.[ch]
diff --git a/chat.c b/chat.c
index 4898411..454ae31 100644
--- a/chat.c
+++ b/chat.c
@@ -371,7 +371,6 @@ int main(int argc, char *argv[]) {
 	set(&network.name, host);
 	set(&self.nick, "*");
 
-	editCompleteAdd();
 	commandCompleteAdd();
 
 	ircConfig(insecure, trust, cert, priv);
diff --git a/chat.h b/chat.h
index 753d1a3..8a9a48f 100644
--- a/chat.h
+++ b/chat.h
@@ -341,31 +341,6 @@ int bufferReflow(
 	struct Buffer *buffer, int cols, enum Heat thresh, size_t tail
 );
 
-enum Edit {
-	EditHead,
-	EditTail,
-	EditPrev,
-	EditNext,
-	EditPrevWord,
-	EditNextWord,
-	EditDeleteHead,
-	EditDeleteTail,
-	EditDeletePrev,
-	EditDeleteNext,
-	EditDeletePrevWord,
-	EditDeleteNextWord,
-	EditPaste,
-	EditTranspose,
-	EditCollapse,
-	EditInsert,
-	EditComplete,
-	EditExpand,
-	EditEnter,
-};
-void edit(uint id, enum Edit op, wchar_t ch);
-char *editBuffer(size_t *pos);
-void editCompleteAdd(void);
-
 const char *complete(uint id, const char *prefix);
 const char *completeSubstr(uint id, const char *substr);
 void completeAccept(void);
diff --git a/edit.c b/edit.c
index 66b5302..21061d7 100644
--- a/edit.c
+++ b/edit.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2020, 2022  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
@@ -13,8 +13,6 @@
  * 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
@@ -25,282 +23,272 @@
  * covered work.
  */
 
-#include <assert.h>
+#include <errno.h>
 #include <limits.h>
 #include <stdbool.h>
-#include <stdio.h>
 #include <stdlib.h>
 #include <wchar.h>
 #include <wctype.h>
 
-#include "chat.h"
+#include "edit.h"
+
+static bool isword(wchar_t ch) {
+	return !iswspace(ch) && !iswpunct(ch);
+}
 
-enum { Cap = 1024 };
-static wchar_t buf[Cap];
-static size_t len;
-static size_t pos;
+void editFree(struct Edit *e) {
+	free(e->buf);
+	free(e->cut.buf);
+	free(e->mbs.buf);
+	e->pos = e->len = e->cap = 0;
+	e->cut.len = 0;
+	e->mbs.pos = e->mbs.len = 0;
+}
 
-char *editBuffer(size_t *mbsPos) {
-	static char mbs[MB_LEN_MAX * Cap];
+char *editString(struct Edit *e) {
+	size_t cap = e->len * MB_CUR_MAX + 1;
+	char *buf = realloc(e->mbs.buf, cap);
+	if (!buf) return NULL;
+	e->mbs.buf = buf;
 
-	const wchar_t *ptr = buf;
-	size_t mbsLen = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL);
-	assert(mbsLen != (size_t)-1);
-	if (mbsPos) *mbsPos = mbsLen;
+	const wchar_t *ptr = e->buf;
+	e->mbs.len = wcsnrtombs(e->mbs.buf, &ptr, e->pos, cap-1, NULL);
+	if (e->mbs.len == (size_t)-1) return NULL;
+	e->mbs.pos = e->mbs.len;
 
-	ptr = &buf[pos];
+	ptr = &e->buf[e->pos];
 	size_t n = wcsnrtombs(
-		&mbs[mbsLen], &ptr, len - pos, sizeof(mbs) - mbsLen - 1, NULL
+		&e->mbs.buf[e->mbs.len], &ptr, e->len - e->pos,
+		cap-1 - e->mbs.len, NULL
 	);
-	assert(n != (size_t)-1);
-	mbsLen += n;
-
-	mbs[mbsLen] = '\0';
-	return mbs;
-}
-
-static struct {
-	wchar_t buf[Cap];
-	size_t len;
-} cut;
+	if (n == (size_t)-1) return NULL;
+	e->mbs.len += n;
 
-static bool reserve(size_t index, size_t count) {
-	if (len + count > Cap) return false;
-	wmemmove(&buf[index + count], &buf[index], len - index);
-	len += count;
-	return true;
+	e->mbs.buf[e->mbs.len] = '\0';
+	return e->mbs.buf;
 }
 
-static void delete(bool copy, size_t index, size_t count) {
-	if (index + count > len) return;
-	if (copy) {
-		wmemcpy(cut.buf, &buf[index], count);
-		cut.len = count;
+int editReserve(struct Edit *e, size_t index, size_t count) {
+	if (index > e->len) {
+		errno = EINVAL;
+		return -1;
 	}
-	wmemmove(&buf[index], &buf[index + count], len - index - count);
-	len -= count;
-}
-
-static const struct {
-	const wchar_t *name;
-	const wchar_t *string;
-} Macros[] = {
-	{ L"\\banhammer", L"▬▬▬▬▬▬▬▋ Ò╭╮Ó" },
-	{ L"\\bear", L"ʕっ•ᴥ•ʔっ" },
-	{ L"\\blush", L"(˶′◡‵˶)" },
-	{ L"\\com", L"\0038,4\2 ☭ " },
-	{ L"\\cool", L"(⌐■_■)" },
-	{ L"\\flip", L"(╯°□°)╯︵ ┻━┻" },
-	{ L"\\gary", L"ᕕ( ᐛ )ᕗ" },
-	{ L"\\hug", L"(っ・∀・)っ" },
-	{ L"\\lenny", L"( ͡° ͜ʖ ͡°)" },
-	{ L"\\look", L"ಠ_ಠ" },
-	{ L"\\shrug", L"¯\\_(ツ)_/¯" },
-	{ L"\\unflip", L"┬─┬ノ(º_ºノ)" },
-	{ L"\\wave", L"ヾ(^∇^)" },
-};
-
-void editCompleteAdd(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);
+	if (e->len + count > e->cap) {
+		size_t cap = (e->cap ? e->cap * 2 : 256);
+		wchar_t *buf = realloc(e->buf, sizeof(*buf) * cap);
+		if (!buf) return -1;
+		e->buf = buf;
+		e->cap = cap;
 	}
+	wmemmove(&e->buf[index + count], &e->buf[index], e->len - index);
+	e->len += count;
+	return 0;
 }
 
-static void macroExpand(void) {
-	size_t macro = pos;
-	while (macro && buf[macro] != L'\\') macro--;
-	if (macro == pos) return;
-	for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) {
-		if (wcsncmp(Macros[i].name, &buf[macro], pos - macro)) continue;
-		if (wcstombs(NULL, Macros[i].string, 0) == (size_t)-1) continue;
-		delete(false, macro, pos - macro);
-		pos = macro;
-		size_t expand = wcslen(Macros[i].string);
-		if (reserve(macro, expand)) {
-			wcsncpy(&buf[macro], Macros[i].string, expand);
-			pos += expand;
-		}
+int editCopy(struct Edit *e, size_t index, size_t count) {
+	if (index + count > e->len) {
+		errno = EINVAL;
+		return -1;
 	}
+	wchar_t *buf = realloc(e->cut.buf, sizeof(*buf) * count);
+	if (!buf) return -1;
+	e->cut.buf = buf;
+	wmemcpy(e->cut.buf, &e->buf[index], count);
+	e->cut.len = count;
+	return 0;
 }
 
-static struct {
-	size_t pos;
-	size_t pre;
-	size_t len;
-	bool suffix;
-} tab;
-
-static void tabComplete(uint id) {
-	if (!tab.len) {
-		tab.pos = pos;
-		while (tab.pos && !iswspace(buf[tab.pos - 1])) tab.pos--;
-		if (tab.pos == pos) return;
-		tab.pre = pos - tab.pos;
-		tab.len = tab.pre;
-		tab.suffix = true;
-	}
-
-	char mbs[MB_LEN_MAX * Cap];
-	const wchar_t *ptr = &buf[tab.pos];
-	size_t n = wcsnrtombs(mbs, &ptr, tab.pre, sizeof(mbs) - 1, NULL);
-	assert(n != (size_t)-1);
-	mbs[n] = '\0';
-
-	const char *comp = complete(id, mbs);
-	if (!comp) {
-		comp = complete(id, mbs);
-		tab.suffix ^= true;
-	}
-	if (!comp) {
-		tab.len = 0;
-		return;
-	}
-
-	wchar_t wcs[Cap];
-	n = mbstowcs(wcs, comp, Cap);
-	assert(n != (size_t)-1);
-	if (tab.pos + n + 2 > Cap) {
-		completeReject();
-		tab.len = 0;
-		return;
-	}
-
-	bool colon = (tab.len >= 2 && buf[tab.pos + tab.len - 2] == L':');
-
-	delete(false, tab.pos, tab.len);
-	tab.len = n;
-	if (wcs[0] == L'\\' || wcschr(wcs, L' ')) {
-		reserve(tab.pos, tab.len);
-	} else if (wcs[0] != L'/' && tab.suffix && (!tab.pos || colon)) {
-		tab.len += 2;
-		reserve(tab.pos, tab.len);
-		buf[tab.pos + n + 0] = L':';
-		buf[tab.pos + n + 1] = L' ';
-	} else if (tab.suffix && tab.pos >= 2 && buf[tab.pos - 2] == L':') {
-		tab.len += 2;
-		reserve(tab.pos, tab.len);
-		buf[tab.pos - 2] = L',';
-		buf[tab.pos + n + 0] = L':';
-		buf[tab.pos + n + 1] = L' ';
-	} else {
-		tab.len++;
-		reserve(tab.pos, tab.len);
-		if (!tab.suffix && tab.pos >= 2 && buf[tab.pos - 2] == L',') {
-			buf[tab.pos - 2] = L':';
-		}
-		buf[tab.pos + n] = L' ';
+int editDelete(struct Edit *e, bool cut, size_t index, size_t count) {
+	if (index + count > e->len) {
+		errno = EINVAL;
+		return -1;
 	}
-	wmemcpy(&buf[tab.pos], wcs, n);
-	pos = tab.pos + tab.len;
-}
-
-static void tabAccept(void) {
-	completeAccept();
-	tab.len = 0;
-}
-
-static void tabReject(void) {
-	completeReject();
-	tab.len = 0;
-}
-
-static bool isword(wchar_t ch) {
-	return !iswspace(ch) && !iswpunct(ch);
+	if (cut && editCopy(e, index, count) < 0) return -1;
+	wmemmove(&e->buf[index], &e->buf[index + count], e->len - index - count);
+	e->len -= count;
+	if (e->pos > e->len) e->pos = e->len;
+	return 0;
 }
 
-void edit(uint id, enum Edit op, wchar_t ch) {
-	size_t init = pos;
-	switch (op) {
-		break; case EditHead: pos = 0;
-		break; case EditTail: pos = len;
-		break; case EditPrev: if (pos) pos--;
-		break; case EditNext: if (pos < len) pos++;
+int editFn(struct Edit *e, enum EditFn fn) {
+	int ret = 0;
+	switch (fn) {
+		break; case EditHead: e->pos = 0;
+		break; case EditTail: e->pos = e->len;
+		break; case EditPrev: if (e->pos) e->pos--;
+		break; case EditNext: if (e->pos < e->len) e->pos++;
 		break; case EditPrevWord: {
-			while (pos && !isword(buf[pos - 1])) pos--;
-			while (pos && isword(buf[pos - 1])) pos--;
+			while (e->pos && !isword(e->buf[e->pos-1])) e->pos--;
+			while (e->pos && isword(e->buf[e->pos-1])) e->pos--;
 		}
 		break; case EditNextWord: {
-			while (pos < len && isword(buf[pos])) pos++;
-			while (pos < len && !isword(buf[pos])) pos++;
+			while (e->pos < e->len && isword(e->buf[e->pos])) e->pos++;
+			while (e->pos < e->len && !isword(e->buf[e->pos])) e->pos++;
 		}
 
-		break; case EditDeleteHead: delete(true, 0, pos); pos = 0;
-		break; case EditDeleteTail: delete(true, pos, len - pos);
-		break; case EditDeletePrev: if (pos) delete(false, --pos, 1);
-		break; case EditDeleteNext: delete(false, pos, 1);
+		break; case EditDeleteHead: {
+			ret = editDelete(e, true, 0, e->pos);
+			e->pos = 0;
+		}
+		break; case EditDeleteTail: {
+			ret = editDelete(e, true, e->pos, e->len - e->pos);
+		}
+		break; case EditDeletePrev: {
+			if (e->pos) editDelete(e, false, --e->pos, 1);
+		}
+		break; case EditDeleteNext: {
+			editDelete(e, false, e->pos, 1);
+		}
 		break; case EditDeletePrevWord: {
-			if (!pos) break;
-			size_t word = pos;
-			while (word && !isword(buf[word - 1])) word--;
-			while (word && isword(buf[word - 1])) word--;
-			delete(true, word, pos - word);
-			pos = word;
+			if (!e->pos) break;
+			size_t word = e->pos;
+			while (word && !isword(e->buf[word-1])) word--;
+			while (word && isword(e->buf[word-1])) word--;
+			ret = editDelete(e, true, word, e->pos - word);
+			e->pos = word;
 		}
 		break; case EditDeleteNextWord: {
-			if (pos == len) break;
-			size_t word = pos;
-			while (word < len && !isword(buf[word])) word++;
-			while (word < len && isword(buf[word])) word++;
-			delete(true, pos, word - pos);
+			if (e->pos == e->len) break;
+			size_t word = e->pos;
+			while (word < e->len && !isword(e->buf[word])) word++;
+			while (word < e->len && isword(e->buf[word])) word++;
+			ret = editDelete(e, true, e->pos, word - e->pos);
 		}
+
 		break; case EditPaste: {
-			if (reserve(pos, cut.len)) {
-				wmemcpy(&buf[pos], cut.buf, cut.len);
-				pos += cut.len;
+			ret = editReserve(e, e->pos, e->cut.len);
+			if (ret == 0) {
+				wmemcpy(&e->buf[e->pos], e->cut.buf, e->cut.len);
+				e->pos += e->cut.len;
 			}
 		}
-
 		break; case EditTranspose: {
-			if (!pos || len < 2) break;
-			if (pos == len) pos--;
-			wchar_t t = buf[pos - 1];
-			buf[pos - 1] = buf[pos];
-			buf[pos++] = t;
+			if (e->len < 2) break;
+			if (!e->pos) e->pos++;
+			if (e->pos == e->len) e->pos--;
+			wchar_t x = e->buf[e->pos-1];
+			e->buf[e->pos-1] = e->buf[e->pos];
+			e->buf[e->pos++] = x;
 		}
 		break; case EditCollapse: {
 			size_t ws;
-			for (pos = 0; pos < len;) {
-				for (; pos < len && !iswspace(buf[pos]); ++pos);
-				for (ws = pos; ws < len && iswspace(buf[ws]); ++ws);
-				if (pos && ws < len) {
-					delete(false, pos, ws - pos - 1);
-					buf[pos++] = L' ';
+			for (e->pos = 0; e->pos < e->len;) {
+				for (; e->pos < e->len && !iswspace(e->buf[e->pos]); ++e->pos);
+				for (ws = e->pos; ws < e->len && iswspace(e->buf[ws]); ++ws);
+				if (e->pos && ws < e->len) {
+					editDelete(e, false, e->pos, ws - e->pos - 1);
+					e->buf[e->pos++] = L' ';
 				} else {
-					delete(false, pos, ws - pos);
+					editDelete(e, false, e->pos, ws - e->pos);
 				}
 			}
 		}
 
-		break; case EditInsert: {
-			char mb[MB_LEN_MAX];
-			if (wctomb(mb, ch) < 0) return;
-			if (reserve(pos, 1)) {
-				buf[pos++] = ch;
-			}
-		}
-		break; case EditComplete: {
-			tabComplete(id);
-			return;
-		}
-		break; case EditExpand: {
-			macroExpand();
-			tabAccept();
-			return;
-		}
-		break; case EditEnter: {
-			tabAccept();
-			command(id, editBuffer(NULL));
-			len = pos = 0;
-			return;
-		}
+		break; case EditClear: e->len = e->pos = 0;
 	}
+	return ret;
+}
+
+int editInsert(struct Edit *e, wchar_t ch) {
+	char mb[MB_LEN_MAX];
+	if (wctomb(mb, ch) < 0) return -1;
+	if (editReserve(e, e->pos, 1) < 0) return -1;
+	e->buf[e->pos++] = ch;
+	return 0;
+}
+
+#ifdef TEST
+#undef NDEBUG
+#include <assert.h>
+#include <string.h>
 
-	if (pos < init) {
-		tabReject();
-	} else {
-		tabAccept();
+static void fix(struct Edit *e, const char *str) {
+	editFn(e, EditClear);
+	for (const char *ch = str; *ch; ++ch) {
+		editInsert(e, (wchar_t)*ch);
 	}
 }
+
+static bool eq(struct Edit *e, const char *str1) {
+	const char *str2 = &str1[strlen(str1) + 1];
+	const char *buf = editString(e);
+	return e->mbs.pos == strlen(str1)
+		&& !strncmp(buf, str1, e->mbs.pos)
+		&& !strcmp(&buf[e->mbs.pos], str2);
+}
+
+int main(void) {
+	struct Edit e = { .mode = EditEmacs };
+
+	fix(&e, "foo bar");
+	editFn(&e, EditHead);
+	assert(eq(&e, "\0foo bar"));
+	editFn(&e, EditTail);
+	assert(eq(&e, "foo bar\0"));
+	editFn(&e, EditPrev);
+	assert(eq(&e, "foo ba\0r"));
+	editFn(&e, EditNext);
+	assert(eq(&e, "foo bar\0"));
+
+	fix(&e, "foo, bar");
+	editFn(&e, EditPrevWord);
+	assert(eq(&e, "foo, \0bar"));
+	editFn(&e, EditPrevWord);
+	assert(eq(&e, "\0foo, bar"));
+	editFn(&e, EditNextWord);
+	assert(eq(&e, "foo, \0bar"));
+	editFn(&e, EditNextWord);
+	assert(eq(&e, "foo, bar\0"));
+
+	fix(&e, "foo bar");
+	editFn(&e, EditPrevWord);
+	editFn(&e, EditDeleteHead);
+	assert(eq(&e, "\0bar"));
+
+	fix(&e, "foo bar");
+	editFn(&e, EditPrevWord);
+	editFn(&e, EditDeleteTail);
+	assert(eq(&e, "foo \0"));
+
+	fix(&e, "foo bar");
+	editFn(&e, EditDeletePrev);
+	assert(eq(&e, "foo ba\0"));
+	editFn(&e, EditHead);
+	editFn(&e, EditDeleteNext);
+	assert(eq(&e, "\0oo ba"));
+
+	fix(&e, "foo, bar");
+	editFn(&e, EditDeletePrevWord);
+	assert(eq(&e, "foo, \0"));
+	editFn(&e, EditDeletePrevWord);
+	assert(eq(&e, "\0"));
+
+	fix(&e, "foo, bar");
+	editFn(&e, EditHead);
+	editFn(&e, EditDeleteNextWord);
+	assert(eq(&e, "\0, bar"));
+	editFn(&e, EditDeleteNextWord);
+	assert(eq(&e, "\0"));
+
+	fix(&e, "foo bar");
+	editFn(&e, EditDeletePrevWord);
+	editFn(&e, EditPaste);
+	assert(eq(&e, "foo bar\0"));
+	editFn(&e, EditPaste);
+	assert(eq(&e, "foo barbar\0"));
+
+	fix(&e, "bar");
+	editFn(&e, EditTranspose);
+	assert(eq(&e, "bra\0"));
+	editFn(&e, EditHead);
+	editFn(&e, EditTranspose);
+	assert(eq(&e, "rb\0a"));
+	editFn(&e, EditTranspose);
+	assert(eq(&e, "rab\0"));
+
+	fix(&e, "  foo  bar  ");
+	editFn(&e, EditCollapse);
+	assert(eq(&e, "foo bar\0"));
+}
+
+#endif /* TEST */
diff --git a/edit.h b/edit.h
new file mode 100644
index 0000000..57edfc1
--- /dev/null
+++ b/edit.h
@@ -0,0 +1,91 @@
+/* Copyright (C) 2022  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/>.
+ *
+ * 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 <stdbool.h>
+#include <stddef.h>
+
+enum EditMode {
+	EditEmacs,
+};
+
+struct Edit {
+	enum EditMode mode;
+	wchar_t *buf;
+	size_t pos;
+	size_t len;
+	size_t cap;
+	struct {
+		wchar_t *buf;
+		size_t len;
+	} cut;
+	struct {
+		char *buf;
+		size_t pos;
+		size_t len;
+	} mbs;
+};
+
+enum EditFn {
+	EditHead,
+	EditTail,
+	EditPrev,
+	EditNext,
+	EditPrevWord,
+	EditNextWord,
+	EditDeleteHead,
+	EditDeleteTail,
+	EditDeletePrev,
+	EditDeleteNext,
+	EditDeletePrevWord,
+	EditDeleteNextWord,
+	EditPaste,
+	EditTranspose,
+	EditCollapse,
+	EditClear,
+};
+
+// Perform an editing function.
+int editFn(struct Edit *e, enum EditFn fn);
+
+// Perform a vi-mode editing function.
+int editVi(struct Edit *e, wchar_t ch);
+
+// Insert a character at the cursor.
+int editInsert(struct Edit *e, wchar_t ch);
+
+// Convert the buffer to a multi-byte string stored in e->mbs.
+char *editString(struct Edit *e);
+
+// Free all buffers.
+void editFree(struct Edit *e);
+
+// Reserve a range in the buffer.
+int editReserve(struct Edit *e, size_t index, size_t count);
+
+// Copy a range of the buffer into e->cut.
+int editCopy(struct Edit *e, size_t index, size_t count);
+
+// Delete a range from the buffer. If cut is true, copy the deleted portion.
+int editDelete(struct Edit *e, bool cut, size_t index, size_t count);
diff --git a/ui.c b/ui.c
index 3df0cd6..212d205 100644
--- a/ui.c
+++ b/ui.c
@@ -54,6 +54,7 @@
 #endif
 
 #include "chat.h"
+#include "edit.h"
 
 // Annoying stuff from <term.h>:
 #undef lines
@@ -752,9 +753,10 @@ static char *inputStop(
 	return stop;
 }
 
+static struct Edit edit;
+
 static void inputUpdate(void) {
-	size_t pos;
-	char *buf = editBuffer(&pos);
+	char *buf = editString(&edit);
 	struct Window *window = windows.ptrs[windows.show];
 
 	const char *prefix = "";
@@ -786,7 +788,7 @@ static void inputUpdate(void) {
 	} else {
 		prompt = "";
 	}
-	if (skip > &buf[pos]) {
+	if (skip > &buf[edit.mbs.pos]) {
 		prefix = prompt = suffix = "";
 		skip = buf;
 	}
@@ -803,10 +805,10 @@ static void inputUpdate(void) {
 	waddstr(input, suffix);
 	getyx(input, y, x);
 
-	int posx;
+	int pos;
 	struct Style style = styleInput;
-	inputStop(styleInput, &style, skip, &buf[pos]);
-	getyx(input, y, posx);
+	inputStop(styleInput, &style, skip, &buf[edit.mbs.pos]);
+	getyx(input, y, pos);
 	wmove(input, y, x);
 
 	style = styleInput;
@@ -818,7 +820,7 @@ static void inputUpdate(void) {
 	}
 	inputAdd(styleInput, &style, ptr);
 	wclrtoeol(input);
-	wmove(input, y, posx);
+	wmove(input, y, pos);
 }
 
 void uiWindows(void) {
@@ -965,6 +967,11 @@ static void showAuto(void) {
 	}
 }
 
+static void inputEnter(uint id) {
+	command(id, editString(&edit));
+	editFn(&edit, EditClear);
+}
+
 static void keyCode(int code) {
 	struct Window *window = windows.ptrs[windows.show];
 	uint id = window->id;
@@ -973,7 +980,7 @@ static void keyCode(int code) {
 		break; case KeyFocusIn:  unmark(window);
 		break; case KeyFocusOut: mark(window);
 
-		break; case KeyMetaEnter: edit(id, EditInsert, L'\n');
+		break; case KeyMetaEnter: editInsert(&edit, L'\n');
 		break; case KeyMetaEqual: window->mute ^= true; statusUpdate();
 		break; case KeyMetaMinus: incThresh(window, -1);
 		break; case KeyMetaPlus:  incThresh(window, +1);
@@ -984,32 +991,32 @@ static void keyCode(int code) {
 
 		break; case KeyMeta0 ... KeyMeta9: uiShowNum(code - KeyMeta0);
 		break; case KeyMetaA: showAuto();
-		break; case KeyMetaB: edit(id, EditPrevWord, 0);
-		break; case KeyMetaD: edit(id, EditDeleteNextWord, 0);
-		break; case KeyMetaF: edit(id, EditNextWord, 0);
+		break; case KeyMetaB: editFn(&edit, EditPrevWord);
+		break; case KeyMetaD: editFn(&edit, EditDeleteNextWord);
+		break; case KeyMetaF: editFn(&edit, EditNextWord);
 		break; case KeyMetaL: windowList(window);
 		break; case KeyMetaM: uiWrite(id, Warm, NULL, "");
 		break; case KeyMetaN: scrollHot(window, +1);
 		break; case KeyMetaP: scrollHot(window, -1);
-		break; case KeyMetaQ: edit(id, EditCollapse, 0);
+		break; case KeyMetaQ: editFn(&edit, EditCollapse);
 		break; case KeyMetaS: spoilerReveal ^= true; mainUpdate();
 		break; case KeyMetaT: toggleTime(window);
 		break; case KeyMetaU: scrollTo(window, window->unreadHard);
 		break; case KeyMetaV: scrollPage(window, +1);
 
-		break; case KeyCtrlLeft: edit(id, EditPrevWord, 0);
-		break; case KeyCtrlRight: edit(id, EditNextWord, 0);
+		break; case KeyCtrlLeft: editFn(&edit, EditPrevWord);
+		break; case KeyCtrlRight: editFn(&edit, EditNextWord);
 
-		break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0);
-		break; case KEY_DC: edit(id, EditDeleteNext, 0);
+		break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev);
+		break; case KEY_DC: editFn(&edit, EditDeleteNext);
 		break; case KEY_DOWN: windowScroll(window, -1);
-		break; case KEY_END: edit(id, EditTail, 0);
-		break; case KEY_ENTER: edit(id, EditEnter, 0);
-		break; case KEY_HOME: edit(id, EditHead, 0);
-		break; case KEY_LEFT: edit(id, EditPrev, 0);
+		break; case KEY_END: editFn(&edit, EditTail);
+		break; case KEY_ENTER: inputEnter(id);
+		break; case KEY_HOME: editFn(&edit, EditHead);
+		break; case KEY_LEFT: editFn(&edit, EditPrev);
 		break; case KEY_NPAGE: scrollPage(window, -1);
 		break; case KEY_PPAGE: scrollPage(window, +1);
-		break; case KEY_RIGHT: edit(id, EditNext, 0);
+		break; case KEY_RIGHT: editFn(&edit, EditNext);
 		break; case KEY_SEND: scrollTo(window, 0);
 		break; case KEY_SHOME: scrollTo(window, BufferCap);
 		break; case KEY_UP: windowScroll(window, +1);
@@ -1020,33 +1027,30 @@ static void keyCtrl(wchar_t ch) {
 	struct Window *window = windows.ptrs[windows.show];
 	uint id = window->id;
 	switch (ch ^ L'@') {
-		break; case L'?': edit(id, EditDeletePrev, 0);
-		break; case L'A': edit(id, EditHead, 0);
-		break; case L'B': edit(id, EditPrev, 0);
+		break; case L'?': editFn(&edit, EditDeletePrev);
+		break; case L'A': editFn(&edit, EditHead);
+		break; case L'B': editFn(&edit, EditPrev);
 		break; case L'C': raise(SIGINT);
-		break; case L'D': edit(id, EditDeleteNext, 0);
-		break; case L'E': edit(id, EditTail, 0);
-		break; case L'F': edit(id, EditNext, 0);
-		break; case L'H': edit(id, EditDeletePrev, 0);
-		break; case L'I': edit(id, EditComplete, 0);
-		break; case L'J': edit(id, EditEnter, 0);
-		break; case L'K': edit(id, EditDeleteTail, 0);
+		break; case L'D': editFn(&edit, EditDeleteNext);
+		break; case L'E': editFn(&edit, EditTail);
+		break; case L'F': editFn(&edit, EditNext);
+		break; case L'H': editFn(&edit, EditDeletePrev);
+		break; case L'J': inputEnter(id);
+		break; case L'K': editFn(&edit, EditDeleteTail);
 		break; case L'L': clearok(curscr, true);
 		break; case L'N': uiShowNum(windows.show + 1);
 		break; case L'P': uiShowNum(windows.show - 1);
-		break; case L'R': scrollSearch(window, editBuffer(NULL), -1);
-		break; case L'S': scrollSearch(window, editBuffer(NULL), +1);
-		break; case L'T': edit(id, EditTranspose, 0);
-		break; case L'U': edit(id, EditDeleteHead, 0);
+		break; case L'R': scrollSearch(window, editString(&edit), -1);
+		break; case L'S': scrollSearch(window, editString(&edit), +1);
+		break; case L'T': editFn(&edit, EditTranspose);
+		break; case L'U': editFn(&edit, EditDeleteHead);
 		break; case L'V': scrollPage(window, -1);
-		break; case L'W': edit(id, EditDeletePrevWord, 0);
-		break; case L'X': edit(id, EditExpand, 0);
-		break; case L'Y': edit(id, EditPaste, 0);
+		break; case L'W': editFn(&edit, EditDeletePrevWord);
+		break; case L'Y': editFn(&edit, EditPaste);
 	}
 }
 
 static void keyStyle(wchar_t ch) {
-	uint id = windows.ptrs[windows.show]->id;
 	if (iswcntrl(ch)) ch = towlower(ch ^ L'@');
 	char buf[8] = {0};
 	enum Color color = Default;
@@ -1077,7 +1081,7 @@ static void keyStyle(wchar_t ch) {
 		snprintf(buf, sizeof(buf), "%c%02d", C, color);
 	}
 	for (char *ch = buf; *ch; ++ch) {
-		edit(id, EditInsert, *ch);
+		editInsert(&edit, *ch);
 	}
 }
 
@@ -1103,7 +1107,7 @@ void uiRead(void) {
 		} else if (ret == KEY_CODE_YES && ch == KeyPasteManual) {
 			paste ^= true;
 		} else if (paste || literal) {
-			edit(windows.ptrs[windows.show]->id, EditInsert, ch);
+			editInsert(&edit, ch);
 		} else if (ret == KEY_CODE_YES) {
 			keyCode(ch);
 		} else if (ch == (L'Z' ^ L'@')) {
@@ -1117,7 +1121,7 @@ void uiRead(void) {
 		} else if (iswcntrl(ch)) {
 			keyCtrl(ch);
 		} else {
-			edit(windows.ptrs[windows.show]->id, EditInsert, ch);
+			editInsert(&edit, ch);
 		}
 		style = false;
 		literal = false;