Hello,

I am a heavy screen user, and recently started to use hyperlinks in my
terminal apps,
but I've found that screen doesn't supports this, so I have created some
changes to
bring OSC 8 hyperlink supported in screen.

Please find attached my patch wrt to this, feel free to take it in original
or modified form into the screen.

I made the patch on the debian's 5.0.2 patched screen version,available
also on my github (
https://github.com/kedazo/screen#gnu-screen-502--osc-8-hyperlinks ).

Best regards,
David Kedves
From: David Kedves <[email protected]>
Date: Tue, 29 Sep 2026 16:02:36 +0200
Subject: Add OSC 8 hyperlink support
Description: Programs like "ls --hyperlink" print OSC 8 hyperlinks, which
 screen silently dropped. Hyperlinks are now stored per cell (a new link id
 plane next to attr/font/colors), so they survive redraws, window switches,
 split regions, copy mode, resizing and reattaching.
 .
 A process wide link table hands out monotonic, never reused ids and is
 garbage collected by mark & sweep over all windows (incl. scrollback)
 and displays. URIs up to 8192 bytes are accepted; control characters
 (C0, DEL, C1, also UTF-8 encoded) are rejected so nothing can be injected
 into the outer terminal. Links are sent with a screen generated id=, so
 pieces of one link drawn separately stay one link.
 .
 Links are only sent to terminals with the new "HL" capability, which is
 set by default for known capable terminal names (xterm*, *-256color,
 kitty*, foot*, ...) and can be forced with "termcapinfo name HL" or
 "termcapinfo name HL@". The new "hyperlinks on|off" command switches the
 feature globally (default on).
 .
 Also: over long OSC strings are now swallowed up to their terminator
 instead of spilling the rest onto the screen.
Author: David Kedves <[email protected]>
Forwarded: no
Last-Update: 2026-09-29
---
diff --git a/Makefile.in b/Makefile.in
index 8fc308e..aee3524 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -50,7 +50,7 @@ SHELL=/bin/sh
 
 CFILES=	screen.c \
 	acls.c ansi.c attacher.c backtick.c canvas.c comm.c \
-	display.c encoding.c fileio.c help.c input.c kmapdef.c layer.c \
+	display.c encoding.c fileio.c help.c hyperlink.c input.c kmapdef.c layer.c \
 	layout.c list_display.c list_generic.c list_license.o list_window.c logfile.c mark.c \
 	misc.c process.c pty.c resize.c sched.c search.c socket.c telnet.c \
 	term.c termcap.c tty.c utmp.c viewport.c window.c winmsg.c \
@@ -176,116 +176,117 @@ depend.in: $(CFILES) term.h
 
 ### Dependencies:
 screen.o: screen.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h \
  fileio.h mark.h attacher.h encoding.h help.h misc.h process.h socket.h \
  termcap.h tty.h utmp.h
 ansi.o: ansi.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h encoding.h \
  fileio.h help.h mark.h misc.h process.h resize.h
 fileio.o: fileio.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h fileio.h misc.h process.h winmsgbuf.h termcap.h encoding.h
 mark.o: mark.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h encoding.h fileio.h mark.h process.h winmsgbuf.h search.h
 misc.o: misc.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h
 resize.o: resize.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h process.h winmsgbuf.h resize.h telnet.h
 socket.o: socket.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h encoding.h fileio.h list_generic.h misc.h process.h \
  winmsgbuf.h resize.h socket.h termcap.h tty.h utmp.h
 search.o: search.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h mark.h input.h
 tty.o: tty.c config.h screen.h os.h ansi.h sched.h acls.h comm.h layer.h \
- term.h image.h canvas.h display.h layout.h viewport.h window.h logfile.h \
+ term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h logfile.h \
  fileio.h misc.h pty.h telnet.h tty.h
 term.o: term.c term.h
 window.o: window.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h fileio.h help.h \
  input.h mark.h misc.h process.h pty.h resize.h telnet.h termcap.h tty.h \
  utmp.h
 utmp.o: utmp.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h misc.h tty.h utmp.h
 help.o: help.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h misc.h list_generic.h process.h winmsgbuf.h
 termcap.o: termcap.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h encoding.h misc.h process.h winmsgbuf.h resize.h termcap.h
 input.o: input.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h misc.h
 attacher.o: attacher.c config.h screen.h os.h ansi.h sched.h acls.h \
  comm.h layer.h term.h image.h canvas.h display.h layout.h viewport.h \
- window.h logfile.h misc.h socket.h tty.h
+ window.h hyperlink.h logfile.h misc.h socket.h tty.h
 pty.o: pty.c config.h screen.h os.h ansi.h sched.h acls.h comm.h layer.h \
- term.h image.h canvas.h display.h layout.h viewport.h window.h logfile.h
+ term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h logfile.h
 process.o: process.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h encoding.h \
  fileio.h help.h input.h kmapdef.h list_generic.h mark.h misc.h process.h \
  resize.h search.h socket.h telnet.h termcap.h tty.h utmp.h
 display.o: display.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h encoding.h mark.h \
  misc.h process.h pty.h resize.h termcap.h tty.h
 comm.o: comm.c config.h os.h screen.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h
 kmapdef.o: kmapdef.c config.h
 acls.o: acls.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h mark.h misc.h process.h winmsgbuf.h
 logfile.o: logfile.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h misc.h
 layer.o: layer.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h encoding.h mark.h tty.h
 winmsg.o: winmsg.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h fileio.h \
  process.h mark.h
 winmsgbuf.o: winmsgbuf.c winmsgbuf.h screen.h os.h ansi.h sched.h acls.h \
  comm.h layer.h term.h image.h canvas.h display.h layout.h viewport.h \
- window.h logfile.h
+ window.h hyperlink.h logfile.h
 winmsgcond.o: winmsgcond.c winmsgcond.h
+hyperlink.o: hyperlink.c hyperlink.h
 backtick.o: backtick.c backtick.h screen.h os.h ansi.h sched.h acls.h \
  comm.h layer.h term.h image.h canvas.h display.h layout.h viewport.h \
- window.h logfile.h fileio.h
+ window.h hyperlink.h logfile.h fileio.h
 sched.o: sched.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h
 telnet.o: telnet.c config.h comm.h
 encoding.o: encoding.c config.h screen.h os.h ansi.h sched.h acls.h \
  comm.h layer.h term.h image.h canvas.h display.h layout.h viewport.h \
- window.h logfile.h encoding.h fileio.h
+ window.h hyperlink.h logfile.h encoding.h fileio.h
 canvas.o: canvas.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h help.h list_generic.h resize.h
 layout.o: layout.c config.h screen.h os.h ansi.h sched.h acls.h comm.h \
- layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h \
+ layer.h term.h image.h canvas.h display.h layout.h viewport.h window.h hyperlink.h \
  logfile.h fileio.h misc.h process.h winmsgbuf.h resize.h
 viewport.o: viewport.c config.h screen.h os.h ansi.h sched.h acls.h \
  comm.h layer.h term.h image.h canvas.h display.h layout.h viewport.h \
- window.h logfile.h
+ window.h hyperlink.h logfile.h
 list_display.o: list_display.c config.h screen.h os.h ansi.h sched.h \
  acls.h comm.h layer.h term.h image.h canvas.h display.h layout.h \
- viewport.h window.h logfile.h list_generic.h misc.h
+ viewport.h window.h hyperlink.h logfile.h list_generic.h misc.h
 list_generic.o: list_generic.c config.h screen.h os.h ansi.h sched.h \
  acls.h comm.h layer.h term.h image.h canvas.h display.h layout.h \
- viewport.h window.h logfile.h input.h list_generic.h misc.h
+ viewport.h window.h hyperlink.h logfile.h input.h list_generic.h misc.h
 list_window.o: list_window.c config.h screen.h os.h ansi.h sched.h acls.h \
  comm.h layer.h term.h image.h canvas.h display.h layout.h viewport.h \
- window.h logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h input.h \
+ window.h hyperlink.h logfile.h winmsg.h winmsgbuf.h winmsgcond.h backtick.h input.h \
  list_generic.h misc.h process.h
 list_license.o: list_license.c list_generic.h misc.h comm.h
diff --git a/ansi.c b/ansi.c
index 0d93cdf..adcfa11 100644
--- a/ansi.c
+++ b/ansi.c
@@ -39,6 +39,7 @@
 #include "encoding.h"
 #include "fileio.h"
 #include "help.h"
+#include "hyperlink.h"
 #include "logfile.h"
 #include "mark.h"
 #include "misc.h"
@@ -64,8 +65,8 @@ struct mline mline_blank;
 struct mline mline_null;
 
 struct mchar mchar_null;
-struct mchar mchar_blank = { ' ', 0, 0, 0, 0, 0 };
-struct mchar mchar_so = { ' ', A_RV, 0, 0, 0, 0};
+struct mchar mchar_blank = { ' ', 0, 0, 0, 0, 0, 0 };
+struct mchar mchar_so = { ' ', A_RV, 0, 0, 0, 0, 0};
 
 uint64_t renditions[NUM_RENDS] = { 65529 /* =ub */ , 65531 /* =b */ , 65533 /* =u */  };
 
@@ -1205,11 +1206,40 @@ static void StringStart(Window *win, enum string_t type)
 {
 	win->w_StringType = type;
 	win->w_stringp = win->w_string;
+	win->w_stringovf = false;
 	win->w_state = ASTR;
 }
 
+/*
+ * The string buffer is full: OSC strings (e.g. OSC 8 hyperlinks with long
+ * URIs) are swallowed up to their terminator and then dropped by
+ * StringEnd(), instead of spilling the rest onto the screen. Other string
+ * types keep the traditional behaviour of falling back to literal mode.
+ */
+static void StringOverflow(Window *win)
+{
+	if (win->w_StringType == OSC)
+		win->w_stringovf = true;
+	else
+		win->w_state = LIT;
+}
+
+/* only OSC 8 hyperlinks (long URIs) may use the whole, larger buffer */
+static size_t StringLimit(Window *win)
+{
+	if (win->w_StringType == OSC && win->w_stringp - win->w_string >= 2
+	    && win->w_string[0] == '8' && win->w_string[1] == ';')
+		return sizeof(win->w_string);
+	return MAXSTR;
+}
+
 static void StringChar(Window *win, int c)
 {
+	char *end = win->w_string + StringLimit(win) - 1;
+
+	if (win->w_stringovf)
+		return;
+
 	/*
 	 * WriteString() decodes UTF-8 input to Unicode code points.
 	 * String buffers are byte-oriented, so non-ASCII characters must be
@@ -1219,8 +1249,8 @@ static void StringChar(Window *win, int c)
 		char utf8[7];
 		int n = ToUtf8(utf8, c);
 
-		if (win->w_stringp + n > win->w_string + MAXSTR - 1)
-			win->w_state = LIT;
+		if (win->w_stringp + n > end)
+			StringOverflow(win);
 		else {
 			memcpy(win->w_stringp, utf8, n);
 			win->w_stringp += n;
@@ -1228,12 +1258,59 @@ static void StringChar(Window *win, int c)
 		return;
 	}
 
-	if (win->w_stringp >= win->w_string + MAXSTR - 1)
-		win->w_state = LIT;
+	if (win->w_stringp >= end)
+		StringOverflow(win);
 	else
 		*(win->w_stringp)++ = c;
 }
 
+/*****************************************************************/
+/* OSC 8 hyperlinks */
+
+static void MarkLinkLines(struct mline *ml, int n, int w)
+{
+	for (; n > 0; n--, ml++) {
+		if (ml->linkid == NULL || ml->linkid == null)
+			continue;
+		for (int x = 0; x < w; x++)
+			if (ml->linkid[x])
+				hl_mark(ml->linkid[x]);
+	}
+}
+
+/* free the links no window and no display refers to anymore */
+static void HyperlinkGC(void)
+{
+	hl_gc_begin();
+	for (Window *w = mru_window; w; w = w->w_prev_mru) {
+		MarkLinkLines(w->w_mlines, w->w_height, w->w_width + 1);
+		MarkLinkLines(w->w_hlines, w->w_histheight, w->w_width + 1);
+		MarkLinkLines(w->w_alt.mlines, w->w_alt.height, w->w_alt.width + 1);
+		MarkLinkLines(w->w_alt.hlines, w->w_alt.histheight, w->w_alt.width + 1);
+		hl_mark(w->w_rend.linkid);	/* saved cursors never hold links */
+	}
+	for (Display *d = displays; d; d = d->d_next) {
+		hl_mark(d->d_rend.linkid);
+		hl_mark(d->d_lpchar.linkid);
+	}
+	hl_gc_end();
+}
+
+/* OSC 8 ; params ; URI: open (or with an empty URI close) a hyperlink */
+static void HyperlinkOSC(Window *win, char *payload)
+{
+	char *app_id, *uri;
+	uint32_t id = 0;
+
+	/* a malformed or rejected link must not extend a previous one */
+	if (hl_parse(payload, &app_id, &uri) == 0 && *uri) {
+		if (hl_gc_wanted())
+			HyperlinkGC();
+		id = hl_intern(app_id, uri);
+	}
+	win->w_rend.linkid = id;
+}
+
 /*
  * Do string processing. Returns -1 if output should be suspended
  * until status is gone.
@@ -1251,12 +1328,21 @@ static int StringEnd(Window *win)
 
 	win->w_state = LIT;
 	*win->w_stringp = '\0';
+	if (win->w_stringovf) {
+		/* over long OSC string, see StringOverflow() */
+		win->w_stringovf = false;
+		return 0;
+	}
 	switch (win->w_StringType) {
 	case OSC:		/* special xterm compatibility hack */
 		if (win->w_string[0] == ';' || (p = strchr(win->w_string, ';')) == NULL)
 			break;
 		typ = atoi(win->w_string);
 		p++;
+		if (typ == 8) {
+			HyperlinkOSC(win, p);
+			break;
+		}
 		if (typ == 83) {	/* 83 = 'S' */
 			/* special execute commands sequence */
 			char *args[MAXARGS];
@@ -1333,7 +1419,8 @@ static int StringEnd(Window *win)
 		}
 		return -1;
 	case DCS:
-		LAY_DISPLAYS(&win->w_layer, AddStr(win->w_string));
+		/* raw passthrough, don't let it land inside an open hyperlink */
+		LAY_DISPLAYS(&win->w_layer, (SetLink(0), AddStr(win->w_string)));
 		break;
 	case AKA:
 		if (win->w_title == win->w_akabuf && !*win->w_string)
@@ -1468,6 +1555,7 @@ static void SaveCursor(Window *win, struct cursor *cursor)
 	cursor->x = win->w_x;
 	cursor->y = win->w_y;
 	cursor->Rend = win->w_rend;
+	cursor->Rend.linkid = 0;	/* see RestoreCursor() */
 	cursor->Charset = win->w_Charset;
 	cursor->CharsetR = win->w_CharsetR;
 	memmove((char *)cursor->Charsets, (char *)win->w_charsets, 4 * sizeof(int));
@@ -1480,7 +1568,10 @@ static void RestoreCursor(Window *win, struct cursor *cursor)
 	LGotoPos(&win->w_layer, cursor->x, cursor->y);
 	win->w_x = cursor->x;
 	win->w_y = cursor->y;
+	/* like xterm/VTE: an open hyperlink is not part of the saved state */
+	uint32_t linkid = win->w_rend.linkid;
 	win->w_rend = cursor->Rend;
+	win->w_rend.linkid = linkid;
 	memmove((char *)win->w_charsets, (char *)cursor->Charsets, 4 * sizeof(int));
 	win->w_Charset = cursor->Charset;
 	win->w_CharsetR = cursor->CharsetR;
@@ -1957,6 +2048,13 @@ static void MFixLine(Window *win, int y, struct mchar *mc)
 			WMsg(win, 0, "Warning: no space for color foreground - turned off");
 		}
 	}
