├── lua ├── osc52 │ └── base64.lua └── osc52.lua ├── LICENSE └── README.md /lua/osc52/base64.lua: -------------------------------------------------------------------------------- 1 | local lshift = require('bit').lshift 2 | local rshift = require('bit').rshift 3 | local band = require('bit').band 4 | local bor = require('bit').bor 5 | 6 | local M = {} 7 | 8 | local base64 = { 9 | [0] = 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 10 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 11 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 12 | 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 13 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 14 | } 15 | local mask = 0x3f -- 0b00111111 16 | 17 | function M.enc(s) 18 | local len = string.len(s) 19 | local output = {} 20 | 21 | for i = 1, len, 3 do 22 | local byte1, byte2, byte3 = string.byte(s, i, i + 2) 23 | local bits = bor(lshift(byte1, 16), lshift(byte2 or 0, 8), byte3 or 0) 24 | table.insert(output, base64[rshift(bits, 18)]) 25 | table.insert(output, base64[band(rshift(bits, 12), mask)]) 26 | table.insert(output, base64[band(rshift(bits, 6), mask)]) 27 | table.insert(output, base64[band(bits, mask)]) 28 | end 29 | 30 | for i = 0, 1 - ((len - 1) % 3) do 31 | output[#output - i] = '=' 32 | end 33 | 34 | return table.concat(output) 35 | end 36 | 37 | return M 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Olivier Roques 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /lua/osc52.lua: -------------------------------------------------------------------------------- 1 | -------------------- VARIABLES ----------------------------- 2 | local base64 = require('osc52.base64') 3 | local fmt = string.format 4 | local commands = { 5 | operator = {block = "`[\\`]y", char = "`[v`]y", line = "'[V']y"}, 6 | visual = {[''] = 'y', ['V'] = 'y', ['v'] = 'y', [''] = 'y'}, 7 | } 8 | local options = { 9 | max_length = 0, -- Maximum length of selection (0 for no limit) 10 | silent = false, -- Disable message on successful copy 11 | trim = false, -- Trim surrounding whitespaces before copy 12 | tmux_passthrough = false, -- Use tmux passthrough (requires tmux: set -g allow-passthrough on) 13 | osc52 = '\27]52;c;%s\7', 14 | } 15 | local M = {} 16 | 17 | -------------------- UTILS --------------------------------- 18 | local function echo(text, hl_group) 19 | vim.api.nvim_echo({{fmt('[osc52] %s', text), hl_group or 'Normal'}}, false, {}) 20 | end 21 | 22 | local function get_text(mode, type) 23 | -- Save user settings 24 | local clipboard = vim.go.clipboard 25 | local register = vim.fn.getreginfo('"') 26 | local visual_marks 27 | 28 | -- Save previous visual marks in operator mode 29 | if mode == 'operator' then 30 | visual_marks = {vim.fn.getpos("'<"), vim.fn.getpos("'>")} 31 | end 32 | 33 | -- Retrieve text 34 | vim.go.clipboard = '' 35 | local command = fmt('keepjumps normal! %s', commands[mode][type]) 36 | vim.cmd(fmt('silent execute "%s"', command)) 37 | local text = vim.fn.getreg('"') 38 | 39 | -- Restore user settings 40 | vim.go.clipboard = clipboard 41 | vim.fn.setreg('"', register) 42 | 43 | -- Restore previous visual marks in operator mode 44 | if mode == 'operator' then 45 | vim.fn.setpos("'<", visual_marks[1]) 46 | vim.fn.setpos("'>", visual_marks[2]) 47 | end 48 | 49 | return text or '' 50 | end 51 | 52 | local function trim_text(text) 53 | local i, j = string.find(text, '^%s+') 54 | 55 | -- Remove common indent from all lines 56 | if i then 57 | local indent = string.rep('%s', j - i + 1) 58 | text = string.gsub(text, fmt('\n%s', indent), '\n') 59 | end 60 | 61 | return vim.trim(text) 62 | end 63 | 64 | local function write(osc52) 65 | local success = false 66 | 67 | if vim.fn.filewritable('/dev/fd/2') == 1 then 68 | success = vim.fn.writefile({osc52}, '/dev/fd/2', 'b') == 0 69 | else 70 | success = vim.fn.chansend(vim.v.stderr, osc52) > 0 71 | end 72 | 73 | return success 74 | end 75 | 76 | -------------------- PUBLIC -------------------------------- 77 | function M.copy(text) 78 | text = options.trim and trim_text(text) or text 79 | 80 | if options.max_length > 0 and #text > options.max_length then 81 | echo(fmt('Selection is too big: length is %d, limit is %d', #text, options.max_length), 'WarningMsg') 82 | return 83 | end 84 | 85 | local text_b64 = base64.enc(text) 86 | local osc52 = fmt(options.osc52, text_b64) 87 | local msg = '%d characters copied' 88 | if options.tmux_passthrough and (os.getenv("TERM"):match("^tmux") or os.getenv("TERM"):match("^screen")) then 89 | osc52 = fmt('\27Ptmux;\27%s\27\\', osc52) 90 | msg = msg .. ' (tmux passthrough)' 91 | end 92 | local success = write(osc52) 93 | 94 | if not success then 95 | echo('Failed to copy selection', 'ErrorMsg') 96 | elseif not options.silent then 97 | echo(fmt(msg, #text)) 98 | end 99 | 100 | return success 101 | end 102 | 103 | function M.paste() 104 | local osc52 = fmt(options.osc52, '?') 105 | local success = write(osc52) 106 | 107 | if not success then 108 | echo('Failed to paste', 'ErrorMsg') 109 | end 110 | 111 | return success 112 | end 113 | 114 | function M.copy_operator_cb(type) 115 | local text = get_text('operator', type) 116 | return M.copy(text) 117 | end 118 | 119 | function M.copy_operator() 120 | vim.go.operatorfunc = "v:lua.require'osc52'.copy_operator_cb" 121 | return 'g@' 122 | end 123 | 124 | function M.copy_visual() 125 | local text = get_text('visual', vim.fn.visualmode()) 126 | return M.copy(text) 127 | end 128 | 129 | function M.copy_register(register) 130 | local text = vim.fn.getreg(register) 131 | return M.copy(text) 132 | end 133 | 134 | -------------------- SETUP --------------------------------- 135 | function M.setup(user_options) 136 | if user_options then 137 | options = vim.tbl_extend('force', options, user_options) 138 | end 139 | end 140 | 141 | ------------------------------------------------------------ 142 | return M 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-osc52 2 | 3 | **Note**: As of Neovim 10.0 (specifically since [this 4 | PR](https://github.com/neovim/neovim/pull/25872)), native support for OSC52 has 5 | been added and therefore this plugin is now obsolete. Check `:h clipboard-osc52` 6 | for more details. 7 | 8 | A Neovim plugin to copy text to the system clipboard using the ANSI OSC52 9 | sequence. 10 | 11 | The plugin wraps a piece of text inside an OSC52 sequence and writes it to 12 | Neovim's stderr. When your terminal detects the OSC52 sequence, it will copy the 13 | text into the system clipboard. 14 | 15 | This is totally location-independent, you can copy text from anywhere including 16 | from remote SSH sessions. The only requirement is that your terminal must 17 | support OSC52 which is the case for most modern terminal emulators. 18 | 19 | nvim-osc52 is a rewrite of 20 | [vim-oscyank](https://github.com/ojroques/vim-oscyank) in Lua. 21 | 22 | ## Installation 23 | With [packer.nvim](https://github.com/wbthomason/packer.nvim) for instance: 24 | ```lua 25 | use {'ojroques/nvim-osc52'} 26 | ``` 27 | 28 | ### Configuration for tmux 29 | 30 | If you are using tmux, run these steps first: [enabling OSC52 in 31 | tmux](https://github.com/tmux/tmux/wiki/Clipboard#quick-summary). 32 | 33 | Then, you can use the tmux option `set-clipboard on` or `allow-passthrough on`. 34 | 35 | For tmux versions before 3.3a, you will need to use the `set-clipboard` option: 36 | `set -s set-clipboard on` 37 | 38 | For tmux versions starting with 3.3a, you can configure tmux to allow passthrough 39 | of escape sequences (`set -g allow-passthrough on`). With this option you can leave 40 | `set-clipboard` to its default (`external`). 41 | The allow-passthrough option works well for nested tmux sessions or when running 42 | tmux on both the local and remote servers. When using allow-passthrough, be sure 43 | to enable `tmux_passthrough` for this plugin. 44 | 45 | ## Usage 46 | Add this to your config (assuming Neovim 0.7+): 47 | ```lua 48 | vim.keymap.set('n', 'c', require('osc52').copy_operator, {expr = true}) 49 | vim.keymap.set('n', 'cc', 'c_', {remap = true}) 50 | vim.keymap.set('v', 'c', require('osc52').copy_visual) 51 | ``` 52 | 53 | Using these mappings: 54 | * In normal mode, \c is an operator that will copy the given 55 | text to the clipboard. 56 | * In normal mode, \cc will copy the current line. 57 | * In visual mode, \c will copy the current selection. 58 | 59 | ## Configuration 60 | The available options with their default values are: 61 | ```lua 62 | require('osc52').setup { 63 | max_length = 0, -- Maximum length of selection (0 for no limit) 64 | silent = false, -- Disable message on successful copy 65 | trim = false, -- Trim surrounding whitespaces before copy 66 | tmux_passthrough = false, -- Use tmux passthrough (requires tmux: set -g allow-passthrough on) 67 | } 68 | ``` 69 | 70 | ## Advanced usage 71 | The following methods are also available: 72 | * `require('osc52').copy(text)`: copy text `text` 73 | * `require('osc52').copy_register(register)`: copy text from register `register` 74 | 75 | For instance, to automatically copy text that was yanked into register `+`: 76 | ```lua 77 | function copy() 78 | if vim.v.event.operator == 'y' and vim.v.event.regname == '+' then 79 | require('osc52').copy_register('+') 80 | end 81 | end 82 | 83 | vim.api.nvim_create_autocmd('TextYankPost', {callback = copy}) 84 | ``` 85 | 86 | ## Using nvim-osc52 as clipboard provider 87 | You can use the plugin as your clipboard provider, see `:h provider-clipboard` 88 | for more details. Simply add these lines to your config: 89 | ```lua 90 | local function copy(lines, _) 91 | require('osc52').copy(table.concat(lines, '\n')) 92 | end 93 | 94 | local function paste() 95 | return {vim.fn.split(vim.fn.getreg(''), '\n'), vim.fn.getregtype('')} 96 | end 97 | 98 | vim.g.clipboard = { 99 | name = 'osc52', 100 | copy = {['+'] = copy, ['*'] = copy}, 101 | paste = {['+'] = paste, ['*'] = paste}, 102 | } 103 | 104 | -- Now the '+' register will copy to system clipboard using OSC52 105 | vim.keymap.set('n', 'c', '"+y') 106 | vim.keymap.set('n', 'cc', '"+yy') 107 | ``` 108 | 109 | Note that if you set your clipboard provider like the example above, copying 110 | text from outside Neovim and pasting with p won't work. But you can 111 | still use the paste shortcut of your terminal emulator (usually 112 | ctrl+shift+v). 113 | --------------------------------------------------------------------------------