synchronize clipboard content between tmux vim and system

Pre

I usually use tmux and vim in workflow, and they work well most time, especially in a TUI environment. They both have copy/paste function, but there’s still some issue bothering me sometime.

Issue

first their clipboard functions are not uniform depending on platforms.

GUI

In a GUI environment, they both have the ablility to copy content to the system clipboard.

Tmux

In default the content copied in tmux can be paste to the system clipboard, so when you copy in tmux/vim, you can paste into other applications.
However it’s not a two-way synchronization. when you copy outside the tmux, the content can’t be paste in the tmux with prefix + ]

Vim

for vim, you can configure to synchronize with system clipboard

1
set clipboard=unnamed

only if the execution binary compiled with “clipboard” function, you can check with :version in vim. a -clipboard means it doesn’t have it.)

between tmux and vim

They can’t really communicate. Let’s say you have a Vim pane in tmux, content copied with tmux prefix + [ can paste into Vim with prefix + ], that’s sure; but content copied in vim with y can’t be paste by tmux paste action prefix + ], and you could get very confused and annoying when you carelesslly use a y instead of the proper prefix + [

TUI

Without GUI, it’s the worst situation, you don’t have a system clipboard. though you could live in tmux at the very begining when you login in the shell, and do all the copy/paste using tmux clipboard, it’s not gentle enough, not mention the cluttered keystrokes may drive you crazy.

Solution

After standing the tingling from time to time, I decide to get rid of it.
In iSH, it’s very hard to select/copy using iOS gesture, so I use tmux copy/paste function to take care of that, keyboard can control it in a more precisely way. And what’s more, vim in alpine apk can’t paste from system clipboard even with set clipboard=unnamed config, because apparently it doesn’t be compiled with clipboard funcion, I happen to find a alternative way to copy content across sections with /dev/clipboard file when I search a solution for it in iSH repo issue: ref: Can’t copy text more than 50 lines · Issue #2092 · ish-app/ish.
In iSH, /dev/clipboard is the clipboard that communicate with iSH and the iOS system, the content is shared across them. Inspired by it, I realized that files could be used for a rough synchronization purpose between tmux/vim/system. We could take files like /dev/clipboard as a intermedia for text transfer between them.

Diagrams

flowchart TD;
C(/dev/clipboard pbcopy pbpaste wlip);
T(tmux) <--> C;
V(vim) <--> C;
S(system) <--> C;

Steps

iSH

  • for tmux, make C-b ] load into paste-buffer and then paste from the /dev/clipboard instead of default paste-buffer -p.
    1
    bind-key ] run-shell 'cat /dev/clipboard | tmux load-buffer - \; paste-buffer -p'
  • for vim(which comes with no clipboard function), tweak the yank/paste function would be enough.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function! WriteVariableToFile()
    " write content into clipboard from vim register
    let content = @"
    call writefile([content], '/dev/clipboard')
    " echo "Variable written to clipboard"
    endfunction
    command! WriteVariableToFile call WriteVariableToFile()
    " hook the yank event in vim, and make the copied content write into clipboard when yank text.
    autocmd TextYankPost * call WriteVariableToFile()

    function! ReplaceRegisterContent()
    let lines = readfile('/dev/clipboard')
    let content = join(lines, "\n")
    call setreg('"', content)
    endfunction
    command! ReplaceRegisterContent call ReplaceRegisterContent()
    " remap key `p` to retrieve the content from clipboard before paste.
    nnoremap <silent> p :call ReplaceRegisterContent()<CR>p

Archlinux

archlinux doesn’t have /dev/clipboard, so I just use a temp file ~/clipboard instead.

  • tmux
    1
    bind-key ] run-shell 'cat ~/clipboard | tmux load-buffer - \; paste-buffer -p'
  • vim
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function! WriteVariableToFile()
    " write content into clipboard from vim register
    let content = @"
    call writefile([content], expand('~/clipboard'))
    " echo "Variable written to clipboard"
    endfunction
    command! WriteVariableToFile call WriteVariableToFile()
    " hook the yank event in vim, and make the copied content write into clipboard when yank text.
    autocmd TextYankPost * call WriteVariableToFile()

    function! ReplaceRegisterContent()
    let lines = readfile(expand('~/clipboard'))
    let content = join(lines, "\n")
    call setreg('"', content)
    endfunction
    command! ReplaceRegisterContent call ReplaceRegisterContent()
    " remap key `p` to retrieve the content from clipboard before paste.
    nnoremap <silent> p :call ReplaceRegisterContent()<CR>p

MacOS

  • for tmux
    1
    bind-key ] run-shell 'pbpaste | tmux load-buffer - \; paste-buffer -p'
  • vim in MacOS is usually compiled with clipboard function, so we don’t have to tweak vim config.

msys2

msys2 has /dev/clipboard.

  • tmux installed with pacman got isssue at communication with system clipboard, plugin tmux-yank will do that for you.
    1
    set -g @plugin 'tmux-plugins/tmux-yank'
    and the same, tmux under msys2 now can copy into system clipboard though, won’t paste from system clipboard, so tweak C-b ] to load text into paste-buffer from system clipboard and then paste.
    1
    bind-key ] run-shell 'cat /dev/clipboard | tmux load-buffer - \; paste-buffer -p'
  • vim in msys2 compiled with clipboard function, so we don’t have to tweak vim config.

