Skip to content

Instantly share code, notes, and snippets.

@dhkts1
Last active August 24, 2025 07:02
Show Gist options
  • Save dhkts1/55709b1925b94aec55083dd1da9d8f39 to your computer and use it in GitHub Desktop.
Save dhkts1/55709b1925b94aec55083dd1da9d8f39 to your computer and use it in GitHub Desktop.
statusline command for Claude Code
#!/bin/bash
# Make sure this file is executable: chmod +x ~/.claude/statusline-command.sh
# Claude Code statusline script
# Reads JSON input from stdin and outputs a formatted status line to stdout
#
# Add to your ~/.claude/settings.json
#
# "statusLine": {
# "type": "command",
# "command": "bash ~/.claude/statusline-command.sh"
# }
#
# SYMBOL LEGEND:
# 🤖 Model indicator
# 📁 Current directory
# Git information:
# +N Staged files count (green)
# ~N Modified files count (yellow)
# ?N Untracked files count (gray)
# ↑N Commits ahead of remote (green)
# ↓N Commits behind remote (yellow)
# ↕N/M Diverged from remote (yellow)
# PR#N Open pull request number (cyan)
# 🌳 Git worktree indicator
# 🐍 Python virtual environment
# ⬢ Node.js version
# 🕐 Current time
# Color codes for better visual separation
readonly BLUE='\033[94m' # Bright blue for model/main info
readonly GREEN='\033[92m' # Bright green for clean git status
readonly YELLOW='\033[93m' # Bright yellow for modified git status
readonly RED='\033[91m' # Bright red for conflicts/errors
readonly PURPLE='\033[95m' # Bright purple for directory
readonly CYAN='\033[96m' # Bright cyan for python venv
readonly WHITE='\033[97m' # Bright white for time
readonly GRAY='\033[37m' # Gray for separators
readonly RESET='\033[0m' # Reset colors
readonly BOLD='\033[1m' # Bold text
# Read JSON input from stdin
input=$(cat)
# Extract data from JSON input using jq
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
current_dir=$(echo "$input" | jq -r '.workspace.current_dir // "."')
# Extract context information using proper getTokenMetrics implementation
# Find the most recent main chain entry (where isSidechain !== true) and calculate context length
transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')
if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
# Read transcript file (JSONL format) and calculate context length
# Each line is a separate JSON object, find the most recent main chain entry
context_length=$(tac "$transcript_path" 2>/dev/null | while IFS= read -r line; do
if [[ -n "$line" ]]; then
# Check if this line has isSidechain !== true and has message.usage
is_main_chain=$(echo "$line" | jq -r 'if .isSidechain == true then "false" else "true" end' 2>/dev/null)
has_usage=$(echo "$line" | jq -r 'if .message.usage then "true" else "false" end' 2>/dev/null)
if [[ "$is_main_chain" == "true" && "$has_usage" == "true" ]]; then
# Calculate context length from this entry
echo "$line" | jq -r '
(.message.usage.input_tokens // 0) +
(.message.usage.cache_read_input_tokens // 0) +
(.message.usage.cache_creation_input_tokens // 0)
' 2>/dev/null
break
fi
fi
done)
# If context_length is empty or null, set to 0
if [[ -z "$context_length" || "$context_length" == "null" ]]; then
context_length=0
fi
else
context_length=0
fi
# Fallback to simple context.length if transcript parsing fails
if [[ "$context_length" == "null" || "$context_length" == "0" || -z "$context_length" ]]; then
context_length=$(echo "$input" | jq -r '.context.length // 0')
fi
# Format context length (e.g., 18.6k)
if [[ "$context_length" -ge 1000 ]]; then
ctx_display=$(awk "BEGIN {printf \"%.1fk\", $context_length/1000}")
else
ctx_display="$context_length"
fi
# Get current directory relative to home directory
if [[ "$current_dir" == "$HOME"* ]]; then
# Replace home path with ~ for display
dir_display="${current_dir/#$HOME/~}"
else
# Keep full path if not under home directory
dir_display="$current_dir"
fi
# Get git status and worktree information with enhanced detection
git_info=""
if git -C "$current_dir" rev-parse --git-dir >/dev/null 2>&1; then
branch=$(git -C "$current_dir" branch --show-current 2>/dev/null)
# If no branch (detached HEAD), show short commit hash
if [[ -z "$branch" ]]; then
branch=$(git -C "$current_dir" rev-parse --short HEAD 2>/dev/null)
branch="detached:${branch}"
fi
# Enhanced worktree detection
worktree_info=""
git_dir=$(git -C "$current_dir" rev-parse --git-dir 2>/dev/null)
# Check if we're in a worktree
if [[ "$git_dir" == *".git/worktrees/"* ]] || [[ -f "$git_dir/gitdir" ]]; then
worktree_name=$(basename "$current_dir")
# Only show worktree indicator if it adds information
# Don't show if branch name already contains the worktree info
if [[ "$worktree_name" =~ ^TOK ]] && [[ "$branch" != *"$worktree_name"* ]]; then
# For TOK worktrees, just show the tree emoji
worktree_info=" ${CYAN}🌳${RESET}"
elif [[ ! "$worktree_name" =~ ^TOK ]] && [[ "$branch" != "$worktree_name" ]]; then
# For other worktrees, just show tree emoji
worktree_info=" ${CYAN}🌳${RESET}"
fi
fi
if [[ -n "$branch" ]]; then
# Comprehensive git status check
# Git status format: XY filename
# X = status of staging area, Y = status of working tree
git_status=$(git --no-optional-locks -C "$current_dir" status --porcelain 2>/dev/null)
# Count different types of changes (handle empty status gracefully)
if [[ -n "$git_status" ]]; then
# Count untracked files (starts with ??)
untracked=$(echo "$git_status" | grep -c '^??')
# Count modified files (M in second column or first column with space)
modified=$(echo "$git_status" | grep -c '^.M\|^ M')
# Count staged files (non-space in first column, excluding ??)
staged=$(echo "$git_status" | grep -c '^[ADMR]')
# Count conflicts (UU, AA, DD)
conflicts=$(echo "$git_status" | grep -c '^UU\|^AA\|^DD')
else
untracked=0
modified=0
staged=0
conflicts=0
fi
# Debug output
if [[ "$DEBUG" == "1" ]]; then
echo "DEBUG: Git Status Raw:" >&2
echo "$git_status" >&2
echo "DEBUG: Counts - Staged:$staged Modified:$modified Untracked:$untracked Conflicts:$conflicts" >&2
fi
# Check for ahead/behind status
ahead_behind=""
upstream=$(git -C "$current_dir" rev-parse --abbrev-ref '@{u}' 2>/dev/null)
if [[ -n "$upstream" ]]; then
ahead=$(git -C "$current_dir" rev-list --count '@{u}..HEAD' 2>/dev/null)
behind=$(git -C "$current_dir" rev-list --count 'HEAD..@{u}' 2>/dev/null)
if [[ "$ahead" -gt 0 ]] && [[ "$behind" -gt 0 ]]; then
ahead_behind=" ${YELLOW}↕${ahead}/${behind}${RESET}"
elif [[ "$ahead" -gt 0 ]]; then
ahead_behind=" ${GREEN}↑${ahead}${RESET}"
elif [[ "$behind" -gt 0 ]]; then
ahead_behind=" ${YELLOW}↓${behind}${RESET}"
fi
fi
# Check for open PRs using GitHub CLI if available
pr_info=""
if command -v gh >/dev/null 2>&1; then
# Only check for PRs if we're in a GitHub repo
remote_url=$(git -C "$current_dir" config --get remote.origin.url 2>/dev/null)
if [[ "$remote_url" == *"github.com"* ]]; then
# Quick PR check (gh caches this, so it's usually fast after first run)
pr_number=$(gh pr view --json number -q .number 2>/dev/null)
if [[ -n "$pr_number" ]]; then
pr_info=" ${CYAN}PR#${pr_number}${RESET}"
fi
fi
fi
# Get git diff stats for insertions/deletions
insertions=0
deletions=0
if [[ -n "$git_status" ]]; then
# Get diff stats using --numstat for accurate counts
diff_stats=$(git --no-optional-locks -C "$current_dir" diff --numstat 2>/dev/null)
if [[ -n "$diff_stats" ]]; then
insertions=$(echo "$diff_stats" | awk '{sum+=$1} END {print sum+0}')
deletions=$(echo "$diff_stats" | awk '{sum+=$2} END {print sum+0}')
fi
# Also check staged changes
staged_stats=$(git --no-optional-locks -C "$current_dir" diff --cached --numstat 2>/dev/null)
if [[ -n "$staged_stats" ]]; then
staged_insertions=$(echo "$staged_stats" | awk '{sum+=$1} END {print sum+0}')
staged_deletions=$(echo "$staged_stats" | awk '{sum+=$2} END {print sum+0}')
insertions=$((insertions + staged_insertions))
deletions=$((deletions + staged_deletions))
fi
fi
# Build git diff indicator (+X,-Y)
git_diff_indicator=""
if [[ "$insertions" -gt 0 ]] || [[ "$deletions" -gt 0 ]]; then
git_diff_indicator=" ${GRAY}│${RESET} ${GREEN}+${insertions}${RESET},${RED}-${deletions}${RESET}"
fi
# Set git color based on status (without icons)
if [[ "$conflicts" -gt 0 ]]; then
git_color="${RED}"
elif [[ -n "$git_status" ]]; then
git_color="${YELLOW}"
else
git_color="${GREEN}"
fi
# Construct git info string with separate sections (no icon)
git_branch_info="${git_color}${branch}${RESET}${worktree_info}${ahead_behind}${pr_info}"
# Always show branch and git diff indicator if present
git_info=" ${GRAY}│${RESET} ${git_branch_info}${git_diff_indicator}"
fi
fi
# Get Python virtual environment info
venv_info=""
if [[ -n "$VIRTUAL_ENV" ]]; then
venv_name=$(basename "$VIRTUAL_ENV")
venv_info=" ${GRAY}│${RESET} ${CYAN}🐍${venv_name}${RESET}"
fi
# Get Node.js version if in a Node project
node_info=""
if [[ -f "$current_dir/package.json" ]]; then
node_version=$(node --version 2>/dev/null | sed 's/v//')
if [[ -n "$node_version" ]]; then
# Truncate to major.minor version
node_version=${node_version%.*}
node_info=" ${GRAY}│${RESET} ${GREEN}⬢ ${node_version}${RESET}"
fi
fi
# Get current time
current_time="$(date '+%H:%M')"
# Build simple linear output - always show all components
output_string=" ${BOLD}${BLUE}${model_name}${RESET} ${GRAY}│${RESET} ${CYAN}Ctx: ${ctx_display}${RESET} ${GRAY}│${RESET} ${PURPLE}${dir_display}${RESET}"
# Add git info if available
if [[ -n "$git_info" ]]; then
output_string="${output_string}${git_info}"
fi
# Add venv info if available
if [[ -n "$venv_info" ]]; then
output_string="${output_string}${venv_info}"
fi
# Add node info if available
if [[ -n "$node_info" ]]; then
output_string="${output_string}${node_info}"
fi
# Add time
output_string="${output_string} ${GRAY}│${RESET} ${WHITE}${current_time}${RESET} "
# Output the complete string
echo -e "$output_string"
@mtompkins
Copy link

Liked your statusline, thanks for sharing. Added conda so thought I'd share back:

--- <unnamed>
+++ <unnamed>
@@ -25,7 +25,7 @@
 #   ↕N/M Diverged from remote (yellow)
 #   PR#N Open pull request number (cyan)
 # 🌳 Git worktree indicator
-# 🐍 Python virtual environment
+# 🐍 Python environment (venv or conda)
 # ⬢  Node.js version
 # 🐳 Docker environment detected
 # 🕐 Current time
@@ -39,7 +39,7 @@
 readonly YELLOW='\033[93m'    # Bright yellow for modified git status
 readonly RED='\033[91m'       # Bright red for conflicts/errors
 readonly PURPLE='\033[95m'    # Bright purple for directory
-readonly CYAN='\033[96m'      # Bright cyan for python venv
+readonly CYAN='\033[96m'      # Bright cyan for python env
 readonly WHITE='\033[97m'     # Bright white for time
 readonly GRAY='\033[37m' # Gray for separators
 readonly RESET='\033[0m'      # Reset colors
@@ -183,10 +183,10 @@
                 status_parts+=("${GRAY}📄 ${untracked} untracked${RESET}")
             fi
             if [[ "$modified" -gt 0 ]]; then
-                status_parts+=("${YELLOW}📝 ${modified} modified${RESET}")
+                status_parts+=("${YELLOW} 📝 ${modified} modified${RESET}")
             fi
             if [[ "$staged" -gt 0 ]]; then
-                status_parts+=("${GREEN}✅ ${staged} staged${RESET}")
+                status_parts+=("${GREEN} ✅ ${staged} staged${RESET}")
             fi
 
             # Join status parts with commas
@@ -211,16 +211,25 @@
     fi
 fi
 
-# Get Python virtual environment info
-venv_info=""
-if [[ -n "$VIRTUAL_ENV" ]]; then
-    venv_name=$(basename "$VIRTUAL_ENV")
+# === MODIFIED SECTION START ===
+# Get Python environment info (conda or venv)
+py_env_info=""
+# Check for a Conda environment first
+if [[ -n "$CONDA_DEFAULT_ENV" ]]; then
+    py_env_name="$CONDA_DEFAULT_ENV"
+    # CONDA_DEFAULT_ENV holds the name, so no basename is needed
+    py_env_info=" ${GRAY}│${RESET} ${CYAN}🐍 ${py_env_name}${RESET}"
+# Then check for a standard Python venv
+elif [[ -n "$VIRTUAL_ENV" ]]; then
+    # VIRTUAL_ENV holds the path, so we take the basename
+    py_env_name=$(basename "$VIRTUAL_ENV")
     # Smart truncation for long venv names
-    if [[ ${#venv_name} -gt 15 ]]; then
-        venv_name="${venv_name:0:12}..."
-    fi
-    venv_info=" ${GRAY}│${RESET} ${CYAN}🐍${venv_name}${RESET}"
-fi
+    if [[ ${#py_env_name} -gt 15 ]]; then
+        py_env_name="${py_env_name:0:12}..."
+    fi
+    py_env_info=" ${GRAY}│${RESET} ${CYAN}🐍 ${py_env_name}${RESET}"
+fi
+# === MODIFIED SECTION END ===
 
 # Get Node.js version if in a Node project
 node_info=""
@@ -247,16 +256,20 @@
 extra_items=""
 
 # Collect non-empty items
-[[ -n "$venv_info" ]] && extra_items="${extra_items} ${venv_info}"
-[[ -n "$node_info" ]] && extra_items="${extra_items} ${node_info}"
+# === MODIFIED LINE START ===
+[[ -n "$py_env_info" ]] && extra_items="${extra_items}${py_env_info}"
+# === MODIFIED LINE END ===
+[[ -n "$node_info" ]] && extra_items="${extra_items}${node_info}"
 
 # Add extra items with separator if there are any
 if [[ -n "$extra_items" ]]; then
-    output_string="${output_string} ${GRAY}│${RESET}${extra_items}"
-fi
+    # Note: the separator is now part of the item string, so we just append them
+    output_string="${output_string}${extra_items}"
+fi
+
 
 # Always add time at the end
-output_string="${output_string} ${GRAY}│${RESET} ${WHITE}🕐${current_time}${RESET} "
+output_string="${output_string} ${GRAY}│${RESET} ${WHITE}🕐 ${current_time}${RESET} "
 
 # Output the complete string
 echo -e "$output_string"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment