Show HN: Custom statusline for Claude Code with Git/PR/environment info

3 months ago 3
#!/bin/bash # Make sure this file is executable: chmod +x ~/.claude/statusline-command.sh # Claude Code statusline script - optimized with colors and emojis # 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 status icons: # ✓ Clean repository (green) # ⚡ Changes present (yellow) # ⚠ Merge conflicts (red) # +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 # 🐳 Docker environment detected # 🕐 Current time # Debug mode - set STATUSLINE_DEBUG=1 to see raw values DEBUG="${STATUSLINE_DEBUG:-0}" # 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 // "."') # Get current directory relative to home directory with smart truncation 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 # Smart path truncation - show beginning and end for context if [[ ${#dir_display} -gt 50 ]]; then # For very long paths, show important parts if [[ "$dir_display" == "~"/* ]]; then # Keep home indicator and last parts # shellcheck disable=SC2088 # We want literal ~ for display, not expansion dir_display="~/.../$(basename "$(dirname "$current_dir")")/$(basename "$current_dir")" else # Show first and last directory dir_display="...$(echo "$dir_display" | grep -o '/[^/]*/[^/]*$')" fi # If still too long, just show current directory if [[ ${#dir_display} -gt 50 ]]; then dir_display="...$(basename "$current_dir")" fi 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 -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 # Build status indicators status_indicators="" if [[ "$conflicts" -gt 0 ]]; then git_color="${RED}" git_icon="" status_indicators="${RED}⚠️ ${conflicts} conflicts${RESET}" elif [[ -n "$git_status" ]]; then git_color="${YELLOW}" git_icon="" # Build status string with descriptive text status_parts=() if [[ "$untracked" -gt 0 ]]; then status_parts+=("${GRAY}📄 ${untracked} untracked${RESET}") fi if [[ "$modified" -gt 0 ]]; then status_parts+=("${YELLOW}📝 ${modified} modified${RESET}") fi if [[ "$staged" -gt 0 ]]; then status_parts+=("${GREEN}${staged} staged${RESET}") fi # Join status parts with commas if [[ ${#status_parts[@]} -gt 0 ]]; then status_indicators=$(IFS=", "; echo "${status_parts[*]}") fi else git_color="${GREEN}" git_icon="" fi # Construct git info string with separate sections git_branch_info="${git_color}${git_icon} ${branch}${RESET}${worktree_info}${ahead_behind}${pr_info}" if [[ -n "$status_indicators" ]]; then # Has changes - show branch and status in separate sections git_info=" ${GRAY}${RESET} ${git_branch_info} ${GRAY}${RESET} ${status_indicators}" else # Clean repository - just show branch git_info=" ${GRAY}${RESET} ${git_branch_info}" fi fi fi # Get Python virtual environment info venv_info="" if [[ -n "$VIRTUAL_ENV" ]]; then venv_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 # 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 with better formatting current_time="$(date '+%H:%M')" # Build output string with smart separators output_string=" ${BLUE}🤖${RESET} ${BOLD}${BLUE}${model_name}${RESET} ${GRAY}${RESET} ${PURPLE}📁${dir_display}${RESET}" # Add git info if present [[ -n "$git_info" ]] && output_string="${output_string}${git_info}" # Add other components with separators only when needed extra_items="" # Collect non-empty items [[ -n "$venv_info" ]] && extra_items="${extra_items} ${venv_info}" [[ -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 # Always add time at the end output_string="${output_string} ${GRAY}${RESET} ${WHITE}🕐${current_time}${RESET} " # Output the complete string echo -e "$output_string"
Read Entire Article