Skip to content

Commit cdacc12

Browse files
author
nicm
committed
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.
1 parent b22edcf commit cdacc12

File tree

15 files changed

+432
-48
lines changed

15 files changed

+432
-48
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ SRCS= alerts.c \
8181
grid-reader.c \
8282
grid-view.c \
8383
grid.c \
84+
hyperlinks.c \
8485
input-keys.c \
8586
input.c \
8687
job.c \

cmd-capture-pane.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ const struct cmd_entry cmd_clear_history_entry = {
5353
.name = "clear-history",
5454
.alias = "clearhist",
5555

56-
.args = { "t:", 0, 0, NULL },
57-
.usage = CMD_TARGET_PANE_USAGE,
56+
.args = { "Ht:", 0, 0, NULL },
57+
.usage = "[-H] " CMD_TARGET_PANE_USAGE,
5858

5959
.target = { 't', CMD_FIND_PANE, 0 },
6060

@@ -204,6 +204,8 @@ cmd_capture_pane_exec(struct cmd *self, struct cmdq_item *item)
204204
if (cmd_get_entry(self) == &cmd_clear_history_entry) {
205205
window_pane_reset_mode_all(wp);
206206
grid_clear_history(wp->base.grid);
207+
if (args_has(args, 'H'))
208+
screen_reset_hyperlinks(wp->screen);
207209
return (CMD_RETURN_NORMAL);
208210
}
209211

cmd-display-panes.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ cmd_display_panes_draw_pane(struct screen_redraw_ctx *ctx,
144144
llen = 0;
145145

146146
if (sx < len * 6 || sy < 5) {
147-
tty_attributes(tty, &fgc, &grid_default_cell, NULL);
147+
tty_attributes(tty, &fgc, &grid_default_cell, NULL, NULL);
148148
if (sx >= len + llen + 1) {
149149
len += llen + 1;
150150
tty_cursor(tty, xoff + px - len / 2, yoff + py);
@@ -161,7 +161,7 @@ cmd_display_panes_draw_pane(struct screen_redraw_ctx *ctx,
161161
px -= len * 3;
162162
py -= 2;
163163

164-
tty_attributes(tty, &bgc, &grid_default_cell, NULL);
164+
tty_attributes(tty, &bgc, &grid_default_cell, NULL, NULL);
165165
for (ptr = buf; *ptr != '\0'; ptr++) {
166166
if (*ptr < '0' || *ptr > '9')
167167
continue;
@@ -179,7 +179,7 @@ cmd_display_panes_draw_pane(struct screen_redraw_ctx *ctx,
179179

180180
if (sy <= 6)
181181
goto out;
182-
tty_attributes(tty, &fgc, &grid_default_cell, NULL);
182+
tty_attributes(tty, &fgc, &grid_default_cell, NULL, NULL);
183183
if (rlen != 0 && sx >= rlen) {
184184
tty_cursor(tty, xoff + sx - rlen, yoff);
185185
tty_putn(tty, rbuf, rlen, rlen);

grid.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,20 @@
3737

3838
/* Default grid cell data. */
3939
const struct grid_cell grid_default_cell = {
40-
{ { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0
40+
{ { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0
4141
};
4242

4343
/*
4444
* Padding grid cell data. Padding cells are the only zero width cell that
4545
* appears in the grid - because of this, they are always extended cells.
4646
*/
4747
static const struct grid_cell grid_padding_cell = {
48-
{ { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 0
48+
{ { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 0, 0
4949
};
5050

5151
/* Cleared grid cell data. */
5252
static const struct grid_cell grid_cleared_cell = {
53-
{ { ' ' }, 0, 1, 1 }, 0, GRID_FLAG_CLEARED, 8, 8, 0
53+
{ { ' ' }, 0, 1, 1 }, 0, GRID_FLAG_CLEARED, 8, 8, 0, 0
5454
};
5555
static const struct grid_cell_entry grid_cleared_entry = {
5656
GRID_FLAG_CLEARED, { .data = { 0, 8, 8, ' ' } }
@@ -90,6 +90,8 @@ grid_need_extended_cell(const struct grid_cell_entry *gce,
9090
return (1);
9191
if (gc->us != 0) /* only supports 256 or RGB */
9292
return (1);
93+
if (gc->link != 0)
94+
return (1);
9395
return (0);
9496
}
9597

@@ -131,6 +133,7 @@ grid_extended_cell(struct grid_line *gl, struct grid_cell_entry *gce,
131133
gee->fg = gc->fg;
132134
gee->bg = gc->bg;
133135
gee->us = gc->us;
136+
gee->link = gc->link;
134137
return (gee);
135138
}
136139

@@ -231,6 +234,8 @@ grid_cells_look_equal(const struct grid_cell *gc1, const struct grid_cell *gc2)
231234
return (0);
232235
if (gc1->attr != gc2->attr || gc1->flags != gc2->flags)
233236
return (0);
237+
if (gc1->link != gc2->link)
238+
return (0);
234239
return (1);
235240
}
236241

@@ -509,6 +514,7 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc)
509514
gc->fg = gee->fg;
510515
gc->bg = gee->bg;
511516
gc->us = gee->us;
517+
gc->link = gee->link;
512518
utf8_to_data(gee->data, &gc->data);
513519
}
514520
return;
@@ -524,6 +530,7 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc)
524530
gc->bg |= COLOUR_FLAG_256;
525531
gc->us = 0;
526532
utf8_set(&gc->data, gce->data.data);
533+
gc->link = 0;
527534
}
528535

529536
/* Get cell for reading. */

hyperlinks.c

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/* $OpenBSD$ */
2+
3+
/*
4+
* Copyright (c) 2021 Will <[email protected]>
5+
* Copyright (c) 2022 Jeff Chiang <[email protected]>
6+
*
7+
* Permission to use, copy, modify, and distribute this software for any
8+
* purpose with or without fee is hereby granted, provided that the above
9+
* copyright notice and this permission notice appear in all copies.
10+
*
11+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15+
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
16+
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
17+
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18+
*/
19+
20+
#include <sys/types.h>
21+
22+
#include <stdlib.h>
23+
#include <string.h>
24+
#include <vis.h>
25+
26+
#include "tmux.h"
27+
28+
/*
29+
* OSC 8 hyperlinks, described at:
30+
*
31+
* https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
32+
*
33+
* Each hyperlink and ID combination is assigned a number ("inner" in this
34+
* file) which is stored in an extended grid cell and maps into a tree here.
35+
*
36+
* Each URI has one inner number and one external ID (which tmux uses to send
37+
* the hyperlink to the terminal) and one internal ID (which is received from
38+
* the sending application inside tmux).
39+
*
40+
* Anonymous hyperlinks are each unique and are not reused even if they have
41+
* the same URI (terminals will not want to tie them together).
42+
*/
43+
44+
#define MAX_HYPERLINKS 5000
45+
46+
static uint64_t hyperlinks_next_external_id = 1;
47+
static u_int global_hyperlinks_count;
48+
49+
struct hyperlinks_uri {
50+
struct hyperlinks *tree;
51+
52+
u_int inner;
53+
const char *internal_id;
54+
const char *external_id;
55+
const char *uri;
56+
57+
TAILQ_ENTRY(hyperlinks_uri) list_entry;
58+
RB_ENTRY(hyperlinks_uri) by_inner_entry;
59+
RB_ENTRY(hyperlinks_uri) by_uri_entry; /* by internal ID and URI */
60+
};
61+
RB_HEAD(hyperlinks_by_uri_tree, hyperlinks_uri);
62+
RB_HEAD(hyperlinks_by_inner_tree, hyperlinks_uri);
63+
64+
TAILQ_HEAD(hyperlinks_list, hyperlinks_uri);
65+
static struct hyperlinks_list global_hyperlinks =
66+
TAILQ_HEAD_INITIALIZER(global_hyperlinks);
67+
68+
struct hyperlinks {
69+
u_int next_inner;
70+
struct hyperlinks_by_inner_tree by_inner;
71+
struct hyperlinks_by_uri_tree by_uri;
72+
};
73+
74+
static int
75+
hyperlinks_by_uri_cmp(struct hyperlinks_uri *left, struct hyperlinks_uri *right)
76+
{
77+
int r;
78+
79+
if (*left->internal_id == '\0' || *right->internal_id == '\0') {
80+
/*
81+
* If both URIs are anonymous, use the inner for comparison so
82+
* that they do not match even if the URI is the same - each
83+
* anonymous URI should be unique.
84+
*/
85+
if (*left->internal_id != '\0')
86+
return (-1);
87+
if (*right->internal_id != '\0')
88+
return (1);
89+
return (left->inner - right->inner);
90+
}
91+
92+
r = strcmp(left->internal_id, right->internal_id);
93+
if (r != 0)
94+
return (r);
95+
return (strcmp(left->uri, right->uri));
96+
}
97+
RB_PROTOTYPE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
98+
hyperlinks_by_uri_cmp);
99+
RB_GENERATE_STATIC(hyperlinks_by_uri_tree, hyperlinks_uri, by_uri_entry,
100+
hyperlinks_by_uri_cmp);
101+
102+
static int
103+
hyperlinks_by_inner_cmp(struct hyperlinks_uri *left,
104+
struct hyperlinks_uri *right)
105+
{
106+
return (left->inner - right->inner);
107+
}
108+
RB_PROTOTYPE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
109+
hyperlinks_by_inner_cmp);
110+
RB_GENERATE_STATIC(hyperlinks_by_inner_tree, hyperlinks_uri, by_inner_entry,
111+
hyperlinks_by_inner_cmp);
112+
113+
/* Remove a hyperlink. */
114+
static void
115+
hyperlinks_remove(struct hyperlinks_uri *hlu)
116+
{
117+
struct hyperlinks *hl = hlu->tree;
118+
119+
TAILQ_REMOVE(&global_hyperlinks, hlu, list_entry);
120+
global_hyperlinks_count--;
121+
122+
RB_REMOVE(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
123+
RB_REMOVE(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
124+
125+
free((void *)hlu->internal_id);
126+
free((void *)hlu->external_id);
127+
free((void *)hlu->uri);
128+
free(hlu);
129+
}
130+
131+
/* Store a new hyperlink or return if it already exists. */
132+
u_int
133+
hyperlinks_put(struct hyperlinks *hl, const char *uri_in,
134+
const char *internal_id_in)
135+
{
136+
struct hyperlinks_uri find, *hlu;
137+
char *uri, *internal_id, *external_id;
138+
139+
/*
140+
* Anonymous URI are stored with an empty internal ID and the tree
141+
* comparator will make sure they never match each other (so each
142+
* anonymous URI is unique).
143+
*/
144+
if (internal_id_in == NULL)
145+
internal_id_in = "";
146+
147+
utf8_stravis(&uri, uri_in, VIS_OCTAL|VIS_CSTYLE);
148+
utf8_stravis(&internal_id, internal_id_in, VIS_OCTAL|VIS_CSTYLE);
149+
150+
if (*internal_id_in != '\0') {
151+
find.uri = uri;
152+
find.internal_id = internal_id;
153+
154+
hlu = RB_FIND(hyperlinks_by_uri_tree, &hl->by_uri, &find);
155+
if (hlu != NULL) {
156+
free (uri);
157+
free (internal_id);
158+
return (hlu->inner);
159+
}
160+
}
161+
xasprintf(&external_id, "tmux%llX", hyperlinks_next_external_id++);
162+
163+
hlu = xcalloc(1, sizeof *hlu);
164+
hlu->inner = hl->next_inner++;
165+
hlu->internal_id = internal_id;
166+
hlu->external_id = external_id;
167+
hlu->uri = uri;
168+
hlu->tree = hl;
169+
RB_INSERT(hyperlinks_by_uri_tree, &hl->by_uri, hlu);
170+
RB_INSERT(hyperlinks_by_inner_tree, &hl->by_inner, hlu);
171+
172+
TAILQ_INSERT_TAIL(&global_hyperlinks, hlu, list_entry);
173+
if (++global_hyperlinks_count == MAX_HYPERLINKS)
174+
hyperlinks_remove(TAILQ_FIRST(&global_hyperlinks));
175+
176+
return (hlu->inner);
177+
}
178+
179+
/* Get hyperlink by inner number. */
180+
int
181+
hyperlinks_get(struct hyperlinks *hl, u_int inner, const char **uri_out,
182+
const char **external_id_out)
183+
{
184+
struct hyperlinks_uri find, *hlu;
185+
186+
find.inner = inner;
187+
188+
hlu = RB_FIND(hyperlinks_by_inner_tree, &hl->by_inner, &find);
189+
if (hlu == NULL)
190+
return (0);
191+
*external_id_out = hlu->external_id;
192+
*uri_out = hlu->uri;
193+
return (1);
194+
}
195+
196+
/* Initialize hyperlink set. */
197+
struct hyperlinks *
198+
hyperlinks_init(void)
199+
{
200+
struct hyperlinks *hl;
201+
202+
hl = xcalloc(1, sizeof *hl);
203+
hl->next_inner = 1;
204+
RB_INIT(&hl->by_uri);
205+
RB_INIT(&hl->by_inner);
206+
return (hl);
207+
}
208+
209+
/* Free all hyperlinks but not the set itself. */
210+
void
211+
hyperlinks_reset(struct hyperlinks *hl)
212+
{
213+
struct hyperlinks_uri *hlu, *hlu1;
214+
215+
RB_FOREACH_SAFE(hlu, hyperlinks_by_inner_tree, &hl->by_inner, hlu1)
216+
hyperlinks_remove(hlu);
217+
}
218+
219+
/* Free hyperlink set. */
220+
void
221+
hyperlinks_free(struct hyperlinks *hl)
222+
{
223+
hyperlinks_reset(hl);
224+
free(hl);
225+
}

0 commit comments

Comments
 (0)