← back to Terminal Status
verification/install/before/.claude/skills/allcolordots/allcolordots.sh
197 lines
#!/usr/bin/env bash
# /allcolordots — scan EVERY active terminal's color dot + label in one shot,
# and (new) present a clickable single-window ROSTER per color that JUMPS focus
# to any tab — the "gather my yellows in one place" navigator (path A).
#
# Prefer semantic LIVE iTerm2 names. If Codex overwrites the dot, recover the
# newest valid ~/.claude or ~/.codex/tab-dots record from this agent lifetime.
# "live" = a `claude`/`codex` process is running on that tty.
#
# Usage:
# allcolordots.sh # grouped human table (orange > purple > yellow > green > pink)
# allcolordots.sh --json # JSON array: [{tty,color,label,live,pid}] (navbar app)
# allcolordots.sh --live # table, but only ACTIVE (live) terminals
# allcolordots.sh --yellow # numbered ROSTER of just the 🟡 needs-direction tabs
# allcolordots.sh --only <color> # numbered roster of one color (yellow|orange|purple|green|pink)
# allcolordots.sh jump <ttyNNN|N> # focus that tab (N = row# from the last roster)
set -u
# Emoji glob-matching (case *🟡*) needs a UTF-8 ctype; some launch contexts pass
# LC_CTYPE= empty which silently breaks the match -> every dot reads "none".
export LANG="${LANG:-en_US.UTF-8}" LC_ALL="en_US.UTF-8"
DOTS="$HOME/.claude/tab-dots"
CODEX_DOTS="$HOME/.codex/tab-dots"
ROSTER_CACHE="$DOTS/.last-roster"
color_of(){ case "$1" in *🟢*) echo green;; *🟡*) echo yellow;; *🟣*) echo purple;; *🟠*) echo orange;; *🩷*) echo pink;; *) echo none;; esac; }
emoji_of(){ case "$1" in orange) echo 🟠;; purple) echo 🟣;; yellow) echo 🟡;; green) echo 🟢;; pink) echo 🩷;; *) echo ⚪;; esac; }
# Codex rewrites its title while working. Recover its semantic state only from
# this tty's files written during the current agent lifetime; old tty numbers
# get reused. Read both painters' stores and choose by mtime, never by color.
saved_label(){
[ -n "${2:-}" ] || return 0
python3 - "$1" "$2" "$DOTS" "$CODEX_DOTS" <<'PY'
import datetime, pathlib, re, subprocess, sys
tty, pid, *directories = sys.argv[1:]
if not re.fullmatch(r"ttys[0-9]+", tty) or not pid.isdigit():
sys.exit(0)
try:
started = subprocess.check_output(
["ps", "-p", pid, "-o", "lstart="], text=True,
stderr=subprocess.DEVNULL).strip()
started = datetime.datetime.strptime(started, "%a %b %d %H:%M:%S %Y").timestamp()
except (ValueError, OSError, subprocess.CalledProcessError):
sys.exit(0)
eligible = []
for directory in directories:
try:
path = pathlib.Path(directory) / (tty + ".dot")
stamp = path.stat().st_mtime
label = path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeError):
continue
if (stamp >= started and label.startswith(("🟢", "🟡", "🟣", "🟠", "🩷"))
and not any(ord(c) < 32 or ord(c) == 127 for c in label)):
eligible.append((stamp, label))
if eligible:
print(max(eligible, key=lambda item: item[0])[1])
PY
}
# ---- LIVE session scan via iTerm2 (name carries the dot+label; freshest source) ----
# Emits: tty<TAB>name (one per session). Falls back silently to .dot files if osascript fails.
live_sessions(){
# NOTE: inside `tell application "iTerm2"`, the word `tab` resolves to iTerm2's
# own `tab` object class — NOT the tab character — so `& tab &` emits the literal
# string "tab". Use an explicit ASCII 9 for the field separator.
osascript 2>/dev/null <<'OSA'
set SEP to (ASCII character 9)
tell application "iTerm2"
set out to ""
repeat with w in windows
repeat with t in tabs of w
repeat with s in sessions of t
set out to out & (tty of s) & SEP & (name of s) & linefeed
end repeat
end repeat
end repeat
return out
end tell
OSA
}
# ---- JUMP: focus the tab whose tty matches (select + activate; NOT a reparent) ----
jump_to(){
local arg="$1" tty=""
# index into the last roster?
if printf '%s' "$arg" | grep -qE '^[0-9]+$' && [ -f "$ROSTER_CACHE" ]; then
tty="$(awk -F'\t' -v n="$arg" 'NR==n{print $1}' "$ROSTER_CACHE")"
fi
[ -z "$tty" ] && tty="$arg"
case "$tty" in /dev/*) :;; ttys*) tty="/dev/$tty";; *) tty="/dev/$tty";; esac
osascript 2>&1 <<OSA
tell application "iTerm2"
repeat with w in windows
repeat with t in tabs of w
repeat with s in sessions of t
if (tty of s) is "$tty" then
select w
select t
select s
activate
return "jumped -> $tty"
end if
end repeat
end repeat
end repeat
return "not found: $tty"
end tell
OSA
}
# ---- collect normalized rows: tty \t color \t live \t pid \t label ----
collect_rows(){
local live_raw; live_raw="$(live_sessions)"
if [ -n "$live_raw" ]; then
# LIVE path: iTerm2 names are the label; a claude/codex proc on the tty = live
printf '%s\n' "$live_raw" | while IFS=$'\t' read -r tty name; do
[ -z "$tty" ] && continue
local short="${tty#/dev/}"
local color; color="$(color_of "$name")"
local pid; pid="$(ps -axo pid=,tty=,command= 2>/dev/null | awk -v t="$short" '$2==t && (/claude/||/codex/) && !/awk/ {print $1; exit}')"
local live=false; [ -n "$pid" ] && live=true
if [ "$color" = none ] && [ "$live" = true ]; then
local saved; saved="$(saved_label "$short" "$pid")"
if [ -n "$saved" ]; then name="$saved"; color="$(color_of "$saved")"; fi
fi
printf '%s\t%s\t%s\t%s\t%s\n' "$short" "$color" "$live" "${pid:-}" "$name"
done
else
# FALLBACK path: persisted .dot files
for f in "$DOTS"/*.dot; do
[ -f "$f" ] || continue
local tty; tty="$(basename "$f" .dot)"
local label; label="$(cat "$f" 2>/dev/null)"
local color; color="$(color_of "$label")"
local pid; pid="$(ps -axo pid=,tty=,command= 2>/dev/null | awk -v t="$tty" '$2==t && (/claude/||/codex/) && !/awk/ {print $1; exit}')"
local live=false; [ -n "$pid" ] && live=true
printf '%s\t%s\t%s\t%s\t%s\n' "$tty" "$color" "$live" "${pid:-}" "$label"
done
fi
}
# ---- roster of ONE color: numbered, jump-ready, caches order for `jump N` ----
print_roster(){
local want="$1" rows; rows="$(collect_rows | awk -F'\t' -v c="$want" '$2==c')"
: > "$ROSTER_CACHE"
local e; e="$(emoji_of "$want")"
local n; n="$(printf '%s\n' "$rows" | grep -c .)"
echo "=== $e $want roster ($n) — jump with: allcolordots.sh jump <#|tty> ==="
[ "$n" = 0 ] && { echo " (none)"; return 0; }
local i=0
printf '%s\n' "$rows" | while IFS=$'\t' read -r tty color live pid label; do
i=$((i+1))
printf '%s\t%s\t%s\n' "$tty" "$label" "$live" >> "$ROSTER_CACHE"
local flag=""; [ "$live" = true ] && flag=" ·live"
printf ' %2d) %-9s %s%s\n' "$i" "$tty" "$label" "$flag"
done
}
# ---------------- dispatch ----------------
case "${1:-}" in
jump) shift; jump_to "${1:-}"; exit $? ;;
--yellow) print_roster yellow; exit 0 ;;
--only) print_roster "${2:-yellow}"; exit 0 ;;
--json)
collect_rows | python3 -c '
import sys,json
out=[]
for line in sys.stdin:
line=line.rstrip("\n")
if not line: continue
tty,color,live,pid,label=(line.split("\t")+["","","","",""])[:5]
out.append({"tty":tty,"color":color,"live":live=="true","pid":pid,"label":label})
pri={"orange":0,"purple":1,"yellow":2,"green":3,"pink":4,"none":5}
out.sort(key=lambda r:(pri.get(r["color"],9), r["tty"]))
print(json.dumps(out))
'
exit 0 ;;
esac
# default / --live : grouped human table
LIVEONLY=0; [ "${1:-}" = "--live" ] && LIVEONLY=1
rows="$(collect_rows)"
[ "$LIVEONLY" = 1 ] && rows="$(printf '%s\n' "$rows" | awk -F'\t' '$3=="true"')"
total="$(printf '%s\n' "$rows" | grep -c . )"
echo "=== active-terminal dots ($total) ==="
for c in orange purple yellow green pink none; do
block="$(printf '%s\n' "$rows" | awk -F'\t' -v c="$c" '$2==c')"
[ -z "$block" ] && continue
n="$(printf '%s\n' "$block" | grep -c .)"
echo "$(emoji_of "$c") ${c} ($n)"
printf '%s\n' "$block" | while IFS=$'\t' read -r tty color live pid label; do
flag=""; [ "$live" = true ] && flag=" ·live"
printf ' %-9s %s%s\n' "$tty" "$label" "$flag"
done
done