Seamless copy-paste between tmux, vim and clipboard over ssh

26 Mar 2023 • 5 min read
Tags:   tools vim

TL;DR

I spend a majority of my development in terminal: from my Mac which acts as a thin development client, I stay over ssh into tmux on my main Linux workstation and numerous other boxes, with my main IDE a.k.a. neovim running remotely. Sometimes, from that tmux I connect over another ssh and fire tmux on a box over two hops from my cosy Mac.

This worked smoothly with one common caveat: I need to be able, from time to time, to select-and-copy into my local clipboard (either to copy into some GUI for demos, or to copy into an ssh-over-ssh connected VM) – keeping my local clipboard an ulimate source of copy-paste storage.

This short post was born as a consequence of “shell user’s block” – reading numerous posts on the topic which make things look quite pessimistic in terms of the complexity, thus dicouraging me to give it a try. It all turned out to be quite trivial. So here, I aggregate a working set of steps – and keep all the credits with the developers of the plugins mentioned in the course of the story.

Example of copying from local tmux pane into vim on remote, and then vice versa from remote vim into local tmux + vim.

Outline

(UPD 2023-12)

As of neovim 10.0, per the claim of the plugin creators, the below plugins are not needed anymore. Though, quick skimming over the web seems to report that the 10.0 is still sluggish/unstable.

Another note is that the usage of the vim-OSCYank, (not nvim-osc52) seemed to produce issues on MacOS for me, where yanking whole line (yy) with subsequent pasting it resulted in pasting it into the cursor’s line, not on the new line. Swiching to nvim-osc52 fixed the problem, below I post the Lua config as well.

Out of scope

As mentioned, we’ll be setting up the ability to copy things downstream, i.e. over one or several tmux/ssh hops into the local clipboard. It stays out of scope to operate the remote clipboards, primarily because this has never popped up in my (quite intense) development activity – it has always been enough to have things in my local clipboard and paste them in INSERT mode into remote, if needed.

OSC52 and OSCYanc

I was a happy user of a terminal that supports OSC52, an ANSI Operating System Command set of escape sequences that, provided the support of the receiving terminal emulator, allow to copy text into the local buffer.

Another bit, vim-OSCYank (and its Lua rewrite by the same author, nvim-osc52) is a wonderful plugin that trivializes sending the OSC52-enriched messages to the local terminal emulator.

Recipe

Set up the vim-oscyank

In your init.lua or equivalent, you’d need to add the vim-oscyank plugin see here, and set it up along the following lines:

{ 
  "ojroques/vim-oscyank",
  config = function()
    -- Should be accompanied by a setting clipboard in tmux.conf, also see
    -- https://github.com/ojroques/vim-oscyank#the-plugin-does-not-work-with-tmux
    vim.g.oscyank_term = "default"
    vim.g.oscyank_max_length = 0  -- unlimited
    -- Below autocmd is for copying to OSC52 for any yank operation,
    -- see https://github.com/ojroques/vim-oscyank#copying-from-a-register
    vim.api.nvim_create_autocmd("TextYankPost", {
      pattern = "*",
      callback = function()
        if vim.v.event.operator == "y" and vim.v.event.regname == "" then
          vim.cmd('OSCYankRegister "')
        end
      end,
    })
  end,
}

(added 2023-12, see UPD) For the Lua version of the plugin (see UPD above), the config would be the following

{
  "ojroques/nvim-osc52",
  config = function()
    require("osc52").setup {
      max_length = 0,          -- Maximum length of selection (0 for no limit)
      silent = false,          -- Disable message on successful copy
      trim = false,            -- Trim surrounding whitespaces before copy
    }
    local function copy()
      if ((vim.v.event.operator == "y" or vim.v.event.operator == "d")
        and vim.v.event.regname == "") then
        require("osc52").copy_register("")
      end
    end

    vim.api.nvim_create_autocmd("TextYankPost", { callback = copy })
  end,
}

Set up the tmux

To enable interoperation of tmux with the clipboard, set in your .tmux.conf:

# Allow clipboard with OSC-52 work, see https://github.com/tmux/tmux/wiki/Clipboard
set -s set-clipboard on

Additionally, I prefer to set y in tmux scroll mode for copying:

# Use vim keybindings in copy mode
setw -g mode-keys vi
unbind -T copy-mode-vi MouseDragEnd1Pane

# Make `y` copy the selected text, not exiting the copy mode. For copy-and-exit
# use ordinary `Enter`
bind -T copy-mode-vi y send-keys -X copy-pipe  # Only copy, no cancel

Bonuses, mouse selection in neovim and tmux

Don’t forget to set the mouse selection in init.lua

vim.opt.mouse = "a"

It is annoying that when selecting things with mouse in tmux scroll mode, this selection sticks until you select something else – common UX is to drop selection on a single click anywhere:

# Clear selection on single click
bind -T copy-mode-vi MouseDown1Pane send-keys -X clear-selection \; select-pane

Want to discuss anything? Comments are welcome via e-mail alexey@gronskiy.com, Telegram @agronskiy or any other social media.