Copying to clipboard from tmux and Vim using OSC 52
Problem
I switched to an Acer C720 chromebook as my primary computer recently. However, its X11-based Aura desktop environment lacks X11 clipboard integration, which prevents me from using X11 forwarding to run the xsel(1) program as a means of copying and pasting text between my system clipboard and programs running remotely in SSH. So, I had to find a different solution for my copy/paste needs.
Approach
In my search, I came across the OSC 52 escape sequence, which tells the receiving terminal to copy some specified text into the user’s system clipboard.
However, this escape sequence is only partially supported in the current stable
version of tmux
(version 1.9a as of this writing), which limits the amount of
text that can be copied (per my observations)
to a meager 180 bytes!
I reported this issue to
the tmux-users mailing list, where developer Nicholas Marriott quickly provided a
patch to
fix it. Armed with this patch, I distilled my working knowledge into a yank
script, integrated with tmux and Vim.
Now I can easily copy text from tmux and Vim running in any terminal session
(whether local, remote, or even nested therein!) without needing X11
forwarding and having to keep environment variables like $DISPLAY
and
$XAUTHORITY
up to date! Finally, to paste text from the system clipboard into
a terminal session, I simply use the Control-Shift-V shortcut of the Secure
Shell terminal
emulator. And so, what began as a problem
led to the discovery of a superior solution! :-)
Solution
OSC 52 escapes (the new way)
Create a ~/bin/yank script
In ~/bin/yank
file, which is also available on GitHub:
#!/bin/sh
#
# Usage: yank [FILE...]
#
# Copies the contents of the given files (or stdin, if no files are given) to
# the terminal that runs this program. If this program is run inside tmux(1),
# then it also copies the given contents into tmux's current clipboard buffer.
# If this program is run inside X11, then it also copies to the X11 clipboard.
#
# This is achieved by writing an OSC 52 escape sequence to the said terminal.
# The maximum length of an OSC 52 escape sequence is 100_000 bytes, of which
# 7 bytes are occupied by a "\033]52;c;" header, 1 byte by a "\a" footer, and
# 99_992 bytes by the base64-encoded result of 74_994 bytes of copyable text.
#
# In other words, this program can only copy up to 74_994 bytes of its input.
# However, in such cases, this program tries to bypass the input length limit
# by copying directly to the X11 clipboard if a $DISPLAY server is available;
# otherwise, it emits a warning (on stderr) about the number of bytes dropped.
#
# NOTE: You might also need to allow this script to bypass tmux (through the
# "Ptmux;" escape sequence) by enabling allow-passthrough in your tmux.conf:
#
# set-window-option -g allow-passthrough on
#
# See http://en.wikipedia.org/wiki/Base64 for the 4*ceil(n/3) length formula.
# See http://sourceforge.net/p/tmux/mailman/message/32221257 for copy limits.
# See http://sourceforge.net/p/tmux/tmux-code/ci/a0295b4c2f6 for DCS in tmux.
#
# Written in 2014 by Suraj N. Kurapati <https://github.com/sunaku>
# Also documented at https://sunaku.github.io/tmux-yank-osc52.html
input=$( cat "$@" )
input() { printf %s "$input" ;}
known() { command -v "$1" >/dev/null ;}
maybe() { known "$1" && input | "$@" ;}
alive() { known "$1" && "$@" >/dev/null 2>&1 ;}
# copy to tmux
test -n "$TMUX" && maybe tmux load-buffer -
# copy via X11
test -n "$DISPLAY" && alive xhost && {
maybe xsel -i -b || maybe xclip -sel c
}
# copy via OSC 52
printf_escape() {
esc=$1
test -n "$TMUX" -o -z "${TERM##screen*}" && esc="\033Ptmux;\033$esc\033\\"
printf "$esc"
}
len=$( input | wc -c ) max=74994
test $len -gt $max && echo "$0: input is $(( len - max )) bytes too long" >&2
printf_escape "\033]52;c;$( input | head -c $max | base64 | tr -d '\r\n' )\a"
Configure your ~/.tmux.conf
NOTE: See also my
tmux.conf
file template on GitHub for reference.
In ~/.tmux.conf
file (if you are using tmux 3.3 or newer):
# pass "Ptmux;" escape sequences through to the terminal
set-window-option -g allow-passthrough on
In ~/.tmux.conf
file (if you are using tmux 2.4 or newer):
# transfer copied text to attached terminal with yank
bind-key -T copy-mode-vi Y send-keys -X copy-pipe 'yank > #{pane_tty}'
# transfer most-recently copied text to attached terminal with yank
bind-key -n M-y run-shell 'tmux save-buffer - | yank > #{pane_tty}'
# transfer previously copied text (chosen from a menu) to attached terminal
bind-key -n M-Y choose-buffer 'run-shell "tmux save-buffer -b \"%%%\" - | yank > #{pane_tty}"'
In ~/.tmux.conf
file (if you are using tmux 2.3 or older):
# transfer copied text to attached terminal with yank
bind-key -t vi-copy y copy-pipe 'yank > #{pane_tty}'
# transfer copied text to attached terminal with yank
bind-key -n M-y run-shell 'tmux save-buffer - | yank > #{pane_tty}'
# transfer previously copied text (chosen from a menu) to attached terminal
bind-key -n M-Y choose-buffer 'run-shell "tmux save-buffer -b \"%%\" - | yank > #{pane_tty}"'
Configure your ~/.vimrc
In ~/.vimrc
file:
" copy to attached terminal using the yank(1) script:
" https://github.com/sunaku/home/blob/master/bin/yank
function! Yank(text) abort
let escape = system('yank', a:text)
if v:shell_error
echoerr escape
else
call writefile([escape], '/dev/tty', 'b')
endif
endfunction
noremap <silent> <Leader>y y:<C-U>call Yank(@0)<CR>
" automatically run yank(1) whenever yanking in Vim
" (this snippet was contributed by Larry Sanderson)
function! CopyYank() abort
call Yank(join(v:event.regcontents, "\n"))
endfunction
autocmd TextYankPost * call CopyYank()
X11 forwarding (the old way)
The disadvantage of this X11 forwarding solution is that you need to run SSH with
the -X
or -Y
options while also keeping environment variables like
$DISPLAY
and $XAUTHORITY
up to date as you disconnect and reconnect your
remote sessions.
Configure your ~/.tmux.conf
In ~/.tmux.conf
file:
# transfer copied text to X primary selection
bind-key -n M-y run-shell 'tmux save-buffer - | xsel -p -i'
# transfer copied text to X clipboard selection
bind-key -n M-Y run-shell 'tmux save-buffer - | xsel -b -i'
# paste X primary selection
bind-key -n M-p run-shell 'xsel -p -o | tmux load-buffer - \; paste-buffer \; delete-buffer'
# paste X clipboard selection
bind-key -n M-P run-shell 'xsel -b -o | tmux load-buffer - \; paste-buffer \; delete-buffer'
Configure your ~/.vimrc
In ~/.vimrc
file:
" copy to primary selection
noremap <Leader>y "*y
" copy to clipboard selection
noremap <Leader>Y "+y
" paste from primary selection
noremap <Leader>p "*p
" paste from clipboard selection
noremap <Leader>P "+p