by Alexey Samoshkin

tmux in practice: integration with the system clipboard

How to build a bridge between tmux copy buffer and system clipboard, and to store selected text on OSX or Linux system clipboard, in a way that address both local and remote usage scenarios

This is the 4th part of my tmux in practice article series.

You can copy text from local or remote, or even nested remote session to your system clipboard

In the previous part of “tmux in practice” series we talked about things like scrollback buffer, copy mode, and slightly touched on the topic of copying text into tmux’s copy buffer.

Sooner or later you’ll realize that whatever you copy in tmux gets stored in tmux’s copy buffer only, but not shared with system clipboard. Copying and pasting are such common operations, that this limitation is itself enough to turn tmux into a useless brick, despite other goodies.

In this post we’ll explore how to build a bridge between the tmux copy buffer and system clipboard, to store copied text on system clipboard, in a way that address both local and remote usage scenarios.

We’ll discuss following techniques:

  1. OSX only, share text with clipboard using “pbcopy”
  2. OSX only, using “reattach-to-user-namespace” wrapper to make pbcopy work properly inside tmux environment
  3. Linux only, share text with X selection using xclip or xsel commands

Techniques above address only local scenarios.
To support remote scenarios there are 2 extra methods:

  1. Use the ANSI OSC 52 escape sequence to talk to controlling/parent terminal to manage and store text on a clipboard of a local machine.
  2. Setup a local network listener which pipes input to pbcopy or xclipor xsel. Pipe copied selected text from remote machine to a listener on the local machine through SSH remote tunneling. This is rather involved, and I will devote a dedicated post to describe it.

OSX. pbcopy and pbpaste commands

pbcopy and pbpaste commands allow you to interact and manipulate system clipboard from command line.

pbcopy reads data from stdin and stores it in the clipboard. pbpaste does the opposite and puts copied text on stdout.

The idea is to hook into various tmux commands, that manage to copy text while in copy mode.

Let’s list them:

$ tmux -f /dev/null list-keys -T copy-mode-vi
bind-key -T copy-mode-vi Enter send-keys -X copy-selection-and-cancelbind-key -T copy-mode-vi C-j send-keys -X copy-selection-and-cancelbind-key -T copy-mode-vi D send-keys -X copy-end-of-linebind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection-and-cancelbind-key -T copy-mode-vi A send-keys -X append-selection-and-cancel

copy-selection-and-cancel and copy-end-of-line are special tmux commands which tmux understand when pane is in copy mode. There are two flavors of copy command: copy-selection and copy-pipe.

Let’s rewrite Enter keybinding with copy-pipe command:

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "pbcopy"

copy-pipe command stores selected text in tmux buffer same to copy-selection, plus pipes selected text to the given command pbcopy. So we get text stored in two places: the tmux copy buffer and the system clipboard.

OSX. reattach-to-user-namespace wrapper

So far so good. However, on some versions of OSX, pbcopy and pbpaste fail to function properly when run under tmux.

Read more details from Chris Johnsen on why it happens:

tmux uses the daemon(3) library function when starting its server process. In Mac OS X 10.5, Apple changed daemon(3) to move the resulting process from its original bootstrap namespace to the root bootstrap namespace. This means that the tmux server, and its children, will automatically and uncontrollably lose access to what would have been their original bootstrap namespace (i.e. the one that has access to the pasteboard service).

A common solution is to use reattach-to-user-namespace wrapper. This allows us to launch a process and have that process be attached to the per-user bootstrap namespace, which makes the program behave as we are expecting. You need to change keybinding properly:

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel “reattach-to-user-namespace pbcopy”

Plus, you would need to tell tmux to run your shell (bash, zsh, …) inside a wrapper, by setting default-command option:

if -b "command -v reattach-to-user-namespace > /dev/null 2>&1" \    "run 'tmux set -g default-command \"exec $(tmux show -gv default-shell) 2>/dev/null & reattach-to-user-namespace -l $(tmux show -gv default-shell)\"'"

Note: some OSX versions works fine even without this hack (OSX 10.11.5 El Capitan), whereas OSX Sierra users report this hack is still needed.

Linux. Interact with X selection via xclip and xsel

We can make use of xclip or xsel commands on Linux to store text in the clipboard, same as pbcopy on OSX. On Linux, there are several kinds of clipboard selections maintained by X server: primary, secondary and clipboard. We only concern with primary and clipboard. Secondary was intended as an alternate to primary.

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xclip -i -f -selection primary | xclip -i -selection clipboard"

Or when using xsel:

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xsel -i --clipboard"

Read here about comparison of xclip vs. xsel, if you’re curious. Also, check out this post on xclip usage and examples. And don’t forget to install one of these utilities, as they might not be a part of your distribution.

Using ANSI OSC 52 escape sequence to cause terminal to store text in the clipboard

So far we covered only local scenarios. When you SSH to remote machine, and start tmux sessions there, you cannot make use of pbcopy, xclip or xsel, because text will be stored in the remote machine’s clipboard, not in your local one. You need some way to transport copied text to your local machine’s clipboard.

ANSI escape sequence is a sequence of bytes sent to the terminal that are interleaved with regular printable characters, and are used to control various terminal aspects: such as text colors, cursor position, text effects, clearing screen. The terminal is capable of detecting such controlling sequence of bytes that causes it to trigger specific actions and not print those characters to the output.

