From 1d0211177dd885e78a0b7b02b4532c05978751c3 Mon Sep 17 00:00:00 2001
Message-Id: <1d0211177dd885e78a0b7b02b4532c05978751c3.1621958144.git.steffen@sdaoden.eu>
From: Steffen Nurpmeso <steffen@sdaoden.eu>
Date: Tue, 25 May 2021 17:55:04 +0200
Subject: [PATCH] Support OSC 8 id= anchor searches

---
 cmd.h           |   1 +
 command.c       | 182 ++++++++++++++++++++++++++++++++++++++++++++++++
 configure.ac    |   7 ++
 decode.c        |   3 +-
 defines.ds      |   7 ++
 defines.o2      |   7 ++
 defines.o9      |   7 ++
 defines.wn      |   7 ++
 less.h          |   3 +
 less.hlp        |   1 +
 less.nro.VER    |   6 ++
 lesskey_parse.c |   3 +
 search.c        |  21 ++++++
 13 files changed, 254 insertions(+), 1 deletion(-)

diff --git a/cmd.h b/cmd.h
index 7141817bae..b4942d44ed 100644
--- a/cmd.h
+++ b/cmd.h
@@ -69,6 +69,7 @@
 /* Note "X116" refers to extended (1006) X11 mouse reporting. */
 #define A_X116MOUSE_IN         68
 #define A_CLR_SEARCH           70
+#define A_OSC8_SEARCH          84
 
 /* These values must not conflict with any A_* or EC_* value. */
 #define A_INVALID              100
diff --git a/command.c b/command.c
index d4e271324d..f019bfdf59 100644
--- a/command.c
+++ b/command.c
@@ -57,6 +57,14 @@ extern int incr_search;
 extern int utf_mode;
 #endif
 
+#if HILITE_SEARCH
+extern int hilite_search;
+#endif
+
+#if OSC8_SEARCH
+public char *osc8_search_line /* = NULL */;
+#endif
+
 #if SHELL_ESCAPE
 static char *shellcmd = NULL;   /* For holding last shell command for "!!" */
 #endif
@@ -84,6 +92,11 @@ static struct ungot* ungot = NULL;
 
 static void multi_search LESSPARAMS((char *pattern, int n, int silent));
 