+	if (mc->linkid && ml->linkid == null) {
+		if ((ml->linkid = calloc(win->w_width + 1, 4)) == NULL) {
+			ml->linkid = null;
+			mc->linkid = win->w_rend.linkid = 0;
+			WMsg(win, 0, "Warning: no space for hyperlink - turned off");
+		}
+	}
 }
 
 /*****************************************************************/
@@ -2046,6 +2144,9 @@ static void MScrollV(Window *win, int n, int ys, int ye, int bce)
 			if (ml->colorfg != null)
 				free(ml->colorfg);
 			ml->colorfg = null;
+			if (ml->linkid != null)
+				free(ml->linkid);
+			ml->linkid = null;
 			memmove(ml->image, blank, (win->w_width + 1) * 4);
 			if (bce)
 				MBceLine(win, i, 0, win->w_width, bce);
@@ -2079,6 +2180,9 @@ static void MScrollV(Window *win, int n, int ys, int ye, int bce)
 			if (ml->colorfg != null)
 				free(ml->colorfg);
 			ml->colorfg = null;
+			if (ml->linkid != null)
+				free(ml->linkid);
+			ml->linkid = null;
 			memmove(ml->image, blank, (win->w_width + 1) * 4);
 			if (bce)
 				MBceLine(win, i, 0, win->w_width, bce);
