From f9e4c65001751bca04b25ffbf2c9867ac213aa3b Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 28 Mar 2026 16:51:14 +0000 Subject: [PATCH] Add claude statusline to my dotfiles --- bootstrap.sh | 4 ++ claude/statusline.sh | 150 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100755 claude/statusline.sh diff --git a/bootstrap.sh b/bootstrap.sh index 9cd1588..50b85a4 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -41,6 +41,10 @@ test -L $HOME/.config/nvim/init.lua || ln -f -s $BASEDIR/neovim/init.lua $HOME/. test -d $HOME/.config/nvim/lsp || mkdir -p $HOME/.config/nvim/lsp test -L $HOME/.config/nvim/lsp/phpactor.lua || ln -f -s $BASEDIR/neovim/lsp/phpactor.lua $HOME/.config/nvim/lsp/phpactor.lua +# Claude Code statusline +test -d $HOME/.claude || mkdir $HOME/.claude +test -L $HOME/.claude/statusline.sh || ln -f -s $BASEDIR/claude/statusline.sh $HOME/.claude/statusline.sh + # .gitconfig gets edited by .extra so we won't symlink it, but copy it echo "For compatibility we shall copy the global gitconfig" cp $BASEDIR/gitconfig $HOME/.gitconfig diff --git a/claude/statusline.sh b/claude/statusline.sh new file mode 100755 index 0000000..8d5a583 --- /dev/null +++ b/claude/statusline.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Status line command for Claude Code +# Two-line display: +# Line 1: [Opus 4.6] 📁 project-name | 🌿 branch +# Line 2: █████░░░░░ 42% | 19.8k tok | ✅ Max | ⏱️ 7m 3s (API thinking time, not wall-clock) + +# Read JSON input from Claude Code +input=$(cat) + +# Debug mode: set CLAUDE_STATUSLINE_DEBUG=1 to dump JSON input +if [ -n "$CLAUDE_STATUSLINE_DEBUG" ]; then + echo "$input" | jq '.' > /tmp/claude-statusline-debug.json 2>/dev/null +fi + +# Extract data from JSON (single jq call for performance) +eval "$(echo "$input" | jq -r ' + @sh "cwd=\(.workspace.current_dir)", + @sh "model_display=\(.model.display_name)", + @sh "model_id=\(.model.id)", + @sh "used_pct_raw=\(.context_window.used_percentage // 0)", + @sh "total_input=\(.context_window.total_input_tokens // 0)", + @sh "total_output=\(.context_window.total_output_tokens // 0)", + @sh "duration_ms=\(.cost.total_api_duration_ms // 0)", + @sh "total_cost=\(.cost.total_cost_usd // 0)" +')" + +dir_name=$(basename "$cwd") + +# Truncate percentage to integer +used_pct=${used_pct_raw%%.*} +: "${used_pct:=0}" + +# Use display_name directly — it already includes the version (e.g. "Opus 4.6") +# Only extract from model_id as fallback if display_name has no version +if [[ "$model_display" =~ [0-9] ]]; then + model_full="$model_display" +else + version="" + if [[ "$model_id" =~ claude-[a-z]+-([0-9]+)-([0-9]+) ]]; then + version="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + fi + if [ -n "$version" ]; then + model_full="$model_display $version" + else + model_full="$model_display" + fi +fi + +# Git branch with caching (5 second TTL) +git_branch="" +cache_dir="/tmp/claude-statusline" +cache_file="${cache_dir}/git-branch-$(echo "$cwd" | md5 -q)" +cache_ttl=5 + +if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then + now=$(date +%s) + file_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0) + cache_age=$((now - file_mtime)) + if [ -f "$cache_file" ] && [ "$cache_age" -lt "$cache_ttl" ]; then + git_branch=$(cat "$cache_file") + else + git_branch=$(git -C "$cwd" branch --show-current 2>/dev/null || echo 'detached') + mkdir -p "$cache_dir" && echo "$git_branch" > "$cache_file" + fi +fi + +# Format tokens (pure bash integer math) +total_tokens=$(( ${total_input%%.*} + ${total_output%%.*} )) +if [ "$total_tokens" -ge 1000000 ]; then + whole=$((total_tokens / 1000000)) + frac=$(( (total_tokens % 1000000) / 100000 )) + token_display="${whole}.${frac}M tok" +elif [ "$total_tokens" -ge 1000 ]; then + whole=$((total_tokens / 1000)) + frac=$(( (total_tokens % 1000) / 100 )) + if [ "$whole" -ge 100 ]; then + token_display="${whole}k tok" + else + token_display="${whole}.${frac}k tok" + fi +else + token_display="${total_tokens} tok" +fi + +# Billing mode detection (cached 60s) +billing_cache="/tmp/.claude-statusline-billing-cache" +billing_ttl=60 +billing_mode="" + +now_bill=$(date +%s) +bill_mtime=$(stat -c %Y "$billing_cache" 2>/dev/null || stat -f %m "$billing_cache" 2>/dev/null || echo 0) +bill_age=$((now_bill - bill_mtime)) + +if [ -f "$billing_cache" ] && [ "$bill_age" -lt "$billing_ttl" ]; then + billing_mode=$(cat "$billing_cache") +else + if security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then + billing_mode="max" + elif [ -n "$ANTHROPIC_API_KEY" ]; then + billing_mode="api" + else + billing_mode="unknown" + fi + echo "$billing_mode" > "$billing_cache" +fi + +if [ "$billing_mode" = "max" ]; then + billing_display='\e[32m✅ Max\e[0m' +elif [ "$billing_mode" = "api" ]; then + billing_display='\e[1;31m⚠️ API\e[0m' +else + billing_display='\e[33m? Auth\e[0m' +fi + +# Format cost +cost_display=$(printf '$%.2f' "$total_cost") + +# Format duration from milliseconds +duration_sec=$((${duration_ms%%.*} / 1000)) +duration_min=$((duration_sec / 60)) +duration_rem=$((duration_sec % 60)) + +# Build progress bar (10 chars wide) +filled=$((used_pct * 10 / 100)) +empty=$((10 - filled)) + +# Color-code based on usage +if [ "$used_pct" -lt 70 ]; then + bar_color='\e[32m' # Green +elif [ "$used_pct" -lt 90 ]; then + bar_color='\e[33m' # Yellow +else + bar_color='\e[31m' # Red +fi + +bar="" +for ((i=0; i