Skip to content

Instantly share code, notes, and snippets.

@danielmrdev
Last active March 1, 2026 19:17
Show Gist options
  • Select an option

  • Save danielmrdev/4a73ae0341314ce6014ad9cd5828f3a0 to your computer and use it in GitHub Desktop.

Select an option

Save danielmrdev/4a73ae0341314ce6014ad9cd5828f3a0 to your computer and use it in GitHub Desktop.
Claude Code status line — powerline-style with git, context window (scaled progress bar), Claude API usage (5h/weekly), and right-aligned in-progress task

Claude Code Statusline

Powerline-style status line for Claude Code with git info, context window usage, and Claude API usage.

Preview

╭  Claude Haiku   ~/Projects/my-repo    feat/my-branch ⇡2 !1
╰ 󰆈 ●●●●●○○○○○ 45%  24% 3h42m 󰨳 8% 3d00h V8:59        ░▒▓ 󰄳 Fixing auth bug

Line 1: Model · Working directory · Git branch + ahead/behind/modified Line 2 (left): Context window (progress bar + %) · 5h usage + time to reset · Weekly usage + predicted depletion + reset day/time (Spanish weekday initial + time, e.g. V8:59 = Friday 8:59; days: L M X J V S D = Mon–Sun) Line 2 (right): Current in-progress task (from Claude Code todos), right-aligned

Context window details

  • The progress bar uses 10 segments (●/○) and scales so that 80% real usage = 100% displayed (Claude Code enforces an 80% context limit).
  • Color changes automatically: green → yellow → orange → red as usage grows.
  • A bridge file /tmp/claude-ctx-<session>.json is written on each render for use by other hooks.

Task segment details

  • Reads the active todo from ~/.claude/todos/<session>-agent-*.json (newest file wins).
  • Displays the activeForm field of the first in_progress task.
  • Hidden when no task is in progress.
  • Text truncated to 40 characters to avoid overflow.
  • Uses a reversed powerline separator (░▒▓) as the right-side entry.

Files

  • statusline.sh — Main script. Reads JSON from stdin (provided by Claude Code) and outputs the status line.
  • claude-usage.py — Fetches Claude API usage from claude.ai and writes a JSON cache to /tmp/claude-usage.json. Reads session cookies from Arc browser.

Installation

1. Install dependencies

The script uses uv to manage Python dependencies automatically via PEP 723 inline metadata — no manual installs needed.

brew install uv

2. Add to Claude Code settings

In ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/status-line/statusline.sh",
    "padding": 0
  }
}

3. Enable right-aligned task segment (optional)

The task segment needs to know the terminal width. Claude Code hooks run without a TTY so $COLUMNS is always 0. Add this to your ~/.zshrc to write the width to a side-channel file on every prompt:

precmd() { printf '%s' "$COLUMNS" > /tmp/claude-term-cols }

Without this the script falls back to 120 columns.

4. Set up cron for usage data (optional)

The usage segment requires claude-usage.py to run periodically to keep a local cache fresh.

(crontab -l 2>/dev/null; echo "*/3 * * * * /opt/homebrew/bin/uv run \${HOME}/.claude/status-line/claude-usage.py") | crontab -

Note: cron's PATH doesn't include /opt/homebrew/bin. If uv is installed elsewhere, adjust the path accordingly (which uv).

Verify it's working:

# Check cron entry
crontab -l | grep claude-usage

# Check cache (updated every 3 min)
cat /tmp/claude-usage.json

# Manually refresh cache
uv run ~/.claude/status-line/claude-usage.py

To remove:

crontab -l | grep -v claude-usage | crontab -

5. Save browser cookies

Bootstrap cookies for each account. For each account, log into claude.ai in Arc with that account, then run:

uv run ~/.claude/status-line/claude-usage.py --save-cookies

Repeat for every account you manage. Cookies are stored per account in ~/.claude-switch-backup/cookies/.

