From cdacc12ce305ad2f3e65e2a01328a988e3200b51 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 30 Jun 2022 09:55:53 +0000 Subject: [PATCH] Add support for OSC 8 hyperlinks (a VTE extension now supported by other terminals such as iTerm2). Originally written by me then extended and completed by first Will Noble and later Jeff Chiang. GitHub issues 911, 2621, 2890, 3240. --- Makefile | 1 + cmd-capture-pane.c | 6 +- cmd-display-panes.c | 6 +- grid.c | 13 ++- hyperlinks.c | 225 ++++++++++++++++++++++++++++++++++++++++++++ input.c | 45 +++++++++ screen-redraw.c | 2 +- screen.c | 14 +++ server.c | 1 - style.c | 2 +- tmux.1 | 11 ++- tmux.h | 23 ++++- tty-features.c | 16 +++- tty-term.c | 1 + tty.c | 114 +++++++++++++++------- 15 files changed, 432 insertions(+), 48 deletions(-) create mode 100644 hyperlinks.c diff --git a/Makefile b/Makefile index fc7ed262..e3e2fd9c 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,7 @@ SRCS= alerts.c \ grid-reader.c \ grid-view.c \ grid.c \ + hyperlinks.c \ input-keys.c \ input.c \ job.c \ diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index a3e84c46..04a88172 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -53,8 +53,8 @@ const struct cmd_entry cmd_clear_history_entry = { .name = "clear-history", .alias = "clearhist", - .args = { "t:", 0, 0, NULL }, - .usage = CMD_TARGET_PANE_USAGE, + .args = { "Ht:", 0, 0, NULL }, + .usage = "[-H] " CMD_TARGET_PANE_USAGE, .target = { 't', CMD_FIND_PANE, 0 }, @@ -204,6 +204,8 @@ cmd_capture_pane_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_clear_history_entry) { window_pane_reset_mode_all(wp); grid_clear_history(wp->base.grid); + if (args_has(args, 'H')) + screen_reset_hyperlinks(wp->screen); return (CMD_RETURN_NORMAL); } diff --git a/cmd-display-panes.c b/cmd-display-panes.c index 5773a2d0..06f6dc27 100644 --- a/cmd-display-panes.c +++ b/cmd-display-panes.c @@ -144,7 +144,7 @@ cmd_display_panes_draw_pane(struct screen_redraw_ctx *ctx, llen = 0; if (sx < len * 6 || sy < 5) { - tty_attributes(tty, &fgc, &grid_default_cell, NULL); + tty_attributes(tty, &fgc, &grid_default_cell, NULL, NULL); if (sx >= len + llen + 1) { len += llen + 1; tty_cursor(tty, xoff + px - len / 2, yoff + py); @@ -161,7 +161,7 @@ cmd_display_panes_draw_pane(struct screen_redraw_ctx *ctx, px -= len * 3; py -= 2; - tty_attributes(tty, &bgc, &grid_default_cell, NULL); + tty_attributes(tty, &bgc, &grid_default_cell, NULL, NULL); for (ptr = buf; *ptr != '\0'; ptr++) { if (*ptr < '0' || *ptr > '9') continue; @@ -179,7 +179,7 @@ cmd_display_panes_draw_pane(struct screen_redraw_ctx *ctx, if (sy <= 6) goto out; - tty_attributes(tty, &fgc, &grid_default_cell, NULL); + tty_attributes(tty, &fgc, &grid_default_cell, NULL, NULL); if (rlen != 0 && sx >= rlen) { tty_cursor(tty, xoff + sx - rlen, yoff); tty_putn(tty, rbuf, rlen, rlen); diff --git a/grid.c b/grid.c index ba251e70..828def68 100644 --- a/grid.c +++ b/grid.c @@ -37,7 +37,7 @@ /* Default grid cell data. */ const struct grid_cell grid_default_cell = { - { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0 + { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 }; /* @@ -45,12 +45,12 @@ const struct grid_cell grid_default_cell = { * appears in the grid - because of this, they are always extended cells. */ static const struct grid_cell grid_padding_cell = { - { { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 0 + { { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 0, 0 }; /* Cleared grid cell data. */ static const struct grid_cell grid_cleared_cell = { - { { ' ' }, 0, 1, 1 }, 0, GRID_FLAG_CLEARED, 8, 8, 0 + { { ' ' }, 0, 1, 1 }, 0, GRID_FLAG_CLEARED, 8, 8, 0, 0 }; static const struct grid_cell_entry grid_cleared_entry = { GRID_FLAG_CLEARED, { .data = { 0, 8, 8, ' ' } } @@ -90,6 +90,8 @@ grid_need_extended_cell(const struct grid_cell_entry *gce, return (1); if (gc->us != 0) /* only supports 256 or RGB */ return (1); + if (gc->link != 0) + return (1); return (0); } @@ -131,6 +133,7 @@ grid_extended_cell(struct grid_line *gl, struct grid_cell_entry *gce, gee->fg = gc->fg; gee->bg = gc->bg; gee->us = gc->us; + gee->link = gc->link; return (gee); } @@ -231,6 +234,8 @@ grid_cells_look_equal(const struct grid_cell *gc1, const struct grid_cell *gc2) return (0); if (gc1->attr != gc2->attr || gc1->flags != gc2->flags) return (0); + if (gc1->link != gc2->link) + return (0); return (1); } @@ -509,6 +514,7 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc) gc->fg = gee->fg; gc->bg = gee->bg; gc->us = gee->us; + gc->link = gee->link; utf8_to_data(gee->data, &gc->data); } return; @@ -524,6 +530,7 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc) gc->bg |= COLOUR_FLAG_256; gc->us = 0; utf8_set(&gc->data, gce->data.data); + gc->link = 0; } /* Get cell for reading. */ diff --git a/hyperlinks.c b/hyperlinks.c new file mode 100644 index 00000000..3dd3efa9 --- /dev/null +++ b/hyperlinks.c @@ -0,0 +1,225 @@ +/* $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 + +#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 uint64_t 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 **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); + *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); +} diff --git a/input.c b/input.c index d4ffe784..4252428b 100644 --- a/input.c +++ b/input.c @@ -135,6 +135,7 @@ static void input_set_state(struct input_ctx *, static void input_reset_cell(struct input_ctx *); static void input_osc_4(struct input_ctx *, const char *); +static void input_osc_8(struct input_ctx *, const char *); static void input_osc_10(struct input_ctx *, const char *); static void input_osc_11(struct input_ctx *, const char *); static void input_osc_12(struct input_ctx *, const char *); @@ -2318,6 +2319,9 @@ input_exit_osc(struct input_ctx *ictx) } } break; + case 8: + input_osc_8(ictx, p); + break; case 10: input_osc_10(ictx, p); break; @@ -2562,6 +2566,47 @@ input_osc_4(struct input_ctx *ictx, const char *p) free(copy); } +/* Handle the OSC 8 sequence for embedding hyperlinks. */ +static void +input_osc_8(struct input_ctx *ictx, const char *p) +{ + struct hyperlinks *hl = ictx->ctx.s->hyperlinks; + struct grid_cell *gc = &ictx->cell.cell; + const char *start, *end, *uri; + char *id = NULL; + + for (start = p; (end = strpbrk(start, ":;")) != NULL; start = end + 1) { + if (end - start >= 4 && strncmp(start, "id=", 3) == 0) { + if (id != NULL) + goto bad; + id = xstrndup(start + 3, end - start - 3); + } + + /* The first ; is the end of parameters and start of the URI. */ + if (*end == ';') + break; + } + if (end == NULL || *end != ';') + goto bad; + uri = end + 1; + if (*uri == '\0') { + gc->link = 0; + free(id); + return; + } + gc->link = hyperlinks_put(hl, uri, id); + if (id == NULL) + log_debug("hyperlink (anonymous) %s = %u", uri, gc->link); + else + log_debug("hyperlink (id=%s) %s = %u", id, uri, gc->link); + free(id); + return; + +bad: + log_debug("bad OSC 8 %s", p); + free(id); +} + /* Handle the OSC 10 sequence for setting and querying foreground colour. */ static void input_osc_10(struct input_ctx *ictx, const char *p) diff --git a/screen-redraw.c b/screen-redraw.c index c4906ab8..470135cc 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -738,7 +738,7 @@ screen_redraw_draw_borders_cell(struct screen_redraw_ctx *ctx, u_int i, u_int j) } } - tty_cell(tty, &gc, &grid_default_cell, NULL); + tty_cell(tty, &gc, &grid_default_cell, NULL, NULL); if (isolates) tty_puts(tty, START_ISOLATE); } diff --git a/screen.c b/screen.c index 87b3330a..09326b2d 100644 --- a/screen.c +++ b/screen.c @@ -90,6 +90,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->sel = NULL; s->write_list = NULL; + s->hyperlinks = NULL; screen_reinit(s); } @@ -119,6 +120,17 @@ screen_reinit(struct screen *s) screen_clear_selection(s); screen_free_titles(s); + screen_reset_hyperlinks(s); +} + +/* Reset hyperlinks of a screen. */ +void +screen_reset_hyperlinks(struct screen *s) +{ + if (s->hyperlinks == NULL) + s->hyperlinks = hyperlinks_init(); + else + hyperlinks_reset(s->hyperlinks); } /* Destroy a screen. */ @@ -137,6 +149,8 @@ screen_free(struct screen *s) grid_destroy(s->saved_grid); grid_destroy(s->grid); + if (s->hyperlinks != NULL) + hyperlinks_free(s->hyperlinks); screen_free_titles(s); } diff --git a/server.c b/server.c index 5cbe3921..f1df1aa4 100644 --- a/server.c +++ b/server.c @@ -211,7 +211,6 @@ server_start(struct tmuxproc *client, int flags, struct event_base *base, RB_INIT(&sessions); key_bindings_init(); TAILQ_INIT(&message_log); - gettimeofday(&start_time, NULL); server_fd = server_create_socket(flags, &cause); diff --git a/style.c b/style.c index 89a4e63a..8407dc68 100644 --- a/style.c +++ b/style.c @@ -30,7 +30,7 @@ /* Default style. */ static struct style style_default = { - { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0 }, + { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 }, 0, 8, diff --git a/tmux.1 b/tmux.1 index 44b96eb7..c7ff6cea 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3656,6 +3656,8 @@ Allows setting the cursor style. Supports extended keys. .It focus Supports focus reporting. +.It hyperlinks +Supports OSC 8 hyperlinks. .It ignorefkeys Ignore function keys from .Xr terminfo 5 @@ -6122,9 +6124,14 @@ a format for each shortcut key; both are evaluated once for each line. starts without the preview. This command works only if at least one client is attached. .Tg clearhist -.It Ic clear-history Op Fl t Ar target-pane +.It Xo Ic clear-history +.Op Fl H +.Op Fl t Ar target-pane +.Xc .D1 Pq alias: Ic clearhist Remove and free the history for the specified pane. +.Fl H +also removes all hyperlinks. .Tg deleteb .It Ic delete-buffer Op Fl b Ar buffer-name .D1 Pq alias: Ic deleteb @@ -6412,6 +6419,8 @@ Disable and enable focus reporting. These are set automatically if the .Em XT capability is present. +.It Em \&Hls +Set or clear a hyperlink annotation. .It Em \&Rect Tell .Nm diff --git a/tmux.h b/tmux.h index c77b77c7..71245ffc 100644 --- a/tmux.h +++ b/tmux.h @@ -49,6 +49,8 @@ struct control_state; struct environ; struct format_job_tree; struct format_tree; +struct hyperlinks_uri; +struct hyperlinks; struct input_ctx; struct job; struct menu_data; @@ -365,6 +367,7 @@ enum tty_code_code { TTYC_ENFCS, TTYC_ENMG, TTYC_FSL, + TTYC_HLS, TTYC_HOME, TTYC_HPA, TTYC_ICH, @@ -689,6 +692,7 @@ struct grid_cell { int fg; int bg; int us; + u_int link; }; /* Grid extended cell entry. */ @@ -699,6 +703,7 @@ struct grid_extd_entry { int fg; int bg; int us; + u_int link; } __packed; /* Grid cell entry. */ @@ -850,6 +855,8 @@ struct screen { struct screen_sel *sel; struct screen_write_cline *write_list; + + struct hyperlinks *hyperlinks; }; /* Screen write context. */ @@ -2246,7 +2253,8 @@ void tty_update_window_offset(struct window *); void tty_update_client_offset(struct client *); void tty_raw(struct tty *, const char *); void tty_attributes(struct tty *, const struct grid_cell *, - const struct grid_cell *, struct colour_palette *); + const struct grid_cell *, struct colour_palette *, + struct hyperlinks *); void tty_reset(struct tty *); void tty_region_off(struct tty *); void tty_margin_off(struct tty *); @@ -2263,7 +2271,8 @@ void tty_puts(struct tty *, const char *); void tty_putc(struct tty *, u_char); void tty_putn(struct tty *, const void *, size_t, u_int); void tty_cell(struct tty *, const struct grid_cell *, - const struct grid_cell *, struct colour_palette *); + const struct grid_cell *, struct colour_palette *, + struct hyperlinks *); int tty_init(struct tty *, struct client *); void tty_resize(struct tty *); void tty_set_size(struct tty *, u_int, u_int, u_int, u_int); @@ -2893,6 +2902,7 @@ void screen_init(struct screen *, u_int, u_int, u_int); void screen_reinit(struct screen *); void screen_free(struct screen *); void screen_reset_tabs(struct screen *); +void screen_reset_hyperlinks(struct screen *); void screen_set_cursor_style(u_int, enum screen_cursor_style *, int *); void screen_set_cursor_colour(struct screen *, int); int screen_set_title(struct screen *, const char *); @@ -3298,4 +3308,13 @@ void server_acl_user_deny_write(uid_t); int server_acl_join(struct client *); uid_t server_acl_get_uid(struct server_acl_user *); +/* hyperlink.c */ +u_int hyperlinks_put(struct hyperlinks *, const char *, + const char *); +int hyperlinks_get(struct hyperlinks *, u_int, + const char **, const char **); +struct hyperlinks *hyperlinks_init(void); +void hyperlinks_reset(struct hyperlinks *); +void hyperlinks_free(struct hyperlinks *); + #endif /* TMUX_H */ diff --git a/tty-features.c b/tty-features.c index 477925e3..396a351e 100644 --- a/tty-features.c +++ b/tty-features.c @@ -87,6 +87,17 @@ static const struct tty_feature tty_feature_clipboard = { 0 }; +/* Terminal supports OSC 8 hyperlinks. */ +static const char *tty_feature_hyperlinks_capabilities[] = { + "*:Hls=\\E]8;%?%p1%l%tid=%p1%s%;;%p2%s\\E\\\\", + NULL +}; +static const struct tty_feature tty_feature_hyperlinks = { + "hyperlinks", + tty_feature_hyperlinks_capabilities, + 0 +}; + /* * Terminal supports RGB colour. This replaces setab and setaf also since * terminals with RGB have versions that do not allow setting colours from the @@ -330,6 +341,7 @@ static const struct tty_feature *tty_features[] = { &tty_feature_bpaste, &tty_feature_ccolour, &tty_feature_clipboard, + &tty_feature_hyperlinks, &tty_feature_cstyle, &tty_feature_extkeys, &tty_feature_focus, @@ -444,14 +456,14 @@ tty_default_features(int *feat, const char *name, u_int version) }, { .name = "tmux", .features = TTY_FEATURES_BASE_MODERN_XTERM - ",ccolour,cstyle,focus,overline,usstyle" + ",ccolour,cstyle,focus,overline,usstyle,hyperlinks" }, { .name = "rxvt-unicode", .features = "256,bpaste,ccolour,cstyle,mouse,title,ignorefkeys" }, { .name = "iTerm2", .features = TTY_FEATURES_BASE_MODERN_XTERM - ",cstyle,extkeys,margins,usstyle,sync,osc7" + ",cstyle,extkeys,margins,usstyle,sync,osc7,hyperlinks" }, { .name = "XTerm", /* diff --git a/tty-term.c b/tty-term.c index 61eda56d..a877ef6b 100644 --- a/tty-term.c +++ b/tty-term.c @@ -100,6 +100,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_ENFCS] = { TTYCODE_STRING, "Enfcs" }, [TTYC_ENMG] = { TTYCODE_STRING, "Enmg" }, [TTYC_FSL] = { TTYCODE_STRING, "fsl" }, + [TTYC_HLS] = { TTYCODE_STRING, "Hls" }, [TTYC_HOME] = { TTYCODE_STRING, "home" }, [TTYC_HPA] = { TTYCODE_STRING, "hpa" }, [TTYC_ICH1] = { TTYCODE_STRING, "ich1" }, diff --git a/tty.c b/tty.c index 7e0a6a3e..78961e47 100644 --- a/tty.c +++ b/tty.c @@ -69,7 +69,7 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code, static void tty_repeat_space(struct tty *, u_int); static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int); static void tty_default_attributes(struct tty *, const struct grid_cell *, - struct colour_palette *, u_int); + struct colour_palette *, u_int, struct hyperlinks *); static int tty_check_overlay(struct tty *, u_int, u_int); static void tty_check_overlay_range(struct tty *, u_int, u_int, u_int, struct overlay_ranges *); @@ -1455,7 +1455,8 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, tty_term_has(tty->term, TTYC_EL1) && !tty_fake_bce(tty, defaults, 8) && c->overlay_check == NULL) { - tty_default_attributes(tty, defaults, palette, 8); + tty_default_attributes(tty, defaults, palette, 8, + s->hyperlinks); tty_cursor(tty, nx - 1, aty); tty_putcode(tty, TTYC_EL1); cleared = 1; @@ -1480,9 +1481,11 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, gcp->fg != last.fg || gcp->bg != last.bg || gcp->us != last.us || + gcp->link != last.link || ux + width + gcp->data.width > nx || (sizeof buf) - len < gcp->data.size)) { - tty_attributes(tty, &last, defaults, palette); + tty_attributes(tty, &last, defaults, palette, + s->hyperlinks); if (last.flags & GRID_FLAG_CLEARED) { log_debug("%s: %zu cleared", __func__, len); tty_clear_line(tty, defaults, aty, atx + ux, @@ -1515,7 +1518,8 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, ux += gcp->data.width; } else if (hidden != 0 || ux + gcp->data.width > nx) { if (~gcp->flags & GRID_FLAG_PADDING) { - tty_attributes(tty, &last, defaults, palette); + tty_attributes(tty, &last, defaults, palette, + s->hyperlinks); for (j = 0; j < OVERLAY_MAX_RANGES; j++) { if (r.nx[j] == 0) continue; @@ -1532,7 +1536,8 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, } } } else if (gcp->attr & GRID_ATTR_CHARSET) { - tty_attributes(tty, &last, defaults, palette); + tty_attributes(tty, &last, defaults, palette, + s->hyperlinks); tty_cursor(tty, atx + ux, aty); for (j = 0; j < gcp->data.size; j++) tty_putc(tty, gcp->data.data[j]); @@ -1544,7 +1549,7 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, } } if (len != 0 && ((~last.flags & GRID_FLAG_CLEARED) || last.bg != 8)) { - tty_attributes(tty, &last, defaults, palette); + tty_attributes(tty, &last, defaults, palette, s->hyperlinks); if (last.flags & GRID_FLAG_CLEARED) { log_debug("%s: %zu cleared (end)", __func__, len); tty_clear_line(tty, defaults, aty, atx + ux, width, @@ -1560,7 +1565,8 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, if (!cleared && ux < nx) { log_debug("%s: %u to end of line (%zu cleared)", __func__, nx - ux, len); - tty_default_attributes(tty, defaults, palette, 8); + tty_default_attributes(tty, defaults, palette, 8, + s->hyperlinks); tty_clear_line(tty, defaults, aty, atx + ux, nx - ux, 8); } @@ -1646,7 +1652,8 @@ tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_cursor_pane(tty, ctx, ctx->ocx, ctx->ocy); @@ -1668,7 +1675,8 @@ tty_cmd_deletecharacter(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_cursor_pane(tty, ctx, ctx->ocx, ctx->ocy); @@ -1678,7 +1686,8 @@ tty_cmd_deletecharacter(struct tty *tty, const struct tty_ctx *ctx) void tty_cmd_clearcharacter(struct tty *tty, const struct tty_ctx *ctx) { - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_clear_pane_line(tty, ctx, ctx->ocy, ctx->ocx, ctx->num, ctx->bg); } @@ -1700,7 +1709,8 @@ tty_cmd_insertline(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_off(tty); @@ -1727,7 +1737,8 @@ tty_cmd_deleteline(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_off(tty); @@ -1740,7 +1751,8 @@ tty_cmd_deleteline(struct tty *tty, const struct tty_ctx *ctx) void tty_cmd_clearline(struct tty *tty, const struct tty_ctx *ctx) { - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_clear_pane_line(tty, ctx, ctx->ocy, 0, ctx->sx, ctx->bg); } @@ -1750,7 +1762,8 @@ tty_cmd_clearendofline(struct tty *tty, const struct tty_ctx *ctx) { u_int nx = ctx->sx - ctx->ocx; - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_clear_pane_line(tty, ctx, ctx->ocy, ctx->ocx, nx, ctx->bg); } @@ -1758,7 +1771,8 @@ tty_cmd_clearendofline(struct tty *tty, const struct tty_ctx *ctx) void tty_cmd_clearstartofline(struct tty *tty, const struct tty_ctx *ctx) { - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_clear_pane_line(tty, ctx, ctx->ocy, 0, ctx->ocx + 1, ctx->bg); } @@ -1784,7 +1798,8 @@ tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_pane(tty, ctx); @@ -1815,7 +1830,8 @@ tty_cmd_linefeed(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_pane(tty, ctx); @@ -1855,7 +1871,8 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_pane(tty, ctx); @@ -1895,7 +1912,8 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_pane(tty, ctx); @@ -1914,7 +1932,8 @@ tty_cmd_clearendofscreen(struct tty *tty, const struct tty_ctx *ctx) { u_int px, py, nx, ny; - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, 0, ctx->sy - 1); tty_margin_off(tty); @@ -1938,7 +1957,8 @@ tty_cmd_clearstartofscreen(struct tty *tty, const struct tty_ctx *ctx) { u_int px, py, nx, ny; - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, 0, ctx->sy - 1); tty_margin_off(tty); @@ -1962,7 +1982,8 @@ tty_cmd_clearscreen(struct tty *tty, const struct tty_ctx *ctx) { u_int px, py, nx, ny; - tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg); + tty_default_attributes(tty, &ctx->defaults, ctx->palette, ctx->bg, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, 0, ctx->sy - 1); tty_margin_off(tty); @@ -1985,7 +2006,8 @@ tty_cmd_alignmenttest(struct tty *tty, const struct tty_ctx *ctx) return; } - tty_attributes(tty, &grid_default_cell, &ctx->defaults, ctx->palette); + tty_attributes(tty, &grid_default_cell, &ctx->defaults, ctx->palette, + ctx->s->hyperlinks); tty_region_pane(tty, ctx, 0, ctx->sy - 1); tty_margin_off(tty); @@ -2031,7 +2053,8 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) tty_margin_off(tty); tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy); - tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette); + tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette, + ctx->s->hyperlinks); } void @@ -2062,7 +2085,7 @@ tty_cmd_cells(struct tty *tty, const struct tty_ctx *ctx) tty_margin_off(tty); tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy); - tty_attributes(tty, ctx->cell, &ctx->defaults, ctx->palette); + tty_attributes(tty, ctx->cell, &ctx->defaults, ctx->palette, ctx->s->hyperlinks); /* Get tty position from pane position for overlay check. */ px = ctx->xoff + ctx->ocx - ctx->wox; @@ -2136,7 +2159,8 @@ tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx) void tty_cell(struct tty *tty, const struct grid_cell *gc, - const struct grid_cell *defaults, struct colour_palette *palette) + const struct grid_cell *defaults, struct colour_palette *palette, + struct hyperlinks *hl) { const struct grid_cell *gcp; @@ -2152,11 +2176,11 @@ tty_cell(struct tty *tty, const struct grid_cell *gc, /* Check the output codeset and apply attributes. */ gcp = tty_check_codeset(tty, gc); - tty_attributes(tty, gcp, defaults, palette); + tty_attributes(tty, gcp, defaults, palette, hl); /* If it is a single character, write with putc to handle ACS. */ if (gcp->data.size == 1) { - tty_attributes(tty, gcp, defaults, palette); + tty_attributes(tty, gcp, defaults, palette, hl); if (*gcp->data.data < 0x20 || *gcp->data.data == 0x7f) return; tty_putc(tty, *gcp->data.data); @@ -2173,6 +2197,8 @@ tty_reset(struct tty *tty) struct grid_cell *gc = &tty->cell; if (!grid_cells_equal(gc, &grid_default_cell)) { + if (gc->link != 0) + tty_putcode_ptr2(tty, TTYC_HLS, "", ""); if ((gc->attr & GRID_ATTR_CHARSET) && tty_acs_needed(tty)) tty_putcode(tty, TTYC_RMACS); tty_putcode(tty, TTYC_SGR0); @@ -2462,9 +2488,29 @@ out: tty->cy = cy; } +static void +tty_hyperlink(struct tty *tty, const struct grid_cell *gc, + struct hyperlinks *hl) +{ + const char *uri, *id; + + if (gc->link == tty->cell.link) + return; + tty->cell.link = gc->link; + + if (hl == NULL) + return; + + if (gc->link == 0 || !hyperlinks_get(hl, gc->link, &uri, &id)) + tty_putcode_ptr2(tty, TTYC_HLS, "", ""); + else + tty_putcode_ptr2(tty, TTYC_HLS, id, uri); +} + void tty_attributes(struct tty *tty, const struct grid_cell *gc, - const struct grid_cell *defaults, struct colour_palette *palette) + const struct grid_cell *defaults, struct colour_palette *palette, + struct hyperlinks *hl) { struct grid_cell *tc = &tty->cell, gc2; int changed; @@ -2482,7 +2528,8 @@ tty_attributes(struct tty *tty, const struct grid_cell *gc, if (gc2.attr == tty->last_cell.attr && gc2.fg == tty->last_cell.fg && gc2.bg == tty->last_cell.bg && - gc2.us == tty->last_cell.us) + gc2.us == tty->last_cell.us && + gc2.link == tty->last_cell.link) return; /* @@ -2559,6 +2606,9 @@ tty_attributes(struct tty *tty, const struct grid_cell *gc, if ((changed & GRID_ATTR_CHARSET) && tty_acs_needed(tty)) tty_putcode(tty, TTYC_SMACS); + /* Set hyperlink if any. */ + tty_hyperlink(tty, gc, hl); + memcpy(&tty->last_cell, &gc2, sizeof tty->last_cell); } @@ -2924,13 +2974,13 @@ tty_default_colours(struct grid_cell *gc, struct window_pane *wp) static void tty_default_attributes(struct tty *tty, const struct grid_cell *defaults, - struct colour_palette *palette, u_int bg) + struct colour_palette *palette, u_int bg, struct hyperlinks *hl) { struct grid_cell gc; memcpy(&gc, &grid_default_cell, sizeof gc); gc.bg = bg; - tty_attributes(tty, &gc, defaults, palette); + tty_attributes(tty, &gc, defaults, palette, hl); } static void