← back to Terminal Status
verification/install/before/.agents/skills/color/scripts/color.sh
168 lines
#!/usr/bin/env bash
# /color — give this Claude Code session a fresh, visually-distinct color and
# apply it to the terminal tab so parallel Claude sessions are tellable apart.
#
# Mechanism: Claude Code's Bash tool runs detached with NO controlling tty
# (verified: `tty` => "not a tty", /dev/tty => "device not configured"), so
# OSC escapes on stdout/stderr never reach the terminal. The working path is
# to locate the `claude` process's own pty by walking process ancestry and
# write the OSC sequence straight to /dev/ttysNNN. OSC color/title codes are
# non-destructive (no cursor moves, no cell writes) so this is glitch-safe.
#
# Colors: golden-angle (137.508 deg) hue rotation => every session maximally
# far from the last on the hue wheel, never repeating. iTerm2 gets a tab tint;
# every terminal (incl. macOS Terminal.app) gets an emoji + name tab title.
set -u
STATE_DIR="${HOME}/.claude"
STATE="${STATE_DIR}/color-session-index"
mkdir -p "$STATE_DIR"
idx=0
[[ -f "$STATE" ]] && idx="$(cat "$STATE" 2>/dev/null)"
[[ "$idx" =~ ^[0-9]+$ ]] || idx=0
session=$(( idx + 1 ))
printf '%s\n' "$session" > "$STATE"
# golden-angle hue -> HSL(90% sat, 58% light) -> RGB, pure awk (no python/flock dep)
read -r R G B HUE < <(awk -v idx="$idx" '
function absf(v){ return v<0 ? -v : v }
BEGIN{
hue = idx*137.508; hue = hue - int(hue/360)*360
s=0.90; l=0.58
c=(1-absf(2*l-1))*s
hp=hue/60.0
m2=hp - int(hp/2)*2
x=c*(1-absf(m2-1))
if(hp<1){r=c;g=x;b=0} else if(hp<2){r=x;g=c;b=0}
else if(hp<3){r=0;g=c;b=x} else if(hp<4){r=0;g=x;b=c}
else if(hp<5){r=x;g=0;b=c} else {r=c;g=0;b=x}
m=l-c/2
printf "%d %d %d %d", int((r+m)*255+0.5),int((g+m)*255+0.5),int((b+m)*255+0.5),int(hue)
}')
# hue -> coarse name + nearest emoji square
if (( HUE < 20 )); then NAME=Red; EMOJI=🟥
elif (( HUE < 45 )); then NAME=Orange; EMOJI=🟧
elif (( HUE < 70 )); then NAME=Amber; EMOJI=🟨
elif (( HUE < 150 )); then NAME=Green; EMOJI=🟩
elif (( HUE < 215 )); then NAME=Teal; EMOJI=🟦
elif (( HUE < 255 )); then NAME=Blue; EMOJI=🟦
elif (( HUE < 290 )); then NAME=Indigo; EMOJI=🟪
elif (( HUE < 330 )); then NAME=Violet; EMOJI=🟪
else NAME=Magenta; EMOJI=🟥
fi
# locate the claude process's controlling pty by walking the ancestry
tty_path=""; p=$$
for _ in 1 2 3 4 5 6 7 8 9 10; do
line="$(ps -o ppid=,tty= -p "$p" 2>/dev/null)" || break
set -- $line
pp="${1:-}"; tt="${2:-}"
[[ -z "$pp" ]] && break
if [[ "$tt" == ttys* ]]; then tty_path="/dev/$tt"; break; fi
p="$pp"
done
# Recall the session TOPIC (the user's first prompt) so re-coloring mid-session
# does NOT clobber the topic with the color name — the color is the tint, the
# text stays the topic. color-title.sh already wrote the topic onto the tmux
# window name (with automatic-rename off), so read it straight back from there.
# Ignore our own color-name placeholder ("<emoji> Indigo #359").
topic=""
if [[ -n "${TMUX_PANE:-}" ]] && command -v tmux >/dev/null 2>&1; then
cur="$(tmux display-message -p -t "$TMUX_PANE" '#{window_name}' 2>/dev/null)"
if [[ -n "$cur" && ! "$cur" =~ (Red|Orange|Amber|Green|Teal|Blue|Indigo|Violet|Magenta)\ \#[0-9]+$ ]]; then
topic="$cur"
fi
fi
# Tab/window TEXT: prefer the topic; until the first prompt sets one, show just
# the emoji so the tab is visibly initialised without shouting the color name.
if [[ -n "$topic" ]]; then label="$topic"; else label="${EMOJI}"; fi
hex=$(printf '#%02X%02X%02X' "$R" "$G" "$B")
# Detect tmux. Inside tmux, OSC escapes are swallowed and never reach
# iTerm2's tab bar — so the user sees no color "at top". Surface the color
# on tmux's own window/pane instead.
inside_tmux=0
[[ -n "${TMUX:-}" ]] && inside_tmux=1
if (( ! inside_tmux )) && command -v tmux >/dev/null 2>&1; then
p2=$$
for _ in 1 2 3 4 5 6 7 8 9 10; do
line2="$(ps -o ppid=,comm= -p "$p2" 2>/dev/null)" || break
set -- $line2
pp2="${1:-}"; cm2="${2:-}"
[[ -z "$pp2" ]] && break
case "$cm2" in *tmux*) inside_tmux=1; break;; esac
p2="$pp2"
done
fi
# tmux swallows the DCS-wrapped iTerm2 tab-color OSC unless passthrough is on.
# Default is OFF, so ensure it on every run — this is what makes the tab header
# actually tint (otherwise the escape is dropped and the tab shows no color).
if (( inside_tmux )) && command -v tmux >/dev/null 2>&1; then
tmux set -g allow-passthrough on 2>/dev/null || true
fi
applied=0
if [[ -n "$tty_path" && -w "$tty_path" ]]; then
if (( inside_tmux )); then
# Inside tmux, raw OSC is consumed by tmux. With allow-passthrough on,
# DCS-wrap (ESC P tmux ; <inner-with-doubled-ESC> ESC \) lets the
# iTerm2 tab-tint + title OSC reach the outer terminal.
{
printf '\033Ptmux;\033\033]6;1;bg;red;brightness;%d\007\033\\' "$R"
printf '\033Ptmux;\033\033]6;1;bg;green;brightness;%d\007\033\\' "$G"
printf '\033Ptmux;\033\033]6;1;bg;blue;brightness;%d\007\033\\' "$B"
printf '\033Ptmux;\033\033]1;%s\007\033\\' "$label"
printf '\033Ptmux;\033\033]2;%s\007\033\\' "$label"
} > "$tty_path" 2>/dev/null
else
{
printf '\033]6;1;bg;red;brightness;%d\007' "$R"
printf '\033]6;1;bg;green;brightness;%d\007' "$G"
printf '\033]6;1;bg;blue;brightness;%d\007' "$B"
printf '\033]1;%s\007' "$label"
printf '\033]2;%s\007' "$label"
} > "$tty_path" 2>/dev/null
fi
applied=1
fi
# tmux indicator path — recolors the tmux window name + current pane border
# so the color surfaces in tmux's status bar regardless of outer terminal.
# Window TEXT = topic if captured, else the color label until first prompt.
if [[ -n "$topic" ]]; then winname="$topic"; else winname="${EMOJI} ${NAME} #${session}"; fi
if (( inside_tmux )) && command -v tmux >/dev/null 2>&1; then
# scope to THIS pane via $TMUX_PANE — without it, plain `rename-window`
# targets the active window of the session, which is almost never the
# window the new claude session is actually running in.
tgt="${TMUX_PANE:-}"
if [[ -n "$tgt" ]]; then
# keep the topic sticky against tmux automatic-rename
[[ -n "$topic" ]] && tmux set-window-option -q -t "$tgt" automatic-rename off 2>/dev/null || true
tmux rename-window -t "$tgt" "$winname" 2>/dev/null || true
tmux set-window-option -q -t "$tgt" window-status-current-style "bg=${hex},fg=#000000,bold" 2>/dev/null || true
tmux set-option -p -q -t "$tgt" pane-active-border-style "fg=${hex},bold" 2>/dev/null || true
tmux set-option -p -q -t "$tgt" pane-border-style "fg=${hex}" 2>/dev/null || true
else
[[ -n "$topic" ]] && tmux set-window-option -q automatic-rename off 2>/dev/null || true
tmux rename-window "$winname" 2>/dev/null || true
tmux set-window-option -q window-status-current-style "bg=${hex},fg=#000000,bold" 2>/dev/null || true
tmux set-option -p -q pane-active-border-style "fg=${hex},bold" 2>/dev/null || true
tmux set-option -p -q pane-border-style "fg=${hex}" 2>/dev/null || true
fi
applied=1
fi
if (( applied )); then
suffix=""
(( inside_tmux )) && suffix=" · tmux window+pane recolored"
echo "/color → ${NAME} ${EMOJI} rgb(${R},${G},${B}) ${hex} · session #${session}${suffix}"
else
echo "/color → ${NAME} ${EMOJI} (session #${session}) — terminal pty not found, color not applied"
fi