Fix clipboard copy across terminal emulator -> tmux -> SSH -> tmux -> nvim

This commit is contained in:
2026-07-17 15:13:40 -07:00
parent 25dcfc6802
commit 0ba29f8d89
5 changed files with 70 additions and 21 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# Copy stdin to the system clipboard, regardless of platform or how deep in
# ssh/tmux this shell is: prefer a local clipboard tool for the current
# display server, and fall back to OSC 52 (wrapped for tmux's DCS
# passthrough, since tmux's own Ms-based OSC 52 forwarding doesn't reliably
# reach every terminal) when no local tool is available.
if command -v pbcopy >/dev/null 2>&1; then
exec pbcopy
elif [ -n "$WAYLAND_DISPLAY" ] && command -v wl-copy >/dev/null 2>&1; then
# wl-copy/xclip both hold the selection by staying alive as a background
# process (X11/Wayland selections have no central store -- the owner has
# to serve paste requests live). setsid -f detaches that process from
# our session so tmux's copy-pipe job cleanup can't kill it right after.
setsid -f wl-copy
elif [ -n "$DISPLAY" ] && command -v xclip >/dev/null 2>&1; then
setsid -f xclip -selection clipboard
else
# No local clipboard tool -- most likely this is a remote host reached
# over ssh, possibly with one or more local tmux hops between here and
# the actual display that this process has no way to detect. Send both
# the raw form and the tmux-DCS-passthrough-wrapped form: whichever one
# the chain in between doesn't understand is silently dropped, and the
# other gets through.
data=$(base64 | tr -d '\n')
printf '\033]52;c;%s\a' "$data" > /dev/tty
printf '\033Ptmux;\033\033]52;c;%s\a\033\\' "$data" > /dev/tty
fi
+16 -10
View File
@@ -60,18 +60,24 @@ opt.splitright = true
opt.updatetime = 300
opt.shortmess:append("c")
-- Clipboard: OSC 52 copy-only. This sends the "+ / "* register content to
-- the real terminal via escape sequence, so <leader>y works whether nvim is
-- local or several ssh/tmux hops deep -- no xclip/wl-copy/pbcopy needed on
-- the remote host. Paste is intentionally left as a no-op: OSC 52 read
-- support is inconsistent across terminals and can hang waiting for a
-- reply, so use the terminal's own native paste (e.g. Ctrl-Shift-V) instead.
local osc52 = require("vim.ui.clipboard.osc52")
-- Clipboard: copy-only, via dotfiles/bin/clipboard-copy. This is a small
-- script (rather than nvim's built-in raw-OSC-52-only provider) because
-- reaching the real clipboard needs more than a raw OSC 52 write: it picks
-- pbcopy/xclip/wl-copy when nvim is running locally, and when none of those
-- are available (nvim on a remote host with no local display) sends OSC 52
-- both raw and wrapped for tmux's DCS passthrough, since it can't know how
-- many local tmux hops are between it and the real terminal and tmux's own
-- native OSC 52 forwarding isn't reliable. Works whether nvim is local or
-- several ssh/tmux hops deep, on any machine sharing this dotfiles repo.
-- Paste is intentionally left as a no-op: OSC 52 read support is
-- inconsistent across terminals and can hang waiting for a reply, so use
-- the terminal's own native paste (e.g. Ctrl-Shift-V) instead.
local clipboard_copy = vim.fn.expand("~/dotfiles/bin/clipboard-copy")
vim.g.clipboard = {
name = "OSC 52 (copy-only)",
name = "clipboard-copy (copy-only)",
copy = {
["+"] = osc52.copy("+"),
["*"] = osc52.copy("*"),
["+"] = { clipboard_copy },
["*"] = { clipboard_copy },
},
paste = {
["+"] = function() return {} end,
+16 -7
View File
@@ -49,8 +49,8 @@ bind a setw synchronize-panes
setw -g mode-keys vi
set-option -g -q mouse on
bind y run 'tmux save-buffer - | reattach-to-user-namespace pbcopy'
bind C-y run 'tmux save-buffer - | reattach-to-user-namespace pbcopy'
bind y run 'tmux save-buffer - | $HOME/dotfiles/bin/clipboard-copy'
bind C-y run 'tmux save-buffer - | $HOME/dotfiles/bin/clipboard-copy'
# Let OSC 52 escape sequences (e.g. nvim's clipboard writes) pass through
# this tmux to the real terminal, even when nested behind ssh/tmux layers.
@@ -68,14 +68,23 @@ if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3003) exit 0;
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3002) exit 0; else exit 1}'" 'set -s extended-keys on; set -as terminal-features "*:extkeys"'
# Also make tmux's own copies (mouse-drag select in copy-mode, prefix-[ y,
# etc.) reach the real system CLIPBOARD via OSC 52 -- not just PRIMARY/
# middle-click -- by declaring the "Ms" (set-clipboard) terminfo capability
# tmux needs to emit the escape sequence itself, then telling tmux to use it.
# "external" only ever writes; it never asks the terminal to reply, so this
# can't hang the way an OSC 52 read query can.
# etc.) reach the real system CLIPBOARD -- not just PRIMARY/middle-click.
# tmux's built-in mechanism for this is declaring the "Ms" terminfo
# capability and setting set-clipboard so tmux emits the OSC 52 escape
# itself, which is what these two lines do. Kept as a harmless attempt for
# terminals where it works, but confirmed unreliable against Alacritty (both
# pane-forwarded and tmux-generated OSC 52 silently failed to reach the
# clipboard even with Ms correctly registered) -- copy-command below is the
# mechanism actually proven to work here, via bin/clipboard-copy.
set -as terminal-overrides ',*:Ms=\E]52;c;%p2%s\007'
set -g set-clipboard external
# The default/no-argument form of copy-pipe (used by double/triple-click and
# a couple of default bindings) pipes to this option instead of a literal
# command -- route it through the same working script as the explicit
# copy-pipe bindings in tmux/modern.conf.
set -g copy-command '$HOME/dotfiles/bin/clipboard-copy'
# iTerm2 supports SIXEL images (since 3.3.0) but doesn't set a distinct TERM
# to identify itself, so this matches on its usual "xterm-256color" report --
# any other client reporting that same TERM will also be treated as
+2 -2
View File
@@ -7,9 +7,9 @@ bind-key -t vi-copy NPage page-down
bind-key -t vi-copy WheelUpPane halfpage-up
bind-key -t vi-copy WheelDownPane halfpage-down
bind-key -t vi-copy v begin-selection
bind-key -t vi-copy y copy-pipe "reattach-to-user-namespace pbcopy"
bind-key -t vi-copy y copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
unbind-key -t vi-copy Enter
bind-key -t vi-copy Enter copy-pipe "reattach-to-user-namespace pbcopy"
bind-key -t vi-copy Enter copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
bind-key -n PPage if-shell -F "#{alternate_on}" "send-keys PPage" "copy-mode -e; send-keys PPage"
bind-key -n WheelUpPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; copy-mode -e; send-keys -M"
+9 -2
View File
@@ -6,9 +6,16 @@ bind-key -T copy-mode-vi NPage send-keys -X page-down
bind-key -T copy-mode-vi WheelUpPane send-keys -X halfpage-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X halfpage-down
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe "reattach-to-user-namespace pbcopy"
bind-key -T copy-mode-vi y send-keys -X copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
unbind-key -T copy-mode-vi Enter
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe "reattach-to-user-namespace pbcopy"
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
# The default MouseDragEnd1Pane binding (copy-selection-and-cancel) relies on
# tmux's own OSC 52 write via the Ms terminfo capability, which doesn't
# reliably reach every terminal (confirmed broken against Alacritty here).
# Route mouse-drag copies through the same clipboard-copy script as keyboard
# yanks instead, so both use the mechanism that's actually proven to work.
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "$HOME/dotfiles/bin/clipboard-copy"
# Let mouse wheel / PageUp pass through to apps using the alternate screen
# (vim, less, etc.) instead of always entering copy-mode