From 03377b95523324a2a1657435f12c13a493ee5360 Mon Sep 17 00:00:00 2001 From: Kyle <50718101+kylesower@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:30:23 -0600 Subject: [PATCH] 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. --- runtime/doc/autocmd.txt | 1 + runtime/doc/news.txt | 1 + runtime/lua/vim/_defaults.lua | 9 ++++++- src/nvim/terminal.c | 15 ++++++++--- src/nvim/vterm/parser.c | 15 +++++++---- src/nvim/vterm/vterm_defs.h | 6 +++++ test/functional/terminal/parser_spec.lua | 34 +++++++++++++++++++++++- 7 files changed, 71 insertions(+), 10 deletions(-) diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 697f6303b4..5d732e6948 100644 --- 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 index 8698043a9a..bd5288a0e3 100644 --- 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 index e7d7735029..0fd99eb2a3 100644 --- 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 index 08eb4f2066..ebacc3224c 100644 --- 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 index 91531db8e5..91cf2dc8f8 100644 --- 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 index 9aa933bef0..8e148524c6 100644 --- 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 index 6c53957e6e..e3cfa243c2 100644 --- 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)