+#if OSC8_SEARCH
+static void osc8_search();
+static void a_osc8_found();
+#endif
+
 /*
  * Move the cursor to start of prompt line before executing a command.
  * This looks nicer if the command takes a long time before
@@ -302,6 +315,11 @@ exec_mca(VOID_PARAM)
 		(void) pipe_mark(pipec, cbuf);
 		error("|done", NULL_PARG);
 		break;
+#endif
+#if OSC8_SEARCH
+	case A_OSC8_SEARCH:
+		osc8_search(cbuf);
+		break;
 #endif
 	}
 }
@@ -1122,6 +1140,159 @@ multi_search(pattern, n, silent)
 	}
 }
 
+/*
+ * OSC 8 search.  Search for an according OSC 8 id= parameter, and scroll the
+ * respective position into view.
+ * If a matching anchor is found it may also be a reference to an external
+ * manual page, if the OSC 8 URI starts with man://: in this case prepare the
+ * necessary man(1) command and give the user the option to confirm the action.
+ * Anyway: set lastmark() so that it is possible to jump back; let the empty
+ * anchor be an alias for the desire to do so
+ */
+#if OSC8_SEARCH
+	static void
+osc8_search(cbuf)
+	char const *cbuf;
+{
+	int const srch_flags = SRCH_NO_REGEX | SRCH_OSC8;
+
+	PARG parg;
+	char *q, *qc;
+	long l;
+# if HILITE_SEARCH
+	int save_hilite_search;
+# endif
+
+	parg.p_string = (char*)cbuf;
+	l = strtol(cbuf, &q, 10);
+
+	/* For convenience */
+	if (*cbuf == '\0')
+		gomark('\'');
+	else if (*q != '\0' || l <= 0)
+		error("Invalid reference anchor: %s", &parg);
+	/* Local references are scrolled into view */
+	else {
+		static char const a_id[] = {ESC,']','8',';','i','d','=','\0'};
+
+		q = ecalloc(sizeof(a_id) + strlen(cbuf) + 1, sizeof *q);
+
+		memcpy(&q[0], a_id, sizeof(a_id) -1);
+		for (qc = &q[sizeof(a_id) -1]; *cbuf != '\0'; ++qc, ++cbuf)
+			*qc = *cbuf;
+		*qc++ = ';';
+		*qc = '\0';
+
+# undef a_OSC8_ID
+
+		lastmark();
+# if HILITE_SEARCH
+		save_hilite_search = hilite_search;
+		repaint_hilite(0);
+		hilite_search = 0;
+# endif
+		if (search(SRCH_FORW | srch_flags, q, 1) == 0 ||
+		    search(SRCH_BACK | srch_flags, q, 1) == 0)
+			a_osc8_found(q);
+		else
+			error("No such anchor: %s", &parg);
+# if HILITE_SEARCH
+		hilite_search = save_hilite_search;
+		repaint_hilite(1);
+# endif
+
+		free(q);
+	}
+}
+
+	static void
+a_osc8_found(anchor)
+	char const *anchor;
+{
+# if HAVE_STRSTR
+	char *cp, *sect, *sect_top, *man, *man_top, c, *buf;
+
+	/* Find the anchor on the line again; play safe */
+	if ((cp = strstr(osc8_search_line, anchor)) == NULL)
+		goto jleave;
+	cp += strlen(anchor);
+
+	/* We only act upon man://.  (XXX Anything -> LESS_BROWSER?? */
+	if(strncmp(cp, "man://", sizeof("man://") -1))
+		goto jleave;
+	cp += sizeof("man://") -1;
+
+	sect = man_top = NULL;
+	for(man = sect_top = cp; (c = *sect_top) != ESC; ++sect_top){
+		if(c == '\0'){
+			sect = NULL;
+			break;
+		}else if(c == '.'){
+			man_top = &sect_top[-1];
+			sect = &sect_top[1];
+		}
+		/* We will single-quote the argument to work around possible
+		 * shell injection problems */
+		else if(c == '\'')
+			goto jbogus;
+	}
+	if(sect == NULL || sect >= sect_top || man >= man_top){
+jbogus:
+		error("Bogus OSC 8 man://ual reference", NULL_PARG);
+		goto jleave;
+	}
+
+	/* An external reference we can! */
+	if (secure) {
+		error("External references not available in secure mode",
+			NULL_PARG);
+		goto jleave;
+	}
+
+#  define __Y "!man '"
+#  define __X "Read external manual: "
+	buf = ecalloc(1, sizeof(__X __Y "'''") -1 +
+		(int)(sect_top - sect) + 1 + (int)(man_top - man) + 1 +1);
+
+	memcpy(buf, __X __Y, sizeof(__X __Y) -1);
+	cp = &buf[sizeof(__X __Y) -1];
+	for (; sect < sect_top; ++sect)
+		*cp++ = *sect;
+	*cp++ = '\'';
+	*cp++ = ' ';
+	*cp++ = '\'';
+	for (; man <= man_top; ++man)
+		*cp++ = *man;
+	*cp++ = '\'';
+	*cp++ = '\0';
+
+	cmd_putstr(buf);
+	switch (getcc()) {
+	case '\n':
+	case '\r':
+#  if HAVE_SYSTEM
+		lsystem(&buf[sizeof(__X)-1 + 1], "!back from external manual");
+#  else
+		error("Command not available", NULL_PARG);
+#  endif
+		/* FALLTHRU */
+	default:
+		break;
+	}
+
+	free(buf);
+#  undef __X
+#  undef __Y
+jleave:
+
+# else /* HAVE_STRSTR */
+	(void)anchor;
+# endif
+
+	free(osc8_search_line);
+}
+#endif /* OSC8_SEARCH */
+
 /*
  * Forward forever, or until a highlighted line appears.
  */
@@ -2039,6 +2210,17 @@ commands(VOID_PARAM)
 		case A_NOACTION:
 			break;
 
