Last active
February 23, 2026 12:41
-
-
Save ondrasek/1f801107be6b7e391b488c723b125260 to your computer and use it in GitHub Desktop.
Claude Code statusline script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Claude Code status line script (bash port of PowerShell original) | |
| # Reads JSON session data from stdin, outputs multiple lines. | |
| # Line order is controlled by the render_* call sequence in section 17. | |
| # --- 1. Constants --- | |
| # oh-my-posh inspired true-color palette | |
| C_BLUE=$'\033[38;2;0;153;255m' | |
| C_ORANGE=$'\033[38;2;255;176;85m' | |
| C_GREEN=$'\033[38;2;0;160;0m' | |
| C_CYAN=$'\033[38;2;46;149;153m' | |
| C_RED=$'\033[38;2;255;85;85m' | |
| C_YELLOW=$'\033[38;2;230;200;0m' | |
| C_DIM=$'\033[38;2;80;80;80m' | |
| C_WHITE=$'\033[38;2;220;220;220m' | |
| C_RESET=$'\033[0m' | |
| # Semantic aliases (change one line here to restyle globally) | |
| C_TITLE="$C_WHITE" # label prefixes: "session:", "git:", "hooks:", etc. | |
| C_EMPTY="$C_DIM" # empty-state placeholders: "none", "(waiting for data)" | |
| C_VALUE="$C_CYAN" # primary data values: percentages, costs, language versions | |
| C_ACCENT="$C_ORANGE" # secondary emphasis: token counts, agent name, vim mode, ETA | |
| C_GOOD="$C_GREEN" # positive states: clean, passing, lines added | |
| C_BAD="$C_RED" # negative states: failing, compaction, lines removed | |
| C_WARN="$C_YELLOW" # warnings/attention: pending, stash count, concurrent sessions | |
| C_MUTED="$C_DIM" # structural dim: separators, brackets, bar empties, reset times | |
| C_MODEL="$C_BLUE" # model name display | |
| USAGE_CACHE="/tmp/claude-statusline-usage.json" | |
| USAGE_CACHE_TTL=20 | |
| PR_CACHE_TTL=20 | |
| # --- 2. Fallback trap --- | |
| set -eE # errexit + inherit ERR trap into subshells and command substitutions | |
| trap 'printf "Claude"; exit 0' ERR | |
| # --- 3. Guard: require jq --- | |
| command -v jq >/dev/null 2>&1 || { printf 'Claude'; exit 0; } | |
| # --- 4. Functions --- | |
| # Portable date parsing: converts ISO 8601 timestamp to epoch seconds. | |
| # GNU date uses -d, macOS/BSD date uses -jf. | |
| date_to_epoch() { | |
| local ts=$1 | |
| date -d "$ts" +%s 2>/dev/null && return | |
| # macOS/BSD: strip fractional seconds for parsing | |
| # Check for Z on the original string BEFORE stripping fractional part, | |
| # because .NNNZ gets removed together by %%.* | |
| local clean="$ts" | |
| local is_utc=false | |
| [[ "$clean" == *Z ]] && is_utc=true | |
| clean=${clean%Z} # drop trailing Z if present | |
| clean=${clean%%.*} # drop .NNN fractional part | |
| if [ "$is_utc" = true ]; then | |
| TZ=UTC date -jf "%Y-%m-%dT%H:%M:%S" "$clean" +%s 2>/dev/null && return | |
| else | |
| date -jf "%Y-%m-%dT%H:%M:%S" "$clean" +%s 2>/dev/null && return | |
| fi | |
| return 1 | |
| } | |
| # Portable date formatting: formats ISO 8601 timestamp with a strftime format. | |
| date_fmt() { | |
| local ts=$1 fmt=$2 | |
| date -d "$ts" "+$fmt" 2>/dev/null && return | |
| local clean="$ts" | |
| local is_utc=false | |
| [[ "$clean" == *Z ]] && is_utc=true | |
| clean=${clean%Z} | |
| clean=${clean%%.*} | |
| if [ "$is_utc" = true ]; then | |
| TZ=UTC date -jf "%Y-%m-%dT%H:%M:%S" "$clean" "+$fmt" 2>/dev/null && return | |
| else | |
| date -jf "%Y-%m-%dT%H:%M:%S" "$clean" "+$fmt" 2>/dev/null && return | |
| fi | |
| return 1 | |
| } | |
| # Portable date arithmetic: outputs formatted date for an expression. | |
| # GNU: date -d "$expr" "+$fmt", macOS: date -v modifier. | |
| date_calc() { | |
| local expr=$1 fmt=$2 | |
| date -d "$expr" "+$fmt" 2>/dev/null && return | |
| # Fallback for macOS: only supports "+1 month from 1st" used below | |
| if [[ "$expr" == *"+1 month"* ]]; then | |
| date -v1d -v+1m "+$fmt" 2>/dev/null && return | |
| fi | |
| return 1 | |
| } | |
| # Portable file mtime: returns epoch seconds of file's last modification. | |
| # GNU stat uses -c %Y, macOS/BSD stat uses -f %m. | |
| file_mtime() { | |
| stat -c %Y "$1" 2>/dev/null && return | |
| stat -f %m "$1" 2>/dev/null && return | |
| echo 0 | |
| } | |
| format_tokens() { | |
| local n=$1 | |
| # Guard: only process numeric values to prevent awk injection | |
| [[ "$n" =~ ^[0-9]+$ ]] || { printf '0'; return; } | |
| if [ "$n" -ge 1000000 ] 2>/dev/null; then | |
| awk -v n="$n" 'BEGIN {v=n/1000000; if (v==int(v)) printf "%dm",v; else printf "%.1fm",v}' | |
| elif [ "$n" -ge 1000 ] 2>/dev/null; then | |
| awk -v n="$n" 'BEGIN {printf "%dk", n/1000}' | |
| else | |
| printf '%d' "$n" | |
| fi | |
| } | |
| format_number() { | |
| # Add comma separators locale-independently: 155000 -> 155,000 | |
| awk -v n="${1:-0}" 'BEGIN { | |
| s = sprintf("%d", n) | |
| len = length(s) | |
| r = "" | |
| for (i = 1; i <= len; i++) { | |
| if (i > 1 && (len - i + 1) % 3 == 0) r = r "," | |
| r = r substr(s, i, 1) | |
| } | |
| print r | |
| }' | |
| } | |
| color_for_pct() { | |
| local pct=$1 | |
| if [ "$pct" -ge 90 ] 2>/dev/null; then printf '%s' "$C_BAD" | |
| elif [ "$pct" -ge 70 ] 2>/dev/null; then printf '%s' "$C_WARN" | |
| elif [ "$pct" -ge 50 ] 2>/dev/null; then printf '%s' "$C_ACCENT" | |
| else printf '%s' "$C_GOOD" | |
| fi | |
| } | |
| build_bar() { | |
| local pct=${1:-0} width=${2:-10} | |
| [ "$pct" -lt 0 ] 2>/dev/null && pct=0 | |
| [ "$pct" -gt 100 ] 2>/dev/null && pct=100 | |
| local filled=$((pct * width / 100)) | |
| [ "$filled" -gt "$width" ] && filled=$width | |
| local empty=$((width - filled)) | |
| local bar_color | |
| bar_color=$(color_for_pct "$pct") | |
| local bar="" | |
| if [ "$filled" -gt 0 ]; then | |
| bar="${bar_color}$(printf '%0.s█' $(seq 1 "$filled"))" | |
| fi | |
| if [ "$empty" -gt 0 ]; then | |
| bar="${bar}${C_MUTED}$(printf '%0.s░' $(seq 1 "$empty"))" | |
| fi | |
| printf '%s%s' "$bar" "$C_RESET" | |
| } | |
| format_reset_time_relative() { | |
| local timestamp=$1 | |
| [ -z "$timestamp" ] && return | |
| local reset_epoch now_epoch diff | |
| reset_epoch=$(date_to_epoch "$timestamp") || return | |
| now_epoch=$(date +%s) | |
| diff=$((reset_epoch - now_epoch)) | |
| if [ "$diff" -gt 0 ]; then | |
| local hours=$((diff / 3600)) | |
| local mins=$(((diff % 3600) / 60)) | |
| if [ "$hours" -gt 0 ]; then | |
| printf '%dh%02dm' "$hours" "$mins" | |
| else | |
| printf '%dm' "$mins" | |
| fi | |
| else | |
| printf 'resetting...' | |
| fi | |
| } | |
| format_reset_time_absolute() { | |
| local timestamp=$1 | |
| [ -z "$timestamp" ] && return | |
| date_fmt "$timestamp" "%b %-d, %-I:%M%P" 2>/dev/null | tr '[:upper:]' '[:lower:]' | |
| } | |
| # --- 5. Parse stdin JSON (single jq call) --- | |
| input=$(cat) | |
| [ -z "$input" ] && { printf 'Claude'; exit 0; } | |
| IFS='|' read -r DISPLAY_NAME PROJECT_DIR CTX_SIZE CTX_PCT INPUT_TOK CACHE_CREATE CACHE_READ \ | |
| TOTAL_COST TOTAL_DURATION TOTAL_API_DURATION LINES_ADDED LINES_REMOVED \ | |
| TOTAL_INPUT_TOK TOTAL_OUTPUT_TOK VIM_MODE AGENT_NAME SESSION_ID <<< \ | |
| "$(printf '%s' "$input" | jq -r '[ | |
| .model.display_name // "?", | |
| .workspace.project_dir // ".", | |
| (.context_window.context_window_size // 200000), | |
| (.context_window.used_percentage // 0 | floor), | |
| (.context_window.current_usage.input_tokens // 0), | |
| (.context_window.current_usage.cache_creation_input_tokens // 0), | |
| (.context_window.current_usage.cache_read_input_tokens // 0), | |
| (.cost.total_cost_usd // 0), | |
| (.cost.total_duration_ms // 0), | |
| (.cost.total_api_duration_ms // 0), | |
| (.cost.total_lines_added // 0), | |
| (.cost.total_lines_removed // 0), | |
| (.context_window.total_input_tokens // 0), | |
| (.context_window.total_output_tokens // 0), | |
| (.vim.mode // ""), | |
| (.agent.name // ""), | |
| (.session_id // "") | |
| ] | join("|")' 2>/dev/null)" | |
| DISPLAY_NAME="${DISPLAY_NAME:-?}" | |
| CTX_SIZE="${CTX_SIZE:-200000}" | |
| CTX_PCT="${CTX_PCT%%.*}" | |
| CTX_PCT="${CTX_PCT:-0}" | |
| [ "$CTX_PCT" -eq "$CTX_PCT" ] 2>/dev/null || CTX_PCT=0 | |
| INPUT_TOK="${INPUT_TOK:-0}" | |
| CACHE_CREATE="${CACHE_CREATE:-0}" | |
| CACHE_READ="${CACHE_READ:-0}" | |
| TOTAL_COST="${TOTAL_COST:-0}" | |
| TOTAL_DURATION="${TOTAL_DURATION:-0}" | |
| TOTAL_API_DURATION="${TOTAL_API_DURATION:-0}" | |
| LINES_ADDED="${LINES_ADDED:-0}" | |
| LINES_REMOVED="${LINES_REMOVED:-0}" | |
| TOTAL_INPUT_TOK="${TOTAL_INPUT_TOK:-0}" | |
| TOTAL_OUTPUT_TOK="${TOTAL_OUTPUT_TOK:-0}" | |
| VIM_MODE="${VIM_MODE:-}" | |
| AGENT_NAME="${AGENT_NAME:-}" | |
| SESSION_ID="${SESSION_ID:-}" | |
| # --- 6. Git branch --- | |
| GIT_BRANCH="" | |
| GIT_WORKTREE="" | |
| GIT_TOPLEVEL="" | |
| GIT_CWD_REL="" | |
| GIT_ORIGIN_REPO="" | |
| GIT_UPSTREAM_REPO="" | |
| IS_GIT_REPO=false | |
| if git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then | |
| IS_GIT_REPO=true | |
| GIT_BRANCH=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null) | |
| # Working copy folder name and cwd relative to repo root | |
| GIT_TOPLEVEL=$(git -C "$PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null) | |
| GIT_TOPLEVEL_NAME="${GIT_TOPLEVEL##*/}" | |
| if [ -n "$GIT_TOPLEVEL" ] && [ -n "$PROJECT_DIR" ]; then | |
| rel="${PROJECT_DIR#"$GIT_TOPLEVEL"}" | |
| rel="${rel#/}" | |
| GIT_CWD_REL="${rel:-.}" | |
| fi | |
| # Detect worktree: git-dir differs from git-common-dir in worktrees | |
| git_dir=$(git -C "$PROJECT_DIR" rev-parse --git-dir 2>/dev/null) | |
| git_common=$(git -C "$PROJECT_DIR" rev-parse --git-common-dir 2>/dev/null) | |
| if [ -n "$git_dir" ] && [ -n "$git_common" ] && [ "$git_dir" != "$git_common" ]; then | |
| GIT_WORKTREE="${GIT_TOPLEVEL_NAME}" | |
| fi | |
| # Origin remote: try gh first, fall back to git remote -v | |
| if command -v gh >/dev/null 2>&1; then | |
| GIT_ORIGIN_REPO=$(cd "$PROJECT_DIR" && gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null) || GIT_ORIGIN_REPO="" | |
| fi | |
| if [ -z "$GIT_ORIGIN_REPO" ]; then | |
| origin_url=$(git -C "$PROJECT_DIR" remote get-url origin 2>/dev/null) || origin_url="" | |
| if [ -n "$origin_url" ]; then | |
| # Parse owner/repo from SSH or HTTPS URLs | |
| GIT_ORIGIN_REPO=$(printf '%s' "$origin_url" | sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#') | |
| fi | |
| fi | |
| # Upstream remote (common for forks) | |
| upstream_url=$(git -C "$PROJECT_DIR" remote get-url upstream 2>/dev/null) || upstream_url="" | |
| if [ -n "$upstream_url" ]; then | |
| GIT_UPSTREAM_REPO=$(printf '%s' "$upstream_url" | sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#') | |
| fi | |
| fi | |
| # --- 7. Context window calculations --- | |
| CURRENT_TOKENS=$((INPUT_TOK + CACHE_CREATE + CACHE_READ)) | |
| REMAIN_PCT=$((100 - CTX_PCT)) | |
| REMAIN_TOKENS=$((CTX_SIZE - CURRENT_TOKENS)) | |
| [ "$REMAIN_TOKENS" -lt 0 ] 2>/dev/null && REMAIN_TOKENS=0 | |
| USED_FMT=$(format_tokens "$CURRENT_TOKENS") | |
| TOTAL_FMT=$(format_tokens "$CTX_SIZE") | |
| USED_COMMA=$(format_number "$CURRENT_TOKENS") | |
| REMAIN_COMMA=$(format_number "$REMAIN_TOKENS") | |
| # --- 8. Thinking status --- | |
| THINKING="off" | |
| THINKING_COLOR="$C_MUTED" | |
| if [ -f "$HOME/.claude/settings.json" ]; then | |
| thinking_val=$(jq -r '.alwaysThinkingEnabled // false' "$HOME/.claude/settings.json" 2>/dev/null) | |
| [ "$thinking_val" = "true" ] && THINKING="on" && THINKING_COLOR="$C_ACCENT" | |
| fi | |
| if [ -n "$MAX_THINKING_TOKENS" ] && [ "$MAX_THINKING_TOKENS" != "0" ]; then | |
| THINKING="on" | |
| THINKING_COLOR="$C_ACCENT" | |
| fi | |
| # --- 9. Usage API fetch (cached) --- | |
| HAVE_USAGE=false | |
| fetch_usage() { | |
| local token="" | |
| # Try credentials file first (Linux/devcontainer) | |
| local creds="$HOME/.claude/.credentials.json" | |
| if [ -f "$creds" ]; then | |
| token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds" 2>/dev/null) | |
| fi | |
| # Fallback: macOS Keychain | |
| if [ -z "$token" ] && command -v security >/dev/null 2>&1; then | |
| local keychain_json | |
| keychain_json=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) || keychain_json="" | |
| if [ -n "$keychain_json" ]; then | |
| token=$(printf '%s' "$keychain_json" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| fi | |
| fi | |
| [ -z "$token" ] && return 1 | |
| local response http_code body curl_status | |
| # Pass credentials via curl --config stdin to keep them in memory only | |
| # (avoids leaking token to temp files or process argument list) | |
| response=$(printf 'header = "Authorization: Bearer %s"\n' "$token" | \ | |
| curl -s -m 5 -w "\n%{http_code}" \ | |
| --config - \ | |
| -H "Accept: application/json" \ | |
| -H "Content-Type: application/json" \ | |
| -H "User-Agent: claude-code/2.1.34" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| "https://api.anthropic.com/api/oauth/usage" 2>/dev/null) | |
| curl_status=$? | |
| [ $curl_status -eq 0 ] || return 1 | |
| http_code=$(printf '%s' "$response" | tail -n1) | |
| body=$(printf '%s' "$response" | sed '$d') | |
| [ "$http_code" = "200" ] || return 1 | |
| local tmp | |
| tmp=$(mktemp "${USAGE_CACHE}.XXXXXX") || return 1 | |
| chmod 600 "$tmp" | |
| printf '%s' "$body" > "$tmp" && mv "$tmp" "$USAGE_CACHE" 2>/dev/null || rm -f "$tmp" | |
| } | |
| now=${now:-$(date +%s)} | |
| cache_age=999 | |
| if [ -f "$USAGE_CACHE" ]; then | |
| cache_mtime=$(file_mtime "$USAGE_CACHE") | |
| cache_age=$((now - cache_mtime)) | |
| fi | |
| if [ "$cache_age" -ge "$USAGE_CACHE_TTL" ]; then | |
| fetch_usage 2>/dev/null || true | |
| fi | |
| # --- 10. Parse usage cache --- | |
| FIVE_HOUR_PCT=0; FIVE_HOUR_RESET="" | |
| SEVEN_DAY_PCT=0; SEVEN_DAY_RESET="" | |
| EXTRA_ENABLED=false; EXTRA_PCT=0; EXTRA_USED="0"; EXTRA_LIMIT="0" | |
| if [ -f "$USAGE_CACHE" ]; then | |
| parsed=$(jq -r '[ | |
| ((.five_hour.utilization // 0) | round), | |
| (.five_hour.resets_at // ""), | |
| ((.seven_day.utilization // 0) | round), | |
| (.seven_day.resets_at // ""), | |
| (.extra_usage.is_enabled // false), | |
| ((.extra_usage.utilization // 0) | round), | |
| ((.extra_usage.used_credits // 0) / 100 * 100 | round / 100), | |
| ((.extra_usage.monthly_limit // 0) / 100 * 100 | round / 100) | |
| ] | @tsv' "$USAGE_CACHE" 2>/dev/null) || parsed="" | |
| if [ -n "$parsed" ]; then | |
| read -r FIVE_HOUR_PCT FIVE_HOUR_RESET SEVEN_DAY_PCT SEVEN_DAY_RESET \ | |
| EXTRA_ENABLED EXTRA_PCT EXTRA_USED EXTRA_LIMIT <<< "$parsed" | |
| FIVE_HOUR_PCT="${FIVE_HOUR_PCT:-0}" | |
| SEVEN_DAY_PCT="${SEVEN_DAY_PCT:-0}" | |
| EXTRA_PCT="${EXTRA_PCT:-0}" | |
| HAVE_USAGE=true | |
| fi | |
| fi | |
| # --- 11. Hooks aggregation --- | |
| HOOKS_LINE="" | |
| if [ -d "$PROJECT_DIR/.claude" ]; then | |
| all_hooks="" | |
| for settings_file in "$PROJECT_DIR/.claude/settings.json" "$PROJECT_DIR/.claude/settings.local.json"; do | |
| [ -f "$settings_file" ] || continue | |
| file_hooks=$(jq -r ' | |
| .hooks // {} | to_entries[] | .key as $ev | | |
| .value[].hooks[]?.command //empty | | |
| "\($ev)|\(split("/")[-1] | sub("\\.(sh|py|js|ts)$";""))" | |
| ' "$settings_file" 2>/dev/null) || file_hooks="" | |
| if [ -n "$file_hooks" ]; then | |
| all_hooks="${all_hooks}${all_hooks:+$'\n'}${file_hooks}" | |
| fi | |
| done | |
| if [ -n "$all_hooks" ]; then | |
| # Deduplicate and group by event | |
| HOOKS_LINE=$(printf '%s' "$all_hooks" | sort -u | awk -F'|' ' | |
| { events[$1] = events[$1] ? events[$1] ", " $2 : $2 } | |
| END { for (ev in events) printf "%s[%s] ", ev, events[ev] } | |
| ') | |
| HOOKS_LINE="${HOOKS_LINE% }" # trim trailing space | |
| fi | |
| fi | |
| # Hook last-run times and results from session debug log | |
| # Log format: "2026-02-22T10:09:49.260Z [DEBUG] Hook SessionStart:startup (SessionStart) success:" | |
| # Stores per event: "timestamp|success", "timestamp|error", or "timestamp|cancelled" | |
| declare -A HOOK_LAST_RUN 2>/dev/null || true # associative array; silently skip on bash <4 | |
| if [ -n "$SESSION_ID" ]; then | |
| debug_log="$HOME/.claude/debug/${SESSION_ID}.txt" | |
| if [ -f "$debug_log" ]; then | |
| while IFS='|' read -r evt ts_raw result; do | |
| [ -n "$evt" ] && HOOK_LAST_RUN["$evt"]="${ts_raw}|${result}" | |
| done < <( | |
| grep -E '\[DEBUG\] Hook .* (success|error|cancelled):' "$debug_log" 2>/dev/null \ | |
| | grep -v 'output does not' \ | |
| | sed -E 's/^([^ ]+) \[DEBUG\] Hook [^ ]+ \(([^)]+)\) (success|error|cancelled):.*/\2|\1|\3/' \ | |
| | sort -t'|' -k1,1 -k2,2 # keeps last occurrence per event after read loop | |
| ) || true | |
| fi | |
| fi | |
| format_hook_age() { | |
| local ts=$1 | |
| [ -z "$ts" ] && return | |
| local hook_epoch | |
| hook_epoch=$(date_to_epoch "$ts") || return | |
| local diff=$((now - hook_epoch)) | |
| [ "$diff" -lt 0 ] && diff=0 | |
| if [ "$diff" -lt 60 ]; then | |
| printf '%ds ago' "$diff" | |
| elif [ "$diff" -lt 3600 ]; then | |
| printf '%dm ago' "$((diff / 60))" | |
| elif [ "$diff" -lt 86400 ]; then | |
| local h=$((diff / 3600)) m=$(((diff % 3600) / 60)) | |
| if [ "$m" -gt 0 ]; then | |
| printf '%dh%dm ago' "$h" "$m" | |
| else | |
| printf '%dh ago' "$h" | |
| fi | |
| else | |
| printf '%dd ago' "$((diff / 86400))" | |
| fi | |
| } | |
| # --- 12. Git detail --- | |
| GIT_DIRTY="" | |
| GIT_AHEAD_BEHIND="" | |
| GIT_STASH_COUNT=0 | |
| if [ "$IS_GIT_REPO" = true ]; then | |
| # Dirty state: count staged, modified, untracked | |
| git_status=$(git -C "$PROJECT_DIR" status --porcelain 2>/dev/null) || git_status="" | |
| if [ -n "$git_status" ]; then | |
| staged=0; modified=0; untracked=0 | |
| while IFS= read -r status_line; do | |
| x="${status_line:0:1}" | |
| y="${status_line:1:1}" | |
| case "$x" in | |
| A|M|R) staged=$((staged + 1)) ;; | |
| esac | |
| case "$y" in | |
| M|D) modified=$((modified + 1)) ;; | |
| esac | |
| if [ "$x$y" = "??" ]; then | |
| untracked=$((untracked + 1)) | |
| fi | |
| done <<< "$git_status" | |
| parts="" | |
| [ "$staged" -gt 0 ] && parts="+${staged} staged" | |
| [ "$modified" -gt 0 ] && parts="${parts:+${parts} }~${modified} modified" | |
| [ "$untracked" -gt 0 ] && parts="${parts:+${parts} }?${untracked} untracked" | |
| GIT_DIRTY="$parts" | |
| fi | |
| # Ahead/behind upstream | |
| ab=$(git -C "$PROJECT_DIR" rev-list --left-right --count HEAD...@{u} 2>/dev/null) || ab="" | |
| if [ -n "$ab" ]; then | |
| read -r ahead behind <<< "$ab" | |
| parts="" | |
| [ "$ahead" -gt 0 ] 2>/dev/null && parts="↑${ahead} ahead" | |
| [ "$behind" -gt 0 ] 2>/dev/null && parts="${parts:+${parts} }↓${behind} behind" | |
| GIT_AHEAD_BEHIND="$parts" | |
| fi | |
| # Worktree count (includes the main worktree) | |
| GIT_WORKTREE_COUNT=$(git -C "$PROJECT_DIR" worktree list 2>/dev/null | wc -l | tr -d ' ') | |
| GIT_WORKTREE_COUNT="${GIT_WORKTREE_COUNT:-1}" | |
| # Stash count | |
| GIT_STASH_COUNT=$(git -C "$PROJECT_DIR" stash list 2>/dev/null | wc -l | tr -d ' ') | |
| GIT_STASH_COUNT="${GIT_STASH_COUNT:-0}" | |
| fi | |
| # --- 13. Environment: PR/CI and Language --- | |
| PR_STATUS="" | |
| LANG_VERSION="" | |
| # PR/CI status (requires gh CLI) | |
| if [ "$IS_GIT_REPO" = true ] && command -v gh >/dev/null 2>&1; then | |
| dir_key=$(printf '%s' "$PROJECT_DIR" | tr '/' '_') | |
| pr_cache="/tmp/claude-statusline-pr-${dir_key}.json" | |
| pr_cache_age=999 | |
| if [ -f "$pr_cache" ]; then | |
| pr_mtime=$(file_mtime "$pr_cache") | |
| pr_cache_age=$((now - pr_mtime)) | |
| fi | |
| if [ "$pr_cache_age" -ge "$PR_CACHE_TTL" ]; then | |
| pr_json=$(cd "$PROJECT_DIR" && gh pr view --json number,state,statusCheckRollup 2>/dev/null) || pr_json="" | |
| if [ -n "$pr_json" ]; then | |
| printf '%s' "$pr_json" > "$pr_cache" 2>/dev/null || true | |
| else | |
| # No PR — write empty marker so we don't re-fetch constantly | |
| printf '{}' > "$pr_cache" 2>/dev/null || true | |
| fi | |
| fi | |
| if [ -f "$pr_cache" ]; then | |
| pr_num=$(jq -r '.number // empty' "$pr_cache" 2>/dev/null) || pr_num="" | |
| if [ -n "$pr_num" ]; then | |
| ci_status=$(jq -r ' | |
| (.statusCheckRollup // []) | | |
| if length == 0 then "none" | |
| elif any(.[]; .conclusion == "FAILURE") then "failing" | |
| elif any(.[]; .conclusion == null or .conclusion == "") then "pending" | |
| else "passing" | |
| end | |
| ' "$pr_cache" 2>/dev/null) || ci_status="none" | |
| case "$ci_status" in | |
| passing) PR_STATUS="${C_TITLE}pr ${C_MUTED}(cached)${C_TITLE}:${C_RESET} #${pr_num} ${C_GOOD}✓ passing${C_RESET}" ;; | |
| failing) PR_STATUS="${C_TITLE}pr ${C_MUTED}(cached)${C_TITLE}:${C_RESET} #${pr_num} ${C_BAD}✗ failing${C_RESET}" ;; | |
| pending) PR_STATUS="${C_TITLE}pr ${C_MUTED}(cached)${C_TITLE}:${C_RESET} #${pr_num} ${C_WARN}● pending${C_RESET}" ;; | |
| *) PR_STATUS="${C_TITLE}pr ${C_MUTED}(cached)${C_TITLE}:${C_RESET} #${pr_num}" ;; | |
| esac | |
| fi | |
| fi | |
| fi | |
| # Language/runtime detection (first match wins) | |
| if [ -n "$PROJECT_DIR" ]; then | |
| if [ -f "${PROJECT_DIR}/go.mod" ] && command -v go >/dev/null 2>&1; then | |
| ver=$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//') || ver="" | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}go ${ver}${C_RESET}" | |
| elif [ -f "${PROJECT_DIR}/package.json" ] && command -v node >/dev/null 2>&1; then | |
| ver=$(node --version 2>/dev/null | sed 's/^v//') || ver="" | |
| ver=$(printf '%s' "$ver" | awk -F. '{print $1"."$2}') | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}node ${ver}${C_RESET}" | |
| elif [ -f "${PROJECT_DIR}/Cargo.toml" ] && command -v rustc >/dev/null 2>&1; then | |
| ver=$(rustc --version 2>/dev/null | awk '{print $2}') || ver="" | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}rust ${ver}${C_RESET}" | |
| elif [ -f "${PROJECT_DIR}/pyproject.toml" ] || [ -f "${PROJECT_DIR}/requirements.txt" ] || [ -f "${PROJECT_DIR}/.python-version" ]; then | |
| if command -v python3 >/dev/null 2>&1; then | |
| ver=$(python3 --version 2>/dev/null | awk '{print $2}') || ver="" | |
| ver=$(printf '%s' "$ver" | awk -F. '{print $1"."$2}') | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}python ${ver}${C_RESET}" | |
| fi | |
| elif [ -f "${PROJECT_DIR}/Gemfile" ] && command -v ruby >/dev/null 2>&1; then | |
| ver=$(ruby --version 2>/dev/null | awk '{print $2}') || ver="" | |
| ver=$(printf '%s' "$ver" | awk -F. '{print $1"."$2}') | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}ruby ${ver}${C_RESET}" | |
| elif [ -f "${PROJECT_DIR}/mix.exs" ] && command -v elixir >/dev/null 2>&1; then | |
| ver=$(elixir --version 2>/dev/null | grep 'Elixir' | awk '{print $2}') || ver="" | |
| ver=$(printf '%s' "$ver" | awk -F. '{print $1"."$2}') | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}elixir ${ver}${C_RESET}" | |
| elif [ -f "${PROJECT_DIR}/pom.xml" ] || [ -f "${PROJECT_DIR}/build.gradle" ]; then | |
| if command -v java >/dev/null 2>&1; then | |
| ver=$(java --version 2>&1 | head -1 | awk '{print $2}') || ver="" | |
| ver=$(printf '%s' "$ver" | awk -F. '{print $1}') | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}java ${ver}${C_RESET}" | |
| fi | |
| elif [ -f "${PROJECT_DIR}/composer.json" ] && command -v php >/dev/null 2>&1; then | |
| ver=$(php --version 2>/dev/null | head -1 | awk '{print $2}') || ver="" | |
| ver=$(printf '%s' "$ver" | awk -F. '{print $1"."$2}') | |
| [ -n "$ver" ] && LANG_VERSION="${C_VALUE}php ${ver}${C_RESET}" | |
| fi | |
| fi | |
| # Package/build tool detection | |
| PKG_TOOL="" | |
| if [ -n "$PROJECT_DIR" ]; then | |
| if [ -f "${PROJECT_DIR}/uv.lock" ] || grep -q '\[tool\.uv\]' "${PROJECT_DIR}/pyproject.toml" 2>/dev/null; then | |
| PKG_TOOL="uv" | |
| elif [ -f "${PROJECT_DIR}/Pipfile" ] || [ -f "${PROJECT_DIR}/Pipfile.lock" ]; then | |
| PKG_TOOL="pipenv" | |
| elif [ -f "${PROJECT_DIR}/poetry.lock" ] || grep -q '\[tool\.poetry\]' "${PROJECT_DIR}/pyproject.toml" 2>/dev/null; then | |
| PKG_TOOL="poetry" | |
| elif [ -f "${PROJECT_DIR}/conda.yaml" ] || [ -f "${PROJECT_DIR}/environment.yml" ]; then | |
| PKG_TOOL="conda" | |
| elif [ -f "${PROJECT_DIR}/requirements.txt" ] || [ -f "${PROJECT_DIR}/setup.py" ] || [ -f "${PROJECT_DIR}/setup.cfg" ]; then | |
| PKG_TOOL="pip" | |
| elif [ -f "${PROJECT_DIR}/pnpm-lock.yaml" ]; then | |
| PKG_TOOL="pnpm" | |
| elif [ -f "${PROJECT_DIR}/yarn.lock" ]; then | |
| PKG_TOOL="yarn" | |
| elif [ -f "${PROJECT_DIR}/bun.lockb" ] || [ -f "${PROJECT_DIR}/bun.lock" ]; then | |
| PKG_TOOL="bun" | |
| elif [ -f "${PROJECT_DIR}/package-lock.json" ]; then | |
| PKG_TOOL="npm" | |
| elif [ -f "${PROJECT_DIR}/Cargo.lock" ]; then | |
| PKG_TOOL="cargo" | |
| elif [ -f "${PROJECT_DIR}/go.sum" ]; then | |
| PKG_TOOL="go mod" | |
| elif [ -f "${PROJECT_DIR}/Gemfile.lock" ]; then | |
| PKG_TOOL="bundler" | |
| elif [ -f "${PROJECT_DIR}/mix.lock" ]; then | |
| PKG_TOOL="mix" | |
| elif [ -f "${PROJECT_DIR}/composer.lock" ]; then | |
| PKG_TOOL="composer" | |
| fi | |
| fi | |
| # --- 14. Session metrics --- | |
| SESSION_COST_FMT="" | |
| SESSION_LINES_FMT="" | |
| API_LATENCY_FMT="" | |
| CACHE_HIT_FMT="" | |
| BURN_RATE_FMT="" | |
| CONCURRENT_FMT="" | |
| HAVE_SESSION_DATA=false | |
| # Check if we have meaningful session data | |
| if [ "$TOTAL_COST" != "0" ] || [ "$LINES_ADDED" != "0" ] || [ "$LINES_REMOVED" != "0" ] || [ "$TOTAL_DURATION" != "0" ]; then | |
| HAVE_SESSION_DATA=true | |
| # Cost (round to 2 decimal places) | |
| cost_fmt=$(awk -v c="$TOTAL_COST" 'BEGIN { printf "%.2f", c }') | |
| SESSION_COST_FMT="${C_TITLE}session:${C_RESET} ${C_VALUE}\$${cost_fmt}${C_RESET}" | |
| # Lines changed | |
| if [ "$LINES_ADDED" != "0" ] || [ "$LINES_REMOVED" != "0" ]; then | |
| SESSION_LINES_FMT="${C_GOOD}+${LINES_ADDED}${C_RESET} ${C_BAD}-${LINES_REMOVED}${C_RESET} lines" | |
| fi | |
| # API latency ratio | |
| if [ "$TOTAL_DURATION" -gt 0 ] 2>/dev/null; then | |
| api_pct=$(awk -v api="$TOTAL_API_DURATION" -v total="$TOTAL_DURATION" \ | |
| 'BEGIN { printf "%.0f", (api / total) * 100 }') | |
| API_LATENCY_FMT="${C_TITLE}API:${C_RESET} ${C_VALUE}${api_pct}%${C_RESET}" | |
| fi | |
| # Cache hit rate | |
| total_cache_tokens=$((INPUT_TOK + CACHE_CREATE + CACHE_READ)) | |
| if [ "$total_cache_tokens" -gt 0 ] 2>/dev/null; then | |
| cache_pct=$(awk -v cr="$CACHE_READ" -v total="$total_cache_tokens" \ | |
| 'BEGIN { printf "%.0f", (cr / total) * 100 }') | |
| CACHE_HIT_FMT="${C_TITLE}cache:${C_RESET} ${C_VALUE}${cache_pct}%${C_RESET}" | |
| fi | |
| # Burn rate + depletion ETA | |
| if [ "$TOTAL_DURATION" -gt 0 ] 2>/dev/null; then | |
| total_tok=$((TOTAL_INPUT_TOK + TOTAL_OUTPUT_TOK)) | |
| if [ "$total_tok" -gt 0 ] 2>/dev/null; then | |
| burn_rate=$(awk -v tok="$total_tok" -v dur="$TOTAL_DURATION" \ | |
| 'BEGIN { printf "%.0f", tok / (dur / 60000) }') | |
| # Format rate display | |
| burn_display=$(awk -v r="$burn_rate" \ | |
| 'BEGIN { if (r >= 1000) printf "~%.1fk tok/min", r/1000; else printf "~%.0f tok/min", r }') | |
| # ETA | |
| eta_display="" | |
| if [ "$burn_rate" -gt 0 ] 2>/dev/null; then | |
| eta_min=$(awk -v rem="$REMAIN_TOKENS" -v rate="$burn_rate" \ | |
| 'BEGIN { printf "%.0f", rem / rate }') | |
| if [ "$eta_min" -gt 60 ] 2>/dev/null; then | |
| eta_hrs=$((eta_min / 60)) | |
| eta_mins=$((eta_min % 60)) | |
| eta_display="~${eta_hrs}h${eta_mins}m left" | |
| elif [ "$eta_min" -gt 0 ] 2>/dev/null; then | |
| eta_display="~${eta_min}m left" | |
| fi | |
| fi | |
| BURN_RATE_FMT="${C_VALUE}${burn_display}${C_RESET}" | |
| [ -n "$eta_display" ] && BURN_RATE_FMT="${BURN_RATE_FMT} ${C_MUTED}→${C_RESET} ${C_ACCENT}${eta_display}${C_RESET}" | |
| fi | |
| fi | |
| fi | |
| # Concurrent sessions | |
| if [ -n "$SESSION_ID" ]; then | |
| touch "/tmp/claude-session-${SESSION_ID}.marker" 2>/dev/null || true | |
| concurrent_count=$(find /tmp -maxdepth 1 -name "claude-session-*.marker" -mmin -2 2>/dev/null | wc -l | tr -d ' ') | |
| [ "$concurrent_count" -gt 1 ] 2>/dev/null && CONCURRENT_FMT="${C_WARN}x${concurrent_count}${C_RESET}" | |
| fi | |
| # --- 15. Rules files discovery --- | |
| RULES_LINE="" | |
| rules_files=() | |
| for candidate in \ | |
| "${PROJECT_DIR}/CLAUDE.md" \ | |
| "${PROJECT_DIR}/CLAUDE.local.md" \ | |
| "${PROJECT_DIR}/.claude/CLAUDE.md" \ | |
| "$HOME/.claude/CLAUDE.md"; do | |
| [ -f "$candidate" ] && rules_files+=("$candidate") | |
| done | |
| if [ -d "${PROJECT_DIR}/.claude/rules" ]; then | |
| for f in "${PROJECT_DIR}/.claude/rules"/*.md; do | |
| [ -f "$f" ] && rules_files+=("$f") | |
| done | |
| fi | |
| if [ ${#rules_files[@]} -gt 0 ]; then | |
| display_names="" | |
| for rf in "${rules_files[@]}"; do | |
| case "$rf" in | |
| "$HOME/.claude/CLAUDE.md") | |
| name="~/.claude/CLAUDE.md" ;; | |
| "${PROJECT_DIR}/"*) | |
| name="${rf#"${PROJECT_DIR}/"}" ;; | |
| *) | |
| name="$rf" ;; | |
| esac | |
| display_names="${display_names:+${display_names} }${name}" | |
| done | |
| RULES_LINE="$display_names" | |
| fi | |
| # --- 16. Render functions --- | |
| # Each function prints its line(s) preceded by \n (except render_model which is first). | |
| # Reorder output by changing the call order in section 17. | |
| sep=" ${C_MUTED}|${C_RESET} " | |
| render_model() { | |
| local used_color | |
| used_color=$(color_for_pct "$CTX_PCT") | |
| local line="${C_MODEL}${DISPLAY_NAME}${C_RESET}" | |
| if [ -n "$AGENT_NAME" ]; then | |
| line="${line} ${C_MUTED}(${C_ACCENT}${AGENT_NAME}${C_MUTED})${C_RESET}" | |
| fi | |
| line="${line}${sep}${C_ACCENT}${USED_FMT}/${TOTAL_FMT}${C_RESET}" | |
| line="${line}${sep}" | |
| if [ "$CTX_PCT" -ge 80 ] 2>/dev/null; then | |
| line="${line}${C_BAD}${CTX_PCT}% used ⚠ COMPACTION${C_RESET} ${C_ACCENT}${USED_COMMA}${C_RESET}" | |
| else | |
| line="${line}${used_color}${CTX_PCT}% used${C_RESET} ${C_ACCENT}${USED_COMMA}${C_RESET}" | |
| fi | |
| line="${line}${sep}${C_VALUE}${REMAIN_PCT}% remain${C_RESET} ${C_MODEL}${REMAIN_COMMA}${C_RESET}" | |
| line="${line}${sep}${C_TITLE}thinking:${C_RESET} ${THINKING_COLOR}${THINKING}${C_RESET}" | |
| if [ -n "$VIM_MODE" ]; then | |
| line="${line}${sep}${C_TITLE}vim:${C_RESET} ${C_ACCENT}${VIM_MODE}${C_RESET}" | |
| fi | |
| printf '%s' "$line" | |
| } | |
| render_usage() { | |
| [ "$HAVE_USAGE" = true ] || return 0 | |
| local bar_width=10 | |
| local five_bar seven_bar | |
| five_bar=$(build_bar "$FIVE_HOUR_PCT" "$bar_width") | |
| seven_bar=$(build_bar "$SEVEN_DAY_PCT" "$bar_width") | |
| local line="${C_TITLE}current ${C_MUTED}(cached)${C_TITLE}:${C_RESET} ${five_bar} ${C_VALUE}${FIVE_HOUR_PCT}%${C_RESET}" | |
| line="${line}${sep}${C_TITLE}weekly:${C_RESET} ${seven_bar} ${C_VALUE}${SEVEN_DAY_PCT}%${C_RESET}" | |
| if [ "$EXTRA_ENABLED" = "true" ]; then | |
| local extra_bar | |
| extra_bar=$(build_bar "$EXTRA_PCT" "$bar_width") | |
| line="${line}${sep}${C_TITLE}extra:${C_RESET} ${extra_bar} ${C_VALUE}\$${EXTRA_USED}/\$${EXTRA_LIMIT}${C_RESET}" | |
| fi | |
| printf '\n%s' "$line" | |
| # Reset times | |
| local five_reset seven_reset | |
| five_reset=$(format_reset_time_relative "$FIVE_HOUR_RESET") | |
| seven_reset=$(format_reset_time_absolute "$SEVEN_DAY_RESET") | |
| if [ -n "$five_reset" ] || [ -n "$seven_reset" ]; then | |
| local rline="" | |
| if [ -n "$five_reset" ]; then | |
| rline="${C_TITLE}resets:${C_RESET} ${C_MUTED}${five_reset}${C_RESET}" | |
| fi | |
| if [ -n "$seven_reset" ]; then | |
| [ -n "$rline" ] && rline="${rline}${sep}" | |
| rline="${rline}${C_TITLE}resets:${C_RESET} ${C_MUTED}${seven_reset}${C_RESET}" | |
| fi | |
| if [ "$EXTRA_ENABLED" = "true" ]; then | |
| local next_month_reset | |
| next_month_reset=$(date_calc "$(date +%Y-%m-01) +1 month" "%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]') | |
| if [ -n "$next_month_reset" ]; then | |
| rline="${rline}${sep}${C_TITLE}resets:${C_RESET} ${C_MUTED}${next_month_reset}${C_RESET}" | |
| fi | |
| fi | |
| printf '\n%s' "$rline" | |
| fi | |
| } | |
| render_session() { | |
| local line | |
| if [ "$HAVE_SESSION_DATA" = true ]; then | |
| line="$SESSION_COST_FMT" | |
| [ -n "$API_LATENCY_FMT" ] && line="${line}${sep}${API_LATENCY_FMT}" | |
| [ -n "$CACHE_HIT_FMT" ] && line="${line}${sep}${CACHE_HIT_FMT}" | |
| [ -n "$SESSION_LINES_FMT" ] && line="${line}${sep}${SESSION_LINES_FMT}" | |
| [ -n "$BURN_RATE_FMT" ] && line="${line}${sep}${BURN_RATE_FMT}" | |
| [ -n "$CONCURRENT_FMT" ] && line="${line}${sep}${CONCURRENT_FMT}" | |
| else | |
| line="${C_TITLE}session:${C_RESET} ${C_EMPTY}(waiting for data)${C_RESET}" | |
| [ -n "$CONCURRENT_FMT" ] && line="${line}${sep}${CONCURRENT_FMT}" | |
| fi | |
| printf '\n%s' "$line" | |
| } | |
| render_git() { | |
| if [ "$IS_GIT_REPO" != true ]; then | |
| printf '\n%s' "${C_TITLE}git:${C_RESET} ${C_EMPTY}not a repo${C_RESET}" | |
| return 0 | |
| fi | |
| local line="${C_TITLE}git:${C_RESET} " | |
| # Branch and worktree | |
| if [ -n "$GIT_BRANCH" ]; then | |
| line="${line}${C_VALUE}${GIT_BRANCH}${C_RESET}" | |
| fi | |
| local wt_count=" ${C_MUTED}${GIT_WORKTREE_COUNT:-1}wt${C_RESET}" | |
| if [ -n "$GIT_WORKTREE" ]; then | |
| line="${line} ${C_MUTED}[${C_ACCENT}${GIT_WORKTREE}${C_MUTED}]${C_RESET}${wt_count}" | |
| else | |
| line="${line} ${C_MUTED}[main]${wt_count}${C_RESET}" | |
| fi | |
| # Working copy folder and cwd | |
| line="${line}${sep}${C_TITLE}dir:${C_RESET} ${C_VALUE}${GIT_TOPLEVEL_NAME:-?}${C_RESET}" | |
| if [ -n "$GIT_CWD_REL" ] && [ "$GIT_CWD_REL" != "." ]; then | |
| line="${line}${C_MUTED}/${C_ACCENT}${GIT_CWD_REL}${C_RESET}" | |
| fi | |
| # Origin and upstream on the same line | |
| if [ -n "$GIT_ORIGIN_REPO" ]; then | |
| line="${line}${sep}${C_TITLE}origin:${C_RESET} ${C_VALUE}${GIT_ORIGIN_REPO}${C_RESET}" | |
| fi | |
| if [ -n "$GIT_UPSTREAM_REPO" ]; then | |
| line="${line}${sep}${C_TITLE}upstream:${C_RESET} ${C_ACCENT}${GIT_UPSTREAM_REPO}${C_RESET}" | |
| fi | |
| printf '\n%s' "$line" | |
| # Second line: file status, ahead/behind, stash | |
| local line3="" | |
| # Dirty state or clean | |
| if [ -n "$GIT_DIRTY" ]; then | |
| line3="${C_GOOD}${GIT_DIRTY}${C_RESET}" | |
| else | |
| line3="${C_GOOD}clean${C_RESET}" | |
| fi | |
| # Ahead/behind | |
| if [ -n "$GIT_AHEAD_BEHIND" ]; then | |
| line3="${line3}${sep}${C_VALUE}${GIT_AHEAD_BEHIND}${C_RESET}" | |
| fi | |
| # Stash | |
| if [ "$GIT_STASH_COUNT" -gt 0 ] 2>/dev/null; then | |
| line3="${line3}${sep}${C_TITLE}stash:${C_RESET} ${C_WARN}${GIT_STASH_COUNT}${C_RESET}" | |
| fi | |
| printf '\n%s%s' "${C_TITLE}git status:${C_RESET} " "$line3" | |
| } | |
| render_hooks() { | |
| if [ -n "$HOOKS_LINE" ]; then | |
| # Inject inline last-run info into each "Event[commands]" token | |
| local decorated="$HOOKS_LINE" | |
| if [ "${#HOOK_LAST_RUN[@]}" -gt 0 ] 2>/dev/null; then | |
| for evt in "${!HOOK_LAST_RUN[@]}"; do | |
| local val="${HOOK_LAST_RUN[$evt]}" | |
| local ts="${val%%|*}" | |
| local result="${val##*|}" | |
| local age | |
| age=$(format_hook_age "$ts") || continue | |
| [ -n "$age" ] || continue | |
| local icon color name_color | |
| if [ "$result" = "success" ]; then | |
| icon="✓"; color="$C_GOOD"; name_color="$C_GOOD" | |
| elif [ "$result" = "cancelled" ]; then | |
| icon="⊘"; color="$C_WARN"; name_color="$C_WARN" | |
| else | |
| icon="✗"; color="$C_BAD"; name_color="$C_BAD" | |
| fi | |
| local suffix=" ${color}${icon}${C_RESET} ${C_MUTED}${age}${C_RESET}" | |
| decorated="${decorated//${evt}\[/${name_color}${evt}${C_RESET}${suffix} \[}" | |
| done | |
| fi | |
| printf '\n%s%s' "${C_TITLE}hooks:${C_RESET} " "$decorated" | |
| else | |
| printf '\n%s' "${C_TITLE}hooks:${C_RESET} ${C_EMPTY}none${C_RESET}" | |
| fi | |
| } | |
| render_rules() { | |
| if [ -n "$RULES_LINE" ]; then | |
| printf '\n%s%s%s' "${C_TITLE}rules:${C_RESET} " "$RULES_LINE" "" | |
| else | |
| printf '\n%s' "${C_TITLE}rules:${C_RESET} ${C_EMPTY}none${C_RESET}" | |
| fi | |
| } | |
| render_environment() { | |
| if [ -n "$PR_STATUS" ] || [ -n "$LANG_VERSION" ] || [ -n "$PKG_TOOL" ]; then | |
| local line="" | |
| [ -n "$PR_STATUS" ] && line="${PR_STATUS}" | |
| if [ -n "$LANG_VERSION" ]; then | |
| [ -n "$line" ] && line="${line}${sep}" | |
| line="${line}${LANG_VERSION}" | |
| fi | |
| if [ -n "$PKG_TOOL" ]; then | |
| [ -n "$line" ] && line="${line}${sep}" | |
| line="${line}${C_MUTED}via${C_RESET} ${C_VALUE}${PKG_TOOL}${C_RESET}" | |
| fi | |
| printf '\n%s%s' "${C_TITLE}env:${C_RESET} " "$line" | |
| else | |
| printf '\n%s' "${C_TITLE}env:${C_RESET} ${C_EMPTY}none${C_RESET}" | |
| fi | |
| } | |
| # --- 17. Output (reorder lines by changing call order below) --- | |
| render_model | |
| render_usage | |
| render_session | |
| render_git | |
| render_hooks | |
| render_rules | |
| render_environment | |
| # Clean up stale session markers (older than 5 minutes) | |
| find /tmp -maxdepth 1 -name "claude-session-*.marker" -mmin +5 -delete 2>/dev/null || true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment