Subject: Re: catgirl: Customizable macros

From: Alcor <[email-masked]>
Date: Thu, 23 Apr 2026 23:10:47 +0200

Alcor <[email-masked]> writes:

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

Attached is a revised of the patch that fixes an issue with macros that
have a common prefix. It also fixes a bug in fgetws().

Cheers,

>From 20978f11d1fd62bd4cd75366e13e05d2c8291a8c Mon Sep 17 00:00:00 2001
From: Alcor <[email-masked]>
Date: Sat, 14 Mar 2026 20:01:20 +0100
Subject: [PATCH 3/4] Configurable macro definitions

---
 catgirl.1 |  9 +++++
 chat.c    |  4 +--
 chat.h    |  2 +-
 input.c   | 99 ++++++++++++++++++++++++++++++++++---------------------
 4 files changed, 74 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 = 'C', .name = "copy", required_argument },
 		{ .val = 'H', .name = "hash", required_argument },
 		{ .val = 'I', .name = "highlight", required_argument },
+		{ .val = 'M', .name = "macros", required_argument },
 		{ .val = 'N', .name = "notify", required_argument },
 		{ .val = 'O', .name = "open", required_argument },
 		{ .val = 'R', .name = "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 = true;
@@ -365,8 +367,6 @@ int main(int argc, char *argv[]) {
 	set(&network.name, host);
 	set(&self.nick, "*");
 
-	inputCompletion();
-
 	ircConfig(insecure, trust, cert, priv);
 
 	uiInit();
diff --git a/chat.h b/chat.h
index 329b199..e94bdff 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);
 
diff --git a/input.c b/input.c
index 7e1f9c1..9020bf1 100644
--- a/input.c
+++ b/input.c
@@ -241,51 +241,76 @@ bool inputPending(uint id) {
 	return edits[id].len;
 }
 
-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"ヾ(＾∇＾)" },
-};
+enum { NMACROS = 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 = p, *mb = q;
+	return wcscmp(ma->name, mb->name);
+}
+
+static int MacroQueryCmp(const void *p, const void *q) {
+	const struct MacroQuery *query = p;
+	const struct Macro *macro = q;
+	return wcsncmp(query->str, macro->name, query->n);
+}
+
+static int MacroQueryExactCmp(const void *p, const void *q) {
+	const struct MacroQuery *query = p;
+	const struct Macro *macro = q;
+	int c = wcsncmp(query->str, macro->name, query->n);
+	if (!c && macro->name[query->n] != L'\0') return -1;
+	return c;
+}
 
-void inputCompletion(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);
-		completePush(None, mbs, Default);
+static void wrtrim(wchar_t *s) {
+	wchar_t *end = s + wcslen(s) - 1;
+	while (end >= s && iswspace(*end)) {
+		*end = L'\0';
+		--end;
 	}
 }
 
+void inputCompletion(FILE *file) {
+	if (!file) return;
+	wchar_t line[512];
+	while (MacrosTop < NMACROS && fgetws(line, ARRAY_LEN(line), file)) {
+		if (*line == L'\n' || *line == L'\r') continue;
+		wchar_t wcvalue[256] = { L'\0' }, wckey[257] = { L'\\' };
+		if (swscanf(line, L" %255l[a-zA-Z0-9_] %255l[^\n]", &wckey[1], wcvalue) == 2) {
+			char key[257];
+			size_t n = wcstombs(key, wckey, sizeof(key));
+			if (n == (size_t)-1 || n >= sizeof(key)) continue;
+			completePush(None, key, Default);
+			Macros[MacrosTop].name = wcsdup(wckey);
+			if (!Macros[MacrosTop].name) err(1, "wcsdup");
+			Macros[MacrosTop].string = (wrtrim(wcvalue), wcsdup(wcvalue));
+			if (!Macros[MacrosTop].string) err(1, "wcsdup");
+			++MacrosTop;
+		}
+	}
+	fclose(file);
+	qsort(Macros, MacrosTop, sizeof(struct Macro), MacroCompare);
+}
+
 static int macroExpand(struct Edit *e) {
 	size_t macro = e->pos;
 	while (macro && e->buf[macro] != L'\\') macro--;
 	if (macro == e->pos) return 0;
-	for (size_t i = 0; i < ARRAY_LEN(Macros); ++i) {
-		if (wcslen(Macros[i].name) != e->pos - macro) continue;
-		if (wcsncmp(Macros[i].name, &e->buf[macro], e->pos - macro)) continue;
-		if (wcstombs(NULL, Macros[i].string, 0) == (size_t)-1) continue;
-		size_t expand = wcslen(Macros[i].string);
-		int error = 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 = macro + expand;
-		break;
-	}
+	const struct MacroQuery query = { &e->buf[macro], e->pos - macro };
+	const struct Macro *match = bsearch(&query, Macros, MacrosTop, sizeof(struct Macro), MacroQueryExactCmp);
+	if (!match) match = bsearch(&query, Macros, MacrosTop, sizeof(struct Macro), MacroQueryCmp);
+	if (!match) return 0;
+	if (wcstombs(NULL, match->string, 0) == (size_t)-1) return 0;
+	size_t expand = wcslen(match->string);
+	int error = 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 = macro + expand;
 	return 0;
 }
 
-- 
2.47.3