ojroques / vim-oscyank

A Vim plugin to copy text through SSH with OSC52
BSD 2-Clause "Simplified" License
629 stars 39 forks source link

Using OSCYank as a clipboard provider #24

Closed rcorre closed 1 year ago

rcorre commented 2 years ago

OSCYank is great, but only works if I'm manually yanking text. I really wanted it to work with anything that copies to the clipboard, like fugitive's GBrowse! command. Neovim (not vim, AFAIK) allows users to implement a custom clipboard provider, which I managed to integrate with OSCYank like so:

let g:clipboard = {
        \   'name': 'osc52',
        \   'copy': {'+': {lines, regtype -> OSCYankString(join(lines, "\n"))}},
        \   'paste': {'+': {-> [split(getreg(''), '\n'), getregtype('')]}},
        \ }

Now regular yanks (using clipboard=unnamedplus) as well as commands like GBrowse! will put text on the local clipboard.

Would this make sense to include in the README? I only discovered g:clipboard today, so I have no idea if it might cause unexpected problems, but so far I've found it useful and wanted to share :)

AprilArcus commented 2 years ago

I love this! Thank you for the tip.

kabouzeid commented 2 years ago

Thanks! The following is even better because there are no errors on :registers:

let g:clipboard = {
        \   'name': 'osc52',
        \   'copy': {
        \     '+': {lines, regtype -> OSCYankString(join(lines, "\n"))},
        \     '*': {lines, regtype -> OSCYankString(join(lines, "\n"))},
        \   },
        \   'paste': {
        \     '+': {-> [split(getreg(''), '\n'), getregtype('')]},
        \     '*': {-> [split(getreg(''), '\n'), getregtype('')]},
        \   },
        \ }
kabouzeid commented 2 years ago

lua version

local function copy(lines, _)
  vim.fn.OSCYankString(table.concat(lines, "\n"))
end

local function paste()
  return {
    vim.fn.split(vim.fn.getreg(''), '\n'),
    vim.fn.getregtype('')
  }
end

vim.g.clipboard = {
  name = "osc52",
  copy = {
    ["+"] = copy,
    ["*"] = copy
  },
  paste = {
    ["+"] = paste,
    ["*"] = paste
  }
}
nathanaelcunningham commented 2 years ago

this doesnt seem to work with 'yy' to yank a line, any way to fix that?

rcorre commented 2 years ago

@nathanaelcunningham y uses the "unnamed" register unless you explicitly specify a register like + or *. If you want y to always use a clipboard register (and consequently work with the clipboard provider described above), try set clipboard=unnamed or set clipboard=unnamedplus. See https://neovim.io/doc/user/provider.html.

To ALWAYS use the clipboard for ALL operations (instead of interacting with the '+' and/or '*' registers explicitly): set clipboard+=unnamedplus

avently commented 2 years ago

Thank you for the solutions. I tried one but result still not perfect: paste is not working. So I added this:

local function copy(lines, _) vim.fn.OSCYankString(table.concat(lines, "\n")) end
local function paste() return { vim.fn.split(vim.fn.getreg(''), '\n'), vim.fn.getregtype('') } end
vim.g.clipboard = {  name = "osc52", copy = { ["+"] = copy, ["*"] = copy }, paste = { ["+"] = paste, ["*"] = paste }}
vim.cmd [[
set clipboard+=unnamedplus
]]

Result:

Pasting doesn't work everywhere (there is a way to paste using Ctrl+Shift+v by using terminal but it's not a good way to do that):

E353: Nothing in register "
E353: Nothing in register +

So did anyone found a way to fix these problems? Don't know what can be done in the situation.

avently commented 2 years ago

I found two ways to fix almost all problems I mentioned. However it's unrelated to this issue about clipboard provider but I hope you can adapt it for your usecases or even to clipboard provider. First of all, if you want to use this plugin, there is one solution but it has drawbacks: it copies successfully in 1-5 of 10 tries in tmux. Have no idea why. It tries to copy but no actual clipboard filling happens in some tries. Without tmux it's working fine (except the limitation of 100000 characters). Example of config. I use '+' here with mswin because I like mswin behaviour and it copies to '+' register:

autocmd TextYankPost * if v:event.operator is 'y' && v:event.regname is '+' | execute 'OSCYankReg +' | endif
source $VIMRUNTIME/mswin.vim

But I don't recommend this way because of tmux issues.

I found the Greatest Ever solution from Reddit user leeren: https://www.reddit.com/r/vim/comments/ac9eyh/comment/ed6kl67/?utm_source=share&utm_medium=web2x&context=3

Original code was good but the problem is that it has a limit of characters you're able to copy. Because bash command was so long (100000 characters long) which zsh, for example, refuses to answer too. So I created another solution using Lua:

