Files
claude-statusline/statusline.sh
Taylor Eernisse b55d1aefd1 feat: complete Rust port of claude-statusline
Port the entire 2236-line bash statusline script to Rust.
Implements all 25 sections, 3-phase layout engine (render, priority
drop, flex/justify), file-based caching with flock, 9-level terminal
width detection, trend sparklines, and deep-merge JSON config.

Release binary: 864K with LTO. Render time: <1ms warm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 14:21:57 -05:00

2246 lines
69 KiB
Bash
Executable File

#!/usr/bin/env bash
# Simple shell-style status line for Claude Code
set -euo pipefail
# Dependencies check
if ! command -v jq &>/dev/null; then
printf "statusline: jq not found\n" >&2
exit 1
fi
# Read JSON from stdin
INPUT=$(cat)
# ANSI Colors (dimmed for status line)
C_RESET='\033[0m'
C_DIM='\033[2m'
C_GREEN='\033[32m'
C_CYAN='\033[36m'
C_YELLOW='\033[33m'
# Get current directory
CWD=$(echo "$INPUT" | jq -r '.workspace.current_dir // .workspace.project_dir // empty')
if [ -z "$CWD" ]; then
CWD=$(pwd)
fi
# Shorten path: replace home with ~
CWD_DISPLAY="${CWD/#$HOME/\~}"
# Get username and hostname
USER=$(whoami)
HOST=$(hostname -s)
# Detect VCS (prefer jj over git)
VCS_BRANCH=""
VCS_DIRTY=""
if [ -d "$CWD/.jj" ]; then
# Jujutsu
VCS_BRANCH=$(cd "$CWD" && jj log -r @ --no-graph -T 'if(bookmarks, bookmarks.join(","), change_id.shortest(8))' --color=never 2>/dev/null || echo "")
if [ -n "$VCS_BRANCH" ]; then
VCS_DIRTY=$(cd "$CWD" && jj diff --stat --color=never 2>/dev/null | tail -1 || echo "")
if [ -n "$VCS_DIRTY" ] && [[ "$VCS_DIRTY" != *"0 files changed"* ]]; then
VCS_DIRTY="*"
else
VCS_DIRTY=""
fi
fi
elif [ -d "$CWD/.git" ]; then
# Git
VCS_BRANCH=$(cd "$CWD" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
if [ -n "$VCS_BRANCH" ]; then
VCS_STATUS=$(cd "$CWD" && git status --porcelain --untracked-files=no 2>/dev/null || echo "")
if [ -n "$VCS_STATUS" ]; then
VCS_DIRTY="*"
else
VCS_DIRTY=""
fi
fi
fi
# Build prompt
OUTPUT=""
# user@host
OUTPUT+="${C_GREEN}${USER}@${HOST}${C_RESET}"
# :path
OUTPUT+="${C_DIM}:${C_RESET}"
OUTPUT+="${C_CYAN}${CWD_DISPLAY}${C_RESET}"
# (branch*)
if [ -n "$VCS_BRANCH" ]; then
OUTPUT+=" ${C_DIM}(${C_RESET}"
OUTPUT+="${C_GREEN}${VCS_BRANCH}${C_RESET}"
if [ -n "$VCS_DIRTY" ]; then
OUTPUT+="${C_YELLOW}${VCS_DIRTY}${C_RESET}"
fi
OUTPUT+="${C_DIM})${C_RESET}"
fi
printf '%b' "$OUTPUT"
# Mock data for testing config without Claude Code
INPUT_JSON='{
"model": {"id": "claude-opus-4-6-20260101", "display_name": "Opus 4.6"},
"cost": {
"total_cost_usd": 0.42,
"total_duration_ms": 840000,
"total_lines_added": 156,
"total_lines_removed": 23,
"total_tool_uses": 7,
"last_tool_name": "Edit",
"total_turns": 12
},
"context_window": {
"used_percentage": 58.5,
"total_input_tokens": 115000,
"total_output_tokens": 8500,
"context_window_size": 200000,
"current_usage": {
"cache_read_input_tokens": 75000,
"cache_creation_input_tokens": 15000
}
},
"workspace": {"project_dir": "'"$(pwd)"'"},
"version": "1.0.80",
"output_style": {"name": "learning"}
}'
elif [[ "$CLI_MODE" == "dump-state" ]]; then
# Dump-state mode uses minimal mock data to compute state
INPUT_JSON='{"workspace":{"project_dir":"'"$(pwd)"'"}}'
else
INPUT_JSON="$(cat)"
if [[ -z "$INPUT_JSON" ]]; then
exit 0
fi
fi
# ── ANSI Colors ───────────────────────────────────────────────────────────────
C_RESET='\033[0m'
C_BOLD='\033[1m'
C_DIM='\033[2m'
C_RED='\033[31m'
C_GREEN='\033[32m'
C_YELLOW='\033[33m'
C_BLUE='\033[34m'
C_MAGENTA='\033[35m'
C_CYAN='\033[36m'
C_WHITE='\033[37m'
# ── Config Loading ────────────────────────────────────────────────────────────
# SCRIPT_DIR already resolved above
DEFAULTS_PATH="$SCRIPT_DIR/defaults.json"
if [[ -f "$DEFAULTS_PATH" ]]; then
DEFAULTS_JSON="$(cat "$DEFAULTS_PATH")"
else
# Minimal fallback if defaults.json is missing
DEFAULTS_JSON='{"global":{},"presets":{"standard":[["model","project"]]},"layout":"standard","sections":{}}'
fi
# User config contains only overrides — merged on top of defaults
USER_CONFIG_PATH="${CLAUDE_STATUSLINE_CONFIG:-$HOME/.claude/statusline.json}"
if [[ -f "$USER_CONFIG_PATH" ]]; then
USER_CONFIG_JSON="$(cat "$USER_CONFIG_PATH")"
# Deep merge: defaults * user (user wins)
CONFIG_JSON="$(jq -s '.[0] * .[1]' <<< "$DEFAULTS_JSON"$'\n'"$USER_CONFIG_JSON")"
else
CONFIG_JSON="$DEFAULTS_JSON"
fi
# ── Config Preloading ─────────────────────────────────────────────────────────
# Extract all config values in a single jq call for performance.
# Each value is stored in a CFG_* variable. The cfg() function checks these
# preloaded values first, falling back to jq for non-preloaded paths.
#
# Variable naming: .global.separator -> CFG__global__separator
# (dots become double underscores to create valid bash variable names)
# Preload all config values in one jq call
eval "$(jq -r '
# Helper to output shell variable assignment
# Converts path like .global.separator to CFG__global__separator
def assign($name; $val):
"CFG_" + ($name | gsub("\\."; "__")) + "=" + ($val | @sh) + "\n";
# Global settings
assign(".global.separator"; .global.separator // " | ") +
assign(".global.justify"; .global.justify // "left") +
assign(".global.width"; (.global.width // "") | tostring) +
assign(".global.width_margin"; (.global.width_margin // 4) | tostring) +
assign(".global.responsive"; (.global.responsive // true) | tostring) +
assign(".global.theme"; .global.theme // "auto") +
assign(".global.vcs"; .global.vcs // "auto") +
assign(".global.cache_dir"; .global.cache_dir // "/tmp/claude-sl-{session_id}") +
assign(".global.breakpoints.narrow"; (.global.breakpoints.narrow // 60) | tostring) +
assign(".global.breakpoints.medium"; (.global.breakpoints.medium // 100) | tostring) +
# Glyphs
assign(".glyphs.enabled"; (.glyphs.enabled // false) | tostring) +
# Glyph set (Nerd Font icons)
(.glyphs.set // {} | to_entries | map(
assign(".glyphs.set." + .key; .value // "")
) | add // "") +
# Glyph fallbacks (ASCII)
(.glyphs.fallback // {} | to_entries | map(
assign(".glyphs.fallback." + .key; .value // "")
) | add // "") +
# Section enabled flags and priorities for all builtin sections
# Need to capture root context since we are inside a map
(. as $root | ["model", "provider", "project", "vcs", "beads", "context_bar", "context_usage", "tokens_raw",
"cache_efficiency", "cost", "cost_velocity", "token_velocity", "cost_trend", "context_trend",
"lines_changed", "duration", "tools", "turns", "load", "version", "time",
"output_style", "hostname"] | map(. as $section |
assign(".sections." + $section + ".enabled"; ($root.sections[$section].enabled // true) | tostring) +
assign(".sections." + $section + ".priority"; ($root.sections[$section].priority // 2) | tostring) +
assign(".sections." + $section + ".flex"; ($root.sections[$section].flex // false) | tostring)
) | add // "") +
# Section-specific settings
# VCS
assign(".sections.vcs.prefer"; .sections.vcs.prefer // "auto") +
assign(".sections.vcs.show_dirty"; (.sections.vcs.show_dirty // true) | tostring) +
assign(".sections.vcs.show_ahead_behind"; (.sections.vcs.show_ahead_behind // true) | tostring) +
assign(".sections.vcs.ttl.branch"; (.sections.vcs.ttl.branch // 3) | tostring) +
assign(".sections.vcs.ttl.dirty"; (.sections.vcs.ttl.dirty // 5) | tostring) +
assign(".sections.vcs.ttl.ahead_behind"; (.sections.vcs.ttl.ahead_behind // 30) | tostring) +
# Beads
assign(".sections.beads.ttl"; (.sections.beads.ttl // 30) | tostring) +
assign(".sections.beads.show_wip"; (.sections.beads.show_wip // true) | tostring) +
assign(".sections.beads.show_wip_count"; (.sections.beads.show_wip_count // true) | tostring) +
assign(".sections.beads.show_ready_count"; (.sections.beads.show_ready_count // true) | tostring) +
assign(".sections.beads.show_open_count"; (.sections.beads.show_open_count // true) | tostring) +
assign(".sections.beads.show_closed_count"; (.sections.beads.show_closed_count // true) | tostring) +
# Context bar
assign(".sections.context_bar.bar_width"; (.sections.context_bar.bar_width // 10) | tostring) +
assign(".sections.context_bar.thresholds.warn"; (.sections.context_bar.thresholds.warn // 50) | tostring) +
assign(".sections.context_bar.thresholds.danger"; (.sections.context_bar.thresholds.danger // 70) | tostring) +
assign(".sections.context_bar.thresholds.critical"; (.sections.context_bar.thresholds.critical // 85) | tostring) +
# Tokens raw
assign(".sections.tokens_raw.format"; .sections.tokens_raw.format // "{input}in/{output}out") +
# Cost
assign(".sections.cost.thresholds.warn"; (.sections.cost.thresholds.warn // 5.00) | tostring) +
assign(".sections.cost.thresholds.danger"; (.sections.cost.thresholds.danger // 8.00) | tostring) +
assign(".sections.cost.thresholds.critical"; (.sections.cost.thresholds.critical // 10.00) | tostring) +
# Cost trend
assign(".sections.cost_trend.width"; (.sections.cost_trend.width // 8) | tostring) +
# Context trend
assign(".sections.context_trend.width"; (.sections.context_trend.width // 8) | tostring) +
assign(".sections.context_trend.thresholds.warn"; (.sections.context_trend.thresholds.warn // 50) | tostring) +
assign(".sections.context_trend.thresholds.danger"; (.sections.context_trend.thresholds.danger // 70) | tostring) +
assign(".sections.context_trend.thresholds.critical"; (.sections.context_trend.thresholds.critical // 85) | tostring) +
# Tools
assign(".sections.tools.show_last_name"; (.sections.tools.show_last_name // true) | tostring) +
# Load
assign(".sections.load.ttl"; (.sections.load.ttl // 10) | tostring) +
# Time
assign(".sections.time.format"; .sections.time.format // "%H:%M") +
# Truncation settings for sections that support it
# Need to capture root context since we are inside a map
(. as $root | ["project", "vcs"] | map(. as $section |
assign(".sections." + $section + ".truncate.enabled"; ($root.sections[$section].truncate.enabled // false) | tostring) +
assign(".sections." + $section + ".truncate.max"; ($root.sections[$section].truncate.max // 0) | tostring) +
assign(".sections." + $section + ".truncate.style"; $root.sections[$section].truncate.style // "right")
) | add // "") +
# Themed color palettes (dark and light)
# Need to capture root context since we are inside nested maps
(. as $root | ["dark", "light"] | map(. as $theme |
["success", "warning", "danger", "critical", "muted", "accent", "highlight", "info"] | map(. as $color |
assign(".colors." + $theme + "." + $color; $root.colors[$theme][$color] // "")
) | add // ""
) | add // "") +
""
' <<< "$CONFIG_JSON" 2>/dev/null)" || true
# Helper: read config value with default (optimized with preloading)
# First checks preloaded CFG_* variables, falls back to jq for non-preloaded paths.
# Path conversion: .global.separator -> CFG__global__separator
cfg() {
local path="$1" default="${2:-}"
# Convert path to variable name: .global.separator -> CFG__global__separator
local var_name="CFG_${path//./__}"
# Check preloaded value first (fast path)
# Use eval for indirect variable access (bash 3.x compatible)
local val
eval "val=\"\${$var_name:-__CFG_UNSET__}\""
if [[ "$val" != "__CFG_UNSET__" ]]; then
if [[ -z "$val" || "$val" == "null" ]]; then
printf '%s' "$default"
else
printf '%s' "$val"
fi
return
fi
# Fallback to jq for non-preloaded paths (slow path)
val="$(jq -e "$path" <<< "$CONFIG_JSON" 2>/dev/null | jq -r 'tostring' 2>/dev/null)" || true
if [[ -z "$val" || "$val" == "null" ]]; then
printf '%s' "$default"
else
printf '%s' "$val"
fi
}
# Helper: read from stdin JSON
inp() {
local path="$1" default="${2:-}"
local val
val="$(jq -r "$path // empty" <<< "$INPUT_JSON" 2>/dev/null)" || true
if [[ -z "$val" ]]; then
printf '%s' "$default"
else
printf '%s' "$val"
fi
}
# Helper: check if section is enabled (uses preloaded values)
section_enabled() {
local section="$1"
local val
val="$(cfg ".sections.${section}.enabled" "true")"
[[ "$val" == "true" ]]
}
# Helper: get section priority (uses preloaded values)
section_priority() {
local section="$1"
cfg ".sections.${section}.priority" "2"
}
# ── Theme Detection ──────────────────────────────────────────────────────────
# Detects terminal background (light/dark) for color palette selection.
# Detection priority:
# 1. Config override (global.theme = "dark" or "light")
# 2. COLORFGBG env var (set by some terminals, format: "fg;bg")
# 3. Default to dark (most common for developers)
#
# Note: OSC 11 query is complex and unreliable without a TTY, so we use
# simpler heuristics. Users can always override via config.
_detect_theme_impl() {
# 1. Config override
local theme_cfg
theme_cfg="$(cfg '.global.theme' 'auto')"
if [[ "$theme_cfg" != "auto" ]]; then
echo "$theme_cfg"
return
fi
# 2. COLORFGBG env var (set by xterm, rxvt, some others)
# Format: "fg;bg" or "fg;bg;unused" — bg > 8 usually indicates light background
if [[ -n "${COLORFGBG:-}" ]]; then
local bg="${COLORFGBG##*;}"
# Standard ANSI colors 0-7 are dark, 8-15 are bright (light)
# Some terminals use 15 for white background
if [[ "$bg" =~ ^[0-9]+$ ]] && (( bg > 8 && bg < 16 )); then
echo "light"
return
fi
# bg values 0-8 or outside range -> dark
if [[ "$bg" =~ ^[0-9]+$ ]] && (( bg <= 8 )); then
echo "dark"
return
fi
fi
# 3. Default to dark (most common for developers)
echo "dark"
}
# Initialize detected theme
DETECTED_THEME="$(_detect_theme_impl)"
# ── Color System ──────────────────────────────────────────────────────────────
color_by_name() {
local name="$1"
# Check for palette reference (p:colorname)
if [[ "$name" == p:* ]]; then
local palette_key="${name#p:}"
local resolved=""
# Try themed palette first: .colors.{theme}.{key}
if [[ -n "$DETECTED_THEME" ]]; then
resolved="$(cfg ".colors.${DETECTED_THEME}.${palette_key}" "")"
fi
# Fall back to flat palette: .colors.{key} (backwards compatibility)
if [[ -z "$resolved" ]]; then
resolved="$(cfg ".colors.${palette_key}" "")"
fi
if [[ -z "$resolved" ]]; then
printf '%s' "$C_RESET"
return
fi
name="$resolved"
fi
# Handle compound styles (e.g., "red bold")
local result=""
for part in $name; do
case "$part" in
red) result+="$C_RED" ;;
green) result+="$C_GREEN" ;;
yellow) result+="$C_YELLOW" ;;
blue) result+="$C_BLUE" ;;
magenta) result+="$C_MAGENTA" ;;
cyan) result+="$C_CYAN" ;;
white) result+="$C_WHITE" ;;
dim) result+="$C_DIM" ;;
bold) result+="$C_BOLD" ;;
*) ;;
esac
done
if [[ -z "$result" ]]; then
printf '%s' "$C_RESET"
else
printf '%s' "$result"
fi
}
# ── Glyph System ─────────────────────────────────────────────────────────────
# Nerd Font glyphs with automatic ASCII fallback.
# When glyphs.enabled is true, uses fancy Unicode/Powerline symbols.
# When disabled (default), uses plain ASCII characters.
GLYPHS_ENABLED="$(cfg '.glyphs.enabled' 'false')"
# glyph NAME — returns the appropriate glyph or fallback
glyph() {
local name="$1"
if [[ "$GLYPHS_ENABLED" == "true" ]]; then
local val
val="$(cfg ".glyphs.set.${name}" "")"
if [[ -n "$val" ]]; then
printf '%s' "$val"
return
fi
fi
# Fallback (either glyphs disabled or specific glyph not in set)
cfg ".glyphs.fallback.${name}" ""
}
# ── Cache Infrastructure ──────────────────────────────────────────────────────
# Session ID needs to be stable across invocations for trends to accumulate.
# Use project directory hash so same project = same session cache.
_project_dir="$(inp '.workspace.project_dir' "$(pwd)")"
# Sanitize project directory: reject paths with shell metacharacters
# This prevents command injection via malicious workspace.project_dir
sanitize_path() {
local path="$1"
# Reject paths containing shell metacharacters that could enable injection
# Allow: alphanumeric, slash, dash, underscore, dot, space, tilde
if [[ "$path" =~ [^a-zA-Z0-9/_.\-\ ~] ]]; then
# Fall back to current directory if path contains suspicious chars
pwd
else
printf '%s' "$path"
fi
}
_project_dir="$(sanitize_path "$_project_dir")"
SESSION_ID="$(printf '%s' "$_project_dir" | md5 -q 2>/dev/null || printf '%s' "$_project_dir" | md5sum | cut -d' ' -f1)"
SESSION_ID="${SESSION_ID:0:12}" # Truncate to 12 chars
CACHE_DIR_TEMPLATE="$(cfg '.global.cache_dir' '/tmp/claude-sl-{session_id}')"
CACHE_DIR="${CACHE_DIR_TEMPLATE//\{session_id\}/$SESSION_ID}"
# Secure cache directory creation: prevent symlink attacks
if [[ ! -d "$CACHE_DIR" ]]; then
mkdir -p "$CACHE_DIR" 2>/dev/null || true
chmod 700 "$CACHE_DIR" 2>/dev/null || true
fi
# Verify we own the cache directory (defense against pre-created symlinks)
if [[ ! -d "$CACHE_DIR" ]] || [[ ! -O "$CACHE_DIR" ]]; then
# Disable caching if directory is suspicious
CACHE_DIR=""
fi
# get_mtime FILE — portable mtime retrieval
get_mtime() {
local file="$1"
case "$(uname -s)" in
Darwin) stat -f %m "$file" 2>/dev/null ;;
*) stat -c %Y "$file" 2>/dev/null ;;
esac || echo 0
}
# cached_value KEY TTL_SECONDS COMMAND [ARGS...]
# Returns cached value if fresh, otherwise runs command and caches result.
# SECURITY: Commands are executed directly (not via eval) to prevent injection.
cached_value() {
local key="$1" ttl="$2"
shift 2
# If caching is disabled, just run the command
if [[ -z "$CACHE_DIR" ]]; then
"$@" 2>/dev/null || true
return
fi
# Sanitize cache key to prevent path traversal
local safe_key="${key//[^a-zA-Z0-9_-]/_}"
local cache_file="$CACHE_DIR/$safe_key"
if [[ -f "$cache_file" ]]; then
local now mtime age
now="$(date +%s)"
mtime="$(get_mtime "$cache_file")"
age=$(( now - mtime ))
if (( age < ttl )); then
cat "$cache_file"
return 0
fi
fi
local result
# Execute command directly without eval — prevents injection
result="$("$@" 2>/dev/null)" || true
printf '%s' "$result" > "$cache_file"
printf '%s' "$result"
}
# ── Terminal Width ────────────────────────────────────────────────────────────
# Claude Code runs the script without a TTY, so tput/COLUMNS often return wrong
# values. Detection priority:
# 1. Explicit config override (global.width)
# 2. CLAUDE_STATUSLINE_WIDTH env var
# 3. Walk process tree to find ancestor with a real TTY, then stty size on it
# 4. stty via /dev/tty (works on some systems)
# 5. COLUMNS env var
# 6. tput cols
# 7. Fallback: 120
detect_width() {
# 1. Config override
local cfg_width
cfg_width="$(cfg '.global.width' '')"
if [[ -n "$cfg_width" && "$cfg_width" != "0" ]]; then
echo "$cfg_width"
return
fi
# 2. Env var override
if [[ -n "${CLAUDE_STATUSLINE_WIDTH:-}" ]]; then
echo "$CLAUDE_STATUSLINE_WIDTH"
return
fi
# 3. Walk process tree to find ancestor's TTY
# The script runs without a TTY, but the shell that launched Claude Code has one.
local pid=$$
while [[ "$pid" -gt 1 ]]; do
local tty_name
tty_name="$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')" || break
if [[ -n "$tty_name" && "$tty_name" != "??" && "$tty_name" != "-" && "$tty_name" != "" ]]; then
local w
w="$(stty size < "/dev/$tty_name" 2>/dev/null | awk '{print $2}')" || true
if [[ -n "$w" ]] && (( w > 0 )); then
echo "$w"
return
fi
fi
pid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')" || break
done
# 4. stty via /dev/tty
local tty_width
tty_width="$(stty size < /dev/tty 2>/dev/null | awk '{print $2}')" || true
if [[ -n "$tty_width" ]] && (( tty_width > 0 )); then
echo "$tty_width"
return
fi
# 5. COLUMNS env var (often wrong in non-interactive shells)
if [[ -n "${COLUMNS:-}" ]] && (( ${COLUMNS:-0} > 0 )); then
echo "$COLUMNS"
return
fi
# 6. tput cols
local tput_width
tput_width="$(tput cols 2>/dev/null)" || true
if [[ -n "$tput_width" ]] && (( tput_width > 0 )); then
echo "$tput_width"
return
fi
# 7. Fallback
echo 120
}
# The detected PTY width may exceed the actual rendering area due to
# terminal multiplexer borders (Zellij/tmux) or Claude Code UI chrome.
# Configurable via global.width_margin (default: 4).
WIDTH_MARGIN="$(cfg '.global.width_margin' '4')"
TERM_WIDTH=$(( $(detect_width) - WIDTH_MARGIN ))
# ── Responsive Layout Selection ──────────────────────────────────────────────
# Auto-select layout preset based on terminal width when responsive mode is enabled.
# Breakpoints: narrow (<60) -> dense, medium (60-99) -> standard, wide (>=100) -> verbose
RESPONSIVE="$(cfg '.global.responsive' 'true')"
RESPONSIVE_LAYOUT=""
if [[ "$RESPONSIVE" == "true" ]]; then
NARROW_BP="$(cfg '.global.breakpoints.narrow' '60')"
MEDIUM_BP="$(cfg '.global.breakpoints.medium' '100')"
if (( TERM_WIDTH < NARROW_BP )); then
RESPONSIVE_LAYOUT="dense"
elif (( TERM_WIDTH < MEDIUM_BP )); then
RESPONSIVE_LAYOUT="standard"
else
RESPONSIVE_LAYOUT="verbose"
fi
fi
# ── Width Tiers for Progressive Disclosure ───────────────────────────────────
# Sections can show different levels of detail based on available width.
# Uses the same breakpoints as responsive layout selection.
if (( TERM_WIDTH < ${NARROW_BP:-60} )); then
WIDTH_TIER="narrow"
elif (( TERM_WIDTH < ${MEDIUM_BP:-100} )); then
WIDTH_TIER="medium"
else
WIDTH_TIER="wide"
fi
# ── Sparkline Helper ─────────────────────────────────────────────────────────
sparkline() {
local values="$1" # comma-separated: "10,20,30,25,40"
local width="${2:-8}"
local chars="▁▂▃▄▅▆▇█"
# Parse values into array
IFS=',' read -ra vals <<< "$values"
local count=${#vals[@]}
if (( count == 0 )); then
printf '%s' ""
return
fi
# Find min/max
local min=${vals[0]} max=${vals[0]}
local v
for v in "${vals[@]}"; do
(( v < min )) && min=$v
(( v > max )) && max=$v
done
# Handle flat line
if (( max == min )); then
local mid_char="${chars:4:1}"
local result=""
local i
for (( i = 0; i < count && i < width; i++ )); do
result+="$mid_char"
done
printf '%s' "$result"
return
fi
# Map values to sparkline characters
local result=""
local range=$((max - min))
local char_count=8
local i
for (( i = 0; i < count && i < width; i++ )); do
local v=${vals[$i]}
local idx=$(( (v - min) * (char_count - 1) / range ))
result+="${chars:$idx:1}"
done
printf '%s' "$result"
}
# ── Trend Tracking ───────────────────────────────────────────────────────────
# Track a value for trend sparkline
track_trend() {
local key="$1" value="$2" max_points="${3:-8}"
local trend_file="$CACHE_DIR/trend_${key}"
# Read existing values
local existing=""
[[ -f "$trend_file" ]] && existing="$(cat "$trend_file")"
# Append new value
if [[ -n "$existing" ]]; then
existing="${existing},${value}"
else
existing="$value"
fi
# Trim to max points
IFS=',' read -ra vals <<< "$existing"
if (( ${#vals[@]} > max_points )); then
vals=("${vals[@]: -$max_points}")
existing=$(IFS=','; echo "${vals[*]}")
fi
printf '%s' "$existing" > "$trend_file"
printf '%s' "$existing"
}
get_trend() {
local key="$1"
local trend_file="$CACHE_DIR/trend_${key}"
[[ -f "$trend_file" ]] && cat "$trend_file" || echo ""
}
# ── Human-Readable Helpers ────────────────────────────────────────────────────
human_tokens() {
local n="${1:-0}"
if (( n >= 1000000 )); then
printf '%.1fM' "$(echo "scale=1; $n / 1000000" | bc 2>/dev/null || echo "$n")"
elif (( n >= 1000 )); then
printf '%.1fk' "$(echo "scale=1; $n / 1000" | bc 2>/dev/null || echo "$n")"
else
printf '%d' "$n"
fi
}
human_duration() {
local ms="${1:-0}"
local secs=$(( ms / 1000 ))
if (( secs >= 3600 )); then
printf '%dh%dm' $(( secs / 3600 )) $(( (secs % 3600) / 60 ))
elif (( secs >= 60 )); then
printf '%dm' $(( secs / 60 ))
else
printf '%ds' "$secs"
fi
}
# ── Truncation Helpers ───────────────────────────────────────────────────────
truncate_right() {
local s="$1" max="$2" ellipsis="${3:-}"
if (( ${#s} <= max )); then
printf '%s' "$s"
else
printf '%s%s' "${s:0:$((max - 1))}" "$ellipsis"
fi
}
truncate_middle() {
local s="$1" max="$2" ellipsis="${3:-}"
if (( ${#s} <= max )); then
printf '%s' "$s"
else
local half=$(( (max - 1) / 2 ))
printf '%s%s%s' "${s:0:$half}" "$ellipsis" "${s: -$((max - 1 - half))}"
fi
}
truncate_left() {
local s="$1" max="$2" ellipsis="${3:-}"
if (( ${#s} <= max )); then
printf '%s' "$s"
else
printf '%s%s' "$ellipsis" "${s: -$((max - 1))}"
fi
}
# Apply truncation to a section based on its config
# Modifies SEC_RAW and SEC_ANSI globals
apply_truncation() {
local id="$1"
local trunc_enabled trunc_max trunc_style
# Read truncation config (works for both builtin and custom sections)
if is_builtin "$id"; then
trunc_enabled="$(cfg ".sections.${id}.truncate.enabled" "false")"
trunc_max="$(cfg ".sections.${id}.truncate.max" "0")"
trunc_style="$(cfg ".sections.${id}.truncate.style" "right")"
else
local idx
idx="$(jq -r --arg id "$id" '.custom | to_entries[] | select(.value.id == $id) | .key' <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ -n "$idx" ]]; then
trunc_enabled="$(jq -r ".custom[$idx].truncate.enabled // false" <<< "$CONFIG_JSON")"
trunc_max="$(jq -r ".custom[$idx].truncate.max // 0" <<< "$CONFIG_JSON")"
trunc_style="$(jq -r ".custom[$idx].truncate.style // \"right\"" <<< "$CONFIG_JSON")"
else
return
fi
fi
# Skip if truncation not enabled or max is 0
if [[ "$trunc_enabled" != "true" ]]; then
return
fi
if [[ "$trunc_max" == "0" || -z "$trunc_max" ]]; then
return
fi
# Only truncate if content exceeds max
if (( ${#SEC_RAW} > trunc_max )); then
case "$trunc_style" in
middle) SEC_RAW="$(truncate_middle "$SEC_RAW" "$trunc_max")" ;;
left) SEC_RAW="$(truncate_left "$SEC_RAW" "$trunc_max")" ;;
*) SEC_RAW="$(truncate_right "$SEC_RAW" "$trunc_max")" ;;
esac
# Regenerate ANSI version - apply dim styling to truncated content
SEC_ANSI="${C_DIM}${SEC_RAW}${C_RESET}"
fi
}
# ── VCS Detection ─────────────────────────────────────────────────────────────
# PROJECT_DIR is already sanitized via _project_dir above
PROJECT_DIR="$_project_dir"
VCS_PREFER="$(cfg '.sections.vcs.prefer' "$(cfg '.global.vcs' 'auto')")"
detect_vcs() {
local dir="${PROJECT_DIR:-.}"
case "$VCS_PREFER" in
jj)
if [[ -d "$dir/.jj" ]]; then echo "jj"; else echo "none"; fi
;;
git)
if [[ -d "$dir/.git" ]]; then echo "git"; else echo "none"; fi
;;
auto|*)
if [[ -d "$dir/.jj" ]]; then echo "jj"
elif [[ -d "$dir/.git" ]]; then echo "git"
else echo "none"
fi
;;
esac
}
VCS_TYPE="$(detect_vcs)"
# ── VCS Helper Commands ───────────────────────────────────────────────────────
# Safe wrappers that accept sanitized paths and don't require eval.
_git_branch() {
local dir="$1"
git -C "$dir" rev-parse --abbrev-ref HEAD
}
_git_dirty() {
local dir="$1"
git -C "$dir" status --porcelain --untracked-files=no | head -1
}
_git_ahead_behind() {
local dir="$1"
git -C "$dir" rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo '0 0'
}
_jj_branch() {
jj log -r @ --no-graph -T 'if(bookmarks, bookmarks.join(","), change_id.shortest(8))' --color=never
}
_jj_dirty() {
jj diff --stat --color=never | tail -1
}
_system_load() {
if [[ "$(uname)" == "Darwin" ]]; then
sysctl -n vm.loadavg | awk '{print $2}'
else
awk '{print $1}' /proc/loadavg
fi
}
_beads_wip_id() {
br list --status=in_progress --json 2>/dev/null | jq -r 'if type == "array" then .[0].id // empty else empty end'
}
# Get all beads stats in one call (cached)
_beads_stats() {
br status --json 2>/dev/null | jq -r '.summary // empty'
}
# ── Section Renderers ─────────────────────────────────────────────────────────
# Each renderer sets two globals:
# SEC_RAW — plain text (for width calculation)
# SEC_ANSI — ANSI-colored text (for display)
render_model() {
local display_name model_id
display_name="$(inp '.model.display_name' '')"
model_id="$(inp '.model.id' '?')"
# Use model_id for parsing (display_name often equals model_id anyway)
local id_to_parse="$model_id"
# Convert to lowercase for matching (bash 3 compatible)
local id_lower
id_lower="$(printf '%s' "$id_to_parse" | tr '[:upper:]' '[:lower:]')"
# Extract base model name
local base_name=""
case "$id_lower" in
*opus*) base_name="Opus" ;;
*sonnet*) base_name="Sonnet" ;;
*haiku*) base_name="Haiku" ;;
*) base_name="${display_name:-$model_id}" ;;
esac
# Try to extract version number (e.g., "4-5" or "3-5" or "4-6" → "4.5" or "3.5" or "4.6")
local version=""
if [[ "$id_lower" =~ (opus|sonnet|haiku)-([0-9]+)-([0-9]+) ]]; then
version="${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
elif [[ "$id_lower" =~ ([0-9]+)-([0-9]+)-(opus|sonnet|haiku) ]]; then
version="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
fi
local name
if [[ -n "$version" ]]; then
name="${base_name} ${version}"
else
name="$base_name"
fi
SEC_RAW="[$name]"
SEC_ANSI="${C_BOLD}[${name}]${C_RESET}"
}
render_provider() {
local model_id
model_id="$(inp '.model.id' '')"
local provider=""
case "$model_id" in
us.anthropic.*|anthropic.*) provider="Bedrock" ;;
*@[0-9][0-9][0-9][0-9]*) provider="Vertex" ;;
claude-*) provider="Anthropic" ;;
*) provider="" ;;
esac
if [[ -z "$provider" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="$provider"
SEC_ANSI="${C_DIM}${provider}${C_RESET}"
}
render_project() {
local dir
dir="$(inp '.workspace.project_dir' '')"
local name
if [[ -n "$dir" ]]; then
name="$(basename "$dir")"
else
name="$(basename "$(pwd)")"
fi
SEC_RAW="$name"
SEC_ANSI="${C_CYAN}${name}${C_RESET}"
}
render_vcs() {
if [[ "$VCS_TYPE" == "none" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local dir="${PROJECT_DIR:-.}"
local branch="" dirty="" ahead="" behind=""
local show_dirty show_ahead_behind
show_dirty="$(cfg '.sections.vcs.show_dirty' 'true')"
show_ahead_behind="$(cfg '.sections.vcs.show_ahead_behind' 'true')"
if [[ "$VCS_TYPE" == "git" ]]; then
local branch_ttl dirty_ttl ab_ttl
branch_ttl="$(cfg '.sections.vcs.ttl.branch' '3')"
dirty_ttl="$(cfg '.sections.vcs.ttl.dirty' '5')"
ab_ttl="$(cfg '.sections.vcs.ttl.ahead_behind' '30')"
branch="$(cached_value "vcs_branch" "$branch_ttl" _git_branch "$dir")"
if [[ "$show_dirty" == "true" ]]; then
local status_out
status_out="$(cached_value "vcs_dirty" "$dirty_ttl" _git_dirty "$dir")"
[[ -n "$status_out" ]] && dirty="$(glyph dirty)"
fi
# Progressive disclosure: ahead/behind only for medium+ width
if [[ "$show_ahead_behind" == "true" && "$WIDTH_TIER" != "narrow" ]]; then
local ab_raw
ab_raw="$(cached_value "vcs_ab" "$ab_ttl" _git_ahead_behind "$dir")"
ahead="$(echo "$ab_raw" | awk '{print $1}')"
behind="$(echo "$ab_raw" | awk '{print $2}')"
fi
elif [[ "$VCS_TYPE" == "jj" ]]; then
local branch_ttl dirty_ttl
branch_ttl="$(cfg '.sections.vcs.ttl.branch' '3')"
dirty_ttl="$(cfg '.sections.vcs.ttl.dirty' '5')"
branch="$(cached_value "vcs_branch" "$branch_ttl" _jj_branch)"
if [[ "$show_dirty" == "true" ]]; then
local jj_diff
jj_diff="$(cached_value "vcs_dirty" "$dirty_ttl" _jj_dirty)"
[[ -n "$jj_diff" && "$jj_diff" != *"0 files changed"* && "$jj_diff" != "0"* ]] && dirty="$(glyph dirty)"
fi
# jj doesn't have a direct ahead/behind concept vs upstream
ahead="0"
behind="0"
fi
# Build output with glyphs
local branch_glyph
branch_glyph="$(glyph branch)"
local raw="${branch_glyph}${branch}${dirty}"
local ansi="${C_GREEN}${branch_glyph}${branch}${C_RESET}"
[[ -n "$dirty" ]] && ansi="${ansi}${C_YELLOW}${dirty}${C_RESET}"
# Progressive disclosure: show ahead/behind only for medium+ width
if [[ "$show_ahead_behind" == "true" && "$WIDTH_TIER" != "narrow" && ("${ahead:-0}" != "0" || "${behind:-0}" != "0") ]]; then
local ahead_glyph behind_glyph ab_str=""
ahead_glyph="$(glyph ahead)"
behind_glyph="$(glyph behind)"
[[ "${ahead:-0}" != "0" ]] && ab_str="${ahead_glyph}${ahead}"
[[ "${behind:-0}" != "0" ]] && ab_str="${ab_str}${behind_glyph}${behind}"
raw="${raw} ${ab_str}"
ansi="${ansi} ${C_DIM}${ab_str}${C_RESET}"
fi
SEC_RAW="$raw"
SEC_ANSI="$ansi"
}
render_beads() {
local ttl
ttl="$(cfg '.sections.beads.ttl' '30')"
local show_wip show_wip_count show_ready show_open show_closed
show_wip="$(cfg '.sections.beads.show_wip' 'true')"
show_wip_count="$(cfg '.sections.beads.show_wip_count' 'true')"
show_ready="$(cfg '.sections.beads.show_ready_count' 'true')"
show_open="$(cfg '.sections.beads.show_open_count' 'true')"
show_closed="$(cfg '.sections.beads.show_closed_count' 'true')"
if ! command -v br &>/dev/null; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local parts=()
local ansi_parts=()
# Get all stats in one cached call
local stats_json
stats_json="$(cached_value "beads_stats" "$ttl" _beads_stats)"
if [[ -z "$stats_json" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Current WIP bead ID (always show if enabled — most important context)
if [[ "$show_wip" == "true" ]]; then
local wip
wip="$(cached_value "beads_wip_id" "$ttl" _beads_wip_id)"
if [[ -n "$wip" ]]; then
parts+=("$wip wip")
ansi_parts+=("${C_YELLOW}${wip}${C_RESET} ${C_DIM}wip${C_RESET}")
fi
fi
# Ready count (high priority — what's available to work on)
if [[ "$show_ready" == "true" ]]; then
local ready_count
ready_count="$(jq -r '.ready_issues // 0' <<< "$stats_json")"
if [[ "${ready_count:-0}" -gt 0 ]]; then
parts+=("${ready_count} ready")
ansi_parts+=("${C_GREEN}${ready_count}${C_RESET} ${C_DIM}ready${C_RESET}")
fi
fi
# Open count (medium+ width — total backlog)
if [[ "$show_open" == "true" && "$WIDTH_TIER" != "narrow" ]]; then
local open_count
open_count="$(jq -r '.open_issues // 0' <<< "$stats_json")"
if [[ "${open_count:-0}" -gt 0 ]]; then
parts+=("${open_count} open")
ansi_parts+=("${C_BLUE}${open_count}${C_RESET} ${C_DIM}open${C_RESET}")
fi
fi
# In-progress count (medium+ width — shows workload)
if [[ "$show_wip_count" == "true" && "$WIDTH_TIER" != "narrow" ]]; then
local wip_count
wip_count="$(jq -r '.in_progress_issues // 0' <<< "$stats_json")"
if [[ "${wip_count:-0}" -gt 0 ]]; then
parts+=("${wip_count} wip")
ansi_parts+=("${C_YELLOW}${wip_count}${C_RESET} ${C_DIM}wip${C_RESET}")
fi
fi
# Closed count (wide only — shows progress/completion)
if [[ "$show_closed" == "true" && "$WIDTH_TIER" == "wide" ]]; then
local closed_count
closed_count="$(jq -r '.closed_issues // 0' <<< "$stats_json")"
if [[ "${closed_count:-0}" -gt 0 ]]; then
parts+=("${closed_count} done")
ansi_parts+=("${C_DIM}${closed_count} done${C_RESET}")
fi
fi
if [[ ${#parts[@]} -eq 0 ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local IFS=" | "
SEC_RAW="${parts[*]}"
SEC_ANSI="${ansi_parts[*]}"
}
render_context_bar() {
local pct
pct="$(inp '.context_window.used_percentage' '')"
if [[ -z "$pct" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Round to integer
local pct_int
pct_int="$(printf '%.0f' "$pct" 2>/dev/null || echo 0)"
local bar_width
bar_width="$(cfg '.sections.context_bar.bar_width' '10')"
local filled=$(( pct_int * bar_width / 100 ))
local empty=$(( bar_width - filled ))
local bar=""
local i
for (( i = 0; i < filled; i++ )); do bar+="="; done
for (( i = 0; i < empty; i++ )); do bar+="-"; done
SEC_RAW="[${bar}] ${pct_int}%"
# Color by threshold
local warn danger critical
warn="$(cfg '.sections.context_bar.thresholds.warn' '50')"
danger="$(cfg '.sections.context_bar.thresholds.danger' '70')"
critical="$(cfg '.sections.context_bar.thresholds.critical' '85')"
local color="$C_GREEN"
if (( pct_int >= critical )); then
color="${C_RED}${C_BOLD}"
elif (( pct_int >= danger )); then
color="$C_RED"
elif (( pct_int >= warn )); then
color="$C_YELLOW"
fi
SEC_ANSI="${color}[${bar}] ${pct_int}%${C_RESET}"
}
render_context_usage() {
# Shows context usage as "154k / 200k" (current context used / total capacity)
# NOTE: total_input/output_tokens are CUMULATIVE session totals, not current context!
# We must calculate current usage from: used_percentage * context_window_size / 100
local pct context_size
pct="$(inp '.context_window.used_percentage' '')"
context_size="$(inp '.context_window.context_window_size' '')"
if [[ -z "$pct" || -z "$context_size" || "$context_size" == "0" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Calculate current used tokens from percentage
local used
used="$(awk "BEGIN{printf \"%.0f\", $pct * $context_size / 100}" 2>/dev/null || echo "0")"
if (( used == 0 )); then
SEC_RAW=""
SEC_ANSI=""
return
fi
local total="$context_size"
local used_h total_h
used_h="$(human_tokens "$used")"
total_h="$(human_tokens "$total")"
SEC_RAW="${used_h}/${total_h}"
# Color by percentage threshold
local pct_int=0
if [[ -n "$pct" ]]; then
pct_int="$(printf '%.0f' "$pct" 2>/dev/null || echo 0)"
fi
local warn danger critical
warn="$(cfg '.sections.context_usage.thresholds.warn' '50')"
danger="$(cfg '.sections.context_usage.thresholds.danger' '70')"
critical="$(cfg '.sections.context_usage.thresholds.critical' '85')"
local color="$C_GREEN"
if (( pct_int >= critical )); then
color="${C_RED}${C_BOLD}"
elif (( pct_int >= danger )); then
color="$C_RED"
elif (( pct_int >= warn )); then
color="$C_YELLOW"
fi
SEC_ANSI="${color}${used_h}${C_RESET}${C_DIM}/${total_h}${C_RESET}"
}
render_tokens_raw() {
local input output
input="$(inp '.context_window.total_input_tokens' '0')"
output="$(inp '.context_window.total_output_tokens' '0')"
# Progressive disclosure: more detail at wider widths
# narrow: "115k/8k" (compact, no labels)
# medium: "115k in/8k out" (with labels)
# wide: "115.2k in / 8.5k out" (decimal precision, spaced)
local input_h output_h raw
case "$WIDTH_TIER" in
narrow)
# Compact format: integer k/M values, no labels
input_h="$(human_tokens "$input")"
output_h="$(human_tokens "$output")"
raw="${input_h}/${output_h}"
;;
medium)
# Standard format: k/M values with labels
input_h="$(human_tokens "$input")"
output_h="$(human_tokens "$output")"
local fmt
fmt="$(cfg '.sections.tokens_raw.format' '{input}in/{output}out')"
raw="${fmt//\{input\}/$input_h}"
raw="${raw//\{output\}/$output_h}"
;;
wide)
# Verbose format: more precision, spaced separators
# Use one decimal place for better precision
if (( input >= 1000000 )); then
input_h="$(printf '%.1fM' "$(echo "scale=2; $input / 1000000" | bc 2>/dev/null || echo "$input")")"
elif (( input >= 1000 )); then
input_h="$(printf '%.1fk' "$(echo "scale=2; $input / 1000" | bc 2>/dev/null || echo "$input")")"
else
input_h="$input"
fi
if (( output >= 1000000 )); then
output_h="$(printf '%.1fM' "$(echo "scale=2; $output / 1000000" | bc 2>/dev/null || echo "$output")")"
elif (( output >= 1000 )); then
output_h="$(printf '%.1fk' "$(echo "scale=2; $output / 1000" | bc 2>/dev/null || echo "$output")")"
else
output_h="$output"
fi
raw="${input_h} in / ${output_h} out"
;;
esac
SEC_RAW="$raw"
SEC_ANSI="${C_DIM}${raw}${C_RESET}"
}
render_cache_efficiency() {
local cache_read cache_creation
cache_read="$(inp '.context_window.current_usage.cache_read_input_tokens' '0')"
cache_creation="$(inp '.context_window.current_usage.cache_creation_input_tokens' '0')"
local total=$(( cache_read + cache_creation ))
if (( total == 0 )); then
SEC_RAW="cache:0%"
SEC_ANSI="${C_DIM}cache:0%${C_RESET}"
return
fi
local pct=$(( cache_read * 100 / total ))
SEC_RAW="cache:${pct}%"
local color="$C_DIM"
(( pct >= 50 )) && color="$C_GREEN"
(( pct >= 80 )) && color="${C_GREEN}${C_BOLD}"
SEC_ANSI="${color}cache:${pct}%${C_RESET}"
}
render_cost() {
local cost_raw
cost_raw="$(inp '.cost.total_cost_usd' '')"
if [[ -z "$cost_raw" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Progressive disclosure: decimal precision based on width tier
# narrow: "$0" (no decimals, rounds to nearest dollar)
# medium: "$0.25" (2 decimals, standard)
# wide: "$0.2547" (4 decimals, precise)
local decimals=2
case "$WIDTH_TIER" in
narrow) decimals=0 ;;
medium) decimals=2 ;;
wide) decimals=4 ;;
esac
local cost
cost="$(printf "%.${decimals}f" "$cost_raw" 2>/dev/null || echo "$cost_raw")"
SEC_RAW="\$${cost}"
local warn danger critical
warn="$(cfg '.sections.cost.thresholds.warn' '5.00')"
danger="$(cfg '.sections.cost.thresholds.danger' '8.00')"
critical="$(cfg '.sections.cost.thresholds.critical' '10.00')"
# Compare using awk (cost_raw retains full precision for comparison)
local color="$C_GREEN"
if awk "BEGIN{exit(!($cost_raw >= $critical))}"; then
color="${C_RED}${C_BOLD}"
elif awk "BEGIN{exit(!($cost_raw >= $danger))}"; then
color="$C_RED"
elif awk "BEGIN{exit(!($cost_raw >= $warn))}"; then
color="$C_YELLOW"
fi
SEC_ANSI="${color}\$${cost}${C_RESET}"
}
render_cost_velocity() {
local cost duration_ms
cost="$(inp '.cost.total_cost_usd' '0')"
duration_ms="$(inp '.cost.total_duration_ms' '0')"
if [[ "$duration_ms" == "0" || -z "$duration_ms" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local velocity
velocity="$(awk "BEGIN{printf \"%.2f\", $cost / ($duration_ms / 60000)}" 2>/dev/null || echo "0.00")"
SEC_RAW="\$${velocity}/m"
SEC_ANSI="${C_DIM}\$${velocity}/m${C_RESET}"
}
render_token_velocity() {
local input output duration_ms
input="$(inp '.context_window.total_input_tokens' '0')"
output="$(inp '.context_window.total_output_tokens' '0')"
duration_ms="$(inp '.cost.total_duration_ms' '0')"
if [[ "$duration_ms" == "0" || -z "$duration_ms" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local total=$(( input + output ))
local velocity
velocity="$(awk "BEGIN{printf \"%.1f\", $total / ($duration_ms / 60000)}" 2>/dev/null || echo "0.0")"
# Progressive disclosure: abbreviate based on width tier
local suffix="tok/m"
if [[ "$WIDTH_TIER" == "narrow" ]]; then
suffix="t/m"
fi
# Human-readable for large values
if awk "BEGIN{exit !($velocity >= 1000)}" 2>/dev/null; then
velocity="$(awk "BEGIN{printf \"%.1f\", $velocity / 1000}" 2>/dev/null)k"
fi
SEC_RAW="${velocity}${suffix}"
SEC_ANSI="${C_DIM}${velocity}${suffix}${C_RESET}"
}
render_lines_changed() {
local added removed
added="$(inp '.cost.total_lines_added' '0')"
removed="$(inp '.cost.total_lines_removed' '0')"
if [[ "$added" == "0" && "$removed" == "0" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="+${added} -${removed}"
SEC_ANSI="${C_GREEN}+${added}${C_RESET} ${C_RED}-${removed}${C_RESET}"
}
render_duration() {
local ms
ms="$(inp '.cost.total_duration_ms' '')"
if [[ -z "$ms" || "$ms" == "0" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local human
human="$(human_duration "$ms")"
SEC_RAW="$human"
SEC_ANSI="${C_DIM}${human}${C_RESET}"
}
render_tools() {
local tool_count
tool_count="$(inp '.cost.total_tool_uses' '0')"
if [[ "$tool_count" == "0" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Progressive disclosure: detail based on width tier
# narrow: "42" (just the count)
# medium: "42 tools" (count with label)
# wide: "42 tools (Read)" (count, label, and last tool name)
local raw ansi
case "$WIDTH_TIER" in
narrow)
raw="$tool_count"
ansi="${C_DIM}${tool_count}${C_RESET}"
;;
medium)
raw="${tool_count} tools"
ansi="${C_DIM}${tool_count} tools${C_RESET}"
;;
wide)
raw="${tool_count} tools"
ansi="${C_DIM}${tool_count} tools${C_RESET}"
local show_last_name
show_last_name="$(cfg '.sections.tools.show_last_name' 'true')"
if [[ "$show_last_name" == "true" ]]; then
local last_tool
last_tool="$(inp '.cost.last_tool_name' '')"
if [[ -n "$last_tool" ]]; then
# Shorten tool name if too long
if (( ${#last_tool} > 12 )); then
last_tool="${last_tool:0:11}~"
fi
raw="${raw} (${last_tool})"
ansi="${ansi} ${C_DIM}(${last_tool})${C_RESET}"
fi
fi
;;
esac
SEC_RAW="$raw"
SEC_ANSI="$ansi"
}
render_turns() {
local turns
turns="$(inp '.cost.total_turns' '')"
if [[ -z "$turns" || "$turns" == "0" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="${turns} turns"
SEC_ANSI="${C_DIM}${turns} turns${C_RESET}"
}
render_load() {
local ttl
ttl="$(cfg '.sections.load.ttl' '10')"
local load_val
load_val="$(cached_value "load" "$ttl" _system_load)"
SEC_RAW="load:${load_val}"
SEC_ANSI="${C_DIM}load:${load_val}${C_RESET}"
}
render_version() {
local ver
ver="$(inp '.version' '')"
if [[ -z "$ver" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="v${ver}"
SEC_ANSI="${C_DIM}v${ver}${C_RESET}"
}
render_time() {
local fmt
fmt="$(cfg '.sections.time.format' '%H:%M')"
local t
t="$(date +"$fmt")"
SEC_RAW="$t"
SEC_ANSI="${C_DIM}${t}${C_RESET}"
}
render_output_style() {
local style
style="$(inp '.output_style.name' '')"
if [[ -z "$style" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="$style"
SEC_ANSI="${C_MAGENTA}${style}${C_RESET}"
}
render_hostname() {
local h
h="$(hostname -s 2>/dev/null || echo "?")"
SEC_RAW="$h"
SEC_ANSI="${C_DIM}${h}${C_RESET}"
}
render_cost_trend() {
local cost
cost="$(inp '.cost.total_cost_usd' '')"
if [[ -z "$cost" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Convert to cents for integer sparkline (avoids floating point issues)
local cents
cents="$(awk "BEGIN{printf \"%.0f\", $cost * 100}" 2>/dev/null || echo "0")"
# Get sparkline width from config
local width
width="$(cfg '.sections.cost_trend.width' '8')"
local trend
trend="$(track_trend "cost" "$cents" "$width")"
local spark
spark="$(sparkline "$trend" "$width")"
if [[ -z "$spark" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="$spark"
SEC_ANSI="${C_DIM}$spark${C_RESET}"
}
render_context_trend() {
local pct
pct="$(inp '.context_window.used_percentage' '')"
if [[ -z "$pct" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Round to integer
local pct_int
pct_int="$(printf '%.0f' "$pct" 2>/dev/null || echo 0)"
# Get sparkline width from config
local width
width="$(cfg '.sections.context_trend.width' '8')"
local trend
trend="$(track_trend "context" "$pct_int" "$width")"
local spark
spark="$(sparkline "$trend" "$width")"
if [[ -z "$spark" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
# Color based on current percentage
local warn danger critical
warn="$(cfg '.sections.context_trend.thresholds.warn' '50')"
danger="$(cfg '.sections.context_trend.thresholds.danger' '70')"
critical="$(cfg '.sections.context_trend.thresholds.critical' '85')"
local color="$C_DIM"
if (( pct_int >= critical )); then
color="${C_RED}${C_BOLD}"
elif (( pct_int >= danger )); then
color="$C_RED"
elif (( pct_int >= warn )); then
color="$C_YELLOW"
fi
SEC_RAW="$spark"
SEC_ANSI="${color}$spark${C_RESET}"
}
# ── Custom Command Renderer ──────────────────────────────────────────────────
# Helper for custom commands — uses bash -c since commands are shell strings
# Custom commands come from user config (trusted source), not stdin
_run_custom_cmd() {
local cmd="$1"
bash -c "$cmd"
}
render_custom() {
local section_id="$1"
local idx
idx="$(jq -r --arg id "$section_id" '
.custom | to_entries[] | select(.value.id == $id) | .key
' <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ -z "$idx" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
local cmd label ttl_val
cmd="$(jq -r ".custom[$idx].command" <<< "$CONFIG_JSON")"
label="$(jq -r ".custom[$idx].label // .custom[$idx].id" <<< "$CONFIG_JSON")"
ttl_val="$(jq -r ".custom[$idx].ttl // 30" <<< "$CONFIG_JSON")"
# Cache key sanitization is handled by cached_value()
local result
result="$(cached_value "custom_${section_id}" "$ttl_val" _run_custom_cmd "$cmd")"
if [[ -z "$result" ]]; then
SEC_RAW=""
SEC_ANSI=""
return
fi
SEC_RAW="${label}:${result}"
# Color matching
local color_name
color_name="$(jq -r --arg val "$result" ".custom[$idx].color.match[\$val] // empty" <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ -n "$color_name" ]]; then
local c
c="$(color_by_name "$color_name")"
SEC_ANSI="${c}${label}:${result}${C_RESET}"
else
SEC_ANSI="${C_DIM}${label}:${result}${C_RESET}"
fi
}
# ── Section Dispatch ──────────────────────────────────────────────────────────
BUILTIN_SECTIONS="model provider project vcs beads context_bar context_usage tokens_raw cache_efficiency cost cost_velocity token_velocity cost_trend context_trend lines_changed duration tools turns load version time output_style hostname"
is_builtin() {
local id="$1"
[[ " $BUILTIN_SECTIONS " == *" $id "* ]]
}
is_spacer() {
local id="$1"
[[ "$id" == "spacer" || "$id" == _spacer* ]]
}
render_section() {
local id="$1"
SEC_RAW=""
SEC_ANSI=""
if is_spacer "$id"; then
SEC_RAW=" "
SEC_ANSI=" "
return
fi
if is_builtin "$id"; then
if ! section_enabled "$id"; then
return
fi
"render_${id}"
else
# Try custom command
render_custom "$id"
fi
# Apply truncation and formatting if section produced output
if [[ -n "$SEC_RAW" ]]; then
apply_truncation "$id"
apply_formatting "$id"
fi
}
# ── Per-Section Formatting ─────────────────────────────────────────────────
# Applies prefix, suffix, color override, and pad+align to any section.
# Uses a single jq call to batch-read all formatting properties.
apply_formatting() {
local id="$1"
# Single jq call using @sh to produce shell-safe variable assignments.
# Tab-split (IFS=$'\t' read) doesn't work: tab is whitespace in POSIX,
# so consecutive tabs collapse and leading empty fields are swallowed.
local prefix="" suffix="" pad="" align="" color_name=""
if is_builtin "$id"; then
eval "$(jq -r '
.sections["'"$id"'"] as $s |
"prefix=" + (@sh "\($s.prefix // "")") +
" suffix=" + (@sh "\($s.suffix // "")") +
" pad=" + (@sh "\(($s.pad // "") | tostring)") +
" align=" + (@sh "\($s.align // "")") +
" color_name=" + (@sh "\($s.color // "")")
' <<< "$CONFIG_JSON" 2>/dev/null)" || true
else
# Custom commands use default_color to avoid conflict with color.match
eval "$(jq -r --arg id "$id" '
(.custom | to_entries[] | select(.value.id == $id) | .value) as $sec |
"prefix=" + (@sh "\($sec.prefix // "")") +
" suffix=" + (@sh "\($sec.suffix // "")") +
" pad=" + (@sh "\(($sec.pad // "") | tostring)") +
" align=" + (@sh "\($sec.align // "")") +
" color_name=" + (@sh "\($sec.default_color // "")")
' <<< "$CONFIG_JSON" 2>/dev/null)" || true
fi
# If nothing to do, return early
if [[ -z "$prefix" && -z "$suffix" && -z "$pad" && -z "$color_name" ]]; then
return
fi
# 1. Apply prefix/suffix
if [[ -n "$prefix" ]]; then
SEC_RAW="${prefix}${SEC_RAW}"
SEC_ANSI="${prefix}${SEC_ANSI}"
fi
if [[ -n "$suffix" ]]; then
SEC_RAW="${SEC_RAW}${suffix}"
SEC_ANSI="${SEC_ANSI}${suffix}"
fi
# 2. Apply color override (re-wrap entire content)
if [[ -n "$color_name" ]]; then
local c
c="$(color_by_name "$color_name")"
SEC_ANSI="${c}${SEC_RAW}${C_RESET}"
fi
# 3. Apply pad + align (spaces stay uncolored)
if [[ -n "$pad" ]] && (( pad > ${#SEC_RAW} )); then
local pad_needed=$(( pad - ${#SEC_RAW} ))
local pad_str=""
local p
for (( p = 0; p < pad_needed; p++ )); do pad_str+=" "; done
case "${align:-left}" in
right)
SEC_RAW="${pad_str}${SEC_RAW}"
SEC_ANSI="${pad_str}${SEC_ANSI}"
;;
center)
local left_pad=$(( pad_needed / 2 ))
local right_pad=$(( pad_needed - left_pad ))
local lpad="" rpad=""
for (( p = 0; p < left_pad; p++ )); do lpad+=" "; done
for (( p = 0; p < right_pad; p++ )); do rpad+=" "; done
SEC_RAW="${lpad}${SEC_RAW}${rpad}"
SEC_ANSI="${lpad}${SEC_ANSI}${rpad}"
;;
*) # left (default)
SEC_RAW="${SEC_RAW}${pad_str}"
SEC_ANSI="${SEC_ANSI}${pad_str}"
;;
esac
fi
}
get_section_priority() {
local id="$1"
if is_spacer "$id"; then
echo "1"
elif is_builtin "$id"; then
section_priority "$id"
else
local idx
idx="$(jq -r --arg id "$id" '.custom | to_entries[] | select(.value.id == $id) | .key' <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ -n "$idx" ]]; then
jq -r ".custom[$idx].priority // 2" <<< "$CONFIG_JSON"
else
echo "2"
fi
fi
}
is_flex_section() {
local id="$1"
if is_spacer "$id"; then
return 0
elif is_builtin "$id"; then
local val
val="$(cfg ".sections.${id}.flex" "false")"
[[ "$val" == "true" ]]
else
local idx
idx="$(jq -r --arg id "$id" '.custom | to_entries[] | select(.value.id == $id) | .key' <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ -n "$idx" ]]; then
local val
val="$(jq -r ".custom[$idx].flex // false" <<< "$CONFIG_JSON")"
[[ "$val" == "true" ]]
else
return 1
fi
fi
}
# ── Layout Resolution ─────────────────────────────────────────────────────────
resolve_layout() {
local layout_val
layout_val="$(jq -r '.layout' <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ -z "$layout_val" || "$layout_val" == "null" ]]; then
layout_val="standard"
fi
# Check if it's a string (preset name) or array
local layout_type
layout_type="$(jq -r '.layout | type' <<< "$CONFIG_JSON" 2>/dev/null)" || true
if [[ "$layout_type" == "string" ]]; then
# When responsive mode is enabled and layout is a preset name (not explicit array),
# use the responsive layout instead of the configured preset
local effective_layout="$layout_val"
if [[ -n "$RESPONSIVE_LAYOUT" ]]; then
effective_layout="$RESPONSIVE_LAYOUT"
fi
# Look up preset
jq -c ".presets[\"$effective_layout\"] // [[\"model\",\"project\"]]" <<< "$CONFIG_JSON"
else
# Explicit layout array provided — use it directly (ignore responsive)
jq -c '.layout' <<< "$CONFIG_JSON"
fi
}
# ── Layout Engine ─────────────────────────────────────────────────────────────
SEPARATOR="$(cfg '.global.separator' ' | ')"
SEP_LEN="${#SEPARATOR}"
JUSTIFY="$(cfg '.global.justify' 'left')"
# Extract the visible "core" of the separator (non-space chars, e.g. "|" from " | ")
# Used as the anchor when building justified gaps
SEP_CORE="${SEPARATOR#"${SEPARATOR%%[! ]*}"}" # trim leading spaces
SEP_CORE="${SEP_CORE%"${SEP_CORE##*[! ]}"}" # trim trailing spaces
SEP_CORE_LEN="${#SEP_CORE}"
# If separator is pure spaces, core is empty — gaps will be pure space
if [[ -z "$SEP_CORE" ]]; then
SEP_CORE_LEN=0
fi
# ── Dump State Mode ───────────────────────────────────────────────────────────
# Output all computed internal state as JSON for debugging
if [[ "$CLI_MODE" == "dump-state" ]]; then
jq -n \
--argjson term_width "$TERM_WIDTH" \
--arg width_tier "$WIDTH_TIER" \
--arg detected_theme "$DETECTED_THEME" \
--arg vcs_type "$VCS_TYPE" \
--arg responsive_layout "${RESPONSIVE_LAYOUT:-}" \
--arg responsive_enabled "$RESPONSIVE" \
--arg justify "$JUSTIFY" \
--arg separator "$SEPARATOR" \
--arg project_dir "$PROJECT_DIR" \
--arg session_id "$SESSION_ID" \
--arg cache_dir "$CACHE_DIR" \
--arg config_path "$USER_CONFIG_PATH" \
--arg defaults_path "$DEFAULTS_PATH" \
--argjson width_margin "$WIDTH_MARGIN" \
--argjson narrow_bp "${NARROW_BP:-60}" \
--argjson medium_bp "${MEDIUM_BP:-100}" \
--arg glyphs_enabled "$GLYPHS_ENABLED" \
'{
terminal: {
detected_width: ($term_width + $width_margin),
effective_width: $term_width,
width_margin: $width_margin,
width_tier: $width_tier
},
responsive: {
enabled: ($responsive_enabled == "true"),
layout: (if $responsive_layout == "" then null else $responsive_layout end),
breakpoints: {narrow: $narrow_bp, medium: $medium_bp}
},
theme: $detected_theme,
vcs: $vcs_type,
layout: {
justify: $justify,
separator: $separator
},
glyphs_enabled: ($glyphs_enabled == "true"),
paths: {
project_dir: $project_dir,
config: $config_path,
defaults: $defaults_path,
cache_dir: $cache_dir
},
session_id: $session_id
}'
exit 0
fi
render_line() {
local line_json="$1"
local section_ids=()
local raw_texts=()
local ansi_texts=()
local priorities=()
local flex_idx=-1
# Parse section IDs from JSON array
local count
count="$(jq 'length' <<< "$line_json")"
local i
for (( i = 0; i < count; i++ )); do
local sid
sid="$(jq -r ".[$i]" <<< "$line_json")"
render_section "$sid"
if [[ -z "$SEC_RAW" ]]; then
continue
fi
section_ids+=("$sid")
raw_texts+=("$SEC_RAW")
ansi_texts+=("$SEC_ANSI")
priorities+=("$(get_section_priority "$sid")")
if is_flex_section "$sid"; then
if (( flex_idx == -1 )); then
# First flex section found
flex_idx=$(( ${#section_ids[@]} - 1 ))
elif is_spacer "$sid" && ! is_spacer "${section_ids[$flex_idx]}"; then
# Spacer overrides a non-spacer flex section, but not another spacer
flex_idx=$(( ${#section_ids[@]} - 1 ))
fi
fi
done
local n=${#section_ids[@]}
if (( n == 0 )); then
return
fi
# If the only sections are spacers, skip the line
local non_spacer=0
for (( i = 0; i < n; i++ )); do
if ! is_spacer "${section_ids[$i]}"; then
non_spacer=1
break
fi
done
if (( non_spacer == 0 )); then
return
fi
# Calculate total width using minimum separators
# Skips separator width when either adjacent section is a spacer
calc_width() {
local total=0
local active=("$@")
local prev_idx=-1
local idx
for idx in "${active[@]}"; do
if (( prev_idx >= 0 )); then
if ! is_spacer "${section_ids[$prev_idx]}" && ! is_spacer "${section_ids[$idx]}"; then
total=$(( total + SEP_LEN ))
fi
fi
total=$(( total + ${#raw_texts[$idx]} ))
prev_idx=$idx
done
echo "$total"
}
# Calculate content-only width (no separators)
calc_content_width() {
local total=0
local active=("$@")
local idx
for idx in "${active[@]}"; do
total=$(( total + ${#raw_texts[$idx]} ))
done
echo "$total"
}
# Build active indices
local active_indices=()
for (( i = 0; i < n; i++ )); do
active_indices+=("$i")
done
local total_width
total_width="$(calc_width "${active_indices[@]}")"
# Priority drop: remove priority 3 from right
if (( total_width > TERM_WIDTH )); then
local new_active=()
for idx in "${active_indices[@]}"; do
if [[ "${priorities[$idx]}" != "3" ]]; then
new_active+=("$idx")
fi
done
active_indices=("${new_active[@]}")
total_width="$(calc_width "${active_indices[@]}")"
fi
# Priority drop: remove priority 2 from right
if (( total_width > TERM_WIDTH )); then
local new_active=()
for idx in "${active_indices[@]}"; do
if [[ "${priorities[$idx]}" != "2" ]]; then
new_active+=("$idx")
fi
done
active_indices=("${new_active[@]}")
total_width="$(calc_width "${active_indices[@]}")"
fi
local active_count=${#active_indices[@]}
# ── Justify: spread / space-between ──────────────────────────────────────
# Distributes extra space into gaps between sections.
# "spread" — equal gaps everywhere (like CSS space-evenly)
# "space-between" — first/last flush to edges, equal gaps in between
# When justify is active, flex on individual sections is ignored.
# Check if any active section is a spacer — if so, bypass justify
local has_spacer=0
for idx in "${active_indices[@]}"; do
if is_spacer "${section_ids[$idx]}"; then
has_spacer=1
break
fi
done
if [[ "$JUSTIFY" != "left" ]] && (( has_spacer == 0 && active_count > 1 && total_width < TERM_WIDTH )); then
local content_width
content_width="$(calc_content_width "${active_indices[@]}")"
local num_gaps=$(( active_count - 1 ))
local available=$(( TERM_WIDTH - content_width ))
# For space-between: all available space goes into the gaps
# For spread: conceptually N+1 slots, but since we can't pad before first
# or after last in a terminal line, we treat it as N gaps with equal size
# (effectively the same as space-between for terminal output)
local gap_width=$(( available / num_gaps ))
local gap_remainder=$(( available % num_gaps ))
# Build gap separators: center the separator core in each gap
# E.g., gap_width=8 with core "|": " | " (3 left + 1 core + 4 right)
# Remainder chars get distributed one per gap from the left
local gap_separators=()
local g
for (( g = 0; g < num_gaps; g++ )); do
local this_gap=$gap_width
# Distribute remainder: first gaps get +1
if (( g < gap_remainder )); then
this_gap=$(( this_gap + 1 ))
fi
local sep_str=""
if (( SEP_CORE_LEN > 0 )); then
local pad_total=$(( this_gap - SEP_CORE_LEN ))
if (( pad_total < 0 )); then pad_total=0; fi
local pad_left=$(( pad_total / 2 ))
local pad_right=$(( pad_total - pad_left ))
local lpad="" rpad=""
for (( i = 0; i < pad_left; i++ )); do lpad+=" "; done
for (( i = 0; i < pad_right; i++ )); do rpad+=" "; done
sep_str="${lpad}${SEP_CORE}${rpad}"
else
# No core char — pure space gap
for (( i = 0; i < this_gap; i++ )); do sep_str+=" "; done
fi
gap_separators+=("$sep_str")
done
# Assemble justified output
local output=""
local gap_idx=0
local first=1
for idx in "${active_indices[@]}"; do
if (( first )); then
first=0
else
output+="${C_DIM}${gap_separators[$gap_idx]}${C_RESET}"
gap_idx=$(( gap_idx + 1 ))
fi
output+="${ansi_texts[$idx]}"
done
printf '%b' "$output"
return
fi
# ── Left-aligned layout with optional flex expansion ─────────────────────
if (( flex_idx >= 0 && total_width < TERM_WIDTH )); then
# Check if flex section is still active
local flex_active=0
for idx in "${active_indices[@]}"; do
if (( idx == flex_idx )); then
flex_active=1
break
fi
done
if (( flex_active )); then
local extra=$(( TERM_WIDTH - total_width ))
local old_raw="${raw_texts[$flex_idx]}"
local old_ansi="${ansi_texts[$flex_idx]}"
# For spacers, replace placeholder with pure spaces
if is_spacer "${section_ids[$flex_idx]}"; then
local padding=""
for (( i = 0; i < extra + 1; i++ )); do padding+=" "; done
raw_texts[$flex_idx]="$padding"
ansi_texts[$flex_idx]="$padding"
elif [[ "${section_ids[$flex_idx]}" == "context_bar" ]]; then
local pct
pct="$(inp '.context_window.used_percentage' '0')"
local pct_int
pct_int="$(printf '%.0f' "$pct" 2>/dev/null || echo 0)"
# Use config bar_width directly (not derived from raw text length,
# which may include formatting prefix/suffix/pad)
local cur_bar_width
cur_bar_width="$(cfg '.sections.context_bar.bar_width' '10')"
local new_bar_width=$(( cur_bar_width + extra ))
(( new_bar_width < 3 )) && new_bar_width=3
local filled=$(( pct_int * new_bar_width / 100 ))
local empty_count=$(( new_bar_width - filled ))
local bar=""
for (( i = 0; i < filled; i++ )); do bar+="="; done
for (( i = 0; i < empty_count; i++ )); do bar+="-"; done
raw_texts[$flex_idx]="[${bar}] ${pct_int}%"
local warn danger critical
warn="$(cfg '.sections.context_bar.thresholds.warn' '50')"
danger="$(cfg '.sections.context_bar.thresholds.danger' '70')"
critical="$(cfg '.sections.context_bar.thresholds.critical' '85')"
local color="$C_GREEN"
if (( pct_int >= critical )); then
color="${C_RED}${C_BOLD}"
elif (( pct_int >= danger )); then
color="$C_RED"
elif (( pct_int >= warn )); then
color="$C_YELLOW"
fi
ansi_texts[$flex_idx]="${color}[${bar}] ${pct_int}%${C_RESET}"
# Re-apply per-section formatting (prefix/suffix/color/pad) since
# the flex rebuild replaced the formatted output from scratch
SEC_RAW="${raw_texts[$flex_idx]}"
SEC_ANSI="${ansi_texts[$flex_idx]}"
apply_formatting "context_bar"
raw_texts[$flex_idx]="$SEC_RAW"
ansi_texts[$flex_idx]="$SEC_ANSI"
else
# Generic flex: pad with spaces
local padding=""
for (( i = 0; i < extra; i++ )); do padding+=" "; done
raw_texts[$flex_idx]="${old_raw}${padding}"
ansi_texts[$flex_idx]="${old_ansi}${padding}"
fi
fi
fi
# Assemble left-aligned output
local output=""
local prev_asm_idx=-1
for idx in "${active_indices[@]}"; do
if (( prev_asm_idx >= 0 )); then
# Suppress separator when either side is a spacer
if ! is_spacer "${section_ids[$prev_asm_idx]}" && ! is_spacer "${section_ids[$idx]}"; then
output+="${C_DIM}${SEPARATOR}${C_RESET}"
fi
fi
output+="${ansi_texts[$idx]}"
prev_asm_idx=$idx
done
printf '%b' "$output"
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
local layout_json
layout_json="$(resolve_layout)"
local line_count
line_count="$(jq 'length' <<< "$layout_json")"
local lines=()
local i
for (( i = 0; i < line_count; i++ )); do
local line_def
line_def="$(jq -c ".[$i]" <<< "$layout_json")"
local rendered
rendered="$(render_line "$line_def")"
if [[ -n "$rendered" ]]; then
lines+=("$rendered")
fi
done
# Output lines separated by newlines
local first=1
for line in "${lines[@]}"; do
if (( first )); then
first=0
else
printf '\n'
fi
printf '%b' "$line"
done
}
main