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:
co-authored by
Claude Opus 5
parent
99293aa05a
commit
5a58f79876
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
show_invitation() {
|
||||
if [[ -n $(omarchy-notification-send -u critical -g "Install Dictation with Voxtype" "Click to install voice dictation for Omarchy." -a) ]]; then
|
||||
# Launch the installer in its own transient unit so this invitation service
|
||||
# can exit right after the click. If it stayed alive for the life of the
|
||||
# install terminal, the installer's omarchy-restart-shell would re-trigger
|
||||
# this still-running *-invitation unit and pop the toast a second time.
|
||||
# KillMode=process because the launcher's setsid forks and returns, so the
|
||||
# unit's main process exits within milliseconds. The default control-group
|
||||
# kill would take the terminal down with it before it ever appears.
|
||||
systemd-run --user --collect --quiet -p KillMode=process \
|
||||
--unit=omarchy-voxtype-install \
|
||||
omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ ${1:-} == "--show" ]]; then
|
||||
show_invitation
|
||||
elif omarchy-done ensure voxtype-install-invitation; then
|
||||
# Keep the notification action alive after the update terminal closes.
|
||||
systemd-run --user --collect --quiet --service-type=exec \
|
||||
--unit=omarchy-voxtype-install-invitation \
|
||||
bash "$0" --show
|
||||
if omarchy-done ensure voxtype-install-invitation; then
|
||||
omarchy-notification-send -u critical -g "Install Dictation with Voxtype" \
|
||||
"Click to install voice dictation for Omarchy." \
|
||||
--exec "omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"
|
||||
fi
|
||||
|
||||
@@ -2,29 +2,11 @@
|
||||
|
||||
set -e
|
||||
|
||||
show_invitation() {
|
||||
if [[ -n $(omarchy-notification-send -u critical -g "Setup Fingerprint Reader" "Enable sudo and unlocking with your fingerprint." -a) ]]; then
|
||||
# Launch the setup in its own transient unit so this invitation service can
|
||||
# exit right after the click. If it stayed alive for the life of the setup
|
||||
# terminal, the setup's omarchy-restart-shell would re-trigger this
|
||||
# still-running *-invitation unit and pop the toast a second time.
|
||||
# KillMode=process because the launcher's setsid forks and returns, so the
|
||||
# unit's main process exits within milliseconds. The default control-group
|
||||
# kill would take the terminal down with it before it ever appears.
|
||||
systemd-run --user --collect --quiet -p KillMode=process \
|
||||
--unit=omarchy-setup-security-fingerprint \
|
||||
omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ ${1:-} == "--show" ]]; then
|
||||
show_invitation
|
||||
# Only invite when there's a reader to use and it isn't set up yet (the lock
|
||||
# PAM file is the last thing the setup writes on success).
|
||||
elif omarchy-hw-fingerprint && [[ ! -f /etc/pam.d/omarchy-lock-fingerprint ]] &&
|
||||
if omarchy-hw-fingerprint && [[ ! -f /etc/pam.d/omarchy-lock-fingerprint ]] &&
|
||||
omarchy-done ensure fingerprint-setup-invitation; then
|
||||
# Keep the notification action alive after the update terminal closes.
|
||||
systemd-run --user --collect --quiet --service-type=exec \
|
||||
--unit=omarchy-fingerprint-setup-invitation \
|
||||
bash "$0" --show
|
||||
omarchy-notification-send -u critical -g "Setup Fingerprint Reader" \
|
||||
"Enable sudo and unlocking with your fingerprint." \
|
||||
--exec "omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
(
|
||||
if [[ -n $(omarchy-notification-send -u critical -g "Learn Keybindings" "Super + K for cheatsheet.\nSuper + Space for application launcher.\nSuper + Alt + Space for Omarchy Menu." -a) ]]; then
|
||||
omarchy-menu-keybindings
|
||||
fi
|
||||
) >/dev/null 2>&1 &
|
||||
omarchy-notification-send -u critical -g "Learn Keybindings" \
|
||||
"Super + K for cheatsheet.\nSuper + Space for application launcher.\nSuper + Alt + Space for Omarchy Menu." \
|
||||
--exec omarchy-menu-keybindings
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
notify_update() {
|
||||
(
|
||||
if [[ -n $(omarchy-notification-send -u critical -g "Update System" "$1" -a) ]]; then
|
||||
omarchy-launch-floating-terminal-with-presentation omarchy-update
|
||||
fi
|
||||
) >/dev/null 2>&1 &
|
||||
omarchy-notification-send -u critical -g "Update System" "$1" \
|
||||
--exec "omarchy-launch-floating-terminal-with-presentation omarchy-update"
|
||||
}
|
||||
|
||||
notify_wifi() {
|
||||
(
|
||||
if [[ -n $(omarchy-notification-send -u critical -g "Setup Wi-Fi" "Click to configure the wireless network." -a) ]]; then
|
||||
omarchy-shell shell toggle omarchy.network
|
||||
fi
|
||||
) >/dev/null 2>&1 &
|
||||
omarchy-notification-send -u critical -g "Setup Wi-Fi" "Click to configure the wireless network." \
|
||||
--exec "omarchy-shell shell toggle omarchy.network"
|
||||
}
|
||||
|
||||
if ! ping -c3 -W1 1.1.1.1 >/dev/null 2>&1; then
|
||||
# Newest stacks on top, and Wi-Fi is what you need first, so send it last.
|
||||
notify_update "When you have internet, click to update the system."
|
||||
# Both toasts are sent from background subshells, so let the update one
|
||||
# register before queueing Wi-Fi. Newest stacks on top, and Wi-Fi is what
|
||||
# you need first.
|
||||
sleep 0.3
|
||||
notify_wifi
|
||||
else
|
||||
notify_update "Click to update the system."
|
||||
|
||||
@@ -42,17 +42,31 @@ function isEphemeralApp(appName) {
|
||||
return name === "notify-send" || name === "omarchy-action"
|
||||
}
|
||||
|
||||
function glyphFromHints(hints) {
|
||||
function stringHint(hints, name) {
|
||||
try {
|
||||
if (hints) {
|
||||
var glyph = hints["omarchy-glyph"]
|
||||
if (glyph !== undefined && glyph !== null) return String(glyph)
|
||||
var value = hints[name]
|
||||
if (value !== undefined && value !== null) return String(value)
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function glyphFromHints(hints) {
|
||||
return stringHint(hints, "omarchy-glyph")
|
||||
}
|
||||
|
||||
// Shell command to run when the card is clicked, sent by
|
||||
// omarchy-notification-send --exec. Carrying the action as data means it
|
||||
// travels with the popup through the persistence files, so a toast restored
|
||||
// after a shell restart clicks through exactly like a live one. A libnotify
|
||||
// action can't: its sender is still waiting on an id from a server generation
|
||||
// that no longer exists.
|
||||
function execFromHints(hints) {
|
||||
return stringHint(hints, "omarchy-exec")
|
||||
}
|
||||
|
||||
function shouldRenderCompactGlyph(glyph, iconSource, singleLineToast) {
|
||||
return String(glyph || "").length > 0 && String(iconSource || "").length === 0 && !!singleLineToast
|
||||
}
|
||||
@@ -71,6 +85,7 @@ function snapshotOf(notification, timestamp) {
|
||||
body: n.body || "",
|
||||
image: n.image || "",
|
||||
glyph: glyphFromHints(n.hints),
|
||||
exec: execFromHints(n.hints),
|
||||
urgency: n.urgency,
|
||||
expireTimeout: expireTimeout,
|
||||
timestamp: timestamp === undefined ? Date.now() : timestamp
|
||||
@@ -88,6 +103,7 @@ function historyEntry(value, normalUrgency) {
|
||||
body: e.body || "",
|
||||
image: e.image || "",
|
||||
glyph: e.glyph || "",
|
||||
exec: e.exec || "",
|
||||
urgency: typeof e.urgency === "number" ? e.urgency : normalUrgency,
|
||||
expireTimeout: 0,
|
||||
timestamp: e.timestamp || 0
|
||||
@@ -187,6 +203,7 @@ function dumpRows(rows) {
|
||||
body: r.body,
|
||||
image: r.image,
|
||||
glyph: r.glyph || "",
|
||||
exec: r.exec || "",
|
||||
urgency: r.urgency,
|
||||
timestamp: r.timestamp
|
||||
})
|
||||
@@ -299,7 +316,9 @@ if (typeof module !== "undefined") {
|
||||
summaryStartsWithGlyph: summaryStartsWithGlyph,
|
||||
shouldBypassDnd: shouldBypassDnd,
|
||||
isEphemeralApp: isEphemeralApp,
|
||||
stringHint: stringHint,
|
||||
glyphFromHints: glyphFromHints,
|
||||
execFromHints: execFromHints,
|
||||
shouldRenderCompactGlyph: shouldRenderCompactGlyph,
|
||||
snapshotOf: snapshotOf,
|
||||
historyEntry: historyEntry,
|
||||
|
||||
@@ -299,6 +299,7 @@ Item {
|
||||
body: row.body,
|
||||
image: row.image,
|
||||
glyph: row.glyph || "",
|
||||
exec: row.exec || "",
|
||||
urgency: row.urgency,
|
||||
expireTimeout: row.expireTimeout || 0,
|
||||
timestamp: row.timestamp
|
||||
@@ -390,6 +391,7 @@ Item {
|
||||
body: "",
|
||||
image: "",
|
||||
glyph: "",
|
||||
exec: "",
|
||||
urgency: NotificationUrgency.Low,
|
||||
expireTimeout: 0,
|
||||
timestamp: Date.now()
|
||||
@@ -438,13 +440,22 @@ Item {
|
||||
scheduleHistorySave()
|
||||
}
|
||||
|
||||
// Invoke the libnotify "default" action on the popup's underlying
|
||||
// notification, if it has one, then dismiss. Clients register the default
|
||||
// action with the canonical identifier "default"; e.g. screenshot toasts
|
||||
// use `notify-send -A default=Edit ...` so click-the-card opens the editor.
|
||||
// Run the popup's click action, then dismiss. Omarchy's own toasts carry the
|
||||
// action as a command in the `exec` role (see execFromHints), which the
|
||||
// persistence files preserve, so restored toasts stay clickable. Third-party
|
||||
// clients register a libnotify action under the canonical identifier
|
||||
// "default" instead; that one only works while the sender is still live.
|
||||
function invokePopupDefault(index) {
|
||||
if (index < 0 || index >= popupModel.count) return
|
||||
var entry = popupModel.get(index)
|
||||
var command = entry ? String(entry.exec || "") : ""
|
||||
if (command) {
|
||||
// Detached so the launched command outlives the shell process, which the
|
||||
// installer toasts depend on: they restart the shell as their first act.
|
||||
Util.execDetached(command)
|
||||
dismissPopup(index)
|
||||
return
|
||||
}
|
||||
// Restored rows have no live actions, and looking up liveRefs by their
|
||||
// old-generation id could fire an unrelated fresh notification's action.
|
||||
var ref = entry && !isRestoredRow(entry) ? liveRefs[entry.originalId] : null
|
||||
@@ -806,6 +817,7 @@ Item {
|
||||
body: r.body,
|
||||
image: r.image,
|
||||
glyph: r.glyph || "",
|
||||
exec: r.exec || "",
|
||||
urgency: r.urgency,
|
||||
expireTimeout: r.expireTimeout || 0,
|
||||
timestamp: r.timestamp
|
||||
|
||||
@@ -31,27 +31,18 @@ chmod +x "$test_bin/omarchy-hw-fingerprint"
|
||||
cat >"$test_bin/omarchy-notification-send" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo notification >>"$TEST_LOG"
|
||||
echo action
|
||||
while (($# > 0)); do
|
||||
[[ $1 == "--exec" ]] && echo "exec:$2" >>"$TEST_LOG"
|
||||
shift
|
||||
done
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-notification-send"
|
||||
|
||||
cat >"$test_bin/omarchy-launch-floating-terminal-with-presentation" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo launch >>"$TEST_LOG"
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-launch-floating-terminal-with-presentation"
|
||||
|
||||
# The shell runs the click command, so the invitation must not need a unit of its
|
||||
# own to keep a blocked sender alive until the toast is answered.
|
||||
cat >"$test_bin/systemd-run" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "systemd-run:$*" >>"$TEST_LOG"
|
||||
while (($# > 0)); do
|
||||
case $1 in
|
||||
-p) shift 2 ;;
|
||||
-*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
exec "$@"
|
||||
EOF
|
||||
chmod +x "$test_bin/systemd-run"
|
||||
|
||||
@@ -70,17 +61,13 @@ run_invitation_hook
|
||||
|
||||
[[ -f $test_home/.local/state/omarchy/done/fingerprint-setup-invitation ]] || fail "fingerprint invitation records completion"
|
||||
[[ -f $hook_path ]] || fail "fingerprint invitation keeps its hook installed"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "fingerprint invitation uses durable user services"
|
||||
grep -q -- '--user --collect --quiet --service-type=exec --unit=omarchy-fingerprint-setup-invitation' "$log_file" || fail "fingerprint invitation configures its user service"
|
||||
# KillMode=process keeps the launcher's setsid child alive once the short-lived
|
||||
# main process exits, otherwise the setup terminal never appears.
|
||||
grep -q -- '--user --collect --quiet -p KillMode=process --unit=omarchy-setup-security-fingerprint ' "$log_file" || fail "fingerprint invitation outlives its launcher unit"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "fingerprint invitation sends one notification"
|
||||
[[ $(grep -c '^launch$' "$log_file") -eq 1 ]] || fail "fingerprint invitation handles the notification action"
|
||||
grep -qx 'exec:omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint' "$log_file" ||
|
||||
fail "fingerprint invitation attaches the setup to the notification"
|
||||
grep -q '^systemd-run:' "$log_file" && fail "fingerprint invitation needs no unit to hold an unanswered toast"
|
||||
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" TEST_HW_MARKER="$hw_marker" bash "$hook_path"
|
||||
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "completed fingerprint invitation does not schedule again"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "completed fingerprint invitation hook does not notify again"
|
||||
|
||||
pass "fingerprint invitation waits for a reader and only runs once"
|
||||
|
||||
@@ -22,23 +22,6 @@ fi
|
||||
SH
|
||||
chmod +x "$stub_bin/omarchy-migrate"
|
||||
|
||||
cat >"$stub_bin/systemd-run" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ ${OMARCHY_TEST_SYSTEMD_RUN:-run} == "fail" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command=${!#}
|
||||
|
||||
# --scope blocks on the command, a transient service detaches it.
|
||||
if [[ " $* " == *" --scope "* ]]; then
|
||||
bash -c "$command"
|
||||
else
|
||||
setsid bash -c "$command" >/dev/null 2>&1 &
|
||||
fi
|
||||
SH
|
||||
chmod +x "$stub_bin/systemd-run"
|
||||
|
||||
# Waiting for the notification server is the notifier's one long pause, so it is
|
||||
# also where an update can start underneath it. Stand one up from inside the
|
||||
# wait to prove the notifier re-checks afterwards instead of sending a toast it
|
||||
@@ -67,18 +50,8 @@ chmod +x "$stub_bin/omarchy-notification-wait"
|
||||
|
||||
cat >"$stub_bin/omarchy-notification-send" <<'SH'
|
||||
#!/bin/bash
|
||||
# Written whole so a reader polling for the file never sees half the arguments.
|
||||
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS.partial"
|
||||
mv "$OMARCHY_TEST_NOTIFY_ARGS.partial" "$OMARCHY_TEST_NOTIFY_ARGS"
|
||||
|
||||
# Stands in for a toast nobody has answered yet, until the test releases it.
|
||||
if [[ -n ${OMARCHY_TEST_NOTIFY_HOLD:-} ]]; then
|
||||
for _ in {1..200}; do
|
||||
[[ -e $OMARCHY_TEST_NOTIFY_HOLD ]] || break
|
||||
sleep 0.05
|
||||
done
|
||||
: >"$OMARCHY_TEST_NOTIFY_ANSWERED"
|
||||
fi
|
||||
[[ ${OMARCHY_TEST_NOTIFY_SEND:-send} == "fail" ]] && exit 1
|
||||
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"
|
||||
SH
|
||||
chmod +x "$stub_bin/omarchy-notification-send"
|
||||
|
||||
@@ -91,21 +64,14 @@ run_notify() {
|
||||
XDG_RUNTIME_DIR="$runtime_dir" \
|
||||
OMARCHY_TEST_PENDING_MIGRATIONS="$1" \
|
||||
OMARCHY_TEST_NOTIFY_ARGS="$test_tmp/notify-args" \
|
||||
OMARCHY_TEST_SYSTEMD_RUN="${2:-run}" \
|
||||
OMARCHY_TEST_NOTIFY_SEND="${2:-send}" \
|
||||
OMARCHY_TEST_LOCK_DURING_WAIT="${OMARCHY_TEST_LOCK_DURING_WAIT:-0}" \
|
||||
OMARCHY_TEST_LOCK_HOLDER_PID="$test_tmp/lock-holder-pid" \
|
||||
OMARCHY_TEST_NOTIFY_HOLD="${OMARCHY_TEST_NOTIFY_HOLD:-}" \
|
||||
OMARCHY_TEST_NOTIFY_ANSWERED="$test_tmp/notify-answered" \
|
||||
"$ROOT/bin/omarchy-migrate-notify"
|
||||
}
|
||||
|
||||
# The notification outlives the notifier, so its arguments land after it exits.
|
||||
wait_for_notify_args() {
|
||||
for _ in {1..200}; do
|
||||
[[ -s $test_tmp/notify-args ]] && return 0
|
||||
sleep 0.05
|
||||
done
|
||||
return 1
|
||||
notify_args_written() {
|
||||
[[ -s $test_tmp/notify-args ]]
|
||||
}
|
||||
|
||||
run_notify 0 >"$test_tmp/not-pending.out" 2>"$test_tmp/not-pending.err"
|
||||
@@ -119,7 +85,7 @@ grep -q '200-migration.sh' "$test_tmp/pending.err" || fail "migration notifier l
|
||||
pass "migration notifier reports pending migrations"
|
||||
|
||||
run_notify 1 >"$test_tmp/notified.out" 2>"$test_tmp/notified.err"
|
||||
wait_for_notify_args || fail "migration notifier sends a notification for pending migrations"
|
||||
notify_args_written || fail "migration notifier sends a notification for pending migrations"
|
||||
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null || fail "migration notifier uses pending migrations title"
|
||||
grep -Fx 'Click to run 1 pending migration.' "$test_tmp/notify-args" >/dev/null || fail "migration notifier describes the pending migration"
|
||||
grep -Fx '' "$test_tmp/notify-args" >/dev/null || fail "migration notifier includes the large-slot glyph"
|
||||
@@ -143,7 +109,7 @@ pass "migration notifier stays quiet while omarchy update holds its lock"
|
||||
exec {update_lock_fd}>&-
|
||||
|
||||
run_notify 1 >/dev/null 2>&1
|
||||
wait_for_notify_args &&
|
||||
notify_args_written &&
|
||||
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null ||
|
||||
fail "migration notifier resumes notifying once the update lock is released"
|
||||
pass "migration notifier resumes notifying after the update releases its lock"
|
||||
@@ -172,20 +138,22 @@ exec {foreign_lock_fd}>"$foreign_lock"
|
||||
flock -n "$foreign_lock_fd" || fail "test could not hold the foreign update lock"
|
||||
|
||||
run_notify 1 >/dev/null 2>&1
|
||||
wait_for_notify_args &&
|
||||
notify_args_written &&
|
||||
grep -Fx 'Pending Omarchy Migrations' "$test_tmp/notify-args" >/dev/null ||
|
||||
fail "migration notifier ignores update locks outside its own runtime directory"
|
||||
pass "migration notifier ignores update locks outside its own runtime directory"
|
||||
|
||||
exec {foreign_lock_fd}>&-
|
||||
|
||||
# The notifier is a Type=oneshot with no start timeout, so blocking on the toast
|
||||
# leaves it activating until the user answers.
|
||||
rm -f "$test_tmp/notify-args" "$test_tmp/notify-answered"
|
||||
: >"$test_tmp/notify-hold"
|
||||
OMARCHY_TEST_NOTIFY_HOLD="$test_tmp/notify-hold" run_notify 1 >/dev/null 2>&1
|
||||
wait_for_notify_args || fail "migration notifier sends the notification it detaches"
|
||||
[[ ! -e $test_tmp/notify-answered ]] ||
|
||||
fail "migration notifier waited for the toast to be answered before exiting"
|
||||
rm -f "$test_tmp/notify-hold"
|
||||
pass "migration notifier exits while the toast is still unanswered"
|
||||
# The notifier is a Type=oneshot with no start timeout, so it must not stay
|
||||
# activating until the toast is answered. Handing the click command to the shell
|
||||
# is what lets it exit immediately -- and what keeps the toast working after the
|
||||
# shell restart an update performs.
|
||||
rm -f "$test_tmp/notify-args"
|
||||
run_notify 1 >/dev/null 2>&1
|
||||
notify_args_written || fail "migration notifier sends the notification before exiting"
|
||||
grep -Fx -- '--exec' "$test_tmp/notify-args" >/dev/null ||
|
||||
fail "migration notifier attaches the click command to the toast"
|
||||
grep -Fx 'omarchy-launch-floating-terminal-with-presentation omarchy-migrate' "$test_tmp/notify-args" >/dev/null ||
|
||||
fail "migration notifier points the click command at omarchy-migrate"
|
||||
pass "migration notifier lets the shell own the click instead of waiting for it"
|
||||
|
||||
@@ -17,7 +17,8 @@ printf '%s\n' \
|
||||
chmod +x "$stub"
|
||||
|
||||
OMARCHY_TEST_NOTIFY_ARGS="$args_file" PATH="$tmpdir:$ROOT/bin:$PATH" \
|
||||
omarchy-notification-send --app-name custom-app -g K -u critical --image /tmp/image.png "Learn Keybindings" "Body" -a
|
||||
omarchy-notification-send --app-name custom-app -g K -u critical --image /tmp/image.png \
|
||||
--exec "omarchy-menu-keybindings 'a b'" "Learn Keybindings" "Body"
|
||||
|
||||
mapfile -t args <"$args_file"
|
||||
|
||||
@@ -27,8 +28,24 @@ mapfile -t args <"$args_file"
|
||||
[[ ${args[3]} == "critical" ]] || fail "notification wrapper uses custom urgency"
|
||||
[[ ${args[4]} == "--hint=string:omarchy-glyph:K" ]] || fail "notification wrapper converts glyph to hint"
|
||||
[[ ${args[5]} == "--hint=string:image-path:/tmp/image.png" ]] || fail "notification wrapper converts image to hint"
|
||||
[[ ${args[6]} == "-A" ]] || fail "notification wrapper passes default action flag"
|
||||
[[ ${args[7]} == "default=default" ]] || fail "notification wrapper enables default action"
|
||||
[[ ${args[8]} == "Learn Keybindings" ]] || fail "notification wrapper preserves headline"
|
||||
[[ ${args[9]} == "Body" ]] || fail "notification wrapper preserves description"
|
||||
pass "notification wrapper supports app, glyph, urgency, image, and action options"
|
||||
[[ ${args[6]} == "--hint=string:omarchy-exec:omarchy-menu-keybindings 'a b'" ]] || fail "notification wrapper converts exec to hint"
|
||||
[[ ${args[7]} == "Learn Keybindings" ]] || fail "notification wrapper preserves headline"
|
||||
[[ ${args[8]} == "Body" ]] || fail "notification wrapper preserves description"
|
||||
pass "notification wrapper supports app, glyph, urgency, image, and exec options"
|
||||
|
||||
# The shell runs the click command itself, so nothing may block the sender on a
|
||||
# libnotify action round-trip.
|
||||
grep -q -- "-A" "$args_file" && fail "notification wrapper must not register a libnotify action"
|
||||
|
||||
: >"$args_file"
|
||||
OMARCHY_TEST_NOTIFY_ARGS="$args_file" PATH="$tmpdir:$ROOT/bin:$PATH" \
|
||||
omarchy-notification-send "Plain" >/dev/null
|
||||
|
||||
grep -q "omarchy-exec" "$args_file" && fail "notification wrapper adds no exec hint without --exec"
|
||||
pass "notification wrapper omits the exec hint when no command is given"
|
||||
|
||||
if OMARCHY_TEST_NOTIFY_ARGS="$args_file" PATH="$tmpdir:$ROOT/bin:$PATH" \
|
||||
omarchy-notification-send "Headline" --exec 2>/dev/null; then
|
||||
fail "notification wrapper rejects --exec without a command"
|
||||
fi
|
||||
pass "notification wrapper rejects --exec without a command"
|
||||
|
||||
@@ -255,6 +255,43 @@ assertEqual(
|
||||
'notifications omit the deadline field until a restore sets it'
|
||||
)
|
||||
|
||||
// A click action carried as a command is the only kind that survives a shell
|
||||
// restart: a libnotify action leaves its sender waiting on an id from a server
|
||||
// generation that no longer exists.
|
||||
assertEqual(
|
||||
notifications.snapshotOf({ id: 3, hints: { 'omarchy-exec': 'omarchy-menu-keybindings' } }, 1).exec,
|
||||
'omarchy-menu-keybindings',
|
||||
'notifications capture the click command from the exec hint'
|
||||
)
|
||||
assertEqual(
|
||||
notifications.snapshotOf({ id: 3, hints: { 'omarchy-glyph': '!' } }, 1).exec,
|
||||
'',
|
||||
'notifications leave the click command empty without an exec hint'
|
||||
)
|
||||
assertEqual(
|
||||
notifications.popupEntry(
|
||||
JSON.parse(notifications.serializePopup({ id: 1, originalId: 1, timestamp: 5, exec: "mpv '/tmp/a b.mp4'" }, 1)),
|
||||
1
|
||||
).exec,
|
||||
"mpv '/tmp/a b.mp4'",
|
||||
'notifications round-trip the click command through popup files'
|
||||
)
|
||||
assertEqual(
|
||||
notifications.popupEntry({ id: 1, originalId: 1, timestamp: 5 }, 1).exec,
|
||||
'',
|
||||
'notifications restore an empty click command for popups without one'
|
||||
)
|
||||
assertEqual(
|
||||
notifications.historyEntry({ id: 1, exec: 'xdg-open /tmp/received' }, 1).exec,
|
||||
'xdg-open /tmp/received',
|
||||
'notifications keep the click command on history rows'
|
||||
)
|
||||
assertEqual(
|
||||
notifications.dumpRows([{ id: 1, exec: 'xdg-open /tmp/received' }])[0].exec,
|
||||
'xdg-open /tmp/received',
|
||||
'notifications write the click command back out with history'
|
||||
)
|
||||
|
||||
assertEqual(notifications.imageExtension('/tmp/screenshot.PNG'), 'png', 'notifications normalize image extensions')
|
||||
assertEqual(notifications.imageExtension('/tmp/no-extension'), 'png', 'notifications default missing image extension')
|
||||
assertEqual(notifications.imageExtension('/tmp/archive.reallylong'), 'png', 'notifications reject suspicious image extensions')
|
||||
@@ -304,4 +341,16 @@ assert(
|
||||
/awk 1 \\"\$1\\"\/\*\.json/.test(serviceQml),
|
||||
'notifications service delimits every popup file during restore'
|
||||
)
|
||||
assert(
|
||||
/var command = entry \? String\(entry\.exec \|\| ""\) : ""[\s\S]{0,300}?Util\.execDetached\(command\)/.test(serviceQml),
|
||||
'notifications service runs the popup click command itself instead of a libnotify action'
|
||||
)
|
||||
assert(
|
||||
/exec: row\.exec \|\| ""/.test(serviceQml),
|
||||
'notifications service carries the click command between models'
|
||||
)
|
||||
assert(
|
||||
/exec: r\.exec \|\| ""/.test(serviceQml),
|
||||
'notifications service saves the click command with history'
|
||||
)
|
||||
JS
|
||||
|
||||
@@ -25,14 +25,6 @@ SH
|
||||
cat >"$WORKDIR/bin/omarchy-notification-send" <<SH
|
||||
#!/bin/bash
|
||||
printf '%s\n' "\$*" >>"$WORKDIR/notifications"
|
||||
# Only the photo notification gets clicked.
|
||||
[[ \$* == *photo.png* ]] && echo default
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$WORKDIR/bin/xdg-open" <<SH
|
||||
#!/bin/bash
|
||||
printf '%s\n' "\$1" >>"$WORKDIR/opened"
|
||||
SH
|
||||
|
||||
chmod +x "$WORKDIR/bin/"*
|
||||
@@ -68,9 +60,14 @@ grep -q "^Received notes with space.pdf .* -g " <<<"$notifications" ||
|
||||
fail "taildrop receive announces other files with a glyph" "$notifications"
|
||||
pass "taildrop receive announces other files with a glyph"
|
||||
|
||||
grep -qxF "$downloads/photo.png" "$WORKDIR/opened" ||
|
||||
fail "taildrop receive opens a clicked file" "$(cat "$WORKDIR/opened" 2>/dev/null)"
|
||||
pass "taildrop receive opens a clicked file"
|
||||
# The shell keeps the click command with the toast, so receiving does not have
|
||||
# to sit blocked on an answer -- and the toast still opens the file after a shell
|
||||
# restart. Names with spaces have to arrive quoted for the shell to run them.
|
||||
grep -qF -- "--exec xdg-open $downloads/photo.png" <<<"$notifications" ||
|
||||
fail "taildrop receive attaches the open command to the notification" "$notifications"
|
||||
grep -qF -- "--exec xdg-open $(printf %q "$downloads/notes with space.pdf")" <<<"$notifications" ||
|
||||
fail "taildrop receive quotes spaced names in the open command" "$notifications"
|
||||
pass "taildrop receive lets a click open the received file"
|
||||
|
||||
grep -q "unrelated.txt" <<<"$notifications" &&
|
||||
fail "taildrop receive leaves the rest of the downloads directory alone" "$notifications"
|
||||
|
||||
@@ -18,27 +18,18 @@ mkdir -p "$(dirname "$hook_path")"
|
||||
cat >"$test_bin/omarchy-notification-send" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo notification >>"$TEST_LOG"
|
||||
echo action
|
||||
while (($# > 0)); do
|
||||
[[ $1 == "--exec" ]] && echo "exec:$2" >>"$TEST_LOG"
|
||||
shift
|
||||
done
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-notification-send"
|
||||
|
||||
cat >"$test_bin/omarchy-launch-floating-terminal-with-presentation" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo launch >>"$TEST_LOG"
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-launch-floating-terminal-with-presentation"
|
||||
|
||||
# The shell runs the click command, so the invitation must not need a unit of its
|
||||
# own to keep a blocked sender alive until the toast is answered.
|
||||
cat >"$test_bin/systemd-run" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "systemd-run:$*" >>"$TEST_LOG"
|
||||
while (($# > 0)); do
|
||||
case $1 in
|
||||
-p) shift 2 ;;
|
||||
-*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
exec "$@"
|
||||
EOF
|
||||
chmod +x "$test_bin/systemd-run"
|
||||
|
||||
@@ -51,19 +42,14 @@ run_invitation_hook
|
||||
|
||||
[[ -f $test_home/.local/state/omarchy/done/voxtype-install-invitation ]] || fail "Voxtype invitation records completion"
|
||||
[[ -f $hook_path ]] || fail "Voxtype invitation keeps its hook installed"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "Voxtype invitation uses durable user services"
|
||||
grep -q -- '--user --collect --quiet --service-type=exec --unit=omarchy-voxtype-install-invitation' "$log_file" || fail "Voxtype invitation configures its user service"
|
||||
# KillMode=process keeps the launcher's setsid child alive once the short-lived
|
||||
# main process exits, otherwise the install terminal never appears.
|
||||
grep -q -- '--user --collect --quiet -p KillMode=process --unit=omarchy-voxtype-install ' "$log_file" || fail "Voxtype invitation outlives its launcher unit"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "Voxtype invitation sends one notification"
|
||||
[[ $(grep -c '^launch$' "$log_file") -eq 1 ]] || fail "Voxtype invitation handles the notification action"
|
||||
grep -qx 'exec:omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install' "$log_file" ||
|
||||
fail "Voxtype invitation attaches the installer to the notification"
|
||||
grep -q '^systemd-run:' "$log_file" && fail "Voxtype invitation needs no unit to hold an unanswered toast"
|
||||
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" bash "$hook_path"
|
||||
|
||||
[[ -f $hook_path ]] || fail "completed Voxtype invitation keeps its hook installed"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "completed Voxtype invitation does not schedule again"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "completed Voxtype invitation hook does not notify again"
|
||||
[[ $(grep -c '^launch$' "$log_file") -eq 1 ]] || fail "completed Voxtype invitation hook does not launch again"
|
||||
|
||||
pass "Voxtype invitation only runs once"
|
||||
|
||||
Reference in New Issue
Block a user