mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
feat(terminal): add support for copying with OSC 52 in embedded terminal (#29117)
When libvterm receives the OSC 52 escape sequence it ignores it because Nvim does not set any selection callbacks. Install selection callbacks that forward to the clipboard provider, so that setting the clipboard with OSC 52 in the embedded terminal writes to the system clipboard using the configured clipboard provider.
This commit is contained in:
parent
39d8651283
commit
3ad977f01d
@ -138,7 +138,8 @@ STARTUP
|
||||
|
||||
TERMINAL
|
||||
|
||||
• TODO
|
||||
• The |terminal| now understands the OSC 52 escape sequence to write to the
|
||||
system clipboard (copy). Querying with OSC 52 (paste) is not supported.
|
||||
|
||||
TREESITTER
|
||||
|
||||
|
@ -164,7 +164,22 @@ directory indicated in the request. >lua
|
||||
To try it out, select the above code and source it with `:'<,'>lua`, then run
|
||||
this command in a :terminal buffer: >
|
||||
|
||||
printf "\033]7;file://./foo/bar\033\\"
|
||||
printf "\033]7;file://./foo/bar\033\\"
|
||||
|
||||
OSC 52: write to system clipboard *terminal-osc52*
|
||||
|
||||
Applications in the :terminal buffer can write to the system clipboard by
|
||||
emitting an OSC 52 sequence. Example: >
|
||||
|
||||
printf '\033]52;;%s\033\\' "$(echo -n 'Hello world' | base64)"
|
||||
|
||||
Nvim uses the configured |clipboard| provider to write to the system
|
||||
clipboard. Reading from the system clipboard with OSC 52 is not supported, as
|
||||
this would allow any arbitrary program in the :terminal to read the user's
|
||||
clipboard.
|
||||
|
||||
OSC 52 sequences sent from the :terminal buffer do not emit a |TermRequest|
|
||||
event. The event is handled directly by Nvim and is not forwarded to plugins.
|
||||
|
||||
==============================================================================
|
||||
Status Variables *terminal-status*
|
||||
|
@ -142,6 +142,7 @@ char *base64_encode(const char *src, size_t src_len)
|
||||
/// @param [out] out_lenp Returns the length of the decoded string
|
||||
/// @return Decoded string
|
||||
char *base64_decode(const char *src, size_t src_len, size_t *out_lenp)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
assert(src != NULL);
|
||||
assert(out_lenp != NULL);
|
||||
|
@ -112,6 +112,9 @@ typedef struct {
|
||||
// libvterm. Improves performance when receiving large bursts of data.
|
||||
#define REFRESH_DELAY 10
|
||||
|
||||
#define TEXTBUF_SIZE 0x1fff
|
||||
#define SELECTIONBUF_SIZE 0x0400
|
||||
|
||||
static TimeWatcher refresh_timer;
|
||||
static bool refresh_pending = false;
|
||||
|
||||
@ -127,7 +130,7 @@ struct terminal {
|
||||
// buffer used to:
|
||||
// - convert VTermScreen cell arrays into utf8 strings
|
||||
// - receive data from libvterm as a result of key presses.
|
||||
char textbuf[0x1fff];
|
||||
char textbuf[TEXTBUF_SIZE];
|
||||
|
||||
ScrollbackLine **sb_buffer; // Scrollback storage.
|
||||
size_t sb_current; // Lines stored in sb_buffer.
|
||||
@ -166,6 +169,9 @@ struct terminal {
|
||||
// When there is a pending TermRequest autocommand, block and store input.
|
||||
StringBuilder *pending_send;
|
||||
|
||||
char *selection_buffer; /// libvterm selection buffer
|
||||
StringBuilder selection; /// Growable array containing full selection data
|
||||
|
||||
size_t refcount; // reference count
|
||||
};
|
||||
|
||||
@ -179,6 +185,12 @@ static VTermScreenCallbacks vterm_screen_callbacks = {
|
||||
.sb_popline = term_sb_pop,
|
||||
};
|
||||
|
||||
static VTermSelectionCallbacks vterm_selection_callbacks = {
|
||||
.set = term_selection_set,
|
||||
// For security reasons we don't support querying the system clipboard from the embedded terminal
|
||||
.query = NULL,
|
||||
};
|
||||
|
||||
static Set(ptr_t) invalidated_terminals = SET_INIT;
|
||||
|
||||
static void emit_termrequest(void **argv)
|
||||
@ -315,6 +327,11 @@ void terminal_open(Terminal **termpp, buf_T *buf, TerminalOptions opts)
|
||||
vterm_screen_set_damage_merge(term->vts, VTERM_DAMAGE_SCROLL);
|
||||
vterm_screen_reset(term->vts, 1);
|
||||
vterm_output_set_callback(term->vt, term_output_callback, term);
|
||||
|
||||
term->selection_buffer = xcalloc(SELECTIONBUF_SIZE, 1);
|
||||
vterm_state_set_selection_callbacks(state, &vterm_selection_callbacks, term,
|
||||
term->selection_buffer, SELECTIONBUF_SIZE);
|
||||
|
||||
// force a initial refresh of the screen to ensure the buffer will always
|
||||
// have as many lines as screen rows when refresh_scrollback is called
|
||||
term->invalid_start = 0;
|
||||
@ -769,6 +786,8 @@ void terminal_destroy(Terminal **termpp)
|
||||
}
|
||||
xfree(term->sb_buffer);
|
||||
xfree(term->title);
|
||||
xfree(term->selection_buffer);
|
||||
kv_destroy(term->selection);
|
||||
vterm_free(term->vt);
|
||||
xfree(term);
|
||||
*termpp = NULL; // coverity[dead-store]
|
||||
@ -1198,6 +1217,54 @@ static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void term_clipboard_set(void **argv)
|
||||
{
|
||||
VTermSelectionMask mask = (VTermSelectionMask)(long)argv[0];
|
||||
char *data = argv[1];
|
||||
|
||||
char regname;
|
||||
switch (mask) {
|
||||
case VTERM_SELECTION_CLIPBOARD:
|
||||
regname = '+';
|
||||
break;
|
||||
case VTERM_SELECTION_PRIMARY:
|
||||
regname = '*';
|
||||
break;
|
||||
default:
|
||||
regname = '+';
|
||||
break;
|
||||
}
|
||||
|
||||
list_T *lines = tv_list_alloc(1);
|
||||
tv_list_append_allocated_string(lines, data);
|
||||
|
||||
list_T *args = tv_list_alloc(3);
|
||||
tv_list_append_list(args, lines);
|
||||
|
||||
const char regtype = 'v';
|
||||
tv_list_append_string(args, ®type, 1);
|
||||
|
||||
tv_list_append_string(args, ®name, 1);
|
||||
eval_call_provider("clipboard", "set", args, true);
|
||||
}
|
||||
|
||||
static int term_selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user)
|
||||
{
|
||||
Terminal *term = user;
|
||||
if (frag.initial) {
|
||||
kv_size(term->selection) = 0;
|
||||
}
|
||||
|
||||
kv_concat_len(term->selection, frag.str, frag.len);
|
||||
|
||||
if (frag.final) {
|
||||
char *data = xmemdupz(term->selection.items, kv_size(term->selection));
|
||||
multiqueue_put(main_loop.events, term_clipboard_set, (void *)mask, data);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// }}}
|
||||
// input handling {{{
|
||||
|
||||
|
65
test/functional/terminal/clipboard_spec.lua
Normal file
65
test/functional/terminal/clipboard_spec.lua
Normal file
@ -0,0 +1,65 @@
|
||||
local t = require('test.testutil')
|
||||
local n = require('test.functional.testnvim')()
|
||||
|
||||
local eq = t.eq
|
||||
local retry = t.retry
|
||||
|
||||
local clear = n.clear
|
||||
local fn = n.fn
|
||||
local testprg = n.testprg
|
||||
local exec_lua = n.exec_lua
|
||||
local eval = n.eval
|
||||
|
||||
describe(':terminal', function()
|
||||
before_each(function()
|
||||
clear()
|
||||
|
||||
exec_lua([[
|
||||
local function clipboard(reg, type)
|
||||
if type == 'copy' then
|
||||
return function(lines)
|
||||
local data = table.concat(lines, '\n')
|
||||
vim.g.clipboard_data = data
|
||||
end
|
||||
end
|
||||
|
||||
if type == 'paste' then
|
||||
return function()
|
||||
error()
|
||||
end
|
||||
end
|
||||
|
||||
error('invalid type: ' .. type)
|
||||
end
|
||||
|
||||
vim.g.clipboard = {
|
||||
name = 'Test',
|
||||
copy = {
|
||||
['+'] = clipboard('+', 'copy'),
|
||||
['*'] = clipboard('*', 'copy'),
|
||||
},
|
||||
paste = {
|
||||
['+'] = clipboard('+', 'paste'),
|
||||
['*'] = clipboard('*', 'paste'),
|
||||
},
|
||||
}
|
||||
]])
|
||||
end)
|
||||
|
||||
it('can write to the system clipboard', function()
|
||||
eq('Test', eval('g:clipboard.name'))
|
||||
|
||||
local text = 'Hello, world! This is some\nexample text\nthat spans multiple\nlines'
|
||||
local encoded = exec_lua('return vim.base64.encode(...)', text)
|
||||
|
||||
local function osc52(arg)
|
||||
return string.format('\027]52;;%s\027\\', arg)
|
||||
end
|
||||
|
||||
fn.termopen({ testprg('shell-test'), '-t', osc52(encoded) })
|
||||
|
||||
retry(nil, 1000, function()
|
||||
eq(text, exec_lua([[ return vim.g.clipboard_data ]]))
|
||||
end)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue
Block a user