This tmux alert system didn't work as nicely as I imagined

This commit is contained in:
David Heinemeier Hansson
2026-07-27 15:32:56 -07:00
parent b6e1a43278
commit bdcdfeb428
19 changed files with 376 additions and 515 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ export PATH="$test_dir/bin:$PATH"
export OMARCHY_PATH="$ROOT"
export XDG_RUNTIME_DIR="$test_dir/run"
# tmux hooks run without WAYLAND_DISPLAY, and qs matches instances by display.
# Callers from a stripped environment have no WAYLAND_DISPLAY, and qs matches
# instances by display.
output=$(env -u WAYLAND_DISPLAY "$ROOT/bin/omarchy-shell" omarchy.indicators refresh)
[[ $output == "display=[wayland-1]" ]] || fail "shell ipc recovers a missing display" "$output"
pass "shell ipc recovers a missing display"
@@ -0,0 +1,246 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1785189600.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/bin"
# Everything the migration reaches for is stubbed: a real tmux here would talk
# to the developer's own server.
cat >"$test_dir/bin/tmux" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$TMUX_CALLS"
case "$1" in
has-session) exit "${TMUX_SERVER_MISSING:-0}" ;;
show-hooks)
cat <<'HOOKS'
after-select-window[0] run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"
alert-activity
alert-bell[0] run-shell -b "omarchy-shell -q omarchy.indicators refresh"
client-focus-in[100] run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"
client-focus-out[42] display-message "user hook"
session-created
HOOKS
;;
list-windows) printf '@0\n@3\n' ;;
esac
STUB
cat >"$test_dir/bin/omarchy-restart-tmux" <<'STUB'
#!/bin/bash
echo restart >>"$TMUX_RESTARTS"
STUB
cat >"$test_dir/bin/omarchy-restart-shell" <<'STUB'
#!/bin/bash
echo restart >>"$SHELL_RESTARTS"
exit "${SHELL_RESTART_STATUS:-0}"
STUB
cat >"$test_dir/bin/omarchy-state" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$STATE_CALLS"
STUB
chmod +x "$test_dir/bin/"*
export TMUX_CALLS="$test_dir/tmux-calls"
export TMUX_RESTARTS="$test_dir/tmux-restarts"
export SHELL_RESTARTS="$test_dir/shell-restarts"
export STATE_CALLS="$test_dir/state-calls"
home="$test_dir/home"
tmux_config="$home/.config/tmux/tmux.conf"
shell_config="$home/.config/omarchy/shell.json"
run_migration() {
: >"$TMUX_CALLS"
: >"$TMUX_RESTARTS"
: >"$SHELL_RESTARTS"
: >"$STATE_CALLS"
HOME="$home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null
}
reset_home() {
rm -rf "$home"
mkdir -p "$home/.config/tmux" "$home/.config/omarchy"
}
# ---------------------------------------------------------------- tmux config
# The shipped config with the alert block put back is exactly what an installed
# machine has, so cleaning it has to land on the shipped config byte for byte.
reset_home
awk '
/^# Status bar$/ {
print "# Alerts"
print "set-hook -g alert-bell '\''run-shell -b \"omarchy-shell -q omarchy.indicators refresh\"'\''"
print "set-hook -g alert-activity '\''run-shell -b \"omarchy-shell -q omarchy.indicators refresh\"'\''"
print "set-hook -g alert-silence '\''run-shell -b \"omarchy-shell -q omarchy.indicators refresh\"'\''"
print "set-hook -g after-select-window '\''run-shell -b \"omarchy-tmux-alert track #{window_id} #{window_activity}\"'\''"
print "set-hook -g client-session-changed '\''run-shell -b \"omarchy-tmux-alert track #{window_id} #{window_activity}\"'\''"
print "set-hook -g client-focus-out[100] '\''run-shell -b \"omarchy-tmux-alert track #{window_id} #{window_activity}\"'\''"
print "set-hook -g client-focus-in[100] '\''run-shell -b \"omarchy-tmux-alert track #{window_id} #{window_activity}\"'\''"
print ""
}
{ print }
' "$ROOT/config/tmux/tmux.conf" >"$tmux_config"
run_migration
diff -u "$ROOT/config/tmux/tmux.conf" "$tmux_config" >/dev/null ||
fail "alert removal restores the shipped tmux config" "$(diff -u "$ROOT/config/tmux/tmux.conf" "$tmux_config")"
pass "alert removal restores the shipped tmux config"
(($(wc -l <"$TMUX_RESTARTS") == 1)) || fail "alert removal reloads tmux once"
pass "alert removal reloads tmux once"
before=$(sha256sum "$tmux_config")
run_migration
[[ $before == $(sha256sum "$tmux_config") ]] || fail "alert removal is idempotent"
[[ ! -s $TMUX_RESTARTS ]] || fail "idempotent alert removal does not reload tmux"
pass "alert removal is idempotent"
# The hooks the removed migrations appended sit at the end of the file, and a
# user's own hooks and blank lines have to survive next to them.
reset_home
cat >"$tmux_config" <<'EOF'
set -g mouse on
# Custom
set-hook -g alert-bell 'display-message "mine"'
set-hook -g client-focus-out[42] 'display-message "custom focus hook"'
# Alerts
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g alert-activity 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g after-select-window 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g client-session-changed 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
set-hook -g client-focus-out[100] 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
EOF
run_migration
expected=$(printf '%s\n' 'set -g mouse on' '' '' '# Custom' "set-hook -g alert-bell 'display-message \"mine\"'" "set-hook -g client-focus-out[42] 'display-message \"custom focus hook\"'")
[[ $(cat "$tmux_config") == "$expected" ]] ||
fail "alert removal drops an appended block and keeps the user's lines" "$(cat -A "$tmux_config")"
pass "alert removal drops an appended block and keeps the user's lines"
# An older config kept the refresh spelling of these two hooks; an indented one
# is still the hook Omarchy wrote.
grep -q 'after-select-window\|client-session-changed' "$tmux_config" &&
fail "alert removal drops the older and indented hook spellings"
pass "alert removal drops the older and indented hook spellings"
# A config that never had the feature is not a config to rewrite.
reset_home
printf '%s\n' 'set -g mouse on' '' '' 'set -g status-position top' '' >"$tmux_config"
before=$(sha256sum "$tmux_config")
run_migration
[[ $before == $(sha256sum "$tmux_config") ]] || fail "alert removal leaves an unrelated config alone" "$(cat -A "$tmux_config")"
[[ ! -s $TMUX_RESTARTS ]] || fail "alert removal does not reload tmux for an unrelated config"
pass "alert removal leaves an unrelated config alone"
# Dotfile setups symlink the config; the link has to survive the rewrite.
reset_home
mkdir -p "$home/dotfiles"
cat >"$home/dotfiles/tmux.conf" <<'EOF'
set -g mouse on
# Alerts
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
EOF
ln -sf "$home/dotfiles/tmux.conf" "$tmux_config"
run_migration
[[ -L $tmux_config ]] || fail "alert removal writes through a symlinked config"
[[ $(readlink "$tmux_config") == "$home/dotfiles/tmux.conf" ]] || fail "alert removal keeps the symlink target"
grep -q 'alert-bell' "$home/dotfiles/tmux.conf" && fail "alert removal cleans the symlink target"
pass "alert removal writes through a symlinked config"
# --------------------------------------------------------------- live server
reset_home
run_migration
grep -Fxq 'set-hook -gu after-select-window[0]' "$TMUX_CALLS" || fail "alert removal unsets live hooks by index" "$(cat "$TMUX_CALLS")"
grep -Fxq 'set-hook -gu alert-bell[0]' "$TMUX_CALLS" || fail "alert removal unsets live alert hooks"
grep -Fxq 'set-hook -gu client-focus-in[100]' "$TMUX_CALLS" || fail "alert removal unsets live focus hooks"
grep -q 'set-hook -gu client-focus-out\[42\]' "$TMUX_CALLS" && fail "alert removal keeps a user's hook at another index"
pass "alert removal unsets the live hooks it installed"
grep -Fxq 'set-option -wqu -t @0 @omarchy_unfocused_activity' "$TMUX_CALLS" || fail "alert removal clears tracked window options"
grep -Fxq 'set-option -wqu -t @3 @omarchy_unfocused_activity' "$TMUX_CALLS" || fail "alert removal clears every tracked window"
pass "alert removal clears the window options it stamped"
TMUX_SERVER_MISSING=1 run_migration
grep -q 'set-hook' "$TMUX_CALLS" && fail "alert removal skips a server that is not running" "$(cat "$TMUX_CALLS")"
pass "alert removal skips a server that is not running"
# ---------------------------------------------------------------- shell.json
reset_home
cat >"$shell_config" <<'EOF'
{
"bar": {
"layout": {
"left": [
{ "id": "omarchy.indicators", "items": ["TmuxAlert", "Dnd", { "id": "TmuxAlert" }, { "id": "NightLight" }] }
],
"center": [
{ "id": "omarchy.indicators", "items": ["TmuxAlert"] },
{ "id": "omarchy.clock" }
],
"right": [
{ "id": "omarchy.indicators", "indicators": ["TmuxAlert", "Dnd"] },
{ "id": "omarchy.indicators", "items": [] }
]
}
}
}
EOF
run_migration
grep -q 'TmuxAlert' "$shell_config" && fail "alert removal strips TmuxAlert from shell.json" "$(cat "$shell_config")"
pass "alert removal strips TmuxAlert from shell.json"
[[ $(jq -c '.bar.layout.left[0].items' "$shell_config") == '["Dnd",{"id":"NightLight"}]' ]] ||
fail "alert removal keeps the other picked indicators" "$(jq -c '.bar.layout.left[0].items' "$shell_config")"
pass "alert removal keeps the other picked indicators"
[[ $(jq -c '[.bar.layout.center[].id]' "$shell_config") == '["omarchy.clock"]' ]] ||
fail "alert removal drops a widget left with nothing to show" "$(jq -c '.bar.layout.center' "$shell_config")"
pass "alert removal drops a widget left with nothing to show"
[[ $(jq -c '.bar.layout.right[0].indicators' "$shell_config") == '["Dnd"]' ]] ||
fail "alert removal handles the older indicators key" "$(jq -c '.bar.layout.right[0]' "$shell_config")"
pass "alert removal handles the older indicators key"
[[ $(jq -c '.bar.layout.right[1]' "$shell_config") == '{"id":"omarchy.indicators","items":[]}' ]] ||
fail "alert removal leaves an already-empty list alone" "$(jq -c '.bar.layout.right[1]' "$shell_config")"
pass "alert removal leaves an already-empty list alone"
(($(wc -l <"$SHELL_RESTARTS") == 1)) || fail "alert removal restarts the shell"
[[ ! -s $STATE_CALLS ]] || fail "a restarted shell needs no deferred restart" "$(cat "$STATE_CALLS")"
pass "alert removal restarts the shell"
# Migrations run from a TTY or over ssh have no shell to restart, and stopping
# there would strand every migration queued behind this one.
reset_home
SHELL_RESTART_STATUS=1 run_migration
grep -Fxq 'set restart-shell-required' "$STATE_CALLS" || fail "an unavailable shell defers its restart" "$(cat "$STATE_CALLS")"
pass "an unavailable shell defers its restart"
-117
View File
@@ -1,117 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
require_command jq
run_node_test <<'JS'
const tmux = requireFromRoot('shell/plugins/services/tmux/TmuxModel.js')
assertEqual(tmux.waitingFromOutput('{"count":2,"tooltip":"claude (Work:2)"}').count, 2, 'tmux model reads the waiting count')
assertEqual(tmux.waitingFromOutput('{"count":2,"tooltip":"claude (Work:2)"}').tooltip, 'claude (Work:2)', 'tmux model reads the tooltip')
assertEqual(tmux.waitingFromOutput('shell noise\n{"count":1,"tooltip":"editor (Work:1)"}').count, 1, 'tmux model ignores output before the json line')
assertEqual(tmux.waitingFromOutput('').count, 0, 'tmux model treats empty output as nothing waiting')
assertEqual(tmux.waitingFromOutput('not json').count, 0, 'tmux model treats unparseable output as nothing waiting')
assertEqual(tmux.waitingFromOutput('not json').tooltip, '', 'tmux model blanks the tooltip on unparseable output')
assertEqual(tmux.waitingFromOutput('{"count":-3}').count, 0, 'tmux model clamps a negative count')
assertEqual(tmux.waitingFromOutput('{"tooltip":"editor (Work:1)"}').count, 0, 'tmux model defaults a missing count')
JS
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
cat >"$test_dir/tmux" <<'STUB'
#!/bin/bash
if [[ $1 == "list-windows" ]]; then
cat "$TMUX_STUB_WINDOWS"
elif [[ $1 == "list-clients" ]]; then
cat "$TMUX_STUB_CLIENTS"
elif [[ $1 == "set-option" ]]; then
printf '%s\n' "$*" >>"$TMUX_STUB_CALLS"
fi
STUB
chmod +x "$test_dir/tmux"
cat >"$test_dir/omarchy-shell" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$TMUX_STUB_CALLS"
STUB
chmod +x "$test_dir/omarchy-shell"
export TMUX_STUB_WINDOWS="$test_dir/windows"
export TMUX_STUB_CLIENTS="$test_dir/clients"
export TMUX_STUB_CALLS="$test_dir/calls"
export PATH="$test_dir:$PATH"
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
@1:000:100::Work:1:editor
@2:100:110::Work:2:claude
@3:001:120::Side|Gig:1:server: still going
WINDOWS
: >"$TMUX_STUB_CLIENTS"
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
expected='{"count":2,"tooltip":"claude (Work:2), server: still going (Side|Gig:1)"}'
[[ $output == "$expected" ]] || fail "tmux alert reports alerted windows" "expected: $expected"$'\n'"actual: $output"
pass "tmux alert reports alerted windows"
output=$("$ROOT/bin/omarchy-tmux-alert" show)
[[ $output == "claude (Work:2), server: still going (Side|Gig:1)" ]] || fail "tmux alert describes alerted windows" "$output"
pass "tmux alert describes alerted windows"
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
@1:000:200:100:Work:1:editor
WINDOWS
echo 'attached,UTF-8:@1' >"$TMUX_STUB_CLIENTS"
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
expected='{"count":1,"tooltip":"editor (Work:1)"}'
[[ $output == "$expected" ]] || fail "tmux alert reports output in an unfocused active window" "expected: $expected"$'\n'"actual: $output"
pass "tmux alert reports output in an unfocused active window"
echo 'attached,focused,UTF-8:@1' >"$TMUX_STUB_CLIENTS"
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert ignores output in a focused active window" "$output"
pass "tmux alert ignores output in a focused active window"
cat >"$TMUX_STUB_CLIENTS" <<'CLIENTS'
attached,UTF-8:@1
attached,focused,UTF-8:@1
CLIENTS
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert ignores a window visible in another focused client" "$output"
pass "tmux alert ignores a window visible in another focused client"
echo 'attached,UTF-8:@1' >"$TMUX_STUB_CLIENTS"
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
@1:000:200:200:Work:1:editor
WINDOWS
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert reports no alerted windows" "$output"
pass "tmux alert reports no alerted windows"
[[ -z $("$ROOT/bin/omarchy-tmux-alert" show) ]] || fail "tmux alert stays quiet without alerts"
pass "tmux alert stays quiet without alerts"
: >"$TMUX_STUB_WINDOWS"
: >"$TMUX_STUB_CLIENTS"
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert handles a missing tmux server" "$output"
pass "tmux alert handles a missing tmux server"
: >"$TMUX_STUB_CALLS"
"$ROOT/bin/omarchy-tmux-alert" track @3 345
expected=$'set-option -wq -t @3 @omarchy_unfocused_activity 345\n-q omarchy.indicators refresh'
output=$(cat "$TMUX_STUB_CALLS")
[[ $output == "$expected" ]] || fail "tmux alert records the activity watermark and refreshes indicators" "expected: $expected"$'\n'"actual: $output"
pass "tmux alert records the activity watermark and refreshes indicators"
: >"$TMUX_STUB_CALLS"
"$ROOT/bin/omarchy-tmux-alert" track invalid nope
[[ ! -s $TMUX_STUB_CALLS ]] || fail "tmux alert ignores invalid tracking arguments" "$(cat "$TMUX_STUB_CALLS")"
pass "tmux alert ignores invalid tracking arguments"
@@ -1,54 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1784955584.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/home/.config/tmux" "$test_dir/bin"
cat >"$test_dir/home/.config/tmux/tmux.conf" <<'EOF'
# Alerts
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g after-select-window 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g client-session-changed 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g client-focus-out[42] 'display-message "custom focus hook"'
EOF
cat >"$test_dir/bin/omarchy-restart-tmux" <<'EOF'
#!/bin/bash
echo restart >>"$TMUX_MIGRATION_RESTART_LOG"
EOF
chmod +x "$test_dir/bin/omarchy-restart-tmux"
export TMUX_MIGRATION_RESTART_LOG="$test_dir/restarts"
tmux_config="$test_dir/home/.config/tmux/tmux.conf"
HOME="$test_dir/home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null
grep -Fq 'after-select-window '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
fail "tmux activity migration tracks newly selected windows"
grep -Fq 'client-session-changed '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
fail "tmux activity migration tracks session changes"
grep -Fq 'client-focus-out[100] '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
fail "tmux activity migration tracks terminal focus loss"
grep -Fq 'client-focus-in[100] '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
fail "tmux activity migration tracks terminal focus gain"
grep -Fq 'client-focus-out[42] '"'"'display-message "custom focus hook"'"'" "$tmux_config" ||
fail "tmux activity migration preserves custom indexed focus hooks"
restart_count=$(wc -l <"$TMUX_MIGRATION_RESTART_LOG")
((restart_count == 1)) || fail "tmux activity migration reloads tmux once"
pass "tmux activity migration installs focus-aware hooks"
before=$(sha256sum "$tmux_config")
HOME="$test_dir/home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null
after=$(sha256sum "$tmux_config")
[[ $before == "$after" ]] || fail "tmux activity migration is idempotent"
restart_count=$(wc -l <"$TMUX_MIGRATION_RESTART_LOG")
((restart_count == 1)) || fail "idempotent tmux activity migration does not reload tmux"
pass "tmux activity migration is idempotent"