#!/bin/bash # omarchy:summary=Send an Omarchy desktop notification # omarchy:args=[--app-name ] [-g ] [-u ] [--image ] [description] [notify-send options] [--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" image= exec_args=() exec_present=0 args=() parsed_option_args=0 # Split off the click command first: everything after --exec is its argv. The # caller's shell has already tokenized those words into discrete arguments, and # the shell runs them as-is (never re-parsed by a shell), so a value carrying # untrusted data — a title, a filename — is only ever one argument and can never # become a command. --exec therefore consumes the rest of the line and must # come last. pre_args=() while (($# > 0)); do if [[ $1 == "--exec" ]]; then shift exec_args=("$@") exec_present=1 break fi pre_args+=("$1") shift done set -- "${pre_args[@]}" parse_omarchy_option() { case $1 in -g | --glyph) if (($# < 2)); then echo "Missing value for $1" >&2 exit 1 fi glyph=$2 parsed_option_args=2 return 0 ;; -u | --urgency) if (($# < 2)); then echo "Missing value for $1" >&2 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 ;; esac return 1 } while (($# > 0)); do if parse_omarchy_option "$@"; then shift "$parsed_option_args" else break fi done if (($# < 1)); then echo "Usage: omarchy-notification-send [--app-name ] [-g ] [-u ] [--image ] [description] [notify-send options] [--exec [args...]]" exit 1 fi headline=$1 shift if (($# > 0)) && [[ $1 != -* ]]; then description=$1 shift fi while (($# > 0)); do if parse_omarchy_option "$@"; then shift "$parsed_option_args" else args+=("$1") shift fi done # Tag as a user-action toast so it pops through DND. args+=("-a" "$app_name" "-u" "$urgency") if [[ -n $glyph ]]; then args+=("--hint=string:omarchy-glyph:$glyph") fi if [[ -n $image ]]; then args+=("--hint=string:image-path:$image") fi # The click command travels with the popup as an argv hint the shell runs # itself, so restored toasts stay clickable and senders don't block on a # libnotify action (which dies when the shell restarts). 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]') args+=("--hint=string:omarchy-exec-argv:$exec_argv_json") fi if [[ -n $description ]]; then notify-send "${args[@]}" "$headline" "$description" else notify-send "${args[@]}" "$headline" fi