commit 03377b95523324a2a1657435f12c13a493ee5360
parent ddd6ac508360111fb04991916432e5baac440213
Author: Kyle <50718101+kylesower@users.noreply.github.com>
Date: Mon, 29 Dec 2025 16:30:23 -0600
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.
Diffstat:
7 files changed, 71 insertions(+), 10 deletions(-)
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
@@ -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
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -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
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
@@ -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,
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
@@ -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;
diff --git a/src/nvim/vterm/parser.c b/src/nvim/vterm/parser.c
@@ -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);
}
}
diff --git a/src/nvim/vterm/vterm_defs.h b/src/nvim/vterm/vterm_defs.h
@@ -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 {
diff --git a/test/functional/terminal/parser_spec.lua b/test/functional/terminal/parser_spec.lua
@@ -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)