diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a969481..6b30198 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -15,7 +15,7 @@ "name": "devpilot-studio", "source": "./", "description": "Real-time terminal status bar for Claude Code — context window, rate limits, 5-hour/7-day quotas, burn-rate alerts, sparkline history, and preset themes.", - "version": "1.3.0", + "version": "1.4.0", "author": { "name": "Kumuditha Tharinda Liyanage" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ff1e368..246b8b8 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "devpilot-studio", "displayName": "DevPilot Studio", - "version": "1.3.0", + "version": "1.4.0", "description": "Real-time terminal status bar for Claude Code — context window, rate limits, 5-hour/7-day quotas, burn-rate alerts, sparkline history, and preset themes. Zero dependencies. 100% offline.", "author": { "name": "Kumuditha Tharinda Liyanage", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0295e2c..3bca664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ All notable changes to DevPilot Studio are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## [1.4.0] - 2026-09-01 + +Theme customization and developer attribution. Every new option is off by +default — an existing config with none of these keys renders exactly as before. + +### Added +- **Custom color theme** (`DPS_THEME=custom`): reads per-role colors from + `DPS_COLOR_MODEL`, `DPS_COLOR_ARROW`, `DPS_COLOR_DIR`, `DPS_COLOR_ORG`, + `DPS_COLOR_BRACKET`, `DPS_COLOR_LABEL`, `DPS_COLOR_RESETTXT`, + `DPS_COLOR_BAR_LOW/MID/HIGH`, and + `DPS_COLOR_EFFORT_LOW/MED/HIGH/XHIGH/MAX/DEFAULT`. Any unset value falls back + to the `default` theme, so an incomplete custom config still renders. +- **True-color** (`DPS_TRUECOLOR=1`): on the custom theme, `DPS_COLOR_*` may be + 6-digit hex (e.g. `ff8800`), converted to 24-bit ANSI. Built-in themes are + unchanged. +- **Configurable bar thresholds** (`DPS_THRESHOLD_MID` = 50, `DPS_THRESHOLD_HIGH` + = 80): the low/mid/high color switch points for every bar. Non-integer, + out-of-range, or `MID >= HIGH` values fall back to 50 / 80. +- **Configurable segment order** (`DPS_LAYOUT` = `ctx,5h,7d,over`): line 2 is + assembled by walking this list. Unknown names are skipped silently. The + default order is byte-for-byte identical to previous output. +- **Terminal-width awareness**: for real status-line rendering, when width is + known (`$COLUMNS` or `tput cols` with a tty) and the terminal is narrow, the + effort segment is dropped first, then the org badge, then `DPS_BAR_WIDTH` + shrinks. When width can't be determined, behavior is unchanged. +- **Named presets**: `dps theme save `, `dps theme use `, + `dps theme list`. Presets live in `$CONFIG_DIR/presets/`; `` is + restricted to `[A-Za-z0-9_-]` to prevent path traversal. `use` loads through + the same safe parser — never sourced. +- **Icon mode** (`DPS_ICONS=1`): swaps the `ctx` / `5h` / `7d` / `over` labels + for Nerd Font glyphs. Plain text is the default. +- **Developer credit**: `DevPilot Studio — built by tharinda.dev` is printed + once by `dps version`, `dps help`, `scripts/setup.sh`, `install.sh`, and the + `npx` installer. It is never printed on the render path. +- **Opt-in status-bar credit** (`DPS_SHOW_CREDIT=1`, default `0`): appends a dim + `· tharinda.dev` to the end of line 1, after the cost segment. The installers + mention it; nothing enables it automatically. + +### Changed +- `apply_theme`'s `default` palette is factored into `_palette_default` (no + behavior change) so the custom theme can reuse it as its fallback. +- The config parser and writer are now driven by a single `DPS_CONFIG_KEYS` + allowlist covering all new keys; assignment still never interprets values as + code. + ## [1.3.0] - 2026-09-01 Bug-fix, cleanup, and security-hardening pass on the Claude Code integration. diff --git a/CLAUDE.md b/CLAUDE.md index ca3d645..e9e6017 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,10 @@ burn-rate velocity alerts, session cost, and 7-day usage sparkline history. ## Architecture -- **`bin/devpilot`** — Core engine (POSIX Bash, ~1100 lines). Handles all rendering, - stats, history, config wizard, gallery, install/uninstall. +- **`bin/devpilot`** — Core engine (POSIX Bash). Handles all rendering, stats, + history, config wizard, gallery, themes (`default`/`mono`/`vivid`/`custom`, + optional true-color), named presets (`dps theme save/use/list`), + install/uninstall. The config file is parsed via an allowlist, never sourced. - **`cli.js`** — Node.js entrypoint wrapper for `npx devpilot-studio` installation. - **`scripts/setup.sh`** — Idempotent plugin setup script (platform detection, jq check, binary install, statusLine config). @@ -49,8 +51,18 @@ bin/devpilot demo history # Test all commands bin/devpilot version bin/devpilot help + +# Theme switching and named presets +bin/devpilot theme vivid +bin/devpilot theme save mypreset +bin/devpilot theme list +bin/devpilot theme use mypreset ``` +When adding a `DPS_*` config key: add it to `DPS_CONFIG_KEYS`, give it a safe +default in `load_config`, and document it in `README.md` and +`docs/CONFIGURATION.md`. Never read config via `source`/`.`. + ## Code Style - Bash scripts: 2-space indent, `set -euo pipefail` in scripts, functions prefixed diff --git a/README.md b/README.md index 3580984..572e2ba 100644 --- a/README.md +++ b/README.md @@ -115,19 +115,27 @@ Configuration is stored in `${XDG_CONFIG_HOME:-$HOME/.config}/devpilot/config`. | Environment Variable | Description | Default | | :--- | :--- | :--- | | `DPS_ORG` | Custom badge text displayed at front of status bar | `""` | -| `DPS_THEME` | Color theme (`default`, `mono`, `vivid`) | `default` | +| `DPS_THEME` | Color theme (`default`, `mono`, `vivid`, `custom`) | `default` | | `DPS_BAR_WIDTH` | Progress bar width in character cells | `10` | | `DPS_BAR_FILLED` | Character used for filled portion of progress bar | `█` | | `DPS_BAR_EMPTY` | Character used for empty portion of progress bar | `░` | +| `DPS_LAYOUT` | Comma-separated order of line-2 segments; unknown names skipped | `ctx,5h,7d,over` | +| `DPS_ICONS` | Use Nerd Font glyphs instead of plain-text segment labels (`1`/`0`) | `0` | +| `DPS_TRUECOLOR` | Allow `DPS_COLOR_*` hex values on the `custom` theme (`1`/`0`) | `0` | +| `DPS_THRESHOLD_MID` | Percent at which bars switch to the "mid" color (int 0–100, `< HIGH`) | `50` | +| `DPS_THRESHOLD_HIGH` | Percent at which bars switch to the "high" color (int 0–100) | `80` | +| `DPS_COLOR_*` | Per-role colors for `DPS_THEME=custom` (raw ANSI, or hex with `DPS_TRUECOLOR=1`). Full list in [docs/CONFIGURATION.md](docs/CONFIGURATION.md). | default theme | | `DPS_SHOW_EFFORT` | Toggle display of model effort level (`1`/`0`) | `1` | | `DPS_SHOW_CTX` | Toggle display of context window capacity bar (`1`/`0`) | `1` | | `DPS_SHOW_5H` | Toggle display of 5-hour rate limit bar (`1`/`0`) | `1` | | `DPS_SHOW_7D` | Toggle display of 7-day rate limit bar (`1`/`0`) | `1` | +| `DPS_SHOW_OVERAGE` | Toggle display of the overage bar (`1`/`0`) | `0` | | `DPS_SHOW_COST` | Toggle display of session cost (`1`/`0`) | `0` | | `DPS_SHOW_BURN` | Toggle velocity burn-rate warning (`1`/`0`) | `1` | +| `DPS_SHOW_CREDIT` | Append a dim `· tharinda.dev` credit to line 1 (`1`/`0`) | `0` | | `DPS_HISTORY` | Toggle local history logging (`1`/`0`) | `1` | -For advanced customization and color codes, see [docs/CONFIGURATION.md](docs/CONFIGURATION.md). +For advanced customization, the full `DPS_COLOR_*` list, and named presets (`dps theme save/use/list`), see [docs/CONFIGURATION.md](docs/CONFIGURATION.md). --- diff --git a/bin/devpilot b/bin/devpilot index a1a5a52..a5744be 100755 --- a/bin/devpilot +++ b/bin/devpilot @@ -16,7 +16,8 @@ # dps stats Show an expanded usage panel from the latest render. # dps history Show usage trends from recorded snapshots. # dps config Run the interactive setup wizard. -# dps theme Switch the color theme (default, mono, vivid) and preview. +# dps theme Switch the color theme (default, mono, vivid, custom). +# dps theme save|use|list Save, load, or list named custom presets. # dps gallery Browse preset looks and apply one. # dps demo Preview with sample data (also: demo stats, demo history). # dps install Copy devpilot to ~/.local/bin and wire into Claude Code. @@ -27,7 +28,7 @@ # Home: https://github.com/mr-kumuditha/devpilot-studio # License: MIT (see LICENSE and NOTICE.md) -DPS_VERSION="1.3.0" +DPS_VERSION="1.4.0" # ---------------------------------------------------------------------------- # Configuration & State Paths @@ -47,6 +48,22 @@ INSTALL_BIN="$HOME/.local/bin/devpilot" # ---------------------------------------------------------------------------- # Config loading # ---------------------------------------------------------------------------- +# Every DPS_* key that may appear in the config file. The file is parsed, never +# sourced, and only these exact names are honored (see _read_config_file). Keep +# this list, load_config's defaults, and the docs in sync. +DPS_CONFIG_KEYS=" +DPS_ORG DPS_THEME DPS_BAR_WIDTH DPS_BAR_FILLED DPS_BAR_EMPTY +DPS_SHOW_EFFORT DPS_SHOW_CTX DPS_SHOW_5H DPS_SHOW_7D DPS_SHOW_OVERAGE +DPS_SHOW_COST DPS_SHOW_BURN DPS_HISTORY +DPS_LAYOUT DPS_ICONS DPS_TRUECOLOR DPS_SHOW_CREDIT +DPS_THRESHOLD_MID DPS_THRESHOLD_HIGH +DPS_COLOR_MODEL DPS_COLOR_ARROW DPS_COLOR_DIR DPS_COLOR_ORG DPS_COLOR_BRACKET +DPS_COLOR_LABEL DPS_COLOR_RESETTXT +DPS_COLOR_BAR_LOW DPS_COLOR_BAR_MID DPS_COLOR_BAR_HIGH +DPS_COLOR_EFFORT_LOW DPS_COLOR_EFFORT_MED DPS_COLOR_EFFORT_HIGH +DPS_COLOR_EFFORT_XHIGH DPS_COLOR_EFFORT_MAX DPS_COLOR_EFFORT_DEFAULT +" + load_config() { _read_config_file @@ -63,6 +80,44 @@ load_config() { DPS_SHOW_COST="${DPS_SHOW_COST:-0}" DPS_SHOW_BURN="${DPS_SHOW_BURN:-1}" DPS_HISTORY="${DPS_HISTORY:-1}" + + # Segment assembly order for line 2. Unknown names are skipped silently. + DPS_LAYOUT="${DPS_LAYOUT:-ctx,5h,7d,over}" + # Nerd Font glyph labels instead of plain text. Off by default. + DPS_ICONS="${DPS_ICONS:-0}" + # Allow DPS_COLOR_* to be hex (e.g. ff8800) on the custom theme. Off by default. + DPS_TRUECOLOR="${DPS_TRUECOLOR:-0}" + # Opt-in "· tharinda.dev" credit at the end of line 1. Off by default. + DPS_SHOW_CREDIT="${DPS_SHOW_CREDIT:-0}" + + # Bar colour-switch thresholds. Must be integers 0-100 with MID < HIGH; + # anything else falls back to the historical 50 / 80. + DPS_THRESHOLD_MID="${DPS_THRESHOLD_MID:-50}" + DPS_THRESHOLD_HIGH="${DPS_THRESHOLD_HIGH:-80}" + case "$DPS_THRESHOLD_MID" in ''|*[!0-9]*) DPS_THRESHOLD_MID=50 ;; esac + case "$DPS_THRESHOLD_HIGH" in ''|*[!0-9]*) DPS_THRESHOLD_HIGH=80 ;; esac + { [ "$DPS_THRESHOLD_MID" -ge 0 ] && [ "$DPS_THRESHOLD_MID" -le 100 ]; } || DPS_THRESHOLD_MID=50 + { [ "$DPS_THRESHOLD_HIGH" -ge 0 ] && [ "$DPS_THRESHOLD_HIGH" -le 100 ]; } || DPS_THRESHOLD_HIGH=80 + [ "$DPS_THRESHOLD_MID" -lt "$DPS_THRESHOLD_HIGH" ] || { DPS_THRESHOLD_MID=50; DPS_THRESHOLD_HIGH=80; } + + # Custom-theme colour overrides. Empty means "use the default theme's value" + # (handled in apply_theme), so an incomplete custom config still renders. + DPS_COLOR_MODEL="${DPS_COLOR_MODEL-}" + DPS_COLOR_ARROW="${DPS_COLOR_ARROW-}" + DPS_COLOR_DIR="${DPS_COLOR_DIR-}" + DPS_COLOR_ORG="${DPS_COLOR_ORG-}" + DPS_COLOR_BRACKET="${DPS_COLOR_BRACKET-}" + DPS_COLOR_LABEL="${DPS_COLOR_LABEL-}" + DPS_COLOR_RESETTXT="${DPS_COLOR_RESETTXT-}" + DPS_COLOR_BAR_LOW="${DPS_COLOR_BAR_LOW-}" + DPS_COLOR_BAR_MID="${DPS_COLOR_BAR_MID-}" + DPS_COLOR_BAR_HIGH="${DPS_COLOR_BAR_HIGH-}" + DPS_COLOR_EFFORT_LOW="${DPS_COLOR_EFFORT_LOW-}" + DPS_COLOR_EFFORT_MED="${DPS_COLOR_EFFORT_MED-}" + DPS_COLOR_EFFORT_HIGH="${DPS_COLOR_EFFORT_HIGH-}" + DPS_COLOR_EFFORT_XHIGH="${DPS_COLOR_EFFORT_XHIGH-}" + DPS_COLOR_EFFORT_MAX="${DPS_COLOR_EFFORT_MAX-}" + DPS_COLOR_EFFORT_DEFAULT="${DPS_COLOR_EFFORT_DEFAULT-}" } # Reverse the escaping applied by _esc_config (\" -> " and \\ -> \). Result in @@ -71,10 +126,10 @@ _unescape_config() { local s="$1" out="" i=0 c n while [ "$i" -lt "${#s}" ]; do c=${s:i:1} - if [ "$c" = '\' ]; then + if [ "$c" = "\\" ]; then n=${s:i+1:1} case "$n" in - '"'|'\') out="$out$n"; i=$(( i + 2 )); continue ;; + '"'|"\\") out="$out$n"; i=$(( i + 2 )); continue ;; esac fi out="$out$c" @@ -83,11 +138,23 @@ _unescape_config() { REPLY_UNESC="$out" } -# Read CONFIG_FILE line by line WITHOUT sourcing it. Only lines of the exact -# form DPS_="" whose KEY is in the explicit allowlist below are -# honored; every other line (including anything executable) is ignored. +# True if $1 is one of the allowlisted DPS_CONFIG_KEYS. +_is_config_key() { + local k + for k in $DPS_CONFIG_KEYS; do + [ "$k" = "$1" ] && return 0 + done + return 1 +} + +# Read a config file line by line WITHOUT sourcing it. Only lines of the exact +# form DPS_="" whose KEY is in DPS_CONFIG_KEYS are honored; every +# other line (including anything executable) is ignored. $1 defaults to +# CONFIG_FILE. Assignment is via `printf -v` to a name taken only from the +# vetted allowlist — the value is never interpreted as code. _read_config_file() { - [ -f "$CONFIG_FILE" ] || return 0 + local file="${1:-$CONFIG_FILE}" + [ -f "$file" ] || return 0 local line key val while IFS= read -r line || [ -n "$line" ]; do case "$line" in @@ -102,22 +169,8 @@ _read_config_file() { case "$val" in *\\*) _unescape_config "$val"; val="$REPLY_UNESC" ;; esac - case "$key" in - DPS_ORG) DPS_ORG="$val" ;; - DPS_THEME) DPS_THEME="$val" ;; - DPS_BAR_WIDTH) DPS_BAR_WIDTH="$val" ;; - DPS_BAR_FILLED) DPS_BAR_FILLED="$val" ;; - DPS_BAR_EMPTY) DPS_BAR_EMPTY="$val" ;; - DPS_SHOW_EFFORT) DPS_SHOW_EFFORT="$val" ;; - DPS_SHOW_CTX) DPS_SHOW_CTX="$val" ;; - DPS_SHOW_5H) DPS_SHOW_5H="$val" ;; - DPS_SHOW_7D) DPS_SHOW_7D="$val" ;; - DPS_SHOW_OVERAGE) DPS_SHOW_OVERAGE="$val" ;; - DPS_SHOW_COST) DPS_SHOW_COST="$val" ;; - DPS_SHOW_BURN) DPS_SHOW_BURN="$val" ;; - DPS_HISTORY) DPS_HISTORY="$val" ;; - esac - done < "$CONFIG_FILE" + _is_config_key "$key" && printf -v "$key" '%s' "$val" + done < "$file" } # Escape a value for embedding in a DPS_="..." line. Result in REPLY_ESC. @@ -128,33 +181,21 @@ _esc_config() { REPLY_ESC="$s" } -# Rewrite CONFIG_FILE from the current DPS_* environment. Callers load_config -# first, so unset keys keep their existing on-disk value rather than a hardcoded -# default. Every string value is escaped so it round-trips through -# _read_config_file without executing anything. +# Rewrite a config file from the current DPS_* environment. Callers load_config +# first, so keys absent from the old file keep their live value rather than a +# hardcoded default. Every value is escaped so it round-trips through +# _read_config_file without executing anything. $1 defaults to CONFIG_FILE. _write_config() { - mkdir -p "$CONFIG_DIR" - local org theme filled empty - _esc_config "${DPS_ORG-}"; org="$REPLY_ESC" - _esc_config "${DPS_THEME-}"; theme="$REPLY_ESC" - _esc_config "${DPS_BAR_FILLED-}"; filled="$REPLY_ESC" - _esc_config "${DPS_BAR_EMPTY-}"; empty="$REPLY_ESC" - cat >"$CONFIG_FILE" <"$file" } # ---------------------------------------------------------------------------- @@ -162,9 +203,66 @@ EOF # ---------------------------------------------------------------------------- RESET='\033[0m' +# The built-in "default" palette, factored out so the custom theme can use each +# value as the fallback for an unset DPS_COLOR_* override. +_palette_default() { + C_MODEL='\033[2m' + C_ARROW='\033[2;37m' + C_DIR='\033[2;36m' + C_ORG='\033[1;31m' + C_BRACKET='\033[2;37m' + C_LABEL='\033[2;37m' + C_RESETTXT='\033[2m' + BAR_LOW='\033[0;32m'; BAR_MID='\033[2;33m'; BAR_HIGH='\033[0;31m' + E_LOW='\033[0;32m'; E_MED='\033[2;36m'; E_HIGH='\033[2;33m' + E_XHIGH='\033[2;35m'; E_MAX='\033[0;31m'; E_DEFAULT='\033[2m' +} + +# Resolve one custom colour. $1 = configured value, $2 = fallback (the default +# theme's value, already set). +# empty -> fallback +# 6-digit hex, DPS_TRUECOLOR=1 -> 24-bit ANSI sequence +# 6-digit hex, DPS_TRUECOLOR=0 -> fallback (never print a bare hex string) +# anything else -> passed through verbatim (raw escape sequence) +_theme_color() { + local v="$1" fb="$2" r g b + [ -n "$v" ] || { printf '%s' "$fb"; return; } + case "$v" in + [0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]) + if [ "${DPS_TRUECOLOR:-0}" = "1" ]; then + r=$(( 16#${v:0:2} )); g=$(( 16#${v:2:2} )); b=$(( 16#${v:4:2} )) + printf '\\033[38;2;%d;%d;%dm' "$r" "$g" "$b" + else + printf '%s' "$fb" + fi + return + ;; + esac + printf '%s' "$v" +} + apply_theme() { local theme="$1" case "$theme" in + custom) + _palette_default + C_MODEL=$(_theme_color "$DPS_COLOR_MODEL" "$C_MODEL") + C_ARROW=$(_theme_color "$DPS_COLOR_ARROW" "$C_ARROW") + C_DIR=$(_theme_color "$DPS_COLOR_DIR" "$C_DIR") + C_ORG=$(_theme_color "$DPS_COLOR_ORG" "$C_ORG") + C_BRACKET=$(_theme_color "$DPS_COLOR_BRACKET" "$C_BRACKET") + C_LABEL=$(_theme_color "$DPS_COLOR_LABEL" "$C_LABEL") + C_RESETTXT=$(_theme_color "$DPS_COLOR_RESETTXT" "$C_RESETTXT") + BAR_LOW=$(_theme_color "$DPS_COLOR_BAR_LOW" "$BAR_LOW") + BAR_MID=$(_theme_color "$DPS_COLOR_BAR_MID" "$BAR_MID") + BAR_HIGH=$(_theme_color "$DPS_COLOR_BAR_HIGH" "$BAR_HIGH") + E_LOW=$(_theme_color "$DPS_COLOR_EFFORT_LOW" "$E_LOW") + E_MED=$(_theme_color "$DPS_COLOR_EFFORT_MED" "$E_MED") + E_HIGH=$(_theme_color "$DPS_COLOR_EFFORT_HIGH" "$E_HIGH") + E_XHIGH=$(_theme_color "$DPS_COLOR_EFFORT_XHIGH" "$E_XHIGH") + E_MAX=$(_theme_color "$DPS_COLOR_EFFORT_MAX" "$E_MAX") + E_DEFAULT=$(_theme_color "$DPS_COLOR_EFFORT_DEFAULT" "$E_DEFAULT") + ;; mono) C_MODEL='\033[2m' C_ARROW='\033[2;37m' @@ -190,16 +288,7 @@ apply_theme() { E_XHIGH='\033[1;35m'; E_MAX='\033[1;31m'; E_DEFAULT='\033[1;37m' ;; *) # default - C_MODEL='\033[2m' - C_ARROW='\033[2;37m' - C_DIR='\033[2;36m' - C_ORG='\033[1;31m' - C_BRACKET='\033[2;37m' - C_LABEL='\033[2;37m' - C_RESETTXT='\033[2m' - BAR_LOW='\033[0;32m'; BAR_MID='\033[2;33m'; BAR_HIGH='\033[0;31m' - E_LOW='\033[0;32m'; E_MED='\033[2;36m'; E_HIGH='\033[2;33m' - E_XHIGH='\033[2;35m'; E_MAX='\033[0;31m'; E_DEFAULT='\033[2m' + _palette_default ;; esac } @@ -215,9 +304,9 @@ render_bar() { [ "$pct" -lt 0 ] && pct=0 local color="$BAR_LOW" - if [ "$pct" -ge 80 ]; then + if [ "$pct" -ge "${DPS_THRESHOLD_HIGH:-80}" ]; then color="$BAR_HIGH" - elif [ "$pct" -ge 50 ]; then + elif [ "$pct" -ge "${DPS_THRESHOLD_MID:-50}" ]; then color="$BAR_MID" fi @@ -285,6 +374,61 @@ render_burn() { fi } +# Nerd Font glyph for a segment name, emitted as raw UTF-8 bytes (octal escapes +# so it works on Bash 3.2 without $'...'). Only used when DPS_ICONS=1. +_icon() { + case "$1" in + ctx) printf '\357\202\256' ;; # U+F0AE nf-fa-tasks + 5h) printf '\357\200\227' ;; # U+F017 nf-fa-clock_o + 7d) printf '\357\201\263' ;; # U+F073 nf-fa-calendar + over) printf '\357\201\261' ;; # U+F071 nf-fa-exclamation_triangle + esac +} + +# The coloured label + trailing alignment space(s) that precede a segment's bar. +# Plain text (ctx / 5h / 7d / over) is the unconditional default and preserves +# the historical spacing exactly; DPS_ICONS=1 swaps in a Nerd Font glyph. +_seg_label() { + local name="$1" glyph + if [ "${DPS_ICONS:-0}" = "1" ]; then + glyph=$(_icon "$name") + [ -n "$glyph" ] || glyph="$name" + printf '%s' "${C_LABEL}${glyph}${RESET} " + return + fi + case "$name" in + ctx) printf '%s' "${C_LABEL}ctx${RESET} " ;; + 5h) printf '%s' "${C_LABEL}5h${RESET} " ;; + 7d) printf '%s' "${C_LABEL}7d${RESET} " ;; + over) printf '%s' "${C_LABEL}over${RESET} " ;; + esac +} + +# Terminal-width adaptation for real status-line rendering. When the terminal is +# narrower than the thresholds below, shed content in this order: +# 1. the effort-level segment 2. the org badge 3. shrink DPS_BAR_WIDTH +# Width is taken from $COLUMNS, else `tput cols` when a tty is attached. If it +# cannot be determined (piped output, no tput), nothing changes and rendering +# keeps today's fixed-width behaviour. +_adapt_width() { + local cols="" + if [ -n "${COLUMNS:-}" ]; then + cols="$COLUMNS" + elif command -v tput >/dev/null 2>&1 && { [ -t 1 ] || [ -t 2 ]; }; then + cols=$(tput cols 2>/dev/null) + fi + case "$cols" in ''|*[!0-9]*) return 0 ;; esac + [ "$cols" -gt 0 ] || return 0 + + [ "$cols" -lt 100 ] && DPS_SHOW_EFFORT=0 + [ "$cols" -lt 80 ] && DPS_ORG="" + if [ "$cols" -lt 60 ]; then + local w=$(( cols / 6 )) + [ "$w" -lt 4 ] && w=4 + [ "$w" -lt "$DPS_BAR_WIDTH" ] && DPS_BAR_WIDTH="$w" + fi +} + # ---------------------------------------------------------------------------- # render — main status line rendering # ---------------------------------------------------------------------------- @@ -296,6 +440,7 @@ cmd_render() { load_config apply_theme "$DPS_THEME" + _adapt_width local input input=$(cat) @@ -392,49 +537,68 @@ _render_status() { line1="${line1} ${C_LABEL}\$$(printf '%.2f' "$cost")${RESET}" fi - local line2="" seg bar - if [ "$DPS_SHOW_CTX" = "1" ] && [ -n "$ctx_pct" ]; then - bar=$(render_bar "$ctx_pct") - line2="${C_LABEL}ctx${RESET} ${bar} $(printf '%.0f' "$ctx_pct")%" - fi - - if [ "$DPS_SHOW_5H" = "1" ]; then - if [ -n "$five_pct" ]; then - bar=$(render_bar "$five_pct") - seg="${C_LABEL}5h${RESET} ${bar} $(printf '%.0f' "$five_pct")%" - if [ -n "$five_reset" ]; then - seg="${seg} ${C_RESETTXT}(resets ${RESET}$(render_reset "$five_reset")${C_RESETTXT})${RESET}" - fi - if [ "$DPS_SHOW_BURN" = "1" ] && [ -n "$five_reset" ]; then - seg="${seg}$(render_burn "$five_pct" "$five_reset" $((5 * 3600)))" - fi - if [ -n "$line2" ]; then line2="${line2} ${seg}"; else line2="$seg"; fi - elif [ -n "$has_rl" ]; then - seg="${C_LABEL}5h${RESET} ${C_RESETTXT}idle${RESET}" - if [ -n "$line2" ]; then line2="${line2} ${seg}"; else line2="$seg"; fi - fi - fi - - if [ "$DPS_SHOW_7D" = "1" ] && [ -n "$week_pct" ]; then - bar=$(render_bar "$week_pct") - seg="${C_LABEL}7d${RESET} ${bar} $(printf '%.0f' "$week_pct")%" - if [ -n "$week_reset" ]; then - seg="${seg} ${C_RESETTXT}(resets ${RESET}$(render_reset "$week_reset")${C_RESETTXT})${RESET}" - fi - if [ "$DPS_SHOW_BURN" = "1" ] && [ -n "$week_reset" ]; then - seg="${seg}$(render_burn "$week_pct" "$week_reset" $((7 * 86400)))" - fi - if [ -n "$line2" ]; then line2="${line2} ${seg}"; else line2="$seg"; fi + # Opt-in author credit at the very end of line 1 (default off). + if [ "${DPS_SHOW_CREDIT:-0}" = "1" ]; then + line1="${line1} ${C_RESETTXT}· tharinda.dev${RESET}" fi - if [ "$DPS_SHOW_OVERAGE" = "1" ] && [ -n "$over_pct" ]; then - bar=$(render_bar "$over_pct") - seg="${C_LABEL}over${RESET} ${bar} $(printf '%.0f' "$over_pct")%" - if [ -n "$over_reset" ]; then - seg="${seg} ${C_RESETTXT}(resets ${RESET}$(render_reset "$over_reset")${C_RESETTXT})${RESET}" - fi + # Line 2 is assembled by walking DPS_LAYOUT (default "ctx,5h,7d,over"). + # Each segment keeps its exact historical rendering; only the order is + # data-driven. Unknown names are skipped silently. + local line2="" seg bar tok + local -a _layout=() + IFS=$',\n' read -ra _layout <<< "$DPS_LAYOUT" + for tok in "${_layout[@]}"; do + seg="" + case "$tok" in + ctx) + if [ "$DPS_SHOW_CTX" = "1" ] && [ -n "$ctx_pct" ]; then + bar=$(render_bar "$ctx_pct") + seg="$(_seg_label ctx)${bar} $(printf '%.0f' "$ctx_pct")%" + fi + ;; + 5h) + if [ "$DPS_SHOW_5H" = "1" ]; then + if [ -n "$five_pct" ]; then + bar=$(render_bar "$five_pct") + seg="$(_seg_label 5h)${bar} $(printf '%.0f' "$five_pct")%" + if [ -n "$five_reset" ]; then + seg="${seg} ${C_RESETTXT}(resets ${RESET}$(render_reset "$five_reset")${C_RESETTXT})${RESET}" + fi + if [ "$DPS_SHOW_BURN" = "1" ] && [ -n "$five_reset" ]; then + seg="${seg}$(render_burn "$five_pct" "$five_reset" $((5 * 3600)))" + fi + elif [ -n "$has_rl" ]; then + seg="$(_seg_label 5h)${C_RESETTXT}idle${RESET}" + fi + fi + ;; + 7d) + if [ "$DPS_SHOW_7D" = "1" ] && [ -n "$week_pct" ]; then + bar=$(render_bar "$week_pct") + seg="$(_seg_label 7d)${bar} $(printf '%.0f' "$week_pct")%" + if [ -n "$week_reset" ]; then + seg="${seg} ${C_RESETTXT}(resets ${RESET}$(render_reset "$week_reset")${C_RESETTXT})${RESET}" + fi + if [ "$DPS_SHOW_BURN" = "1" ] && [ -n "$week_reset" ]; then + seg="${seg}$(render_burn "$week_pct" "$week_reset" $((7 * 86400)))" + fi + fi + ;; + over) + if [ "$DPS_SHOW_OVERAGE" = "1" ] && [ -n "$over_pct" ]; then + bar=$(render_bar "$over_pct") + seg="$(_seg_label over)${bar} $(printf '%.0f' "$over_pct")%" + if [ -n "$over_reset" ]; then + seg="${seg} ${C_RESETTXT}(resets ${RESET}$(render_reset "$over_reset")${C_RESETTXT})${RESET}" + fi + fi + ;; + *) seg="" ;; + esac + [ -n "$seg" ] || continue if [ -n "$line2" ]; then line2="${line2} ${seg}"; else line2="$seg"; fi - fi + done printf "%b" "$line1" [ -n "$line2" ] && printf "\n%b" "$line2" @@ -630,7 +794,7 @@ cmd_history() { tzoff=$(date +%z 2>/dev/null) case "$tzoff" in [+-][0-9][0-9][0-9][0-9]) - sign=${tzoff%${tzoff#?}}; hh=${tzoff:1:2}; mm=${tzoff:3:2} + sign=${tzoff%"${tzoff#?}"}; hh=${tzoff:1:2}; mm=${tzoff:3:2} off_sec=$(( 10#$hh * 3600 + 10#$mm * 60 )) [ "$sign" = "-" ] && off_sec=$(( -off_sec )) ;; @@ -688,8 +852,7 @@ $rows EOF local i label mid for (( i = ${#LINES[@]} - 1; i >= 0; i-- )); do - set -- ${LINES[$i]} - di=$1; p5=$2; p7=$3; dc=$4 + read -r di p5 p7 dc <<<"${LINES[$i]}" mid=$(( di * 86400 - off_sec + 43200 )) label=$(_date_fmt "$mid" "%b %d") printf " %-9s %6s%% %6s%% %9s\n" "$label" "$p5" "$p7" "\$$dc" @@ -1039,34 +1202,95 @@ cmd_config() { } # ---------------------------------------------------------------------------- -# theme — fast single-key theme switch +# theme — fast theme switch + named custom presets # ---------------------------------------------------------------------------- +PRESET_DIR="$CONFIG_DIR/presets" + +# Strict preset-name allowlist: letters, digits, dash, underscore only. Blocks +# path traversal (no "/", no ".", no "..") before the name touches a path. +_valid_preset_name() { + case "$1" in + ''|*[!A-Za-z0-9_-]*) return 1 ;; + *) return 0 ;; + esac +} + +_theme_preview() { + command -v jq >/dev/null 2>&1 || return 0 + apply_theme "$DPS_THEME" + _say "" + _render_status "$(_demo_json)" +} + +_theme_save() { + local name="$1" + if ! _valid_preset_name "$name"; then + _err "invalid preset name '$name' — use letters, digits, dash, underscore only" + return 2 + fi + load_config + mkdir -p "$PRESET_DIR" + _write_config "$PRESET_DIR/$name" + _say "Saved current config as preset \"$name\" → $PRESET_DIR/$name" +} + +_theme_use() { + local name="$1" + if ! _valid_preset_name "$name"; then + _err "invalid preset name '$name' — use letters, digits, dash, underscore only" + return 2 + fi + local f="$PRESET_DIR/$name" + if [ ! -f "$f" ]; then + _err "no saved preset '$name' — run 'dps theme list' to see what's available" + return 1 + fi + load_config # defaults + current config + _read_config_file "$f" # overlay the preset (safe parser, never sourced) + _write_config # persist merged result to the live config + _say "Applied preset \"$name\" → $CONFIG_FILE" + _say "Start a new session to apply it everywhere." + _theme_preview +} + +_theme_list() { + if [ ! -d "$PRESET_DIR" ]; then + _say "No saved presets yet. Create one with: dps theme save " + return 0 + fi + local f found=0 + for f in "$PRESET_DIR"/*; do + [ -f "$f" ] || continue + found=1 + _say " $(basename "$f")" + done + [ "$found" = "1" ] || _say "No saved presets yet. Create one with: dps theme save " +} + cmd_theme() { - local theme="${1-}" - case "$theme" in + local sub="${1-}" + case "$sub" in + save) shift; _theme_save "${1-}"; return $? ;; + use) shift; _theme_use "${1-}"; return $? ;; + list) _theme_list; return $? ;; default|mono|vivid) : ;; "") - _err "usage: dps theme " + _err "usage: dps theme | save | use | list" return 2 ;; *) - _err "unknown theme '$theme' — valid options: default, mono, vivid" + _err "unknown theme '$sub' — valid options: default, mono, vivid (or: save, use, list)" return 2 ;; esac load_config - DPS_THEME="$theme" + DPS_THEME="$sub" _write_config - _say "Theme set to \"$theme\" → $CONFIG_FILE" + _say "Theme set to \"$sub\" → $CONFIG_FILE" _say "Start a new session to apply it everywhere." - - if command -v jq >/dev/null 2>&1; then - apply_theme "$DPS_THEME" - _say "" - _render_status "$(_demo_json)" - fi + _theme_preview } # ---------------------------------------------------------------------------- @@ -1146,7 +1370,10 @@ cmd_uninstall() { # ---------------------------------------------------------------------------- _err() { printf "devpilot: %b\n" "$1" >&2; } -cmd_version() { printf "DevPilot Studio (dps / devpilot) %s\n" "$DPS_VERSION"; } +cmd_version() { + printf "DevPilot Studio (dps / devpilot) %s\n" "$DPS_VERSION" + printf "DevPilot Studio — built by tharinda.dev\n" +} cmd_help() { cat <<'EOF' @@ -1157,7 +1384,8 @@ Usage: dps stats Show expanded usage metrics panel from latest render. dps history Show usage trends & sparklines from recorded snapshots. dps config Run interactive setup wizard. - dps theme Switch color theme (default | mono | vivid) and preview. + dps theme Switch color theme (default | mono | vivid | custom) and preview. + dps theme save / use / list Manage named custom presets. dps gallery Browse preset looks and apply one. dps demo Preview status bar with sample data (also: dps demo stats/history). dps install Copy devpilot script to ~/.local/bin and wire into settings. @@ -1167,6 +1395,8 @@ Usage: Config file: ${XDG_CONFIG_HOME:-~/.config}/devpilot/config Home: https://github.com/mr-kumuditha/devpilot-studio + +DevPilot Studio — built by tharinda.dev EOF } diff --git a/cli.js b/cli.js index c95c78a..b7bae6f 100755 --- a/cli.js +++ b/cli.js @@ -105,6 +105,8 @@ function runInstall() { console.log('\nPreview:'); spawnSync(BIN, ['demo'], { stdio: 'inherit' }); console.log('\nStart a new Claude Code session (or restart) to see your status bar.'); + console.log(dim('Optional: set DPS_SHOW_CREDIT=1 in your config for a subtle status-bar credit (off by default).')); + console.log('\nDevPilot Studio — built by tharinda.dev'); if (!(process.env.PATH || '').split(path.delimiter).includes(BIN_DIR)) { console.log(''); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 4ded30a..70789c5 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -9,7 +9,9 @@ All configurations are stored locally in the following file: ${XDG_CONFIG_HOME:-$HOME/.config}/devpilot/config ``` -The file consists of standard bash variable exports, making it trivial to source or modify via automated scripts. +The file consists of plain `DPS_KEY="value"` assignments. DevPilot Studio +**parses** this file line by line — it is never sourced or executed (see +[Config File Parsing](#config-file-parsing) below). *Note: Any changes to this file require restarting Claude Code to take effect.* --- @@ -38,10 +40,11 @@ To change only the color theme without stepping through the full wizard, use: dps theme ``` -Where `` is one of `default`, `mono`, or `vivid`. This updates `DPS_THEME` -in your config file, leaves every other setting (badge, segment visibility, bar -style, width) exactly as it was, prints a one-line confirmation, and shows a -live preview. Invalid names are rejected with the list of valid options. +Where `` is one of `default`, `mono`, `vivid`, or `custom`. This updates +`DPS_THEME` in your config file, leaves every other setting (badge, segment +visibility, bar style, width) exactly as it was, prints a one-line confirmation, +and shows a live preview. Invalid names are rejected with the list of valid +options. ```bash dps theme vivid # switch to the high-contrast palette @@ -53,6 +56,21 @@ Also available through the npm wrapper: `npx devpilot-studio theme vivid`. Restart Claude Code (or start a new session) for the change to take effect in the status bar. +### Named presets + +Save the whole current config (theme, colors, layout, segment visibility, +everything) as a reusable preset: + +```bash +dps theme save work # write current config to $CONFIG_DIR/presets/work +dps theme list # list saved presets +dps theme use work # load 'work' back into the live config +``` + +Preset names are restricted to letters, digits, `-`, and `_`. `dps theme use` +loads the preset through the same safe parser as the main config — it is never +sourced. + --- ## Environment Variable Reference @@ -67,7 +85,7 @@ If you prefer to configure DevPilot Studio manually or via dotfiles, here is the ### Theming | Variable | Description | Default | | :--- | :--- | :--- | -| `DPS_THEME` | The color palette used for rendering the status bar. Supported values: `default` (dimmed), `mono` (grayscale), `vivid` (bright cyberpunk). | `default` | +| `DPS_THEME` | The color palette used for rendering the status bar. `default` (dimmed), `mono` (grayscale), `vivid` (bright), or `custom` (per-role `DPS_COLOR_*`). | `default` | ### Bar Aesthetics | Variable | Description | Default | @@ -75,6 +93,8 @@ If you prefer to configure DevPilot Studio manually or via dotfiles, here is the | `DPS_BAR_WIDTH` | Width (in character cells) for all progress bars. | `10` | | `DPS_BAR_FILLED` | Unicode or ASCII character used to represent the filled portion of a progress bar. | `█` | | `DPS_BAR_EMPTY` | Unicode or ASCII character used to represent the empty portion of a progress bar. | `░` | +| `DPS_THRESHOLD_MID` | Bar fill percent at which the color switches from "low" to "mid". Integer 0–100; must be `< DPS_THRESHOLD_HIGH`. Invalid values fall back to `50`. | `50` | +| `DPS_THRESHOLD_HIGH` | Bar fill percent at which the color switches from "mid" to "high". Integer 0–100. Invalid values fall back to `80`. | `80` | *Examples of popular bar styles:* - Solid Blocks: `FILLED="█"`, `EMPTY="░"` @@ -82,6 +102,32 @@ If you prefer to configure DevPilot Studio manually or via dotfiles, here is the - Squares: `FILLED="■"`, `EMPTY="□"` - ASCII: `FILLED="="`, `EMPTY="-"` +### Layout & Icons +| Variable | Description | Default | +| :--- | :--- | :--- | +| `DPS_LAYOUT` | Comma-separated assembly order for the line-2 segments. Valid names: `ctx`, `5h`, `7d`, `over`. Unknown or misspelled names are skipped silently. Each segment keeps its normal rendering (bar, reset countdown, burn suffix). | `ctx,5h,7d,over` | +| `DPS_ICONS` | When `1`, replace the plain-text segment labels (`ctx`, `5h`, `7d`, `over`) with Nerd Font glyphs. Requires a patched font; plain text is the safe default. | `0` | + +### Custom Theme Colors +Used only when `DPS_THEME=custom`. Each unset value falls back to the `default` +theme's value, so a partial custom config still renders. Values are raw ANSI +escape sequences (e.g. `\033[1;35m`). If `DPS_TRUECOLOR=1`, a value may instead +be a 6-digit hex color (e.g. `ff8800`), which is converted to a 24-bit ANSI +sequence; a hex value with `DPS_TRUECOLOR=0` falls back to the default color. + +| Variable | Role | +| :--- | :--- | +| `DPS_TRUECOLOR` | `1` to accept hex `DPS_COLOR_*` values (24-bit). Default `0`. | +| `DPS_COLOR_MODEL` | Model name | +| `DPS_COLOR_ARROW` | The `->` between model and directory | +| `DPS_COLOR_DIR` | Workspace directory name | +| `DPS_COLOR_ORG` | Org / badge label | +| `DPS_COLOR_BRACKET` | The `[` `]` around the inner group | +| `DPS_COLOR_LABEL` | Segment labels (`ctx`, `5h`, …) and cost | +| `DPS_COLOR_RESETTXT` | Dim "(resets …)" text and the opt-in credit | +| `DPS_COLOR_BAR_LOW` / `_MID` / `_HIGH` | Bar color below MID / between / at or above HIGH threshold | +| `DPS_COLOR_EFFORT_LOW` / `_MED` / `_HIGH` / `_XHIGH` / `_MAX` / `_DEFAULT` | Effort-level word, by level | + ### Segment Visibility Visibility flags accept `1` (true/visible) or `0` (false/hidden). @@ -91,8 +137,10 @@ Visibility flags accept `1` (true/visible) or `0` (false/hidden). | `DPS_SHOW_CTX` | Shows the Context Window utilization progress bar. | `1` | | `DPS_SHOW_5H` | Shows the 5-Hour Rate Limit progress bar (only active for Claude Pro). | `1` | | `DPS_SHOW_7D` | Shows the 7-Day Plan Quota progress bar (only active for Claude Pro). | `1` | +| `DPS_SHOW_OVERAGE` | Shows the overage bar when the payload reports one. | `0` | | `DPS_SHOW_COST` | Shows the live session cost (e.g., `$1.42`). | `0` | | `DPS_SHOW_BURN` | Shows the `⚠` warning when token velocity projects hitting rate limits before they reset. | `1` | +| `DPS_SHOW_CREDIT` | Appends a dim `· tharinda.dev` to the end of line 1 (after the cost segment). Off by default. | `0` | ### History & Logging | Variable | Description | Default | @@ -101,6 +149,17 @@ Visibility flags accept `1` (true/visible) or `0` (false/hidden). --- +## Terminal-Width Adaptation + +When DevPilot Studio renders the real status line and can determine the terminal +width (from `$COLUMNS`, or `tput cols` when a TTY is attached), a narrow terminal +sheds content in this order: **1.** the effort-level segment, **2.** the org +badge, **3.** `DPS_BAR_WIDTH` shrinks (down to 4). When the width cannot be +determined (piped output, no `tput`), nothing is dropped and rendering keeps its +fixed-width behavior. + +--- + ## Config File Parsing The config file is **parsed line by line, never executed**. Only lines of the diff --git a/install.sh b/install.sh index 7a896f8..274db8d 100755 --- a/install.sh +++ b/install.sh @@ -115,6 +115,9 @@ say "Preview:" "$BIN" demo || true say "" say "Start a new Claude Code session (or restart) to see your status bar." +say "${DIM}Optional: set DPS_SHOW_CREDIT=1 in your config for a subtle status-bar credit (off by default).${R}" +say "" +say "DevPilot Studio — built by tharinda.dev" case ":$PATH:" in *":$BIN_DIR:"*) : ;; diff --git a/package.json b/package.json index 5723194..62f3ed1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devpilot-studio", - "version": "1.3.0", + "version": "1.4.0", "description": "DevPilot Studio — A professional desktop utility for managing Claude Code workflows, AI development sessions, project status, MCP servers, prompts, Git repositories, and developer productivity.", "bin": { "dps": "cli.js", diff --git a/scripts/setup.sh b/scripts/setup.sh index 71d0b11..73b385c 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -18,7 +18,7 @@ set -euo pipefail # -------------------------------------------------------------------------- # Constants # -------------------------------------------------------------------------- -DPS_VERSION="1.3.0" +DPS_VERSION="1.4.0" INSTALL_BIN="$HOME/.local/bin/devpilot" SETTINGS_DIR="$HOME/.claude" @@ -220,6 +220,9 @@ main() { printf "\n" >&2 info "DevPilot Studio setup complete" info "Start a new Claude Code session (or restart) to see your status bar" + info "Optional: set DPS_SHOW_CREDIT=1 in your config for a subtle status-bar credit (off by default)" + printf "\n" >&2 + info "DevPilot Studio — built by tharinda.dev" printf "\n" >&2 }