@@ -2259,6 +2363,12 @@ static void WAddLineToHist(Window *win, struct mline *ml)
 	o = hml->colorfg;
 	hml->colorfg = q;
 	ml->colorfg = null;
+	if (o != null)
+		free(o);
+	q = ml->linkid;
+	o = hml->linkid;
+	hml->linkid = q;
+	ml->linkid = null;
 	if (o != null)
 		free(o);
 
@@ -2282,6 +2392,8 @@ int MFindUsedLine(Window *win, int ye, int ys)
 			break;
 		if (ml->colorfg != null && memcmp(ml->colorfg, null, win->w_width * 4))
 			break;
+		if (ml->linkid != null && memcmp(ml->linkid, null, win->w_width * 4))
+			break;
 		if (win->w_encoding == UTF8) {
 			if (ml->font != null && memcmp(ml->font, null, win->w_width))
 				break;
diff --git a/comm.c b/comm.c
index dd7f650..ba954da 100644
--- a/comm.c
+++ b/comm.c
@@ -128,6 +128,7 @@ struct comm comms[RC_LAST + 1] =
   { "help",		NEED_LAYER|ARGS_02,		{NULL} },
   { "history",		NEED_DISPLAY|NEED_FORE|ARGS_0,	{NULL} },
   { "hstatus",		NEED_FORE|ARGS_1,		{NULL} },
+  { "hyperlinks",	ARGS_01,			{NULL} },
   { "idle",		ARGS_0|ARGS_ORMORE,		{NULL} },
   { "ignorecase",	ARGS_01,			{NULL} },
   { "info",		CAN_QUERY|NEED_LAYER|ARGS_0,	{NULL} },
diff --git a/display.c b/display.c
index e129682..7ab9182 100644
--- a/display.c
+++ b/display.c
@@ -44,6 +44,7 @@
 
 #include "canvas.h"
 #include "encoding.h"
+#include "hyperlink.h"
 #include "mark.h"
 #include "misc.h"
 #include "process.h"
@@ -78,6 +79,9 @@ static void INSERTCHAR(uint32_t);
 static void RAW_PUTCHAR(uint32_t);
 static void SetBackColor(int);
 static void RemoveStatusMinWait(void);
+static void AddStrRaw(const char *);
+static void LinkOff(void);
+static void LinkReset(void);
 
 Display *display, *displays;
 
@@ -330,6 +334,8 @@ void InitTerm(int adapt)
 	AddCStr(D_ME);
 	AddCStr(D_EA);
 	AddCStr(D_CE0);
+	/* no hyperlink is open yet; don't emit anything here, a later
+	 * "hyperlinks off" in the rc file must be able to prevent all OSC 8 */
 	D_rend = mchar_null;
 	D_atyp = 0;
 	if (adapt == 0)
@@ -858,6 +864,7 @@ void ClearArea(int x1, int y1, int xs, int xe, int x2, int y2, int bce, int usel
 		xs = x1;
 	if (xe == -1)
 		xe = x2;
+	LinkOff();		/* erased cells must not become part of a link */
 	if (D_UT)		/* Safe to erase ? */
 		SetRendition(&mchar_null);
 	if (D_BE)
@@ -985,6 +992,7 @@ void ScrollH(int y, int xs, int xe, int n, int bce, struct mline *oml)
 		/* UpdateLine(oml, y, xs, xe); */
 		return;
 	}
+	LinkOff();
 	GotoPos(xs, y);
 	if (D_UT)
 		SetRendition(&mchar_null);
@@ -1054,6 +1062,7 @@ void ScrollV(int xs, int ys, int xe, int ye, int n, int bce)
 
 	if (n == 0)
 		return;
+	LinkOff();
 	if (n >= ye - ys + 1 || -n >= ye - ys + 1) {
 		ClearArea(xs, ys, xs, xe, xe, ye, bce, 0);
 		return;
@@ -1412,6 +1421,8 @@ void SetRendition(struct mchar *mc)
 		SetColor(mc->colorfg, mc->colorbg);
 	if (D_rend.font != mc->font)
 		SetFont(mc->font);
+	if (D_rend.linkid != mc->linkid)
+		SetLink(mc->linkid);
 }
 
 void SetRenditionMline(struct mline *ml, int x)
@@ -1428,6 +1439,79 @@ void SetRenditionMline(struct mline *ml, int x)
 	}
 	if (D_rend.font != ml->font[x])
 		SetFont(ml->font[x]);
+	if (D_rend.linkid != ml->linkid[x])
+		SetLink(ml->linkid[x]);
+}
+
+/*
+ * OSC 8 hyperlinks
+ *
+ * D_rend.linkid is the link currently open on the terminal. It is tracked
+ * even for displays that don't get hyperlinks, only the output is skipped.
+ * Links are independent of SGR: ME ("\033[m") does not close them.
+ */
+
+static bool HyperlinksOn(void)
+{
+	return hyperlinks && D_CHL;
+}
+
+static void EmitLink(uint32_t id)
+{
+	static pid_t pid;
+	const char *uri = hl_uri(id);
+	char buf[48];
+
+	if (uri) {
+		if (!pid)
+			pid = getpid();
+		/* our own id: pieces of a link drawn separately stay one link,
+		 * and links of different windows/sessions never merge */
+		sprintf(buf, "\033]8;id=scr%ld-%lu;", (long)pid, (unsigned long)id);
+		AddStrRaw(buf);
+		AddStrRaw(uri);	/* already UTF-8, see StringChar() */
+	} else
+		AddStrRaw("\033]8;;");
+	AddStrRaw("\033\\");
+}
+
+void SetLink(uint32_t id)
+{
+	if (!display || D_rend.linkid == id)
+		return;
+	if (HyperlinksOn())
+		EmitLink(id);
+	D_rend.linkid = id;
+}
+
+/* close the open link, e.g. before the terminal erases or scrolls cells */
+static void LinkOff(void)
+{
+	if (D_rend.linkid)
+		SetLink(0);
+}
+
+/* the terminal's link state is unknown (fresh or flushed output): close */
+static void LinkReset(void)
+{
+	if (HyperlinksOn())
+		EmitLink(0);
+	D_rend.linkid = 0;
+}
+
+/* runtime switch of the global "hyperlinks" setting */
+void SetHyperlinks(bool on)
+{
+	Display *olddisplay = display;
+
+	if (on == hyperlinks)
+		return;
+	/* close open links while we still may talk OSC 8 */
+	if (!on)
+		for (display = displays; display; display = display->d_next)
+			LinkOff();
+	display = olddisplay;
+	hyperlinks = on;
 }
 
 void MakeStatus(char *msg)
@@ -1906,6 +1990,7 @@ void ClearLine(struct mline *oml, int y, int from, int to, int bce)
 	int x;
 	struct mchar bcechar;
 
+	LinkOff();
 	if (D_UT)		/* Safe to erase ? */
 		SetRendition(&mchar_null);
 	if (D_BE)
@@ -2397,6 +2482,7 @@ void NukePending(void)
 	AddCStr(D_CE0);
 	D_rend = mchar_null;
 	D_atyp = 0;
+	LinkReset();
 	AddCStr(D_DS);
 	D_hstatus = false;
 	AddCStr(D_VE);
@@ -2987,6 +3073,7 @@ void KillBlanker(void)
 	AddCStr(D_CE0);
 	D_rend = mchar_null;
 	D_atyp = 0;
+	LinkReset();
 	D_curvis = 0;
 	D_x = D_y = -1;
 	ChangeScrollRegion(oldtop, oldbot);
diff --git a/display.h b/display.h
index 96a76c1..32dcd6a 100644
--- a/display.h
+++ b/display.h
@@ -362,6 +362,8 @@ void  BracketedPasteMode (bool);
 void  CursorStyle (int);
 void  SetRendition (struct mchar *);
 void  SetRenditionMline (struct mline *, int);
+void  SetLink (uint32_t);
+void  SetHyperlinks (bool);
 void  MakeStatus (char *);
 void  RemoveStatus (void);
 int   ResizeDisplay (int, int);
diff --git a/doc/screen.1 b/doc/screen.1
index 3270bb7..7fdb3af 100644
--- a/doc/screen.1
+++ b/doc/screen.1
@@ -2406,6 +2406,17 @@ scrollback buffer).
 Change the window's hardstatus line to the string \fIstatus\fP.
 .RE
 .TP
