diff --git a/shell/plugins/clipboard/capture.sh b/shell/plugins/clipboard/capture.sh index d6ce2b28..a8f76fd2 100755 --- a/shell/plugins/clipboard/capture.sh +++ b/shell/plugins/clipboard/capture.sh @@ -10,25 +10,31 @@ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy" IMAGE_DIR="$STATE_DIR/clipboard-images" mkdir -p "$IMAGE_DIR" -types=$(wl-paste --list-types 2>/dev/null || true) +types=$(timeout 2s wl-paste --list-types 2>/dev/null || true) if [[ ${CLIPBOARD_STATE:-} == "sensitive" ]] || grep -qx 'x-kde-passwordManagerHint' <<<"$types"; then exit 0 fi +tmp= +trap 'rm -f -- "$tmp"' EXIT + +# The clipboard owner streams the copy and can stall without closing its end, so +# every read is bounded, and a copy cut off at the deadline is dropped, not kept. +read_copy() { + timeout 2s "$@" >"$tmp" 2>/dev/null && [[ -s $tmp ]] +} + emit_image() { local mime="$1" - local ext tmp hash file + local ext hash file + shift ext=${mime#image/} [[ $ext == jpeg ]] && ext=jpg tmp=$(mktemp --tmpdir="$IMAGE_DIR" clipboard.XXXXXX) || return 0 - cat >"$tmp" - if [[ ! -s $tmp ]]; then - rm -f "$tmp" - return 0 - fi + read_copy "$@" || return hash=$(sha256sum "$tmp" | awk '{print $1}') file="$IMAGE_DIR/$hash.$ext" @@ -43,6 +49,9 @@ emit_image() { } emit_text() { + tmp=$(mktemp --tmpdir="$STATE_DIR" clipboard.XXXXXX) || return 0 + read_copy "$@" || return + perl -MEncode=decode,FB_CROAK,LEAVE_SRC -MJSON::PP=encode_json -0777 -e ' my $raw = ; exit unless length $raw; @@ -86,21 +95,21 @@ emit_text() { } $text = decode("UTF-8", $raw) unless defined $text; print "{\"type\":\"text\",\"text\":", encode_json($text), "}\n"; - ' + ' <"$tmp" } case "${1:-}" in -text) emit_text; exit 0 ;; -image/*) emit_image "$1"; exit 0 ;; +text) emit_text cat; exit 0 ;; +image/*) emit_image "$1" cat; exit 0 ;; esac for mime in image/png image/jpeg image/webp image/gif image/bmp image/tiff; do if grep -qx "$mime" <<<"$types"; then - timeout 2s wl-paste --type "$mime" 2>/dev/null | emit_image "$mime" + emit_image "$mime" wl-paste --type "$mime" exit 0 fi done if grep -q '^text/' <<<"$types" || grep -qx 'UTF8_STRING' <<<"$types" || grep -qx 'STRING' <<<"$types"; then - wl-paste --type text --no-newline 2>/dev/null | emit_text + emit_text wl-paste --type text --no-newline fi diff --git a/test/shell.d/clipboard-test.sh b/test/shell.d/clipboard-test.sh index 5580a694..17415ef0 100644 --- a/test/shell.d/clipboard-test.sh +++ b/test/shell.d/clipboard-test.sh @@ -255,8 +255,14 @@ SH cat >"$TMPDIR/bin/wl-paste" <<'SH' #!/bin/bash if [[ $1 == "--list-types" ]]; then + [[ ${WL_PASTE_STALL:-} == "list-types" ]] && sleep 10 printf '%b' "${WL_PASTE_TYPES:-text/plain\n}" elif [[ $1 == "--type" && $2 == "text" ]]; then + [[ ${WL_PASTE_STALL:-} == "text" ]] && sleep 10 + if [[ ${WL_PASTE_STALL:-} == "text-midway" ]]; then + printf 'first half ' + sleep 10 + fi printf '%s' "${WL_PASTE_TEXT:-terminal copy}" fi SH @@ -292,6 +298,52 @@ capture_output=$(printf 'closing app copy' | WL_PASTE_TEXT="stale read" XDG_RUNT [[ $capture_output == '{"type":"text","text":"closing app copy"}' ]] || fail "clipboard capture records watched text from stdin" pass "clipboard capture records watched text from stdin" +if capture_output=$(printf 'watched copy after stalled types' | WL_PASTE_STALL="list-types" XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh" text); then + capture_status=0 +else + capture_status=$? +fi +[[ $capture_status -eq 0 ]] || fail "clipboard watched text survives a stalled type query" "capture exited with status $capture_status" +[[ $capture_output == '{"type":"text","text":"watched copy after stalled types"}' ]] || fail "clipboard watched text remains intact after a stalled type query" +pass "clipboard watched text survives a stalled type query" +pass "clipboard watched text remains intact after a stalled type query" + +if capture_output=$(WL_PASTE_STALL="text" XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh"); then + capture_status=0 +else + capture_status=$? +fi +[[ $capture_status -eq 124 ]] || fail "clipboard snapshot text read uses its internal timeout" "capture exited with status $capture_status" +[[ -z $capture_output ]] || fail "clipboard snapshot timeout emits no partial entry" "actual: $capture_output" +pass "clipboard snapshot text read uses its internal timeout" +pass "clipboard snapshot timeout emits no partial entry" + +if capture_output=$(WL_PASTE_STALL="text-midway" XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh"); then + capture_status=0 +else + capture_status=$? +fi +[[ $capture_status -eq 124 ]] || fail "clipboard snapshot text read stalled midway uses its internal timeout" "capture exited with status $capture_status" +[[ -z $capture_output ]] || fail "clipboard snapshot stalled midway emits no partial entry" "actual: $capture_output" +pass "clipboard snapshot text read stalled midway uses its internal timeout" +pass "clipboard snapshot stalled midway emits no partial entry" + +for watched_mime in text image/png; do + if capture_output=$({ printf 'copy whose owner never closes'; sleep 5; } | XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" timeout --preserve-status 4s "$ROOT/shell/plugins/clipboard/capture.sh" "$watched_mime"); then + capture_status=0 + else + capture_status=$? + fi + [[ $capture_status -eq 0 ]] || fail "clipboard watched $watched_mime read is bounded when the owner stalls" "capture exited with status $capture_status" + [[ -z $capture_output ]] || fail "clipboard watched $watched_mime cut off by a stalled owner emits no entry" "actual: $capture_output" + pass "clipboard watched $watched_mime read is bounded when the owner stalls" + pass "clipboard watched $watched_mime cut off by a stalled owner emits no entry" +done + +leftover=$(find "$TMPDIR/state/omarchy" -name 'clipboard.*' -print) +[[ -z $leftover ]] || fail "clipboard capture removes copies it dropped" "left: $leftover" +pass "clipboard capture removes copies it dropped" + capture_output=$(printf '%s' 'UTF-16 clipboard - fixed' | iconv -f UTF-8 -t UTF-16LE | XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" "$ROOT/shell/plugins/clipboard/capture.sh" text) [[ $capture_output == '{"type":"text","text":"UTF-16 clipboard - fixed"}' ]] || fail "clipboard capture decodes UTF-16LE text" pass "clipboard capture decodes UTF-16LE text"