WSL

Defaultly you can use Shift+Insert to copy contents from system to wsl, and copy text selected by mouse with a Enter keystroke.
Though, that’s not elegant enough.
Luckily, at least we can communicate with system clipboard using clip.exe. But it seems to have some performance issue. Besides you have to call powershell.exe to execute the clip.exe to handle UTF-8 issue, and you have to config "$HOME\Documents\WindowsPowerShell\" to achieve it.

However luckily again, I found a tool wclip which handle the all the things for you, and it’s fast won’t lag you down.

Using wclip tool: a blazing fast one

ref: wsl-clipboard

donwload resource with wget

1
2
3
wget https://github.com/memoryInject/wsl-clipboard/releases/download/v0.1.0/wclip.exe
wget https://github.com/memoryInject/wsl-clipboard/releases/download/v0.1.0/wcopy
wget https://github.com/memoryInject/wsl-clipboard/releases/download/v0.1.0/wpaste

mv to path

1
cp wcopy /usr/bin && cp wpaste /usr/bin

run below in powershell (warning: as administrator)

1
cp \\wsl.localhost\Ubuntu\home\Anonymous\wclip.exe C:\Windows\System32\

That’s the wclip part.

Next we need to config wcopy/wpaste in wsl.vimrc and tmuxFile/wsl.tmux.conf"

for tmux

Defaultly tmux can copy content to system clipboard with its copy mode, while it can’t copy from the system clipboard.
So, override the paste function in wsl.tmux.conf:

1
bind-key ] run-shell 'wpaste | tmux load-buffer -\; paste-buffer -p'
for vim

Default Vim installed comes with no clipboard function. Though it’s heard that vim-gtk got clipboard funtion, didn’t try it.
Vimscript provide a TextYankPost, which is a kill for hooking copying aciotn.
in wsl.vimrc:

1
2
" 1. copy to system
autocmd TextYankPost * :call system('wcopy -i',@") "wcopy is indeed super blazing fast

For pasting, if implementation of the basic pasting function from system will satisfy you then it’s an easy job.
But if you want a behaviour highly obey the native one, that’s annoying. Vimscript does’t provide paste hook like a TextPasteHook, NO, there is no such thing.
But anyway, it’s done. More than basic, the following almost make it behave like a native paste performance, including:

  • override selected text in visual mode
  • receive multiply count repeat argument [count]
  • receive register designation argument "[x]
    in wsl.vimrc:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    function! CheckAndUpdateClipboardWithSystem()
    let clipboard_content = system('wpaste')
    let register_content = getreg('"')
    " update unless neccessary(because if there is a preserved content in register_0 after delete action, set value to unnamed register will also replace content of the register_0)
    if clipboard_content != register_content
    call setreg('"', clipboard_content)
    endif
    endfunction

    function! Hook_key_p()
    call CheckAndUpdateClipboardWithSystem()
    " simulate the origin p behaviour
    " echo v:register
    " v:register usage is inspired by: [ref: How can I define an operator that takes a register as an argument?](https://vi.stackexchange.com/questions/8934/how-can-i-define-an-operator-that-takes-a-register-as-an-argument)
    let count_arg = v:count ? v:count : 1
    let cmd = "normal! " .count_arg.'"'.v:register . "p"
    execute cmd
    " [pending: personally didn't use repeat for now] Relies on vim-repeat for repeatability by the . command.
    " [ref: vim-wsl-copy-paste](https://github.com/Konfekt/vim-wsl-copy-paste/blob/main/plugin/wsl-copy-paste.vim)
    " let repeat_cmd = 'call repeat#set("cp",' . v:count . ')'
    " execute repeat_cmd
    endfunction
    " [deprecated 1] input `10p` on this keymap will: execute [paste content in Hook_key_p function] and <CR> for 10 times, besides symbol `:` will interrupt visual mode
    " nnoremap p :call Hook_key_p()<CR>
    " [deprecated 2] if only use p like below(don't execute p in the Hook_key_p function), will
    " execute <CR> for 10 times, but only paste content for 1 time.
    " execute <CR> for 10 times, but only paste content for 1 time.
    " nnoremap p :call Hook_key_p()<CR>p
    " The special text <Cmd> begins a "command mapping", it executes the command directly without changing modes.
    nnoremap p <Cmd>call Hook_key_p()<CR>
    " [deprecated 3]
    " function! Hook_key_visual_p()
    " visual mode is highly possible to be interrputed by other operatons like input `:`, to ensure replacement of the selected text, better use `gv` to restore the visual mode.
    " execute 'normal gv'
    "" let cmd = "normal! " .'"'.v:register . "p"
    " execute cmd
    " endfunction
    vnoremap p <Cmd>call Hook_key_p()<CR>
    function! Hook_key_P()
    call CheckAndUpdateClipboardWithSystem()
    let count_arg = v:count ? v:count : 1
    let cmd = "normal! " .count_arg.'"'.v:register . "P"
    execute cmd
    endfunction
    nnoremap P <Cmd>call Hook_key_P()<CR>
    vnoremap P <Cmd>call Hook_key_P()<CR>

Finally

Finally, you could just copy/paste more carelesslly, Without considering which way to be used for copy/paste operation.