mirror of
https://github.com/neovim/neovim.git
synced 2025-09-05 19:08:15 +00:00
Merge pull request #34860 from gpanders/push-lorwmnmtysnt
feat(tui): use DA1 response to determine OSC 52 support
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
|
||||
|
@@ -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:
|
||||
|
||||
|
@@ -270,7 +270,7 @@ TREESITTER
|
||||
|
||||
TUI
|
||||
|
||||
• |TermResponse| now supports APC query responses.
|
||||
• |TermResponse| now supports DA1 and APC query responses.
|
||||
|
||||
UI
|
||||
|
||||
|
@@ -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|
|
||||
|
||||
|
@@ -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[] = "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;
|
||||
|
||||
|
@@ -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}',
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user