diff options
Diffstat (limited to 'default/.claude/statuslines/statusline.sh')
| -rwxr-xr-x | default/.claude/statuslines/statusline.sh | 337 |
1 files changed, 285 insertions, 52 deletions
diff --git a/default/.claude/statuslines/statusline.sh b/default/.claude/statuslines/statusline.sh index 0d524d2..e000b2b 100755 --- a/default/.claude/statuslines/statusline.sh +++ b/default/.claude/statuslines/statusline.sh @@ -2,10 +2,10 @@ set -euo pipefail 2>/dev/null || set -eu # ============================================================ -# STATUSLINE v2.0.0 - Claude Code Status Line +# STATUSLINE v2.1.0 - Claude Code Status Line # ============================================================ -readonly STATUSLINE_VERSION="2.0.0" +readonly STATUSLINE_VERSION="2.1.0" # ============================================================ # CONFIGURATION @@ -36,9 +36,16 @@ readonly CONTEXT_ICON="๐ง " readonly DIR_ICON="๐" readonly GIT_ICON="๐ฟ" readonly COST_ICON="๐ฐ" +readonly DAILY_COST_ICON="๐
" readonly TOKEN_ICON="๐" -readonly TIME_ICON="โฑ๏ธ" +readonly CACHE_ICON="๐พ" +readonly TIME_ICON="๐" +readonly REMAINING_ICON="โณ" readonly VERSION_ICON="๐" +readonly PYTHON_ICON="๐" +readonly GO_ICON="๐ฆซ" +readonly NODE_ICON="๐ฆ" +readonly RUST_ICON="๐ฆ" # Git state constants readonly STATE_NOT_REPO="not_repo" @@ -120,7 +127,7 @@ readonly LOG_FILE="${SCRIPT_DIR}/statusline.log" log_debug() { local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$timestamp] $*" >> "$LOG_FILE" 2>/dev/null || true + echo "[$timestamp] $*" >>"$LOG_FILE" 2>/dev/null || true } # ============================================================ @@ -139,7 +146,10 @@ sep() { echo -n " ${SEPARATOR} "; } # Format numbers with K/M suffixes format_number() { local num="${1:-0}" - [[ ! "$num" =~ ^[0-9]+$ ]] && { echo "$num"; return; } + [[ ! "$num" =~ ^[0-9]+$ ]] && { + echo "$num" + return + } if [[ "$num" -lt 1000 ]]; then echo "$num" @@ -167,7 +177,10 @@ format_number() { # Format duration (ms to human readable) format_duration() { local ms="${1:-0}" - [[ ! "$ms" =~ ^[0-9]+$ ]] && { echo ""; return; } + [[ ! "$ms" =~ ^[0-9]+$ ]] && { + echo "" + return + } local seconds=$((ms / 1000)) local minutes=$((seconds / 60)) @@ -217,6 +230,86 @@ get_context_message() { } # ============================================================ +# LANGUAGE/RUNTIME DETECTION +# ============================================================ + +detect_language_info() { + local current_dir="$1" + local lang_info="" + + # Use current_dir or fallback to PWD + local check_dir="${current_dir:-$PWD}" + [[ "$check_dir" == "$NULL_VALUE" || "$check_dir" == "unknown" ]] && check_dir="$PWD" + + # Check for Python project + if [[ -n "${VIRTUAL_ENV:-}" ]]; then + local venv_name="${VIRTUAL_ENV##*/}" + local folder_name="${check_dir##*/}" + # Show venv name unless it's generic + if [[ "$venv_name" == ".venv" || "$venv_name" == "venv" ]]; then + venv_name="$folder_name" + fi + local pyver + pyver=$(python3 --version 2>/dev/null | cut -d' ' -f2 || echo '') + [[ -n "$pyver" ]] && lang_info="${PYTHON_ICON} ${GREEN}${pyver}${NC} ${GRAY}(${venv_name})${NC}" + elif [[ -f "$check_dir/requirements.txt" || -f "$check_dir/setup.py" || -f "$check_dir/pyproject.toml" || -f "$check_dir/Pipfile" ]]; then + local pyver + pyver=$(python3 --version 2>/dev/null | cut -d' ' -f2 || echo '') + [[ -n "$pyver" ]] && lang_info="${PYTHON_ICON} ${GREEN}${pyver}${NC}" + # Check for Go project + elif [[ -f "$check_dir/go.mod" || -f "$check_dir/go.sum" ]]; then + local gover + gover=$(go version 2>/dev/null | grep -oE 'go[0-9]+\.[0-9]+(\.[0-9]+)?' | sed 's/go//' || echo '') + [[ -n "$gover" ]] && lang_info="${GO_ICON} ${CYAN}${gover}${NC}" + # Check for Node.js project + elif [[ -f "$check_dir/package.json" ]]; then + local nodever + nodever=$(node --version 2>/dev/null | sed 's/v//' || echo '') + [[ -n "$nodever" ]] && lang_info="${NODE_ICON} ${GREEN}${nodever}${NC}" + # Check for Rust project + elif [[ -f "$check_dir/Cargo.toml" ]]; then + local rustver + rustver=$(rustc --version 2>/dev/null | cut -d' ' -f2 || echo '') + [[ -n "$rustver" ]] && lang_info="${RUST_ICON} ${ORANGE}${rustver}${NC}" + fi + + echo "$lang_info" +} + +# ============================================================ +# CCUSAGE DATA FETCHING +# ============================================================ + +get_ccusage_data() { + local session_id="$1" + + # Check if npx is available + command -v npx >/dev/null 2>&1 || return 1 + + # Get daily cost + local daily_cost="" + local daily_json + daily_json=$(npx ccusage daily --json 2>/dev/null) + if [[ -n "$daily_json" ]]; then + daily_cost=$(echo "$daily_json" | jq -r '.totals.totalCost // 0' 2>/dev/null) + fi + + # Get active block data (projections, remaining time) + local remaining_minutes="" projected_cost="" block_cost="" burn_rate="" + local blocks_json + blocks_json=$(npx ccusage blocks --active --json 2>/dev/null) + if [[ -n "$blocks_json" ]]; then + remaining_minutes=$(echo "$blocks_json" | jq -r '.blocks[0].projection.remainingMinutes // empty' 2>/dev/null) + projected_cost=$(echo "$blocks_json" | jq -r '.blocks[0].projection.totalCost // empty' 2>/dev/null) + block_cost=$(echo "$blocks_json" | jq -r '.blocks[0].costUSD // empty' 2>/dev/null) + burn_rate=$(echo "$blocks_json" | jq -r '.blocks[0].burnRate.costPerHour // empty' 2>/dev/null) + fi + + # Output: daily_cost|remaining_minutes|projected_cost|block_cost|burn_rate + echo "${daily_cost:-0}|${remaining_minutes:-0}|${projected_cost:-0}|${block_cost:-0}|${burn_rate:-0}" +} + +# ============================================================ # JSON PARSING # ============================================================ @@ -237,7 +330,9 @@ parse_with_jq() { (.cost.total_cost_usd // 0), (.cost.total_duration_ms // 0), .version // "", - .session_id // "" + .session_id // "", + (.context_window.current_usage.cache_creation_input_tokens // 0), + (.context_window.current_usage.cache_read_input_tokens // 0) ' 2>/dev/null } @@ -266,6 +361,7 @@ parse_without_jq() { local model_name current_dir context_size current_usage local total_input total_output cost_usd duration_ms version session_id + local cache_creation cache_read model_name=$(extract_json_string "$input" "display_name" "Claude") current_dir=$(extract_json_string "$input" "current_dir" "") @@ -279,9 +375,12 @@ parse_without_jq() { duration_ms=$(extract_json_string "$input" "total_duration_ms" "0") version=$(extract_json_string "$input" "version" "") session_id=$(extract_json_string "$input" "session_id" "") + cache_creation=$(extract_json_string "$input" "cache_creation_input_tokens" "0") + cache_read=$(extract_json_string "$input" "cache_read_input_tokens" "0") printf '%s\n' "$model_name" "$current_dir" "$context_size" "$current_usage" \ - "$total_input" "$total_output" "$cost_usd" "$duration_ms" "$version" "$session_id" + "$total_input" "$total_output" "$cost_usd" "$duration_ms" "$version" "$session_id" \ + "$cache_creation" "$cache_read" } # ============================================================ @@ -314,18 +413,18 @@ get_git_info() { 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#-}" - ;; + "# 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" + done <<<"$status_output" branch="${branch:-(detached)}" ahead="${ahead:-0}" @@ -335,7 +434,7 @@ get_git_info() { local file_count=0 while IFS= read -r line; do [[ "$line" != \#* && -n "$line" ]] && ((file_count++)) - done <<< "$status_output" + done <<<"$status_output" if [[ "$file_count" -eq 0 ]]; then echo "$STATE_CLEAN|$branch|$ahead|$behind" @@ -350,7 +449,7 @@ get_git_info() { while IFS=$'\t' read -r a r _; do [[ "$a" =~ ^[0-9]+$ ]] && added=$((added + a)) [[ "$r" =~ ^[0-9]+$ ]] && removed=$((removed + r)) - done <<< "$diff_output" + done <<<"$diff_output" fi echo "$STATE_DIRTY|$branch|$file_count|$added|$removed|$ahead|$behind" @@ -392,9 +491,10 @@ build_context_component() { local filled=$((context_percent * BAR_WIDTH / 100)) local empty=$((BAR_WIDTH - filled)) local bar="${bar_color}" - bar+=$(printf "%${filled}s" | tr ' ' "$BAR_FILLED") + local i + for ((i = 0; i < filled; i++)); do bar+="$BAR_FILLED"; done bar+="${GRAY}" - bar+=$(printf "%${empty}s" | tr ' ' "$BAR_EMPTY") + for ((i = 0; i < empty; i++)); do bar+="$BAR_EMPTY"; done bar+="${NC}" # Format numbers @@ -428,30 +528,30 @@ build_git_component() { local git_data="$1" local state - IFS='|' read -r state _ <<< "$git_data" + 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 - ;; + "$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 } @@ -510,6 +610,91 @@ build_version_component() { echo -n "${VERSION_ICON} ${GRAY}v${version}${NC}" } +build_language_component() { + local lang_info="$1" + [[ -z "$lang_info" ]] && return 0 + echo -n "$lang_info" +} + +build_daily_cost_component() { + local daily_cost="$1" + + [[ -z "$daily_cost" || "$daily_cost" == "0" || "$daily_cost" == "$NULL_VALUE" ]] && return 0 + + local cost_fmt + cost_fmt=$(format_cost "$daily_cost") + echo -n "${DAILY_COST_ICON} ${ORANGE}\$${cost_fmt}/day${NC}" +} + +build_remaining_time_component() { + local remaining_minutes="$1" + local projected_cost="$2" + + [[ -z "$remaining_minutes" || "$remaining_minutes" == "0" || "$remaining_minutes" == "$NULL_VALUE" ]] && return 0 + + local hours=$((remaining_minutes / 60)) + local mins=$((remaining_minutes % 60)) + + local time_str + if [[ "$hours" -gt 0 ]]; then + time_str="${hours}h ${mins}m" + else + time_str="${mins}m" + fi + + # Color based on remaining time + local time_color + if [[ "$remaining_minutes" -gt 120 ]]; then + time_color="$GREEN" + elif [[ "$remaining_minutes" -gt 60 ]]; then + time_color="$YELLOW" + elif [[ "$remaining_minutes" -gt 30 ]]; then + time_color="$ORANGE" + else + time_color="$RED" + fi + + echo -n "${REMAINING_ICON} ${time_color}${time_str} left${NC}" + + # Show projected cost if available + if [[ -n "$projected_cost" && "$projected_cost" != "0" && "$projected_cost" != "$NULL_VALUE" ]]; then + local proj_fmt + proj_fmt=$(format_cost "$projected_cost") + echo -n " ${GRAY}(๐ต\$${proj_fmt})${NC}" + fi +} + +build_cache_component() { + local cache_creation="$1" + local cache_read="$2" + + # Skip if no significant cache usage + local total_cache=$((cache_creation + cache_read)) + [[ "$total_cache" -lt 1000 ]] && return 0 + + local creation_fmt read_fmt + creation_fmt=$(format_number "$cache_creation") + read_fmt=$(format_number "$cache_read") + + # Calculate cache hit ratio + local hit_ratio=0 + if [[ "$total_cache" -gt 0 ]]; then + hit_ratio=$((cache_read * 100 / total_cache)) + fi + + # Color based on hit ratio (higher is better for cost) + local ratio_color + if [[ "$hit_ratio" -ge 80 ]]; then + ratio_color="$GREEN" + elif [[ "$hit_ratio" -ge 50 ]]; then + ratio_color="$YELLOW" + else + ratio_color="$ORANGE" + fi + + echo -n "${CACHE_ICON} ${GRAY}+${creation_fmt}${NC}/${ratio_color}โบ${read_fmt}${NC} ${GRAY}(${hit_ratio}% hit)${NC}" +} + # ============================================================ # MAIN # ============================================================ @@ -542,6 +727,7 @@ main() { # Extract fields local model_name current_dir context_size current_usage local total_input total_output cost_usd duration_ms version session_id + local cache_creation cache_read { read -r model_name read -r current_dir @@ -553,7 +739,9 @@ main() { read -r duration_ms read -r version read -r session_id - } <<< "$parsed" + read -r cache_creation + read -r cache_read + } <<<"$parsed" log_debug "Parsed: model=$model_name, dir=$current_dir, context=$current_usage/$context_size, cost=$cost_usd, duration=$duration_ms" @@ -561,39 +749,78 @@ main() { local git_data git_data=$(get_git_info "$current_dir") + # Get language/runtime info + local lang_info + lang_info=$(detect_language_info "$current_dir") + + # Get ccusage data (daily cost, remaining time, etc.) + local ccusage_data="" daily_cost="" remaining_minutes="" projected_cost="" block_cost="" ccusage_burn_rate="" + if check_jq; then + ccusage_data=$(get_ccusage_data "$session_id" 2>/dev/null || echo "") + if [[ -n "$ccusage_data" ]]; then + IFS='|' read -r daily_cost remaining_minutes projected_cost block_cost ccusage_burn_rate <<<"$ccusage_data" + fi + fi + # Build components local output="" - # Line 1: Model | Directory | Git | Version + # Line 1: Model | Directory | Language | Git | Version output+=$(build_model_component "$model_name") output+=$(sep) output+=$(build_directory_component "$current_dir") + local lang_component + lang_component=$(build_language_component "$lang_info") + [[ -n "$lang_component" ]] && { + output+=$(sep) + output+="$lang_component" + } + local git_component git_component=$(build_git_component "$git_data") - [[ -n "$git_component" ]] && { output+=$(sep); output+="$git_component"; } + [[ -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"; } + [[ -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 + # Line 3: Cost | Daily Cost | Remaining Time | Tokens | Time + local cost_component daily_cost_component remaining_component token_component time_component cache_component cost_component=$(build_cost_component "$cost_usd" "$duration_ms") + daily_cost_component=$(build_daily_cost_component "$daily_cost") + remaining_component=$(build_remaining_time_component "$remaining_minutes" "$projected_cost") token_component=$(build_token_component "$total_input" "$total_output" "$duration_ms") time_component=$(build_time_component "$duration_ms") + cache_component=$(build_cache_component "${cache_creation:-0}" "${cache_read:-0}") - if [[ -n "$cost_component" || -n "$token_component" || -n "$time_component" ]]; then + if [[ -n "$cost_component" || -n "$daily_cost_component" || -n "$remaining_component" || -n "$token_component" || -n "$time_component" ]]; then output+=$'\n' local first=1 if [[ -n "$cost_component" ]]; then output+="$cost_component" first=0 fi + if [[ -n "$daily_cost_component" ]]; then + [[ "$first" -eq 0 ]] && output+=$(sep) + output+="$daily_cost_component" + first=0 + fi + if [[ -n "$remaining_component" ]]; then + [[ "$first" -eq 0 ]] && output+=$(sep) + output+="$remaining_component" + first=0 + fi if [[ -n "$token_component" ]]; then [[ "$first" -eq 0 ]] && output+=$(sep) output+="$token_component" @@ -605,6 +832,12 @@ main() { fi fi + # Line 4: Cache info (if significant) + if [[ -n "$cache_component" ]]; then + output+=$'\n' + output+="$cache_component" + fi + echo -e "$output" } |