function WriteToClipboard(text)
  local is_tmux = os.getenv("TMUX")
  local ssh_connection = os.getenv("SSH_CONNECTION")
  if not is_tmux and not ssh_connection then
    -- Not in a SSH session, nothing to do, copy will work anyway
    return
  end

  local filename = "/tmp/vim_clipboard.tmp"
  local file = io.open(filename, 'w')
  file:write(text)
  file:close()
  if is_tmux then
    -- Copy to each client because there is no way in tmux to differentiate only current client
    os.execute("for pts in $(tmux list-clients -F '#{client_tty}'); do echo -ne $(cat < " .. filename .. 
      " | base64 -w0 | sed 's/\\(.*\\)/\\\\e]52;c;\\0\\\\x07/') > $pts; done")
  else
    os.execute("echo -ne $(cat < " .. filename .. " | base64 -w0 | sed 's/\\(.*\\)/\\\\e]52;c;\\0\\\\x07/') > " .. ssh_connection)
  end
  os.remove(filename)
end

Again I use '+' register, you can omit it in if statement if you want or if you just don't use mswin.

Normally mswin setups <C-c> keybinding for copying into '+' register for us but it won't happen if we connect via SSH without tmux since in https://github.com/vim/vim/blob/master/runtime/mswin.vim#L26 vim checks clipboard support and don't add keybindings if there is no clipboard (in tmux there is a clipboard available, so that's why it works there). So we need to setup keybindings ourselves if we want to have clipboard working:

" CTRL-X and SHIFT-Del are Cut
vnoremap <C-X> "+x
vnoremap <S-Del> "+x
" CTRL-C and CTRL-Insert are Copy
vnoremap <C-C> "+y
vnoremap <C-Insert> "+y
" CTRL-V and SHIFT-Insert are Paste
map <C-V>       "+gP
map <S-Insert>      "+gP
cmap <C-V>      <C-R>+
cmap <S-Insert>     <C-R>+

And the final result is:

function WriteToClipboard(text)
  local is_tmux = os.getenv("TMUX")
  local ssh_connection = os.getenv("SSH_CONNECTION")
  if not is_tmux and not ssh_connection then
    -- Not in a SSH session, nothing to do, copy will work anyway
    return
  end

  local filename = "/tmp/vim_clipboard.tmp"
  local file = io.open(filename, 'w')
  file:write(text)
  file:close()
  if is_tmux then
    -- Copy to each client because there is no way in tmux to differentiate only current client
    os.execute("for pts in $(tmux list-clients -F '#{client_tty}'); do echo -ne $(cat < " .. filename .. 
      " | base64 -w0 | sed 's/\\(.*\\)/\\\\e]52;c;\\0\\\\x07/') > $pts; done")
  else
    os.execute("echo -ne $(cat < " .. filename .. " | base64 -w0 | sed 's/\\(.*\\)/\\\\e]52;c;\\0\\\\x07/') > " .. ssh_connection)
  end
  os.remove(filename)
end
vim.cmd [[
au TextYankPost * if v:event.operator is 'y' && v:event.regname is '+' | call v:lua.WriteToClipboard(join(v:event.regcontents, "\n")) | endif

" CTRL-X and SHIFT-Del are Cut
vnoremap <C-X> "+x
vnoremap <S-Del> "+x
" CTRL-C and CTRL-Insert are Copy
vnoremap <C-C> "+y
vnoremap <C-Insert> "+y
" CTRL-V and SHIFT-Insert are Paste
map <C-V>       "+gP
map <S-Insert>      "+gP
cmap <C-V>      <C-R>+
cmap <S-Insert>     <C-R>+

source $VIMRUNTIME/mswin.vim
]]

The final result works with:

P.S. Actually Linux can process 10.000.000 characters and much more. Even via SSH. /dev/pts/0 is a strong thing!

echo -ne "\e]52;c;$(< /dev/urandom tr -dc "[:alnum:]" | head -c1000000 | base64 -w0)\x07" > /dev/pts/0 && xclip -selection clipboard -out | wc -c

-> 10000000

GabeDuarteM commented 2 years ago

I wanted to jump in here as well since I had this issue a while ago, and found a solution which might not be perfect, but works for all cases I typically go through: I develop on an SSH server, so I was looking for something that when:

To solve this, I have the following Lua file in my config:

local isInSsh = not (vim.env.SSH_CLIENT == nil) -- I share this config with my local machine, so I need this to ignore this provider if not on ssh

if isInSsh then
  local clipboard = {}

  local function refreshClipboard()
    vim.fn.jobstart(
    -- I have a reverse ssh tunnel to my local machine called "local", which allows me to get stuff from my macbook's pbpaste
      "ssh local pbpaste",
      {
        stdout_buffered = true,
        on_stdout = function(_, data) clipboard = data end
      }
    )
  end

  refreshClipboard() -- Initialize the clipboard when the file is loaded

  local function copy(lines, regtype)
    vim.fn.OSCYankString(table.concat(lines, "\n"))
    clipboard = { lines, regtype }
  end

  -- doesn't necessarily paste the current clipboard content (if, for example, I copy something outside vim)
  -- but it refreshes when called, so if you try again it will paste the current clipboard content
  -- I do this as a compromise to avoid delays when pasting
  local function paste()
    refreshClipboard()
    return clipboard
  end

  vim.api.nvim_create_user_command('RefreshClipboard', refreshClipboard, {})

  vim.g.clipboard = {
    name = "osc52",
    copy = {
      ["+"] = copy,
      ["*"] = copy
    },
    paste = {
      ["+"] = paste,
      ["*"] = paste
    }
  }
end

Then I have on my ssh machine's ~/.ssh/config:

Host local
  HostName localhost
  Port 2222
  User <host user>

and then on my local machine's ~/.ssh/config:

Host dev
  HostName <IP for the SSH machine>
  User <user>
  RemoteForward 127.0.0.1:2222 127.0.0.1:22 # this allows me to access the local machine from the remote machine
  ServerAliveInterval 240

This allows me to keep the clipboard in sync in almost all cases, the only quirk is that if I have vim already open and copy something outside vim (on a webpage, for example), when you paste it inside vim, it will paste the old contents of the clipboard, and sync it behind the scene, so I have to undo and paste again. I do this as a compromise, to avoid adding any delay when pasting since I'd have to wait for the ssh to connect to my local machine to get the content. In general, this doesn't happen that often with me, so I'm okay with it :)

Hope this helps someone else as well!

avently commented 2 years ago

Updated the solution for clipboard copying. Now it has no characters limit! Can be used by anyone with tmux/without, locally/over ssh. https://github.com/ojroques/vim-oscyank/issues/24#issuecomment-1175539644

qtdzz commented 2 years ago

Thanks @avently. I have been using your awesome solution for a while in my linux machine and it works very well. Though I ran into some problems when I started using it in my macOS (intel) machine (local Alacritty + zsh + tmux) yesterday because of two issues:

Here is my current updated script which works in macOS (local Alacritty + zsh + tmux) and Linux (ssh Alacritty + zsh + tmux). I've not tested in other combinations:

function WriteToClipboard(text)
    local is_tmux = os.getenv("TMUX")

    local ssh_connection = os.getenv("SSH_CONNECTION")
    if not is_tmux and not ssh_connection then
        -- Not in a SSH session, nothing to do, copy will work anyway
        return
    end

    -- https://stackoverflow.com/questions/46463027/base64-doesnt-have-w-option-in-mac
    local is_macos = vim.fn.has("macunix")
    local base64_opts = "-w0"
    if is_macos then
        base64_opts = ""
    end

    local filename = "/tmp/vim_clipboard.tmp"
    local file = io.open(filename, "w")
    file:write(text)
    file:close()
    if is_tmux then
        -- Copy to each client because there is no way in tmux to differentiate only current client
        os.execute(
            "for pts in $(tmux list-clients -F '#{client_tty}'); do printf $(cat < "
                .. filename
                .. " | base64 "
                .. base64_opts
                .. " | sed 's/\\(.*\\)/\\\\e]52;c;\\0\\\\x07/') > $pts; done"
        )
    else
        os.execute(
            "echo -ne $(cat < "
                .. filename
                .. " | base64 -w0 | sed 's/\\(.*\\)/\\\\e]52;c;\\0\\\\x07/') > "
                .. ssh_connection
        )
    end
    os.remove(filename)
end
vim.cmd [[
au TextYankPost * if v:event.operator is 'y' && v:event.regname is '+' | call v:lua.WriteToClipboard(join(v:event.regcontents, "\n")) | endif

" CTRL-X and SHIFT-Del are Cut
vnoremap <C-X> "+x
vnoremap <S-Del> "+x
" CTRL-C and CTRL-Insert are Copy
vnoremap <C-C> "+y
vnoremap <C-Insert> "+y
" CTRL-V and SHIFT-Insert are Paste
map <C-V>       "+gP
map <S-Insert>      "+gP
cmap <C-V>      <C-R>+
cmap <S-Insert>     <C-R>+

source $VIMRUNTIME/mswin.vim
]]
evan-goode commented 1 year ago

If you get the following error using @kabouzeid's config:

Error detected while processing function provider#clipboard#Call[9]..function provider#clipboard#Call[6]..11[10]..<lambda>1:                                                                               
line    1:
E117: Unknown function: OSCYankString

Try the following instead:

let g:clipboard = {
        \   'name': 'osc52',
        \   'copy': {
        \     '+': {lines, regtype -> OSCYank(join(lines, "\n"))},
        \     '*': {lines, regtype -> OSCYank(join(lines, "\n"))},
        \   },
        \   'paste': {
        \     '+': {-> [split(getreg(''), '\n'), getregtype('')]},
        \     '*': {-> [split(getreg(''), '\n'), getregtype('')]},
        \   },
        \ }

It seems OSCYankString has been renamed to OSCYank.

I have not found an elegant way to get paste working, but yanking from a remote vim now works seamlessly.