From mboxrd@z Thu Jan  1 00:00:00 1970
From: Alcor <alcor@tilde.club>
To: list@causal.agency
Cc: june@causal.agency
Subject: catgirl: Customizable macros
Date: Sun, 15 Mar 2026 01:09:53 +0100
Message-ID: <87ikay6jhq.fsf@tilde.club>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="

--=-=-=
Content-Type: text/plain

Hi all,

The attached patch implements an option that allows loading \backslash
macros from a user-defined file.

A macro file is defined as a two-column text file which maps macro names
to their values. Any whitespace is interpreted as a column delimiter. An
example is attached alongside the patch.

Cheers,
-Alcor


--=-=-=
Content-Type: text/x-diff; charset=utf-8
Content-Disposition: attachment;
 filename=0001-Configurable-macro-definitions.patch
Content-Transfer-Encoding: quoted-printable

>From 920179b142cfe2b3e8877cae57c6b9d4aad9eca3 Mon Sep 17 00:00:00 2001
From: Alcor <alcor@tilde.club>
Date: Sat, 14 Mar 2026 20:01:20 +0100
Subject: [PATCH] Configurable macro definitions

---
 catgirl.1 |  9 ++++++
 chat.c    |  4 +--
 chat.h    |  2 +-
 input.c   | 90 ++++++++++++++++++++++++++++++++-----------------------
 4 files changed, 65 insertions(+), 40 deletions(-)

diff --git a/catgirl.1 b/catgirl.1
index 9f8ceeb..704bd2f 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -12,6 +12,7 @@
 .Op Fl C Ar copy
 .Op Fl H Ar hash
 .Op Fl I Ar highlight
+.Op Fl M Ar macros
 .Op Fl N Ar notify
 .Op Fl O Ar open
 .Op Fl S Ar bind
@@ -171,6 +172,14 @@ joins your favourite channel:
 .Pp
 .Dl highlight crush!*@* join #channel
 .
+.It Fl M Ar filename | Cm macros Ar filename
+Read text macro definitions from
+.Cm filename ,
+which should be a two-column
+text file mapping macro names to their
+respective expansions. A macro name may only
+contain alphanumeric characters and underscores.
+.
 .It Fl N Ar util | Cm notify Ar util
 Send notifications using a utility.
 Subsequent
