← back to Terminal Status

verification/install/before/.claude/skills/color/scripts/paint-tab.sh

95 lines

#!/usr/bin/env bash
# paint-tab.sh <state>  — tint THIS Claude session's iTerm tab by state.
#   working | green   -> SOLID green  (Claude is busy)  + stop any blinker
#   waiting | purple  -> BLINKING purple (turn handed back to Steve — answer
#                        needed). A background loop alternates purple<->default
#                        until the next `working` call kills it.
#   solid-purple      -> solid purple, no blink (internal/manual use)
#   stop              -> kill the blinker only
#
# Why slot-prefix matching: the color hook/bash subprocess runs detached (tty
# "??"), so walking process ancestry to find the tab pty is unreliable. And the
# inherited ITERM_SESSION_ID's UUID half goes STALE on an iTerm reattach. The
# w#t#p# (window/tab/pane) PREFIX is stable, so we match THAT against each live
# `claude` process's env to resolve the correct /dev/ttysNNN. Verified 2026-06-26.
set -u

GREEN=(114 244 52)        # busy
PURPLE=(168 85 247)       # awaiting Steve — bright phase
PURPLE_DIM=(84 42 124)    # awaiting Steve — dim phase (PULSE, never blank)
BLINK_SEC="${PAINT_BLINK_SEC:-0.7}"   # cadence of the idle pulse

state="${1:-waiting}"

# --- resolve THIS tab's pty by stable w#t#p# slot prefix --------------------
[[ -z "${ITERM_SESSION_ID:-}" ]] && exit 0
prefix="${ITERM_SESSION_ID%%:*}"
tty=""
for pid in $(pgrep -x claude 2>/dev/null); do
  sid="$(ps eww -p "$pid" 2>/dev/null | grep -o 'ITERM_SESSION_ID=[^ ]*' | head -1 | cut -d= -f2-)"
  [[ "${sid%%:*}" == "$prefix" ]] && tty="/dev/$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')"
done
[[ -n "$tty" && -w "$tty" ]] || exit 0

# Per-tab blinker pidfile, keyed by slot prefix so one tab never kills another's.
RUN_DIR="${HOME}/.claude/working-state"
mkdir -p "$RUN_DIR" 2>/dev/null || true
pidfile="${RUN_DIR}/blink-${prefix//[^A-Za-z0-9]/_}.pid"

write_rgb() {  # write_rgb <tty> R G B
  { printf '\033]6;1;bg;red;brightness;%d\007'   "$2"
    printf '\033]6;1;bg;green;brightness;%d\007' "$3"
    printf '\033]6;1;bg;blue;brightness;%d\007'  "$4"
  } > "$1" 2>/dev/null
}
reset_tint() { printf '\033]6;1;bg;*;default\007' > "$1" 2>/dev/null; }

stop_blink() {
  [[ -f "$pidfile" ]] || return 0
  kill "$(cat "$pidfile" 2>/dev/null)" 2>/dev/null || true
  rm -f "$pidfile"
}

case "$state" in
  green|working)
    stop_blink
    write_rgb "$tty" "${GREEN[@]}"
    ;;
  purple|waiting)
    # blinking purple: stop any prior blinker, then start a fresh detached loop.
    # tty is resolved HERE (parent) and handed to the loop — once backgrounded
    # the loop may be reparented and lose the ability to re-resolve it.
    stop_blink
    ( trap 'exit 0' TERM INT
      br="${PURPLE[0]}";     bg="${PURPLE[1]}";     bb="${PURPLE[2]}"      # bright
      dr="${PURPLE_DIM[0]}"; dg="${PURPLE_DIM[1]}"; db="${PURPLE_DIM[2]}"  # dim
      while :; do
        # PULSE bright purple <-> dim purple — always a color, never blank.
        { printf '\033]6;1;bg;red;brightness;%d\007'   "$br"
          printf '\033]6;1;bg;green;brightness;%d\007' "$bg"
          printf '\033]6;1;bg;blue;brightness;%d\007'  "$bb"
        } > "$tty" 2>/dev/null
        sleep "$BLINK_SEC"
        { printf '\033]6;1;bg;red;brightness;%d\007'   "$dr"
          printf '\033]6;1;bg;green;brightness;%d\007' "$dg"
          printf '\033]6;1;bg;blue;brightness;%d\007'  "$db"
        } > "$tty" 2>/dev/null
        sleep "$BLINK_SEC"
      done ) >/dev/null 2>&1 &
    echo $! > "$pidfile"
    disown 2>/dev/null || true
    ;;
  solid-purple)
    stop_blink
    write_rgb "$tty" "${PURPLE[@]}"
    ;;
  stop)
    stop_blink
    reset_tint "$tty"
    ;;
  *)
    write_rgb "$tty" 128 128 128
    ;;
esac
exit 0