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: Implement Wallops (patch)
Date: Sun, 10 May 2026 10:19:43 +0200
Message-ID: <87v7cv65i8.fsf@tilde.club>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="

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

Hi all,

The following patchset implements IRC Wallops in catgirl. Similarly to
other clients e.g. Halloy, received Wallops are displayed in the server
buffer.

A Wallops can be sent via the /wallops command.

Thanks to RektIRC for help with testing this on a real IRC server.

Cheers,
-Alcor.


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=0003-Display-WALLOPS-messages.patch

>From 186c21c06697b9fbea7ae4e4a32290583b56ee1e Mon Sep 17 00:00:00 2001
From: Alcor <alcor@tilde.club>
Date: Fri, 8 May 2026 12:16:58 +0200
Subject: [PATCH 3/7] Display WALLOPS messages

Special thanks to RektIRC for help with testing.
---
 handle.c | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/handle.c b/handle.c
index 0b01de6..67159fa 100644
--- a/handle.c
+++ b/handle.c
@@ -739,6 +739,19 @@ log:
 	urlScan(id, msg->nick, msg->params[1]);
 }
 
+static void handleWallops(struct Message *msg) {
+	require(msg, true, 1);
+	enum Heat heat = filterCheck(Warm, Network, msg);
+	enum Color color = hash(msg->user);
+	completePush(Network, msg->nick, color);
+	uiFormat(
+		Network, heat, tagTime(msg),
+		"\3%d!%s!\3\t%s",
+		color, msg->nick, msg->params[0]
+	);
+	logFormat(Network, tagTime(msg), "!%s! %s", msg->nick, msg->params[0]);
+}
+
 static const char *UserModes[256] = {
 	['O'] = "local oper",
 	['i'] = "invisible",
@@ -1412,6 +1425,7 @@ static const struct Handler {
 	{ "QUIT", 0, handleQuit },
 	{ "SETNAME", 0, handleSetname },
 	{ "TOPIC", 0, handleTopic },
+	{ "WALLOPS", 0, handleWallops },
 	{ "WARN", 0, handleStandardReply },
 };
 
-- 
2.47.3


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment; filename=0004-add-wallops-command.patch

>From 535af954ad067adf6863e433932f49a732be6d32 Mon Sep 17 00:00:00 2001
From: Alcor <alcor@tilde.club>
Date: Fri, 8 May 2026 20:41:50 +0200
Subject: [PATCH 4/7] add /wallops command

Special thanks to RektIRC for help with testing.
---
 catgirl.1 |  2 ++
 chat.h    |  1 +
 command.c | 33 ++++++++++++++++++++++++++++-----
 input.c   |  4 ++++
 4 files changed, 35 insertions(+), 5 deletions(-)

diff --git a/catgirl.1 b/catgirl.1
index 9f8ceeb..eb644a6 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -811,6 +811,8 @@ Remove masks from the channel ban exception list.
 Remove masks from the channel invite list.
 .It Ic /voice Op Ar nick ...
 Grant users or yourself voice in the channel.
+.It Ic /wallops Ar message
+Send a WALLOPS.
 .El
 .
 .Sh KEY BINDINGS
diff --git a/chat.h b/chat.h
index bd69b5e..d08fe38 100644
--- a/chat.h
+++ b/chat.h
@@ -303,6 +303,7 @@ void command(uint id, char *input);
 const char *commandIsPrivmsg(uint id, const char *input);
 const char *commandIsNotice(uint id, const char *input);
 const char *commandIsAction(uint id, const char *input);
+const char *commandIsWallops(uint id, const char *input);
 size_t commandWillSplit(uint id, const char *input);
 void commandCompletion(void);
 
diff --git a/command.c b/command.c
index 35e1d63..b985ed5 100644
--- a/command.c
+++ b/command.c
@@ -53,24 +53,32 @@ static void commandQuote(uint id, char *params) {
 
 static void echoMessage(char *cmd, uint id, char *params) {
 	if (!params) return;
-	ircFormat("%s %s :%s\r\n", cmd, idNames[id], params);
+	bool wallops = (*cmd == 'W');
+	if (wallops) {
+		ircFormat("%s :%s\r\n", cmd, params);
+	} else {
+		ircFormat("%s %s :%s\r\n", cmd, idNames[id], params);
+	}
 	struct Message msg = {
 		.nick = self.nick,
 		.user = self.user,
 		.cmd = cmd,
-		.params[0] = idNames[id],
-		.params[1] = params,
+		.params[0] = (wallops ? params : idNames[id]),
+		.params[1] = (wallops ? NULL : params),
 	};
 	handle(&msg);
 }
 
 static int splitChunk(const char *cmd, uint id) {
+	bool wallops = (*cmd == 'W');
 	int overhead = snprintf(
-		NULL, 0, ":%s!%*s@%*s %s %s :\r\n",
+		NULL, 0, ":%s!%*s@%*s %s%s%s :\r\n",
 		self.nick,
 		(self.user ? 0 : network.userLen), (self.user ?: "*"),
 		(self.host ? 0 : network.hostLen), (self.host ?: "*"),
-		cmd, idNames[id]
+		cmd,
+		(wallops ? "" : " "),
+		(wallops ? "" : idNames[id])
 	);
 	assert(overhead > 0 && overhead < 512);
 	return 512 - overhead;
@@ -315,6 +323,12 @@ static void commandVoice(uint id, char *params) {
 	}
 }
 
+static void commandWallops(uint id, char *params) {
+	(void)id;
+	if (!params) return;
+	splitMessage("WALLOPS", Network, params);
+}
+
 static void commandDevoice(uint id, char *params) {
 	channelListMode(id, '-', 'v', (params ?: self.nick));
 }
@@ -619,6 +633,7 @@ static const struct Handler {
 	{ "/unignore", commandUnignore, 0, 0 },
 	{ "/uninvex", commandUninvex, 0, 0 },
 	{ "/voice", commandVoice, 0, 0 },
+	{ "/wallops", commandWallops, Multiline, 0 },
 	{ "/whois", commandWhois, 0, 0 },
 	{ "/whowas", commandWhowas, 0, 0 },
 	{ "/window", commandWindow, 0, 0 },
@@ -650,6 +665,12 @@ const char *commandIsAction(uint id, const char *input) {
 	return &input[4];
 }
 
+const char *commandIsWallops(uint id, const char *input) {
+	if (id == Debug) return NULL;
+	if (strncmp(input, "/wallops ", 9)) return NULL;
+	return &input[9];
+}
+
 size_t commandWillSplit(uint id, const char *input) {
 	int chunk;
 	const char *params;
@@ -659,6 +680,8 @@ size_t commandWillSplit(uint id, const char *input) {
 		chunk = splitChunk("NOTICE", id);
 	} else if (NULL != (params = commandIsAction(id, input))) {
 		chunk = splitChunk("PRIVMSG \1ACTION\1", id);
+	} else if (NULL != (params = commandIsWallops(id, input))) {
+		chunk = splitChunk("WALLOPS", id);
 	} else if (id != Network && id != Debug && !strncmp(input, "/say ", 5)) {
 		params = &input[5];
 		chunk = splitChunk("PRIVMSG", id);
diff --git a/input.c b/input.c
index 748dca2..920c5f1 100644
--- a/input.c
+++ b/input.c
@@ -184,6 +184,7 @@ void inputUpdate(void) {
 	const char *privmsg = commandIsPrivmsg(id, buf);
 	const char *notice = commandIsNotice(id, buf);
 	const char *action = commandIsAction(id, buf);
+	const char *wallops = commandIsWallops(id, buf);
 	if (privmsg) {
 		prefix = "<"; suffix = "> ";
 		skip = privmsg;
@@ -196,6 +197,9 @@ void inputUpdate(void) {
 		stylePrompt.attr |= Italic;
 		styleInput.attr |= Italic;
 		skip = action;
+	} else if (wallops) {
+		prefix = "!"; suffix = "! ";
+		skip = wallops;
 	} else if (id == Debug && buf[0] != '/') {
 		prompt = "<< ";
 		stylePrompt.fg = Gray;
-- 
2.47.3


--=-=-=--

From mboxrd@z Thu Jan  1 00:00:00 1970
From: Alcor <alcor@tilde.club>
To: list@causal.agency
Cc: june@causal.agency
Subject: Re: catgirl: Implement Wallops (patch)
In-Reply-To: <87v7cv65i8.fsf@tilde.club> (alcor@tilde.club's message of "Sun,
	10 May 2026 10:19:43 +0200")
References: <87v7cv65i8.fsf@tilde.club>
Date: Sun, 10 May 2026 17:38:03 +0200
Message-ID: <87fr3z8eck.fsf@tilde.club>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="

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

> The following patchset implements IRC Wallops in catgirl. Similarly to
> other clients e.g. Halloy, received Wallops are displayed in the server
> buffer.

Included is a tiny revision to the /wallops patch to ensure the sent
message is not echoed (most ircds will echo it anyway).

Cheers,

--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment; filename=0004-add-wallops-command-v2.patch

>From e362e4a8a11051bf250328889792b068eb952d5a Mon Sep 17 00:00:00 2001
From: Alcor <alcor@tilde.club>
Date: Fri, 8 May 2026 20:41:50 +0200
Subject: [PATCH 4/7] add /wallops command (v2)

Special thanks to RektIRC for help with testing.
---
 catgirl.1 |  2 ++
 chat.h    |  1 +
 command.c | 34 +++++++++++++++++++++++++++++-----
 input.c   |  4 ++++
 4 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/catgirl.1 b/catgirl.1
index 9f8ceeb..eb644a6 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -811,6 +811,8 @@ Remove masks from the channel ban exception list.
 Remove masks from the channel invite list.
 .It Ic /voice Op Ar nick ...
 Grant users or yourself voice in the channel.
+.It Ic /wallops Ar message
+Send a WALLOPS.
 .El
 .
 .Sh KEY BINDINGS
diff --git a/chat.h b/chat.h
index bd69b5e..d08fe38 100644
--- a/chat.h
+++ b/chat.h
@@ -303,6 +303,7 @@ void command(uint id, char *input);
 const char *commandIsPrivmsg(uint id, const char *input);
 const char *commandIsNotice(uint id, const char *input);
 const char *commandIsAction(uint id, const char *input);
+const char *commandIsWallops(uint id, const char *input);
 size_t commandWillSplit(uint id, const char *input);
 void commandCompletion(void);
 
diff --git a/command.c b/command.c
index 35e1d63..4dda518 100644
--- a/command.c
+++ b/command.c
@@ -53,24 +53,33 @@ static void commandQuote(uint id, char *params) {
 
 static void echoMessage(char *cmd, uint id, char *params) {
 	if (!params) return;
-	ircFormat("%s %s :%s\r\n", cmd, idNames[id], params);
+	bool wallops = (*cmd == 'W');
+	if (wallops) {
+		ircFormat("%s :%s\r\n", cmd, params);
+		return; /* no echo */
+	} else {
+		ircFormat("%s %s :%s\r\n", cmd, idNames[id], params);
+	}
 	struct Message msg = {
 		.nick = self.nick,
 		.user = self.user,
 		.cmd = cmd,
-		.params[0] = idNames[id],
-		.params[1] = params,
+		.params[0] = (wallops ? params : idNames[id]),
+		.params[1] = (wallops ? NULL : params),
 	};
 	handle(&msg);
 }
 
 static int splitChunk(const char *cmd, uint id) {
+	bool wallops = (*cmd == 'W');
 	int overhead = snprintf(
-		NULL, 0, ":%s!%*s@%*s %s %s :\r\n",
+		NULL, 0, ":%s!%*s@%*s %s%s%s :\r\n",
 		self.nick,
 		(self.user ? 0 : network.userLen), (self.user ?: "*"),
 		(self.host ? 0 : network.hostLen), (self.host ?: "*"),
-		cmd, idNames[id]
+		cmd,
+		(wallops ? "" : " "),
+		(wallops ? "" : idNames[id])
 	);
 	assert(overhead > 0 && overhead < 512);
 	return 512 - overhead;
@@ -315,6 +324,12 @@ static void commandVoice(uint id, char *params) {
 	}
 }
 
+static void commandWallops(uint id, char *params) {
+	(void)id;
+	if (!params) return;
+	splitMessage("WALLOPS", Network, params);
+}
+
 static void commandDevoice(uint id, char *params) {
 	channelListMode(id, '-', 'v', (params ?: self.nick));
 }
@@ -619,6 +634,7 @@ static const struct Handler {
 	{ "/unignore", commandUnignore, 0, 0 },
 	{ "/uninvex", commandUninvex, 0, 0 },
 	{ "/voice", commandVoice, 0, 0 },
+	{ "/wallops", commandWallops, Multiline, 0 },
 	{ "/whois", commandWhois, 0, 0 },
 	{ "/whowas", commandWhowas, 0, 0 },
 	{ "/window", commandWindow, 0, 0 },
@@ -650,6 +666,12 @@ const char *commandIsAction(uint id, const char *input) {
 	return &input[4];
 }
 
+const char *commandIsWallops(uint id, const char *input) {
+	if (id == Debug) return NULL;
+	if (strncmp(input, "/wallops ", 9)) return NULL;
+	return &input[9];
+}
+
 size_t commandWillSplit(uint id, const char *input) {
 	int chunk;
 	const char *params;
@@ -659,6 +681,8 @@ size_t commandWillSplit(uint id, const char *input) {
 		chunk = splitChunk("NOTICE", id);
 	} else if (NULL != (params = commandIsAction(id, input))) {
 		chunk = splitChunk("PRIVMSG \1ACTION\1", id);
+	} else if (NULL != (params = commandIsWallops(id, input))) {
+		chunk = splitChunk("WALLOPS", id);
 	} else if (id != Network && id != Debug && !strncmp(input, "/say ", 5)) {
 		params = &input[5];
 		chunk = splitChunk("PRIVMSG", id);
diff --git a/input.c b/input.c
index 748dca2..920c5f1 100644
--- a/input.c
+++ b/input.c
@@ -184,6 +184,7 @@ void inputUpdate(void) {
 	const char *privmsg = commandIsPrivmsg(id, buf);
 	const char *notice = commandIsNotice(id, buf);
 	const char *action = commandIsAction(id, buf);
+	const char *wallops = commandIsWallops(id, buf);
 	if (privmsg) {
 		prefix = "<"; suffix = "> ";
 		skip = privmsg;
@@ -196,6 +197,9 @@ void inputUpdate(void) {
 		stylePrompt.attr |= Italic;
 		styleInput.attr |= Italic;
 		skip = action;
+	} else if (wallops) {
+		prefix = "!"; suffix = "! ";
+		skip = wallops;
 	} else if (id == Debug && buf[0] != '/') {
 		prompt = "<< ";
 		stylePrompt.fg = Gray;
-- 
2.47.3


--=-=-=--

