From cd10fc083daba47fb0c559f43579f8f838aed37f Mon Sep 17 00:00:00 2001
From: Alcor <alcor@tilde.club>
Date: Sun, 7 Jun 2026 21:49:10 +0200
Subject: [PATCH 19/19] make URLs inherit their message heat

Also make /open, /copy etc. check the URL heat against the window threshold.
---
 chat.h   |  3 ++-
 handle.c | 18 +++++++++---------
 ui.c     |  5 +++--
 url.c    | 26 ++++++++++++++++++++++----
 window.c |  4 ++++
 5 files changed, 40 insertions(+), 16 deletions(-)

diff --git a/chat.h b/chat.h
index beca89d..ae0ec7a 100644
--- a/chat.h
+++ b/chat.h
@@ -393,6 +393,7 @@ void windowToggleMute(void);
 void windowToggleTime(void);
 void windowToggleThresh(int n);
 bool windowTimeEnable(void);
+enum Heat windowThresh(uint num);
 void windowScroll(enum Scroll by, int n);
 void windowSearch(const char *str, int dir);
 int windowSave(FILE *file);
@@ -441,7 +442,7 @@ void completeReject(struct Cursor *curs);
 
 extern struct Util urlOpenUtil;
 extern struct Util urlCopyUtil;
-void urlScan(uint id, const char *nick, const char *mesg);
+void urlScan(uint id, const char *nick, const char *mesg, enum Heat heat);
 void urlOpenCount(uint id, uint count);
 void urlOpenMatch(uint id, const char *str);
 void urlCopyMatch(uint id, const char *str);
