diff --git a/config/claude/CLAUDE.md b/config/claude/CLAUDE.md new file mode 100644 index 0000000..31680bf --- /dev/null +++ b/config/claude/CLAUDE.md @@ -0,0 +1,186 @@ +# CLAUDE.md + +Shared context for any project I work on. A project-specific `CLAUDE.md` closer to the cwd adds to or overrides anything here. + +## Before making changes + +When I open a fresh session vaguely ("help me with this project"), read `CLAUDE.md`, check `git status` and recent commit history, and summarize the current state before doing anything. Don't edit a dirty repo without an explicit ask. I usually want to discuss first. + +If the repo is clean and I've given you a clear task, go ahead. The "wait and discuss" rule is for the ambiguous starts. + +When a request has multiple reasonable interpretations, state your interpretation before acting, or ask which one I meant. Don't silently pick one and proceed. + +Before suggesting modifications to a file or proposing a fix, read the file. Don't guess at what the code looks like or assume what's there from the filename. Same goes for follow-the-imports work: if you need to understand how a function is used, read the call sites. + +## Planning complex work + +For multi-step work that touches several files or has real dependencies between steps, lay out the plan before executing rather than improvising as you go. For genuinely non-trivial changes, use plan mode (`ExitPlanMode` in Claude Code) so I can review before any code is written. Expect me to scrutinize both the plan and the resulting changes. + +When you do plan, identify natural commit breakpoints rather than treating everything as one giant commit. Each breakpoint should leave the project in a working state, ideally one I'd be willing to push as its own commit. + +## Sensitive files + +Never stage or commit `.env`, `.env.*`, `*.pem`, `*.key`, credentials files, API keys, certificates, or anything else that looks like a secret. If a sensitive-looking file shows up in `git status` while you're preparing a commit, stop and surface it before continuing. + +## Commits and branches + +Use conventional commit prefixes (`feat`, `fix`, `chore`, `refactor`, `docs`, `ci`, `test`, `perf`, `build`, `style`, `revert`) unless I say otherwise. Branches follow `/` for the same reason. + +Don't commit, push, or open a PR unless I've explicitly asked for it. Showing me the diff or running tests is fine. The actual git and `gh` actions are mine to authorize. + +I use `gpg-touchid` (a GPG pinentry that requires Touch ID confirmation) to sign commits, so any commit you trigger will surface a Touch ID prompt I have to approve. If a commit fails because of GPG signing or pinentry rejection, it's almost always me deliberately blocking it rather than a tool failure. Don't debug it, don't work around it, and don't retry. Ask whether I meant to block the commit entirely or whether I wanted you to change something first. + +Default to feature branches and PRs (via `gh pr create` for GitHub repos), even for solo projects. Most projects I work on have CI that enforces conventions on PRs. + +When you need the current date or time (for a `Last Updated:` line in a doc, anywhere else), get it from `date` rather than guessing or pulling from earlier in the conversation. + +### Commit message body + +Commit messages summarize what changed as a whole, not itemize individual changes. Prefer a single subject line that captures everything. A commit that renames three functions and adds a call site is "refactor: renamed helpers and added geolocation check to registration flow," not four bullets. List related items inline: "docs: add Architecture section covering disposable LXC, per-container modules, hybrid access, and layered backups." + +Skip the body when the subject line covers it, which should be most of the time. A body is for when a commit genuinely spans multiple unrelated areas that can't fit in one line. Even then, keep it to a few short bullets describing themes, not individual changes. Order by what matters most to a future reader. Trivial items (typo fixes, formatting, gitignore tweaks) can be tacked onto the subject line as an aside ("also fixed typos") or dropped entirely when there's more significant work in the same commit. If the commit is nothing but trivial changes, mention them normally. + +## Pull requests + +Before opening a PR, run all the project's checks locally (tests, linters, formatter, relevant manual smoke tests). For things genuinely untestable locally (PR-only CI workflows, deploy targets, anything that needs real credentials), it's fine to open the PR and rely on CI feedback, but call out which checks were skipped and why. + +Default to squash merge via `gh pr merge --squash` unless I ask for a rebase merge. The squashed message should follow the same conventional-commit subject + bullet body shape as a regular commit. + +PR bodies use a `## Summary` section followed by a `## Test plan` section. Summary is a short bulleted list describing the themes of the change, same as commit messages. A single-sentence summary is fine when the PR is focused enough. Test plan is a checklist with checkboxes pre-checked for anything verified locally and unchecked for things that need CI or manual verification. If the project has a `.github/PULL_REQUEST_TEMPLATE.md` (or any of the other names GitHub recognizes), use that as the starting structure. If it doesn't, prompt me once in a while to add one so the format gets enforced. + +Prefer smaller, separated PRs over one big bundled PR when the changes are independent. I have a habit of making PRs too big, so help me catch myself when I'm doing it. When changes are tightly coupled (a refactor that enables a feature, a schema change that needs a code update), say so and ask whether to bundle them into one PR, stack them on a single branch, or split them into sequential PRs. + +A note on stacking specifically. With squash merges, "stacking on one branch" is effectively the same as bundling into one PR, just with the intermediate commits visible during review before they collapse into a single commit on `main`. True branch-stacking (PR B targets PR A's branch instead of `main`) adds review complexity and rarely pays off when the squash will throw the intermediate state away anyway. The usual decision: separate PRs when changes are independent, one bundled PR when they're tightly coupled, and only true-stack when each step genuinely needs its own commit on `main`. + +Don't force-push unless we're on a feature branch and there's a strong reason. Even then, ask first and explain why you're recommending it. Never force-push to `main`. + +## Writing code + +Code should look like a human wrote it, comments included. Before writing or modifying anything, look at how the rest of the project already does things and match it. Style, naming, import order, file layout, error handling patterns. If the project uses an idiom consistently, use that idiom even if you'd normally reach for something else. New code should fit in with whatever was there before instead of introducing a competing style. + +Sometimes I'll point you at a reference project (like `~/Code/misc/learning/rust/personal-playground/wordify/` for Rust style) and ask you to match its style. Read it before writing code in that language. If a project's `CLAUDE.md` or `.docs/AGENTS.md` mentions a reference project, treat it the same way. + +Files should end with a single trailing newline. POSIX text-file convention, and a lot of tooling expects it. + +When you create temp files, scratch directories, or experimental output during a session, clean them up when you're done unless I've asked you to keep them around. The `trap 'rm -rf "$WORK_DIR"' EXIT` pattern is the right shape for shell scripts. + +Don't create new documentation files (`README.md`, `TODO.md`, anything with extensive prose) unless I ask. Modifying existing docs at the project root is fine. Files inside `.docs/` follow their own rule below. + +## Scope and reporting + +Stick to what I asked. Don't refactor unrelated code, don't add features I didn't request, don't "improve" things I didn't mention, don't add error handling for cases that can't happen. If you notice something worth changing on the side, mention it but don't do it. + +Don't auto-fix or remove `TODO`/`FIXME`/`XXX` comments in code you're touching unless I've asked, or unless resolving them is genuinely required for the change at hand. If a TODO looks like a quick win, surface it as a suggestion rather than acting on it. + +The flip side: when I explicitly ask for your thoughts on an approach, speak up. If you see a better approach than mine, say so and explain why. If you spot a bug or logic issue in what I'm proposing, point it out before I commit to it. The "don't expand scope" rule is about unsolicited side quests, not about staying quiet when I'm asking for input. + +On projects I own (owner: `me`), flag anti-patterns and non-idiomatic code unsolicited. If I'm doing something the wrong way for the language, the wrong way for the framework, or noticeably clumsier than the idiomatic approach, point it out. I'd rather learn the right pattern now than reinforce the wrong one. This applies whether the project is public or private. If it's public it doubles as portfolio polish, and if it's private I still want to know. + +Don't apply this to projects owned by someone else, even when they're public. On forked PR contributions or open source work, follow the upstream project's conventions instead of imposing what I'd consider idiomatic. My own projects are the one place where unsolicited feedback on style and patterns is welcome. + +When you say something is verified, working, or fixed, be specific about how you checked. "Ran `cargo test`, all 6 tests passed" is good. "This should work now" or "the change is complete" without saying how you verified is not. When you can't actually verify something (no credentials, the check only runs in CI, the test target isn't reachable from here), say so explicitly rather than implying it works. + +## Comments + +Comments are for things the code can't say on its own. If a reader can tell what a line does just by reading it, the comment is noise. Strip it before committing. + +Keep comments for cases like: +- Workarounds someone would otherwise question (a sleep to dodge a race, a magic number from a vendor doc) +- Edge cases the code handles silently +- The why behind a non-obvious choice + +Surviving comments should be tight. One line where one line will do. More detail only when it's actually warranted. + +No em-dashes mid-sentence as a pause. If you reach for one, rewrite the sentence instead. The only place an em-dash is fine is as the bullet marker itself (in place of `-` or `*`), never inside the text of the bullet. + +These same rules apply to markdown I'm adding to a project (`README.md`, `TODO.md`, files inside `.docs/`, anything else): concise by default, no em-dashes as pauses. + +## The `.docs/` pattern + +Many projects I work on use a `.docs/` directory for rules, specs, planning notes, and private session context (briefing/debriefing, persisted notes, local overrides to this file). Used across personal, employer, and open source projects where I want private notes that don't end up upstream. The conventions: + +- `.docs/` is its own git repo (the directory contains a `.git/`). It is not tracked by the parent project's git history. +- `.docs/` is gitignored globally at `~/.gitignore` (line `.docs`), so it never appears in `git status` for the parent project. +- The parent project's `CLAUDE.md` is typically a symlink to `.docs/AGENTS.md` so Claude Code picks up the rules while they live in the nested repo. +- A project may also keep an `archived/` subdirectory inside `.docs/` for old planning docs that have been replaced. + +What this means in practice: + +- To commit doc changes, `cd .docs/` first. Committing from the parent repo will silently do nothing because `.docs/` is gitignored. +- The nested repo usually has no remote. Doc history stays local unless I set one up. +- Watch out for symlinks in both directions: a parent project's `CLAUDE.md` may actually be `.docs/AGENTS.md` through a symlink, and `.docs/` itself may eventually be a symlink to a centralized docs repo. Run `ls -l` on either before assuming you're editing the file you think you are. +- Don't modify files inside `.docs/` without asking first. If you notice docs are stale (significant changes since last update, or enough accumulated drift), suggest an update. Before making changes, list what you'd update and which files you'd touch so I can approve or trim the scope. +- When I ask you to create a new doc inside `.docs/`, include a date near the top. The pattern I use is a `Last Updated: YYYY-MM-DD` line in the file header, matching how the existing `SPEC.md` does it. + +### Files inside `.docs/` + +`.docs/` always contains `AGENTS.md`. Beyond that it will usually contain either a `SPEC.md` or an `ARCHITECTURE.md`, but almost never both. Default to `SPEC.md`, since it's the broader of the two and can include architecture as a section while also covering goals, features, behavior, and interface. Reach for `ARCHITECTURE.md` only when the doc is purely about how the code is organized and there's no behavior or interface worth spelling out. + +When `.docs/` ends up with more than two markdown files (counting beyond `AGENTS.md` and the spec/architecture file), add a `README.md` inside `.docs/` that acts as an index. One short line per file explaining what it's for. + +### Project metadata + +A project's `CLAUDE.md` (or `.docs/README.md` if there is one) can include a metadata block at the top describing the project's context. This helps you apply rules at the right strictness level. Useful fields: + +- **Reference project**: a path or name of an existing project whose style or conventions I want this one to follow. Multiple references can be listed in priority order, with the first being the strongest influence and each subsequent one a step below. If two references contradict each other on something specific, ask me which to follow. +- **Visibility**: `public` or `private`. +- **Owner**: `me`, `employer`, or `other` (forked repo, collaborator project I'm helping with, etc.). +- **Assistance level**: `hints`, `suggestions`, or `solutions`. Controls how much I want spelled out when I hit a problem. Default is `solutions` (give me the answer). The one exception: Rust projects I own default to `hints`, since I'm actively learning Rust. + +Strictness comes from combining the two: a project is relaxed only when it's both `private` and owned by `me`. Anything else (public regardless of owner, or any visibility when the owner isn't me) gets the strict treatment, with the rules in this file followed closely. When the owner isn't me, the owning party's existing conventions take precedence over mine if there's a conflict, so check the project's existing files and patterns before assuming any rule from this file applies. + +If a project has no metadata block, assume the worst case: public, not owned by me, strict. + +If a metadata block is present, treat it as authoritative. Metadata reflects intent, not current state. I might set `public` for a repo that's currently private on GitHub but I plan to share, or `private` for one I'm pulling back. Follow the metadata, not the GitHub setting. + +What each assistance level means: + +- `hints`: don't give the answer. Nudge with leading questions, doc references, or pointers at where to look. Make me work it out. +- `suggestions`: describe the approach in plain language without writing the code. "You could use a `match` here with arms for `Some` and `None`," or "Implement the `From` trait for this conversion." +- `solutions`: give me the answer or code directly. + +Don't escalate on your own. If `hints` aren't enough, I'll ask for `suggestions`. If `suggestions` aren't enough, I'll ask for `solutions`. Each new problem in a session starts at the project's default level. The exception is when I ask for a specific level on a one-off basis ("just give me the answer here"). + +#### Keeping metadata fresh + +The metadata block is meant to evolve with the project. Two cases to watch for: + +- If a metadata block exists but is missing one of the always-required fields (`owner`, `visibility`, `assistance`), ask me what the value should be rather than silently falling back to the worst-case default. Same for open-ended fields like `reference project` when the context suggests one ought to be set. +- If a project has no `CLAUDE.md` at the root and no `AGENTS.md` anywhere (root or `.docs/`), but the project has grown enough to warrant rules (it has a `.docs/SPEC.md` or `.docs/ARCHITECTURE.md`, or it's clearly past the toy stage), suggest creating one. The default placement is `.docs/AGENTS.md` with a symlink at the project root as `CLAUDE.md`. At minimum the new file should establish the metadata block so future sessions have something to anchor on. + +Note on the symlink-to-`.docs/AGENTS.md` pattern: this only works for personal projects where you don't expect anyone else to clone the repo, since `.docs/` is gitignored and the symlink would be broken on a fresh clone. For projects intended to be shared with collaborators, create `CLAUDE.md` as a real committed file at the project root instead, with `.docs/AGENTS.md` serving as your private extended notes. + +Related: if `git status` shows an LLM rules file being added or modified, and the file is a symlink whose target sits outside the repo or inside a gitignored directory (typically `.docs/`), ask whether the commit was intended. If yes, default to replacing the symlink with a real file containing the resolved content before committing. Only leave it as a symlink if I tell you there's a specific reason to keep it that way. Committing a broken symlink means anyone cloning the repo ends up with a rules file that fails to read, which defeats the point of committing it. + +By "LLM rules file" I mean any of the standard project-instruction conventions used by AI coding tools: + +- `CLAUDE.md` (Claude Code, the main one for me) +- `AGENTS.md` (OpenAI Codex, also adopted as a cross-tool convention) +- `GEMINI.md` (Gemini CLI) +- `.cursorrules` or files under `.cursor/rules/` (Cursor) +- `.windsurfrules` (Windsurf) +- `.github/copilot-instructions.md` (GitHub Copilot) +- any other well-known equivalent you recognize that I may have missed + +`CLAUDE.md` is the one that matters most to me, but I sometimes use other tools alongside it, so the same rule applies to all of them. + +## TODO files + +TODO content for a project lives in exactly one place. Where depends on whether I want it public or private: + +- Public TODO: rolled into the project's root `README.md`, or split out as a `TODO.md` at the project root when the list gets too noisy for a README. Lately I prefer rolling it into the README. +- Private TODO: kept as a `TODO.md` inside `.docs/`. Since `.docs/` is its own gitignored repo, the TODO never shows up publicly. + +Either location is valid. The important thing is there should be only one canonical TODO for the project. If you find TODOs in two places, one is almost certainly stale and worth flagging. If both look reasonable and current, ask which one I want to keep. + +## Toolchains + +Toolchains are managed via `mise`. Check `mise list` and the project's `mise.toml` (or `.mise.toml`) before reaching for `brew install` for a language runtime. + +--- + +When this file's defaults (commit format, branch naming, code style, file structure) conflict with what a project already does, follow the project. The rules here are my defaults for new or greenfield projects, not laws to impose on existing ones. The exceptions are the safety and personal-preference rules: no committing secrets, no em-dashes mid-sentence, no force-pushing without permission. These apply everywhere regardless of project convention. + +Any of these can be overridden when I say so explicitly. When you're not sure whether something here applies, ask before breaking it. + +@~/.claude/preferences.md diff --git a/config/claude/settings.json b/config/claude/settings.json new file mode 100644 index 0000000..099411c --- /dev/null +++ b/config/claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "defaultMode": "default" + }, + "statusLine": { + "type": "command", + "command": "bash ~/.claude/statusline-command.sh" + } +} diff --git a/config/claude/statusline-command.sh b/config/claude/statusline-command.sh new file mode 100644 index 0000000..8460e3e --- /dev/null +++ b/config/claude/statusline-command.sh @@ -0,0 +1,689 @@ +#!/usr/bin/env bash +# Claude Code statusline +# +# Renders a multi-line status bar below the Claude Code prompt. Receives JSON +# via stdin describing the current session (model, context window, rate limits, +# workspace path). Outputs ANSI-colored text to stdout. +# +# Requires: jq (https://jqlang.github.io/jq/) +# +# The directory line mimics Starship's default prompt format (directory, git, +# package version, language runtimes) using Nerd Font glyphs for icons. +# Glyph codepoints sourced from: +# https://github.com/tsilenzio/dotfiles/blob/main/config/starship/starship.toml +# +# Structure: +# 1. DATA — parse input, detect environment, set variables +# 2. PRIMITIVES — reusable drawing functions (bars, gradients, colors) +# 3. LAYOUT — compose the final output (edit this section to restyle) +# +# Environment variables: +# CLAUDE_STATUSLINE_DEBUG=1 — dump raw JSON and computed values to ~/.claude/statusline-debug.* +# CLAUDE_STATUSLINE_BLUE — override gradient stop (R;G;B), default: 70;130;200 +# CLAUDE_STATUSLINE_GREEN — override gradient stop (R;G;B), default: 60;170;80 +# CLAUDE_STATUSLINE_YELLOW — override gradient stop (R;G;B), default: 255;160;0 +# CLAUDE_STATUSLINE_ORANGE — override gradient stop (R;G;B), default: 190;120;40 +# CLAUDE_STATUSLINE_RED — override gradient stop (R;G;B), default: 255;20;10 +# CLAUDE_STATUSLINE_GRAY — override burn-rate neutral (R;G;B), default: 130;130;130 + +input=$(cat) + +# Debug: dump raw input JSON to /tmp/claude-statusline//.json +# Enable via env var at launch or touch /tmp/claude-statusline-debug to toggle live +if [ "${CLAUDE_STATUSLINE_DEBUG:-0}" = "1" ] || [ -f /tmp/claude-statusline-debug ]; then + debug_session=$(echo "$input" | jq -r '.session_id // "unknown"') + debug_dir="/tmp/claude-statusline/${debug_session}" + mkdir -p "$debug_dir" 2>/dev/null + echo "$input" | jq . > "${debug_dir}/$(date +%Y%m%d-%H%M%S).json" 2>/dev/null +fi + +# ========================================================================== +# DATA — parse input, detect environment, set variables +# ========================================================================== + +# Constants +CTX_LIMIT_1M=1000000 +CTX_LIMIT_200K=200000 +WINDOW_5H=18000 # 5 hours in seconds +WINDOW_7D=604800 # 7 days in seconds +SECS_PER_DAY=86400 +SECS_PER_HOUR=3600 +BAR_WIDTH=20 + +# Nerd font glyphs via ANSI-C escapes. +# BMP private-use area glyphs get stripped by some text pipelines; these survive. +ICON_GIT=$'\uF418' +ICON_PKG=$'\U000F03D7' +ICON_NODEJS=$'\uE718' +ICON_PYTHON=$'\uE235' +ICON_RUST=$'\U000F1617' +ICON_GOLANG=$'\uE627' +ICON_RUBY=$'\uE791' +ICON_ELIXIR=$'\uE62D' +ICON_DENO=$'\uE7C0' +ICON_BUN=$'\uE76F' +ICON_PHP=$'\uE608' +ICON_LUA=$'\uE620' +ICON_JAVA=$'\uE256' +ICON_KOTLIN=$'\uE634' +ICON_SCALA=$'\uE737' +ICON_HASKELL=$'\uE777' +ICON_OCAML=$'\uE67A' +ICON_DART=$'\uE798' +ICON_SWIFT=$'\uE755' +ICON_ZIG=$'\uE6A9' +ICON_CRYSTAL=$'\uE62F' +ICON_PERL=$'\uE67E' +ICON_TF=$'\uE69A' + +# ANSI styling +RESET=$'\033[0m' +DIM=$'\033[2m' +BOLD=$'\033[1m' +RED=$'\033[31m' +YELLOW=$'\033[33m' +GREEN=$'\033[32m' +CYAN=$'\033[36m' +PURPLE=$'\033[35m' +ORANGE=$'\033[38;5;208m' +FADED_WHITE=$'\033[38;2;230;230;230m' + +# Bar drawing +TICK=$'\u258F' # flush-left partial block for position markers +BAR_EMPTY_RGB="40;40;40" # empty bar cell background +TICK_FILLED_RGB="255;255;255" # tick foreground inside filled bar +TICK_EMPTY_RGB="200;200;200" # tick foreground inside empty bar +HEADER_LINE_RGB="80;80;80" # section header underline color + +# Gradient palette — override with CLAUDE_STATUSLINE_='R;G;B' env vars +SL_BLUE="${CLAUDE_STATUSLINE_BLUE:-70;130;200}" +SL_GREEN="${CLAUDE_STATUSLINE_GREEN:-60;170;80}" +SL_YELLOW="${CLAUDE_STATUSLINE_YELLOW:-255;160;0}" +SL_ORANGE="${CLAUDE_STATUSLINE_ORANGE:-190;120;40}" +SL_RED="${CLAUDE_STATUSLINE_RED:-255;20;10}" +SL_GRAY="${CLAUDE_STATUSLINE_GRAY:-130;130;130}" + +IFS=';' read -r SL_BLUE_R SL_BLUE_G SL_BLUE_B <<< "$SL_BLUE" +IFS=';' read -r SL_GREEN_R SL_GREEN_G SL_GREEN_B <<< "$SL_GREEN" +IFS=';' read -r SL_YELLOW_R SL_YELLOW_G SL_YELLOW_B <<< "$SL_YELLOW" +IFS=';' read -r SL_ORANGE_R SL_ORANGE_G SL_ORANGE_B <<< "$SL_ORANGE" +IFS=';' read -r SL_RED_R SL_RED_G SL_RED_B <<< "$SL_RED" +IFS=';' read -r SL_GRAY_R SL_GRAY_G SL_GRAY_B <<< "$SL_GRAY" + +# Parse all JSON fields in a single jq call (tab-separated to handle spaces in values) +# Use newline-separated output to avoid IFS tab-collapsing empty fields +{ +read -r cwd +read -r model_name +read -r model_id +read -r ctx_tokens +read -r ctx_limit +read -r ctx_pct_raw +read -r rate_5h_pct +read -r rate_5h_resets_at +read -r rate_7d_pct +read -r rate_7d_resets_at +} <<< "$(echo "$input" | jq -r ' + (.workspace.current_dir // .cwd // ""), + (.model.display_name // ""), + (.model.id // ""), + (.context_window.current_usage | + if . == null then "" + else ((.input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation_input_tokens // 0) | tostring) + end), + (.context_window.context_window_size // ""), + (.context_window.used_percentage // ""), + (.rate_limits.five_hour.used_percentage // "" | if . != "" then floor else . end), + (.rate_limits.five_hour.resets_at // ""), + (.rate_limits.seven_day.used_percentage // "" | if . != "" then floor else . end), + (.rate_limits.seven_day.resets_at // "") +')" + +dir_name=$(basename "$cwd") + +# Context window limit from JSON, falling back to model ID heuristic +if [ -z "$ctx_limit" ]; then + if [[ "$model_id" == *"[1m]"* ]]; then + ctx_limit=$CTX_LIMIT_1M + else + ctx_limit=$CTX_LIMIT_200K + fi +fi + +ctx_pct="" +ctx_used_k="" +ctx_limit_label="" +ctx_tick=-1 +has_ctx=false + +if [ -n "$ctx_tokens" ] && [ "$ctx_tokens" != "0" ]; then + ctx_used_k=$(awk "BEGIN { printf \"%.1f\", $ctx_tokens / 1000 }") + if [ -n "$ctx_pct_raw" ]; then + ctx_pct=$(printf '%.0f' "$ctx_pct_raw") + else + ctx_pct=$(awk "BEGIN { printf \"%.0f\", $ctx_tokens / $ctx_limit * 100 }") + fi + has_ctx=true + if [ "$ctx_limit" -eq "$CTX_LIMIT_1M" ]; then + ctx_limit_label="1m" + # Marks where a 200k context window would end on 1M models + ctx_tick=$((BAR_WIDTH * 20 / 100)) + else + ctx_limit_label="200k" + fi +fi + +# Rate limits +now=$(date +%s) + +compute_elapsed_pct() { + local resets_at=$1 window_secs=$2 + local remaining=$((resets_at - now)) + [ "$remaining" -lt 0 ] && remaining=0 + local elapsed=$((window_secs - remaining)) + [ "$elapsed" -lt 0 ] && elapsed=0 + awk "BEGIN { printf \"%.0f\", $elapsed / $window_secs * 100 }" +} + +format_remaining() { + local resets_at=$1 + local secs=$((resets_at - now)) + [ "$secs" -lt 0 ] && secs=0 + if [ "$secs" -ge "$SECS_PER_DAY" ]; then + printf '%dd %dh' $((secs / SECS_PER_DAY)) $(( (secs % SECS_PER_DAY) / SECS_PER_HOUR )) + elif [ "$secs" -ge "$SECS_PER_HOUR" ]; then + printf '%dh %02dm' $((secs / SECS_PER_HOUR)) $(( (secs % SECS_PER_HOUR) / 60 )) + else + printf '%dm' $((secs / 60)) + fi +} + +has_rate_5h=false +rate_5h_elapsed_pct="" +rate_5h_remaining="" +if [ -n "$rate_5h_pct" ] && [ -n "$rate_5h_resets_at" ]; then + rate_5h_elapsed_pct=$(compute_elapsed_pct "$rate_5h_resets_at" "$WINDOW_5H") + rate_5h_remaining=$(format_remaining "$rate_5h_resets_at") + has_rate_5h=true +fi + +has_rate_7d=false +rate_7d_elapsed_pct="" +rate_7d_remaining="" +if [ -n "$rate_7d_pct" ] && [ -n "$rate_7d_resets_at" ]; then + rate_7d_elapsed_pct=$(compute_elapsed_pct "$rate_7d_resets_at" "$WINDOW_7D") + rate_7d_remaining=$(format_remaining "$rate_7d_resets_at") + has_rate_7d=true +fi + +has_rate_limits=false +($has_rate_5h || $has_rate_7d) && has_rate_limits=true + +# Git +git_branch="" +git_status_chars="" +if [ -n "$cwd" ] && git -C "$cwd" rev-parse --is-inside-work-tree &>/dev/null; then + git_branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null \ + || git -C "$cwd" rev-parse --short HEAD 2>/dev/null) + if [ -n "$git_branch" ]; then + porcelain=$(git -C "$cwd" status --porcelain 2>/dev/null) + if [ -n "$porcelain" ]; then + git_status_chars=$(echo "$porcelain" | awk ' + /^.M/ { m=1 } + /^[MARCD]/ { s=1 } + /^\?\?/ { u=1 } + END { if(m) printf "!"; if(s) printf "+"; if(u) printf "?" } + ') + fi + fi +fi + +# Package version +pkg_ver="" +if [ -f "$cwd/Cargo.toml" ]; then + pkg_ver=$(grep -m1 '^version' "$cwd/Cargo.toml" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/') +elif [ -f "$cwd/package.json" ]; then + pkg_ver=$(grep -m1 '"version"' "$cwd/package.json" 2>/dev/null | sed -E 's/.*"version"[^"]*"([^"]+)".*/\1/') +elif [ -f "$cwd/pyproject.toml" ]; then + pkg_ver=$(grep -m1 '^version' "$cwd/pyproject.toml" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/') +elif [ -f "$cwd/composer.json" ]; then + pkg_ver=$(grep -m1 '"version"' "$cwd/composer.json" 2>/dev/null | sed -E 's/.*"version"[^"]*"([^"]+)".*/\1/') +fi + +# Language detection helpers +_pin() { + local lang="$1" f v + for f in "$cwd/.tool-versions" "$cwd/mise.toml" "$cwd/.mise.toml"; do + [ -f "$f" ] || continue + if [[ "$f" == *".tool-versions" ]]; then + v=$(grep -m1 "^${lang}[[:space:]]" "$f" 2>/dev/null | awk '{print $2}') + else + v=$(grep -m1 "^${lang}[[:space:]]*=" "$f" 2>/dev/null | sed 's/.*=[[:space:]]*"\([^"]*\)".*/\1/') + fi + [ -n "$v" ] && { echo "$v"; return; } + done +} + +# langs array: each entry is "icon;ver;color" for the layout to iterate +langs=() + +_detect() { + local icon="$1" ver="$2" color="$3" + local v="" + [ -n "$ver" ] && v=" v${ver}" + langs+=("${icon};${ver};${color}") +} + +# Rust +if [ -f "$cwd/Cargo.toml" ] || [ -f "$cwd/rust-toolchain.toml" ]; then + ver=$(_pin "rust") + if [ -z "$ver" ] && [ -f "$cwd/rust-toolchain.toml" ]; then + ver=$(grep -m1 '^channel' "$cwd/rust-toolchain.toml" 2>/dev/null | sed 's/.*"\([^"]*\)".*/\1/') + fi + [ -z "$ver" ] && command -v rustc &>/dev/null && ver=$(rustc --version 2>/dev/null | awk '{print $2}') + _detect "$ICON_RUST" "$ver" "$RED" +fi + +# Node +if [ -f "$cwd/package.json" ] || [ -f "$cwd/.nvmrc" ]; then + ver=$(_pin "nodejs") + [ -z "$ver" ] && [ -f "$cwd/.nvmrc" ] && ver=$(tr -d '[:space:]v' < "$cwd/.nvmrc" 2>/dev/null) + [ -z "$ver" ] && command -v node &>/dev/null && ver=$(node --version 2>/dev/null | tr -d 'v') + _detect "$ICON_NODEJS" "$ver" "$GREEN" +fi + +# Python +if [ -f "$cwd/pyproject.toml" ] || [ -f "$cwd/requirements.txt" ] || [ -f "$cwd/Pipfile" ] || [ -f "$cwd/.python-version" ]; then + ver=$(_pin "python") + [ -z "$ver" ] && [ -f "$cwd/.python-version" ] && ver=$(tr -d '[:space:]' < "$cwd/.python-version" 2>/dev/null) + [ -z "$ver" ] && command -v python3 &>/dev/null && ver=$(python3 --version 2>/dev/null | awk '{print $2}') + _detect "$ICON_PYTHON" "$ver" "$YELLOW" +fi + +# Go +if [ -f "$cwd/go.mod" ]; then + ver=$(_pin "golang") + [ -z "$ver" ] && ver=$(grep -m1 '^go ' "$cwd/go.mod" 2>/dev/null | awk '{print $2}') + _detect "$ICON_GOLANG" "$ver" "$CYAN" +fi + +# Ruby +if [ -f "$cwd/Gemfile" ]; then + ver=$(_pin "ruby") + [ -z "$ver" ] && command -v ruby &>/dev/null && ver=$(ruby --version 2>/dev/null | awk '{print $2}') + _detect "$ICON_RUBY" "$ver" "$RED" +fi + +# Elixir +if [ -f "$cwd/mix.exs" ]; then + ver=$(_pin "elixir") + _detect "$ICON_ELIXIR" "$ver" "$PURPLE" +fi + +# Deno +if [ -f "$cwd/deno.json" ] || [ -f "$cwd/deno.jsonc" ]; then + ver=$(_pin "deno") + _detect "$ICON_DENO" "$ver" "$GREEN" +fi + +# Bun +if [ -f "$cwd/bun.lockb" ]; then + ver=$(_pin "bun") + _detect "$ICON_BUN" "$ver" "$RED" +fi + +# PHP +if [ -f "$cwd/composer.json" ]; then + ver=$(_pin "php") + _detect "$ICON_PHP" "$ver" "$PURPLE" +fi + +# Lua +if [ -f "$cwd/.lua-version" ] || ls "$cwd"/*.lua &>/dev/null; then + ver=$(_pin "lua") + [ -z "$ver" ] && command -v lua &>/dev/null && ver=$(lua -v 2>&1 | awk '{print $2}') + _detect "$ICON_LUA" "$ver" "$CYAN" +fi + +# Java +if [ -f "$cwd/pom.xml" ] || [ -f "$cwd/build.gradle.kts" ] || [ -f "$cwd/build.sbt" ] || [ -f "$cwd/.java-version" ]; then + ver=$(_pin "java") + _detect "$ICON_JAVA" "$ver" "$RED" +fi + +# Kotlin +if ls "$cwd"/*.kt "$cwd"/*.kts &>/dev/null; then + ver=$(_pin "kotlin") + _detect "$ICON_KOTLIN" "$ver" "$PURPLE" +fi + +# Scala +if [ -f "$cwd/build.sbt" ] || [ -f "$cwd/.scalaenv" ] || [ -f "$cwd/.sbtenv" ]; then + ver=$(_pin "scala") + _detect "$ICON_SCALA" "$ver" "$RED" +fi + +# Haskell +if [ -f "$cwd/stack.yaml" ] || [ -f "$cwd/cabal.project" ]; then + ver=$(_pin "haskell") + [ -z "$ver" ] && command -v ghc &>/dev/null && ver=$(ghc --numeric-version 2>/dev/null) + _detect "$ICON_HASKELL" "$ver" "$PURPLE" +fi + +# OCaml +if [ -f "$cwd/dune" ] || [ -f "$cwd/dune-project" ] || [ -f "$cwd/.merlin" ]; then + ver=$(_pin "ocaml") + [ -z "$ver" ] && command -v ocaml &>/dev/null && ver=$(ocaml -vnum 2>/dev/null) + _detect "$ICON_OCAML" "$ver" "$YELLOW" +fi + +# Dart +if [ -f "$cwd/pubspec.yaml" ] || [ -f "$cwd/pubspec.yml" ] || [ -f "$cwd/pubspec.lock" ]; then + ver=$(_pin "dart") + [ -z "$ver" ] && command -v dart &>/dev/null && ver=$(dart --version 2>&1 | awk '{print $4}') + _detect "$ICON_DART" "$ver" "$CYAN" +fi + +# Swift +if [ -f "$cwd/Package.swift" ]; then + ver=$(_pin "swift") + [ -z "$ver" ] && command -v swift &>/dev/null && ver=$(swift --version 2>/dev/null | awk '{print $4}') + _detect "$ICON_SWIFT" "$ver" "$ORANGE" +fi + +# Zig +if [ -f "$cwd/build.zig" ] || [ -f "$cwd/build.zig.zon" ]; then + ver=$(_pin "zig") + [ -z "$ver" ] && command -v zig &>/dev/null && ver=$(zig version 2>/dev/null) + _detect "$ICON_ZIG" "$ver" "$YELLOW" +fi + +# Crystal +if [ -f "$cwd/shard.yml" ]; then + ver=$(_pin "crystal") + [ -z "$ver" ] && command -v crystal &>/dev/null && ver=$(crystal --version 2>/dev/null | awk 'NR==1{print $2}') + _detect "$ICON_CRYSTAL" "$ver" "$PURPLE" +fi + +# Perl +if [ -f "$cwd/Makefile.PL" ] || [ -f "$cwd/Build.PL" ] || [ -f "$cwd/cpanfile" ] || [ -f "$cwd/.perl-version" ]; then + ver=$(_pin "perl") + [ -z "$ver" ] && command -v perl &>/dev/null && ver=$(perl -e 'printf "%vd",$^V' 2>/dev/null) + _detect "$ICON_PERL" "$ver" "$CYAN" +fi + +# Terraform (icon only, no version) +if ls "$cwd"/*.tf "$cwd"/*.tfvars &>/dev/null; then + _detect "$ICON_TF" "" "$PURPLE" +fi + +# Debug: dump computed values alongside the raw JSON +if [ "${CLAUDE_STATUSLINE_DEBUG:-0}" = "1" ] || [ -f /tmp/claude-statusline-debug ]; then + { + echo "=== Computed Values ===" + echo "cwd=$cwd" + echo "dir_name=$dir_name" + echo "model_name=$model_name" + echo "model_id=$model_id" + echo "ctx_tokens=$ctx_tokens ctx_pct=$ctx_pct ctx_used_k=$ctx_used_k" + echo "ctx_limit=$ctx_limit ctx_limit_label=$ctx_limit_label ctx_tick=$ctx_tick" + echo "has_ctx=$has_ctx has_rate_limits=$has_rate_limits" + echo "has_rate_5h=$has_rate_5h rate_5h_pct=$rate_5h_pct rate_5h_elapsed_pct=$rate_5h_elapsed_pct rate_5h_remaining=$rate_5h_remaining" + echo "has_rate_7d=$has_rate_7d rate_7d_pct=$rate_7d_pct rate_7d_elapsed_pct=$rate_7d_elapsed_pct rate_7d_remaining=$rate_7d_remaining" + echo "git_branch=$git_branch git_status_chars=$git_status_chars" + echo "pkg_ver=$pkg_ver" + echo "langs=(${langs[*]})" + } > "${debug_dir}/$(date +%Y%m%d-%H%M%S)-computed.txt" 2>/dev/null +fi + +# ========================================================================== +# PRIMITIVES — reusable drawing functions +# ========================================================================== + +# 5-stop usage gradient: blue -> green -> yellow -> orange -> red (0-100%) +# Maps a percentage to an R;G;B string for coloring percentage text and context bars. +usage_gradient_rgb() { + awk -v pct="$1" \ + -v br="$SL_BLUE_R" -v bg="$SL_BLUE_G" -v bb="$SL_BLUE_B" \ + -v gr="$SL_GREEN_R" -v gg="$SL_GREEN_G" -v gb="$SL_GREEN_B" \ + -v yr="$SL_YELLOW_R" -v yg="$SL_YELLOW_G" -v yb="$SL_YELLOW_B" \ + -v or_="$SL_ORANGE_R" -v og="$SL_ORANGE_G" -v ob="$SL_ORANGE_B" \ + -v rr="$SL_RED_R" -v rg="$SL_RED_G" -v rb="$SL_RED_B" \ + 'function interp(a,b,t) { return int(a+(b-a)*t+0.5) } + BEGIN { + t = pct / 100; if (t > 1) t = 1; if (t < 0) t = 0 + if (t <= 0.25) { + s = t / 0.25 + r = interp(br,gr,s); g = interp(bg,gg,s); b = interp(bb,gb,s) + } else if (t <= 0.5) { + s = (t-0.25) / 0.25 + r = interp(gr,yr,s); g = interp(gg,yg,s); b = interp(gb,yb,s) + } else if (t <= 0.75) { + s = (t-0.5) / 0.25 + r = interp(yr,or_,s); g = interp(yg,og,s); b = interp(yb,ob,s) + } else { + s = (t-0.75) / 0.25 + r = interp(or_,rr,s); g = interp(og,rg,s); b = interp(ob,rb,s) + } + printf "%d;%d;%d", r, g, b + }' +} + +# 5-stop burn rate gradient: blue -> green -> gray -> orange -> red +# Ratio = usage_pct / elapsed_pct. Gray at 1.0 means on-pace. +# Used for rate limit bar fill and time-remaining label coloring. +burn_rate_rgb() { + local pct=$1 elapsed_pct=$2 + awk -v pct="$pct" -v ep="$elapsed_pct" \ + -v br="$SL_BLUE_R" -v bg_="$SL_BLUE_G" -v bb="$SL_BLUE_B" \ + -v gr="$SL_GREEN_R" -v gg="$SL_GREEN_G" -v gb="$SL_GREEN_B" \ + -v xr="$SL_GRAY_R" -v xg="$SL_GRAY_G" -v xb="$SL_GRAY_B" \ + -v or_="$SL_ORANGE_R" -v og="$SL_ORANGE_G" -v ob="$SL_ORANGE_B" \ + -v rr="$SL_RED_R" -v rg="$SL_RED_G" -v rb="$SL_RED_B" \ + 'function interp(a,b,t) { return int(a+(b-a)*t+0.5) } + BEGIN { + if (ep < 1) ep = 1 + ratio = pct / ep + if (ratio < 0.4) { + r = br; g = bg_; b = bb + } else if (ratio < 0.7) { + t = (ratio - 0.4) / 0.3 + r = interp(br,gr,t); g = interp(bg_,gg,t); b = interp(bb,gb,t) + } else if (ratio < 1.0) { + t = (ratio - 0.7) / 0.3 + r = interp(gr,xr,t); g = interp(gg,xg,t); b = interp(gb,xb,t) + } else if (ratio < 1.3) { + t = (ratio - 1.0) / 0.3 + r = interp(xr,or_,t); g = interp(xg,og,t); b = interp(xb,ob,t) + } else if (ratio < 2.0) { + t = (ratio - 1.3) / 0.7 + r = interp(or_,rr,t); g = interp(og,rg,t); b = interp(ob,rb,t) + } else { + r = rr; g = rg; b = rb + } + printf "%d;%d;%d", r, g, b + }' +} + +# draw_bar [tick_pos] +# +# Draws a horizontal bar. Caller computes the fill color and passes RGB in. +# tick_pos is optional: omit or pass -1 to skip the position marker. +draw_bar() { + local pct=$1 w=$2 fr=$3 fg=$4 fb=$5 tick=${6:--1} + local fill=$((w * pct / 100)) + [ "$tick" -ge "$w" ] 2>/dev/null && tick=$((w - 1)) + + IFS=';' read -r er eg eb <<< "$BAR_EMPTY_RGB" + IFS=';' read -r tfr tfg tfb <<< "$TICK_FILLED_RGB" + IFS=';' read -r ter teg teb <<< "$TICK_EMPTY_RGB" + + for ((i=0; i= 0 && i == tick)); then + printf '\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm%s\033[0m' "$tfr" "$tfg" "$tfb" "$fr" "$fg" "$fb" "$TICK" + else + printf '\033[48;2;%d;%d;%dm \033[0m' "$fr" "$fg" "$fb" + fi + else + if ((tick >= 0 && i == tick)); then + printf '\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm%s\033[0m' "$ter" "$teg" "$teb" "$er" "$eg" "$eb" "$TICK" + else + printf '\033[48;2;%d;%d;%dm \033[0m' "$er" "$eg" "$eb" + fi + fi + done +} + +# rgb_fg "R;G;B" "text" — print text in the given foreground color +rgb_fg() { + IFS=';' read -r r g b <<< "$1" + printf '\033[38;2;%d;%d;%dm%s\033[0m' "$r" "$g" "$b" "$2" +} + +# pad_to +# Pads with spaces so the content starts at the target column. +pad_to() { + local used=$1 target=$2; shift 2 + local gap=$((target - used)) + [ "$gap" -lt 0 ] && gap=0 + printf '%*s%s' "$gap" "" "$@" +} + +# render_rate_bar