+.BR "hyperlinks " [ on | off ]
+.RS 0
+.PP
+Enables or disables forwarding of OSC 8 hyperlinks (as printed e.g. by
+\*Qls \-\-hyperlink\*U) to the terminal. Hyperlinks are stored with the text,
+so they survive redraws, window switches, split regions, copy mode and
+reattaching. They are only sent to terminals that support them, see the
+\fBHL\fP capability. Without an argument the current setting is displayed.
+Default is `on'.
+.RE
+.TP
 .IR "\fBidle\fR " [ timeout [ "cmd-args" ]]
 .RS 0
 .PP
@@ -5066,6 +5077,13 @@ Terminal needs bold to display high-intensity colors (e.g. Eterm).
 .TP 13
 .BI TF "	(bool)"
 Add missing capabilities to the termcap/info entry. (Set by default).
+.TP 13
+.BI HL "	(bool)"
+Terminal supports OSC 8 hyperlinks. Set by default for terminals whose name
+matches xterm*, *\-256color, *\-direct, foot*, kitty*, alacritty*, wezterm*,
+ghostty*, contour*, tmux*, rxvt\-unicode*, mintty*, iterm*, vte*, gnome*,
+konsole* or st\-*. Use \*Qtermcapinfo name HL\*U to enable or
+\*Qtermcapinfo name HL@\*U to disable hyperlinks for a terminal.
 
 .SH CHARACTER TRANSLATION
 \fIScreen\fP has a powerful mechanism to translate characters to arbitrary
diff --git a/doc/screen.texinfo b/doc/screen.texinfo
index 4cfbd31..e50e622 100644
--- a/doc/screen.texinfo
+++ b/doc/screen.texinfo
@@ -1348,6 +1348,10 @@ Find previous command beginning @dots{}.
 Change the window's hardstatus line.
 @xref{Hardstatus}.
 
+@item hyperlinks [@var{state}]
+Forward OSC 8 hyperlinks to the terminal.
+@xref{Special Capabilities}.
+
 @item idle [@var{timeout} [@var{cmd} @var{args}]]
 Define a screen saver command.
 @xref{Screen Saver}.
@@ -5282,8 +5286,26 @@ Terminal needs bold to display high-intensity colors (e.g. Eterm).
 @item TF
 (bool)@*
 Add missing capabilities to the termcap/info entry. (Set by default).
+
+@item HL
+(bool)@*
+Terminal supports OSC 8 hyperlinks. Set by default for terminals whose
+name matches xterm*, *-256color, *-direct, foot*, kitty*, alacritty*,
+wezterm*, ghostty*, contour*, tmux*, rxvt-unicode*, mintty*, iterm*, vte*,
+gnome*, konsole* or st-*. Use @samp{termcapinfo name HL} to enable or
+@samp{termcapinfo name HL@@} to disable hyperlinks for a terminal.
 @end table
 
+@deffn Command hyperlinks [@code{on|off}]
+(none)@*
+Enables or disables forwarding of OSC 8 hyperlinks (as printed e.g. by
+@samp{ls --hyperlink}) to the terminal. Hyperlinks are stored with the
+text, so they survive redraws, window switches, split regions, copy mode
+and reattaching. They are only sent to terminals with the @code{HL}
+capability. Without an argument the current setting is displayed.
+Default is @samp{on}.
+@end deffn
+
 @node Autonuke
 @section Autonuke
 @deffn Command autonuke @var{state}
diff --git a/encoding.c b/encoding.c
index b9f210b..c32a213 100644
--- a/encoding.c
+++ b/encoding.c
@@ -506,6 +506,7 @@ struct mline *recode_mline(struct mline *ml, int w, int from, int to)
 	rl->attr = ml->attr;
 	rl->colorbg = ml->colorbg;
 	rl->colorfg = ml->colorfg;
+	rl->linkid = ml->linkid;
 	for (i = 0; i < w; i++) {
 		c = ml->image[i] | (ml->font[i] << 8);
 		if (from != UTF8 && c < 256)
diff --git a/image.h b/image.h
index 29a4e9a..33d6c9b 100644
--- a/image.h
+++ b/image.h
@@ -40,6 +40,7 @@ struct mchar {
 	uint32_t colorbg;	/* background color */
 	uint32_t colorfg;	/* foreground color */
 	uint32_t mbcs;		/* used for multi byte character sets; TODO: possible to remove? use image now that it has 32 bits*/
+	uint32_t linkid;	/* OSC 8 hyperlink id (see hyperlink.h), 0 = none */
 };
 
 struct mline {
@@ -48,6 +49,7 @@ struct mline {
 	uint32_t *font;
 	uint32_t *colorbg;
 	uint32_t *colorfg;
+	uint32_t *linkid;
 };
 
 
@@ -58,6 +60,7 @@ struct mline {
 	memmove(mline_old.font,    (ml)->font,    (n) * 4);	\
 	memmove(mline_old.colorbg, (ml)->colorbg, (n) * 4);	\
 	memmove(mline_old.colorfg, (ml)->colorfg, (n) * 4);	\
+	memmove(mline_old.linkid,  (ml)->linkid,  (n) * 4);	\
 }
 
 #define copy_mline(ml, xf, xt, n) {					\
@@ -66,6 +69,7 @@ struct mline {
 	memmove((ml)->font    + (xt), (ml)->font    + (xf), (n) * 4);	\
 	memmove((ml)->colorbg + (xt), (ml)->colorbg + (xf), (n) * 4);	\
 	memmove((ml)->colorfg + (xt), (ml)->colorfg + (xf), (n) * 4);	\
+	memmove((ml)->linkid  + (xt), (ml)->linkid  + (xf), (n) * 4);	\
 }
 
 #define clear_mline(ml, x, n) {							\
@@ -74,6 +78,7 @@ struct mline {
 	if ((ml)->font    != null) memset((ml)->font    + (x), 0, (n) * 4);	\
 	if ((ml)->colorbg != null) memset((ml)->colorbg + (x), 0, (n) * 4);	\
 	if ((ml)->colorfg != null) memset((ml)->colorfg + (x), 0, (n) * 4);	\
+	if ((ml)->linkid  != null) memset((ml)->linkid  + (x), 0, (n) * 4);	\
 }
 
 #define cmp_mline(ml1, ml2, x) (			\
@@ -82,6 +87,7 @@ struct mline {
 	&& (ml1)->font[x]    == (ml2)->font[x]		\
 	&& (ml1)->colorbg[x] == (ml2)->colorbg[x]	\
 	&& (ml1)->colorfg[x] == (ml2)->colorfg[x]	\
+	&& (ml1)->linkid[x]  == (ml2)->linkid[x]	\
 )
 
 #define cmp_mchar(mc1, mc2) (				\
@@ -90,6 +96,7 @@ struct mline {
 	&& (mc1)->font    == (mc2)->font		\
 	&& (mc1)->colorbg == (mc2)->colorbg		\
 	&& (mc1)->colorfg == (mc2)->colorfg		\
+	&& (mc1)->linkid  == (mc2)->linkid		\
 )
 
 #define cmp_mchar_mline(mc, ml, x) (			\
@@ -98,6 +105,7 @@ struct mline {
 	&& (mc)->font    == (ml)->font[x]		\
 	&& (mc)->colorbg == (ml)->colorbg[x]		\
 	&& (mc)->colorfg == (ml)->colorfg[x]		\
+	&& (mc)->linkid  == (ml)->linkid[x]		\
 )
 
 #define copy_mchar2mline(mc, ml, x) {			\
@@ -106,6 +114,7 @@ struct mline {
 	(ml)->font[x]    = (mc)->font;			\
 	(ml)->colorbg[x] = (mc)->colorbg;		\
 	(ml)->colorfg[x] = (mc)->colorfg;		\
+	(ml)->linkid[x]  = (mc)->linkid;		\
 }
 
 #define copy_mline2mchar(mc, ml, x) {			\
@@ -114,6 +123,7 @@ struct mline {
 	(mc)->font    = (ml)->font[x];			\
 	(mc)->colorbg = (ml)->colorbg[x];		\
 	(mc)->colorfg = (ml)->colorfg[x];		\
+	(mc)->linkid  = (ml)->linkid[x];		\
 	(mc)->mbcs    = 0;				\
 }
 
diff --git a/layer.c b/layer.c
index d7e82bf..6f4c460 100644
--- a/layer.c
+++ b/layer.c
@@ -56,6 +56,7 @@ static struct mline *mlineoffset(const struct mline *ml, const int offset)
 	mml.font = ml->font + offset;
 	mml.colorbg = ml->colorbg + offset;
 	mml.colorfg = ml->colorfg + offset;
+	mml.linkid = ml->linkid + offset;
 	return &mml;
 }
 
diff --git a/process.c b/process.c
index edb4996..25b7be8 100644
--- a/process.c
+++ b/process.c
@@ -4682,6 +4682,20 @@ static void DoCommandTruecolor(struct action *act)
 		Activate(-1);
 }
 
+static void DoCommandHyperlinks(struct action *act)
+{
+	bool b;
+
+	if (!act->args[0]) {
+		OutputMsg(0, "OSC 8 hyperlinks are turned %s", hyperlinks ? "on" : "off");
+		return;
+	}
+	if (ParseOnOff(act, &b) == 0 && b != hyperlinks) {
+		SetHyperlinks(b);
+		Activate(-1);
+	}
+}
+
 void DoAction(struct action *act)
 {
 	int nr = act->nr;
@@ -5198,6 +5212,9 @@ void DoAction(struct action *act)
 	case RC_DEFHSTATUS:
 		DoCommandDefhstatus(act);
 		break;
+	case RC_HYPERLINKS:
+		DoCommandHyperlinks(act);
+		break;
 	case RC_HSTATUS:
 		DoCommandHstatus(act);
 		break;
@@ -6305,6 +6322,12 @@ static void ShowDInfo(void)
 		l -= w;
 		p += w;
 	}
+	if (D_CHL && hyperlinks) {
+		strncpy(p, " hyperlinks", l);
+		w = strlen(p);
+		l -= w;
+		p += w;
+	}
 	if (D_CG0)
 		strncpy(p, " iso2022", l);
 	else if (D_CS0 && *D_CS0)
diff --git a/resize.c b/resize.c
index 895b96a..7e92ccb 100644
--- a/resize.c
+++ b/resize.c
@@ -59,7 +59,8 @@ static struct mline mline_zero = {
 	.attr    = NULL,
 	.font    = NULL,
 	.colorbg = NULL,
-	.colorfg = NULL
+	.colorfg = NULL,
+	.linkid  = NULL
 };
 
 /*
@@ -314,6 +315,8 @@ static void FreeMline(struct mline *ml)
 		free(ml->colorbg);
 	if (ml->colorfg && ml->colorfg != null)
 		free(ml->colorfg);
+	if (ml->linkid && ml->linkid != null)
+		free(ml->linkid);
 	*ml = mline_zero;
 }
 
@@ -324,6 +327,7 @@ static int AllocMline(struct mline *ml, int w)
 	ml->font = null;
 	ml->colorbg = null;
 	ml->colorfg = null;
+	ml->linkid = null;
 	if (ml->image == NULL)
 		return -1;
 	return 0;
@@ -358,6 +362,12 @@ static int BcopyMline(struct mline *mlf, int xf, struct mline *mlt, int xt, int
 	}
 	if (mlt->colorfg != null)
 		memmove(mlt->colorfg + xt, mlf->colorfg + xf, l * 4);
+	if (mlf->linkid != null && mlt->linkid == null) {
+		if ((mlt->linkid = calloc(w, 4)) == NULL)
+			mlt->linkid = null, r = -1;
+	}
+	if (mlt->linkid != null)
+		memmove(mlt->linkid + xt, mlf->linkid + xf, l * 4);
 	return r;
 }
 
@@ -383,7 +393,8 @@ static void CheckMaxSize(int wi)
 	mline_old.font = xrealloc(mline_old.font, maxwidth * 4);
 	mline_old.colorbg = xrealloc(mline_old.colorbg, maxwidth * 4);
 	mline_old.colorfg = xrealloc(mline_old.colorfg, maxwidth * 4);
-	if (!(blank && null && mline_old.image && mline_old.attr && mline_old.font && mline_old.colorbg && mline_old.colorfg))
+	mline_old.linkid = xrealloc(mline_old.linkid, maxwidth * 4);
+	if (!(blank && null && mline_old.image && mline_old.attr && mline_old.font && mline_old.colorbg && mline_old.colorfg && mline_old.linkid))
 		Panic(0, "%s", strnomem);
 
 	MakeBlankLine(blank, maxwidth);
@@ -399,6 +410,8 @@ static void CheckMaxSize(int wi)
 	mline_null.colorbg = null;
 	mline_blank.colorfg = null;
 	mline_null.colorfg = null;
+	mline_blank.linkid = null;
+	mline_null.linkid = null;
 
 #define RESET_AFC(x, bl)	\
 do {				\
@@ -415,6 +428,7 @@ do {						\
 		RESET_AFC(ml->font, null);	\
 		RESET_AFC(ml->colorbg, null);	\
 		RESET_AFC(ml->colorfg, null);	\
+		RESET_AFC(ml->linkid, null);	\
 	}					\
 } while (0)
 
diff --git a/screen.c b/screen.c
index 93d5a9f..11c473c 100644
--- a/screen.c
+++ b/screen.c
@@ -142,6 +142,7 @@ int       rflag;
 int       dflag;
 int       queryflag = -1;
 bool      hastruecolor = false;
+bool      hyperlinks = true;	/* forward OSC 8 hyperlinks to capable displays */
 
 char     *multi;
 int       multiattach;
diff --git a/screen.h b/screen.h
index efb2835..3b571fe 100644
--- a/screen.h
+++ b/screen.h
@@ -274,6 +274,7 @@ extern bool cjkwidth;
 extern bool default_startup;
 extern bool do_auth;
 extern bool hastruecolor;
+extern bool hyperlinks;
 extern bool iflag;
 extern bool logtstamp_on;
 extern bool lsflag;
diff --git a/term.c b/term.c
index ec8c6b3..8d40408 100644
--- a/term.c
+++ b/term.c
@@ -168,6 +168,7 @@ struct term term[T_N] =
   { "VN", T_STR  },
   { "TF", T_FLG  },
   { "XT", T_FLG  },
+  { "HL", T_FLG  },
 
 /* d_font setting */
   { "G0", T_FLG  },
diff --git a/termcap.c b/termcap.c
index e971180..514f833 100644
--- a/termcap.c
+++ b/termcap.c
@@ -31,6 +31,8 @@
 #include "termcap.h"
 
 #include <sys/types.h>
+#include <fnmatch.h>
+#include <stdbool.h>
 #include <stdint.h>
 
 /* to get index() declaration on Solaris */
@@ -53,6 +55,8 @@ static int copyarg(char **, char *);
 static int e_tgetent(char *, char *);
 static char *e_tgetstr(char *, char **);
 static int e_tgetflag(char *);
+static int e_tgetflag_extra(char *);
+static bool HyperlinkTerm(const char *);
 static int e_tgetnum(char *);
 static int findseq_ge(char *, int, unsigned char **);
 static void setseqoff(unsigned char *, int, int);
@@ -188,6 +192,11 @@ int InitTermcap(int width, int height)
 		if (D_CXT)
 			D_BE = 1;
 	}
+	/* OSC 8 hyperlinks: "HL" / "HL@" via termcapinfo, else known terminals */
+	if ((t = e_tgetflag_extra("HL")) >= 0)
+		D_CHL = t;
+	else
+		D_CHL = HyperlinkTerm(D_termname);
 	if (nwin_default.flowflag == nwin_undef.flowflag)
 		nwin_default.flowflag = D_CNF ? FLOW_OFF : D_NX ? FLOW_ON : FLOW_AUTOFLAG;
 	D_CLP |= (!D_AM || D_XV || D_XN);
@@ -1181,6 +1190,32 @@ static int e_tgetflag(char *cap)
 	return tgetflag(cap) > 0;
 }
 
+/* 1: cap set, 0: cancelled with "cap@", -1: not given via termcap/terminfo commands */
+static int e_tgetflag_extra(char *cap)
+{
+	char buf[2], *bufp;
+	char *tep;
+	bufp = buf;
+	if ((tep = findcap(cap, &bufp, 2)))
+		return (*tep == '@') ? 0 : 1;
+	return -1;
+}
+
+/* terminals known to support OSC 8 hyperlinks (or to ignore them silently) */
+static bool HyperlinkTerm(const char *termname)
+{
+	static const char *const patterns[] = {
+		"xterm*", "*-256color", "*-direct", "foot*", "kitty*", "alacritty*",
+		"wezterm*", "ghostty*", "contour*", "tmux*", "rxvt-unicode*",
+		"mintty*", "iterm*", "vte*", "gnome*", "konsole*", "st-*",
+	};
+
+	for (size_t i = 0; i < ARRAY_SIZE(patterns); i++)
+		if (fnmatch(patterns[i], termname, 0) == 0)
+			return true;
+	return false;
+}
+
 static int e_tgetnum(char *cap)
 {
 	char buf[20], *bufp;
diff --git a/window.h b/window.h
index e1c1a68..519fac9 100644
--- a/window.h
+++ b/window.h
@@ -40,6 +40,10 @@
 #include "screen.h"
 #include "layer.h"
 #include "display.h"
+#include "hyperlink.h"
+
+/* OSC 8 needs room for "8;", the params and a HL_MAX_URI long URI */
+#define MAXOSCSTR	(HL_MAX_URI + 512)
 
 struct NewWindow {
 	int	StartAt;	/* where to start the search for the slot */
@@ -211,8 +215,9 @@ struct Window {
 	bool     w_c1;			/* enable C1 flag */
 	int	 w_decodestate;		/* state of our input decoder */
 	int	 w_mbcs;		/* saved char for multibytes charset */
-	char	 w_string[MAXSTR];
+	char	 w_string[MAXOSCSTR];	/* only OSC 8 may use more than MAXSTR */
 	char	*w_stringp;
+	bool	 w_stringovf;		/* over long OSC string being swallowed */
 	char	*w_tabs;		/* line with tabs */
 	int	 w_bell;		/* bell status of this window */
 	int	 w_flow;		/* flow flags */
diff --git a/hyperlink.c b/hyperlink.c
new file mode 100644
index 0000000..39fd02f
--- /dev/null
+++ b/hyperlink.c
@@ -0,0 +1,380 @@
+/* Copyright (c) 2026
+ *      David Kedves ([email protected])
+ *
+ * This file is part of GNU screen.
+ *
+ * GNU screen is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program (see the file COPYING); if not, see
+ * <https://www.gnu.org/licenses>.
+ *
+ ****************************************************************
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "hyperlink.h"
+
+struct hl_entry {
+	uint32_t id;
+	uint32_t keyhash;	/* hash of (app_id, uri), valid if app_id != NULL */
+	bool marked;		/* gc mark */
+	char *app_id;		/* id= given by the application, or NULL */
+	char *uri;
+};
+
+/* Two open addressing (linear probing) tables over the same entries:
+ * by_id holds every entry, by_key only those with an application id.
+ * Entries are only ever removed by hl_gc_end()/hl_reset(), which rebuild
+ * the tables from scratch, so no tombstones are needed. */
+static struct hl_entry **by_id;
+static size_t by_id_cap;	/* 0 or a power of two */
+static size_t n_links;
+static struct hl_entry **by_key;
+static size_t by_key_cap;	/* 0 or a power of two */
+static size_t n_keyed;
+
+static uint32_t next_id = 1;
+static size_t gc_threshold = HL_GC_MIN;
+
+static size_t id_slot(uint32_t id, size_t cap)
+{
+	return (size_t)(id * 2654435761u) & (cap - 1);
+}
+
+static uint32_t key_hash(const char *app_id, const char *uri)
+{
+	uint32_t h = 2166136261u;	/* FNV-1a */
+	const unsigned char *p;
+
+	for (p = (const unsigned char *)app_id; *p; p++)
+		h = (h ^ *p) * 16777619u;
+	h *= 16777619u;			/* the '\0' separator */
+	for (p = (const unsigned char *)uri; *p; p++)
+		h = (h ^ *p) * 16777619u;
+	return h;
+}
+
+static void put_id(struct hl_entry **tab, size_t cap, struct hl_entry *e)
+{
+	size_t i = id_slot(e->id, cap);
+
+	while (tab[i])
+		i = (i + 1) & (cap - 1);
+	tab[i] = e;
+}
+
+static void put_key(struct hl_entry **tab, size_t cap, struct hl_entry *e)
+{
+	size_t i = e->keyhash & (cap - 1);
+
+	while (tab[i])
+		i = (i + 1) & (cap - 1);
+	tab[i] = e;
+}
+
+static struct hl_entry *find_id(uint32_t id)
+{
+	size_t i;
+
+	if (id == 0 || by_id_cap == 0)
+		return NULL;
+	for (i = id_slot(id, by_id_cap); by_id[i]; i = (i + 1) & (by_id_cap - 1))
+		if (by_id[i]->id == id)
+			return by_id[i];
+	return NULL;
+}
+
+static struct hl_entry *find_key(const char *app_id, const char *uri, uint32_t h)
+{
+	size_t i;
+
+	if (by_key_cap == 0)
+		return NULL;
+	for (i = h & (by_key_cap - 1); by_key[i]; i = (i + 1) & (by_key_cap - 1))
+		if (by_key[i]->keyhash == h && !strcmp(by_key[i]->app_id, app_id) && !strcmp(by_key[i]->uri, uri))
+			return by_key[i];
+	return NULL;
+}
+
+/* smallest power of two table (min 64) keeping the load factor <= 1/2 */
+static size_t cap_for(size_t n)
+{
+	size_t cap = 64;
+
+	while (cap < 2 * n)
+		cap *= 2;
+	return cap;
+}
+
+/* make room for one more entry in by_id (and by_key if keyed) */
+static bool reserve(bool keyed)
+{
+	struct hl_entry **tab;
+	size_t cap, i;
+
+	cap = cap_for(n_links + 1);
+	if (cap > by_id_cap) {
+		if ((tab = calloc(cap, sizeof(*tab))) == NULL)
+			return false;
+		for (i = 0; i < by_id_cap; i++)
+			if (by_id[i])
+				put_id(tab, cap, by_id[i]);
+		free(by_id);
+		by_id = tab;
+		by_id_cap = cap;
+	}
+	if (!keyed)
+		return true;
+	cap = cap_for(n_keyed + 1);
+	if (cap > by_key_cap) {
+		if ((tab = calloc(cap, sizeof(*tab))) == NULL)
+			return false;
+		for (i = 0; i < by_key_cap; i++)
+			if (by_key[i])
+				put_key(tab, cap, by_key[i]);
+		free(by_key);
+		by_key = tab;
+		by_key_cap = cap;
+	}
+	return true;
+}
+
+static char *dupstr(const char *s)
+{
+	size_t l = strlen(s) + 1;
+	char *d = malloc(l);
+
+	if (d)
+		memcpy(d, s, l);
+	return d;
+}
+
+static void free_entry(struct hl_entry *e)
+{
+	free(e->app_id);
+	free(e->uri);
+	free(e);
+}
+
+/*
+ * Only printable ASCII and well formed UTF-8 for code points >= U+00A0 is
+ * accepted. The URI is sent to the outer terminal again later, so control
+ * characters (C0, DEL and C1 - also in their UTF-8 encoding) must never
+ * get through: they could terminate the OSC early and inject sequences.
+ */
+static bool valid_text(const char *s, bool ascii_only)
+{
+	const unsigned char *p = (const unsigned char *)s;
+
+	while (*p) {
+		uint32_t c = *p, cp;
+		int n, i;
+
+		if (c >= 0x20 && c < 0x7f) {
+			p++;
+			continue;
+		}
+		if (ascii_only)
+			return false;
+		if (c >= 0xc2 && c <= 0xdf)
+			n = 1, cp = c & 0x1f;
+		else if (c >= 0xe0 && c <= 0xef)
+			n = 2, cp = c & 0x0f;
+		else if (c >= 0xf0 && c <= 0xf4)
+			n = 3, cp = c & 0x07;
+		else
+			return false;
+		for (i = 1; i <= n; i++) {
+			if ((p[i] & 0xc0) != 0x80)
+				return false;
+			cp = cp << 6 | (p[i] & 0x3f);
+		}
+		if (cp < 0xa0 || (n == 2 && cp < 0x800) || (n == 3 && cp < 0x10000)
+		    || (cp >= 0xd800 && cp <= 0xdfff) || cp > 0x10ffff)
+			return false;
+		p += n + 1;
+	}
+	return true;
+}
+
+static bool valid_app_id(const char *app_id)
+{
+	return app_id && *app_id && strlen(app_id) <= HL_MAX_ID && valid_text(app_id, true);
+}
+
+int hl_parse(char *payload, char **app_id, char **uri)
+{
+	char *sep, *p, *next;
+
+	*app_id = NULL;
+	*uri = NULL;
+	if (payload == NULL || (sep = strchr(payload, ';')) == NULL)
+		return -1;
+	*sep = '\0';
+	if (strlen(sep + 1) > HL_MAX_URI || !valid_text(sep + 1, false))
+		return -1;
+	*uri = sep + 1;
+	/* params are "key=value" pairs separated by ':', we only care for id */
+	for (p = payload; p; p = next) {
+		if ((next = strchr(p, ':')) != NULL)
+			*next++ = '\0';
+		if (!strncmp(p, "id=", 3))
+			*app_id = valid_app_id(p + 3) ? p + 3 : NULL;
+	}
+	return 0;
+}
+
+static uint32_t new_id(void)
+{
+	uint32_t id;
+
+	do {
+		id = next_id++;
+		if (next_id == 0)
+			next_id = 1;
+	} while (id == 0 || find_id(id));
+	return id;
+}
+
+uint32_t hl_intern(const char *app_id, const char *uri)
+{
+	struct hl_entry *e;
+	uint32_t h = 0;
+
+	if (uri == NULL || *uri == '\0' || strlen(uri) > HL_MAX_URI || !valid_text(uri, false))
+		return 0;
+	if (app_id && !valid_app_id(app_id))
+		app_id = NULL;
+	if (app_id) {
+		h = key_hash(app_id, uri);
+		if ((e = find_key(app_id, uri, h)) != NULL)
+			return e->id;
+	}
+	if (!reserve(app_id != NULL))
+		return 0;
+	if ((e = calloc(1, sizeof(*e))) == NULL)
+		return 0;
+	e->uri = dupstr(uri);
+	e->app_id = app_id ? dupstr(app_id) : NULL;
+	if (e->uri == NULL || (app_id && e->app_id == NULL)) {
+		free_entry(e);
+		return 0;
+	}
+	e->keyhash = h;
+	e->id = new_id();
+	put_id(by_id, by_id_cap, e);
+	n_links++;
+	if (app_id) {
+		put_key(by_key, by_key_cap, e);
+		n_keyed++;
+	}
+	return e->id;
+}
+
+const char *hl_uri(uint32_t id)
+{
+	struct hl_entry *e = find_id(id);
+
+	return e ? e->uri : NULL;
+}
+
+size_t hl_count(void)
+{
+	return n_links;
+}
+
+bool hl_gc_wanted(void)
+{
+	return n_links >= gc_threshold;
+}
+
+void hl_gc_begin(void)
+{
+	size_t i;
+
+	for (i = 0; i < by_id_cap; i++)
+		if (by_id[i])
+			by_id[i]->marked = false;
+}
+
+void hl_mark(uint32_t id)
+{
+	struct hl_entry *e = find_id(id);
+
+	if (e)
+		e->marked = true;
+}
+
+void hl_gc_end(void)
+{
+	struct hl_entry **nid, **nkey = NULL;
+	size_t live = 0, keyed = 0, idcap, keycap = 0, i;
+
+	for (i = 0; i < by_id_cap; i++)
+		if (by_id[i] && by_id[i]->marked) {
+			live++;
+			if (by_id[i]->app_id)
+				keyed++;
+		}
+	idcap = cap_for(live);
+	nid = calloc(idcap, sizeof(*nid));
+	if (keyed) {
+		keycap = cap_for(keyed);
+		nkey = calloc(keycap, sizeof(*nkey));
+	}
+	if (nid == NULL || (keyed && nkey == NULL)) {
+		/* collection is only an optimisation: keep everything, retry later */
+		free(nid);
+		free(nkey);
+		gc_threshold = 2 * n_links > HL_GC_MIN ? 2 * n_links : HL_GC_MIN;
+		return;
+	}
+	for (i = 0; i < by_id_cap; i++) {
+		struct hl_entry *e = by_id[i];
+
+		if (e == NULL)
+			continue;
+		if (!e->marked) {
+			free_entry(e);
+			continue;
+		}
+		put_id(nid, idcap, e);
+		if (e->app_id)
+			put_key(nkey, keycap, e);
+	}
+	free(by_id);
+	free(by_key);
+	by_id = nid;
+	by_id_cap = idcap;
+	n_links = live;
+	by_key = nkey;
+	by_key_cap = keycap;
+	n_keyed = keyed;
+	gc_threshold = 2 * live > HL_GC_MIN ? 2 * live : HL_GC_MIN;
+}
+
+void hl_reset(void)
+{
+	size_t i;
+
+	for (i = 0; i < by_id_cap; i++)
+		if (by_id[i])
+			free_entry(by_id[i]);
+	free(by_id);
+	free(by_key);
+	by_id = by_key = NULL;
+	by_id_cap = by_key_cap = 0;
+	n_links = n_keyed = 0;
+	gc_threshold = HL_GC_MIN;
+}
diff --git a/hyperlink.h b/hyperlink.h
new file mode 100644
index 0000000..45b9265
--- /dev/null
+++ b/hyperlink.h
@@ -0,0 +1,82 @@
+/* Copyright (c) 2026
+ *      David Kedves ([email protected])
+ *
+ * This file is part of GNU screen.
+ *
+ * GNU screen is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program (see the file COPYING); if not, see
+ * <https://www.gnu.org/licenses>.
+ *
+ ****************************************************************
+ */
+
+#ifndef SCREEN_HYPERLINK_H
+#define SCREEN_HYPERLINK_H
+
+/*
+ * OSC 8 hyperlink table.
+ *
+ * Every cell carries a 32 bit link id (struct mchar.linkid), 0 meaning
+ * "no link". Ids are process wide, handed out monotonically and never
+ * reused, so a stale id can only ever miss on lookup; it can never
+ * resolve to somebody else's URI.
+ *
+ * Unreferenced entries are reclaimed by mark & sweep: when hl_gc_wanted()
+ * says so, the caller runs hl_gc_begin(), hl_mark()s every id still
+ * referenced anywhere and finishes with hl_gc_end().
+ *
+ * This module deliberately knows nothing about windows or displays so it
+ * can be unit tested on its own (tests/test-hyperlink.c).
+ */
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#define HL_MAX_URI	8192	/* longest URI we store */
+#define HL_MAX_ID	250	/* longest application supplied id= we honour */
+#define HL_GC_MIN	1024	/* never collect below this many live links */
+
+/*
+ * Split an OSC 8 payload ("params;URI", i.e. the part after "8;") in place.
+ * *app_id is set to the id= parameter (NULL if absent or too long), *uri to
+ * the URI ("" means: close the current link). Returns -1 for a malformed
+ * payload (no ';' separator or URI longer than HL_MAX_URI), 0 otherwise.
+ */
+int hl_parse(char *payload, char **app_id, char **uri);
+
+/*
+ * Register a link and return its id. Links with the same application id
+ * and URI share one id (so an application's multi part link stays one
+ * link); links without an application id always get a fresh one.
+ * Returns 0 if the link could not be stored (out of memory, bad input).
+ */
+uint32_t hl_intern(const char *app_id, const char *uri);
+
+/* URI of a link, or NULL if the id is 0 or unknown (e.g. already collected) */
+const char *hl_uri(uint32_t id);
+
+/* number of live links */
+size_t hl_count(void);
+
+/* true once enough links accumulated that a collection is worthwhile */
+bool hl_gc_wanted(void);
+
+void hl_gc_begin(void);
+void hl_mark(uint32_t id);
+void hl_gc_end(void);
+
+/* drop every link (id numbering continues) */
+void hl_reset(void);
+
+#endif /* SCREEN_HYPERLINK_H */
diff --git a/tests/test-hyperlink.c b/tests/test-hyperlink.c
new file mode 100644
index 0000000..6caebb6
--- /dev/null
+++ b/tests/test-hyperlink.c
@@ -0,0 +1,251 @@
+/* Copyright (c) 2026
+ *      David Kedves ([email protected])
+ *
+ * This file is part of GNU screen.
+ *
+ * GNU screen is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program (see the file COPYING); if not, see
+ * <https://www.gnu.org/licenses>.
+ *
+ ****************************************************************
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include "../hyperlink.h"
+#include "signature.h"
+#include "macros.h"
+
+SIGNATURE_CHECK(hl_parse, int, (char *, char **, char **));
+SIGNATURE_CHECK(hl_intern, uint32_t, (const char *, const char *));
+SIGNATURE_CHECK(hl_uri, const char *, (uint32_t));
+SIGNATURE_CHECK(hl_count, size_t, (void));
+SIGNATURE_CHECK(hl_gc_wanted, bool, (void));
+SIGNATURE_CHECK(hl_gc_begin, void, (void));
+SIGNATURE_CHECK(hl_mark, void, (uint32_t));
+SIGNATURE_CHECK(hl_gc_end, void, (void));
+SIGNATURE_CHECK(hl_reset, void, (void));
+
+extern bool _mallocmock_fail;
+
+static int parse(const char *in, char **app_id, char **uri)
+{
+	static char buf[HL_MAX_URI + 1024];
+
+	strcpy(buf, in);
+	return hl_parse(buf, app_id, uri);
+}
+
+static void test_parse(void)
+{
+	static char big[HL_MAX_URI + 16];
+	char *id, *uri;
+
+	/* plain link, no params */
+	ASSERT(parse(";https://example.com/";, &id, &uri) == 0);
+	ASSERT(id == NULL);
+	ASSERT(STREQ(uri, "https://example.com/";));
+
+	/* close */
+	ASSERT(parse(";", &id, &uri) == 0);
+	ASSERT(id == NULL);
+	ASSERT(STREQ(uri, ""));
+
+	/* id param; ';' inside the URI belongs to the URI */
+	ASSERT(parse("id=foo;http://a/b;c";, &id, &uri) == 0);
+	ASSERT(STREQ(id, "foo"));
+	ASSERT(STREQ(uri, "http://a/b;c";));
+
+	/* other params are ignored */
+	ASSERT(parse("x=1:id=bar:y=2;file:///tmp", &id, &uri) == 0);
+	ASSERT(STREQ(id, "bar"));
+	ASSERT(STREQ(uri, "file:///tmp"));
+
+	/* malformed: no separator */
+	ASSERT(parse("https://example.com/";, &id, &uri) == -1);
+	ASSERT(hl_parse(NULL, &id, &uri) == -1);
+
+	/* UTF-8 is fine */
+	ASSERT(parse(";https://example.com/\xc3\xa4\xe2\x82\xac\xf0\x9f\x98\x80";, &id, &uri) == 0);
+
+	/* control characters could inject sequences into the outer terminal */
+	ASSERT(parse(";http://x/\033[31m";, &id, &uri) == -1);
+	ASSERT(parse(";http://x/\x7f";, &id, &uri) == -1);
+	ASSERT(parse(";http://x/\xc2\x9c";, &id, &uri) == -1);	/* C1 ST as UTF-8 */
+	ASSERT(parse(";http://x/\xc2\x80";, &id, &uri) == -1);
+	/* invalid / overlong / surrogate UTF-8 */
+	ASSERT(parse(";http://x/\xe4";, &id, &uri) == -1);
+	ASSERT(parse(";http://x/\xc0\xaf";, &id, &uri) == -1);
+	ASSERT(parse(";http://x/\xe0\x80\xaf";, &id, &uri) == -1);
+	ASSERT(parse(";http://x/\xed\xa0\x80";, &id, &uri) == -1);
+	ASSERT(parse(";http://x/\xf4\x90\x80\x80";, &id, &uri) == -1);
+
+	/* bad ids are dropped, the link itself stays valid */
+	ASSERT(parse("id=\xc3\xa4;http://x/";, &id, &uri) == 0);
+	ASSERT(id == NULL);
+	ASSERT(parse("id=;http://x/";, &id, &uri) == 0);
+	ASSERT(id == NULL);
+	memcpy(big, "id=", 3);
+	memset(big + 3, 'i', HL_MAX_ID + 1);
+	strcpy(big + 3 + HL_MAX_ID + 1, ";http://x/";);
+	ASSERT(parse(big, &id, &uri) == 0);
+	ASSERT(id == NULL);
+
+	/* URI length limit */
+	big[0] = ';';
+	memset(big + 1, 'u', HL_MAX_URI);
+	big[HL_MAX_URI + 1] = '\0';
+	ASSERT(parse(big, &id, &uri) == 0);
+	ASSERT(strlen(uri) == HL_MAX_URI);
+	big[HL_MAX_URI + 1] = 'u';
+	big[HL_MAX_URI + 2] = '\0';
+	ASSERT(parse(big, &id, &uri) == -1);
+}
+
+static void test_intern(void)
+{
+	uint32_t a, b, c, d, e;
+
+	hl_reset();
+	ASSERT(hl_count() == 0);
+	ASSERT(hl_uri(0) == NULL);
+
+	/* without app id every open is a new link */
+	a = hl_intern(NULL, "https://a/";);
+	b = hl_intern(NULL, "https://a/";);
+	ASSERT(a != 0 && b != 0 && a != b);
+	ASSERT(b > a);
+	ASSERT(STREQ(hl_uri(a), "https://a/";));
+	ASSERT(STREQ(hl_uri(b), "https://a/";));
+
+	/* with app id: same (id, uri) is the same link */
+	c = hl_intern("x", "https://a/";);
+	d = hl_intern("x", "https://a/";);
+	e = hl_intern("x", "https://b/";);
+	ASSERT(c != 0 && c == d);
+	ASSERT(e != 0 && e != c);
+	ASSERT(hl_intern("y", "https://a/";) != c);
+	ASSERT(hl_count() == 5);
+
+	/* rejected input */
+	ASSERT(hl_intern(NULL, "") == 0);
+	ASSERT(hl_intern(NULL, NULL) == 0);
+	ASSERT(hl_intern(NULL, "http://x/\033";) == 0);
+	ASSERT(hl_count() == 5);
+
+	/* unknown ids */
+	ASSERT(hl_uri(0xfffffff0u) == NULL);
+
+	hl_reset();
+	ASSERT(hl_count() == 0);
+	ASSERT(hl_uri(a) == NULL);
+	/* numbering continues after a reset: ids are never reused */
+	ASSERT(hl_intern(NULL, "https://c/";) > e);
+	hl_reset();
+}
+
+#define MANY 5000
+
+static void test_gc(void)
+{
+	static uint32_t ids[MANY];
+	uint32_t keyed, maxid = 0;
+	size_t i;
+
+	hl_reset();
+	ASSERT(!hl_gc_wanted());
+	for (i = 0; i < MANY; i++) {
+		char uri[64];
+
+		sprintf(uri, "https://example.com/%zu";, i);
+		ids[i] = hl_intern(NULL, uri);
+		ASSERT(ids[i] != 0);
+		if (ids[i] > maxid)
+			maxid = ids[i];
+	}
+	keyed = hl_intern("keep", "https://keyed/";);
+	ASSERT(hl_count() == MANY + 1);
+	ASSERT(hl_gc_wanted());
+	for (i = 0; i < MANY; i++) {
+		char uri[64];
+
+		sprintf(uri, "https://example.com/%zu";, i);
+		ASSERT(STREQ(hl_uri(ids[i]), uri));
+	}
+
+	/* keep every third link and the keyed one */
+	hl_gc_begin();
+	for (i = 0; i < MANY; i += 3)
+		hl_mark(ids[i]);
+	hl_mark(keyed);
+	hl_mark(0);			/* harmless */
+	hl_mark(0xfffffff0u);		/* unknown: harmless */
+	hl_gc_end();
+
+	ASSERT(hl_count() == (MANY + 2) / 3 + 1);
+	for (i = 0; i < MANY; i++) {
+		if (i % 3 == 0)
+			ASSERT(hl_uri(ids[i]) != NULL);
+		else
+			ASSERT(hl_uri(ids[i]) == NULL);
+	}
+	ASSERT(!hl_gc_wanted());
+
+	/* dedupe still works for surviving keyed links */
+	ASSERT(hl_intern("keep", "https://keyed/";) == keyed);
+	/* collected ids are never handed out again */
+	ASSERT(hl_intern(NULL, "https://new/";) > maxid);
+
+	/* collect everything */
+	hl_gc_begin();
+	hl_gc_end();
+	ASSERT(hl_count() == 0);
+	ASSERT(hl_uri(keyed) == NULL);
+	/* a collected keyed link gets a new id when it shows up again */
+	ASSERT(hl_intern("keep", "https://keyed/";) != keyed);
+	hl_reset();
+}
+
+static void test_nomem(void)
+{
+	uint32_t id;
+	void *probe;
+
+	/* the mock is bypassed e.g. under valgrind: nothing to test then */
+	_mallocmock_fail = true;
+	probe = malloc(1);
+	_mallocmock_fail = false;
+	if (probe) {
+		free(probe);
+		return;
+	}
+
+	hl_reset();
+	_mallocmock_fail = true;
+	id = hl_intern(NULL, "https://oom/";);
+	_mallocmock_fail = false;
+	ASSERT(id == 0);
+	ASSERT(hl_count() == 0);
+	ASSERT(hl_intern(NULL, "https://oom/";) != 0);
+	hl_reset();
+}
+
+int main(void)
+{
+	test_parse();
+	test_intern();
+	test_gc();
+	test_nomem();
+	return 0;
+}

Reply via email to