+		case A_OSC8_SEARCH:
+#if OSC8_SEARCH
+			start_mca(A_OSC8_SEARCH, "[OSC 8 anchor]:",
+				(void*)NULL, CF_QUIT_ON_ERASE);
+			c = getcc();
+			goto again;
+#else
+			error("Command not available", NULL_PARG);
+			break;
+#endif
+
 		default:
 			bell();
 			break;
diff --git a/configure.ac b/configure.ac
index 008a3d619d..0415e7f2bc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -602,6 +602,13 @@ AH_TOP([
  */
 #define	LOGFILE		(!SECURE)
 
+/*
+ * OSC8_SEARCH is 1 if you wish to support searching for OSC8 local references
+ * (as can be created via OSC 8 id= parameters).
+ * As well as external OSC 8 man://ual page references.
+ */
+#define	OSC8_SEARCH	(!SECURE)
+
 /*
  * GNU_OPTIONS is 1 if you wish to support the GNU-style command
  * line options --help and --version.
diff --git a/decode.c b/decode.c
index 9fa891969d..baa185c445 100644
--- a/decode.c
+++ b/decode.c
@@ -169,7 +169,8 @@ static unsigned char cmdtable[] =
 	'Q',0,                          A_QUIT,
 	':','q',0,                      A_QUIT,
 	':','Q',0,                      A_QUIT,
-	'Z','Z',0,                      A_QUIT
+	'Z','Z',0,                      A_QUIT,
+	CONTROL('A'),0,			A_OSC8_SEARCH
 };
 
 static unsigned char edittable[] =
diff --git a/defines.ds b/defines.ds
index 9d30129029..46e5f8a9c4 100644
--- a/defines.ds
+++ b/defines.ds
@@ -90,6 +90,13 @@
  */
 #define	LOGFILE		(!SECURE)
 
+/*
+ * OSC8_SEARCH is 1 if you wish to support searching for OSC8 local references
+ * (as can be created via OSC 8 id= parameters).
+ * As well as external OSC 8 man://ual page references.
+ */
+#define	OSC8_SEARCH	(!SECURE)
+
 /*
  * GNU_OPTIONS is 1 if you wish to support the GNU-style command
  * line options --help and --version.
diff --git a/defines.o2 b/defines.o2
index c3a0ed4c6a..908777825d 100644
--- a/defines.o2
+++ b/defines.o2
@@ -83,6 +83,13 @@
  */
 #define	LOGFILE		(!SECURE)
 
+/*
+ * OSC8_SEARCH is 1 if you wish to support searching for OSC8 local references
+ * (as can be created via OSC 8 id= parameters).
+ * As well as external OSC 8 man://ual page references.
+ */
+#define	OSC8_SEARCH	(!SECURE)
+
 /*
  * GNU_OPTIONS is 1 if you wish to support the GNU-style command
  * line options --help and --version.
diff --git a/defines.o9 b/defines.o9
index 55fdb85af2..b6415449d5 100644
--- a/defines.o9
+++ b/defines.o9
@@ -82,6 +82,13 @@
  */
 #define	LOGFILE		(!SECURE)
 
+/*
+ * OSC8_SEARCH is 1 if you wish to support searching for OSC8 local references
+ * (as can be created via OSC 8 id= parameters).
+ * As well as external OSC 8 man://ual page references.
+ */
+#define	OSC8_SEARCH	(!SECURE)
+
 /*
  * GNU_OPTIONS is 1 if you wish to support the GNU-style command
  * line options --help and --version.
diff --git a/defines.wn b/defines.wn
index 9127ca8a9c..69f412fb87 100644
--- a/defines.wn
+++ b/defines.wn
@@ -83,6 +83,13 @@
  */
 #define	LOGFILE		(!SECURE)
 
+/*
+ * OSC8_SEARCH is 1 if you wish to support searching for OSC8 local references
+ * (as can be created via OSC 8 id= parameters).
+ * As well as external OSC 8 man://ual page references.
+ */
+#define	OSC8_SEARCH	(!SECURE)
+
 /*
  * GNU_OPTIONS is 1 if you wish to support the GNU-style command
  * line options --help and --version.
diff --git a/less.h b/less.h
index a00a64db49..e8970ac93b 100644
--- a/less.h
+++ b/less.h
@@ -360,6 +360,9 @@ struct wchar_range_table
 #define SRCH_FILTER     (1 << 13) /* Search is for '&' (filter) command */
 #define SRCH_AFTER_TARGET (1 << 14) /* Start search after the target line */
 #define SRCH_WRAP       (1 << 15) /* Wrap-around search (continue at BOF/EOF) */
+#if OSC8_SEARCH
+# define SRCH_OSC8	(1 << 16) /* OSC 8 id= search */
+#endif
 
 #define SRCH_REVERSE(t) (((t) & SRCH_FORW) ? \
                                 (((t) & ~SRCH_FORW) | SRCH_BACK) : \
diff --git a/less.hlp b/less.hlp
index 333a0b576c..41c65f7e8f 100644
--- a/less.hlp
+++ b/less.hlp
@@ -73,6 +73,7 @@
 
   m_<_l_e_t_t_e_r_>            Mark the current top line with <letter>.
   M_<_l_e_t_t_e_r_>            Mark the current bottom line with <letter>.
+  ^A_<_t_e_x_t_>             Go to OSC 8 anchor <text>.
   '_<_l_e_t_t_e_r_>            Go to a previously marked position.
   ''                   Go to the previous position.
   ^X^X                 Same as '.
diff --git a/less.nro.VER b/less.nro.VER
index 2f7d956262..bbb6f8cd60 100644
--- a/less.nro.VER
+++ b/less.nro.VER
@@ -105,6 +105,12 @@ and LEFTARROW commands.
 Scroll horizontally right to show the end of the longest displayed line.
 .IP "ESC-{ or ^LEFTARROW"
 Scroll horizontally left back to the first column.
+.IP "^A"
+Enter the
+.I OSC 8
+anchor to go to; the position before scrolling to an anchor will be
+recognized as the last position mark and can thus be scrolled to either
+by using ^A again with an empty anchor or by using the "''" command sequence.
 .IP "r or ^R or ^L"
 Repaint the screen.
 .IP R
diff --git a/lesskey_parse.c b/lesskey_parse.c
index 3190cd113e..3fc4fc3aa3 100644
--- a/lesskey_parse.c
+++ b/lesskey_parse.c
@@ -55,6 +55,9 @@ static struct lesskey_cmdname cmdnames[] =
 	{ "index-file",           A_INDEX_FILE },
 	{ "invalid",              A_UINVALID },
 	{ "left-scroll",          A_LSHIFT },
+#if OSC8_SEARCH
+	{ "osc8-search",          A_OSC8_SEARCH },
+#endif
 	{ "next-file",            A_NEXT_FILE },
 	{ "next-tag",             A_NEXT_TAG },
 	{ "noaction",             A_NOACTION },
diff --git a/search.c b/search.c
index 6277fe3c4e..01daec7d7c 100644
--- a/search.c
+++ b/search.c
@@ -1375,6 +1375,27 @@ search_range(pos, endpos, search_type, matches, maxlines, plinepos, pendpos, pla
 			continue;
 #endif
 
+		/* Special case OSC 8 searches: we want to match plain line
+		 * content, so avoid allocations and text conversions */
+#if OSC8_SEARCH
+		if (search_type & SRCH_OSC8) {
+			line_match = match_pattern(info_compiled(&search_info),
+				search_info.text, line, line_len, &sp, &ep, 0,
+				search_type);
+			if (line_match) {
+				extern char *osc8_search_line;
+
+				osc8_search_line = (char*)ecalloc(1, line_len +1);
+				memcpy(osc8_search_line, line, line_len);
+				osc8_search_line[line_len] = '\0';
+				if (plinepos != NULL)
+					*plinepos = linepos;
+				return (0);
+			}
+			continue;
+		}
+#endif
+
 		/*
 		 * If it's a caseless search, convert the line to lowercase.
 		 * If we're doing backspace processing, delete backspaces.
-- 
2.31.1

