Attention Required: notification rules with effects for Omarchy
Rules watch every notification for words and apps and answer with an effect: nudge, flash, banner, airplane, confetti, blink, sound, focus, or a command. Silenced notifications that match a rule are let through. Settings popup behind a bar bell, rules kept in one JSON file.
This commit is contained in:
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Runs a shell command of yours, with the notification in the environment
|
||||
# (AR_APP, AR_SUMMARY, AR_BODY, AR_RULE, ...; see bin/ar-effect).
|
||||
#
|
||||
# {"type": "command", "run": "notify-send \"$AR_RULE\" \"$AR_SUMMARY\""}
|
||||
set -uo pipefail
|
||||
[[ -n ${AR_OPT_RUN:-} ]] || { echo "command: the effect needs a \"run\" option" >&2; exit 2; }
|
||||
exec bash -c "$AR_OPT_RUN"
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Brings the window of the app that sent the notification to the front.
|
||||
#
|
||||
# The notification only carries the app's name ("Google Chrome", "Telegram
|
||||
# Desktop"); windows carry a class ("google-chrome", "org.telegram.desktop")
|
||||
# and a title. Both are compared with everything but letters and digits
|
||||
# stripped, so "Google Chrome" finds "google-chrome". The most recently
|
||||
# focused matching window wins.
|
||||
#
|
||||
# Options (set on the effect in rules.json):
|
||||
# window what to look for instead of the app's name (a class or a title)
|
||||
set -uo pipefail
|
||||
|
||||
want=${AR_OPT_WINDOW:-${AR_APP:-}}
|
||||
[[ -n $want ]] || exit 0
|
||||
command -v hyprctl >/dev/null 2>&1 || exit 1
|
||||
command -v jq >/dev/null 2>&1 || exit 1
|
||||
|
||||
find_window() {
|
||||
hyprctl -j clients 2>/dev/null | jq -r --arg want "$1" '
|
||||
def key: ascii_downcase | gsub("[^a-z0-9]"; "");
|
||||
($want | key) as $w
|
||||
| [ .[] | select($w != "" and (
|
||||
((.class // "") | key | contains($w)) or
|
||||
((.initialClass // "") | key | contains($w)) or
|
||||
((.title // "") | key | contains($w)) or
|
||||
((.initialTitle // "") | key | contains($w)))) ]
|
||||
| sort_by(.focusHistoryID) | .[0].address // empty'
|
||||
}
|
||||
|
||||
addr=$(find_window "$want")
|
||||
# "Telegram Desktop" may only match as "telegram": try the first word too.
|
||||
[[ -n $addr ]] || addr=$(find_window "${want%% *}")
|
||||
[[ -n $addr ]] || { echo "focus: no window for '$want'" >&2; exit 0; }
|
||||
# The address goes into a line of Lua for the compositor: only a hex address will do.
|
||||
[[ $addr =~ ^0x[0-9a-fA-F]{1,16}$ ]] || { echo "focus: odd window address '$addr'" >&2; exit 1; }
|
||||
|
||||
# Lua-configured Hyprland takes the dispatcher through eval; older,
|
||||
# conf-configured installs still take `hyprctl dispatch`.
|
||||
hyprctl -q eval "hl.dispatch(hl.dsp.focus({ window = \"address:$addr\" }))" >/dev/null 2>&1 \
|
||||
|| hyprctl -q dispatch focuswindow "address:$addr" >/dev/null 2>&1
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# The MSN Messenger nudge: the whole screen shakes for a moment.
|
||||
#
|
||||
# Done with a Hyprland screen shader that moves the whole picture to a new
|
||||
# random offset `speed` times a second, applied for `duration` seconds and
|
||||
# then taken away again. Damage tracking is switched off for those seconds
|
||||
# so a frame is a full redraw; it is put back exactly as it was. (Hyprland
|
||||
# still only draws when something changed: the service shows a 2px overlay
|
||||
# that repaints every frame while this runs, see FrameTicker.qml.)
|
||||
#
|
||||
# Options (set on the effect in rules.json):
|
||||
# duration seconds, default 1
|
||||
# intensity 0.5..30, how far the picture moves, default 1.5 (a phone buzz; 6 is a proper MSN nudge)
|
||||
# speed new positions per second, default 200: at or above the refresh rate every frame differs
|
||||
set -uo pipefail
|
||||
|
||||
duration=${AR_OPT_DURATION:-1}
|
||||
intensity=${AR_OPT_INTENSITY:-1.5}
|
||||
speed=${AR_OPT_SPEED:-200}
|
||||
num='^[0-9]*\.?[0-9]+$'
|
||||
[[ $duration =~ $num ]] || duration=1
|
||||
[[ $intensity =~ $num ]] || intensity=1.5
|
||||
[[ $speed =~ $num ]] || speed=200
|
||||
|
||||
command -v hyprctl >/dev/null 2>&1 || { echo "nudge: hyprctl not found" >&2; exit 1; }
|
||||
|
||||
run_dir=${XDG_RUNTIME_DIR:-/tmp}/attention-required
|
||||
mkdir -p "$run_dir" || exit 1
|
||||
shader=$run_dir/nudge.frag
|
||||
|
||||
# One nudge at a time. A second one arriving mid-shake is dropped rather
|
||||
# than queued: by the time it ran, the first would have made the point.
|
||||
exec 9>"$run_dir/nudge.lock"
|
||||
flock -n 9 || exit 0
|
||||
|
||||
amp=$(awk -v i="$intensity" 'BEGIN { printf "%.5f", (i > 30 ? 30 : i) / 1000 }')
|
||||
rate=$(awk -v s="$speed" 'BEGIN { printf "%.1f", (s < 1 ? 1 : s) }')
|
||||
|
||||
cat > "$shader" <<FRAG
|
||||
#version 320 es
|
||||
precision highp float;
|
||||
in vec2 v_texcoord;
|
||||
uniform sampler2D tex;
|
||||
uniform float time;
|
||||
out vec4 fragColor;
|
||||
// A new pseudo-random offset every 1/$rate s, so the picture jumps rather
|
||||
// than glides: what a fast vibration looks like at any frame rate.
|
||||
void main() {
|
||||
float step = floor(mod(time, 1000.0) * $rate);
|
||||
float r1 = fract(sin(step * 12.9898) * 43758.5453);
|
||||
float r2 = fract(sin(step * 78.2330) * 43758.5453);
|
||||
vec2 off = (vec2(r1, r2) - 0.5) * 2.0 * $amp;
|
||||
fragColor = texture(tex, clamp(v_texcoord + off, vec2(0.0), vec2(1.0)));
|
||||
}
|
||||
FRAG
|
||||
|
||||
lua_string() {
|
||||
local s=${1//\\/\\\\}
|
||||
s=${s//\"/\\\"}
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# The config keys are written through Hyprland's Lua API (hl.config), which
|
||||
# is what a Lua-configured Hyprland accepts at runtime; older, conf-configured
|
||||
# installs still take `hyprctl keyword`.
|
||||
set_option() {
|
||||
local lua_section=$1 lua_key=$2 conf_key=$3 value=$4
|
||||
if hyprctl -q eval "hl.config({ $lua_section = { $lua_key = $value } })" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
local plain=$value
|
||||
[[ $plain == \"*\" ]] && plain=${plain:1:-1}
|
||||
hyprctl -q keyword "$conf_key" "$plain" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
prev_shader=$(hyprctl -j getoption decoration:screen_shader 2>/dev/null | jq -r '.str // empty')
|
||||
[[ -z $prev_shader || $prev_shader == "$shader" ]] && prev_shader='[[EMPTY]]'
|
||||
prev_damage=$(hyprctl -j getoption debug:damage_tracking 2>/dev/null | jq -r '.int // 2')
|
||||
[[ $prev_damage =~ ^[0-9]+$ ]] || prev_damage=2
|
||||
|
||||
restore() {
|
||||
set_option decoration screen_shader decoration:screen_shader "\"$(lua_string "$prev_shader")\""
|
||||
set_option debug damage_tracking debug:damage_tracking "$prev_damage"
|
||||
}
|
||||
trap restore EXIT INT TERM
|
||||
|
||||
set_option debug damage_tracking debug:damage_tracking 0
|
||||
set_option decoration screen_shader decoration:screen_shader "\"$(lua_string "$shader")\""
|
||||
sleep "$duration"
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Plays a sound.
|
||||
#
|
||||
# Options (set on the effect in rules.json):
|
||||
# file path to a sound file; default is the freedesktop "new message" chime
|
||||
# intensity volume, 0.0 .. 1.0, default 1 (`volume` works too)
|
||||
# speed how many times to play it, default 1 (`repeat` works too)
|
||||
set -uo pipefail
|
||||
|
||||
file=${AR_OPT_FILE:-/usr/share/sounds/freedesktop/stereo/message-new-instant.oga}
|
||||
volume=${AR_OPT_INTENSITY:-${AR_OPT_VOLUME:-1}}
|
||||
times=${AR_OPT_SPEED:-${AR_OPT_REPEAT:-1}}
|
||||
[[ $volume =~ ^[0-9]*\.?[0-9]+$ ]] || volume=1
|
||||
[[ $times =~ ^[0-9]+$ ]] || times=1
|
||||
file=${file/#\~/$HOME}
|
||||
|
||||
[[ -r $file ]] || { echo "sound: cannot read $file" >&2; exit 1; }
|
||||
|
||||
play() {
|
||||
if command -v pw-play >/dev/null 2>&1; then
|
||||
pw-play --volume="$volume" "$file"
|
||||
elif command -v paplay >/dev/null 2>&1; then
|
||||
paplay "$file"
|
||||
elif command -v mpv >/dev/null 2>&1; then
|
||||
mpv --no-video --really-quiet --volume="$(awk -v v="$volume" 'BEGIN { printf "%d", v * 100 }')" "$file"
|
||||
else
|
||||
echo "sound: no player found (pw-play, paplay or mpv)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
for ((i = 0; i < times; i++)); do
|
||||
play || exit 1
|
||||
done
|
||||
Reference in New Issue
Block a user