statuslin.es

Pace Dot Meters

bash

Two-line status with a block context bar and quarter-step dot meters (◔◑◕●) for the 5-hour and weekly limits — fill shows usage, color shows whether you are ahead of or behind pace — plus model, effort, git and cost. Needs no jq. Source: github.com/AndrewP-GH/cc-statusline

Preview

Clean repo
Opus 4.8 [high] ~/app main 17:27:05 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m
New session
Opus 4.8 [high] ~/app main 17:27:06 │ ⏱ 10m
Dirty branch
Sonnet 4.6 [medium] ~/app feat/auth (+2) 17:27:07 | ⛁ █████░░░░░ 48% | 5h ●●○○○ 40% ↻1h12m | 7d ●◔○○○○○ 18% ↻2d22h │ $0.41 ⏱ 10m
Near-full
Opus 4.8 [max] ~/app main 17:27:08 | ⛁ █████████░ 91% | 5h ●●●●◔ 88% ↻18m | 7d ●●●●◔○○ 61% ↻20h0m │ $4.12 ⏱ 10m
1M context
Fable 5 [xhigh] ~/app main 17:27:08 | ⛁ ██████░░░░ 64% | 5h ●◑○○○ 33% ↻3h30m | 7d ◕○○○○○○ 12% ↻5d │ $0.41 ⏱ 10m
Post-compact
Haiku 4.5 ~/app main 17:27:09 | 5h ●●◑○○ 52% ↻4h2m │ $0.41 ⏱ 10m
Worktree
Opus 4.8 [low] ~/.wt/feature worktree-feature 17:27:10 | ⛁ ████░░░░░░ 37% | 5h ●●○○○ 44% ↻2h50m | 7d ●◔○○○○○ 20% ↻1d7h │ $0.41 ⏱ 10m
Non-git
Opus 4.8 [high] ~/scratch 17:27:10 | ⛁ ██░░░░░░░░ 22% │ $0.41 ⏱ 10m

Source

#!/usr/bin/env bash
# cc-statusline — two-line Claude Code status line, pure POSIX shell + awk.
# Reads the status JSON on stdin; needs no node/bun/jq. git & kubectl are
# optional and only invoked when present.
#
# Line 1: model [effort] cwd  ⎈ k8s(ns)  git-branch (+changes)  HH:MM:SS
# Line 2: context bar | 5h <dots> <used%> ↻rem | 7d <dots> <used%> ↻rem │ $cost ⏱ dur
#   dots FILL  = usage % consumed (5h: 5 dots, 7d: 7 dots; in-progress dot = quarter steps)
#   dots COLOR = burn pace (used% vs time-elapsed%): blue ok, yellow tight, red too fast
#   ↻rem       = time left until the window resets
#
# Customize via env: CC_SL_FULL CC_SL_HALF CC_SL_EMPTY (dot glyphs),
# CC_SL_PACE_FLOOR (used% below which pace color stays blue; default 5).

input=$(cat)

# --- parse the JSON once (path-aware scalar extraction) -----------------------
read_fields() {
  printf '%s' "$input" | awk '
    { rec = rec $0 "\n" }
    function ws() { while (pos <= len) { c = substr(S, pos, 1)
        if (c == " " || c == "\t" || c == "\n" || c == "\r") pos++; else break } }
    function str(   out, c, nc) { pos++; out = ""
      while (pos <= len) { c = substr(S, pos, 1)
        if (c == "\\") { nc = substr(S, pos + 1, 1)
          if (nc == "n") out = out "\n"; else if (nc == "t") out = out "\t"
          else out = out nc; pos += 2; continue }
        if (c == "\"") { pos++; break }
        out = out c; pos++ }
      return out }
    function prim(   out, c) { out = ""
      while (pos <= len) { c = substr(S, pos, 1)
        if (c == "," || c == "}" || c == "]" || c == " " || c == "\t" || c == "\n" || c == "\r") break
        out = out c; pos++ }
      return out }
    function value(path,   c) { ws(); c = substr(S, pos, 1)
      if (c == "{") obj(path)
      else if (c == "[") arr(path)
      else if (c == "\"") V[path] = str()
      else V[path] = prim() }
    function obj(path,   k) { pos++; ws()
      if (substr(S, pos, 1) == "}") { pos++; return }
      while (1) { ws(); k = str(); ws(); pos++   # skip :
        value(path "." k); ws(); c = substr(S, pos, 1); pos++
        if (c == ",") continue; else break } }   # skip , or }
    function arr(path,   i) { pos++; ws()
      if (substr(S, pos, 1) == "]") { pos++; return }
      i = 0
      while (1) { value(path "." i); i++; ws(); c = substr(S, pos, 1); pos++
        if (c == ",") continue; else break } }
    END {
      S = rec; len = length(S); pos = 1; value("")
      eff = (V[".effort.level"] != "") ? V[".effort.level"] : V[".effort"]
      print V[".model.display_name"]
      print eff
      print V[".workspace.current_dir"]
      print V[".context_window.used_percentage"]
      print V[".rate_limits.five_hour.used_percentage"]
      print V[".rate_limits.five_hour.resets_at"]
      print V[".rate_limits.seven_day.used_percentage"]
      print V[".rate_limits.seven_day.resets_at"]
      print V[".cost.total_cost_usd"]
      print V[".cost.total_duration_ms"]
    }'
}

