From 8853afffa114db30136308ea53e5322d0ba9fe90 Mon Sep 17 00:00:00 2001 From: Taylor Eernisse Date: Mon, 9 Feb 2026 23:42:58 -0500 Subject: [PATCH] feat: update defaults, schema, and installer for new capabilities Configuration and deployment updates to match the new feature set. defaults.json: - Dark theme palette switched from named ANSI to Dracula-inspired hex: success=#50fa7b, warning=#f1fa8c, danger=#ff5555, accent=#8be9fd, info=#bd93f9. Light theme unchanged (named ANSI with bold). - Glyph characters normalized to Unicode escapes (clean/ahead/behind). - Verbose layout (3 lines) reorganized: Line 1: model, provider, spacer, lines_changed, project, vcs, beads Line 2: context_bar, context_usage, cache_efficiency, spacer, cost, cost_velocity, cost_trend, duration Line 3: context_trend, tokens_raw, spacer, tools, turns, load, cloud_profile, k8s_context, python_env, toolchain - context_usage, cost_trend, context_trend now enabled by default. - Trend widths increased from 8 to 12 characters. - Context bar: bar_style=block, gradient=true. - Tools: show_breakdown=true, top_n=7. - New sections enabled: cloud_profile (P2), k8s_context (P3, ttl=30), python_env (P3), toolchain (P3). schema.json: - Added $schema self-reference field for editor autocomplete. - Expanded colorName from fixed enum to freeform string with docs covering hex, bg:, modifiers, and palette refs. - New section schemas: trendSection, contextTrendSection with width, gradient, and threshold properties. - context_bar: added bar_style, gradient, fill_char, empty_char. - tools: added show_breakdown, top_n, palette array. - All section types: added background and placeholder properties. - Width description updated to clarify it's a max cap + fallback, not an override. - colorMatch additionalProperties relaxed from enum to string. install.sh: Complete rewrite for Rust binary workflow: - Checks cargo and jq prerequisites. - Builds release binary via cargo build --release. - Installs to ~/.local/bin/claude-statusline with PATH check. - Creates/updates ~/.claude/settings.json with statusLine command (type: "command", --color=always, padding: 0). - Symlinks statusline.json config if present in project. - Removed all bash-script-era logic (symlinks, bash version checks). Co-Authored-By: Claude Opus 4.6 --- defaults.json | 71 ++++++++---- install.sh | 130 ++++++++++++++-------- schema.json | 296 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 399 insertions(+), 98 deletions(-) diff --git a/defaults.json b/defaults.json index 02c7742..5bda736 100644 --- a/defaults.json +++ b/defaults.json @@ -2,6 +2,7 @@ "version": 1, "global": { "separator": " | ", + "separator_style": "text", "justify": "space-between", "vcs": "auto", "cache_dir": "/tmp/claude-sl-{session_id}-{cache_version}-{config_hash}", @@ -14,14 +15,14 @@ }, "colors": { "dark": { - "success": "green", - "warning": "yellow", - "danger": "red", - "critical": "red bold", + "success": "#50fa7b", + "warning": "#f1fa8c", + "danger": "#ff5555", + "critical": "#ff5555 bold", "muted": "dim", - "accent": "cyan", + "accent": "#8be9fd", "highlight": "bold", - "info": "blue" + "info": "#bd93f9" }, "light": { "success": "green bold", @@ -41,9 +42,9 @@ "separator_alt": "", "branch": "", "dirty": "*", - "clean": "✓", - "ahead": "↑", - "behind": "↓", + "clean": "\u2713", + "ahead": "\u2191", + "behind": "\u2193", "folder": "", "clock": "", "dollar": "" @@ -93,24 +94,33 @@ [ "model", "provider", + "spacer", + "lines_changed", "project", "vcs", "beads" ], [ "context_bar", - "tokens_raw", + "context_usage", "cache_efficiency", + "spacer", "cost", - "cost_velocity" + "cost_velocity", + "cost_trend", + "duration" ], [ - "lines_changed", - "duration", + "context_trend", + "tokens_raw", + "spacer", "tools", "turns", "load", - "version" + "cloud_profile", + "k8s_context", + "python_env", + "toolchain" ] ] }, @@ -167,6 +177,8 @@ "flex": true, "min_width": 15, "bar_width": 10, + "bar_style": "block", + "gradient": true, "thresholds": { "warn": 50, "danger": 70, @@ -174,7 +186,7 @@ } }, "context_usage": { - "enabled": false, + "enabled": true, "priority": 2, "capacity": 200000, "thresholds": { @@ -210,14 +222,16 @@ "priority": 3 }, "cost_trend": { - "enabled": false, + "enabled": true, "priority": 3, - "width": 8 + "width": 12, + "gradient": true }, "context_trend": { - "enabled": false, + "enabled": true, "priority": 3, - "width": 8, + "width": 12, + "gradient": true, "thresholds": { "warn": 50, "danger": 70, @@ -237,6 +251,8 @@ "priority": 2, "min_width": 6, "show_last_name": true, + "show_breakdown": true, + "top_n": 7, "ttl": 2 }, "turns": { @@ -265,6 +281,23 @@ "hostname": { "enabled": true, "priority": 3 + }, + "cloud_profile": { + "enabled": true, + "priority": 2 + }, + "k8s_context": { + "enabled": true, + "priority": 3, + "ttl": 30 + }, + "python_env": { + "enabled": true, + "priority": 3 + }, + "toolchain": { + "enabled": true, + "priority": 3 } }, "custom": [] diff --git a/install.sh b/install.sh index ca932e4..5baf1db 100755 --- a/install.sh +++ b/install.sh @@ -1,75 +1,109 @@ #!/usr/bin/env bash -# install.sh — Set up claude-statusline symlinks +# install.sh — Build and install claude-statusline (Rust binary) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="$HOME/.local/bin" +BINARY_NAME="claude-statusline" CLAUDE_DIR="$HOME/.claude" +SETTINGS="$CLAUDE_DIR/settings.json" echo "claude-statusline installer" echo "===========================" echo "" -# Check dependencies +# ── Check toolchain ────────────────────────────────────────────────── +if ! command -v cargo &>/dev/null; then + echo "ERROR: cargo (Rust toolchain) is required but not installed." + echo " Install via: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + exit 1 +fi +echo "[ok] cargo found ($(cargo --version))" + if ! command -v jq &>/dev/null; then - echo "ERROR: jq is required but not installed." + echo "ERROR: jq is required for settings.json updates." echo " macOS: brew install jq" echo " Ubuntu: sudo apt install jq" - echo " Fedora: sudo dnf install jq" exit 1 fi echo "[ok] jq found" -# Check bash version -if (( BASH_VERSINFO[0] < 4 )); then - echo "WARNING: bash 4+ recommended (you have ${BASH_VERSION})" - echo " macOS ships bash 3. Install bash 4+:" - echo " brew install bash" +# ── Build release binary ───────────────────────────────────────────── +echo "" +echo "Building release binary..." +(cd "$SCRIPT_DIR" && cargo build --release --quiet) +echo "[ok] Built: $(ls -lh "$SCRIPT_DIR/target/release/$BINARY_NAME" | awk '{print $5}')" + +# ── Install binary ─────────────────────────────────────────────────── +mkdir -p "$INSTALL_DIR" +cp "$SCRIPT_DIR/target/release/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" +chmod +x "$INSTALL_DIR/$BINARY_NAME" +echo "[ok] Installed to $INSTALL_DIR/$BINARY_NAME" + +# Verify it's on PATH +if ! command -v "$BINARY_NAME" &>/dev/null; then + echo "[warn] $INSTALL_DIR is not on your PATH" + echo " Add to your shell config: export PATH=\"$INSTALL_DIR:\$PATH\"" fi -# Ensure ~/.claude exists +# ── Configure Claude Code settings.json ────────────────────────────── +echo "" mkdir -p "$CLAUDE_DIR" -# Create symlinks -create_link() { - local src="$1" dst="$2" name="$3" - if [[ -L "$dst" ]]; then - local existing - existing="$(readlink "$dst")" - if [[ "$existing" == "$src" ]]; then - echo "[ok] $name already linked" - return - fi - echo "[update] $name: updating symlink" - ln -sf "$src" "$dst" - elif [[ -f "$dst" ]]; then - echo "[skip] $name: $dst exists as a regular file" - echo " To use the symlink, rename or remove the existing file first." - return - else - ln -s "$src" "$dst" - echo "[ok] $name linked" +BINARY_PATH="$INSTALL_DIR/$BINARY_NAME" + +# The binary runs in a non-TTY context, so force color on. +STATUSLINE_CMD="$BINARY_PATH --color=always" + +if [[ -f "$SETTINGS" ]]; then + # Update existing settings.json + CURRENT_CMD=$(jq -r '.statusLine.command // empty' "$SETTINGS" 2>/dev/null || true) + if [[ -n "$CURRENT_CMD" ]]; then + echo "[info] Current statusLine command: $CURRENT_CMD" fi -} -create_link "$SCRIPT_DIR/statusline.sh" "$CLAUDE_DIR/statusline.sh" "statusline.sh" - -# Optionally link user config if they want to start from an example -if [[ ! -f "$CLAUDE_DIR/statusline.json" ]]; then - echo "[info] No statusline.json found. You can copy an example from:" - echo " $SCRIPT_DIR/examples/" + # Write updated settings + TMP="$SETTINGS.tmp.$$" + jq --arg cmd "$STATUSLINE_CMD" '.statusLine = {"type": "command", "command": $cmd, "padding": 0}' "$SETTINGS" > "$TMP" + mv "$TMP" "$SETTINGS" + echo "[ok] Updated statusLine in $SETTINGS" +else + # Create minimal settings.json + jq -n --arg cmd "$STATUSLINE_CMD" '{"statusLine": {"type": "command", "command": $cmd, "padding": 0}}' > "$SETTINGS" + echo "[ok] Created $SETTINGS" fi +# ── Symlink config ─────────────────────────────────────────────────── +CONFIG_SRC="$SCRIPT_DIR/statusline.json" +CONFIG_DST="$CLAUDE_DIR/statusline.json" + +if [[ -f "$CONFIG_SRC" ]]; then + if [[ -L "$CONFIG_DST" ]]; then + EXISTING="$(readlink "$CONFIG_DST")" + if [[ "$EXISTING" == "$CONFIG_SRC" ]]; then + echo "[ok] Config already linked" + else + ln -sf "$CONFIG_SRC" "$CONFIG_DST" + echo "[ok] Config symlink updated (was: $EXISTING)" + fi + elif [[ -f "$CONFIG_DST" ]]; then + echo "[skip] $CONFIG_DST exists as a regular file" + echo " To use the symlink, remove it first: rm $CONFIG_DST" + else + ln -s "$CONFIG_SRC" "$CONFIG_DST" + echo "[ok] Config linked: $CONFIG_DST -> $CONFIG_SRC" + fi +else + echo "[info] No statusline.json in project. To customize, create:" + echo " $CONFIG_SRC" + echo "" + echo " Print defaults: $BINARY_NAME --print-defaults" + echo " Print schema: $BINARY_NAME --config-schema" +fi + +# ── Done ───────────────────────────────────────────────────────────── echo "" -echo "Symlinks created. Now add the status line to your Claude Code settings." +echo "Done. Restart Claude Code to see the status line." echo "" -echo "Add this to ~/.claude/settings.json:" -echo "" -echo ' "statusLine": "'$CLAUDE_DIR'/statusline.sh"' -echo "" -echo "If ~/.claude/settings.json doesn't exist yet, create it:" -echo "" -echo ' {' -echo ' "statusLine": "'$CLAUDE_DIR'/statusline.sh"' -echo ' }' -echo "" -echo "Then restart Claude Code to see the status line." +echo "Quick test: $BINARY_NAME --test --color=always" +echo "Debug: $BINARY_NAME --test --dump-state=json" diff --git a/schema.json b/schema.json index b3ad5fc..5dc5464 100644 --- a/schema.json +++ b/schema.json @@ -4,11 +4,12 @@ "title": "Claude Code Status Line Configuration", "description": "Configuration for the claude-statusline status line script", "type": "object", - "required": [ - "version" - ], "additionalProperties": false, "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for editor support" + }, "version": { "type": "integer", "const": 1, @@ -77,6 +78,15 @@ "token_velocity": { "$ref": "#/$defs/basicSection" }, + "cost_trend": { + "$ref": "#/$defs/trendSection" + }, + "context_trend": { + "$ref": "#/$defs/contextTrendSection" + }, + "context_remaining": { + "$ref": "#/$defs/basicSection" + }, "lines_changed": { "$ref": "#/$defs/basicSection" }, @@ -154,9 +164,15 @@ "properties": { "separator": { "type": "string", - "description": "Separator between sections on a line. When justify is 'left', this is used as-is. When justify is 'spread' or 'space-between', the non-space characters (e.g. '|') are kept as a visual anchor and extra space is added around them.", + "description": "Separator text between sections (used when separator_style is 'text'). When justify is 'spread' or 'space-between', the non-space characters are kept as visual anchors.", "default": " | " }, + "separator_style": { + "type": "string", + "enum": ["text", "powerline", "arrow", "none"], + "description": "Separator style between sections. 'text': use the separator string. 'powerline': Nerd Font triangle (auto-falls-back to arrow if glyphs disabled). 'arrow': Unicode heavy angle quotation mark. 'none': spaces only.", + "default": "text" + }, "justify": { "type": "string", "enum": [ @@ -181,7 +197,7 @@ "width": { "type": "integer", "minimum": 40, - "description": "Explicit terminal width override. If omitted, auto-detection walks the process tree to find an ancestor with a real TTY, falling back to stty via /dev/tty, COLUMNS, tput cols, or 120." + "description": "Maximum terminal width cap and fallback. Dynamic sources (ioctl, process tree, stty) take priority when available." }, "width_margin": { "type": "integer", @@ -227,22 +243,11 @@ }, "colorName": { "type": "string", - "enum": [ - "red", - "green", - "yellow", - "blue", - "magenta", - "cyan", - "white", - "dim", - "bold" - ], - "description": "Named ANSI color" + "description": "Color specifier. Supports: named colors (red, green, yellow, blue, magenta, cyan, white), modifiers (dim, bold, italic, underline, strikethrough), hex (#FF6B35, #F00), backgrounds (bg:red, bg:#FF6B35), palette refs (p:success). Multiple can be space-separated (e.g., '#FF6B35 bold')." }, "colorPalette": { "type": "object", - "description": "Semantic color palette for a theme", + "description": "Semantic color palette for a theme. Values support all colorName formats.", "properties": { "success": { "type": "string" }, "warning": { "type": "string" }, @@ -343,6 +348,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -415,6 +428,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -475,6 +496,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -503,6 +532,25 @@ "minimum": 3, "default": 10 }, + "bar_style": { + "type": "string", + "enum": ["classic", "block"], + "default": "block", + "description": "Bar rendering style. 'classic': = and - characters. 'block': Unicode block characters with optional gradient." + }, + "gradient": { + "type": "boolean", + "default": true, + "description": "Enable per-character gradient coloring (green to yellow to red). Only applies when bar_style is 'block'." + }, + "fill_char": { + "type": "string", + "description": "Override character for filled portion. Default: '=' (classic) or full block (block)." + }, + "empty_char": { + "type": "string", + "description": "Override character for empty portion. Default: '-' (classic) or light shade (block)." + }, "thresholds": { "$ref": "#/$defs/thresholds" }, @@ -523,6 +571,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -563,6 +619,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -599,6 +663,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -633,6 +705,135 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." + } + }, + "additionalProperties": false + }, + "trendSection": { + "type": "object", + "description": "Sparkline trend visualization", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "priority": { + "$ref": "#/$defs/priority" + }, + "width": { + "type": "integer", + "minimum": 3, + "default": 8, + "description": "Number of sparkline characters" + }, + "gradient": { + "type": "boolean", + "default": true, + "description": "Enable per-character gradient coloring based on value" + }, + "flex": { + "type": "boolean", + "default": false + }, + "min_width": { + "type": "integer", + "minimum": 0 + }, + "prefix": { + "type": "string" + }, + "suffix": { + "type": "string" + }, + "pad": { + "type": "integer", + "minimum": 1 + }, + "align": { + "type": "string", + "enum": ["left", "right", "center"], + "default": "left" + }, + "color": { + "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." + } + }, + "additionalProperties": false + }, + "contextTrendSection": { + "type": "object", + "description": "Context usage sparkline trend with threshold coloring", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "priority": { + "$ref": "#/$defs/priority" + }, + "width": { + "type": "integer", + "minimum": 3, + "default": 8, + "description": "Number of sparkline characters" + }, + "gradient": { + "type": "boolean", + "default": true, + "description": "Enable per-character gradient coloring based on value" + }, + "thresholds": { + "$ref": "#/$defs/thresholds" + }, + "flex": { + "type": "boolean", + "default": false + }, + "min_width": { + "type": "integer", + "minimum": 0 + }, + "prefix": { + "type": "string" + }, + "suffix": { + "type": "string" + }, + "pad": { + "type": "integer", + "minimum": 1 + }, + "align": { + "type": "string", + "enum": ["left", "right", "center"], + "default": "left" + }, + "color": { + "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -656,6 +857,26 @@ "type": "boolean", "default": true }, + "show_breakdown": { + "type": "boolean", + "description": "Show per-tool breakdown in wide terminals (e.g. Bash: 84/Read: 35/Edit: 34)", + "default": true + }, + "top_n": { + "type": "integer", + "minimum": 0, + "description": "Max number of tools to show in breakdown (0 = all). Adaptively reduced to fit terminal width.", + "default": 7 + }, + "palette": { + "type": "array", + "items": { + "type": "string", + "description": "Hex color string (#RRGGBB or #RGB)" + }, + "description": "Rotating color palette for tool names. Override to customize; leave empty (default) to auto-detect from terminal config (WezTerm/Kitty/Alacritty) or fall back to built-in Dracula palette.", + "default": [] + }, "ttl": { "type": "number", "default": 2 @@ -677,6 +898,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -711,6 +940,14 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false @@ -747,26 +984,23 @@ }, "color": { "$ref": "#/$defs/colorName" + }, + "background": { + "$ref": "#/$defs/colorName", + "description": "Background color for the section. Accepts named colors (green, red), hex (#50fa7b), or palette refs (p:success). Applied as a background wrap around the section output, preserving foreground colors." + }, + "placeholder": { + "type": ["string", "null"], + "description": "Text shown when the section has no data (e.g. after /clear). Set to empty string or null to hide the section instead. Default: '--'." } }, "additionalProperties": false }, "colorMatch": { "type": "object", - "description": "Map of output value to color name", + "description": "Map of output value to color specifier", "additionalProperties": { - "type": "string", - "enum": [ - "red", - "green", - "yellow", - "blue", - "magenta", - "cyan", - "white", - "dim", - "bold" - ] + "type": "string" } }, "customCommand": {