fix: make clip2path safe and ssh-aware

Text paste went through `kitty @ send-text`, which has no bracketed
paste, so multi-line clipboard text ran line by line in a shell. Route
text through `kitty @ action paste_from_clipboard` instead and keep
send-text for the image path only.

Over SSH the typed path pointed at the local /tmp and the remote process
could not read it. Read the focused window's foreground processes from
`kitty @ ls`, and when one of them is an ssh client, scp the image to
that host and type the remote path. Nothing is typed into the focused
app but a path, so this still works when the target is a TUI rather than
a shell. The real ssh process is preferred over the `kitten ssh`
wrapper, whose long options the argument parser does not understand.

Also: mktemp instead of a timestamp that collides within a second,
a prune of clip files older than a day, image/png preferred over
whatever format the source advertises first, and the clipboard tools
pinned so the script does not depend on kitty's inherited PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-08-18 08:30:20 +01:00
parent 05037e40d8
commit 4a8f8d330c
+98 -17
View File
@@ -1,27 +1,108 @@
{ pkgs, ... }:
let
clip2path = pkgs.writeShellScript "clip2path" ''
set -e
set -euo pipefail
if [ -n "$WAYLAND_DISPLAY" ]; then
types=$(wl-paste --list-types)
if grep -q '^image/' <<<"$types"; then
ext=$(grep -m1 '^image/' <<<"$types" | cut -d/ -f2 | cut -d';' -f1)
file="/tmp/clip_$(date +%s).''${ext}"
wl-paste --type "image/''${ext}" > "$file"
printf '%q' "$file" | kitty @ send-text --stdin
jq=${pkgs.jq}/bin/jq
scp=${pkgs.openssh}/bin/scp
wlpaste=${pkgs.wl-clipboard}/bin/wl-paste
xclip=${pkgs.xclip}/bin/xclip
find=${pkgs.findutils}/bin/find
# kitty/kitten stay unpinned: remote control has to reach the running
# instance, whose build may differ from pkgs.kitty.
"$find" /tmp -maxdepth 1 -name 'clip_*' -mtime +1 -delete 2>/dev/null || true
# Some apps advertise a lossy format before image/png.
pick_type() {
if grep -qx 'image/png' <<<"$1"; then
echo image/png
else
wl-paste --no-newline | kitty @ send-text --stdin
grep -m1 '^image/' <<<"$1" | cut -d';' -f1
fi
elif [ -n "$DISPLAY" ]; then
types=$(xclip -selection clipboard -t TARGETS -o)
if grep -q '^image/' <<<"$types"; then
ext=$(grep -m1 '^image/' <<<"$types" | cut -d/ -f2 | cut -d';' -f1)
file="/tmp/clip_$(date +%s).''${ext}"
xclip -selection clipboard -t "image/''${ext}" -o > "$file"
printf '%q' "$file" | kitty @ send-text --stdin
}
ext_of() {
local e=''${1#image/}
e=''${e%%+*}
e=''${e#x-}
tr -cd 'a-zA-Z0-9' <<<"$e"
}
# Destination of the ssh client running in the focused window, empty if
# that window is local. Prefers the real ssh process over the `kitten ssh`
# wrapper, whose own long options would confuse the parser below.
ssh_dest() {
local -a argv
mapfile -t argv < <(kitty @ ls | "$jq" -r '
[ .[] | select(.is_focused)
| .tabs[] | select(.is_focused)
| .windows[] | select(.is_focused)
| .foreground_processes[]?
| { b: ((.cmdline[0] // "") | split("/") | last), c: .cmdline }
] as $p
| ( [ $p[] | select(.b == "ssh") ] + [ $p[] | select(.b == "kitten") ] )
| (.[0].c // [])[]')
[ ''${#argv[@]} -gt 0 ] || return 0
if [ "''${argv[0]##*/}" = kitten ]; then
[ "''${argv[1]:-}" = ssh ] || return 0
argv=("''${argv[@]:2}")
else
xclip -selection clipboard -o | kitty @ send-text --stdin
argv=("''${argv[@]:1}")
fi
local valueflags=bcDEeFIiJLlmOopQRSWw a
while [ ''${#argv[@]} -gt 0 ]; do
a=''${argv[0]}
case "$a" in
--) echo "''${argv[1]:-}"; return 0 ;;
-?)
case "$valueflags" in *"''${a#-}"*) argv=("''${argv[@]:1}") ;; esac
argv=("''${argv[@]:1}")
;;
-*) argv=("''${argv[@]:1}") ;;
*) echo "$a"; return 0 ;;
esac
done
}
send_path() {
local file=$1 dest remote
dest=$(ssh_dest)
if [ -n "$dest" ]; then
remote=/tmp/''${file##*/}
if ! "$scp" -q -o BatchMode=yes -o ConnectTimeout=5 "$file" "$dest:$remote"; then
kitten notify --app-name clip2path clip2path "copy to $dest failed, nothing pasted"
return 1
fi
file=$remote
fi
printf '%q' "$file" | kitty @ send-text --stdin
}
# Text goes through the real paste action. send-text has no bracketed
# paste, so multi-line text would run line by line in a shell.
if [ -n "''${WAYLAND_DISPLAY:-}" ]; then
types=$("$wlpaste" --list-types)
if grep -q '^image/' <<<"$types"; then
type=$(pick_type "$types")
file=$(mktemp --suffix=".$(ext_of "$type")" /tmp/clip_XXXXXXXX)
"$wlpaste" --type "$type" > "$file"
send_path "$file"
else
kitty @ action paste_from_clipboard
fi
elif [ -n "''${DISPLAY:-}" ]; then
types=$("$xclip" -selection clipboard -t TARGETS -o)
if grep -q '^image/' <<<"$types"; then
type=$(pick_type "$types")
file=$(mktemp --suffix=".$(ext_of "$type")" /tmp/clip_XXXXXXXX)
"$xclip" -selection clipboard -t "$type" -o > "$file"
send_path "$file"
else
kitty @ action paste_from_clipboard
fi
fi
'';