aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregory Anders <8965202+gpanders@users.noreply.github.com>2024-08-15 06:09:14 -0500
committerGitHub <noreply@github.com>2024-08-15 06:09:14 -0500
commit4199671047b0cb62781995a8f6a4b66fb6e8b6fd (patch)
tree9018e8b4765279f423c89b41a3a27f52d7029fdc
parentf3677c71f04ee6ef52449664dfb37f0477fd7305 (diff)
downloadrneovim-4199671047b0cb62781995a8f6a4b66fb6e8b6fd.tar.gz
rneovim-4199671047b0cb62781995a8f6a4b66fb6e8b6fd.tar.bz2
rneovim-4199671047b0cb62781995a8f6a4b66fb6e8b6fd.zip
feat(term): support OSC 8 hyperlinks in :terminal (#30050)
-rw-r--r--runtime/doc/news.txt2
-rw-r--r--src/nvim/terminal.c61
-rw-r--r--src/vterm/pen.c70
-rw-r--r--src/vterm/screen.c16
-rw-r--r--src/vterm/vterm.c1
-rw-r--r--src/vterm/vterm.h6
-rw-r--r--src/vterm/vterm_internal.h1
-rw-r--r--test/functional/terminal/highlight_spec.lua20
8 files changed, 173 insertions, 4 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 68a05a99a4..7c842f42dd 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -166,6 +166,8 @@ TERMINAL
• The terminal buffer now supports reflow (wrapped lines adapt when the buffer
is resized horizontally). Note: Lines that are not visible and kept in
'scrollback' are not reflown.
+• The |terminal| now supports OSC 8 escape sequences and will display
+ hyperlinks in supporting host terminals.
TREESITTER
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
index 54a0de9c22..2b44763ddd 100644
--- a/src/nvim/terminal.c
+++ b/src/nvim/terminal.c
@@ -227,11 +227,66 @@ static void schedule_termrequest(Terminal *term, char *payload, size_t payload_l
term->pending_send);
}
+static int parse_osc8(VTermStringFragment frag, int *attr)
+ FUNC_ATTR_NONNULL_ALL
+{
+ // Parse the URI from the OSC 8 sequence and add the URL to our URL set.
+ // Skip the ID, we don't use it (for now)
+ size_t i = 0;
+ for (; i < frag.len; i++) {
+ if (frag.str[i] == ';') {
+ break;
+ }
+ }
+
+ // Move past the semicolon
+ i++;
+
+ if (i >= frag.len) {
+ // Invalid OSC sequence
+ return 0;
+ }
+
+ // Find the terminator
+ const size_t start = i;
+ for (; i < frag.len; i++) {
+ if (frag.str[i] == '\a' || frag.str[i] == '\x1b') {
+ break;
+ }
+ }
+
+ const size_t len = i - start;
+ if (len == 0) {
+ // Empty OSC 8, no URL
+ *attr = 0;
+ return 1;
+ }
+
+ char *url = xmemdupz(&frag.str[start], len + 1);
+ url[len] = 0;
+ *attr = hl_add_url(0, url);
+ xfree(url);
+
+ return 1;
+}
+
static int on_osc(int command, VTermStringFragment frag, void *user)
{
+ Terminal *term = user;
+
if (frag.str == NULL) {
return 0;
}
+
+ if (command == 8) {
+ int attr = 0;
+ if (parse_osc8(frag, &attr)) {
+ VTermState *state = vterm_obtain_state(term->vt);
+ VTermValue value = { .number = attr };
+ vterm_state_set_penattr(state, VTERM_ATTR_URI, VTERM_VALUETYPE_INT, &value);
+ }
+ }
+
if (!has_event(EVENT_TERMREQUEST)) {
return 1;
}
@@ -239,7 +294,7 @@ static int on_osc(int command, VTermStringFragment frag, void *user)
StringBuilder request = KV_INITIAL_VALUE;
kv_printf(request, "\x1b]%d;", command);
kv_concat_len(request, frag.str, frag.len);
- schedule_termrequest(user, request.items, request.size);
+ schedule_termrequest(term, request.items, request.size);
return 1;
}
@@ -992,6 +1047,10 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *te
});
}
+ if (cell.uri > 0) {
+ attr_id = hl_combine_attr(attr_id, cell.uri);
+ }
+
if (term->cursor.visible && term->cursor.row == row
&& term->cursor.col == col) {
attr_id = hl_combine_attr(attr_id,
diff --git a/src/vterm/pen.c b/src/vterm/pen.c
index 562005435e..1876eb9881 100644
--- a/src/vterm/pen.c
+++ b/src/vterm/pen.c
@@ -182,6 +182,8 @@ INTERNAL void vterm_state_resetpen(VTermState *state)
state->pen.fg = state->default_fg; setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg);
state->pen.bg = state->default_bg; setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg);
+
+ state->pen.uri = 0; setpenattr_int(state, VTERM_ATTR_URI, 0);
}
INTERNAL void vterm_state_savepen(VTermState *state, int save)
@@ -205,6 +207,8 @@ INTERNAL void vterm_state_savepen(VTermState *state, int save)
setpenattr_col( state, VTERM_ATTR_FOREGROUND, state->pen.fg);
setpenattr_col( state, VTERM_ATTR_BACKGROUND, state->pen.bg);
+
+ setpenattr_int( state, VTERM_ATTR_URI, state->pen.uri);
}
}
@@ -600,9 +604,75 @@ int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue
val->number = state->pen.baseline;
return 1;
+ case VTERM_ATTR_URI:
+ val->number = state->pen.uri;
+ return 1;
+
case VTERM_N_ATTRS:
return 0;
}
return 0;
}
+
+int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val)
+{
+ if (!val) {
+ return 0;
+ }
+
+ if(type != vterm_get_attr_type(attr)) {
+ DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n",
+ attr, vterm_get_attr_type(attr), type);
+ return 0;
+ }
+
+ switch (attr) {
+ case VTERM_ATTR_BOLD:
+ state->pen.bold = val->boolean;
+ break;
+ case VTERM_ATTR_UNDERLINE:
+ state->pen.underline = val->number;
+ break;
+ case VTERM_ATTR_ITALIC:
+ state->pen.italic = val->boolean;
+ break;
+ case VTERM_ATTR_BLINK:
+ state->pen.blink = val->boolean;
+ break;
+ case VTERM_ATTR_REVERSE:
+ state->pen.reverse = val->boolean;
+ break;
+ case VTERM_ATTR_CONCEAL:
+ state->pen.conceal = val->boolean;
+ break;
+ case VTERM_ATTR_STRIKE:
+ state->pen.strike = val->boolean;
+ break;
+ case VTERM_ATTR_FONT:
+ state->pen.font = val->number;
+ break;
+ case VTERM_ATTR_FOREGROUND:
+ state->pen.fg = val->color;
+ break;
+ case VTERM_ATTR_BACKGROUND:
+ state->pen.bg = val->color;
+ break;
+ case VTERM_ATTR_SMALL:
+ state->pen.small = val->boolean;
+ break;
+ case VTERM_ATTR_BASELINE:
+ state->pen.baseline = val->number;
+ break;
+ case VTERM_ATTR_URI:
+ state->pen.uri = val->number;
+ break;
+ default:
+ return 0;
+ }
+
+ if(state->callbacks && state->callbacks->setpenattr)
+ (*state->callbacks->setpenattr)(attr, val, state->cbdata);
+
+ return 1;
+}
diff --git a/src/vterm/screen.c b/src/vterm/screen.c
index 720d1bb939..bd3cbd6bd0 100644
--- a/src/vterm/screen.c
+++ b/src/vterm/screen.c
@@ -17,6 +17,9 @@ typedef struct
/* After the bitfield */
VTermColor fg, bg;
+ /* Opaque ID that maps to a URI in a set */
+ int uri;
+
unsigned int bold : 1;
unsigned int underline : 2;
unsigned int italic : 1;
@@ -444,6 +447,9 @@ static int setpenattr(VTermAttr attr, VTermValue *val, void *user)
case VTERM_ATTR_BASELINE:
screen->pen.baseline = val->number;
return 1;
+ case VTERM_ATTR_URI:
+ screen->pen.uri = val->number;
+ return 1;
case VTERM_N_ATTRS:
return 0;
@@ -705,6 +711,8 @@ static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new
dst->pen.fg = src->fg;
dst->pen.bg = src->bg;
+ dst->pen.uri = src->uri;
+
if(src->width == 2 && pos.col < (new_cols-1))
(dst + 1)->chars[0] = (uint32_t) -1;
}
@@ -997,6 +1005,8 @@ int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCe
cell->fg = intcell->pen.fg;
cell->bg = intcell->pen.bg;
+ cell->uri = intcell->pen.uri;
+
if(pos.col < (screen->cols - 1) &&
getcell(screen, pos.row, pos.col + 1)->chars[0] == (uint32_t)-1)
cell->width = 2;
@@ -1116,9 +1126,11 @@ static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b)
return 1;
if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg))
return 1;
- if((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small))
+ if((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small))
+ return 1;
+ if((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline))
return 1;
- if((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline))
+ if((attrs & VTERM_ATTR_URI_MASK) && (a->pen.uri != b->pen.uri))
return 1;
return 0;
diff --git a/src/vterm/vterm.c b/src/vterm/vterm.c
index e1f676f5b6..870a61566e 100644
--- a/src/vterm/vterm.c
+++ b/src/vterm/vterm.c
@@ -278,6 +278,7 @@ VTermValueType vterm_get_attr_type(VTermAttr attr)
case VTERM_ATTR_BACKGROUND: return VTERM_VALUETYPE_COLOR;
case VTERM_ATTR_SMALL: return VTERM_VALUETYPE_BOOL;
case VTERM_ATTR_BASELINE: return VTERM_VALUETYPE_INT;
+ case VTERM_ATTR_URI: return VTERM_VALUETYPE_INT;
case VTERM_N_ATTRS: return 0;
}
diff --git a/src/vterm/vterm.h b/src/vterm/vterm.h
index 44e15023c0..929418c63a 100644
--- a/src/vterm/vterm.h
+++ b/src/vterm/vterm.h
@@ -245,6 +245,7 @@ typedef enum {
VTERM_ATTR_BACKGROUND, // color: 40-49 100-107
VTERM_ATTR_SMALL, // bool: 73, 74, 75
VTERM_ATTR_BASELINE, // number: 73, 74, 75
+ VTERM_ATTR_URI, // number
VTERM_N_ATTRS
} VTermAttr;
@@ -470,6 +471,7 @@ void vterm_state_set_default_colors(VTermState *state, const VTermColor *default
void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col);
void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright);
int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val);
+int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val);
int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val);
void vterm_state_focus_in(VTermState *state);
void vterm_state_focus_out(VTermState *state);
@@ -529,6 +531,7 @@ typedef struct {
char width;
VTermScreenCellAttrs attrs;
VTermColor fg, bg;
+ int uri;
} VTermScreenCell;
typedef struct {
@@ -589,8 +592,9 @@ typedef enum {
VTERM_ATTR_CONCEAL_MASK = 1 << 9,
VTERM_ATTR_SMALL_MASK = 1 << 10,
VTERM_ATTR_BASELINE_MASK = 1 << 11,
+ VTERM_ATTR_URI_MASK = 1 << 12,
- VTERM_ALL_ATTRS_MASK = (1 << 12) - 1
+ VTERM_ALL_ATTRS_MASK = (1 << 13) - 1
} VTermAttrMask;
int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs);
diff --git a/src/vterm/vterm_internal.h b/src/vterm/vterm_internal.h
index e79d74be6f..53f9b5e100 100644
--- a/src/vterm/vterm_internal.h
+++ b/src/vterm/vterm_internal.h
@@ -40,6 +40,7 @@ struct VTermPen
{
VTermColor fg;
VTermColor bg;
+ int uri;
unsigned int bold:1;
unsigned int underline:2;
unsigned int italic:1;
diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua
index 4f3d010d02..ca41cbf4a2 100644
--- a/test/functional/terminal/highlight_spec.lua
+++ b/test/functional/terminal/highlight_spec.lua
@@ -380,3 +380,23 @@ describe(':terminal highlight with custom palette', function()
]])
end)
end)
+
+describe(':terminal', function()
+ before_each(clear)
+
+ it('can display URLs', function()
+ local screen = Screen.new(50, 7)
+ screen:add_extra_attr_ids {
+ [100] = { url = 'https://example.com' },
+ }
+ screen:attach()
+ local chan = api.nvim_open_term(0, {})
+ api.nvim_chan_send(chan, '\027]8;;https://example.com\027\\Example\027]8;;\027\\')
+ screen:expect({
+ grid = [[
+ {100:^Example} |
+ |*6
+ ]],
+ })
+ end)
+end)