diff --git a/bin/clipboard-copy b/bin/clipboard-copy new file mode 100755 index 0000000..28e1e9e --- /dev/null +++ b/bin/clipboard-copy @@ -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 diff --git a/config/nvim/lua/config/options.lua b/config/nvim/lua/config/options.lua index b2f476e..4caad53 100644 --- a/config/nvim/lua/config/options.lua +++ b/config/nvim/lua/config/options.lua @@ -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 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, diff --git a/config/tmux.conf b/config/tmux.conf index 7b44d64..3fad7bb 100644 --- a/config/tmux.conf +++ b/config/tmux.conf @@ -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 diff --git a/config/tmux/legacy.conf b/config/tmux/legacy.conf index 796ad2a..562a609 100644 --- a/config/tmux/legacy.conf +++ b/config/tmux/legacy.conf @@ -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" diff --git a/config/tmux/modern.conf b/config/tmux/modern.conf index a188ce8..b377a33 100644 --- a/config/tmux/modern.conf +++ b/config/tmux/modern.conf @@ -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