Alert on tmux output while its terminal is unfocused
tmux only raises an activity flag for windows that are not currently
selected, so a long-running command in the window you left selected --
the common case, since you switch away by moving your Hyprland focus
elsewhere rather than by selecting another tmux window -- finished
silently and the indicator never lit.
Windows now also count as waiting when they are selected in an attached
client, every client showing them is unfocused, and their window_activity
is newer than an @omarchy_unfocused_activity watermark. The watermark is
stamped by a new `track` subcommand wired to client-focus-in/out and the
existing select-window hooks, so it records where attention last was.
The hooks pass #{window_id} and #{window_activity} as arguments, which
tmux expands when the hook fires; run-shell -b would otherwise let output
arriving during the handoff be swallowed by the new watermark. The focus
hooks take index 100 to leave a user's own bindings alone.
That state has no hook of its own, so it needs polling to be noticed.
The probe therefore moves out of the indicator and into a service plugin,
alongside nightlight and battery. A bar surface exists per monitor and
each one instantiates every indicator twice, once per block, so a timer
on the indicator meant a shell-out per instance per tick -- four probe
processes every three seconds on a two-monitor machine, forever. One
service polls for the whole shell instead.
Sharing the state also fixes what per-instance polling would have papered
over: each indicator used to own its own count, so a timer-driven update
only refreshed the bar it ran on and left the other monitors stale.
Refreshes still arrive over the existing indicator broadcast, which now
coalesces into a single run no matter how many bars relay it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
This commit is contained in:
co-authored by
Claude Opus 5
parent
2503edadfc
commit
e880cf77cc
@@ -6,6 +6,19 @@ 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
|
||||
|
||||
@@ -14,18 +27,32 @@ cat >"$test_dir/tmux" <<'STUB'
|
||||
|
||||
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'
|
||||
000:Work:1:editor
|
||||
100:Work:2:claude
|
||||
001:Side|Gig:1:server: still going
|
||||
@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)"}'
|
||||
@@ -37,7 +64,31 @@ output=$("$ROOT/bin/omarchy-tmux-alert" show)
|
||||
pass "tmux alert describes alerted windows"
|
||||
|
||||
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||
000:Work:1:editor
|
||||
@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)
|
||||
@@ -48,6 +99,19 @@ pass "tmux alert reports no alerted windows"
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user