mirror of
https://github.com/neovim/neovim.git
synced 2025-09-05 19:08:15 +00:00
feat(tui): use DA1 response to determine OSC 52 support
Many terminals now include support for OSC 52 in their Primary Device Attributes (DA1) response. This is preferable to using XTGETTCAP because DA1 is _much_ more broadly supported.
This commit is contained in:
@@ -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
|
||||
|
@@ -1049,7 +1049,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:
|
||||
|
||||
|
@@ -261,7 +261,7 @@ TREESITTER
|
||||
|
||||
TUI
|
||||
|
||||
• |TermResponse| now supports APC query responses.
|
||||
• |TermResponse| now supports DA1 and APC query responses.
|
||||
|
||||
UI
|
||||
|
||||
|
@@ -325,8 +325,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|
|
||||
|
||||
|
@@ -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,
|
||||
})
|
||||
|
||||
|
@@ -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|.
|
||||
///
|
||||
|
@@ -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;
|
||||
|
@@ -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[] = "1;2;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;
|
||||
|
||||
|
@@ -3526,9 +3526,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, '1;2')
|
||||
end)
|
||||
|
||||
exec_lua([[
|
||||
_G.query = false
|
||||
vim.api.nvim_create_autocmd('TermRequest', {
|
||||
callback = function(args)
|
||||
local req = args.data.sequence
|
||||
@@ -3536,6 +3551,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,
|
||||
@@ -3546,7 +3562,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 = {
|
||||
@@ -3560,6 +3575,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)
|
||||
@@ -3578,6 +3594,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([[
|
||||
@@ -3588,6 +3641,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,
|
||||
})
|
||||
@@ -3597,7 +3651,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}',
|
||||
|
@@ -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[?1;2;52c')
|
||||
|
||||
-- XTVERSION
|
||||
reset(state, nil)
|
||||
|
Reference in New Issue
Block a user