summaryrefslogtreecommitdiff
path: root/default/.claude/statuslines/statusline.sh
diff options
context:
space:
mode:
Diffstat (limited to 'default/.claude/statuslines/statusline.sh')
-rwxr-xr-xdefault/.claude/statuslines/statusline.sh801
1 files changed, 569 insertions, 232 deletions
diff --git a/default/.claude/statuslines/statusline.sh b/default/.claude/statuslines/statusline.sh
index 54579fa..0d524d2 100755
--- a/default/.claude/statuslines/statusline.sh
+++ b/default/.claude/statuslines/statusline.sh
@@ -1,274 +1,611 @@
-#!/bin/bash
+#!/usr/bin/env bash
+set -euo pipefail 2>/dev/null || set -eu
+
+# ============================================================
+# STATUSLINE v2.0.0 - Claude Code Status Line
+# ============================================================
+
+readonly STATUSLINE_VERSION="2.0.0"
+
+# ============================================================
+# CONFIGURATION
+# ============================================================
+readonly BAR_WIDTH=12
+readonly BAR_FILLED="โ–ˆ"
+readonly BAR_EMPTY="โ–‘"
+
+# Colors (256-color palette)
+readonly RED='\033[38;5;203m'
+readonly GREEN='\033[38;5;150m'
+readonly BLUE='\033[38;5;117m'
+readonly MAGENTA='\033[38;5;147m'
+readonly CYAN='\033[38;5;81m'
+readonly ORANGE='\033[38;5;215m'
+readonly YELLOW='\033[38;5;222m'
+readonly GRAY='\033[38;5;245m'
+readonly LIGHT_GRAY='\033[38;5;249m'
+readonly NC='\033[0m'
+
+# Derived constants
+readonly SEPARATOR="${GRAY}โ”‚${NC}"
+readonly NULL_VALUE="null"
+
+# Icons
+readonly MODEL_ICON="๐Ÿค–"
+readonly CONTEXT_ICON="๐Ÿง "
+readonly DIR_ICON="๐Ÿ“"
+readonly GIT_ICON="๐ŸŒฟ"
+readonly COST_ICON="๐Ÿ’ฐ"
+readonly TOKEN_ICON="๐Ÿ“Š"
+readonly TIME_ICON="โฑ๏ธ"
+readonly VERSION_ICON="๐Ÿ“Ÿ"
+
+# Git state constants
+readonly STATE_NOT_REPO="not_repo"
+readonly STATE_CLEAN="clean"
+readonly STATE_DIRTY="dirty"
+
+# Context usage messages (tiered by usage percentage)
+readonly CONTEXT_MSG_VERY_LOW=(
+ "just getting started"
+ "barely touched it"
+ "fresh as a daisy"
+ "room for an elephant"
+ "zero stress mode"
+ "could do this all day"
+ "warming up the engines"
+ "practically empty"
+ "smooth sailing ahead"
+ "all systems nominal"
+)
+
+readonly CONTEXT_MSG_LOW=(
+ "light snacking"
+ "taking it easy"
+ "smooth operator"
+ "just vibing"
+ "cruising altitude"
+ "nice and steady"
+ "zen mode activated"
+ "coasting along"
+ "comfortable cruise"
+ "looking good"
+)
+
+readonly CONTEXT_MSG_MEDIUM=(
+ "halfway there"
+ "finding the groove"
+ "building momentum"
+ "picking up speed"
+ "getting interesting"
+ "entering the zone"
+ "getting warmer"
+ "balanced perfectly"
+ "sweet spot territory"
+ "gears are meshing"
+)
+
+readonly CONTEXT_MSG_HIGH=(
+ "getting spicy"
+ "filling up fast"
+ "things heating up"
+ "turning up the heat"
+ "entering danger zone"
+ "feeling the pressure"
+ "approaching red zone"
+ "intensity rising"
+ "full throttle mode"
+ "hold on tight"
+)
+
+readonly CONTEXT_MSG_CRITICAL=(
+ "living dangerously"
+ "pushing the limits"
+ "houston we have a problem"
+ "danger zone activated"
+ "running on fumes"
+ "this is fine ๐Ÿ”ฅ"
+ "critical mass approaching"
+ "maximum overdrive"
+ "context go brrrr"
+ "about to explode"
+)
+
+# ============================================================
+# LOGGING
+# ============================================================
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+readonly LOG_FILE="${SCRIPT_DIR}/statusline.log"
-STATUSLINE_VERSION="1.4.0"
+log_debug() {
+ local timestamp
+ timestamp=$(date '+%Y-%m-%d %H:%M:%S')
+ echo "[$timestamp] $*" >> "$LOG_FILE" 2>/dev/null || true
+}
-input=$(cat)
+# ============================================================
+# UTILITY FUNCTIONS
+# ============================================================
-# ---- check jq availability ----
-HAS_JQ=0
-if command -v jq >/dev/null 2>&1; then
- HAS_JQ=1
-fi
+# Check jq availability
+check_jq() {
+ command -v jq >/dev/null 2>&1
+}
-# Get the directory where this statusline script is located
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-LOG_FILE="${SCRIPT_DIR}/statusline.log"
-TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
-
-# ---- logging ----
-{
- echo "[$TIMESTAMP] Status line triggered (cc-statusline v${STATUSLINE_VERSION})"
- echo "[$TIMESTAMP] Input:"
- if [ "$HAS_JQ" -eq 1 ]; then
- echo "$input" | jq . 2>/dev/null || echo "$input"
- echo "[$TIMESTAMP] Using jq for JSON parsing"
+# String utilities
+get_dirname() { echo "${1##*/}"; }
+sep() { echo -n " ${SEPARATOR} "; }
+
+# Format numbers with K/M suffixes
+format_number() {
+ local num="${1:-0}"
+ [[ ! "$num" =~ ^[0-9]+$ ]] && { echo "$num"; return; }
+
+ if [[ "$num" -lt 1000 ]]; then
+ echo "$num"
+ elif [[ "$num" -lt 1000000 ]]; then
+ local k=$((num / 1000))
+ local remainder=$((num % 1000))
+ if [[ "$k" -lt 10 ]]; then
+ local decimal=$((remainder / 100))
+ echo "${k}.${decimal}K"
+ else
+ echo "${k}K"
+ fi
else
- echo "$input"
- echo "[$TIMESTAMP] WARNING: jq not found, using bash fallback for JSON parsing"
+ local m=$((num / 1000000))
+ local remainder=$((num % 1000000))
+ if [[ "$m" -lt 10 ]]; then
+ local decimal=$((remainder / 100000))
+ echo "${m}.${decimal}M"
+ else
+ echo "${m}M"
+ fi
fi
- echo "---"
-} >>"$LOG_FILE" 2>/dev/null
-
-# ---- color helpers (force colors for Claude Code) ----
-use_color=1
-[ -n "$NO_COLOR" ] && use_color=0
-
-C() { if [ "$use_color" -eq 1 ]; then printf '\033[%sm' "$1"; fi; }
-RST() { if [ "$use_color" -eq 1 ]; then printf '\033[0m'; fi; }
-
-# ---- modern sleek colors ----
-dir_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;117m'; fi; } # sky blue
-model_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;147m'; fi; } # light purple
-version_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;180m'; fi; } # soft yellow
-cc_version_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;249m'; fi; } # light gray
-style_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;245m'; fi; } # gray
-rst() { if [ "$use_color" -eq 1 ]; then printf '\033[0m'; fi; }
-
-# ---- time helpers ----
-progress_bar() {
- pct="${1:-0}"
- width="${2:-10}"
- [[ "$pct" =~ ^[0-9]+$ ]] || pct=0
- ((pct < 0)) && pct=0
- ((pct > 100)) && pct=100
- filled=$((pct * width / 100))
- empty=$((width - filled))
- printf '%*s' "$filled" '' | tr ' ' '='
- printf '%*s' "$empty" '' | tr ' ' '-'
}
-# git utilities
-num_or_zero() {
- v="$1"
- [[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0
+# Format duration (ms to human readable)
+format_duration() {
+ local ms="${1:-0}"
+ [[ ! "$ms" =~ ^[0-9]+$ ]] && { echo ""; return; }
+
+ local seconds=$((ms / 1000))
+ local minutes=$((seconds / 60))
+ local hours=$((minutes / 60))
+
+ if [[ "$hours" -gt 0 ]]; then
+ local remaining_mins=$((minutes % 60))
+ echo "${hours}h${remaining_mins}m"
+ elif [[ "$minutes" -gt 0 ]]; then
+ local remaining_secs=$((seconds % 60))
+ echo "${minutes}m${remaining_secs}s"
+ else
+ echo "${seconds}s"
+ fi
+}
+
+# Format cost
+format_cost() {
+ local cost="${1:-0}"
+ if [[ "$cost" =~ ^[0-9.]+$ ]]; then
+ printf "%.2f" "$cost"
+ else
+ echo "0.00"
+ fi
+}
+
+# Get random context message based on usage percentage
+get_context_message() {
+ local percent="${1:-0}"
+ local messages=()
+
+ if [[ "$percent" -le 20 ]]; then
+ messages=("${CONTEXT_MSG_VERY_LOW[@]}")
+ elif [[ "$percent" -le 40 ]]; then
+ messages=("${CONTEXT_MSG_LOW[@]}")
+ elif [[ "$percent" -le 60 ]]; then
+ messages=("${CONTEXT_MSG_MEDIUM[@]}")
+ elif [[ "$percent" -le 80 ]]; then
+ messages=("${CONTEXT_MSG_HIGH[@]}")
+ else
+ messages=("${CONTEXT_MSG_CRITICAL[@]}")
+ fi
+
+ local count=${#messages[@]}
+ local index=$((RANDOM % count))
+ echo "${messages[$index]}"
}
-# ---- JSON extraction utilities ----
+# ============================================================
+# JSON PARSING
+# ============================================================
+
+parse_with_jq() {
+ local input="$1"
+
+ echo "$input" | jq -r '
+ .model.display_name // "Claude",
+ .workspace.current_dir // .cwd // "unknown",
+ (.context_window.context_window_size // 200000),
+ (
+ (.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)
+ ),
+ (.context_window.total_input_tokens // 0),
+ (.context_window.total_output_tokens // 0),
+ (.cost.total_cost_usd // 0),
+ (.cost.total_duration_ms // 0),
+ .version // "",
+ .session_id // ""
+ ' 2>/dev/null
+}
+
+# Bash fallback for JSON parsing
extract_json_string() {
local json="$1"
local key="$2"
local default="${3:-}"
- local field="${key##*.}"
- field="${field%% *}"
- local value=$(echo "$json" | grep -o "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/')
- if [ -n "$value" ]; then
- value=$(echo "$value" | sed 's/\\\\/\//g')
- fi
- if [ -z "$value" ] || [ "$value" = "null" ]; then
- value=$(echo "$json" | grep -o "\"${field}\"[[:space:]]*:[[:space:]]*[0-9.]\+" | head -1 | sed 's/.*:[[:space:]]*\([0-9.]\+\).*/\1/')
+
+ local value
+ value=$(echo "$json" | grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/')
+
+ if [[ -z "$value" || "$value" == "null" ]]; then
+ value=$(echo "$json" | grep -o "\"${key}\"[[:space:]]*:[[:space:]]*[0-9.]\+" | head -1 | sed 's/.*:[[:space:]]*\([0-9.]\+\).*/\1/')
fi
- if [ -n "$value" ] && [ "$value" != "null" ]; then
+
+ if [[ -n "$value" && "$value" != "null" ]]; then
echo "$value"
else
echo "$default"
fi
}
-# ---- basics ----
-if [ "$HAS_JQ" -eq 1 ]; then
- current_dir=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "unknown"' 2>/dev/null | sed "s|^$HOME|~|g")
- model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null)
- model_version=$(echo "$input" | jq -r '.model.version // ""' 2>/dev/null)
- session_id=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
- cc_version=$(echo "$input" | jq -r '.version // ""' 2>/dev/null)
- output_style=$(echo "$input" | jq -r '.output_style.name // ""' 2>/dev/null)
-else
- current_dir=$(echo "$input" | grep -o '"workspace"[[:space:]]*:[[:space:]]*{[^}]*"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | sed 's/\\\\/\//g')
- if [ -z "$current_dir" ] || [ "$current_dir" = "null" ]; then
- current_dir=$(echo "$input" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | sed 's/\\\\/\//g')
- fi
- [ -z "$current_dir" ] && current_dir="unknown"
- current_dir=$(echo "$current_dir" | sed "s|^$HOME|~|g")
- model_name=$(echo "$input" | grep -o '"model"[[:space:]]*:[[:space:]]*{[^}]*"display_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"display_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
- [ -z "$model_name" ] && model_name="Claude"
- model_version=""
+parse_without_jq() {
+ local input="$1"
+
+ local model_name current_dir context_size current_usage
+ local total_input total_output cost_usd duration_ms version session_id
+
+ model_name=$(extract_json_string "$input" "display_name" "Claude")
+ current_dir=$(extract_json_string "$input" "current_dir" "")
+ [[ -z "$current_dir" ]] && current_dir=$(extract_json_string "$input" "cwd" "unknown")
+
+ context_size=$(extract_json_string "$input" "context_window_size" "200000")
+ current_usage=$(extract_json_string "$input" "input_tokens" "0")
+ total_input=$(extract_json_string "$input" "total_input_tokens" "0")
+ total_output=$(extract_json_string "$input" "total_output_tokens" "0")
+ cost_usd=$(extract_json_string "$input" "total_cost_usd" "0")
+ duration_ms=$(extract_json_string "$input" "total_duration_ms" "0")
+ version=$(extract_json_string "$input" "version" "")
session_id=$(extract_json_string "$input" "session_id" "")
- cc_version=$(echo "$input" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
- output_style=$(echo "$input" | grep -o '"output_style"[[:space:]]*:[[:space:]]*{[^}]*"name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
-fi
-
-# ---- git colors ----
-git_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;150m'; fi; } # soft green
-
-# ---- git ----
-git_branch=""
-if git rev-parse --git-dir >/dev/null 2>&1; then
- git_branch=$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)
-fi
-
-# ---- context window calculation (native) ----
-context_pct=""
-context_remaining_pct=""
-context_color() { if [ "$use_color" -eq 1 ]; then printf '\033[1;37m'; fi; } # default white
-
-if [ "$HAS_JQ" -eq 1 ]; then
- CONTEXT_SIZE=$(echo "$input" | jq -r '.context_window.context_window_size // 200000' 2>/dev/null)
- USAGE=$(echo "$input" | jq '.context_window.current_usage' 2>/dev/null)
-
- if [ "$USAGE" != "null" ] && [ -n "$USAGE" ]; then
- CURRENT_TOKENS=$(echo "$USAGE" | jq '(.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0)' 2>/dev/null)
-
- if [ -n "$CURRENT_TOKENS" ] && [ "$CURRENT_TOKENS" -gt 0 ] 2>/dev/null; then
- context_used_pct=$((CURRENT_TOKENS * 100 / CONTEXT_SIZE))
- context_remaining_pct=$((100 - context_used_pct))
- ((context_remaining_pct < 0)) && context_remaining_pct=0
- ((context_remaining_pct > 100)) && context_remaining_pct=100
-
- if [ "$context_remaining_pct" -le 20 ]; then
- context_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;203m'; fi; } # coral red
- elif [ "$context_remaining_pct" -le 40 ]; then
- context_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;215m'; fi; } # peach
- else
- context_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;158m'; fi; } # mint green
- fi
- context_pct="${context_remaining_pct}%"
- fi
+ printf '%s\n' "$model_name" "$current_dir" "$context_size" "$current_usage" \
+ "$total_input" "$total_output" "$cost_usd" "$duration_ms" "$version" "$session_id"
+}
+
+# ============================================================
+# GIT OPERATIONS (Optimized with porcelain v2)
+# ============================================================
+
+get_git_info() {
+ local current_dir="$1"
+ local git_opts=()
+
+ [[ -n "$current_dir" && "$current_dir" != "$NULL_VALUE" && "$current_dir" != "unknown" ]] && git_opts=(-C "$current_dir")
+
+ # Check if git repo
+ git "${git_opts[@]}" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
+ echo "$STATE_NOT_REPO"
+ return 0
+ }
+
+ # Single git status call with all info (porcelain v2)
+ local status_output
+ status_output=$(git "${git_opts[@]}" status --porcelain=v2 --branch --untracked-files=all 2>/dev/null) || {
+ # Fallback for older git versions
+ local branch
+ branch=$(git "${git_opts[@]}" branch --show-current 2>/dev/null || git "${git_opts[@]}" rev-parse --short HEAD 2>/dev/null || echo "unknown")
+ echo "$STATE_CLEAN|$branch|0|0"
+ return 0
+ }
+
+ # Parse porcelain v2 output
+ local branch="" ahead="0" behind="0"
+ while IFS= read -r line; do
+ case "$line" in
+ "# branch.head "*)
+ branch="${line#\# branch.head }"
+ ;;
+ "# branch.ab "*)
+ local ab="${line#\# branch.ab }"
+ ahead="${ab%% *}"
+ ahead="${ahead#+}"
+ behind="${ab##* }"
+ behind="${behind#-}"
+ ;;
+ esac
+ done <<< "$status_output"
+
+ branch="${branch:-(detached)}"
+ ahead="${ahead:-0}"
+ behind="${behind:-0}"
+
+ # Count modified files (lines not starting with #)
+ local file_count=0
+ while IFS= read -r line; do
+ [[ "$line" != \#* && -n "$line" ]] && ((file_count++))
+ done <<< "$status_output"
+
+ if [[ "$file_count" -eq 0 ]]; then
+ echo "$STATE_CLEAN|$branch|$ahead|$behind"
+ return 0
fi
-fi
-
-# ---- usage colors ----
-usage_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;189m'; fi; } # lavender
-cost_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;222m'; fi; } # light gold
-burn_color() { if [ "$use_color" -eq 1 ]; then printf '\033[38;5;220m'; fi; } # bright gold
-
-# ---- cost and usage extraction ----
-cost_usd=""
-cost_per_hour=""
-tpm=""
-tot_tokens=""
-
-# Extract cost and token data from Claude Code's native input
-if [ "$HAS_JQ" -eq 1 ]; then
- # Cost data
- cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty' 2>/dev/null)
- total_duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // empty' 2>/dev/null)
-
- # Calculate burn rate ($/hour) from cost and duration
- if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
- cost_per_hour=$(echo "$cost_usd $total_duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}')
+
+ # Get line changes
+ local added=0 removed=0
+ local diff_output
+ diff_output=$(git "${git_opts[@]}" diff HEAD --numstat 2>/dev/null || true)
+ if [[ -n "$diff_output" ]]; then
+ while IFS=$'\t' read -r a r _; do
+ [[ "$a" =~ ^[0-9]+$ ]] && added=$((added + a))
+ [[ "$r" =~ ^[0-9]+$ ]] && removed=$((removed + r))
+ done <<< "$diff_output"
fi
- # Token data from native context_window (no ccusage needed)
- input_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0' 2>/dev/null)
- output_tokens=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0' 2>/dev/null)
+ echo "$STATE_DIRTY|$branch|$file_count|$added|$removed|$ahead|$behind"
+}
+
+# ============================================================
+# COMPONENT BUILDERS
+# ============================================================
+
+build_model_component() {
+ local model_name="$1"
+ echo -n "${MODEL_ICON} ${CYAN}${model_name}${NC}"
+}
+
+build_context_component() {
+ local context_size="$1"
+ local current_usage="$2"
- if [ "$input_tokens" != "null" ] && [ "$output_tokens" != "null" ]; then
- tot_tokens=$((input_tokens + output_tokens))
- [ "$tot_tokens" -eq 0 ] && tot_tokens=""
+ local context_percent=0
+ if [[ "$current_usage" != "0" && "$context_size" -gt 0 ]] 2>/dev/null; then
+ context_percent=$((current_usage * 100 / context_size))
fi
- # Calculate tokens per minute from native data
- if [ -n "$tot_tokens" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
- tpm=$(echo "$tot_tokens $total_duration_ms" | awk '{if ($2 > 0) printf "%.0f", $1 * 60000 / $2; else print ""}')
+ # Determine color based on usage (inverted - higher usage = more warning)
+ local bar_color
+ if [[ "$context_percent" -le 20 ]]; then
+ bar_color="$GREEN"
+ elif [[ "$context_percent" -le 40 ]]; then
+ bar_color="$CYAN"
+ elif [[ "$context_percent" -le 60 ]]; then
+ bar_color="$YELLOW"
+ elif [[ "$context_percent" -le 80 ]]; then
+ bar_color="$ORANGE"
+ else
+ bar_color="$RED"
fi
-else
- # Bash fallback for cost extraction
- cost_usd=$(echo "$input" | grep -o '"total_cost_usd"[[:space:]]*:[[:space:]]*[0-9.]*' | sed 's/.*:[[:space:]]*\([0-9.]*\).*/\1/')
- total_duration_ms=$(echo "$input" | grep -o '"total_duration_ms"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\([0-9]*\).*/\1/')
- if [ -n "$cost_usd" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
- cost_per_hour=$(echo "$cost_usd $total_duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}')
+ # Build progress bar
+ local filled=$((context_percent * BAR_WIDTH / 100))
+ local empty=$((BAR_WIDTH - filled))
+ local bar="${bar_color}"
+ bar+=$(printf "%${filled}s" | tr ' ' "$BAR_FILLED")
+ bar+="${GRAY}"
+ bar+=$(printf "%${empty}s" | tr ' ' "$BAR_EMPTY")
+ bar+="${NC}"
+
+ # Format numbers
+ local usage_fmt size_fmt
+ usage_fmt=$(format_number "$current_usage")
+ size_fmt=$(format_number "$context_size")
+
+ # Get random message
+ local message
+ message=$(get_context_message "$context_percent")
+
+ echo -n "${CONTEXT_ICON} ${GRAY}[${NC}${bar}${GRAY}]${NC} ${context_percent}% ${usage_fmt}/${size_fmt} ${GRAY}ยท ${message}${NC}"
+}
+
+build_directory_component() {
+ local current_dir="$1"
+
+ local dir_name
+ if [[ -n "$current_dir" && "$current_dir" != "$NULL_VALUE" && "$current_dir" != "unknown" ]]; then
+ # Replace home with ~
+ current_dir="${current_dir/#$HOME/\~}"
+ dir_name=$(get_dirname "$current_dir")
+ else
+ dir_name=$(get_dirname "$PWD")
fi
- # Token data from native context_window (bash fallback)
- input_tokens=$(echo "$input" | grep -o '"total_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\([0-9]*\).*/\1/')
- output_tokens=$(echo "$input" | grep -o '"total_output_tokens"[[:space:]]*:[[:space:]]*[0-9]*' | sed 's/.*:[[:space:]]*\([0-9]*\).*/\1/')
+ echo -n "${DIR_ICON} ${BLUE}${dir_name}${NC}"
+}
- if [ -n "$input_tokens" ] && [ -n "$output_tokens" ]; then
- tot_tokens=$((input_tokens + output_tokens))
- [ "$tot_tokens" -eq 0 ] && tot_tokens=""
+build_git_component() {
+ local git_data="$1"
+
+ local state
+ IFS='|' read -r state _ <<< "$git_data"
+
+ case "$state" in
+ "$STATE_NOT_REPO")
+ return 0
+ ;;
+ "$STATE_CLEAN")
+ local branch ahead behind
+ IFS='|' read -r _ branch ahead behind <<< "$git_data"
+ echo -n "${GIT_ICON} ${MAGENTA}${branch}${NC}"
+ [[ "$ahead" -gt 0 ]] 2>/dev/null && echo -n " ${GREEN}โ†‘${ahead}${NC}"
+ [[ "$behind" -gt 0 ]] 2>/dev/null && echo -n " ${RED}โ†“${behind}${NC}"
+ ;;
+ "$STATE_DIRTY")
+ local branch files added removed ahead behind
+ IFS='|' read -r _ branch files added removed ahead behind <<< "$git_data"
+ echo -n "${GIT_ICON} ${MAGENTA}${branch}${NC}"
+ [[ "$ahead" -gt 0 ]] 2>/dev/null && echo -n " ${GREEN}โ†‘${ahead}${NC}"
+ [[ "$behind" -gt 0 ]] 2>/dev/null && echo -n " ${RED}โ†“${behind}${NC}"
+ echo -n " ${GRAY}ยท${NC} ${ORANGE}${files} files${NC}"
+ if [[ "$added" -gt 0 || "$removed" -gt 0 ]] 2>/dev/null; then
+ echo -n " ${GREEN}+${added}${NC}/${RED}-${removed}${NC}"
+ fi
+ ;;
+ esac
+}
+
+build_cost_component() {
+ local cost_usd="$1"
+ local duration_ms="$2"
+
+ [[ -z "$cost_usd" || "$cost_usd" == "0" || "$cost_usd" == "$NULL_VALUE" ]] && return 0
+
+ local cost_fmt
+ cost_fmt=$(format_cost "$cost_usd")
+ echo -n "${COST_ICON} ${YELLOW}\$${cost_fmt}${NC}"
+
+ # Calculate burn rate ($/hour)
+ if [[ -n "$duration_ms" && "$duration_ms" -gt 0 ]] 2>/dev/null; then
+ local burn_rate
+ burn_rate=$(echo "$cost_usd $duration_ms" | awk '{printf "%.2f", $1 * 3600000 / $2}')
+ echo -n " ${GRAY}(\$${burn_rate}/h)${NC}"
fi
+}
- if [ -n "$tot_tokens" ] && [ -n "$total_duration_ms" ] && [ "$total_duration_ms" -gt 0 ]; then
- tpm=$(echo "$tot_tokens $total_duration_ms" | awk '{if ($2 > 0) printf "%.0f", $1 * 60000 / $2; else print ""}')
+build_token_component() {
+ local total_input="$1"
+ local total_output="$2"
+ local duration_ms="$3"
+
+ local total_tokens=$((total_input + total_output))
+ [[ "$total_tokens" -eq 0 ]] && return 0
+
+ local tokens_fmt
+ tokens_fmt=$(format_number "$total_tokens")
+ echo -n "${TOKEN_ICON} ${LIGHT_GRAY}${tokens_fmt} tok${NC}"
+
+ # Calculate TPM
+ if [[ -n "$duration_ms" && "$duration_ms" -gt 0 ]] 2>/dev/null; then
+ local tpm
+ tpm=$(echo "$total_tokens $duration_ms" | awk '{if ($2 > 0) printf "%.0f", $1 * 60000 / $2; else print "0"}')
+ echo -n " ${GRAY}(${tpm} tpm)${NC}"
fi
-fi
-
-# ---- log extracted data ----
-{
- echo "[$TIMESTAMP] Extracted: dir=${current_dir:-}, model=${model_name:-}, version=${model_version:-}, git=${git_branch:-}, context=${context_pct:-}, cost=${cost_usd:-}, cost_ph=${cost_per_hour:-}, tokens=${tot_tokens:-}, tpm=${tpm:-}"
-} >>"$LOG_FILE" 2>/dev/null
-
-# ---- render statusline ----
-# Line 1: Core info (directory, git, model, claude code version, output style)
-printf '๐Ÿ“ %s%s%s' "$(dir_color)" "$current_dir" "$(rst)"
-if [ -n "$git_branch" ]; then
- printf ' ๐ŸŒฟ %s%s%s' "$(git_color)" "$git_branch" "$(rst)"
-fi
-printf ' ๐Ÿค– %s%s%s' "$(model_color)" "$model_name" "$(rst)"
-if [ -n "$model_version" ] && [ "$model_version" != "null" ]; then
- printf ' ๐Ÿท๏ธ %s%s%s' "$(version_color)" "$model_version" "$(rst)"
-fi
-if [ -n "$cc_version" ] && [ "$cc_version" != "null" ]; then
- printf ' ๐Ÿ“Ÿ %sv%s%s' "$(cc_version_color)" "$cc_version" "$(rst)"
-fi
-if [ -n "$output_style" ] && [ "$output_style" != "null" ]; then
- printf ' ๐ŸŽจ %s%s%s' "$(style_color)" "$output_style" "$(rst)"
-fi
-
-# Line 2: Context and session time
-line2=""
-if [ -n "$context_pct" ]; then
- context_bar=$(progress_bar "$context_remaining_pct" 10)
- line2="๐Ÿง  $(context_color)Context Remaining: ${context_pct} [${context_bar}]$(rst)"
-fi
-if [ -z "$line2" ] && [ -z "$context_pct" ]; then
- line2="๐Ÿง  $(context_color)Context Remaining: TBD$(rst)"
-fi
-
-# Line 3: Cost and usage analytics
-line3=""
-if [ -n "$cost_usd" ] && [[ "$cost_usd" =~ ^[0-9.]+$ ]]; then
- if [ -n "$cost_per_hour" ] && [[ "$cost_per_hour" =~ ^[0-9.]+$ ]]; then
- cost_per_hour_formatted=$(printf '%.2f' "$cost_per_hour")
- line3="๐Ÿ’ฐ $(cost_color)\$$(printf '%.2f' "$cost_usd")$(rst) ($(burn_color)\$${cost_per_hour_formatted}/h$(rst))"
+}
+
+build_time_component() {
+ local duration_ms="$1"
+
+ [[ -z "$duration_ms" || "$duration_ms" == "0" || "$duration_ms" == "$NULL_VALUE" ]] && return 0
+
+ local duration_fmt
+ duration_fmt=$(format_duration "$duration_ms")
+ [[ -n "$duration_fmt" ]] && echo -n "${TIME_ICON} ${LIGHT_GRAY}${duration_fmt}${NC}"
+}
+
+build_version_component() {
+ local version="$1"
+
+ [[ -z "$version" || "$version" == "$NULL_VALUE" ]] && return 0
+ echo -n "${VERSION_ICON} ${GRAY}v${version}${NC}"
+}
+
+# ============================================================
+# MAIN
+# ============================================================
+
+main() {
+ # Read input
+ local input
+ input=$(cat) || {
+ echo "Error: Failed to read stdin" >&2
+ exit 1
+ }
+
+ log_debug "Status line triggered (v${STATUSLINE_VERSION})"
+
+ # Parse JSON
+ local parsed
+ if check_jq; then
+ parsed=$(parse_with_jq "$input")
+ log_debug "Using jq for JSON parsing"
else
- line3="๐Ÿ’ฐ $(cost_color)\$$(printf '%.2f' "$cost_usd")$(rst)"
+ parsed=$(parse_without_jq "$input")
+ log_debug "Using bash fallback for JSON parsing"
fi
-fi
-if [ -n "$tot_tokens" ] && [[ "$tot_tokens" =~ ^[0-9]+$ ]]; then
- if [ -n "$tpm" ] && [[ "$tpm" =~ ^[0-9.]+$ ]]; then
- tpm_formatted=$(printf '%.0f' "$tpm")
- if [ -n "$line3" ]; then
- line3="$line3 ๐Ÿ“Š $(usage_color)${tot_tokens} tok (${tpm_formatted} tpm)$(rst)"
- else
- line3="๐Ÿ“Š $(usage_color)${tot_tokens} tok (${tpm_formatted} tpm)$(rst)"
+
+ if [[ -z "$parsed" ]]; then
+ echo "Error: Failed to parse input" >&2
+ exit 1
+ fi
+
+ # Extract fields
+ local model_name current_dir context_size current_usage
+ local total_input total_output cost_usd duration_ms version session_id
+ {
+ read -r model_name
+ read -r current_dir
+ read -r context_size
+ read -r current_usage
+ read -r total_input
+ read -r total_output
+ read -r cost_usd
+ read -r duration_ms
+ read -r version
+ read -r session_id
+ } <<< "$parsed"
+
+ log_debug "Parsed: model=$model_name, dir=$current_dir, context=$current_usage/$context_size, cost=$cost_usd, duration=$duration_ms"
+
+ # Get git info
+ local git_data
+ git_data=$(get_git_info "$current_dir")
+
+ # Build components
+ local output=""
+
+ # Line 1: Model | Directory | Git | Version
+ output+=$(build_model_component "$model_name")
+ output+=$(sep)
+ output+=$(build_directory_component "$current_dir")
+
+ local git_component
+ git_component=$(build_git_component "$git_data")
+ [[ -n "$git_component" ]] && { output+=$(sep); output+="$git_component"; }
+
+ local version_component
+ version_component=$(build_version_component "$version")
+ [[ -n "$version_component" ]] && { output+=$(sep); output+="$version_component"; }
+
+ # Line 2: Context
+ output+=$'\n'
+ output+=$(build_context_component "$context_size" "$current_usage")
+
+ # Line 3: Cost | Tokens | Time
+ local cost_component token_component time_component
+ cost_component=$(build_cost_component "$cost_usd" "$duration_ms")
+ token_component=$(build_token_component "$total_input" "$total_output" "$duration_ms")
+ time_component=$(build_time_component "$duration_ms")
+
+ if [[ -n "$cost_component" || -n "$token_component" || -n "$time_component" ]]; then
+ output+=$'\n'
+ local first=1
+ if [[ -n "$cost_component" ]]; then
+ output+="$cost_component"
+ first=0
fi
- else
- if [ -n "$line3" ]; then
- line3="$line3 ๐Ÿ“Š $(usage_color)${tot_tokens} tok$(rst)"
- else
- line3="๐Ÿ“Š $(usage_color)${tot_tokens} tok$(rst)"
+ if [[ -n "$token_component" ]]; then
+ [[ "$first" -eq 0 ]] && output+=$(sep)
+ output+="$token_component"
+ first=0
+ fi
+ if [[ -n "$time_component" ]]; then
+ [[ "$first" -eq 0 ]] && output+=$(sep)
+ output+="$time_component"
fi
fi
-fi
-
-# Print lines
-if [ -n "$line2" ]; then
- printf '\n%s' "$line2"
-fi
-if [ -n "$line3" ]; then
- printf '\n%s' "$line3"
-fi
-printf '\n'
+
+ echo -e "$output"
+}
+
+main "$@"