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
+41
-10
@@ -8,24 +8,43 @@ set -e
|
|||||||
|
|
||||||
# Session names cannot contain colons, and window names can, so they always
|
# Session names cannot contain colons, and window names can, so they always
|
||||||
# come last when parsed.
|
# come last when parsed.
|
||||||
WINDOW_FORMAT='#{window_bell_flag}#{window_activity_flag}#{window_silence_flag}:#{session_name}:#{window_index}:#{window_name}'
|
WINDOW_FORMAT='#{window_id}:#{window_bell_flag}#{window_activity_flag}#{window_silence_flag}:#{window_activity}:#{@omarchy_unfocused_activity}:#{session_name}:#{window_index}:#{window_name}'
|
||||||
|
CLIENT_FORMAT='#{client_flags}:#{window_id}'
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: omarchy-tmux-alert <show [--json]|focus>" >&2
|
echo "Usage: omarchy-tmux-alert <show [--json]|focus>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Windows tmux has flagged with a bell, activity, or silence alert, which is the
|
# Include tmux alert flags plus output newer than the last focus transition for
|
||||||
# same state that highlights the window in the tmux status bar.
|
# selected windows whose attached terminals are all unfocused.
|
||||||
alerted_windows() {
|
alerted_windows() {
|
||||||
local flags session index name
|
local client_flags window_id flags activity seen_activity session index name
|
||||||
|
local waiting
|
||||||
|
local -A attached_windows=()
|
||||||
|
local -A focused_windows=()
|
||||||
|
|
||||||
tmux list-windows -a -F "$WINDOW_FORMAT" 2>/dev/null |
|
while IFS=':' read -r client_flags window_id; do
|
||||||
while IFS=':' read -r flags session index name; do
|
[[ -n $window_id ]] || continue
|
||||||
if [[ $flags == *1* ]]; then
|
attached_windows[$window_id]=1
|
||||||
printf '%s:%s:%s\n' "$session" "$index" "$name"
|
if [[ ,$client_flags, == *",focused,"* ]]; then
|
||||||
fi
|
focused_windows[$window_id]=1
|
||||||
done
|
fi
|
||||||
|
done < <(tmux list-clients -F "$CLIENT_FORMAT" 2>/dev/null)
|
||||||
|
|
||||||
|
while IFS=':' read -r window_id flags activity seen_activity session index name; do
|
||||||
|
waiting=0
|
||||||
|
[[ $flags == *1* ]] && waiting=1
|
||||||
|
|
||||||
|
if [[ ${attached_windows[$window_id]:-} == "1" &&
|
||||||
|
${focused_windows[$window_id]:-} != "1" &&
|
||||||
|
$activity =~ ^[0-9]+$ &&
|
||||||
|
$seen_activity =~ ^[0-9]+$ ]] && ((activity > seen_activity)); then
|
||||||
|
waiting=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
((waiting)) && printf '%s:%s:%s\n' "$session" "$index" "$name"
|
||||||
|
done < <(tmux list-windows -a -F "$WINDOW_FORMAT" 2>/dev/null)
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
@@ -73,6 +92,14 @@ most_recent_client() {
|
|||||||
tmux list-clients "$@" -F '#{client_activity}:#{client_tty}:#{client_pid}' 2>/dev/null | sort -rn | head -n1
|
tmux list-clients "$@" -F '#{client_activity}:#{client_tty}:#{client_pid}' 2>/dev/null | sort -rn | head -n1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
track() {
|
||||||
|
local window=${1:-} activity=${2:-}
|
||||||
|
|
||||||
|
[[ $window =~ ^@[0-9]+$ && $activity =~ ^[0-9]+$ ]] || return 0
|
||||||
|
tmux set-option -wq -t "$window" @omarchy_unfocused_activity "$activity" 2>/dev/null || return 0
|
||||||
|
omarchy-shell -q omarchy.indicators refresh
|
||||||
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
local target session index client tty pid
|
local target session index client tty pid
|
||||||
|
|
||||||
@@ -103,6 +130,10 @@ case "${1:-}" in
|
|||||||
focus)
|
focus)
|
||||||
focus
|
focus
|
||||||
;;
|
;;
|
||||||
|
track)
|
||||||
|
shift
|
||||||
|
track "$@"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
usage
|
usage
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ set -sg escape-time 10
|
|||||||
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
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 alert-activity 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||||
set-hook -g alert-silence 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
set-hook -g alert-silence '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 after-select-window 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
|
||||||
set-hook -g client-session-changed '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}"'
|
||||||
|
set-hook -g client-focus-in[100] 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
|
||||||
|
|
||||||
# Status bar
|
# Status bar
|
||||||
set -g status-position top
|
set -g status-position top
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
echo "Track tmux output while its terminal is unfocused"
|
||||||
|
|
||||||
|
tmux_config="$HOME/.config/tmux/tmux.conf"
|
||||||
|
|
||||||
|
if [[ -f $tmux_config ]] && ! grep -q 'client-focus-out\[100\].*omarchy-tmux-alert track' "$tmux_config"; then
|
||||||
|
sed -i \
|
||||||
|
-e 's|^set-hook -g after-select-window .*|set-hook -g after-select-window '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'"'|' \
|
||||||
|
-e 's|^set-hook -g client-session-changed .*|set-hook -g client-session-changed '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'"'|' \
|
||||||
|
"$tmux_config"
|
||||||
|
|
||||||
|
cat >>"$tmux_config" <<'EOF'
|
||||||
|
set-hook -g client-focus-out[100] 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
|
||||||
|
set-hook -g client-focus-in[100] 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
omarchy-restart-tmux
|
||||||
|
fi
|
||||||
@@ -1,41 +1,23 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Ui
|
import qs.Ui
|
||||||
|
|
||||||
BarIndicator {
|
BarIndicator {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int waitingCount: 0
|
readonly property var tmuxService: bar?.shell?.firstPartyServiceFor("omarchy.tmux")
|
||||||
property string tooltip: ""
|
|
||||||
property bool refreshPending: false
|
|
||||||
|
|
||||||
active: waitingCount > 0
|
active: tmuxService ? tmuxService.waiting : false
|
||||||
activeText: ""
|
activeText: ""
|
||||||
inactiveText: ""
|
inactiveText: ""
|
||||||
activeTooltipText: tooltip
|
activeTooltipText: tmuxService ? tmuxService.tooltip : ""
|
||||||
inactiveTooltipText: "No Terminal Waiting"
|
inactiveTooltipText: "No Terminal Waiting"
|
||||||
|
|
||||||
|
// The service owns the probe and its polling; the tmux hooks still reach it
|
||||||
|
// through the indicator host's refresh broadcast, which coalesces into a
|
||||||
|
// single run no matter how many bars relay it.
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (statusProc.running) refreshPending = true
|
if (root.tmuxService) root.tmuxService.refresh()
|
||||||
else statusProc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(raw) {
|
|
||||||
var data = extractData(raw)
|
|
||||||
waitingCount = Number(data.count || 0)
|
|
||||||
tooltip = String(data.tooltip || "")
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: refresh()
|
|
||||||
|
|
||||||
// Alerts can also disappear without a hook, such as when the window or its
|
|
||||||
// session is killed while still flagged.
|
|
||||||
Timer {
|
|
||||||
interval: 5000
|
|
||||||
running: root.active
|
|
||||||
repeat: true
|
|
||||||
onTriggered: root.refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
@@ -58,26 +40,6 @@ BarIndicator {
|
|||||||
function onRefreshRequested() { root.refresh() }
|
function onRefreshRequested() { root.refresh() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: statusProc
|
|
||||||
command: ["omarchy-tmux-alert", "show", "--json"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
waitForEnd: true
|
|
||||||
onStreamFinished: root.update(text)
|
|
||||||
}
|
|
||||||
onExited: function(exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
root.waitingCount = 0
|
|
||||||
root.tooltip = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.refreshPending) {
|
|
||||||
root.refreshPending = false
|
|
||||||
root.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPressed: function() {
|
onPressed: function() {
|
||||||
Quickshell.execDetached(["omarchy-tmux-alert", "focus"])
|
Quickshell.execDetached(["omarchy-tmux-alert", "focus"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import "TmuxModel.js" as TmuxModel
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Injected by omarchy-shell (the first-party service loader).
|
||||||
|
property var shell: null
|
||||||
|
|
||||||
|
property int waitingCount: 0
|
||||||
|
property string tooltip: ""
|
||||||
|
property bool refreshPending: false
|
||||||
|
|
||||||
|
readonly property bool waiting: waitingCount > 0
|
||||||
|
|
||||||
|
// A bar surface exists per monitor and each one instantiates the indicator
|
||||||
|
// twice (active and inactive block), so the probe lives here instead: one
|
||||||
|
// process for the whole shell, and every indicator reads the same answer.
|
||||||
|
function refresh() {
|
||||||
|
if (statusProc.running) refreshPending = true
|
||||||
|
else statusProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// tmux hooks cover the flag transitions, but output in a selected window
|
||||||
|
// whose terminal sits on another workspace never fires one, and neither does
|
||||||
|
// a window or session killed while it was still flagged.
|
||||||
|
Timer {
|
||||||
|
interval: 3000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: statusProc
|
||||||
|
command: ["omarchy-tmux-alert", "show", "--json"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
waitForEnd: true
|
||||||
|
onStreamFinished: {
|
||||||
|
var waiting = TmuxModel.waitingFromOutput(text)
|
||||||
|
root.waitingCount = waiting.count
|
||||||
|
root.tooltip = waiting.tooltip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: function(exitCode) {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.waitingCount = 0
|
||||||
|
root.tooltip = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.refreshPending) {
|
||||||
|
root.refreshPending = false
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refresh()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Shape of `omarchy-tmux-alert show --json`. The command emits one JSON object
|
||||||
|
// on the last line; anything tmux or a shell profile printed before it is
|
||||||
|
// ignored so a noisy environment cannot blank the indicator.
|
||||||
|
function waitingFromOutput(output) {
|
||||||
|
var text = String(output === undefined || output === null ? "" : output).trim()
|
||||||
|
if (!text) return { count: 0, tooltip: "" }
|
||||||
|
|
||||||
|
var lines = text.split("\n")
|
||||||
|
var data
|
||||||
|
try {
|
||||||
|
data = JSON.parse(lines[lines.length - 1])
|
||||||
|
} catch (e) {
|
||||||
|
return { count: 0, tooltip: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || typeof data !== "object") return { count: 0, tooltip: "" }
|
||||||
|
|
||||||
|
var count = Number(data.count)
|
||||||
|
return {
|
||||||
|
count: isFinite(count) && count > 0 ? Math.floor(count) : 0,
|
||||||
|
tooltip: data.tooltip === undefined || data.tooltip === null ? "" : String(data.tooltip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = {
|
||||||
|
waitingFromOutput: waitingFromOutput
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "omarchy.tmux",
|
||||||
|
"name": "Tmux",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Omarchy",
|
||||||
|
"description": "Owns the set of tmux windows waiting for attention for the bar indicator.",
|
||||||
|
"kinds": [
|
||||||
|
"service"
|
||||||
|
],
|
||||||
|
"entryPoints": {
|
||||||
|
"service": "Service.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,19 @@ source "$(dirname "$0")/base-test.sh"
|
|||||||
|
|
||||||
require_command jq
|
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)
|
test_dir=$(mktemp -d)
|
||||||
trap 'rm -rf "$test_dir"' EXIT
|
trap 'rm -rf "$test_dir"' EXIT
|
||||||
|
|
||||||
@@ -14,18 +27,32 @@ cat >"$test_dir/tmux" <<'STUB'
|
|||||||
|
|
||||||
if [[ $1 == "list-windows" ]]; then
|
if [[ $1 == "list-windows" ]]; then
|
||||||
cat "$TMUX_STUB_WINDOWS"
|
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
|
fi
|
||||||
STUB
|
STUB
|
||||||
chmod +x "$test_dir/tmux"
|
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_WINDOWS="$test_dir/windows"
|
||||||
|
export TMUX_STUB_CLIENTS="$test_dir/clients"
|
||||||
|
export TMUX_STUB_CALLS="$test_dir/calls"
|
||||||
export PATH="$test_dir:$PATH"
|
export PATH="$test_dir:$PATH"
|
||||||
|
|
||||||
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||||
000:Work:1:editor
|
@1:000:100::Work:1:editor
|
||||||
100:Work:2:claude
|
@2:100:110::Work:2:claude
|
||||||
001:Side|Gig:1:server: still going
|
@3:001:120::Side|Gig:1:server: still going
|
||||||
WINDOWS
|
WINDOWS
|
||||||
|
: >"$TMUX_STUB_CLIENTS"
|
||||||
|
|
||||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||||
expected='{"count":2,"tooltip":"claude (Work:2), server: still going (Side|Gig:1)"}'
|
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"
|
pass "tmux alert describes alerted windows"
|
||||||
|
|
||||||
cat >"$TMUX_STUB_WINDOWS" <<'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
|
WINDOWS
|
||||||
|
|
||||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
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"
|
pass "tmux alert stays quiet without alerts"
|
||||||
|
|
||||||
: >"$TMUX_STUB_WINDOWS"
|
: >"$TMUX_STUB_WINDOWS"
|
||||||
|
: >"$TMUX_STUB_CLIENTS"
|
||||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||||
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert handles a missing tmux server" "$output"
|
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert handles a missing tmux server" "$output"
|
||||||
pass "tmux alert handles a missing tmux server"
|
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