Files
attention-required/bin/attention-required
T

182 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# attention-required: rules that turn a notification into an effect.
#
# attention-required list the rules, as a table
# attention-required add NAME [options] add a rule
# --words a,b,c words to look for (any of them; /regex/ allowed)
# --apps x,y only notifications from these apps (substring of the app name; default: any app)
# --effects e1,e2 nudge | flash | sound | command | your own (default: nudge)
# --all every word must be present, not any
# --cooldown SECONDS at most one firing per rule in this many seconds (default 3)
# attention-required patch NAME '{"key": value, ...}' merge these keys into a rule (a null value removes the key)
# attention-required remove NAME
# attention-required enable NAME | disable NAME
# attention-required effects the effect catalog with every option and range, as JSON
# attention-required docs print the configuration reference
# attention-required test EFFECT run one effect now, e.g. `test nudge`
# or `test '{"type":"flash","color":"urgent"}'`
# attention-required simulate APP SUMMARY [BODY] run a made-up notification past the rules
# attention-required settings open the settings popup (also: click the bell in the bar)
# attention-required set EFFECT OPTION VALUE change a default, e.g. `set nudge intensity 6`
# attention-required toggle | on | off pause or resume every effect (also: right-click the bell)
# attention-required status what the service has seen and fired
# attention-required reload re-read rules.json (it is also watched)
# attention-required edit open rules.json in $EDITOR
# attention-required path print where rules.json is
# attention-required export [FILE] copy the rules to FILE (or print them) to keep or share
# attention-required import FILE replace the rules with FILE's (the old file is kept as a .bak)
set -euo pipefail
config_dir=${XDG_CONFIG_HOME:-$HOME/.config}/attention-required
rules=$config_dir/rules.json
here=$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)
die() { echo "attention-required: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "$1 is not installed"; }
need jq
ipc() {
if command -v omarchy-shell >/dev/null 2>&1; then
omarchy-shell attention-required "$@"
else
quickshell ipc -p "${OMARCHY_PATH:-/usr/share/omarchy}/shell" call attention-required "$@"
fi
}
ipc_panel() {
if command -v omarchy-shell >/dev/null 2>&1; then
omarchy-shell alanfortlink.attention-required "$@"
else
quickshell ipc -p "${OMARCHY_PATH:-/usr/share/omarchy}/shell" call alanfortlink.attention-required "$@"
fi
}
ensure_rules() {
mkdir -p "$config_dir/effects"
[[ -e $rules ]] || cp "$here/rules.example.json" "$rules"
}
# Written in place rather than moved into place, so the shell's file watch
# sees the change without a restart.
write_rules() {
local tmp
tmp=$(mktemp "$config_dir/.rules.XXXXXX")
cat > "$tmp"
jq . "$tmp" >/dev/null || { rm -f "$tmp"; die "refusing to write invalid JSON"; }
cat "$tmp" > "$rules"
rm -f "$tmp"
}
split_list() { jq -cn --arg s "$1" '$s | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))'; }
have_rule() { jq -e --arg n "$1" '.rules // [] | any(.name == $n)' "$rules" >/dev/null; }
cmd_list() {
ensure_rules
jq -r '
(.rules // []) | if length == 0 then "no rules" else
(["NAME", "ON", "WORDS", "APPS", "EFFECTS", "COOLDOWN"] | @tsv),
(.[] | [
.name,
(if .enabled == false then "off" else "on" end),
((.words // []) | join(", ")),
((.apps // []) | if length == 0 then "any" else join(", ") end),
((.effects // ["nudge"]) | map(if type == "string" then . else .type end) | join(", ")),
(.cooldown // 3 | tostring)
] | @tsv) end' "$rules" | column -t -s $'\t'
}
cmd_add() {
local name=${1:-}; shift || true
[[ -n $name ]] || die "usage: add NAME --words a,b [--apps x,y] [--effects nudge,flash] [--all] [--cooldown N]"
local words='[]' apps='[]' effects='["nudge"]' match=any cooldown=3
while (($#)); do
case $1 in
--words) words=$(split_list "${2:-}"); shift 2 ;;
--apps) apps=$(split_list "${2:-}"); shift 2 ;;
--effects) effects=$(split_list "${2:-}"); shift 2 ;;
--all) match=all; shift ;;
--cooldown) cooldown=${2:-3}; shift 2 ;;
*) die "unknown option $1" ;;
esac
done
ensure_rules
have_rule "$name" && die "a rule named '$name' exists; remove it first"
jq --arg name "$name" --argjson words "$words" --argjson apps "$apps" --argjson effects "$effects" \
--arg match "$match" --argjson cooldown "$cooldown" '
.rules = ((.rules // []) + [{name: $name, words: $words, apps: $apps, effects: $effects, match: $match, cooldown: $cooldown}])
| .version = (.version // 1)' "$rules" | write_rules
echo "added '$name'"
ipc reload >/dev/null 2>&1 || true
}
cmd_patch() {
local name=${1:-} patch=${2:-}
[[ -n $name && -n $patch ]] || die "usage: patch NAME '{\"key\": value, ...}'"
jq -e 'type == "object"' <<<"$patch" >/dev/null 2>&1 || die "the patch must be a JSON object"
ensure_rules
have_rule "$name" || die "no rule named '$name'"
jq --arg n "$name" --argjson p "$patch" '
.rules = ((.rules // []) | map(if .name == $n then (. + $p | with_entries(select(.value != null))) else . end))' "$rules" | write_rules
echo "patched '$name'"
ipc reload >/dev/null 2>&1 || true
}
cmd_remove() {
local name=${1:-}
[[ -n $name ]] || die "usage: remove NAME"
ensure_rules
have_rule "$name" || die "no rule named '$name'"
jq --arg n "$name" '.rules = ((.rules // []) | map(select(.name != $n)))' "$rules" | write_rules
echo "removed '$name'"
ipc reload >/dev/null 2>&1 || true
}
cmd_toggle() {
local enabled=$1 name=${2:-}
[[ -n $name ]] || die "usage: enable|disable NAME"
ensure_rules
have_rule "$name" || die "no rule named '$name'"
jq --arg n "$name" --argjson e "$enabled" '.rules = ((.rules // []) | map(if .name == $n then .enabled = $e else . end))' "$rules" | write_rules
echo "'$name' is now $([[ $enabled == true ]] && echo on || echo off)"
ipc reload >/dev/null 2>&1 || true
}
case ${1:-help} in
list|ls) cmd_list ;;
add) shift; cmd_add "$@" ;;
patch) shift; cmd_patch "$@" ;;
remove|rm) shift; cmd_remove "$@" ;;
effects) ipc effects | jq . ;;
docs) cat "$here/REFERENCE.md" ;;
enable) shift; cmd_toggle true "$@" ;;
disable) shift; cmd_toggle false "$@" ;;
test) [[ -n ${2:-} ]] || die "usage: test EFFECT"; ipc test "$2" ;;
simulate|sim) [[ -n ${3:-} ]] || die "usage: simulate APP SUMMARY [BODY]"; ipc simulate "$2" "$3" "${4:-}" ;;
settings) ipc_panel toggle ;;
set) [[ -n ${4:-} ]] || die "usage: set <effect> <option> <value> e.g. set nudge intensity 6"; ipc setDefault "$2" "$3" "$4" ;;
toggle) ipc toggle ;;
on|resume) ipc setEnabled true ;;
off|pause) ipc setEnabled false ;;
status) ipc state | jq . ;;
rules) ipc rules | jq . ;;
reload) ipc reload ;;
edit) ensure_rules; "${EDITOR:-nvim}" "$rules" ;;
path) echo "$rules" ;;
export)
ensure_rules
if [[ -n ${2:-} ]]; then cp "$rules" "$2" && echo "saved to $2"; else cat "$rules"; fi ;;
import)
[[ -n ${2:-} && -r $2 ]] || die "usage: import FILE"
jq -e '(.rules | type) == "array"' "$2" >/dev/null 2>&1 || die "$2 is not a rules file (needs a \"rules\" list)"
ensure_rules
backup="$rules.bak.$(date +%s)"
cp "$rules" "$backup"
jq '.version = (.version // 1)' "$2" | write_rules
echo "imported $2 ($(jq '.rules | length' "$rules") rules); the previous file is $backup"
ipc reload >/dev/null 2>&1 || true ;;
help|-h|--help) sed -n '2,/^set -euo/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//' ;;
*) die "unknown command '$1' (try: attention-required help)" ;;
esac