feat(omo): play omo's own synthesised notification sounds

omo-notify and omo-remote played Apple's Glass and Ping through afplay, which exist only on macOS and are not ours to ship. They now play omo-done (task finished), omo-input (needs your input) and omo-error (stopped with an error), rendered by sfx from .config/omo/sounds/recipes.toml at the same loudness as Glass. The new omo-sound plays a sound by name or any audio file, with afplay on macOS and pw-play, paplay, aplay or ffplay on Linux, and finds the sounds through the dotfiles checkout it is linked from.
This commit is contained in:
David Ibia
2026-10-18 12:56:09 +01:00
parent 5805f45358
commit 66d1d21011
8 changed files with 66 additions and 11 deletions
+8 -7
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Ding, tab marker and notification for omo (senpi) hooks: UserPromptSubmit,
# Stop and Notification. Reads the hook JSON payload on stdin. Wired up in
# ~/.omo/agent/hooks.json. The sound plays through afplay, so Focus modes and
# notification permissions cannot mute it; the banner is a silent extra.
# ~/.omo/agent/hooks.json. The sound plays through omo-sound (afplay on macOS),
# so Focus modes and notification permissions cannot mute it; the banner is a
# silent extra.
# Ghostty posts the banner, so clicking it opens the tab it came from, and
# omo-status then selects the session's pane. omo-status also colours that tab:
# blue working, amber waiting for you, green finished, red error.
@@ -32,13 +33,13 @@ case "$event" in
case "$(field '.stopReason')" in
# Esc/interrupts are your own doing; no need to be told about them.
aborted) state="clear" ;;
error) state="error" message="Stopped with an error" sound="Glass" ;;
*) state="done" message="Task finished" sound="Glass" ;;
error) state="error" message="Stopped with an error" sound="omo-error" ;;
*) state="done" message="Task finished" sound="omo-done" ;;
esac
;;
Notification)
if [[ "$(field '.kind')" == "ask-user-asked" ]]; then
state=input message="Needs your input: $(field '.title')" sound="Ping"
state=input message="Needs your input: $(field '.title')" sound="omo-input"
else
# Answered or timed out: the agent carries on.
state=working
@@ -50,8 +51,8 @@ case "$event" in
esac
# Background the ding so the hook returns immediately; redirect output so the
# hook runner is not left waiting on afplay's inherited pipes.
[[ -n "$sound" ]] && afplay "/System/Library/Sounds/${sound}.aiff" >/dev/null 2>&1 &
# hook runner is not left waiting on the player's inherited pipes.
[[ -n "$sound" ]] && "$HOME/.local/bin/omo-sound" "$sound" >/dev/null 2>&1 &
# Find the tmux pane omo runs in: hooks run detached without TMUX_PANE, so walk
# up the process tree to the first ancestor whose tty is a tmux pane.
+4 -4
View File
@@ -124,9 +124,9 @@ case ${1:-} in
[[ $tags == *,omo,* ]] || exit 0 # Claude Code posts to the same topic
(($(date +%s) - ${NTFY_TIME:-0} <= fresh)) || exit 0
case $tags in
*,question,*) state=input sound=Ping ;;
*,x,*) state=error sound=Glass ;;
*) state="done" sound=Glass ;;
*,question,*) state=input sound=omo-input ;;
*,x,*) state=error sound=omo-error ;;
*) state="done" sound=omo-done ;;
esac
# "attached" only says some terminal shows the session: a tmux pane here
# (the title path has it), a plain tab, or a mosh-server left behind.
@@ -135,7 +135,7 @@ case ${1:-} in
logline "ntfy $state ${NTFY_TITLE:-} skipped: a tmux pane got it through the title"
exit 0
fi
afplay "/System/Library/Sounds/${sound}.aiff" >/dev/null 2>&1 &
"$HOME/.local/bin/omo-sound" "$sound" >/dev/null 2>&1 &
# Ghostty posts the banner from a plain tab connected with mosh or ssh, so
# clicking it lands there, or else from the tmux tab you used last.
target=$(plain_tab_tty "$tags")
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Play one of omo's notification sounds (omo-done, omo-input, omo-error) or any
# audio file with whatever player this machine has: afplay on macOS, else
# PipeWire, PulseAudio, ALSA or ffplay on Linux. It blocks until playback ends,
# so hooks that must not wait run it in the background.
#
# The sounds are rendered by sfx from .config/omo/sounds/recipes.toml in the
# dotfiles checkout this script is linked from.
#
# Usage: omo-sound NAME|FILE
sound=${1:?usage: omo-sound NAME|FILE}
if [[ ! -f $sound ]]; then
sound="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../../.config/omo/sounds/$sound.wav"
fi
if [[ ! -f $sound ]]; then
echo "omo-sound: no such sound or file: $1" >&2
exit 1
fi
if command -v afplay >/dev/null; then
exec afplay "$sound"
elif command -v pw-play >/dev/null; then
exec pw-play "$sound"
elif command -v paplay >/dev/null; then
exec paplay "$sound"
elif command -v aplay >/dev/null; then
exec aplay -q "$sound"
elif command -v ffplay >/dev/null; then
exec ffplay -nodisp -autoexit -loglevel quiet "$sound"
fi
echo "omo-sound: no audio player found (afplay, pw-play, paplay, aplay or ffplay)" >&2
exit 1