Replace --exec-arg with an ergonomic --exec that consumes the rest of the line as the click command. The caller's shell tokenizes the words into discrete arguments before the tool sees them, and the shell runs them as positional parameters (never a re-parsed string), so safety is identical to the argv form while the call sites read naturally: `--exec omarchy toggle something`. Crucially the tool never splits a string itself — a single quoted whole-command argument is rejected and points at the unquoted form, because whitespace- splitting a string hands argument boundaries to whoever controls its content (the injection we are avoiding). --exec must come last; migrate every caller.
151 lines
3.8 KiB
Bash
Executable File
151 lines
3.8 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Send an Omarchy desktop notification
|
|
# omarchy:args=[--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options] [--exec <program> [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 <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options] [--exec <program> [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 <program> [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
|