From af5e918dd5afa2b26de9116a12bf4e4601996a78 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 25 May 2026 23:51:48 +0200 Subject: [PATCH] Improve actionable notification wrapper --- bin/omarchy-capture-screenrecording | 5 +- bin/omarchy-capture-screenshot | 5 +- bin/omarchy-notification-send | 53 +++++++++++++++---- install/first-run/install-voxtype.hook | 7 ++- install/first-run/welcome.sh | 5 +- install/first-run/wifi.sh | 10 ++-- .../notifications/NotificationLogic.js | 6 +++ shell/plugins/notifications/Service.qml | 6 +-- test/shell.d/notification-send-test.sh | 34 ++++++++++++ test/shell.d/notifications-test.sh | 2 + 10 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 test/shell.d/notification-send-test.sh diff --git a/bin/omarchy-capture-screenrecording b/bin/omarchy-capture-screenrecording index 05d802e2..6ed81d9c 100755 --- a/bin/omarchy-capture-screenrecording +++ b/bin/omarchy-capture-screenrecording @@ -245,8 +245,9 @@ stop_screenrecording() { ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null ( - ACTION=$(notify-send -a omarchy-action "Screen recording saved" "Open with Super + Alt + , (or click this)" -t 10000 --hint="string:image-path:${preview:-$filename}" -A "default=open") - [[ $ACTION == "default" ]] && mpv "$filename" + 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 rm -f "$preview" ) & fi diff --git a/bin/omarchy-capture-screenshot b/bin/omarchy-capture-screenshot index b0be76e1..ca18d125 100755 --- a/bin/omarchy-capture-screenshot +++ b/bin/omarchy-capture-screenshot @@ -133,8 +133,9 @@ case "$PROCESSING" in wl-copy --type image/png <"$FILEPATH" ( - ACTION=$(notify-send -a omarchy-action "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" -t 10000 --hint="string:image-path:$FILEPATH" -A "default=edit") - [[ $ACTION == "default" ]] && open_editor "$FILEPATH" + if [[ -n $(omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" -t 10000 --image "$FILEPATH" -a) ]]; then + open_editor "$FILEPATH" + fi ) >/dev/null 2>&1 & ;; copy) diff --git a/bin/omarchy-notification-send b/bin/omarchy-notification-send index aa1d26fc..05961410 100755 --- a/bin/omarchy-notification-send +++ b/bin/omarchy-notification-send @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Send an Omarchy desktop notification -# omarchy:args=[-g ] [-u ] [description] [notify-send options] +# omarchy:args=[-a] [--app-name ] [-g ] [-u ] [--image ] [description] [notify-send options] # omarchy:examples=omarchy notification send "Reminder" "5 minutes are up" -g 󰢌 set -euo pipefail @@ -10,7 +10,11 @@ headline="" description="" glyph= urgency="low" +app_name="omarchy-action" +image= +click_action=0 args=() +parsed_option_args=0 parse_omarchy_option() { case $1 in @@ -20,6 +24,7 @@ parse_omarchy_option() { exit 1 fi glyph=$2 + parsed_option_args=2 return 0 ;; -u | --urgency) @@ -28,6 +33,30 @@ parse_omarchy_option() { exit 1 fi urgency="$2" + parsed_option_args=2 + return 0 + ;; + --app-name) + if (($# < 2)); then + echo "Missing value for $1" >&2 + exit 1 + fi + app_name=$2 + parsed_option_args=2 + return 0 + ;; + --image) + if (($# < 2)); then + echo "Missing value for $1" >&2 + exit 1 + fi + image=$2 + parsed_option_args=2 + return 0 + ;; + -a | --action) + click_action=1 + parsed_option_args=1 return 0 ;; esac @@ -37,14 +66,14 @@ parse_omarchy_option() { while (($# > 0)); do if parse_omarchy_option "$@"; then - shift 2 + shift "$parsed_option_args" else break fi done if (($# < 1)); then - echo "Usage: omarchy-notification-send [-g ] [-u ] [description] [notify-send options]" + echo "Usage: omarchy-notification-send [-a] [--app-name ] [-g ] [-u ] [--image ] [description] [notify-send options]" exit 1 fi @@ -58,20 +87,26 @@ fi while (($# > 0)); do if parse_omarchy_option "$@"; then - shift 2 + shift "$parsed_option_args" else args+=("$1") shift fi done -# Tag as a user-action toast so it pops through DND. See dnd-fix-plan.md. -args+=("-a" "omarchy-action" "-u" "$urgency") +# Tag as a user-action toast so it pops through DND. +args+=("-a" "$app_name" "-u" "$urgency") -# Keep Omarchy action toasts compact by rendering glyphs inline instead of -# forcing the notification card's icon slot. if [[ -n $glyph ]]; then - headline="$glyph $headline" + args+=("--hint=string:omarchy-glyph:$glyph") +fi + +if [[ -n $image ]]; then + args+=("--hint=string:image-path:$image") +fi + +if ((click_action)); then + args+=("-A" "default=default") fi if [[ -n $description ]]; then diff --git a/install/first-run/install-voxtype.hook b/install/first-run/install-voxtype.hook index ebb7d250..68943952 100644 --- a/install/first-run/install-voxtype.hook +++ b/install/first-run/install-voxtype.hook @@ -5,7 +5,6 @@ set -e # Remove this hook after use rm -f "$0" -( - action=$(notify-send -a omarchy-action -u critical --hint=string:omarchy-glyph: "Install Dictation with Voxtype" "Click to install voice dictation for Omarchy." -A "default=Install") - [[ $action == "default" ]] && omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install -) >/dev/null 2>&1 & +if [[ -n $(omarchy-notification-send -u critical -g  "Install Dictation with Voxtype" "Click to install voice dictation for Omarchy." -a) ]]; then + omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install +fi >/dev/null 2>&1 & diff --git a/install/first-run/welcome.sh b/install/first-run/welcome.sh index a740846a..0ff2ec31 100644 --- a/install/first-run/welcome.sh +++ b/install/first-run/welcome.sh @@ -1,4 +1,5 @@ ( - action=$(notify-send -a omarchy-action -u critical --hint=string:omarchy-glyph: "Learn Keybindings" "Super + K for cheatsheet.\nSuper + Space for application launcher.\nSuper + Alt + Space for Omarchy Menu." -A "default=Open") - [[ $action == "default" ]] && omarchy-menu-keybindings + 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 & diff --git a/install/first-run/wifi.sh b/install/first-run/wifi.sh index d03e7ad7..2dc12c9e 100644 --- a/install/first-run/wifi.sh +++ b/install/first-run/wifi.sh @@ -1,14 +1,16 @@ notify_update() { ( - action=$(notify-send -a omarchy-action -u critical --hint=string:omarchy-glyph: "Update System" "$1" -A "default=Update") - [[ $action == "default" ]] && omarchy-launch-floating-terminal-with-presentation omarchy-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 & } notify_wifi() { ( - action=$(notify-send -a omarchy-action -u critical --hint=string:omarchy-glyph:󰖩 "Click to Setup Wi-Fi" -A "default=Setup") - [[ $action == "default" ]] && omarchy-shell omarchy.network toggle + if [[ -n $(omarchy-notification-send -u critical -g 󰖩 "Click to Setup Wi-Fi" -a) ]]; then + omarchy-shell omarchy.network toggle + fi ) >/dev/null 2>&1 & } diff --git a/shell/plugins/notifications/NotificationLogic.js b/shell/plugins/notifications/NotificationLogic.js index adf8099a..44b048f9 100644 --- a/shell/plugins/notifications/NotificationLogic.js +++ b/shell/plugins/notifications/NotificationLogic.js @@ -37,6 +37,11 @@ function shouldBypassDnd(notification, criticalUrgency) { return appName === "notify-send" && notification && notification.urgency === criticalUrgency } +function isEphemeralApp(appName) { + var name = String(appName || "") + return name === "notify-send" || name === "omarchy-action" +} + function glyphFromHints(hints) { try { if (hints) { @@ -167,6 +172,7 @@ if (typeof module !== "undefined") { sanitizeBody: sanitizeBody, summaryStartsWithGlyph: summaryStartsWithGlyph, shouldBypassDnd: shouldBypassDnd, + isEphemeralApp: isEphemeralApp, glyphFromHints: glyphFromHints, snapshotOf: snapshotOf, historyEntry: historyEntry, diff --git a/shell/plugins/notifications/Service.qml b/shell/plugins/notifications/Service.qml index a4ce58ad..1f0856f8 100644 --- a/shell/plugins/notifications/Service.qml +++ b/shell/plugins/notifications/Service.qml @@ -133,15 +133,15 @@ Item { // - app_name is "notify-send" (the CLI default — means the sender // didn't bother declaring an identity, so it's almost certainly // ephemeral test/feedback noise) - // - app_name is "omarchy-action" (omarchy's own user-action - // confirmation toasts — the user just triggered them, they don't + // - app_name is "omarchy-action" (Omarchy's own user-action + // toasts — the user just triggered them, they don't // need to be archived) var transient = false try { transient = !!(notification.hints && notification.hints["transient"]) } catch (e) { transient = false } var appName = String(notification.appName || "") - var ephemeralApp = appName === "notify-send" || appName === "omarchy-action" + var ephemeralApp = NotificationLogic.isEphemeralApp(appName) if (transient || ephemeralApp) { if (service.doNotDisturb && !shouldBypassDnd(notification)) { notification.tracked = false diff --git a/test/shell.d/notification-send-test.sh b/test/shell.d/notification-send-test.sh new file mode 100644 index 00000000..c68a59ab --- /dev/null +++ b/test/shell.d/notification-send-test.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +stub="$tmpdir/notify-send" +args_file="$tmpdir/args" + +printf '%s\n' \ + '#!/bin/bash' \ + 'printf "%s\n" "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"' \ + >"$stub" +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 + +mapfile -t args <"$args_file" + +[[ ${args[0]} == "-a" ]] || fail "notification wrapper passes app flag" +[[ ${args[1]} == "custom-app" ]] || fail "notification wrapper uses custom app name" +[[ ${args[2]} == "-u" ]] || fail "notification wrapper passes urgency flag" +[[ ${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" diff --git a/test/shell.d/notifications-test.sh b/test/shell.d/notifications-test.sh index 817f806f..928c2927 100644 --- a/test/shell.d/notifications-test.sh +++ b/test/shell.d/notifications-test.sh @@ -42,6 +42,8 @@ assert(notifications.shouldBypassDnd({ appName: 'omarchy-action', urgency: 1 }, assert(notifications.shouldBypassDnd({ appName: 'notify-send', urgency: 2 }, 2), 'critical notify-send bypasses DND') assert(!notifications.shouldBypassDnd({ appName: 'notify-send', urgency: 1 }, 2), 'normal notify-send does not bypass DND') assert(!notifications.shouldBypassDnd({ appName: 'Slack', urgency: 2 }, 2), 'critical app notifications do not bypass DND') +assert(!notifications.shouldBypassDnd({ appName: 'omarchy-menu-keybindings', urgency: 1 }, 2), 'omarchy command app names do not bypass DND') +assert(!notifications.isEphemeralApp('omarchy-menu-keybindings'), 'notifications treat omarchy command app names as normal apps') const notification = { id: 12,