feat(clipboard): add read-only clipboard-check diagnostic script

Lists selection owner/metadata via wl-paste -l to tell Firefox, kitty OSC 52
and wl-copy apart when a copy appears to do nothing. 'clipboard-check.sh test'
runs a 20s observation window. Never writes the clipboard or restarts anything.
Lives in scripts/ with ~/clipboard-check.sh symlinked in, matching the
existing scripts/ convention.
This commit is contained in:
jozamudi committed 2026-10-29 08:55:14 -07:00
1 parent bf87375356
commit 4bb1c06883
1 file changed
+95
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# clipboard-check.sh -- diagnose "copy did nothing" on sway/Wayland.
# READ-ONLY: lists selection metadata and reads text; never writes the clipboard,
# never runs wl-copy, never restarts anything.
# ./clipboard-check.sh -> status (what is configured right now)
# ./clipboard-check.sh test -> 20s window: copy in Firefox, then tell me who failed
set -uo pipefail
own_sig() { # identify the owner from its advertised type list
local t
t=$(wl-paste -l 2>&1); [[ -z "$t" ]] && { echo "(clipboard empty / no owner)"; return; }
case "$t" in
*x-moz-url-priv*|*text/html*) echo "Firefox (has moz/html types)" ;;
*glfw+clipboard*) echo "kitty/GLFW terminal (OSC 52 or copy_on_select)" ;;
*image/png*) echo "image (screenshot? wl-copy -b)" ;;
*) echo "some other client: $(echo "$t" | tr '\n' ' ')" ;;
esac
}
if [[ "${1:-status}" == test ]]; then
echo "BEFORE owner: $(own_sig)"
echo
echo " >> Copy some text in Firefox NOW (Ctrl+C), then press Enter."
read -r _
echo "AFTER owner: $(own_sig)"
echo
if wl-paste -l >/dev/null 2>&1; then
n=$(wl-paste 2>/dev/null | wc -c)
echo " text available: $n byte(s)"
[[ $n -gt 0 ]] && echo " preview: $(wl-paste 2>/dev/null | head -c 70 | tr '\n' ' ')"
echo
echo " VERDICT: the compositor HAS the copy -> the loss is on the paste side"
echo " (stale PRIMARY for middle-click, or a clipboard-history keybind"
echo " that no longer exists since clipman was removed)."
else
echo " VERDICT: the compositor has NOTHING -> Firefox never took ownership."
echo " Usual causes, in order of likelihood:"
echo " 1) Firefox lacked keyboard focus at Ctrl+C -- Wayland only"
echo " accepts set_selection from the focused surface (a PiP window,"
echo " a toast, or a workspace switch stealing focus is enough)."
echo " 2) The page's own JS intercepted the copy event (sites that"
echo " block copying). dom.event.clipboardevents.enabled=true by"
echo " default, so page scripts may do this."
echo " 3) The offer got cancelled mid-transfer ('selection cancelled'"
echo " in sway's log) -- see: journalctl -b | grep 'selection cancelled'"
fi
exit 0
fi
echo "== components =="
printf ' %-22s %s\n' "sway" "$(swaymsg --version 2>&1 | head -1)"
printf ' %-22s %s\n' "wl-clipboard" "$(wl-copy --version 2>&1 | head -1)"
printf ' %-22s %s\n' "firefox" "$(firefox --version 2>&1 | head -1) [$(pacman -Q firefox 2>/dev/null || echo '?')]"
printf ' %-22s %s\n' "cliphist" "$(command -v cliphist || echo 'NOT INSTALLED (history keybinds are dead)')"
echo
echo "== clipboard managers / watchers (a watcher that dies mid-copy LOSES the copy) =="
m=$(pgrep -af 'wl-paste|cliphist|clipman|copyq|klipper|parcellite|flameshot' 2>/dev/null || true)
[[ -n "$m" ]] && echo "$m" | sed 's/^/ RUNNING /' || echo " none running <- no history is being saved, but also nothing can steal a copy"
echo
echo "== XWayland mixing (X11<->Wayland clipboard skew is a classic cause) =="
if pgrep -x Xwayland >/dev/null 2>&1; then
echo " Xwayland IS running -> any X11 app can own the selection out-of-band:"
pgrep -a Xwayland | sed 's/^/ /'
else
echo " no Xwayland -> pure Wayland, that whole failure class is ruled out"
fi
echo
echo "== window shells (a copy from an X11-surfaced Firefox behaves differently) =="
swaymsg -t get_tree 2>/dev/null | python3 -c "
import json,sys
def walk(n):
yield n
for k in ('nodes','floating_nodes'):
for c in n.get(k) or []: yield from walk(c)
for n in walk(json.load(sys.stdin)):
if n.get('pid') and n.get('type') in ('con','floating_con'):
shell = 'x11' if n.get('window') is not None else 'wayland'
wp = n.get('window_properties') or {}
ident = str(n.get('app_id') or wp.get('class'))[:24]
print(' %-8s %-26s %s' % (shell, ident, (n.get('name') or '')[:38]))
" 2>&1 | sort -u | head -12
echo
echo "== current selections (read-only) =="
printf ' CLIPBOARD owner : %s\n' "$(own_sig)"
printf ' PRIMARY types : %s\n' "$(wl-paste -p -l 2>&1 | tr '\n' ' ' | cut -c1-100)"
echo
echo "== Firefox clipboard prefs (blank = default) =="
P=$(ls ~/.mozilla/firefox/*/prefs.js 2>/dev/null | head -1)
grep -hE 'dom\.event\.clipboardevents|middlepaste|widget\.wayland' "$P" 2>/dev/null | sed 's/^/ /' || true
grep -hE 'user_pref' "$(dirname "$P")/user.js" 2>/dev/null | grep -iE 'clipboard|wayland' | sed 's/^/ user.js: /' || true
echo
echo "== past cancellations sway logged (each one = a copy that vanished) =="
journalctl -b 0 --no-pager 2>/dev/null | grep -ic 'selection cancelled' | sed 's/^/ '\''selection cancelled'\'' events this boot: /'
echo
echo "Run '$0 test' to A/B an actual Firefox copy."