diff --git a/chat.c b/chat.c
index daefcec..a9f8cd9 100644
--- a/chat.c
+++ b/chat.c
@@ -244,6 +244,7 @@ int main(int argc, char *argv[]) {
 		{ .val =3D 'C', .name =3D "copy", required_argument },
 		{ .val =3D 'H', .name =3D "hash", required_argument },
 		{ .val =3D 'I', .name =3D "highlight", required_argument },
+		{ .val =3D 'M', .name =3D "macros", required_argument },
 		{ .val =3D 'N', .name =3D "notify", required_argument },
 		{ .val =3D 'O', .name =3D "open", required_argument },
 		{ .val =3D 'R', .name =3D "restrict", no_argument },
@@ -284,6 +285,7 @@ int main(int argc, char *argv[]) {
 			break; case 'C': utilPush(&urlCopyUtil, optarg);
 			break; case 'H': parseHash(optarg);
 			break; case 'I': filterAdd(Hot, optarg);
+			break; case 'M': inputCompletion(configOpen(optarg, "r"));
 			break; case 'N': utilPush(&uiNotifyUtil, optarg);
 			break; case 'O': utilPush(&urlOpenUtil, optarg);
 			break; case 'R': self.restricted =3D true;
@@ -365,8 +367,6 @@ int main(int argc, char *argv[]) {
 	set(&network.name, host);
 	set(&self.nick, "*");
=20
-	inputCompletion();
-
 	ircConfig(insecure, trust, cert, priv);
=20
 	uiInit();
diff --git a/chat.h b/chat.h
index 958f598..b8b9fae 100644
--- a/chat.h
+++ b/chat.h
@@ -346,7 +346,7 @@ void inputWait(void);
 void inputUpdate(void);
 bool inputPending(uint id);
 void inputRead(void);
-void inputCompletion(void);
+void inputCompletion(FILE *file);
 int inputSave(FILE *file);
 void inputLoad(FILE *file, size_t version);
=20
diff --git a/input.c b/input.c
index 7e1f9c1..2738fa3 100644
--- a/input.c
+++ b/input.c
@@ -241,51 +241,67 @@ bool inputPending(uint id) {
 	return edits[id].len;
 }
=20
-static const struct {
-	const wchar_t *name;
-	const wchar_t *string;
-} Macros[] =3D {
-	{ L"\\banhammer", L"=E2=96=AC=E2=96=AC=E2=96=AC=E2=96=AC=E2=96=AC=E2=96=
=AC=E2=96=AC=E2=96=8B =C3=92=E2=95=AD=E2=95=AE=C3=93" },
-	{ L"\\bear", L"=CA=95=E3=81=A3=E2=80=A2=E1=B4=A5=E2=80=A2=CA=94=E3=81=A3"=
 },
-	{ L"\\blush", L"=EF=BC=88=CB=B6=E2=80=B2=E2=97=A1=E2=80=B5=CB=B6=EF=BC=89=
" },
-	{ L"\\com", L"\0038,4\2 =E2=98=AD " },
-	{ L"\\cool", L"(=E2=8C=90=E2=96=A0_=E2=96=A0)" },
-	{ L"\\flip", L"(=E2=95=AF=C2=B0=E2=96=A1=C2=B0=EF=BC=89=E2=95=AF=EF=B8=B5=
 =E2=94=BB=E2=94=81=E2=94=BB" },
-	{ L"\\gary", L"=E1=95=95( =E1=90=9B )=E1=95=97" },
-	{ L"\\hug", L"=EF=BC=88=E3=81=A3=E3=83=BB=E2=88=80=E3=83=BB=EF=BC=89=E3=
=81=A3" },
-	{ L"\\lenny", L"( =CD=A1=C2=B0 =CD=9C=CA=96 =CD=A1=C2=B0)" },
-	{ L"\\look", L"=E0=B2=A0_=E0=B2=A0" },
-	{ L"\\shrug", L"=C2=AF\\_(=E3=83=84)_/=C2=AF" },
-	{ L"\\unflip", L"=E2=94=AC=E2=94=80=E2=94=AC=E3=83=8E(=C2=BA_=C2=BA=E3=83=
=8E)" },
-	{ L"\\wave", L"=E3=83=BE(=EF=BC=BE=E2=88=87=EF=BC=BE)" },
-};
+enum { NMACROS =3D 256 };
+static struct Macro { const wchar_t *name, *string; } Macros[NMACROS];
+struct MacroQuery { const wchar_t *str; size_t n; };
+static size_t MacrosTop;
+
+static int MacroCompare(const void *p, const void *q) {
+	const struct Macro *ma =3D p, *mb =3D q;
+	return wcscmp(ma->name, mb->name);
+}
+
+static int MacroQueryCmp(const void *p, const void *q) {
+	const struct MacroQuery *query =3D p;
+	const struct Macro *macro =3D q;
+	return wcsncmp(query->str, macro->name, query->n);
+}
+
+static void wrtrim(wchar_t *s) {
+	wchar_t *end =3D s + wcslen(s) - 1;
+	while (end >=3D s && iswspace(*end)) {
+		*end =3D L'\0';
+		--end;
+	}
+}
=20
-void inputCompletion(void) {
-	char mbs[256];
-	for (size_t i =3D 0; i < ARRAY_LEN(Macros); ++i) {
-		size_t n =3D wcstombs(mbs, Macros[i].name, sizeof(mbs));
-		assert(n !=3D (size_t)-1);
-		completePush(None, mbs, Default);
+void inputCompletion(FILE *file) {
+	if (!file) return;
+	wchar_t line[512];
+	while (MacrosTop < NMACROS && fgetws(line, sizeof(line), file)) {
+		if (*line =3D=3D L'\n' || *line =3D=3D L'\r') continue;
+		wchar_t wcvalue[256] =3D { L'\0' }, wckey[257] =3D { L'\\' };
+		if (swscanf(line, L" %255l[a-zA-Z0-9_] %255l[^\n]", &wckey[1], wcvalue) =
=3D=3D 2) {
+			char key[257];
+			size_t n =3D wcstombs(key, wckey, sizeof(key));
+			if (n =3D=3D (size_t)-1 || n >=3D sizeof(key)) continue;
+			completePush(None, key, Default);
+			Macros[MacrosTop].name =3D wcsdup(wckey);
+			if (!Macros[MacrosTop].name) err(1, "wcsdup");
+			Macros[MacrosTop].string =3D (wrtrim(wcvalue), wcsdup(wcvalue));
+			if (!Macros[MacrosTop].string) err(1, "wcsdup");
+			++MacrosTop;
+		}
 	}
+	fclose(file);
+	qsort(Macros, MacrosTop, sizeof(struct Macro), MacroCompare);
 }
=20
 static int macroExpand(struct Edit *e) {
 	size_t macro =3D e->pos;
 	while (macro && e->buf[macro] !=3D L'\\') macro--;
 	if (macro =3D=3D e->pos) return 0;
-	for (size_t i =3D 0; i < ARRAY_LEN(Macros); ++i) {
-		if (wcslen(Macros[i].name) !=3D e->pos - macro) continue;
-		if (wcsncmp(Macros[i].name, &e->buf[macro], e->pos - macro)) continue;
-		if (wcstombs(NULL, Macros[i].string, 0) =3D=3D (size_t)-1) continue;
-		size_t expand =3D wcslen(Macros[i].string);
-		int error =3D 0
-			|| editDelete(e, false, macro, e->pos - macro)
-			|| editReserve(e, macro, expand);
-		if (error) return error;
-		wcsncpy(&e->buf[macro], Macros[i].string, expand);
-		e->pos =3D macro + expand;
-		break;
-	}
+	const struct MacroQuery query =3D { &e->buf[macro], e->pos - macro };
+	const struct Macro *match =3D bsearch(&query, Macros, MacrosTop, sizeof(s=
truct Macro), MacroQueryCmp);
+	if (!match) return 0;
+	if (wcstombs(NULL, match->string, 0) =3D=3D (size_t)-1) return 0;
+	size_t expand =3D wcslen(match->string);
+	int error =3D 0
+		|| editDelete(e, false, macro, e->pos - macro)
+		|| editReserve(e, macro, expand);
+	if (error) return error;
+	wcsncpy(&e->buf[macro], match->string, expand);
+	e->pos =3D macro + expand;
 	return 0;
 }
=20
--=20
2.47.3


--=-=-=
Content-Type: text/plain; charset=utf-8
Content-Disposition: attachment; filename=macros.txt
Content-Transfer-Encoding: base64

YWNhdCDOozozDQpiZWFyIMqV44Gj4oCi4bSl4oCiypTjgaMNCmJsdXNoIO+8iMu24oCy4peh4oC1
y7bvvIkNCmNsYXAg8J+Rjw0KZHVzdCAo776J4peV44Ou4peVKe++iSo6772l776f4pynDQplc2Nh
cGUg4pSAPeKJoc6jKCgoIOOBpO+8nu+8nCnjgaQNCmZpcmUg8J+UpQ0KaGVhcnQg4p2k77iPDQpo
dWcg77yI44Gj44O74oiA44O777yJ44GjDQpqb3kgKCriiafPieKJpiopDQpsaWNrIPCfkYUNCmxv
b2sg4LKgX+CyoA0Kc2hydWcgwq9cXF8o44OEKV8vwq8NCnNrdWxsIPCfkoANCnNwYXJrbGVzIOKc
qA0Kd2F2ZSDjg74o77y+4oiH77y+KQ0K
--=-=-=--

