[Sync] Lua Hyprland chain, licence cleanup

This commit is contained in:
Coja committed 2026-11-09 18:25:35 +01:00
1 parent 93b4f695ce
commit 47166d877f
183 files changed
+5618 -5839

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# agent-next.sh: switch to the next window (after the current one, wrapping) that holds an
# agent mark you have not looked at yet; with none left, fall back to any grey mark; with
# nothing marked, say so. Bound to prefix+Tab and to the » tap target in the status bar.
cur=$(tmux display -p '#{window_index}')
pick() { # $1 empty = unseen marks only, "any" = grey ones too
tmux list-panes -a -F '#{window_index} #{@agent} #{@agent_seen}' |
awk -v c="$cur" -v any="$1" '
$2 != "" && $2 != "busy" && (any != "" || $3 == "") { if ($1 > c && !n) n = $1; if (!f) f = $1 }
END { if (n) print n; else if (f) print f }'
}
w=$(pick); [ -n "$w" ] || w=$(pick any)
if [ -n "$w" ]; then tmux select-window -t ":$w"; else tmux display-message "no agent waiting"; fi
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
# agent-state.sh <cc|oc|pi|cx> <busy|done|input|error|off>
#
# Per-pane "what is the agent in this pane doing" for tmux, shared by every agent's hooks:
# Claude Code (claude/settings.json), codex (~/.codex/hooks.json), pi (agent-state.ts
# extension) and opencode (plugins/agent-state.ts). No-op outside tmux.
#
# Which pane: $TMUX_PANE when the agent runs in the pane itself (pi, opencode, codex). Claude
# Code sessions run in a worker the claude daemon pre-spawned ("claude bg-spare"), whose
# environment has no tmux variables, so for them the worker is found by walking up the parent
# chain to the pid registered in ~/.claude/sessions/<pid>.json, and the pane is the one whose
# title Claude set to that session's name ("✳ <name>").
#
# Pane options read by tmux.conf's @agent_badge: @agent (the state, drawn as ✓ ? ! after the
# window name; busy draws nothing), @agent_seen (set by tmux's pane-focus-in hook: the glyph
# loses its colour once you visit the pane and stays, grey, until the next prompt turns the
# state busy) and @agent_tool (only the termux notification title uses it). `off` unsets all.
#
# Phone-level notice only for a NEW mark on a pane that is not in view: termux-notification
# when the agent runs natively in termux, otherwise a BEL into the pane (tmux forwards the bell
# to the outer terminal; termux's default bell-character=vibrate buzzes, and the window gets
# tmux's bell flag). A state change on an already-marked pane (e.g. Claude's idle_prompt ~60s
# after Stop turning done into input) only swaps the glyph: no second buzz, seen stays as is.
command -v tmux >/dev/null 2>&1 || exit 0
tool=$1 state=$2 pane=$TMUX_PANE
if [ -z "$pane" ]; then
p=$$ worker=""
for _ in 1 2 3 4 5 6 7 8; do
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
[ -n "$p" ] && [ "$p" != 1 ] || break
[ -f "$HOME/.claude/sessions/$p.json" ] && { worker=$p; break; }
done
[ -n "$worker" ] || exit 0
name=$(python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1])).get("name",""))' \
"$HOME/.claude/sessions/$worker.json" 2>/dev/null)
[ -n "$name" ] || exit 0
pane=$(tmux list-panes -a -F '#{pane_id} #{pane_title}' 2>/dev/null |
awk -F'\t' -v n="$name" '$2==n || substr($2,length($2)-length(n))==" " n {print $1; exit}')
[ -n "$pane" ] || exit 0
fi
if [ "$state" = off ]; then
for o in @agent @agent_seen @agent_tool; do tmux set -p -t "$pane" -u "$o" 2>/dev/null; done
exit 0
fi
tmux set -p -t "$pane" @agent_tool "$tool" 2>/dev/null || exit 0 # no server reachable (sandbox): give up quietly
case $state in
busy)
tmux set -p -t "$pane" @agent busy
tmux set -p -t "$pane" -u @agent_seen 2>/dev/null
command -v termux-notification-remove >/dev/null 2>&1 && termux-notification-remove "agent-$pane"
;;
*)
already=$(tmux show -pv -t "$pane" @agent 2>/dev/null)
tmux set -p -t "$pane" @agent "$state"
if [ "$(tmux display -p -t "$pane" '#{pane_active}#{window_active}')" = 11 ]; then
tmux set -p -t "$pane" @agent_seen 1 # in view: you saw it happen, so it starts out seen
exit 0
fi
[ -n "$already" ] && [ "$already" != busy ] && exit 0 # glyph swapped on an existing mark
tmux set -p -t "$pane" -u @agent_seen 2>/dev/null
if command -v termux-notification >/dev/null 2>&1; then
termux-notification --id "agent-$pane" --title "$tool: $state" --content "${PWD##*/}" \
--vibrate 200 --action "tmux select-pane -t $pane"
else
printf '\a' > "$(tmux display -p -t "$pane" '#{pane_tty}')" 2>/dev/null
fi
;;
esac
exit 0
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# CPU utilisation (%) averaged over the interval since the PREVIOUS status refresh:
# /proc/stat totals persist in a state file between runs (same pattern as net.sh),
# so the value covers the whole status-interval. (The old version slept 0.3s and
# sampled just that window — a 6% duty cycle that aliased bursty loads.) No sleep,
# ~5ms. First run (no state yet) shows the since-boot average. Glyph = Waybar's
# cpu module icon (U+F035B).
printf '\U000f035b '
rt="${XDG_RUNTIME_DIR:-/tmp}"
st="$rt/tmux_cpu_stat" # previous totals: tot idle
read -r _ u n s i w rest </proc/stat
idle=$((i + w))
tot=$((u + n + s + i + w))
for v in $rest; do tot=$((tot + v)); done
p_tot=0
p_idle=0
[ -r "$st" ] && read -r p_tot p_idle <"$st"
[ "$p_tot" -gt "$tot" ] && {
p_tot=0
p_idle=0
} # stale state from a previous boot
printf '%s %s\n' "$tot" "$idle" >"$st"
dt=$((tot - p_tot))
di=$((idle - p_idle))
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
# RAM utilisation (%) — used = MemTotal - MemAvailable, from /proc/meminfo.
# Called from tmux status-right via #(); cached per status-interval.
printf '\U000f0f86 ' # Waybar's memory glyph (U+F0F86), then the percentage
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# metrics.sh full|compact — the status-right system segment in ONE process per refresh.
# (tmux used to spawn net dl, net ul, cpu, mem and temp as five separate #() jobs per tick.)
# Prints #[fg=…] style markers, which tmux honours inside #() output.
# full : net dl · net ul · cpu · mem · temp (temp vanishes when no sensor exists)
# compact : cpu · mem
# CPU and net are averaged over the interval since the PREVIOUS refresh: the /proc totals
# persist in $XDG_RUNTIME_DIR state files, so no sleeps and the value covers the whole
# status-interval. First run (no state yet) shows the since-boot average. Glyphs are Waybar's:
# cpu U+F035B, memory U+F0F86, thermometer U+F050F, download U+F019, upload U+F093.
mode=${1:-full}
rt="${XDG_RUNTIME_DIR:-/tmp}"
cpu() { # utilisation % since the previous refresh
local st="$rt/tmux_cpu_stat" u n s i w rest idle tot v p_tot=0 p_idle=0 dt di
read -r _ u n s i w rest </proc/stat
idle=$((i + w))
tot=$((u + n + s + i + w))
for v in $rest; do tot=$((tot + v)); done
[ -r "$st" ] && read -r p_tot p_idle <"$st"
[ "$p_tot" -gt "$tot" ] && { p_tot=0; p_idle=0; } # stale state from a previous boot
printf '%s %s\n' "$tot" "$idle" >"$st"
dt=$((tot - p_tot))
di=$((idle - p_idle))
printf '\U000f035b '
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
}
mem() { # used = MemTotal - MemAvailable
printf '\U000f0f86 '
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
}
temp() { # CPU °C: a CPU-ish thermal zone, else a CPU hwmon chip, else the hottest zone; nothing if none
local cpu_zone="" any_zone="" hw="" d t ty f
for d in /sys/class/thermal/thermal_zone*/; do
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
t=$(<"${d}temp")
case $t in '' | *[!0-9]*) continue ;; esac
ty=$(<"${d}type"); ty=${ty,,}
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi ;;
esac
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
done
if [ -z "$cpu_zone" ]; then
for d in /sys/class/hwmon/hwmon*/; do
[ -r "${d}name" ] || continue
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
for f in "${d}"temp*_input; do
[ -r "$f" ] || continue
t=$(<"$f")
case $t in '' | *[!0-9]*) continue ;; esac
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
done ;;
esac
done
fi
t=${cpu_zone:-${hw:-$any_zone}}
[ -n "$t" ] || return 0
printf '\U000f050f %d° ' $(((t + 500) / 1000))
}
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
while (b >= 1024 && i < 5) { b /= 1024; i++ }
if (i == 1) printf "%d%s", b, u[i]; else printf "%.1f%s", b, u[i] }'; }
net() { # sets dl/ul: bytes per second over all non-lo interfaces since the previous refresh
local ctr="$rt/tmux_net_ctr" now rx tx prx ptx pnow drx=0 dtx=0 dt=1
now=$(date +%s.%N)
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
if [ -r "$ctr" ]; then
read -r prx ptx pnow <"$ctr"
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
drx=$((rx - prx)); dtx=$((tx - ptx))
[ "$drx" -lt 0 ] && drx=0
[ "$dtx" -lt 0 ] && dtx=0
fi
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
}
case $mode in
compact)
printf '#[fg=#89b4fa]%s #[fg=#94e2d5]%s' "$(cpu)" "$(mem)"
;;
*)
net
printf '#[fg=#a6e3a1]\U0000f019 %s #[fg=#f9e2af]\U0000f093 %s #[fg=#89b4fa]%s #[fg=#94e2d5]%s #[fg=#fab387]%s' \
"$dl" "$ul" "$(cpu)" "$(mem)" "$(temp)"
;;
esac
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env bash
# Network throughput from /proc/net/dev (sum of all non-lo interfaces), averaged
# over the interval between status refreshes — no blocking sleep.
#
# Usage: net.sh dl -> " 1.2M" (download icon U+F019 + rate)
# net.sh ul -> " 45K" (upload icon U+F093 + rate)
#
# tmux calls both from status-right. A short (<3s) cache lets the two calls of a
# single refresh share one computation, so the up/down figures are consistent and
# only one of them re-samples the counters per refresh.
which=${1:-dl}
rt="${XDG_RUNTIME_DIR:-/tmp}"
ctr="$rt/tmux_net_ctr" # previous counters: rx tx epoch.ns
cache="$rt/tmux_net_cache" # computed rates: dl_rate ul_rate epoch_s
emit() { # emit <icon-printf-escape> <rate>
printf "$1 %s" "$2"
}
# Reuse a fresh cache so dl and ul agree and we only sample once per refresh.
fresh_emit() { # print from the cache and exit — if the cache is fresh enough
[ -r "$cache" ] || return 1
read -r cdl cul cts <"$cache"
{ [ -n "$cts" ] && [ "$(($(date +%s) - cts))" -lt 3 ]; } || return 1
if [ "$which" = ul ]; then emit '\U0000f093' "$cul"; else emit '\U0000f019' "$cdl"; fi
exit 0
}
fresh_emit
# tmux launches the dl and ul jobs CONCURRENTLY each refresh, so without a lock
# both miss the stale cache and compute independently — worst case the loser reads
# the counters milliseconds after the winner rewrote them and shows a ~0 rate.
# Serialize: the loser waits ~30ms on the lock, then hits the winner's fresh cache.
exec 9>"$rt/tmux_net_lock"
flock 9
fresh_emit
now=$(date +%s.%N)
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
drx=0
dtx=0
dt=1
if [ -r "$ctr" ]; then
read -r prx ptx pnow <"$ctr"
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
drx=$((rx - prx))
dtx=$((tx - ptx))
[ "$drx" -lt 0 ] && drx=0
[ "$dtx" -lt 0 ] && dtx=0
fi
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
while (b >= 1024 && i < 5) { b /= 1024; i++ }
if (i == 1) printf "%d%s", b, u[i]; else printf "%.1f%s", b, u[i] }'; }
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
printf '%s %s %s\n' "$dl" "$ul" "$(date +%s)" >"$cache"
if [ "$which" = ul ]; then emit '\U0000f093' "$ul"; else emit '\U0000f019' "$dl"; fi
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# CPU temperature (°C) for the tmux status bar (thermometer glyph U+F050F).
# Sensor hunt, most→least specific: a CPU-ish thermal zone (x86_pkg_temp/…),
# else a CPU hwmon chip (coretemp/k10temp/…), else the hottest remaining zone
# (acpitz can be a bogus constant on desktops, hence last). Prints NOTHING when
# no sensor exists (VMs/containers) so the segment simply disappears — which is
# also why the trailing spacing lives in here, not in status-right.
cpu_zone=""
any_zone=""
hw=""
for d in /sys/class/thermal/thermal_zone*/; do
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
t=$(<"${d}temp")
case $t in '' | *[!0-9]*) continue ;; esac
ty=$(<"${d}type")
ty=${ty,,}
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi
;;
esac
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
done
if [ -z "$cpu_zone" ]; then
for d in /sys/class/hwmon/hwmon*/; do
[ -r "${d}name" ] || continue
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
for f in "${d}"temp*_input; do
[ -r "$f" ] || continue
t=$(<"$f")
case $t in '' | *[!0-9]*) continue ;; esac
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
done
;;
esac
done
fi
t=${cpu_zone:-${hw:-$any_zone}}
[ -n "$t" ] || exit 0
printf '\U000f050f %d° ' $(((t + 500) / 1000))
+61 -16
View File
@@ -2,13 +2,21 @@
# Without this, tmux collapses them to a plain Enter. Requires tmux 3.5+ for csi-u.
set -g extended-keys on
set -g extended-keys-format csi-u
# termux reports xterm-256color: without RGB every #rrggbb below is approximated to the 256
# palette on the phone, and without extkeys tmux never asks the terminal for Shift/Ctrl+Enter.
# (kitty is xterm-kitty, whose terminfo already carries both.)
set -as terminal-features 'xterm-256color:RGB:extkeys'
# Let apps wrap escape sequences in DCS tmux passthrough (Claude Code / pi notifications,
# Claude's progress bar) so they reach the outer terminal instead of being swallowed.
set -g allow-passthrough on
# Mouse/touch reporting. Chiefly so the window list in the status bar is
# tappable — tmux binds MouseDown1Status to `switch-client -t =` out of the box,
# and status-format[1] below keeps the `range=window|…` markers that make each
# entry a hit target, so a tap on a tab switches to it with no extra binding.
# Wheel over the bar cycles windows; right-click a tab or the session name for
# tmux's built-in menus (no touch equivalent — a phone can't right-click).
# tmux's built-in menus; tmux-menus (plugin below) puts the same menus on prefix+\,
# which a phone can tap through.
#
# The cost: drag-select now goes to tmux instead of kitty, so hold Shift when you
# want kitty's own selection. Scroll wheel enters copy-mode on the pane rather
@@ -21,6 +29,18 @@ set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
# One long agent turn scrolls past tmux's default 2000 lines; resurrect captures pane
# contents from this buffer too. Applies to panes created after the change.
set -g history-limit 50000
# Copy-mode: vi keys, v/y like an editor. set-clipboard on pushes every copy — tmux's own and
# those apps inside make (nvim, pi) — over OSC 52 to the CLIENT terminal, so a yank on the
# phone lands on the phone and on kitty lands on the desktop, wherever the server runs.
setw -g mode-keys vi
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-selection-and-cancel
set -s set-clipboard on
# --- Session persistence (survives reboot) ---------------------------------
# resurrect: save/restore sessions, windows, panes, layout and working dirs
# (prefix + Ctrl-s to save, prefix + Ctrl-r to restore).
@@ -29,6 +49,8 @@ set -g renumber-windows on
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
# menus: tmux's context menus on prefix+\ (tappable; no right-click needed on a phone).
set -g @plugin 'jaclu/tmux-menus'
# Also capture pane scrollback and restore running programs (nvim, …).
# nvim 'session' strategy restores via Session.vim when present, else plain nvim.
@@ -87,20 +109,23 @@ set -g status-left "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@left_ful
# Middle: flat window list — inactive muted, active mauve/bold, 2-space gaps.
set -g window-status-separator " "
setw -g window-status-format "#[fg=#7f849c]#I #[fg=#6c7086]#W"
setw -g window-status-format "#[fg=#7f849c]#I#{P:#{E:@agent_badge}} #[fg=#6c7086]#W#[default]"
setw -g window-status-current-format "#[fg=#cba6f7,bold]#I #[fg=#cdd6f4,bold]#W"
setw -g window-status-activity-style "fg=#f9e2af"
# The agent-state bell flags the window (monitor-bell) so the outer terminal buzzes; tmux's
# default bell style is `reverse`, which painted the window entry — only the glyph gets colour.
setw -g window-status-bell-style default
# Right: net dl/ul · cpu · mem │ date │ clock. continuum prepends its invisible
# save hook to status-right (this block sits above the tpm `run` line, so the
# hook survives every reload). Metrics are scripts/*.sh, symlinked into
# ~/.config/tmux/scripts/. The hook lands ahead of the #{?} below, i.e. outside
# it, so it keeps running for every client regardless of which variant is drawn.
set -g @right_full "#[fg=#a6e3a1]#(~/.config/tmux/scripts/net.sh dl) #[fg=#f9e2af]#(~/.config/tmux/scripts/net.sh ul) #[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) #[fg=#fab387]#(~/.config/tmux/scripts/temp.sh)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
# Right: net dl/ul · cpu · mem · temp │ date │ clock. One #() job per refresh: metrics.sh
# prints the whole coloured segment (it was five separate scripts per tick). continuum
# prepends its invisible save hook to status-right (this block sits above the tpm `run`
# line, so the hook survives every reload). The hook lands ahead of the #{?} below, i.e.
# outside it, so it keeps running for every client regardless of which variant is drawn.
set -g @right_full "#(~/.config/tmux/scripts/metrics.sh full)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
# Compact: cpu + mem only. Net rates and the temp glyph go first (widest, least
# useful on a phone), then the date, then the clock — a phone shows its own.
set -g @right_compact "#[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) "
set -g @right_compact "#(~/.config/tmux/scripts/metrics.sh compact) "
set -g status-right "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@right_full},#{E:@right_compact}}"
@@ -125,28 +150,48 @@ bind S {
# Two rows: [0] drawn top-border rule · [1] the bar content. The rule doubles as
# the separator from the pane content above; tmux status height is whole rows, so
# one row above the content is the smallest gap that still shows a border.
# Full-width run of ─ (yes|head|paste avoids %, backslashes and nested parens so
# tmux's strftime/quote/#() parsing leave it alone); tmux truncates it to width.
# The rule is a literal run of ─ in @rule (300 columns; tmux clips it to the client
# width), so drawing it costs no process.
# status-format[1] is tmux 3.7's default content template (status-left/windows/right).
set -g status 2
set -g status-format[0] "#[fg=#6c7086,bg=default]#(yes ─ | head -n 400 | paste -sd '')"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
set -g @rule "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
set -g status-format[0] "#[fg=#6c7086,bg=default]#{@rule}"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[range=user|agentnext] #[fg=#6c7086]»#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
# The `+` appended to the window list above sits in a `range=user|newwin`
# marker. Per tmux(1), a user range triggers the same `Status` mouse key as a
# window range and exposes its argument in #{mouse_status_range} — window ranges
# report the literal string "window" — so one binding can serve both: click a
# tab to select it (tmux's stock behaviour, kept in the else branch), click the
# `+` to open a new window. Padded with spaces on both sides so it's a wide
# enough target to hit with a thumb.
# `+` to open a new window, click the `»` to jump to the next window with an agent
# mark (scripts/agent-next.sh, also prefix+Tab). Padded with spaces on both sides so
# each is a wide enough target to hit with a thumb.
bind -n MouseDown1Status {
if -F '#{==:#{mouse_status_range},newwin}' {
new-window
} {
switch-client -t =
if -F '#{==:#{mouse_status_range},agentnext}' {
run-shell '~/.config/tmux/scripts/agent-next.sh'
} {
switch-client -t =
}
}
}
# Agent state per pane — set by ~/.config/tmux/scripts/agent-state.sh <tool> <state> from the
# Claude Code / codex / pi / opencode hooks: @agent = busy|done|input|error. @agent_badge draws
# one glyph glued to the window number ("2✓ name") in the window list, the same for every tool: ✓ done (green),
# ? waiting for input (yellow), ! error (red); busy draws nothing. Visiting the pane only takes
# the colour away (pane-focus-in sets @agent_seen; needs focus-events): the glyph stays, grey,
# until the next prompt turns the state busy. prefix K clears every mark; prefix Tab (or the »
# in the bar) jumps to the next window with a mark, coloured ones first. A coloured glyph
# always means "this happened while you were not looking".
set -g @agent_badge "#{?#{==:#{@agent},done},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#a6e3a1]}✓,#{?#{==:#{@agent},input},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f9e2af]}?,#{?#{==:#{@agent},error},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f38ba8]}!,}}}"
set -g focus-events on
set-hook -g pane-focus-in 'set -p @agent_seen 1'
bind Tab run-shell '~/.config/tmux/scripts/agent-next.sh'
bind K run-shell 'for p in $(tmux list-panes -a -F "##{pane_id}"); do tmux set -p -t $p -u @agent; tmux set -p -t $p -u @agent_seen; done' \; display-message "agent marks cleared"
# Round out the theme so no default green leaks into borders/messages/modes.
set -g pane-border-style "fg=#313244"
set -g pane-active-border-style "fg=#cba6f7"