Files
dotfiles/bin/clipboard-copy
branderson d76d2f4b3a Fix clipboard OSC 52 fallback: walk past nvim's tty-less embed process
Nvim isn't a single process attached to the terminal: the process that
actually spawns jobs (clipboard-copy included) is a separate `nvim --embed`
core process with no tty of its own (ps shows `?`); the real pty belongs to
a distinct parent UI process. Checking only the immediate parent's tty (as
the previous fix did) found the embed process's `?` and fell through to the
broken /dev/tty path every time in real interactive use, even though it
looked fine in headless testing. Walk up the process tree past any tty-less
ancestors instead.

Verified against the real docker-compose path in actual (non-headless) TUI
mode, with real keystrokes fed through a pty: the OSC 52 sequence now
reaches the terminal with no error, where it previously still failed.
2026-07-19 11:46:31 -07:00

46 lines
2.1 KiB
Bash
Executable File

#!/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 a single plain OSC 52 write when no local
# tool is available (e.g. a headless remote host). Any tmux hops in between
# here and the real terminal relay that onward themselves, recursively, via
# the pane-set-clipboard hook in tmux.conf -- this doesn't need to know or
# guess how many hops there are.
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
data=$(base64 | tr -d '\n')
# /dev/tty needs this process's own session to have a controlling
# terminal, but a job spawned by nvim's clipboard provider doesn't get
# one (nvim's job-spawning doesn't establish a ctty for children). Nvim
# itself also isn't a single process: the one actually spawning jobs is
# a `nvim --embed` core process with no tty of its own (shown as `?` in
# ps) -- the real terminal belongs to a separate parent UI process. Walk
# up the process tree past any tty-less ancestors to find it.
real_tty=""
pid="$PPID"
i=0
while [ "$i" -lt 10 ] && [ -n "$pid" ] && [ "$pid" != "0" ]; do
t=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
if [ -n "$t" ] && [ "$t" != "?" ]; then
real_tty="/dev/$t"
break
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
i=$((i + 1))
done
if [ -n "$real_tty" ] && [ -w "$real_tty" ]; then
printf '\033]52;c;%s\a' "$data" > "$real_tty"
else
printf '\033]52;c;%s\a' "$data" > /dev/tty
fi
fi