From 501af21980ce428a17fd9b382246e344a47a6334 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:12:19 +0900 Subject: modified claude/settings.json, deleted claude/statusline.sh, created statuslines/ --- ar/.config/claude/settings.json | 9 +- ar/.config/claude/statusline.sh | 611 --------------------------- ar/.config/claude/statuslines/statusline.sh | 624 ++++++++++++++++++++++++++++ 3 files changed, 628 insertions(+), 616 deletions(-) delete mode 100755 ar/.config/claude/statusline.sh create mode 100755 ar/.config/claude/statuslines/statusline.sh diff --git a/ar/.config/claude/settings.json b/ar/.config/claude/settings.json index 8c88f47..b1df728 100644 --- a/ar/.config/claude/settings.json +++ b/ar/.config/claude/settings.json @@ -1,5 +1,4 @@ { - "model": "sonnet", "permissions": { "deny": [ "Bash(rm -rf:*)", @@ -11,10 +10,6 @@ "Write(**/.password-store/**/*)" ] }, - "statusLine": { - "type": "command", - "command": "$CLAUDE_CONFIG_DIR/statuslines/statusline.sh" - }, "hooks": { "Notification": [ { @@ -38,5 +33,9 @@ ] } ] + }, + "statusLine": { + "type": "command", + "command": "$CLAUDE_CONFIG_DIR/statuslines/statusline.sh" } } diff --git a/ar/.config/claude/statusline.sh b/ar/.config/claude/statusline.sh deleted file mode 100755 index 0d524d2..0000000 --- a/ar/.config/claude/statusline.sh +++ /dev/null @@ -1,611 +0,0 @@ -#!/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" - -log_debug() { - local timestamp - timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$timestamp] $*" >> "$LOG_FILE" 2>/dev/null || true -} - -# ============================================================ -# UTILITY FUNCTIONS -# ============================================================ - -# Check jq availability -check_jq() { - command -v jq >/dev/null 2>&1 -} - -# 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 - 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 -} - -# 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 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 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 - echo "$value" - else - echo "$default" - fi -} - -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" "") - - 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 - - # 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 - - 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" - - local context_percent=0 - if [[ "$current_usage" != "0" && "$context_size" -gt 0 ]] 2>/dev/null; then - context_percent=$((current_usage * 100 / context_size)) - fi - - # 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 - - # 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 - - echo -n "${DIR_ICON} ${BLUE}${dir_name}${NC}" -} - -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 -} - -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 -} - -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 - parsed=$(parse_without_jq "$input") - log_debug "Using bash fallback for JSON parsing" - fi - - 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 - 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 - - echo -e "$output" -} - -main "$@" diff --git a/ar/.config/claude/statuslines/statusline.sh b/ar/.config/claude/statuslines/statusline.sh new file mode 100755 index 0000000..047590a --- /dev/null +++ b/ar/.config/claude/statuslines/statusline.sh @@ -0,0 +1,624 @@ +#!/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" + +log_debug() { + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] $*" >>"$LOG_FILE" 2>/dev/null || true +} + +# ============================================================ +# UTILITY FUNCTIONS +# ============================================================ + +# Check jq availability +check_jq() { + command -v jq >/dev/null 2>&1 +} + +# 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 + 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 +} + +# 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 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 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 + echo "$value" + else + echo "$default" + fi +} + +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" "") + + 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 + + # 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 + + 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" + + local context_percent=0 + if [[ "$current_usage" != "0" && "$context_size" -gt 0 ]] 2>/dev/null; then + context_percent=$((current_usage * 100 / context_size)) + fi + + # 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 + + # Build progress bar + local filled=$((context_percent * BAR_WIDTH / 100)) + local empty=$((BAR_WIDTH - filled)) + local bar="${bar_color}" + local i + for ((i = 0; i < filled; i++)); do bar+="$BAR_FILLED"; done + bar+="${GRAY}" + for ((i = 0; i < empty; i++)); do bar+="$BAR_EMPTY"; done + 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 + + echo -n "${DIR_ICON} ${BLUE}${dir_name}${NC}" +} + +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 +} + +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 +} + +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 + parsed=$(parse_without_jq "$input") + log_debug "Using bash fallback for JSON parsing" + fi + + 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 + 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 + + echo -e "$output" +} + +main "$@" -- cgit v1.2.3