summaryrefslogtreecommitdiffstats
path: root/hyperlinks.c
diff options
context:
space:
mode:
Diffstat (limited to 'hyperlinks.c')
-rw-r--r--hyperlinks.c227
1 files changed, 227 insertions, 0 deletions
diff --git a/hyperlinks.c b/hyperlinks.c
new file mode 100644
index 0000000..70c2f4e
--- /dev/null
+++ b/hyperlinks.c
@@ -0,0 +1,227 @@
+/* $OpenBSD$ */
+
+/*
+ * Copyright (c) 2021 Will <author@will.party>
+ * Copyright (c) 2022 Jeff Chiang <pobomp@gmail.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
+ * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "tmux.h"
+
+/*
+ * OSC 8 hyperlinks, described at:
+ *
+ * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+ *
+ * Each hyperlink and ID combination is assigned a number ("inner" in this
+ * file) which is stored in an extended grid cell and maps into a tree here.
+ *
+ * Each URI has one inner number and one external ID (which tmux uses to send
+ * the hyperlink to the terminal) and one internal ID (which is received from
+ * the sending application inside tmux).
+ *
+ * Anonymous hyperlinks are each unique and are not reused even if they have
+ * the same URI (terminals will not want to tie them together).
+ */
+
+#define MAX_HYPERLINKS 5000
+
+static long long hyperlinks_next_external_id = 1;
+static u_int global_hyperlinks_count;
+
+struct hyperlinks_uri {
+ struct hyperlinks *tree;
+
+ u_int inner;
+ const char *internal_id;
+ const char *external_id;
+ const char *uri;
+
+ TAILQ_ENTRY(hyperlinks_uri) list_entry;
+ RB_ENTRY(hyperlinks_uri) by_inner_entry;
+ RB_ENTRY(hyperlinks_uri) by_uri_entry; /* by internal ID and URI */
+};
+RB_HEAD(hyperlinks_by_uri_tree, hyperlinks_uri);
+RB_HEAD(hyperlinks_by_inner_tree, hyperlinks_uri);
+
+TAILQ_HEAD(hyperlinks_list, hyperlinks_uri);
+static struct hyperlinks_list global_hyperlinks =
+ TAILQ_HEAD_INITIALIZER(global_hyperlinks);
+
+struct hyperlinks {
+ u_int next_inner;
+ struct hyperlinks_by_inner_tree by_inner;
+ struct hyperlinks_by_uri_tree by_uri;
+};
+
+static int
+hyperlinks_by_uri_cmp(struct hyperlinks_uri *left, struct hyperlinks_uri *right)
+{
+ int r;
+
+ if (*left->internal_id == '\0' || *right->internal_id == '\0') {
+ /*
+ * If both URIs are anonymous, use the inner for comparison so
+ * that they do not match even if the URI is the same - each
+ * anonymous URI should be unique.
+ */
+ if (*left->internal_id != '\0')
+ return (-1);
+ if (*right->internal_id != '\0')
+ return (1);
+ return (left->inner - right->inner);
+ }
+
+ r = strcmp(left->internal_id, right->internal_id);
+ if (r != 0)
+ return (r);
+ return (strcmp(left->uri, right->uri));
+}
+RB_PROTOTYPE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
+ hyperlinks_by_uri_cmp);
+RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
+ hyperlinks_by_uri_cmp);
+
+static int
+hyperlinks_by_inner_cmp(struct hyperlinks_uri *left,
+ struct hyperlinks_uri *right)
+{
+ return (left->inner - right->inner);
+}
+RB_PROTOTYPE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
+ hyperlinks_by_inner_cmp);
+RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
+ hyperlinks_by_inner_cmp);
+
+/* Remove a hyperlink. */
+static void
+hyperlinks_remove(struct hyperlinks_uri *hlu)
+{
+ struct hyperlinks *hl = hlu->tree;
+
+ TAILQ_REMOVE(&global_hyperlinks, hlu, list_entry);
+ global_hyperlinks_count--;
+
+ RB_REMOVE(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
+ RB_REMOVE(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
+
+ free((void *)hlu->internal_id);
+ free((void *)hlu->external_id);
+ free((void *)hlu->uri);
+ free(hlu);
+}
+
+/* Store a new hyperlink or return if it already exists. */
+u_int
+hyperlinks_put(struct hyperlinks *hl, const char *uri_in,
+ const char *internal_id_in)
+{
+ struct hyperlinks_uri find, *hlu;
+ char *uri, *internal_id, *external_id;
+
+ /*
+ * Anonymous URI are stored with an empty internal ID and the tree
+ * comparator will make sure they never match each other (so each
+ * anonymous URI is unique).
+ */
+ if (internal_id_in == NULL)
+ internal_id_in = "";
+
+ utf8_stravis(&uri, uri_in, VIS_OCTAL|VIS_CSTYLE);
+ utf8_stravis(&internal_id, internal_id_in, VIS_OCTAL|VIS_CSTYLE);
+
+ if (*internal_id_in != '\0') {
+ find.uri = uri;
+ find.internal_id = internal_id;
+
+ hlu = RB_FIND(hyperlinks_by_uri_tree, &hl->by_uri, &find);
+ if (hlu != NULL) {
+ free (uri);
+ free (internal_id);
+ return (hlu->inner);
+ }
+ }
+ xasprintf(&external_id, "tmux%llX", hyperlinks_next_external_id++);
+
+ hlu = xcalloc(1, sizeof *hlu);
+ hlu->inner = hl->next_inner++;
+ hlu->internal_id = internal_id;
+ hlu->external_id = external_id;
+ hlu->uri = uri;
+ hlu->tree = hl;
+ RB_INSERT(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
+ RB_INSERT(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
+
+ TAILQ_INSERT_TAIL(&global_hyperlinks, hlu, list_entry);
+ if (++global_hyperlinks_count == MAX_HYPERLINKS)
+ hyperlinks_remove(TAILQ_FIRST(&global_hyperlinks));
+
+ return (hlu->inner);
+}
+
+/* Get hyperlink by inner number. */
+int
+hyperlinks_get(struct hyperlinks *hl, u_int inner, const char **uri_out,
+ const char **internal_id_out, const char **external_id_out)
+{
+ struct hyperlinks_uri find, *hlu;
+
+ find.inner = inner;
+
+ hlu = RB_FIND(hyperlinks_by_inner_tree, &hl->by_inner, &find);
+ if (hlu == NULL)
+ return (0);
+ if (internal_id_out != NULL)
+ *internal_id_out = hlu->internal_id;
+ if (external_id_out != NULL)
+ *external_id_out = hlu->external_id;
+ *uri_out = hlu->uri;
+ return (1);
+}
+
+/* Initialize hyperlink set. */
+struct hyperlinks *
+hyperlinks_init(void)
+{
+ struct hyperlinks *hl;
+
+ hl = xcalloc(1, sizeof *hl);
+ hl->next_inner = 1;
+ RB_INIT(&hl->by_uri);
+ RB_INIT(&hl->by_inner);
+ return (hl);
+}
+
+/* Free all hyperlinks but not the set itself. */
+void
+hyperlinks_reset(struct hyperlinks *hl)
+{
+ struct hyperlinks_uri *hlu, *hlu1;
+
+ RB_FOREACH_SAFE(hlu, hyperlinks_by_inner_tree, &hl->by_inner, hlu1)
+ hyperlinks_remove(hlu);
+}
+
+/* Free hyperlink set. */
+void
+hyperlinks_free(struct hyperlinks *hl)
+{
+ hyperlinks_reset(hl);
+ free(hl);
+}