#!/bin/bash # omarchy:summary=Send an Omarchy desktop notification # omarchy:args=[--app-name ] [-g ] [-u ] [-i ] [-t ] [--image ] [description] [--exec [args...]] # omarchy:examples=omarchy notification send "Reminder" "5 minutes are up" -g 󰢌 set -euo pipefail headline="" description="" glyph= urgency="low" app_name="omarchy-action" app_icon="" image= expire_timeout=-1 exec_args=() exec_present=0 parsed_option_args=0 usage() { echo "Usage: omarchy-notification-send [--app-name ] [-g ] [-u ] [-i ] [-t ] [--image ] [description] [--exec [args...]]" >&2 } need_value() { if (($1 < 2)); then echo "Missing value for $2" >&2 exit 1 fi } parse_omarchy_option() { case $1 in -g | --glyph) need_value $# "$1" glyph=$2 parsed_option_args=2 return 0 ;; -u | --urgency) need_value $# "$1" urgency="$2" parsed_option_args=2 return 0 ;; --app-name) need_value $# "$1" app_name=$2 parsed_option_args=2 return 0 ;; -i | --icon) need_value $# "$1" app_icon=$2 parsed_option_args=2 return 0 ;; -t | --expire-time) need_value $# "$1" if [[ $2 != *[!0-9-]* && $2 =~ ^-?[0-9]+$ ]]; then expire_timeout=$2 else echo "Invalid $1 value (milliseconds expected): $2" >&2 exit 1 fi parsed_option_args=2 return 0 ;; --image) need_value $# "$1" image=$2 parsed_option_args=2 return 0 ;; esac return 1 } while (($# > 0)); do if parse_omarchy_option "$@"; then shift "$parsed_option_args" else break fi done if (($# < 1)); then usage exit 1 fi headline=$1 shift if (($# > 0)) && [[ $1 != -* ]]; then description=$1 shift fi while (($# > 0)); do if [[ $1 == "--exec" ]]; then # --exec consumes the rest of the line as the click command's argv. The # caller's shell already tokenized those words into discrete arguments, and # the shell runs them as-is (never re-parsed), so untrusted data in an # argument is only ever one argument and can never become a command. # Detected only here, after the headline/description positionals are # captured, so an untrusted headline that is literally "--exec" is taken as # text and can't be mistaken for the delimiter. --exec therefore comes last. shift exec_args=("$@") exec_present=1 break elif parse_omarchy_option "$@"; then shift "$parsed_option_args" else echo "Unknown option: $1" >&2 usage exit 1 fi done case $urgency in low) urgency_byte=0 ;; normal) urgency_byte=1 ;; critical) urgency_byte=2 ;; *) echo "Unknown urgency: $urgency (use low, normal, or critical)" >&2 exit 1 ;; esac # a{sv} hints, as busctl triples (key, variant type, value). urgency is a byte; # the rest are strings. The click command rides here as omarchy-exec-argv, built # only from --exec below. hints=(urgency y "$urgency_byte") if [[ -n $glyph ]]; then hints+=(omarchy-glyph s "$glyph") fi if [[ -n $image ]]; then hints+=(image-path s "$image") fi if ((exec_present)); then if ((${#exec_args[@]} == 0)); then echo "--exec needs a command: --exec [args...]" >&2 exit 1 fi # A single word with a space is almost always a whole command passed as one # quoted string — which would run a program literally named that. Splitting it # ourselves is exactly the injection we avoid, so reject it and point at the # unquoted form instead. if ((${#exec_args[@]} == 1)) && [[ ${exec_args[0]} == *[[:space:]]* ]]; then echo "--exec takes the command as separate words, not one quoted string." >&2 echo "Write: --exec ${exec_args[0]}" >&2 exit 1 fi # NUL-delimit into jq so every byte survives as data: jq's own --args would eat # a bare "--", and a newline in an arg must not split the vector. exec_argv_json=$(printf '%s\0' "${exec_args[@]}" | jq -Rsc 'split("\u0000")[:-1]') hints+=(omarchy-exec-argv s "$exec_argv_json") fi hint_count=$((${#hints[@]} / 3)) # Call org.freedesktop.Notifications.Notify directly — never notify-send. Its # argv parsing is the surface that reinterprets a relayed headline like # `--hint=…` or `-rf` as options or hints; busctl takes each value as one typed # D-Bus parameter instead, and the leading `--` keeps a dash-leading value # (headline, description, a negative timeout) positional rather than a busctl # option. So the summary and body are strings that can never become a hint, and # omarchy-exec-argv is set only from --exec. # # Signature susssasa{sv}i: app_name, replaces_id, app_icon, summary, body, # actions (empty), hints, expire_timeout. busctl --user -- call \ org.freedesktop.Notifications /org/freedesktop/Notifications \ org.freedesktop.Notifications Notify susssasa{sv}i \ "$app_name" 0 "$app_icon" "$headline" "$description" \ 0 \ "$hint_count" "${hints[@]}" \ "$expire_timeout" >/dev/null