Keep clicking a notification working after a shell restart (#6636)

* Keep clicking a notification working after a shell restart

Notification actions lived only in the sending process: `-a` appended
`-A default=default`, so notify-send blocked on a D-Bus ActionInvoked signal and
the caller ran the command when it arrived. Nothing about that reached disk, so a
restored popup had no action to run and its sender stayed blocked forever.

Replace `-a` with `--exec <command>`, carried as an `omarchy-exec` hint into the
snapshot's `exec` role. It travels through the popup files and history, and the
shell runs it on click, so restored toasts behave exactly like live ones and the
sender exits immediately.

That drops the scaffolding whose only job was keeping a blocked sender alive: the
first-run invitations lose their `--show` re-entry and two transient units each,
omarchy-migrate-notify loses its transient service, and the screenshot,
recording, download, and taildrop toasts lose their wrapper subshells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep a failed toast from failing the work it announces

Moving these sends out of their backgrounded subshells put a fallible command
on the foreground path, where the `&` used to swallow its exit status. A
notification outage — including the shell restart this branch targets — now
propagates:

- taildrop's receiver dies under `set -e` mid-delivery
- omarchy-capture-screenshot reports failure for a screenshot it already saved
- a completed download exits before scheduling its thumbnail cleanup, leaking
  the mktemp file

Announcing is best-effort in all three: the work is already done by the time
the toast goes out.

Also drop the first-run sleep that spaced out the welcome and Wi-Fi toasts.
It compensated for the background notify-send processes this branch removes;
each send now returns only once the server has taken the toast, so sending in
order is enough to stack them newest-on-top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop tying the preview cleanup to the toast's expiry

The shell loads a notification thumbnail into memory when the toast appears and
never re-reads the file, so the preview only has to outlive that load. Deriving
the cleanup delay from the expiry was false precision, and it turned -t into a
variable for no reason: -t is already the helper's expiry setting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-09 12:30:47 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 99293aa05a
commit 5a58f79876
19 changed files with 220 additions and 226 deletions
+8 -3
View File
@@ -226,10 +226,15 @@ stop_screenrecording() {
# Generate a preview thumbnail from the first frame
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
-t 10000 --image "${preview:-$filename}" \
--exec "$(printf 'mpv %q' "$filename")"
# The shell loads the thumbnail into memory when the toast appears and never
# re-reads the file, so the preview only has to outlive that load -- not the
# toast. Clear it out of the recordings directory a moment later.
(
if [[ -n $(omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" -t 10000 --image "${preview:-$filename}" -a) ]]; then
mpv "$filename"
fi
sleep 2
rm -f "$preview"
) &
fi
+5 -10
View File
@@ -29,11 +29,6 @@ for arg in "$@"; do
done
set -- "${ARGS[@]}"
open_editor() {
local filepath="$1"
"$SCREENSHOT_EDITOR" "$filepath"
}
MODE="${1:-smart}"
PROCESSING="${2:-slurp}"
@@ -71,11 +66,11 @@ case "$PROCESSING" in
echo "$FILEPATH"
wl-copy --type image/png <"$FILEPATH"
(
if [[ -n $(omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" --image "$FILEPATH" -a) ]]; then
open_editor "$FILEPATH"
fi
) >/dev/null 2>&1 &
# Best-effort: the screenshot is already saved and on the clipboard, so a
# notification outage must not report the capture itself as failed.
omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" \
--image "$FILEPATH" \
--exec "$(printf '%q %q' "$SCREENSHOT_EDITOR" "$FILEPATH")" || true
;;
copy)
grim -g "$SELECTION" - | wl-copy --type image/png
+10 -4
View File
@@ -92,11 +92,17 @@ download_url() {
-vf "crop='min(iw,ih)':'min(iw,ih)',scale=256:256" -q:v 2 \
"$preview" -loglevel quiet 2>/dev/null || true
# Clicking the toast opens the video in mpv (-a blocks until clicked or timed out).
# Best-effort: the download already succeeded, and under `set -e` a failed
# toast would exit before the thumbnail cleanup below is ever scheduled.
omarchy-notification-send -g 󰄬 "Download complete" "$title" \
-t 10000 --image "${preview:-$filepath}" \
--exec "$(printf 'mpv %q' "$filepath")" || true
# The shell loads the thumbnail into memory when the toast appears and never
# re-reads the file, so the preview only has to outlive that load, not the
# toast.
(
if [[ -n $(omarchy-notification-send -g 󰄬 "Download complete" "$title" -t 10000 --image "${preview:-$filepath}" -a) ]]; then
mpv "$filepath"
fi
sleep 2
rm -f "$preview"
) &
else
+2 -4
View File
@@ -82,12 +82,10 @@ run_first_run_step "apply speaker tuning" \
bash "$OMARCHY_PATH/install/user/first-run/audio-tuning.sh"
omarchy-notification-wait || log_first_run "Timed out waiting for notification service; continuing"
# Each send returns only once the server has taken the toast, so sending in
# order is enough to stack them newest-on-top.
run_first_run_step "show welcome notification" \
bash "$OMARCHY_PATH/install/user/first-run/welcome.sh"
# The first-run notification scripts register action callbacks in background
# notify-send processes. Give the notification server a tick to ingest the
# welcome toast before queueing the Wi-Fi/update toasts.
sleep 0.3
run_first_run_step "show Wi-Fi/update notifications" \
bash "$OMARCHY_PATH/install/user/first-run/wifi.sh"
+4 -7
View File
@@ -26,8 +26,6 @@ else
message="Click to run $pending_count pending migrations."
fi
notify_command=$(printf 'if [[ -n $(omarchy-notification-send -u critical -g  "Pending Omarchy Migrations" %q -a) ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-migrate; fi' "$message")
# This runs from omarchy-migrate-notify.service after graphical-session.target,
# but the target can be reached before the shell has claimed
# org.freedesktop.Notifications. Without the wait the toast is sent into the
@@ -40,11 +38,10 @@ if update_in_progress; then
exit 0
fi
# A transient service rather than a scope, which would block here until the
# toast was answered and keep this oneshot activating for that whole time. The
# graphical slice ends an unanswered toast at logout.
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
systemd-run --user --collect --slice=background-graphical.slice --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
# The shell keeps the click command with the toast, so this oneshot can hand the
# invitation over and exit instead of staying activated until it is answered.
omarchy-notification-send -u critical -g  "Pending Omarchy Migrations" "$message" \
--exec "omarchy-launch-floating-terminal-with-presentation omarchy-migrate" && exit 0
# Reached when the notification could not be handed off, so fall back to telling
# the user in the terminal.
+15 -8
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# omarchy:summary=Send an Omarchy desktop notification
# omarchy:args=[-a] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]
# omarchy:args=[--exec <command>] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]
# omarchy:examples=omarchy notification send "Reminder" "5 minutes are up" -g 󰢌
set -euo pipefail
@@ -12,7 +12,7 @@ glyph=
urgency="low"
app_name="omarchy-action"
image=
click_action=0
exec_command=
args=()
parsed_option_args=0
@@ -54,9 +54,13 @@ parse_omarchy_option() {
parsed_option_args=2
return 0
;;
-a | --action)
click_action=1
parsed_option_args=1
--exec)
if (($# < 2)); then
echo "Missing value for $1" >&2
exit 1
fi
exec_command=$2
parsed_option_args=2
return 0
;;
esac
@@ -73,7 +77,7 @@ while (($# > 0)); do
done
if (($# < 1)); then
echo "Usage: omarchy-notification-send [-a] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]"
echo "Usage: omarchy-notification-send [--exec <command>] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]"
exit 1
fi
@@ -105,8 +109,11 @@ if [[ -n $image ]]; then
args+=("--hint=string:image-path:$image")
fi
if ((click_action)); then
args+=("-A" "default=default")
# The shell runs the click command itself, from a copy it keeps alongside the
# on-screen popup. A libnotify action would instead keep this process blocked
# until the click, and die unanswered whenever the shell restarts underneath it.
if [[ -n $exec_command ]]; then
args+=("--hint=string:omarchy-exec:$exec_command")
fi
if [[ -n $description ]]; then
+4 -6
View File
@@ -70,11 +70,9 @@ announce() {
;;
esac
# Clicking the notification opens the file, so this waits for the toast to
# go away. Callers background it to keep receiving in the meantime.
if [[ -n $(omarchy-notification-send "${args[@]}" -a) ]]; then
xdg-open "$path"
fi
# Announcing is best-effort: the file is already delivered, and under `set -e`
# a notification outage would otherwise kill the long-running receiver.
omarchy-notification-send "${args[@]}" --exec "$(printf 'xdg-open %q' "$path")" || true
}
deliver() {
@@ -82,7 +80,7 @@ deliver() {
while IFS= read -r staged; do
target=$(claim_path "$staged") || continue
announce "$target" &
announce "$target"
done < <(find "$staging" -mindepth 1 -maxdepth 1)
}