{
  IFS= read -r model
  IFS= read -r effort
  IFS= read -r cwd
  IFS= read -r ctx
  IFS= read -r fh_used
  IFS= read -r fh_reset
  IFS= read -r sd_used
  IFS= read -r sd_reset
  IFS= read -r cost
  IFS= read -r dur
} < <(read_fields)

now=$(date +%s)

# --- colors / glyphs ----------------------------------------------------------
BLUE=$'\033[94m'; YELLOW=$'\033[33m'; RED=$'\033[31m'; GREEN=$'\033[32m'
DIM=$'\033[2;37m'; RESET=$'\033[0m'
ORANGE=$'\033[38;5;173m'; EFFORTC=$'\033[38;5;179m'; CYAN=$'\033[1;36m'
K8S=$'\033[2;34m'; GITC=$'\033[31m'; GITN=$'\033[2;33m'; TIMEC=$'\033[1;33m'
FULL=${CC_SL_FULL:-●}; EMPTY=${CC_SL_EMPTY:-○}
Q1=${CC_SL_Q1:-◔}; HALF=${CC_SL_HALF:-◑}; Q3=${CC_SL_Q3:-◕}

is_set() { [ -n "$1" ] && [ "$1" != "null" ]; }

# --- line 1 -------------------------------------------------------------------
[ -z "$model" ] || [ "$model" = "null" ] && model="Claude"
cwd_tilde="${cwd/#$HOME/\~}"
line1="${ORANGE}${model}${RESET}"
is_set "$effort" && line1+=" ${EFFORTC}[${effort}]${RESET}"
is_set "$cwd_tilde" && line1+=" ${CYAN}${cwd_tilde}${RESET}"

if command -v kubectl >/dev/null 2>&1; then
  kctx=$(kubectl config current-context 2>/dev/null)
  if [ -n "$kctx" ]; then
    kns=$(kubectl config view --minify -o "jsonpath={..namespace}" 2>/dev/null)
    if [ -n "$kns" ]; then line1+=" ${K8S}⎈ ${kctx}(${kns})${RESET}"
    else line1+=" ${K8S}⎈ ${kctx}${RESET}"; fi
  fi
fi

