From 4bb1c06883b53c21805a276fb726cdc652f24c1e Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Thu, 29 Oct 2026 08:55:14 -0700 Subject: [PATCH] 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. --- scripts/clipboard-check.sh | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100755 scripts/clipboard-check.sh diff --git a/scripts/clipboard-check.sh b/scripts/clipboard-check.sh new file mode 100755 index 0000000..35e2957 --- /dev/null +++ b/scripts/clipboard-check.sh @@ -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."