Multi-account cookie strategy:

  • Arc cookies are tried first. If they authenticate successfully (200), they are saved for the current Claude account and used.
  • If Arc is logged into a different account (403), the script falls back to the saved cookies for the active Claude account.
  • This means you only need to re-run --save-cookies when saved cookies expire, not on every account switch.

Notes

  • claude-usage.py reads session cookies from Arc browser's local SQLite database. If you use a different browser, the usage segment will be empty.
  • Git data is cached asynchronously in /tmp/claude-git-<hash>.json to avoid blocking.
  • The usage cache is valid for 3 hours; if older the usage segment is hidden until the next cron run. On fetch failure the existing cache is preserved (not overwritten with empty data).
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "cloudscraper",
# "cryptography",
# ]
# ///
"""
Fetches Claude Code session usage from the claude.ai API.
Persists cookies per account and reuses them automatically when switching accounts.
"""
import sqlite3, subprocess, hashlib, json, os, sys, time, base64
from datetime import datetime, timezone
import cloudscraper
CACHE_FILE = "/tmp/claude-usage.json"
CLAUDE_CONFIG = os.path.expanduser("~/.claude.json")
BACKUP_DIR = os.path.expanduser("~/.claude-switch-backup/cookies")
def get_current_account_email():
"""Read the current account email from ~/.claude.json"""
try:
with open(CLAUDE_CONFIG) as f:
config = json.load(f)
oauth = config.get("oauthAccount", {})
if isinstance(oauth, dict):
email = oauth.get("emailAddress")
if email:
return email
except Exception:
pass
return None
def get_cached_account_email():
"""Read the account email from the last cache file"""
try:
with open(CACHE_FILE) as f:
cache = json.load(f)
return cache.get("account_email")
except Exception:
return None
def get_cookies_file_path(email):
"""Return the cookies file path for the given account"""
if not email:
return None
safe_email = email.replace("@", "_at_").replace(".", "_")
return os.path.join(BACKUP_DIR, f".claude-cookies-{safe_email}.json")
def get_saved_cookies(email):
"""Read saved cookies for the given account"""
if not email:
return None
cookie_file = get_cookies_file_path(email)
if not cookie_file or not os.path.exists(cookie_file):
return None
try:
with open(cookie_file) as f:
return json.load(f)
except Exception:
return None
def save_cookies(email, cookies_dict):
"""Save cookies for the given account"""
if not email:
return False
os.makedirs(BACKUP_DIR, exist_ok=True)
cookie_file = get_cookies_file_path(email)
try:
with open(cookie_file, "w") as f:
json.dump(cookies_dict, f)
os.chmod(cookie_file, 0o600)
return True
except Exception:
return False
def decrypt_cookie(encrypted_value, key):
"""Decrypt an Arc browser cookie value"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
b = bytes(encrypted_value)
if b[:3] != b'v10':
return None
cipher = Cipher(algorithms.AES(key), modes.CBC(b' ' * 16), backend=default_backend())
decryptor = cipher.decryptor()
raw = decryptor.update(b[3:]) + decryptor.finalize()
pad_len = raw[-1]
if 0 < pad_len <= 16:
raw = raw[:-pad_len]
if b'sk-ant-' in raw:
idx = raw.find(b'sk-ant-')
return raw[idx:].decode('ascii', errors='ignore')
raw = raw[16:]
for i in range(len(raw)):
chunk = raw[i:i+8]
if all(32 <= c < 127 for c in chunk):
return raw[i:].decode('ascii', errors='ignore')
return None
def get_arc_cookies():
"""Read current claude.ai session cookies from Arc browser"""
try:
pw = subprocess.run(
['security', 'find-generic-password', '-s', 'Arc Safe Storage', '-w'],
capture_output=True, text=True, timeout=3
).stdout.strip()
if not pw:
return None
key = hashlib.pbkdf2_hmac('sha1', pw.encode(), b'saltysalt', 1003, dklen=16)
except Exception:
return None
db_path = os.path.expanduser(
"~/Library/Application Support/Arc/User Data/Default/Cookies"
)
try:
conn = sqlite3.connect(f"file:{db_path}?immutable=1&timeout=2", uri=True)
cursor = conn.cursor()
cursor.execute(
"SELECT name, encrypted_value FROM cookies "
"WHERE host_key LIKE '%claude.ai%' AND name IN ('sessionKey', 'cf_clearance')"
)
cookies = {}
for name, enc_val in cursor.fetchall():
val = decrypt_cookie(enc_val, key)
if val:
cookies[name] = val
conn.close()
return cookies if cookies else None
except Exception:
return None
def fetch_usage():
"""Fetch usage data using saved or Arc browser cookies"""
import signal
def timeout_handler(signum, frame):
raise TimeoutError("Fetch timeout")
# Global 6-second timeout (allows Arc attempt + saved fallback)
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(6)
try:
current_email = get_current_account_email()
arc_cookies = get_arc_cookies()
saved_cookies = get_saved_cookies(current_email)
# If Arc's sessionKey differs from the saved one for this account,
# Arc is logged into a different account — exclude it from candidates.
arc_is_different_account = (
arc_cookies is not None and
saved_cookies is not None and
arc_cookies.get('sessionKey') != saved_cookies.get('sessionKey')
)
candidates = []
if arc_cookies and not arc_is_different_account:
candidates.append(('arc', arc_cookies))
if saved_cookies:
candidates.append(('saved', saved_cookies))
base_headers = {
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.9",
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
),
"Referer": "https://claude.ai/",
}
for source, cookies in candidates:
if not cookies or 'sessionKey' not in cookies:
continue
try:
scraper = cloudscraper.create_scraper()
scraper.headers.update({
**base_headers,
"Cookie": "; ".join(f"{k}={v}" for k, v in cookies.items()),
})
orgs_resp = scraper.get("https://claude.ai/api/organizations", timeout=3)
if orgs_resp.status_code != 200:
continue # wrong account or expired — try next source
orgs = orgs_resp.json()
org_id = (orgs[0] if isinstance(orgs, list) else orgs).get("uuid") if orgs else None
if not org_id:
continue
usage_resp = scraper.get(
f"https://claude.ai/api/organizations/{org_id}/usage", timeout=3
)
if usage_resp.status_code != 200:
continue
return usage_resp.json()
except Exception:
continue
return None
except (TimeoutError, Exception):
return None
finally:
signal.alarm(0)
def format_duration(total_minutes):
"""Format minutes as '2d15h', '1h23m' or '45m'."""
total_minutes = int(total_minutes)
if total_minutes <= 0:
return "now"
days = total_minutes // (60 * 24)
hours = (total_minutes % (60 * 24)) // 60
minutes = total_minutes % 60
if days > 0:
return f"{days}d{hours:02d}h"
if hours > 0:
return f"{hours}h{minutes:02d}m"
return f"{minutes}m"
def format_time_until_reset(resets_at_iso):
"""Return time remaining until reset as '1h23m' or '45m'."""
try:
reset_time = datetime.fromisoformat(resets_at_iso)
now = datetime.now(timezone.utc)
diff = reset_time - now
total_seconds = int(diff.total_seconds())
if total_seconds <= 0:
return "now"
return format_duration(total_seconds // 60)
except Exception:
return ""
def weekly_prediction(utilization, resets_at_iso):
"""
Estimate time remaining before the weekly limit is exhausted
at the current consumption rate.
"""
try:
from datetime import timedelta
reset_time = datetime.fromisoformat(resets_at_iso)
started_at = reset_time - timedelta(days=7)
now = datetime.now(timezone.utc)
elapsed_minutes = (now - started_at).total_seconds() / 60
if utilization <= 0 or elapsed_minutes <= 0:
return ""
remaining_pct = 100 - utilization
if remaining_pct <= 0:
return "!"
minutes_left = remaining_pct * elapsed_minutes / utilization
seconds_to_reset = (reset_time - now).total_seconds()
if minutes_left * 60 > seconds_to_reset:
return ""
return format_duration(minutes_left)
except Exception:
return ""
def save_cache_file(content, account_email=None):
"""Write cache JSON with timestamp and account email"""
try:
cache_data = {
"timestamp": int(time.time()),
"five_h_info": content.get("five_h_info", ""),
"weekly_info": content.get("weekly_info", ""),
"account_email": account_email
}
with open(CACHE_FILE, "w") as f:
json.dump(cache_data, f)
except Exception:
pass
def _mask_key(key):
"""Show only first and last 4 chars of a session key."""
if not key or len(key) < 12:
return "(too short)"
return f"{key[:8]}...{key[-4:]}"
def cmd_check_cookies():
"""Command: compare Arc vs saved cookies and report if --save-cookies is needed"""
email = get_current_account_email()
if not email:
print("Error: no active account found in ~/.claude.json")
return
print(f"Account : {email}")
print()
arc_cookies = get_arc_cookies()
saved_cookies = get_saved_cookies(email)
arc_key = arc_cookies.get('sessionKey') if arc_cookies else None
saved_key = saved_cookies.get('sessionKey') if saved_cookies else None
# Arc status
if arc_key:
print(f"Arc : {_mask_key(arc_key)}")
else:
print("Arc : not found (Arc not running or not logged in to claude.ai)")
# Saved status
if saved_key:
cookie_file = get_cookies_file_path(email)
mtime = os.path.getmtime(cookie_file)
age_days = (time.time() - mtime) / 86400
print(f"Saved : {_mask_key(saved_key)} (saved {age_days:.0f}d ago)")
else:
print("Saved : not found")
# Cache status
print()
try:
with open(CACHE_FILE) as f:
cache = json.load(f)
cache_email = cache.get("account_email", "unknown")
cache_age = (time.time() - cache.get("timestamp", 0)) / 60
match = "✓" if cache_email == email else "✗ MISMATCH"
print(f"Cache : {cache_email} [{match}] ({cache_age:.0f} min ago)")
except Exception:
print("Cache : not found")
# Verdict
print()
if not arc_key and not saved_key:
print("✗ No cookies available — cannot fetch usage data")
elif not arc_key and saved_key:
print("✓ Arc not available, will use saved cookies")
elif arc_key and not saved_key:
print("! No saved cookies — run: uv run claude-usage.py --save-cookies")
elif arc_key == saved_key:
print("✓ Arc and saved cookies match — no action needed")
else:
print("✗ Arc session differs from saved cookies")
print(" Arc is logged into a different account than Claude Code")
print(" To fix: log Arc into the correct account, then run:")
print(" uv run claude-usage.py --save-cookies")
def cmd_save_cookies():
"""Command: save cookies for the current account"""
email = get_current_account_email()
if not email:
print("Error: No active Claude account found")
return False
cookies = get_arc_cookies()
if not cookies:
print("Error: No cookies found for current account")
return False
if save_cookies(email, cookies):
print(f"✓ Cookies saved for {email}")
return True
else:
print(f"Error: Failed to save cookies for {email}")
return False
def main():
# Handle commands
if len(sys.argv) > 1:
if sys.argv[1] == "--save-cookies":
cmd_save_cookies()
return
if sys.argv[1] == "--check-cookies":
cmd_check_cookies()
return
# Normal flow: fetch and cache usage
current_email = get_current_account_email()
data = fetch_usage()
if not data:
return
# 5-hour window
five_h = data.get("five_hour") or {}
utilization = five_h.get("utilization")
resets_at = five_h.get("resets_at", "")
five_h_parts = []
if utilization is not None:
five_h_parts.append(f"{int(utilization)}%")
if resets_at:
remaining = format_time_until_reset(resets_at)
if remaining:
five_h_parts.append(remaining)
five_h_info = " ".join(five_h_parts)
# 7-day window
seven_d = data.get("seven_day") or {}
w_util = seven_d.get("utilization")
w_resets_at = seven_d.get("resets_at", "")
weekly_parts = []
if w_util is not None:
weekly_parts.append(f"{int(w_util)}%")
if w_resets_at:
if w_util is not None:
pred = weekly_prediction(w_util, w_resets_at)
if pred:
weekly_parts.append(pred)
try:
reset_time = datetime.fromisoformat(w_resets_at).astimezone()
day_letters = ["L", "M", "X", "J", "V", "S", "D"] # Spanish weekday initials: Mon-Sun (Wed=X to avoid duplicate M)
day_letter = day_letters[reset_time.weekday()]
weekly_parts.append(f"{day_letter}{reset_time.hour}:{reset_time.minute:02d}")
except Exception:
pass
weekly_info = " ".join(weekly_parts)
save_cache_file({"five_h_info": five_h_info, "weekly_info": weekly_info}, current_email)
if __name__ == "__main__":
main()
#!/usr/bin/env bash
# Status line for Claude Code
# Receives JSON via stdin, outputs a single status line
# Read stdin with timeout to prevent blocking
input=$(timeout 1 command cat 2>/dev/null || echo "{}")
model=$( printf '%s' "$input" | jq -r '.model.display_name // ""' 2>/dev/null)
remaining_pct=$(printf '%s' "$input" | jq -r '.context_window.remaining_percentage // ""' 2>/dev/null)
session=$( printf '%s' "$input" | jq -r '.session_id // ""' 2>/dev/null)
work_dir=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // ""' 2>/dev/null)
[[ -z "$work_dir" || "$work_dir" == "null" ]] && work_dir="$PWD"
git_branch=""
git_ahead=0
git_behind=0
git_modified=0
git_untracked=0
if [[ -n "$work_dir" && -d "$work_dir" ]]; then
# Cache file per directory
dir_hash=$(printf '%s' "$work_dir" | cksum | awk '{print $1}')
git_cache="/tmp/claude-git-${dir_hash}.json"
# Read from cache (instant, never blocks)
if [[ -f "$git_cache" ]]; then
git_branch=$(jq -r '.branch // ""' "$git_cache" 2>/dev/null)
git_ahead=$(jq -r '.ahead // 0' "$git_cache" 2>/dev/null)
git_behind=$(jq -r '.behind // 0' "$git_cache" 2>/dev/null)
git_modified=$(jq -r '.modified // 0' "$git_cache" 2>/dev/null)
git_untracked=$(jq -r '.untracked // 0' "$git_cache" 2>/dev/null)
fi
# Update cache in background (for next render)
{
gs=$(git -C "$work_dir" --no-optional-locks status --porcelain=v2 --branch 2>/dev/null)
if [[ -n "$gs" ]]; then
b=$(printf '%s' "$gs" | awk '/^# branch.head / { print $3; exit }')
[[ "$b" == "(detached)" ]] && b=""
a=$(printf '%s' "$gs" | awk '/^# branch.ab / { gsub(/[^0-9]/,"",$3); print $3+0; exit }')
bh=$(printf '%s' "$gs" | awk '/^# branch.ab / { gsub(/[^0-9]/,"",$4); print $4+0; exit }')
m=$(printf '%s' "$gs" | awk '/^[12] / { c++ } END { print c+0 }')
u=$(printf '%s' "$gs" | awk '/^\? / { c++ } END { print c+0 }')
printf '{"branch":"%s","ahead":%s,"behind":%s,"modified":%s,"untracked":%s}' \
"$b" "${a:-0}" "${bh:-0}" "${m:-0}" "${u:-0}" > "$git_cache"
fi
} &>/dev/null &
disown
fi
# Current in-progress task from todos
task=""
if [[ -n "$session" ]]; then
todos_dir="$HOME/.claude/todos"
if [[ -d "$todos_dir" ]]; then
task_file=$(ls -t "$todos_dir/${session}"*-agent-*.json 2>/dev/null | head -1)
if [[ -n "$task_file" && -f "$task_file" ]]; then
task=$(jq -r '[.[] | select(.status == "in_progress")] | first | .activeForm // ""' \
"$task_file" 2>/dev/null)
[[ "$task" == "null" ]] && task=""
fi
fi
fi
# Read cached usage from file (updated by cron)
five_h_info=""
weekly_info=""
if [[ -f /tmp/claude-usage.json ]]; then
cached=$(command cat /tmp/claude-usage.json 2>/dev/null)
timestamp=$(printf '%s' "$cached" | jq -r '.timestamp // 0' 2>/dev/null)
now=$(date +%s)
age=$((now - timestamp))
# Only use cache if less than 3 hours old
if [[ $age -lt 10800 ]]; then
five_h_info=$(printf '%s' "$cached" | jq -r '.five_h_info // ""' 2>/dev/null)
weekly_info=$(printf '%s' "$cached" | jq -r '.weekly_info // ""' 2>/dev/null)
fi
fi
MAX_DIR_LEN=20
# ANSI
RESET=$'\033[0m'
BOLD=$'\033[1m'
NOBOLD=$'\033[22m'
FIRST_LINE="╭"
SECOND_LINE="╰"
END_SEG="▓▒░"
SEP_ICON=""
DIR_ICON=""
ROBOT_ICON="󱙺"
GIT_ICON=""
BRANCH_ICON=""
PUSH_ICON="⇡"
PULL_ICON="⇣"
CTX_ICON="󰆈"
CLOCK_ICON="󰥔"
WEEK_ICON="󰨳"
C_DIR=$'\033[48;2;129;161;193m\033[38;2;236;239;244m'
C_MODEL=$'\033[48;2;94;129;172m\033[38;2;255;255;255m'
C_CTX=$'\033[48;2;180;142;173m\033[38;2;236;239;244m'
C_GIT=$'\033[48;2;163;190;140m\033[38;2;46;52;64m'
C_GIT_DIRTY=$'\033[48;2;235;203;139m\033[38;2;46;52;64m'
C_USAGE=$'\033[48;2;208;135;112m\033[38;2;46;52;64m'
BG_DIR=$'\033[48;2;129;161;193m'
BG_MODEL=$'\033[48;2;94;129;172m'
BG_CTX=$'\033[48;2;180;142;173m'
BG_GIT=$'\033[48;2;163;190;140m'
BG_GIT_DIRTY=$'\033[48;2;235;203;139m'
BG_USAGE=$'\033[48;2;208;135;112m'
C_TASK=$'\033[48;2;59;66;82m\033[38;2;216;222;233m'
BG_TASK=$'\033[48;2;59;66;82m'
# Context window: scale so 80% real usage = 100% displayed
used_pct=""
ctx_bar=""
C_CTX_DYN="$C_CTX"
BG_CTX_DYN="$BG_CTX"
if [[ -n "$remaining_pct" && "$remaining_pct" != "null" ]]; then
raw_used=$(awk -v r="$remaining_pct" 'BEGIN { v=100-r; if(v<0)v=0; if(v>100)v=100; printf "%d", int(v+0.5) }')
used=$(awk -v ru="$raw_used" 'BEGIN { v=(ru/80)*100; if(v>100)v=100; printf "%d", int(v+0.5) }')
used_pct="$used"
# Bridge file for context-monitor PostToolUse hook
if [[ -n "$session" ]]; then
bridge_path="/tmp/claude-ctx-${session}.json"
now=$(date +%s)
printf '{"session_id":"%s","remaining_percentage":%s,"used_pct":%s,"timestamp":%s}' \
"$session" "$remaining_pct" "$used" "$now" > "$bridge_path" 2>/dev/null || true
fi
# Progress bar (10 segments)
filled=$(( used / 10 ))
bar=""
for (( j=0; j<filled && j<10; j++ )); do bar+="●"; done
for (( j=filled; j<10; j++ )); do bar+="○"; done
ctx_bar="$bar"
# Dynamic color based on scaled usage
if (( used < 63 )); then # ~50% real
C_CTX_DYN=$'\033[48;2;163;190;140m\033[38;2;46;52;64m'
BG_CTX_DYN=$'\033[48;2;163;190;140m'
elif (( used < 81 )); then # ~65% real
C_CTX_DYN=$'\033[48;2;235;203;139m\033[38;2;46;52;64m'
BG_CTX_DYN=$'\033[48;2;235;203;139m'
elif (( used < 95 )); then # ~76% real
C_CTX_DYN=$'\033[48;2;208;135;112m\033[38;2;46;52;64m'
BG_CTX_DYN=$'\033[48;2;208;135;112m'
else # critical
C_CTX_DYN=$'\033[48;2;191;97;106m\033[38;2;236;239;244m'
BG_CTX_DYN=$'\033[48;2;191;97;106m'
ctx_bar="💀 $bar"
fi
fi
MODEL_ICON="$ROBOT_ICON"
if [[ -n "$model" ]]; then
model="${model/Claude /}"
model_lower=$(echo "$model" | tr '[:upper:]' '[:lower:]')
case "$model_lower" in
haiku*) C_MODEL=$'\033[48;2;236;239;244m\033[38;2;163;190;140m' ; MODEL_ICON="" ;;
sonnet*) C_MODEL=$'\033[48;2;236;239;244m\033[38;2;235;203;139m' ; MODEL_ICON="" ;;
opus*) C_MODEL=$'\033[48;2;236;239;244m\033[38;2;191;97;106m' ; MODEL_ICON="" ;;
esac
BG_MODEL="${C_MODEL%%$'\033[38'*}"
fi
# Powerline separator: fg = prev bg, bg = next bg (empty = terminal default)
make_sep() {
local prev_fg="${1/\[48;/\[38;}"
[[ -n "$2" ]] && printf '%s%s%s' "$2" "$prev_fg" "$SEP_ICON" \
|| printf '%s%s%s' "$RESET" "$prev_fg" "$SEP_ICON"
}
# End segment: uses END_SEG instead of separator
make_end() {
local prev_fg="${1/\[48;/\[38;}"
printf '%s%s%s' "$RESET" "$prev_fg" "$END_SEG"
}
# Start of right-aligned segment: reversed entry separator (░▒▓)
make_start_right() {
local seg_fg="${1/\[48;/\[38;}"
printf '%s%s░▒▓' "$RESET" "$seg_fg"
}
parts=()
bgs=()
[[ -n "$model" ]] && { parts+=("${C_MODEL} ${MODEL_ICON} "); bgs+=("$BG_MODEL"); }
if [[ -n "$work_dir" ]]; then
display_dir="~${work_dir#"$HOME"}"
dir_base="${display_dir##*/}"
dir_prefix="${display_dir%"$dir_base"}"
if (( ${#display_dir} > MAX_DIR_LEN )); then
avail=$(( MAX_DIR_LEN - 1 ))
if (( ${#dir_base} >= avail )); then
dir_prefix="…"
else
prefix_avail=$(( avail - ${#dir_base} ))
dir_prefix="…${dir_prefix: -$prefix_avail}"
fi
fi
parts+=("${C_DIR} ${DIR_ICON} ${dir_prefix}${BOLD}${dir_base}${NOBOLD} ")
bgs+=("$BG_DIR")
fi
if [[ -n "$git_branch" ]]; then
git_seg="${GIT_ICON} ${BRANCH_ICON} ${git_branch}"
[[ "$git_ahead" -gt 0 ]] && git_seg+=" ${PUSH_ICON}${git_ahead}"
[[ "$git_behind" -gt 0 ]] && git_seg+=" ${PULL_ICON}${git_behind}"
[[ "$git_modified" -gt 0 ]] && git_seg+=" !${git_modified}"
[[ "$git_untracked" -gt 0 ]] && git_seg+=" ?${git_untracked}"
if [[ "$git_modified" -gt 0 ]]; then
C_GIT_ACTIVE="$C_GIT_DIRTY"; BG_GIT_ACTIVE="$BG_GIT_DIRTY"
else
C_GIT_ACTIVE="$C_GIT"; BG_GIT_ACTIVE="$BG_GIT"
fi
parts+=("${C_GIT_ACTIVE} ${git_seg} ")
bgs+=("$BG_GIT_ACTIVE")
fi
parts2=()
bgs2=()
if [[ -n "$used_pct" ]]; then
parts2+=("${C_CTX_DYN} ${CTX_ICON} ${ctx_bar} ${used_pct}% "); bgs2+=("$BG_CTX_DYN")
fi
usage_seg=""
[[ -n "$five_h_info" ]] && usage_seg+="${CLOCK_ICON} ${five_h_info}"
[[ -n "$weekly_info" ]] && usage_seg+=" ${WEEK_ICON} ${weekly_info}"
if [[ -n "$usage_seg" ]]; then
parts2+=("${C_USAGE} ${usage_seg} ")
bgs2+=("$BG_USAGE")
fi
line1="${FIRST_LINE}"
for (( i=0; i<${#parts[@]}; i++ )); do
(( i == 0 )) && line1+="${parts[$i]}" \
|| line1+="$(make_sep "${bgs[$((i-1))]}" "${bgs[$i]}")${parts[$i]}"
done
(( ${#parts[@]} > 0 )) && line1+="$(make_end "${bgs[${#bgs[@]}-1]}")"
line2="${RESET}${SECOND_LINE}"
for (( i=0; i<${#parts2[@]}; i++ )); do
(( i == 0 )) && line2+="${parts2[$i]}" \
|| line2+="$(make_sep "${bgs2[$((i-1))]}" "${bgs2[$i]}")${parts2[$i]}"
done
(( ${#parts2[@]} > 0 )) && line2+="$(make_end "${bgs2[${#bgs2[@]}-1]}")"
# Right-aligned task segment on line 2
if [[ -n "$task" ]]; then
TASK_ICON="󰄳"
[[ ${#task} -gt 40 ]] && task="${task:0:40}…"
right_content=" ${TASK_ICON} ${task} "
right_entry="$(make_start_right "$BG_TASK")"
right_seg="${right_entry}${C_TASK}${right_content}${RESET}"
# Get terminal width — hook has no TTY; read from side-channel file written by precmd
cols=${COLUMNS:-0}
(( cols <= 0 )) && cols=$(cat /tmp/claude-term-cols 2>/dev/null || echo 0)
(( cols <= 0 )) && cols=120
# Nerd font icons in line2: each is 1 codepoint but renders as 2 columns
nf_left=0
[[ -n "$used_pct" ]] && (( nf_left++ )) # CTX_ICON 󰆈
[[ -n "$five_h_info" ]] && (( nf_left++ )) # CLOCK_ICON 󰥔
[[ -n "$weekly_info" ]] && (( nf_left++ )) # WEEK_ICON 󰨳
line2_vis=$(printf '%s' "$line2" | sed $'s/\033\\[[0-9;]*[a-zA-Z]//g')
left_len=$(( ${#line2_vis} + nf_left ))
right_vis="░▒▓${right_content}"
right_len=$(( ${#right_vis} + 1 )) # +1 for TASK_ICON 󰄳 (2-wide)
pad=$(( cols - 3 - left_len - right_len ))
(( pad >= 1 )) && line2+="$(printf '%*s' "$pad" '')${right_seg}"
fi
printf "%s\n%s" "$line1" "$line2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment