aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/doc/nvim_terminal_emulator.txt17
-rw-r--r--src/nvim/base64.c1
-rw-r--r--src/nvim/terminal.c69
-rw-r--r--test/functional/terminal/clipboard_spec.lua65
5 files changed, 152 insertions, 3 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 2ff6b0302c..455b38b5fa 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -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
diff --git a/runtime/doc/nvim_terminal_emulator.txt b/runtime/doc/nvim_terminal_emulator.txt
index a6ebc7e958..81bcd89146 100644
--- a/runtime/doc/nvim_terminal_emulator.txt
+++ b/runtime/doc/nvim_terminal_emulator.txt
@@ -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*
diff --git a/src/nvim/base64.c b/src/nvim/base64.c
index 39e4ec4872..99d3c5a33e 100644
--- a/src/nvim/base64.c
+++ b/src/nvim/base64.c
@@ -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);
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
index 027ff79696..818f8abbb5 100644
--- a/src/nvim/terminal.c
+++ b/src/nvim/terminal.c
@@ -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, &regtype, 1);
+
+ tv_list_append_string(args, &regname, 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 {{{
diff --git a/test/functional/terminal/clipboard_spec.lua b/test/functional/terminal/clipboard_spec.lua
new file mode 100644
index 0000000000..4a1a0e29fd
--- /dev/null
+++ b/test/functional/terminal/clipboard_spec.lua
@@ -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)