Copying to clipboard from tmux and Vim using OSC 52

Suraj N. Kurapati

  1. Problem
    1. Approach
      1. Solution
        1. OSC 52 escapes (the new way)
          1. X11 forwarding (the old way)

          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)

          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}"'
          

          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()
          

          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.
          #
          # 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
          
          put() {
            esc=$1
            test -n "$TMUX" -o -z "${TERM##screen*}" && esc="\033Ptmux;\033$esc\033\\"
            printf "$esc"
          }
          
          # copy via OSC 52
          buf=$( cat "$@" )
          len=$( printf %s "$buf" | wc -c ) max=74994
          test $len -gt $max && echo "$0: input is $(( len - max )) bytes too long" >&2
          put "\033]52;c;$( printf %s "$buf" | head -c $max | base64 | tr -d '\r\n' )\a"
          
          # also copy to tmux
          test -n "$TMUX" && tmux set-buffer "$buf" ||:
          
          # also copy via X11
          test -n "$DISPLAY" && printf %s "$buf" | { xsel -i -b || xclip -sel c ;} ||:
          

          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.

          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'
          

          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
          

          Updates