mirror of
https://github.com/boxpositron/absolute-dotfiles.git
synced 2026-10-24 08:23:19 +00:00
cd0ed704ab
Running fs in a plain Ghostty tab left the Mac silent: no local tmux reads the title, and the server tagged its pushes attached, so the ntfy agent skipped them. The agent now skips an attached push only when a tmux pane here recorded that exact event through its title, waiting up to three seconds for it; otherwise it plays the sound and posts the banner. The banner comes from the plain tab whose mosh-client uses the port the server tags (mosh-<port>), so clicking it lands on the right tab, and sessions whose tab closed while their mosh-server lingers still notify.
The server's title now reads marker first (🟢 studio · omo:done:<time>), so a plain tab shows the state at a glance; omo-remote parses that format.
195 lines
8.3 KiB
Bash
Executable File
195 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Ding, tab marker and notification for omo sessions on a remote server
|
|
# (flowstate-remote), the same as omo-notify gives local sessions. Two paths,
|
|
# so each event arrives once:
|
|
#
|
|
# Title: the server's omo-notify sets the terminal title of every tmux client
|
|
# attached to the session to "<marker> <project> · omo:<state>:<unix time>",
|
|
# plus ":<question>" when it asks you something, e.g.
|
|
# "🟢 studio · omo:done:1792249251". mosh and ssh carry titles to this Mac
|
|
# (mosh drops the OSC 777 banner). In a tmux pane running mosh or ssh, tmux's
|
|
# pane-title-changed hook (~/.tmux.conf) runs `omo-remote title <pane>`, which
|
|
# hands the event to omo-notify for that pane. A plain Ghostty tab just shows
|
|
# the title, marker first.
|
|
#
|
|
# ntfy: the server also posts each finish, error and question to ntfy, tagged
|
|
# "omo", plus "attached" when the title reached a terminal. The launchd agent
|
|
# runs `omo-remote subscribe`, which dings and posts the banner unless a tmux
|
|
# pane here already handled that event through its title. Sessions no terminal
|
|
# shows, and sessions in plain tabs, still notify, and only once. A
|
|
# "mosh-<port>" tag names the mosh connection, so the banner comes from the
|
|
# plain tab running that mosh-client.
|
|
#
|
|
# Setup: put NTFY_URL, NTFY_TOPIC and NTFY_TOKEN in ~/.config/ntfy/omo-remote.env
|
|
# (chmod 600), then run `omo-remote install` once.
|
|
#
|
|
# Usage: omo-remote title <pane> (run by the tmux pane-title-changed hook)
|
|
# omo-remote subscribe (run by the launchd agent)
|
|
# omo-remote message (run by ntfy for each push; reads $NTFY_*)
|
|
# omo-remote install (install and start the launchd agent)
|
|
|
|
# Hooks and launchd run with a minimal PATH; make Homebrew tools reachable.
|
|
export PATH="$PATH:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
|
|
label="local.omo-remote"
|
|
conf="${OMO_REMOTE_CONF:-$HOME/.config/ntfy/omo-remote.env}"
|
|
log="${OMO_REMOTE_LOG:-$HOME/.local/state/omo-remote.log}"
|
|
# An older event happened while you were away (the Mac slept or mosh was
|
|
# reconnecting): mark the tab, but skip the ding and the banner.
|
|
fresh=120
|
|
|
|
logline() {
|
|
mkdir -p "$(dirname "$log")"
|
|
printf '%s %s\n' "$(date '+%F %T')" "$*" >>"$log"
|
|
}
|
|
|
|
# Whether a tmux pane here running mosh or ssh got this event through its
|
|
# title: the same state and project, stamped within a few seconds of the push.
|
|
# The title usually lands first, so give it up to three seconds.
|
|
title_got_through() {
|
|
local state=$1 project=$2 sent=$3 i panes
|
|
for ((i = 0; i < 12; i++)); do
|
|
panes=$(tmux list-panes -a -F '#{pane_current_command} #{@omo_remote_event}' 2>/dev/null |
|
|
awk '$1 == "mosh-client" || $1 == "ssh"')
|
|
[[ -n $panes ]] || return 1 # no tmux pane here talks to a server
|
|
awk -v s="$state" -v p="$project" -v t="$sent" '
|
|
{ e = substr($0, index($0, " ") + 1)
|
|
i = index(e, ":"); st = substr(e, 1, i - 1); e = substr(e, i + 1)
|
|
i = index(e, ":"); w = substr(e, 1, i - 1) + 0; pr = substr(e, i + 1)
|
|
if (st == s && pr == p && w - t <= 5 && t - w <= 5) found = 1 }
|
|
END { exit !found }' <<<"$panes" && return 0
|
|
sleep 0.25
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# The terminal of a Ghostty tab running mosh or ssh outside tmux: the one on
|
|
# the mosh port the push names (tags ",mosh-<port>," in $1), else the newest.
|
|
plain_tab_tty() {
|
|
local panes
|
|
panes=$(tmux list-panes -a -F '#{pane_tty}' 2>/dev/null)
|
|
ps -A -o tty=,args= | PANES=$panes TAGS=$1 awk '
|
|
BEGIN { n = split(ENVIRON["PANES"], list, "\n"); for (i = 1; i <= n; i++) pane[list[i]] = 1 }
|
|
{ cmd = $2; sub(/.*\//, "", cmd) }
|
|
$1 == "??" || (("/dev/" $1) in pane) || (cmd != "mosh-client" && cmd != "ssh") { next }
|
|
{ tty = "/dev/" $1; if (cmd == "mosh-client" && index(ENVIRON["TAGS"], ",mosh-" $NF ",")) named = tty }
|
|
END { if (named != "") print named; else if (tty != "") print tty }'
|
|
}
|
|
|
|
case ${1:-} in
|
|
title)
|
|
pane=${2:?pane id required}
|
|
title=$(tmux display-message -p -t "$pane" '#{pane_title}' 2>/dev/null) || exit 0
|
|
[[ $title == *" · omo:"* ]] || exit 0
|
|
IFS=: read -r state when question <<<"${title#*" · omo:"}"
|
|
[[ $when =~ ^[0-9]+$ ]] || exit 0
|
|
project=${title%%" · omo:"*}
|
|
project=${project#"[mosh] "} # mosh puts "[mosh] " in front
|
|
case $project in "🔵 "* | "🟡 "* | "🟢 "* | "🔴 "*) project=${project#* } ;; esac
|
|
# Act once per event, however often the title is set again. The ntfy
|
|
# handler reads this to tell whether the title already got through.
|
|
event="$state:$when:$project"
|
|
[[ $(tmux show-option -pqv -t "$pane" @omo_remote_event) == "$event" ]] && exit 0
|
|
tmux set-option -p -t "$pane" @omo_remote_event "$event"
|
|
|
|
case $state in
|
|
working) payload='{"event":"UserPromptSubmit"}' ;;
|
|
done) payload='{"hook_event_name":"Stop","stopReason":"stop"}' ;;
|
|
error) payload='{"hook_event_name":"Stop","stopReason":"error"}' ;;
|
|
clear) payload='{"hook_event_name":"Stop","stopReason":"aborted"}' ;;
|
|
input) payload=$(jq -cn --arg q "$question" '{hook_event_name:"Notification",kind:"ask-user-asked",title:$q}') ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
if (($(date +%s) - when > fresh)); then
|
|
exec "$HOME/.local/bin/omo-status" set "$pane" "$state"
|
|
fi
|
|
payload=$(jq -c --arg cwd "${project:-remote}" '. + {cwd: $cwd}' <<<"$payload")
|
|
OMO_NOTIFY_PANE=$pane exec "$HOME/.local/bin/omo-notify" <<<"$payload"
|
|
;;
|
|
subscribe)
|
|
if [[ ! -r $conf ]]; then
|
|
echo "omo-remote: $conf is missing" >&2
|
|
exit 1
|
|
fi
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$conf"
|
|
set +a
|
|
: "${NTFY_URL:?NTFY_URL is not set in $conf}" "${NTFY_TOPIC:?NTFY_TOPIC is not set in $conf}"
|
|
# ntfy reads the token from $NTFY_TOKEN, which keeps it out of `ps`.
|
|
exec ntfy subscribe "${NTFY_URL%/}/$NTFY_TOPIC" "$(printf '%q' "$0") message"
|
|
;;
|
|
message)
|
|
tags=",${NTFY_TAGS:-},"
|
|
[[ $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 ;;
|
|
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.
|
|
if [[ $tags == *,attached,* ]] &&
|
|
title_got_through "$state" "${NTFY_TITLE##* · }" "${NTFY_TIME:-0}"; then
|
|
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 &
|
|
# 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")
|
|
[[ -n $target ]] ||
|
|
target=$(tmux list-clients -F '#{client_activity} #{client_tty}' 2>/dev/null | sort -rn | awk 'NR == 1 { print $2 }')
|
|
clean() { tr -d '\000-\037\177' <<<"$1" | tr ';' ','; }
|
|
if [[ -n $target && -w $target ]]; then
|
|
printf '\033]777;notify;%s;%s\a' "$(clean "omo: ${NTFY_TITLE:-remote session}")" \
|
|
"$(clean "$(head -n 1 <<<"${NTFY_MESSAGE:-}")")" >"$target"
|
|
fi
|
|
logline "ntfy $state ${NTFY_TITLE:-} target=${target:-none}"
|
|
;;
|
|
install)
|
|
if [[ ! -r $conf ]]; then
|
|
echo "omo-remote: put NTFY_URL, NTFY_TOPIC and NTFY_TOKEN in $conf first" >&2
|
|
exit 1
|
|
fi
|
|
command -v ntfy >/dev/null || {
|
|
echo "omo-remote: install the ntfy CLI first (brew install ntfy)" >&2
|
|
exit 1
|
|
}
|
|
plist="$HOME/Library/LaunchAgents/$label.plist"
|
|
mkdir -p "$(dirname "$log")" "$(dirname "$plist")"
|
|
cat >"$plist" <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>$label</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>$HOME/.local/bin/omo-remote</string>
|
|
<string>subscribe</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>ThrottleInterval</key>
|
|
<integer>30</integer>
|
|
<key>StandardOutPath</key>
|
|
<string>$log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>$log</string>
|
|
</dict>
|
|
</plist>
|
|
PLIST
|
|
launchctl bootout "gui/$(id -u)/$label" 2>/dev/null
|
|
launchctl bootstrap "gui/$(id -u)" "$plist" && echo "omo-remote: listening for ntfy pushes ($plist)"
|
|
;;
|
|
*)
|
|
sed -n '2,/^$/s/^# \{0,1\}//p' "$0" >&2
|
|
exit 2
|
|
;;
|
|
esac
|