feat(terminal): include sequence terminator in TermRequest event (#37152)

Problem:
Terminals should respond with the terminator (either BEL or ST) used in
the query so that clients can reliably parse the responses. The
`TermRequest` autocmd used to handle background color requests in the
terminal does not have access to the original sequence terminator, so it
always uses BEL. #37018

Solution:
Update vterm parsing to include the terminator type, then forward this
data into the emitted `TermRequest` events for OSC/DCS/APC sequences.
Update the foreground/background `TermRequest` callback to use the same
terminator as the original request.

Details:
I didn't add the terminator to the `TermResponse` event. However, I
assume the `TermResponse` event doesn't care about the terminator
because the sequence is already parsed. I also didn't update any of the
functions in `src/nvim/vterm/state.c` that write out responses. It
looked like those all pretty much used ST, and it would be a much larger
set of changes. In that same file, there's also logic for 8 bit ST
sequences, but from what I can tell, 8 bit doesn't really work (see `:h
xterm-8bit`), so I didn't use the 8 bit ST at all.
This commit is contained in:
Kyle
2025-12-29 16:30:23 -06:00
committed by GitHub
parent ddd6ac5083
commit 03377b9552
7 changed files with 71 additions and 10 deletions

View File

@@ -1081,6 +1081,7 @@ TermRequest When a |:terminal| child process emits an OSC,
fields:
- sequence: the received sequence
- terminator: the received sequence terminator (i.e. BEL or ST)
- cursor: (1,0)-indexed, buffer-relative
position of the cursor when the sequence was
received (line number may be <= 0 if the

View File

@@ -231,6 +231,7 @@ EVENTS
• Creating or updating a progress message with |nvim_echo()| triggers a |Progress| event.
• |MarkSet| is triggered after a |mark| is set by the user (currently doesn't
support implicit marks like |'[| or |'<|, …).
• New `terminator` parameter for |TermRequest| event.
HIGHLIGHTS

View File

@@ -567,7 +567,14 @@ do
red, green, blue = 65535, 65535, 65535
end
local command = fg_request and 10 or 11
local data = string.format('\027]%d;rgb:%04x/%04x/%04x\007', command, red, green, blue)
local data = string.format(
'\027]%d;rgb:%04x/%04x/%04x%s',
command,
red,
green,
blue,
args.data.terminator
)
vim.api.nvim_chan_send(channel, data)
end
end,

View File

@@ -202,6 +202,7 @@ struct terminal {
StringBuilder selection; ///< Growable array containing full selection data
StringBuilder termrequest_buffer; ///< Growable array containing unfinished request sequence
VTermTerminator termrequest_terminator; ///< Terminator (BEL or ST) used in the termrequest
size_t refcount; // reference count
};
@@ -234,6 +235,7 @@ static void emit_termrequest(void **argv)
int row = (int)(intptr_t)argv[4];
int col = (int)(intptr_t)argv[5];
size_t sb_deleted = (size_t)(intptr_t)argv[6];
VTermTerminator terminator = (VTermTerminator)(intptr_t)argv[7];
if (term->sb_pending > 0) {
// Don't emit the event while there is pending scrollback because we need
@@ -242,7 +244,7 @@ static void emit_termrequest(void **argv)
// terminal is refreshed and the pending scrollback is cleared.
multiqueue_put(term->pending.events, emit_termrequest, term, sequence, (void *)sequence_length,
pending_send, (void *)(intptr_t)row, (void *)(intptr_t)col,
(void *)(intptr_t)sb_deleted);
(void *)(intptr_t)sb_deleted, (void *)(intptr_t)terminator);
return;
}
@@ -252,10 +254,13 @@ static void emit_termrequest(void **argv)
ADD_C(cursor, INTEGER_OBJ(row - (int64_t)(term->sb_deleted - sb_deleted)));
ADD_C(cursor, INTEGER_OBJ(col));
MAXSIZE_TEMP_DICT(data, 2);
MAXSIZE_TEMP_DICT(data, 3);
String termrequest = { .data = sequence, .size = sequence_length };
PUT_C(data, "sequence", STRING_OBJ(termrequest));
PUT_C(data, "cursor", ARRAY_OBJ(cursor));
PUT_C(data, "terminator",
terminator ==
VTERM_TERMINATOR_BEL ? STATIC_CSTR_AS_OBJ("\x07") : STATIC_CSTR_AS_OBJ("\x1b\\"));
buf_T *buf = handle_get_buffer(term->buf_handle);
apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, NULL,
@@ -284,7 +289,8 @@ static void schedule_termrequest(Terminal *term)
xmemdup(term->termrequest_buffer.items, term->termrequest_buffer.size),
(void *)(intptr_t)term->termrequest_buffer.size, term->pending.send,
(void *)(intptr_t)line, (void *)(intptr_t)term->cursor.col,
(void *)(intptr_t)term->sb_deleted);
(void *)(intptr_t)term->sb_deleted,
(void *)(intptr_t)term->termrequest_terminator);
}
static int parse_osc8(const char *str, int *attr)
@@ -336,6 +342,7 @@ static int on_osc(int command, VTermStringFragment frag, void *user)
}
kv_concat_len(term->termrequest_buffer, frag.str, frag.len);
if (frag.final) {
term->termrequest_terminator = frag.terminator;
if (has_event(EVENT_TERMREQUEST)) {
schedule_termrequest(term);
}
@@ -370,6 +377,7 @@ static int on_dcs(const char *command, size_t commandlen, VTermStringFragment fr
}
kv_concat_len(term->termrequest_buffer, frag.str, frag.len);
if (frag.final) {
term->termrequest_terminator = frag.terminator;
schedule_termrequest(term);
}
return 1;
@@ -392,6 +400,7 @@ static int on_apc(VTermStringFragment frag, void *user)
}
kv_concat_len(term->termrequest_buffer, frag.str, frag.len);
if (frag.final) {
term->termrequest_terminator = frag.terminator;
schedule_termrequest(term);
}
return 1;

View File

@@ -72,13 +72,15 @@ static void do_escape(VTerm *vt, char command)
DEBUG_LOG("libvterm: Unhandled escape ESC 0x%02x\n", command);
}
static void string_fragment(VTerm *vt, const char *str, size_t len, bool final)
static void string_fragment(VTerm *vt, const char *str, size_t len, bool final,
VTermTerminator terminator)
{
VTermStringFragment frag = {
.str = str,
.len = len,
.initial = vt->parser.string_initial,
.final = final,
.terminator = terminator,
};
switch (vt->parser.state) {
@@ -160,7 +162,8 @@ size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len)
if (c == 0x00 || c == 0x7f) { // NUL, DEL
if (IS_STRING_STATE()) {
string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false);
string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false,
VTERM_TERMINATOR_ST);
string_start = bytes + pos + 1;
}
if (vt->parser.emit_nul) {
@@ -188,7 +191,8 @@ size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len)
continue; // All other C0s permitted in SOS
}
if (IS_STRING_STATE()) {
string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false);
string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false,
VTERM_TERMINATOR_ST);
}
do_control(vt, c);
if (IS_STRING_STATE()) {
@@ -316,7 +320,8 @@ string_state:
case PM:
case SOS:
if (c == 0x07 || (c1_allowed && c == 0x9c)) {
string_fragment(vt, string_start, string_len, true);
string_fragment(vt, string_start, string_len, true,
c == 0x07 ? VTERM_TERMINATOR_BEL : VTERM_TERMINATOR_ST);
ENTER_NORMAL_STATE();
}
break;
@@ -395,7 +400,7 @@ string_state:
if (vt->parser.in_esc) {
string_len -= 1;
}
string_fragment(vt, string_start, string_len, false);
string_fragment(vt, string_start, string_len, false, VTERM_TERMINATOR_ST);
}
}

View File

@@ -91,11 +91,17 @@ typedef enum {
VTERM_N_PROPS,
} VTermProp;
typedef enum {
VTERM_TERMINATOR_BEL, // \x07
VTERM_TERMINATOR_ST, // \x1b\x5c
} VTermTerminator;
typedef struct {
const char *str;
size_t len : 30;
bool initial : 1;
bool final : 1;
VTermTerminator terminator;
} VTermStringFragment;
typedef union {

View File

@@ -1,11 +1,13 @@
local n = require('test.functional.testnvim')()
local clear = n.clear
local api = n.api
local assert_alive = n.assert_alive
local clear = n.clear
local exec_lua = n.exec_lua
local OSC_PREFIX = string.char(0x1b, 0x5d)
local BEL = string.char(0x07)
local ST = string.char(0x1b, 0x5c)
local NUL = string.char(0x00)
describe(':terminal', function()
@@ -60,4 +62,34 @@ describe(':terminal', function()
api.nvim_chan_send(chan, input)
assert_alive()
end)
it('uses terminator matching query for OSC TermRequest #37018', function()
local chan = api.nvim_open_term(0, {})
exec_lua([[
vim.api.nvim_create_autocmd("TermRequest", {
callback = function(args)
_G.osc10_response = {sequence = args.data.sequence, terminator = args.data.terminator }
end
})
]])
local function send_osc_with_terminator(terminator)
local input = OSC_PREFIX .. '10;?' .. terminator
api.nvim_chan_send(chan, input)
end
send_osc_with_terminator(BEL)
--- @type string
assert.same(
{ sequence = OSC_PREFIX .. '10;?', terminator = BEL },
exec_lua([[return _G.osc10_response]])
)
send_osc_with_terminator(ST)
--- @type string
assert.same(
{ sequence = OSC_PREFIX .. '10;?', terminator = ST },
exec_lua([[return _G.osc10_response]])
)
end)
end)