Merge pull request #34860 from gpanders/push-lorwmnmtysnt

feat(tui): use DA1 response to determine OSC 52 support
This commit is contained in:
Gregory Anders
2025-07-17 18:47:33 -05:00
committed by GitHub
10 changed files with 180 additions and 43 deletions

View File

@@ -4130,9 +4130,9 @@ nvim_ui_term_event({event}, {value}) *nvim_ui_term_event()*
Tells Nvim when a terminal event has occurred
The following terminal events are supported:
• "termresponse": The terminal sent an OSC, DCS, or APC response sequence
to Nvim. The payload is the received response. Sets |v:termresponse| and
fires |TermResponse|.
• "termresponse": The terminal sent a DA1, OSC, DCS, or APC response
sequence to Nvim. The payload is the received response. Sets
|v:termresponse| and fires |TermResponse|.
Attributes: ~
|RPC| only

View File

@@ -1043,7 +1043,7 @@ TermRequest When a |:terminal| child process emits an OSC,
autocommand defined without |autocmd-nested|.
*TermResponse*
TermResponse When Nvim receives an OSC, DCS, or APC response from
TermResponse When Nvim receives a DA1, OSC, DCS, or APC response from
the host terminal. Sets |v:termresponse|. The
|event-data| is a table with the following fields:

View File

@@ -270,7 +270,7 @@ TREESITTER
TUI
• |TermResponse| now supports APC query responses.
• |TermResponse| now supports DA1 and APC query responses.
UI

View File

@@ -326,8 +326,8 @@ Events (autocommands):
- |TabNewEntered|
- |TermClose|
- |TermOpen|
- |TermResponse| is fired for any OSC sequence received from the terminal,
instead of the Primary Device Attributes response. |v:termresponse|
- |TermResponse| is fired for DCS, OSC, and APC sequences received from the terminal,
in addition to the Primary Device Attributes response. |v:termresponse|
- |UIEnter|
- |UILeave|

View File

@@ -19,33 +19,77 @@ vim.api.nvim_create_autocmd('UIEnter', {
end
end
-- Do not query when any of the following is true:
-- * No TUI is attached
-- * Using a badly behaved terminal
if not tty or vim.env.TERM_PROGRAM == 'Apple_Terminal' then
local termfeatures = vim.g.termfeatures or {} ---@type TermFeatures
termfeatures.osc52 = nil
vim.g.termfeatures = termfeatures
-- Do not query when no TUI is attached
if not tty then
return
end
require('vim.termcap').query('Ms', function(cap, found, seq)
if not found then
return
end
assert(cap == 'Ms')
-- If the terminal reports a sequence other than OSC 52 for the Ms capability
-- then ignore it. We only support OSC 52 (for now)
if not seq or not seq:match('^\027%]52') then
return
end
-- Clear existing OSC 52 value, since this is a new UI we might be attached to a different
-- terminal
do
local termfeatures = vim.g.termfeatures or {} ---@type TermFeatures
termfeatures.osc52 = true
termfeatures.osc52 = nil
vim.g.termfeatures = termfeatures
end)
end
-- Check DA1 first
vim.api.nvim_create_autocmd('TermResponse', {
group = id,
nested = true,
callback = function(args)
local resp = args.data.sequence ---@type string
local params = resp:match('^\027%[%?([%d;]+)c$')
if params then
-- Check termfeatures again, it may have changed between the query and response.
if vim.g.termfeatures ~= nil and vim.g.termfeatures.osc52 ~= nil then
return true
end
for param in string.gmatch(params, '%d+') do
if param == '52' then
local termfeatures = vim.g.termfeatures or {} ---@type TermFeatures
termfeatures.osc52 = true
vim.g.termfeatures = termfeatures
return true
end
end
-- Do not use XTGETTCAP on terminals that echo unknown sequences
if vim.env.TERM_PROGRAM == 'Apple_Terminal' then
return true
end
-- Fallback to XTGETTCAP
require('vim.termcap').query('Ms', function(cap, found, seq)
if not found then
return
end
-- Check termfeatures again, it may have changed between the query and response.
if vim.g.termfeatures ~= nil and vim.g.termfeatures.osc52 ~= nil then
return
end
assert(cap == 'Ms')
-- If the terminal reports a sequence other than OSC 52 for the Ms capability
-- then ignore it. We only support OSC 52 (for now)
if not seq or not seq:match('^\027%]52') then
return
end
local termfeatures = vim.g.termfeatures or {} ---@type TermFeatures
termfeatures.osc52 = true
vim.g.termfeatures = termfeatures
end)
return true
end
end,
})
-- Write DA1 request
io.stdout:write('\027[c')
end,
})

