From bcd54d12d852f97586fc76ac93da9002d0804ef5 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 29 Jun 2026 17:08:52 +0000 Subject: [PATCH] Add link=/nolink to styles to specific a hyperlink. GitHub issue 4280 from Moritz Angermann. --- format-draw.c | 14 +++++++++++++- style.c | 48 +++++++++++++++++++++++++++++++++++++++++++----- tmux.1 | 22 ++++++++++++++++++++++ tmux.h | 3 +++ tty-features.c | 1 + 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/format-draw.c b/format-draw.c index 510f28e08..47bf192e6 100644 --- a/format-draw.c +++ b/format-draw.c @@ -710,6 +710,7 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, "AFTER" }; size_t size = strlen(expanded); struct screen *os = octx->s, s[TOTAL]; + struct hyperlinks *hl = os->hyperlinks; struct screen_write_ctx ctx[TOTAL]; u_int ocx = os->cx, ocy = os->cy, n, i, width[TOTAL]; u_int map[] = { LEFT, @@ -723,7 +724,7 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, struct grid_cell gc, current_default, base_default; struct style sy, saved_sy; struct utf8_data *ud = &sy.gc.data; - const char *cp, *end; + const char *cp, *end, *link_uri; enum utf8_state more; char *tmp; struct format_range *fr = NULL, *fr1; @@ -838,6 +839,17 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, sy.gc.fg = base->fg; } + /* + * Resolve any hyperlink and store it in the cell. The URI + * doubles as the internal ID so repeated links share one entry + * and the ID stays stable across redraws. + */ + link_uri = style_link(&sy); + if (link_uri != NULL && hl != NULL) + sy.gc.link = hyperlinks_put(hl, link_uri, link_uri); + else + sy.gc.link = 0; + /* If this style has a fill colour, store it for later. */ if (sy.fill != 8) fill = sy.fill; diff --git a/style.c b/style.c index 25b8b0196..2ca5e37af 100644 --- a/style.c +++ b/style.c @@ -42,9 +42,17 @@ static struct style style_default = { STYLE_WIDTH_DEFAULT, 0, STYLE_PAD_DEFAULT, - STYLE_DEFAULT_BASE + STYLE_DEFAULT_BASE, + + 0 }; +/* + * Global hyperlink set holding the URIs for #[link=...] styles, so a style + * only needs to store a small ID rather than the URI itself. + */ +static struct hyperlinks *style_hyperlinks; + /* Set range string. */ static void style_set_range_string(struct style *sy, const char *s) @@ -91,6 +99,7 @@ style_parse(struct style *sy, const struct grid_cell *base, const char *in) sy->gc.us = base->us; sy->gc.attr = base->attr; sy->gc.flags = base->flags; + sy->link = 0; } else if (strcasecmp(tmp, "ignore") == 0) sy->ignore = 1; else if (strcasecmp(tmp, "noignore") == 0) @@ -234,7 +243,9 @@ style_parse(struct style *sy, const struct grid_cell *base, const char *in) } else if (strcasecmp(tmp, "none") == 0) sy->gc.attr = 0; else if (end > 2 && strncasecmp(tmp, "no", 2) == 0) { - if (strcmp(tmp + 2, "attr") == 0) + if (strcmp(tmp + 2, "link") == 0) + sy->link = 0; + else if (strcmp(tmp + 2, "attr") == 0) sy->gc.attr |= GRID_ATTR_NOATTR; else { value = attributes_fromstring(tmp + 2); @@ -262,6 +273,15 @@ style_parse(struct style *sy, const struct grid_cell *base, const char *in) if (errstr != NULL) goto error; sy->pad = (int)n; + } else if (strncasecmp(tmp, "link=", 5) == 0) { + if (tmp[5] == '\0') + sy->link = 0; + else { + if (style_hyperlinks == NULL) + style_hyperlinks = hyperlinks_init(); + sy->link = hyperlinks_put(style_hyperlinks, + tmp + 5, tmp + 5); + } } else { if ((value = attributes_fromstring(tmp)) == -1) goto error; @@ -284,8 +304,8 @@ style_tostring(struct style *sy) { struct grid_cell *gc = &sy->gc; int off = 0; - const char *comma = "", *tmp = ""; - static char s[1024]; + const char *comma = "", *tmp = "", *uri; + static char s[2048]; char b[21]; *s = '\0'; @@ -389,15 +409,33 @@ style_tostring(struct style *sy) comma = ","; } if (sy->pad >= 0) { - xsnprintf(s + off, sizeof s - off, "%spad=%u", comma, + off += xsnprintf(s + off, sizeof s - off, "%spad=%u", comma, sy->pad); comma = ","; } + uri = style_link(sy); + if (uri != NULL) { + xsnprintf(s + off, sizeof s - off, "%slink=%s", comma, uri); + comma = ","; + } if (*s == '\0') return ("default"); return (s); } +/* Get the hyperlink URI for a style, or NULL if it has none. */ +const char * +style_link(struct style *sy) +{ + const char *uri; + + if (sy->link == 0 || style_hyperlinks == NULL) + return (NULL); + if (!hyperlinks_get(style_hyperlinks, sy->link, &uri, NULL, NULL)) + return (NULL); + return (uri); +} + /* Apply a style on top of the given style. */ struct style * style_add(struct grid_cell *gc, struct options *oo, const char *name, diff --git a/tmux.1 b/tmux.1 index 8f74205f8..2e4d2bceb 100644 --- a/tmux.1 +++ b/tmux.1 @@ -7266,6 +7266,28 @@ Set the width of the styled area. .Ar N may be a column count or a percentage (for example .Ql 50% ) . +.It Xo Ic link=uri +(or +.Ic nolink ) +.Xc +Make the styled text an OSC 8 hyperlink to +.Ar uri , +for example +.Ql #[link=https://example.com]text#[nolink] . +This is emitted only to terminals with the +.Ic hyperlinks +feature (see +.Ic terminal-features ) ; +it works in the status line and in other formats drawn with styles. +The link continues until +.Ic nolink , +.Ic default , +or an empty +.Ic link= +is given. +The +.Ar uri +may not contain spaces or commas and is limited in length. .It Xo Ic list=on , .Ic list=focus , .Ic list=left\-marker , diff --git a/tmux.h b/tmux.h index f34b3b324..4bfdf4300 100644 --- a/tmux.h +++ b/tmux.h @@ -984,6 +984,8 @@ struct style { int pad; enum style_default_type default_type; + + u_int link; }; /* Cursor style. */ @@ -3960,6 +3962,7 @@ int style_parse(struct style *,const struct grid_cell *, int style_parse_colour(struct style *, const struct grid_cell *, const char *); const char *style_tostring(struct style *); +const char *style_link(struct style *); struct style *style_add(struct grid_cell *, struct options *, const char *, struct format_tree *); void style_apply(struct grid_cell *, struct options *, diff --git a/tty-features.c b/tty-features.c index 4f0537b14..b321af279 100644 --- a/tty-features.c +++ b/tty-features.c @@ -570,6 +570,7 @@ tty_default_features(int *feat, const char *name, u_int version) "cstyle," "extkeys," "focus," + "hyperlinks," "usstyle" }, { .name = "XTerm",