Add support for OSC 8 hyperlinks (a VTE extension now supported by other
authornicm <nicm@openbsd.org>
Thu, 30 Jun 2022 09:55:53 +0000 (09:55 +0000)
committernicm <nicm@openbsd.org>
Thu, 30 Jun 2022 09:55:53 +0000 (09:55 +0000)
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.

15 files changed:
usr.bin/tmux/Makefile
usr.bin/tmux/cmd-capture-pane.c
usr.bin/tmux/cmd-display-panes.c
usr.bin/tmux/grid.c
usr.bin/tmux/hyperlinks.c [new file with mode: 0644]
usr.bin/tmux/input.c
usr.bin/tmux/screen-redraw.c
usr.bin/tmux/screen.c
usr.bin/tmux/server.c
usr.bin/tmux/style.c
usr.bin/tmux/tmux.1
usr.bin/tmux/tmux.h
usr.bin/tmux/tty-features.c
usr.bin/tmux/tty-term.c
usr.bin/tmux/tty.c

index e73ff61..7691ca0 100644 (file)
@@ -1,4 +1,4 @@
-# $OpenBSD: Makefile,v 1.109 2022/05/30 12:48:57 nicm Exp $
+# $OpenBSD: Makefile,v 1.110 2022/06/30 09:55:53 nicm Exp $
 
 PROG=  tmux
 SRCS=  alerts.c \
@@ -81,6 +81,7 @@ SRCS= alerts.c \
        grid-reader.c \
        grid-view.c \
        grid.c \
+       hyperlinks.c \
        input-keys.c \
        input.c \
        job.c \
index b860886..08ec85a 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: cmd-capture-pane.c,v 1.56 2022/06/07 10:02:19 nicm Exp $ */
+/* $OpenBSD: cmd-capture-pane.c,v 1.57 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2009 Jonathan Alvarado <radobobo@users.sourceforge.net>
@@ -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);
        }
 
index 6edcedc..2546ff7 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: cmd-display-panes.c,v 1.44 2021/08/25 08:51:55 nicm Exp $ */
+/* $OpenBSD: cmd-display-panes.c,v 1.45 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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);
index a018975..3658fcc 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: grid.c,v 1.124 2022/06/21 09:30:01 nicm Exp $ */
+/* $OpenBSD: grid.c,v 1.125 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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/usr.bin/tmux/hyperlinks.c b/usr.bin/tmux/hyperlinks.c
new file mode 100644 (file)
index 0000000..6e336ec
--- /dev/null
@@ -0,0 +1,225 @@
+/* $OpenBSD: hyperlinks.c,v 1.1 2022/06/30 09:55:53 nicm Exp $ */
+
+/*
+ * 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 <vis.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 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);
+}
index 711e1f0..147a3e6 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: input.c,v 1.205 2022/06/11 16:59:33 nicm Exp $ */
+/* $OpenBSD: input.c,v 1.206 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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)
index 710762b..dd87e0e 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: screen-redraw.c,v 1.95 2022/03/16 17:00:17 nicm Exp $ */
+/* $OpenBSD: screen-redraw.c,v 1.96 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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);
 }
index 41de516..30267c4 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: screen.c,v 1.80 2022/05/30 12:55:25 nicm Exp $ */
+/* $OpenBSD: screen.c,v 1.81 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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);
 }
 
index f98ea71..3fd1a92 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: server.c,v 1.202 2022/06/21 09:30:01 nicm Exp $ */
+/* $OpenBSD: server.c,v 1.203 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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);
index 609872d..040ca3b 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: style.c,v 1.30 2021/08/12 20:46:30 nicm Exp $ */
+/* $OpenBSD: style.c,v 1.31 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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,
index c437541..5ac2a63 100644 (file)
@@ -1,4 +1,4 @@
-.\" $OpenBSD: tmux.1,v 1.892 2022/06/20 07:59:37 nicm Exp $
+.\" $OpenBSD: tmux.1,v 1.893 2022/06/30 09:55:53 nicm Exp $
 .\"
 .\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
 .\"
@@ -14,7 +14,7 @@
 .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.Dd $Mdocdate: June 20 2022 $
+.Dd $Mdocdate: June 30 2022 $
 .Dt TMUX 1
 .Os
 .Sh NAME
@@ -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
index 9f9e3bc..38a174e 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: tmux.h,v 1.1175 2022/06/21 09:30:01 nicm Exp $ */
+/* $OpenBSD: tmux.h,v 1.1176 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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 */
index 631a7eb..41a0697 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: tty-features.c,v 1.24 2022/06/14 07:29:00 nicm Exp $ */
+/* $OpenBSD: tty-features.c,v 1.25 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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",
                  /*
index 36e3f2b..197c50f 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: tty-term.c,v 1.92 2022/03/24 09:05:57 nicm Exp $ */
+/* $OpenBSD: tty-term.c,v 1.93 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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" },
index a36d843..8da3cec 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: tty.c,v 1.420 2022/06/09 09:12:55 nicm Exp $ */
+/* $OpenBSD: tty.c,v 1.421 2022/06/30 09:55:53 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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