Simplify clipboard watcher lifecycle with pdeathsig

Replace the three-layer process management (trap cleanup, PPID-polling
watchdog subshell, /proc-scanning reaper) with kernel-level lifetime
binding: each wl-paste watcher is spawned under setpriv --pdeathsig TERM,
so it dies with the shell no matter how the shell exits.

- Delete watch.sh: the shell now spawns both watchers directly as
  declarative Process commands instead of a wrapper script with traps
  and a 1s polling loop
- Delete init.sh: reaping stale watchers from a crashed pre-pdeathsig
  shell is a single pkill at startup
- Collapse capture.sh's dual stream/snapshot modes: wl-paste forwards
  arguments to its watch command, so the mime rides along as $1 instead
  of an env var, letting both modes share one emit_image function
- Turn the six-branch image mime chain into a loop

Net: two fewer files, three fewer runtime processes, and the watch-mode
capture tests now exercise the same argument protocol the shell uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-02 21:32:30 -07:00
co-authored by Claude Fable 5
parent 055ffaccd4
commit 60e0a2a0cd
5 changed files with 72 additions and 180 deletions
+17 -7
View File
@@ -19,8 +19,6 @@ Item {
property string historyPath: Quickshell.env("HOME") + "/.local/state/omarchy/clipboard-history.json"
property string captureScript: root.omarchyPath + "/shell/plugins/clipboard/capture.sh"
property string initScript: root.omarchyPath + "/shell/plugins/clipboard/init.sh"
property string watchScript: root.omarchyPath + "/shell/plugins/clipboard/watch.sh"
// Shares the [menu] surface tokens — themes that style the menu also
// style the clipboard. Selected-row colors composed in the
// singleton so consumers drop them straight into Rectangle bindings.
@@ -260,19 +258,22 @@ Item {
onFileChanged: reload()
}
// Reap watchers left behind by a previous shell instance, then start our
// own. The pdeathsig on the watchers makes the kernel kill them whenever
// the shell exits, however it exits, so no further lifecycle management.
Process {
id: initProc
command: [root.initScript, root.captureScript]
command: ["pkill", "-f", "wl-paste .*--watch .*/shell/plugins/clipboard/capture\\.sh"]
onExited: {
currentProc.command = [root.captureScript]
currentProc.running = true
watchProc.command = [root.watchScript, root.captureScript]
watchProc.running = true
textWatchProc.running = true
imageWatchProc.running = true
}
}
Process {
id: currentProc
command: [root.captureScript]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.addClipboardJson(text)
@@ -280,7 +281,16 @@ Item {
}
Process {
id: watchProc
id: textWatchProc
command: ["setpriv", "--pdeathsig", "TERM", "wl-paste", "--type", "text", "--watch", root.captureScript, "text"]
stdout: SplitParser {
onRead: function(data) { root.addClipboardJson(data) }
}
}
Process {
id: imageWatchProc
command: ["setpriv", "--pdeathsig", "TERM", "wl-paste", "--type", "image/png", "--watch", root.captureScript, "image/png"]
stdout: SplitParser {
onRead: function(data) { root.addClipboardJson(data) }
}
+20 -51
View File
@@ -1,5 +1,9 @@
#!/bin/bash
# Captures the current clipboard as a JSON entry on stdout. In watch mode,
# wl-paste invokes this with the payload on stdin and the mime as $1. Without
# arguments, it snapshots the current selection itself.
set -o pipefail
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy"
@@ -14,35 +18,10 @@ fi
emit_image() {
local mime="$1"
local ext="$2"
local tmp=""
local hash=""
local file=""
local ext tmp hash file
tmp=$(mktemp --tmpdir="$IMAGE_DIR" clipboard.XXXXXX) || return 0
if ! timeout 2s wl-paste --type "$mime" > "$tmp" 2>/dev/null || [[ ! -s $tmp ]]; then
rm -f "$tmp"
return 0
fi
hash=$(sha256sum "$tmp" | awk '{print $1}')
file="$IMAGE_DIR/$hash.$ext"
if [[ -e $file ]]; then
rm -f "$tmp"
else
mv "$tmp" "$file"
fi
jq -cn --arg mime "$mime" --arg path "$file" --arg captured_at "$(date +'%A %H:%M')" \
'{type:"image", mime:$mime, path:$path, capturedAt:$captured_at}'
}
emit_image_stream() {
local mime="$1"
local ext="$2"
local tmp=""
local hash=""
local file=""
ext=${mime#image/}
[[ $ext == jpeg ]] && ext=jpg
tmp=$(mktemp --tmpdir="$IMAGE_DIR" clipboard.XXXXXX) || return 0
cat >"$tmp"
@@ -63,32 +42,22 @@ emit_image_stream() {
'{type:"image", mime:$mime, path:$path, capturedAt:$captured_at}'
}
emit_text_stream() {
emit_text() {
jq -cRs 'select(length > 0) | {type:"text", text:.}'
}
case "${OMARCHY_CLIPBOARD_WATCH_MIME:-}" in
text) emit_text_stream; exit 0 ;;
image/png) emit_image_stream 'image/png' 'png'; exit 0 ;;
image/jpeg) emit_image_stream 'image/jpeg' 'jpg'; exit 0 ;;
image/webp) emit_image_stream 'image/webp' 'webp'; exit 0 ;;
image/gif) emit_image_stream 'image/gif' 'gif'; exit 0 ;;
image/bmp) emit_image_stream 'image/bmp' 'bmp'; exit 0 ;;
image/tiff) emit_image_stream 'image/tiff' 'tiff'; exit 0 ;;
case "${1:-}" in
text) emit_text; exit 0 ;;
image/*) emit_image "$1"; exit 0 ;;
esac
if grep -qx 'image/png' <<<"$types"; then
emit_image 'image/png' 'png'
elif grep -qx 'image/jpeg' <<<"$types"; then
emit_image 'image/jpeg' 'jpg'
elif grep -qx 'image/webp' <<<"$types"; then
emit_image 'image/webp' 'webp'
elif grep -qx 'image/gif' <<<"$types"; then
emit_image 'image/gif' 'gif'
elif grep -qx 'image/bmp' <<<"$types"; then
emit_image 'image/bmp' 'bmp'
elif grep -qx 'image/tiff' <<<"$types"; then
emit_image 'image/tiff' 'tiff'
elif grep -q '^text/' <<<"$types" || grep -qx 'UTF8_STRING' <<<"$types" || grep -qx 'STRING' <<<"$types"; then
wl-paste --type text --no-newline 2>/dev/null | jq -cRs 'select(length > 0) | {type:"text", text:.}'
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"
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
fi
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
current_script=${1:-}
[[ -n $current_script ]] || exit 0
is_clipboard_capture() {
local path=$1
[[ $path == */shell/plugins/clipboard/capture.sh ]]
}
watched_script_for_pid() {
local pid=$1
local -a args=()
local i
[[ -r /proc/$pid/cmdline ]] || return 1
mapfile -d '' -t args <"/proc/$pid/cmdline" || return 1
((${#args[@]} > 0)) || return 1
for ((i = 0; i < ${#args[@]}; i++)); do
if [[ ${args[i]} == "--watch" ]]; then
printf '%s\n' "${args[i + 1]:-}"
return 0
fi
done
return 1
}
for pid in $(pgrep -x wl-paste 2>/dev/null || true); do
watched_script=$(watched_script_for_pid "$pid" || true)
[[ -n $watched_script ]] || continue
is_clipboard_capture "$watched_script" || continue
if [[ $watched_script == $current_script || ! -e $watched_script ]]; then
kill "$pid" 2>/dev/null || true
fi
done
-46
View File
@@ -1,46 +0,0 @@
#!/bin/bash
set -o pipefail
capture_script=${1:-}
[[ -n $capture_script && -x $capture_script ]] || exit 1
owner_pid=$PPID
watchdog_pid=""
cleanup() {
local pid
if [[ -n $watchdog_pid ]]; then
kill "$watchdog_pid" 2>/dev/null || true
fi
for pid in $(jobs -p); do
[[ $pid == $watchdog_pid ]] && continue
kill "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
}
stop() {
cleanup
exit 0
}
trap cleanup EXIT
trap stop HUP INT TERM
(
while kill -0 "$owner_pid" 2>/dev/null; do
sleep 1
done
kill -TERM "$$" 2>/dev/null || true
) &
watchdog_pid=$!
OMARCHY_CLIPBOARD_WATCH_MIME=text wl-paste --type text --watch "$capture_script" 2>/dev/null &
OMARCHY_CLIPBOARD_WATCH_MIME=image/png wl-paste --type image/png --watch "$capture_script" 2>/dev/null &
wait