← back to Terminal Status

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

171 lines

#!/usr/bin/env bash
# tab-state.sh — drive the session-close visual state machine on the Claude
# Code terminal tab. States:
#   red          solid red   — session closing (/cs)
#   flash-red    blinking red — compaction in progress (PreCompact hook)
#   flash-orange blinking orange — STOPPED, waiting on a background result
#               (e.g. a background task / DTD panel / long job to land)
#   flash-purple blinking purple — STOPPED & WAITING ON STEVE (Stop hook): the
#   (alias:      turn has been handed back and Claude is idle awaiting input.
#    waiting)    Cleared by a `stop`/`green`/`red` (or the next UserPromptSubmit).
#   purple       solid purple
#   flash-green  green flash ×3 then solid green — compaction complete (SessionStart compact)
#   green        solid green
#   stop         kill any running flasher
#
# Reuses /color's mechanism: Claude Code's Bash tool has no controlling tty, so
# OSC escapes on stdout never reach the terminal. Instead we locate the `claude`
# process's own pty by walking process ancestry and write the OSC tab-tint code
# straight to /dev/ttysNNN. OSC color codes are non-destructive (no cursor moves)
# so writing them — even repeatedly from a background flasher — is glitch-safe.
set -u

MODE="${1:-}"

# Streamline / lite mode — skip cosmetic tab-state painting fleet-wide, but
# always allow 'stop' so a running flasher can still be cleaned up. rm to restore.
case "$MODE" in stop) : ;; *) [ -f "$HOME/.claude/hooks-lite" ] && exit 0 ;; esac

PIDFILE="${HOME}/.claude/tab-flash.pid"

# Walk ancestry from $$ to find the controlling ttys* (same as color.sh).
resolve_tty() {
  local p=$$ line pp tt
  for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
    line="$(ps -o ppid=,tty= -p "$p" 2>/dev/null)" || break
    # shellcheck disable=SC2086
    set -- $line; pp="${1:-}"; tt="${2:-}"
    [[ -z "$pp" ]] && break
    if [[ "$tt" == ttys* ]]; then echo "/dev/$tt"; return 0; fi
    p="$pp"
  done
  return 1
}

# write_color <tty> <R> <G> <B>  — tint the iTerm2 tab (tmux-wrapped if needed)
write_color() {
  local tty="$1" R="$2" G="$3" B="$4"
  [[ -w "$tty" ]] || return 1
  if [[ -n "${TMUX:-}" ]]; then
    { 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"
    } > "$tty" 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"
    } > "$tty" 2>/dev/null
  fi
}

# reset_color <tty> — clear the tab tint back to iTerm2 default (the "off" half
# of a blink)
reset_color() {
  local tty="$1"
  [[ -w "$tty" ]] || return 1
  if [[ -n "${TMUX:-}" ]]; then
    printf '\033Ptmux;\033\033]6;1;bg;*;default\007\033\\' > "$tty" 2>/dev/null
  else
    printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
  fi
}

stop_flasher() {
  if [[ -f "$PIDFILE" ]]; then
    kill "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null || true
    rm -f "$PIDFILE"
  fi
}

RED=(255 0 0)
GREEN=(0 200 0)
PURPLE=(148 0 211)

case "$MODE" in
  red)
    stop_flasher
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    write_color "$tty" "${RED[@]}" && echo "tab → solid RED (closing)"
    ;;
  green)
    stop_flasher
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    write_color "$tty" "${GREEN[@]}" && echo "tab → solid GREEN (done)"
    ;;
  purple)
    stop_flasher
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    write_color "$tty" "${PURPLE[@]}" && echo "tab → solid PURPLE (stopped, waiting on Steve)"
    ;;
  flash-red)
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    stop_flasher
    # Resolve tty in the parent, then hand it to the detached loop — once
    # backgrounded the loop may be reparented and lose the ancestry walk.
    ( trap 'exit 0' TERM INT
      while :; do
        printf '\033]6;1;bg;red;brightness;255\007\033]6;1;bg;green;brightness;0\007\033]6;1;bg;blue;brightness;0\007' > "$tty" 2>/dev/null
        sleep 0.5
        printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
        sleep 0.5
      done ) >/dev/null 2>&1 &
    echo $! > "$PIDFILE"
    disown 2>/dev/null || true
    echo "tab → FLASHING RED (compacting) pid $(cat "$PIDFILE")"
    ;;
  flash-orange)
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    stop_flasher
    # STOPPED, waiting on a background result. Blink orange (255,140,0) until
    # something kills the flasher (a `stop`/`green`/`red`, or work resumes).
    ( trap 'exit 0' TERM INT
      while :; do
        printf '\033]6;1;bg;red;brightness;255\007\033]6;1;bg;green;brightness;140\007\033]6;1;bg;blue;brightness;0\007' > "$tty" 2>/dev/null
        sleep 0.5
        printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
        sleep 0.5
      done ) >/dev/null 2>&1 &
    echo $! > "$PIDFILE"
    disown 2>/dev/null || true
    echo "tab → FLASHING ORANGE (stopped, waiting on background result) pid $(cat "$PIDFILE")"
    ;;
  flash-purple|waiting)
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    stop_flasher
    # STOPPED & WAITING ON STEVE — the turn was handed back and Claude is idle
    # awaiting input. Blink purple (148,0,211) until something kills the flasher
    # (a `stop`/`green`/`red`, or the next UserPromptSubmit when Steve replies).
    # Resolve tty in the parent, then hand it to the detached loop — once
    # backgrounded the loop may be reparented and lose the ancestry walk.
    ( trap 'exit 0' TERM INT
      while :; do
        printf '\033]6;1;bg;red;brightness;148\007\033]6;1;bg;green;brightness;0\007\033]6;1;bg;blue;brightness;211\007' > "$tty" 2>/dev/null
        sleep 0.5
        printf '\033]6;1;bg;*;default\007' > "$tty" 2>/dev/null
        sleep 0.5
      done ) >/dev/null 2>&1 &
    echo $! > "$PIDFILE"
    disown 2>/dev/null || true
    echo "tab → FLASHING PURPLE (stopped, waiting on Steve) pid $(cat "$PIDFILE")"
    ;;
  flash-green)
    stop_flasher
    tty="$(resolve_tty)" || { echo "tab-state: no pty found"; exit 1; }
    for _ in 1 2 3; do
      write_color "$tty" "${GREEN[@]}"; sleep 0.3
      reset_color "$tty"; sleep 0.3
    done
    write_color "$tty" "${GREEN[@]}"
    echo "tab → GREEN flash ×3 then solid (compaction complete)"
    ;;
  stop)
    stop_flasher
    echo "tab-state: flasher stopped"
    ;;
  *)
    echo "usage: tab-state.sh {red|green|purple|flash-red|flash-orange|flash-purple|waiting|flash-green|stop}"
    exit 2
    ;;
esac