diff --git a/handle.c b/handle.c
index e8607c5..fe979f4 100644
--- a/handle.c
+++ b/handle.c
@@ -341,7 +341,7 @@ static void handleReplyISupport(struct Message *msg) {
 static void handleReplyMOTD(struct Message *msg) {
 	require(msg, false, 2);
 	char *line = msg->params[1];
-	urlScan(Network, NULL, line);
+	urlScan(Network, NULL, line, Warm);
 	if (!strncmp(line, "- ", 2)) {
 		uiFormat(Network, Warm, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]);
 	} else {
@@ -351,7 +351,7 @@ static void handleReplyMOTD(struct Message *msg) {
 
 static void handleReplyHelp(struct Message *msg) {
 	require(msg, false, 3);
-	urlScan(Network, NULL, msg->params[2]);
+	urlScan(Network, NULL, msg->params[2], Warm);
 	uiWrite(Network, Warm, tagTime(msg), msg->params[2]);
 }
 
@@ -409,7 +409,7 @@ static void handlePart(struct Message *msg) {
 	}
 	completeRemove(id, msg->nick);
 	enum Heat heat = filterCheck(Cold, id, msg);
-	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
+	urlScan(id, msg->nick, msg->params[1], heat);
 	uiFormat(
 		id, heat, tagTime(msg),
 		"\3%02d%s\3\tleaves \3%02d%s\3%s%s",
@@ -428,7 +428,7 @@ static void handleKick(struct Message *msg) {
 	uint id = idFor(msg->params[0]);
 	bool kicked = !strcmp(msg->params[1], self.nick);
 	completePull(id, msg->nick, hash(msg->user));
-	urlScan(id, msg->nick, msg->params[2]);
+	urlScan(id, msg->nick, msg->params[2], (kicked ? Hot : Cold));
 	uiFormat(
 		id, (kicked ? Hot : Cold), tagTime(msg),
 		"%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s",
@@ -490,7 +490,7 @@ static void handleQuit(struct Message *msg) {
 	struct Cursor curs = {0};
 	for (uint id; (id = completeEachID(&curs, msg->nick));) {
 		enum Heat heat = filterCheck(Cold, id, msg);
-		if (heat > Ice) urlScan(id, msg->nick, msg->params[0]);
+		urlScan(id, msg->nick, msg->params[0], heat);
 		uiFormat(
 			id, heat, tagTime(msg),
 			"\3%02d%s\3\tleaves%s%s",
@@ -622,7 +622,7 @@ static void handleReplyTopic(struct Message *msg) {
 	uint id = idFor(msg->params[1]);
 	topicComplete(id, msg->params[2]);
 	if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return;
-	urlScan(id, NULL, msg->params[2]);
+	urlScan(id, NULL, msg->params[2], (replies[ReplyTopicAuto] ? Cold : Warm));
 	uiFormat(
 		id, (replies[ReplyTopicAuto] ? Cold : Warm), tagTime(msg),
 		"The sign in \3%02d%s\3 reads: %s",
@@ -736,14 +736,14 @@ log:
 		msg->nick, msg->params[0], msg->params[1]
 	);
 	topicComplete(id, msg->params[1]);
-	urlScan(id, msg->nick, msg->params[1]);
+	urlScan(id, msg->nick, msg->params[1], Warm);
 }
 
 static void handleWallops(struct Message *msg) {
 	require(msg, true, 1);
 	enum Heat heat = filterCheck(Warm, Network, msg);
 	enum Color color = hash(msg->user);
-	urlScan(Network, msg->nick, msg->params[0]);
+	urlScan(Network, msg->nick, msg->params[0], heat);
 	completePush(Network, msg->nick, color);
 	uiFormat(
 		Network, heat, tagTime(msg),
@@ -1301,7 +1301,7 @@ static void handlePrivmsg(struct Message *msg) {
 	if (!notice && !mine && heat > Ice) {
 		completePull(id, msg->nick, hash(msg->user));
 	}
-	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
+	urlScan(id, msg->nick, msg->params[1], heat);
 
 	char buf[1024];
 	char *ptr = buf, *end = &buf[sizeof(buf)];
diff --git a/ui.c b/ui.c
index 0dc1139..a6a5dc0 100644
--- a/ui.c
+++ b/ui.c
@@ -300,7 +300,8 @@ static const uint64_t Signatures[] = {
 	0x6C72696774616306, // no thresh
 	0x6C72696774616307, // no window time
 	0x6C72696774616308, // no input
-	0x6C72696774616309,
+	0x6C72696774616309, // no url.heat
+	0x6C72696774616310
 };
 
 static size_t signatureVersion(uint64_t signature) {
@@ -317,7 +318,7 @@ static int writeUint64(FILE *file, uint64_t u) {
 int uiSave(void) {
 	return 0
 		|| ftruncate(fileno(saveFile), 0)
-		|| writeUint64(saveFile, Signatures[8])
+		|| writeUint64(saveFile, Signatures[9])
 		|| writeUint64(saveFile, self.pos)
 		|| windowSave(saveFile)
 		|| inputSave(saveFile)
diff --git a/url.c b/url.c
index 349dc00..722bc0b 100644
--- a/url.c
+++ b/url.c
@@ -72,6 +72,7 @@ static void compile(void) {
 struct URL {
 	uint id;
 	char *nick;
+	enum Heat heat;
 	char *url;
 };
 
@@ -82,7 +83,7 @@ static struct {
 } ring;
 _Static_assert(!(Cap & (Cap - 1)), "Cap is power of two");
 
-static void push(uint id, const char *nick, const char *str, size_t len) {
+static void push(uint id, const char *nick, const char *str, size_t len, enum Heat heat) {
 	struct URL *url = &ring.urls[ring.len++ % Cap];
 	free(url->nick);
 	free(url->url);
@@ -93,6 +94,7 @@ static void push(uint id, const char *nick, const char *str, size_t len) {
 		url->nick = strdup(nick);
 		if (!url->nick) err(1, "strdup");
 	}
+	url->heat = heat;
 	url->url = malloc(len + 1);
 	if (!url->url) err(1, "malloc");
 
@@ -101,13 +103,13 @@ static void push(uint id, const char *nick, const char *str, size_t len) {
 	styleStrip(url->url, len + 1, buf);
 }
 
-void urlScan(uint id, const char *nick, const char *mesg) {
+void urlScan(uint id, const char *nick, const char *mesg, enum Heat heat) {
 	if (!mesg) return;
 	compile();
 	regmatch_t match = {0};
 	for (const char *ptr = mesg; *ptr; ptr += match.rm_eo) {
 		if (regexec(&Regex, ptr, 1, &match, 0)) break;
-		push(id, nick, &ptr[match.rm_so], match.rm_eo - match.rm_so);
+		push(id, nick, &ptr[match.rm_so], match.rm_eo - match.rm_so, heat);
 	}
 }
 
@@ -200,6 +202,7 @@ void urlOpenCount(uint id, uint count) {
 		const struct URL *url = &ring.urls[(ring.len - i) % Cap];
 		if (!url->url) break;
 		if (url->id != id) continue;
+		if (url->heat < windowThresh(windowFor(id))) continue;
 		urlOpen(url->url);
 		if (!--count) break;
 	}
@@ -210,6 +213,7 @@ void urlOpenMatch(uint id, const char *str) {
 		const struct URL *url = &ring.urls[(ring.len - i) % Cap];
 		if (!url->url) break;
 		if (url->id != id) continue;
+		if (url->heat < windowThresh(windowFor(id))) continue;
 		if ((url->nick && !strcmp(url->nick, str)) || strstr(url->url, str)) {
 			urlOpen(url->url);
 			break;
@@ -222,6 +226,7 @@ void urlCopyMatch(uint id, const char *str) {
 		const struct URL *url = &ring.urls[(ring.len - i) % Cap];
 		if (!url->url) break;
 		if (url->id != id) continue;
+		if (url->heat < windowThresh(windowFor(url->id))) continue;
 		if (
 			!str
 			|| (url->nick && !strcmp(url->nick, str))
@@ -236,12 +241,15 @@ void urlCopyMatch(uint id, const char *str) {
 static int writeString(FILE *file, const char *str) {
 	return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1);
 }
+
 static ssize_t readString(FILE *file, char **buf, size_t *cap) {
 	ssize_t len = getdelim(buf, cap, '\0', file);
 	if (len < 0 && !feof(file)) err(1, "getdelim");
 	return len;
 }
 
+static const char *heatCode[] = { "I", "C", "W", "H" };
+
 int urlSave(FILE *file) {
 	for (size_t i = 0; i < Cap; ++i) {
 		const struct URL *url = &ring.urls[(ring.len + i) % Cap];
@@ -249,7 +257,8 @@ int urlSave(FILE *file) {
 		int error = 0
 			|| writeString(file, idNames[url->id])
 			|| writeString(file, (url->nick ?: ""))
-			|| writeString(file, url->url);
+			|| writeString(file, url->url)
+			|| writeString(file, heatCode[url->heat]);
 		if (error) return error;
 	}
 	return writeString(file, "");
@@ -265,6 +274,7 @@ void urlLoad(FILE *file, size_t version) {
 		free(url->url);
 		url->id = idFor(buf);
 		url->nick = NULL;
+		url->heat = Cold;
 		readString(file, &buf, &cap);
 		if (buf[0]) {
 			url->nick = strdup(buf);
@@ -273,6 +283,14 @@ void urlLoad(FILE *file, size_t version) {
 		readString(file, &buf, &cap);
 		url->url = strdup(buf);
 		if (!url->url) err(1, "strdup");
+		if (version < 9) continue;
+		if (readString(file, &buf, &cap) < 1) continue;
+		switch (*buf) {
+		case 'I': url->heat = Ice; break;
+		case 'C': url->heat = Cold; break;
+		case 'W': url->heat = Warm; break;
+		case 'H': url->heat = Hot; break;
+		}
 	}
 	free(buf);
 }
diff --git a/window.c b/window.c
index afea0be..ce9abde 100644
--- a/window.c
+++ b/window.c
@@ -549,6 +549,10 @@ bool windowTimeEnable(void) {
 	return windows[show]->time;
 }
 
+enum Heat windowThresh(uint num) {
+	return windows[num]->thresh;
+}
+
 void windowScroll(enum Scroll by, int n) {
 	struct Window *window = windows[show];
 	switch (by) {
-- 
2.47.3

