Skip to content

Commit

Permalink
Integration with clipboard: pbcopy, reattach-to-user-namespace, xclip…
Browse files Browse the repository at this point in the history
…, xsel, OSC 52 ANSI escape sequence
  • Loading branch information
samoshkin committed Nov 24, 2017
1 parent 5636f7a commit 59dbdbe
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 6 deletions.
40 changes: 34 additions & 6 deletions tmux/tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ set -g visual-activity on
# set -g visual-bell on
# setw -g bell-action other

# ================================
# === Copy and scroll ===
# ================================
# ================================================
# === Copy mode, scroll and clipboard ===
# ================================================
# Prefer vi style key table
setw -g mode-keys vi

Expand All @@ -144,6 +144,7 @@ bind C-p choose-buffer

# trigger copy mode by
bind -n M-Up copy-mode
bind -n PageUp copy-mode

# Scroll up/down by 1 line, half screen, whole screen
bind -T copy-mode-vi M-Up send-keys -X scroll-up
Expand All @@ -157,11 +158,38 @@ bind -T copy-mode-vi PageUp send-keys -X page-up
bind -T copy-mode-vi WheelUpPane select-pane \; send-keys -X -N 2 scroll-up
bind -T copy-mode-vi WheelDownPane select-pane \; send-keys -X -N 2 scroll-down

# wrap default shell in reattach-to-user-namespace if available
# there is some hack with `exec & reattach`, credits to "https://github.com/gpakosz/.tmux"
# don't really understand how it works, but at least window are not renamed to "reattach-to-user-namespace"
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)\"'"

yank="~/.tmux/yank.sh"

# Remap keys which perform copy to pipe copied text to OS clipboard
bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "$yank"
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "$yank"
bind -T copy-mode-vi Y send-keys -X copy-pipe-and-cancel "$yank; tmux paste-buffer"
bind -T copy-mode-vi C-j send-keys -X copy-pipe-and-cancel "$yank"
bind-key -T copy-mode-vi D send-keys -X copy-end-of-line \;\
run "tmux save-buffer - | $yank"
bind-key -T copy-mode-vi A send-keys -X append-selection-and-cancel \;\
run "tmux save-buffer - | $yank"

# Do not copy selection and cancel copy mode on drag end event
# More iTerm style selection: select, then need to mouse click to copy to buffer
# Prefer iTerm style selection: select, then mouse click to copy to buffer
unbind -T copy-mode-vi MouseDragEnd1Pane
bind -T copy-mode-vi MouseDown1Pane select-pane \; send-keys -X copy-selection

bind -T copy-mode-vi MouseDown1Pane select-pane \;\
send-keys -X copy-pipe-and-cancel "$yank"

# iTerm2 works with clipboard out of the box, set-clipboard already set to "external"
# tmux show-options -g -s set-clipboard
# set-clipboard on|external

# TODO: open new window/pane and retain pwd
# bind '"' split-window -c "#{pane_current_path}"
# bind % split-window -h -c "#{pane_current_path}"
# bind c new-window -c "#{pane_current_path}"


# =====================================
Expand Down
57 changes: 57 additions & 0 deletions tmux/yank.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash

set -eu

is_app_installed() {
type "$1" &>/dev/null
}

# get data either form stdin or from file
buf=$(cat "$@")

# Resolve copy backend: pbcopy (OSX), reattach-to-user-namespace (OSX), xclip/xsel (Linux)
copy_backend=""
if is_app_installed pbcopy; then
copy_backend="pbcopy"
elif is_app_installed reattach-to-user-namespace; then
copy_backend="reattach-to-user-namespace pbcopy"
elif [ -n "${DISPLAY-}" ] && is_app_installed xclip; then
copy_backend="xclip -selection clipboard -i"
elif [ -n "${DISPLAY-}" ] && is_app_installed xsel; then
copy_backend="xsel -i --clipboard"
fi

# if copy backend is resolved, copy and exit
if [ -n "$copy_backend" ]; then
printf "$buf" | "$copy_backend"
exit;
fi

# TODO: send to local socket which is remote tunneled to "pbcopy|xclip" listener on local machine
# TODO: otherwise fallback to OSC 52 escape sequence

# Copy via OSC 52 ANSI escape sequence to controlling terminal
buflen=$( printf %s "$buf" | wc -c )

# https://sunaku.github.io/tmux-yank-osc52.html
# 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
maxlen=74994

# warn if exceeds maxlen
if [ "$buflen" -gt "$maxlen" ]; then
printf "input is %d bytes too long" "$(( buflen - maxlen ))" >&2
fi

# build up OSC 52 ANSI escape sequence
esc="\033]52;c;$( printf %s "$buf" | head -c $maxlen | base64 | tr -d '\r\n' )\a"
esc="\033Ptmux;\033$esc\033\\"

# 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 machine
pane_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"

0 comments on commit 59dbdbe

Please sign in to comment.