View File

@@ -543,7 +543,7 @@ void nvim_ui_pum_set_bounds(uint64_t channel_id, Float width, Float height, Floa
///
/// The following terminal events are supported:
///
/// - "termresponse": The terminal sent an OSC, DCS, or APC response sequence to
/// - "termresponse": The terminal sent a DA1, OSC, DCS, or APC response sequence to
/// Nvim. The payload is the received response. Sets
/// |v:termresponse| and fires |TermResponse|.
///

View File

@@ -577,7 +577,7 @@ static size_t handle_bracketed_paste(TermInput *input, const char *ptr, size_t s
return 0;
}
/// Handle an OSC or DCS response sequence from the terminal.
/// Handle an OSC, DCS, or APC response sequence from the terminal.
static void handle_term_response(TermInput *input, const TermKeyKey *key)
FUNC_ATTR_NONNULL_ALL
{
@@ -622,6 +622,47 @@ static void handle_term_response(TermInput *input, const TermKeyKey *key)
}
}
/// Handle a Primary Device Attributes (DA1) response from the terminal.
static void handle_primary_device_attr(TermInput *input, TermKeyCsiParam *params, size_t nparams)
FUNC_ATTR_NONNULL_ALL
{
if (input->callbacks.primary_device_attr) {
void (*cb_save)(TUIData *) = input->callbacks.primary_device_attr;
// Clear the callback before invoking it, as it may set a new callback. #34031
input->callbacks.primary_device_attr = NULL;
cb_save(input->tui_data);
}
if (nparams == 0) {
return;
}
MAXSIZE_TEMP_ARRAY(args, 2);
ADD_C(args, STATIC_CSTR_AS_OBJ("termresponse"));
StringBuilder response = KV_INITIAL_VALUE;
kv_concat(response, "\x1b[?");
for (size_t i = 0; i < nparams; i++) {
int arg;
if (termkey_interpret_csi_param(params[i], &arg, NULL, NULL) != TERMKEY_RES_KEY) {
goto out;
}
kv_printf(response, "%d", arg);
if (i < nparams - 1) {
kv_push(response, ';');
}
}
kv_push(response, 'c');
ADD_C(args, STRING_OBJ(cbuf_as_string(response.items, response.size)));
rpc_send_event(ui_client_channel_id, "nvim_ui_term_event", args);
out:
kv_destroy(response);
}
/// Handle a mode report (DECRPM) sequence from the terminal.
static void handle_modereport(TermInput *input, const TermKeyKey *key)
FUNC_ATTR_NONNULL_ALL
@@ -668,13 +709,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key)
switch (initial) {
case '?':
// Primary Device Attributes (DA1) response
if (input->callbacks.primary_device_attr) {
void (*cb_save)(TUIData *) = input->callbacks.primary_device_attr;
// Clear the callback before invoking it, as it may set a new callback. #34031
input->callbacks.primary_device_attr = NULL;
cb_save(input->tui_data);
}
handle_primary_device_attr(input, params, nparams);
break;
}
break;

View File

