From 2b9b9f1a7786a24eeacb98314172ee53a1e173c3 Mon Sep 17 00:00:00 2001
From: Alcor <alcor@tilde.club>
Date: Tue, 12 May 2026 17:06:31 +0200
Subject: [PATCH 14/19] Implement per-buffer/window input history.

---
 catgirl.1 |  6 ++++
 input.c   | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+)

diff --git a/catgirl.1 b/catgirl.1
index 09590e8..7eedd93 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -877,6 +877,12 @@ Delete next word.
 Move to next word.
 .It Ic M-q
 Collapse all whitespace.
+.It Ic M-z
+Zap/kill line and push it to the edit history.
+.It Ic M-,
+Navigate to the previous edit history entry.
+.It Ic M-.
+Navigate to the next edit history entry.
 .It Ic Tab
 Complete nick, channel, command or macro.
 .El
diff --git a/input.c b/input.c
index d4d9a4f..483af07 100644
--- a/input.c
+++ b/input.c
@@ -69,6 +69,7 @@
 	X(KeyMetaT, "\33t", NULL) \
 	X(KeyMetaU, "\33u", NULL) \
 	X(KeyMetaV, "\33v", NULL) \
+	X(KeyMetaZ, "\33z", NULL) \
 	X(KeyMetaEnter, "\33\r", "\33\n") \
 	X(KeyMetaGt, "\33>", "\33.") \
 	X(KeyMetaLt, "\33<", "\33,") \
@@ -76,6 +77,8 @@
 	X(KeyMetaMinus, "\33-", "\33_") \
 	X(KeyMetaPlus, "\33+", NULL) \
 	X(KeyMetaSlash, "\33/", "\33?") \
+	X(KeyMetaComma, "\33,", "\33[1;3A")	 \
+	X(KeyMetaDot, "\33.", "\33[1;3B") \
 	X(KeyFocusIn, "\33[I", NULL) \
 	X(KeyFocusOut, "\33[O", NULL) \
 	X(KeyPasteOn, "\33[200~", NULL) \
@@ -92,6 +95,81 @@ enum {
 static struct Edit cut;
 static struct Edit edits[IDCap];
 
+#define HistoryCap 64
+
+static struct {
+	wchar_t *lines[HistoryCap];
+	size_t len;
+	size_t write;
+	size_t pos;
+	wchar_t *draft;
+} histories[IDCap];
+
+static void historyPush(uint id, const wchar_t *buf, size_t len) {
+	if (!len) return;
+
+	if (histories[id].len) {
+		const wchar_t *last = histories[id].lines[(histories[id].write + HistoryCap - 1) % HistoryCap];
+		if (last && !wcsncmp(last, buf, len) && last[len] == L'\0') goto record;
+	}
+
+	wchar_t *line = calloc(len + 1, sizeof(wchar_t));
+	if (!line) err(1, "calloc");
+	wmemcpy(line, buf, len);
+
+	free(histories[id].lines[histories[id].write]);
+	histories[id].lines[histories[id].write] = line;
+
+	histories[id].write = (histories[id].write + 1) % HistoryCap;
+	if (histories[id].len < HistoryCap) histories[id].len++;
+
+record:
+	histories[id].pos = histories[id].len;
+}
+
+static void historyNav(uint id, int dir) {
+	struct Edit *e = &edits[id];
+
+	if (dir < 0 && histories[id].pos > 0) {
+		if (histories[id].pos == histories[id].len) {
+			free(histories[id].draft);
+			histories[id].draft = NULL;
+			if (e->len) {
+				histories[id].draft = calloc(e->len + 1, sizeof(wchar_t));
+				if (!histories[id].draft) err(1, "calloc");
+				wmemcpy(histories[id].draft, e->buf, e->len);
+			}
+		}
+		histories[id].pos--;
+	} else if (dir > 0 && histories[id].pos < histories[id].len) {
+		histories[id].pos++;
+	} else {
+		return;
+	}
+
+	const wchar_t *line;
+	if (histories[id].pos == histories[id].len) {
+		line = histories[id].draft;
+	} else {
+		size_t offset = histories[id].len == HistoryCap ? histories[id].write : 0;
+		line = histories[id].lines[(offset + histories[id].pos) % HistoryCap];
+	}
+
+	editFn(e, EditClear);
+	if (line) {
+		size_t n = wcslen(line);
+		if (editReserve(e, 0, n)) err(1, "editReserve");
+		wmemcpy(e->buf, line, n);
+		e->len = n;
+		e->pos = n;
+	}
+}
+
+static int zapToHistory(uint id, struct Edit *e) {
+	historyPush(id, e->buf, e->len);
+	return editFn(e, EditClear);
+}
+
 void inputInit(void) {
 	for (size_t i = 0; i < ARRAY_LEN(edits); ++i) {
 		edits[i].cut = &cut;
@@ -442,6 +520,7 @@ fail:
 
 static void inputEnter(void) {
 	uint id = windowID();
+	historyPush(id, edits[id].buf, edits[id].len);
 	char *cmd = editString(&edits[id], &buf, &cap, NULL);
 	if (!cmd) err(1, "editString");
 
@@ -481,6 +560,9 @@ static void keyCode(int code) {
 		break; case KeyMetaT: windowToggleTime();
 		break; case KeyMetaU: windowScroll(ScrollUnread, 0);
 		break; case KeyMetaV: windowScroll(ScrollPage, +1);
+		break; case KeyMetaZ: error = zapToHistory(windowID(), edit);
+		break; case KeyMetaComma: historyNav(windowID(), -1);
+		break; case KeyMetaDot: historyNav(windowID(), +1);
 
 		break; case KeyCtrlLeft: error = editFn(edit, EditPrevWord);
 		break; case KeyCtrlRight: error = editFn(edit, EditNextWord);
-- 
2.47.3