if [ -n "$cwd" ] && git -C "$cwd" --no-optional-locks -c core.useBuiltinFSMonitor=false rev-parse --git-dir >/dev/null 2>&1; then
  G=(git -C "$cwd" --no-optional-locks -c core.useBuiltinFSMonitor=false)
  br=$("${G[@]}" symbolic-ref --short HEAD 2>/dev/null || "${G[@]}" rev-parse --short HEAD 2>/dev/null)
  ch=$("${G[@]}" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
  if [ -n "$br" ]; then
    if [ "${ch:-0}" -gt 0 ]; then line1+=" ${GITC}${br}${RESET} ${GITN}(+${ch})${RESET}"
    else line1+=" ${GITC}${br}${RESET}"; fi
  fi
fi
# --- line 2 -------------------------------------------------------------------
round() { printf '%.0f' "$1" 2>/dev/null; }

fmt_remaining() { # $1=sec $2=daily(1/0) — rounds to nearest unit
  local sec=$1 daily=$2 h m d rh
  [ "$sec" -lt 0 ] && sec=0
  if [ "$daily" -eq 1 ]; then
    h=$(( (sec + 1800) / 3600 ))           # nearest hour
    d=$(( h / 24 )); rh=$(( h % 24 ))
    if [ "$d" -gt 0 ]; then
      if [ "$rh" -gt 0 ]; then printf '%dd%dh' "$d" "$rh"; else printf '%dd' "$d"; fi
      return
    fi
    # <1 day left: fall through to h/m so we never show "0h"
  fi
  m=$(( (sec + 30) / 60 ))                  # nearest minute
  h=$(( m / 60 )); m=$(( m % 60 ))
  if [ "$h" -gt 0 ]; then printf '%dh%dm' "$h" "$m"; else printf '%dm' "$m"; fi
}

render_window() { # $1=label $2=windowSec $3=units $4=daily $5=used $6=reset
  local label=$1 win=$2 units=$3 daily=$4 used=$5 reset=$6
  is_set "$reset" || return 0
  reset=${reset%.*}
  case "$reset" in ''|*[!0-9]*) return 0 ;; esac
  local elapsed=$(( now - (reset - win) ))
  [ "$elapsed" -lt 0 ] && elapsed=0
  [ "$elapsed" -gt "$win" ] && elapsed=$win
  local u; u=$(round "$used"); u=${u:-0}
  [ "$u" -lt 0 ] && u=0; [ "$u" -gt 100 ] && u=100
  # Dots track usage %, not the clock. Floor to the last fully-consumed quarter so a
  # part never claims usage you haven't reached (each part = a quarter of one dot).
  local maxq=$(( units * 4 ))
  local quarters=$(( u * maxq / 100 ))
  [ "$quarters" -gt "$maxq" ] && quarters=$maxq
  local full=$(( quarters / 4 )) rem=$(( quarters % 4 ))
  local partial=""
  case "$rem" in
    1) partial="$Q1" ;;
    2) partial="$HALF" ;;
    3) partial="$Q3" ;;
  esac
  local pcount=0; [ -n "$partial" ] && pcount=1
  local empty=$(( units - full - pcount ))
  # Color = burn pace: usage % vs the share of the window's time already elapsed.
  # Just after a reset elapsed≈0, so the ratio explodes and even 1% reads as
  # "too fast". Below PACE_FLOOR% used you can't exhaust the window regardless of
  # pace, so stay blue and skip the unstable ratio entirely.
  local col=$DIM pct=""
  if is_set "$used"; then
    if [ "$u" -lt "${CC_SL_PACE_FLOOR:-5}" ]; then col=$BLUE
    elif [ $(( u * win )) -le $(( elapsed * 100 )) ]; then col=$BLUE
    elif [ $(( 2 * u * win )) -le $(( 3 * elapsed * 100 )) ]; then col=$YELLOW
    else col=$RED; fi
    pct=" ${col}${u}%${RESET}"
  fi
  local dots="" i=0
  while [ $i -lt $full ]; do dots+="$FULL"; i=$((i+1)); done
  [ -n "$partial" ] && dots+="$partial"
  i=0; while [ $i -lt $empty ]; do dots+="$EMPTY"; i=$((i+1)); done
  printf '%s %s%s%s%s %s↻%s%s' "$label" "$col" "$dots" "$RESET" "$pct" "$DIM" "$(fmt_remaining $(( reset - now )) "$daily")" "$RESET"
}

ctxseg=""
if is_set "$ctx"; then
  cpct=$(round "$ctx"); cpct=${cpct:-0}
  [ "$cpct" -lt 0 ] && cpct=0; [ "$cpct" -gt 100 ] && cpct=100
  filled=$(( (cpct + 5) / 10 )); [ "$filled" -gt 10 ] && filled=10
  if [ "$cpct" -lt 60 ]; then cc=$GREEN; elif [ "$cpct" -le 80 ]; then cc=$YELLOW; else cc=$RED; fi
  bar="" i=0
  while [ $i -lt $filled ]; do bar+="█"; i=$((i+1)); done
  while [ $i -lt 10 ]; do bar+="░"; i=$((i+1)); done
  ctxseg="⛁ ${cc}${bar} ${cpct}%${RESET}"
fi

seg5=$(render_window "5h" 18000 5 0 "$fh_used" "$fh_reset")
seg7=$(render_window "7d" 604800 7 1 "$sd_used" "$sd_reset")
usage="$seg5"
[ -n "$seg7" ] && { [ -n "$usage" ] && usage="$usage | $seg7" || usage="$seg7"; }

stat=""
if is_set "$cost"; then
  cf=$(printf '$%.2f' "$cost" 2>/dev/null)
  [ "$cf" != '$0.00' ] && stat="$cf"
fi
if is_set "$dur"; then
  ds=$(( ${dur%.*} / 1000 ))
  if [ "$ds" -gt 0 ]; then
    h=$(( ds / 3600 )); m=$(( (ds % 3600) / 60 ))
    if [ "$h" -gt 0 ]; then d="${h}h${m}m"; else d="${m}m"; fi
    [ -n "$stat" ] && stat="$stat$d" || stat="⏱ $d"
  fi
fi

# Time leads line 2, sitting under the model name on line 1.
line2="${TIMEC}$(date +%T)${RESET}"
[ -n "$ctxseg" ] && line2="$line2 | $ctxseg"
[ -n "$usage" ] && line2="$line2 | $usage"
[ -n "$stat" ] && line2="$line2$stat"

printf '%s\n%s\n' "$line1" "$line2"