@@ -17,6 +17,11 @@
#define strneq(a, b, n) (strncmp(a, b, n) == 0)
// Primary Device Attributes (DA1) response.
// We make this a global (extern) variable so that we can override it with FFI
// in tests.
char vterm_primary_device_attr[] = "61;22;52";
// Some convenient wrappers to make callback functions easier
static void putglyph(VTermState *state, const schar_T schar, int width, VTermPos pos)
@@ -1385,7 +1390,7 @@ static int on_csi(const char *leader, const long args[], int argcount, const cha
val = CSI_ARG_OR(args[0], 0);
if (val == 0) {
// DEC VT100 response
vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?1;2c");
vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%sc", vterm_primary_device_attr);
}
break;

View File

@@ -3410,9 +3410,24 @@ describe('TUI', function()
end)
end)
it('queries the terminal for OSC 52 support', function()
it('queries the terminal for OSC 52 support with XTGETTCAP', function()
clear()
if not exec_lua('return pcall(require, "ffi")') then
pending('missing LuaJIT FFI')
end
-- Change vterm's DA1 response so that it doesn't include 52
exec_lua(function()
local ffi = require('ffi')
ffi.cdef [[
extern char vterm_primary_device_attr[]
]]
ffi.copy(ffi.C.vterm_primary_device_attr, '61;22')
end)
exec_lua([[
_G.query = false
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
@@ -3420,6 +3435,7 @@ describe('TUI', function()
if sequence and vim.text.hexdecode(sequence) == 'Ms' then
local resp = string.format('\027P1+r%s=%s\027\\', sequence, vim.text.hexencode('\027]52;;\027\\'))
vim.api.nvim_chan_send(vim.bo[args.buf].channel, resp)
_G.query = true
return true
end
end,
@@ -3430,7 +3446,6 @@ describe('TUI', function()
screen = tt.setup_child_nvim({
'--listen',
child_server,
-- Use --clean instead of -u NONE to load the osc52 plugin
'--clean',
}, {
env = {
@@ -3444,6 +3459,7 @@ describe('TUI', function()
retry(nil, 1000, function()
eq({ true, { osc52 = true } }, { child_session:request('nvim_eval', 'g:termfeatures') })
end)
eq(true, exec_lua([[return _G.query]]))
-- Attach another (non-TUI) UI to the child instance
local alt = Screen.new(nil, nil, nil, child_session)
@@ -3462,6 +3478,43 @@ describe('TUI', function()
eq({ true, {} }, { child_session:request('nvim_eval', 'g:termfeatures') })
end)
it('determines OSC 52 support from DA1 response', function()
clear()
exec_lua([[
-- Check that we do not emit an XTGETTCAP request when DA1 indicates support
_G.query = false
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
local sequence = req:match('^\027P%+q([%x;]+)$')
if sequence and vim.text.hexdecode(sequence) == 'Ms' then
_G.query = true
return true
end
end,
})
]])
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--listen',
child_server,
'--clean',
}, {
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
},
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, 1000, function()
eq({ true, { osc52 = true } }, { child_session:request('nvim_eval', 'g:termfeatures') })
end)
eq(false, exec_lua([[return _G.query]]))
end)
it('does not query the terminal for OSC 52 support when disabled', function()
clear()
exec_lua([[
@@ -3472,6 +3525,7 @@ describe('TUI', function()
local sequence = req:match('^\027P%+q([%x;]+)$')
if sequence and vim.text.hexdecode(sequence) == 'Ms' then
_G.query = true
return true
end
end,
})
@@ -3481,7 +3535,6 @@ describe('TUI', function()
screen = tt.setup_child_nvim({
'--listen',
child_server,
-- Use --clean instead of -u NONE to load the osc52 plugin
'--clean',
'--cmd',
'let g:termfeatures = #{osc52: v:false}',

View File

@@ -2659,7 +2659,7 @@ putglyph 1f3f4,200d,2620,fe0f 2 0,4]])
-- DA
reset(state, nil)
push('\x1b[c', vt)
expect_output('\x1b[?1;2c')
expect_output('\x1b[?61;22;52c')
-- XTVERSION
reset(state, nil)