/* $OpenBSD$ */ /* * Copyright (c) 2021 Will * Copyright (c) 2022 Jeff Chiang * * 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 #include #include #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); }