The ANSI escape sequence can be detected as they start with ESC ASCII character (0x1b hex, 027 decimal, \033 in octal). For example, when the terminal sees the \033[2A sequence, it will move the cursor position 2 lines up.

There are really a lot of those known sequences. Some of them are the same across different terminal types, while others can vary and be very specific to your terminal emulator. Useinfocmp command to query terminfo database for escape sequences supported by different types of terminals.

Okay great, but how can it help us regarding the clipboard? It turns out that there is a special category of escape sequences: “Operating System Controls” (OSC) and the “OSC 52" escape sequence, which allows applications to interact with the clipboard.

If you’re using iTerm, try to execute following command, and then “⌘V” to see contents of system clipboard. Make sure to turn on OSC 52 escape sequence handling: “Preferences -> General -> Applications in terminal may access clipboard”.

printf "\033]52;c;$(printf "%s" "blabla" | base64)\a"

The conclusion is that we can store text in the system clipboard by sending a specially crafted ANSI escape sequence to our terminal.

Let’s write the shell script

set -eu
# get data either form stdin or from filebuf=$(cat "$@")
# Get buffer lengthbuflen=$( printf %s "$buf" | wc -c )
# warn if exceeds maxlenif [ "$buflen" -gt "$maxlen" ]; then   printf "input is %d bytes too long" "$(( buflen - maxlen ))" >&2fi
# build up OSC 52 ANSI escape sequenceesc="\033]52;c;$( printf %s "$buf" | head -c $maxlen | base64 | tr -d '\r\n' )\a"

So, we read text to copy from stdin, then check if it’s length exceeds the maximum length of 74994 bytes. If true, we crop it, and finally convert data to base64 and wrap in OSC 52 escape sequence: \033]53;c;${data_in_base64}\a

Then let’s wire it with our tmux keybindings. That’s pretty easy: just pipe the selected text to our script, just as we pipe it to pbcopy or xclip.

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "$yank"

However, there is one piece left to complete the puzzle. Where should we send the escape sequence? Apparently, just sending it to stdout won’t work. The target should be our parent terminal emulator, but we don’t know the right tty. So, we’re going to send it to tmux’s active pane tty, and tell tmux to further resend it to the parent terminal emulator:

# build up OSC 52 ANSI escape sequenceesc="\033]52;c;$( printf %s "$buf" | head -c $maxlen | base64 | tr -d '\r\n' )\a"esc="\033Ptmux;\033$esc\033\\"
pane_active_tty=$(tmux list-panes -F "#{pane_active} #{pane_tty}" | awk '$1=="1" { print $2 }')
printf "$esc" > "$pane_active_tty"

We use tmux list-panes command to query for the active pane and it’s tty. We also put our OSC 52 sequence in an additional wrapper escape sequence (Device Control String, ESC P), so tmux unwraps this envelope and passes OSC 52 to parent terminal.

In newer versions of tmux, you can tell tmux to handle interactions with the clipboard for you. Seeset-clipboard tmux option. on — tmux will create an inner buffer and attempt to set the terminal clipboard using OSC 52. external — do not create a buffer, but still attempt to set the terminal clipboard.

Just make sure it’s either external or on:

set -g set-clipboard on

So, if tmux is already capable of this feature, why we need to bother ourselves with manual wiring OSC 52 stuff? That’s because set-clipboard does not work when you have a remote tmux session nested in a local one. And it only works in those terminals which supports OSC 52 escape sequence handling.

The trick for nested remote sessions is to bypass the remote session and send our OSC 52 escape sequence directly to the local session, so it hits our local terminal emulator (iTerm).

Use $SSH_TTY for this purpose:

# resolve target terminal to send escape sequence# if we are on remote machine, send directly to SSH_TTY to transport escape sequence# to terminal on local machine, so data lands in clipboard on our local machinepane_active_tty=$(tmux list-panes -F "#{pane_active} #{pane_tty}" | awk '$1=="1" { print $2 }')target_tty="${SSH_TTY:-$pane_active_tty}"
printf "$esc" > "$target_tty"

That’s it. Now we have a completely working solution, be it a local session, remote or both, nested in each other. Credits to this great post, where I first read about this approach.

The major drawback of using OSC escape sequences,is that despite being declared in spec, only a few terminals support this in practice: iTerm and xterm do, whereas OSX Terminal, Terminator, and Gnome terminal does not. So, an otherwise great solution (especially in remote scenarios, when you cannot just pipe to xclip or pbcopy) lacks wider terminal support.

You might want to checkout complete version of script.

There is yet another solution to support remote scenarios, which is rather crazy, and I’ll describe it in another dedicated post. The idea is to setup a local network listener which pipes input to pbcopy or xclipor xsel; and pipes copied selected text from a remote machine to a listener on the local machine through SSH remote tunneling. Stay tuned.

ANSI escape code — Wikipedia —

What are OSC terminal control sequences / escape codes? | ivucica blog —

Copying to clipboard from tmux and Vim using OSC 52 — The Terminal Programmer —

Copy Shell Prompt Output To Linux / UNIX X Clipboard Directly — nixCraft —

software recommendation — ‘xclip’ vs. ‘xsel’ — Ask Ubuntu —

Everything you need to know about Tmux copy paste · rushiagr —

macos — Synchronize pasteboard between remote tmux session and local Mac OS pasteboard — Super User —

linux — Getting Items on the Local Clipboard from a Remote SSH Session — Stack Overflow —

Use tmux set-clipboard in gnome-terminal (XTerm’s disallowedWindowOps) — Ask Ubuntu —