https://github.com/tmck-code/yet-another-statusline
A statusline for Claude Code inspired by terminal monitor programs
https://github.com/tmck-code/yet-another-statusline
ansi claude monitoring-tool terminal
Last synced: 11 days ago
JSON representation
A statusline for Claude Code inspired by terminal monitor programs
- Host: GitHub
- URL: https://github.com/tmck-code/yet-another-statusline
- Owner: tmck-code
- License: bsd-3-clause
- Created: 2026-04-22T12:35:06.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-07-17T14:39:10.000Z (23 days ago)
- Last Synced: 2026-07-17T16:08:03.360Z (23 days ago)
- Topics: ansi, claude, monitoring-tool, terminal
- Language: Python
- Homepage:
- Size: 3.35 MB
- Stars: 229
- Watchers: 1
- Forks: 23
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Notice: NOTICE
Awesome Lists containing this project
README
# YAS! (Yet Another Statusline)
🌈 Check out the official landing page here: [YAS! Yet Another Statusline](https://tmck-code.github.io/pages/yas.html)

_Most common form is displaying the first few rows, which include the loaded plugins & skills. Extra sections appear below them as needed_
## Install/Update
Requires Python 3.10+, and a [Nerd Font](https://www.nerdfonts.com/font-downloads) to render the icons.
```bash
curl -fsSL https://raw.githubusercontent.com/tmck-code/yet-another-statusline/main/ops/install.sh | bash
```
Alongside `statusLine.command`, the installer also wires a `UserPromptSubmit`
hook (`yas-prompt-hook.py`) that records per-session prompt timestamps, enabling
**accurate turn-scoped subagent display**. It is upserted idempotently — foreign
hooks are preserved, stale paths are rewritten on upgrade — and removed on
uninstall.
Or install manually:
```bash
claude plugin marketplace add tmck-code/yet-another-statusline
claude plugin install yas@yet-another-statusline
claude -p "/yas:init"
```
If you need to install non-interactively (e.g. in CI/docker)
```bash
curl -fsSL https://raw.githubusercontent.com/tmck-code/yet-another-statusline/main/ops/install.sh | YAS_NO_TTY=1 bash
# install with specific python version e.g. 3.15
curl -fsSL https://raw.githubusercontent.com/tmck-code/yet-another-statusline/main/ops/install.sh | YAS_NO_TTY=1 YAS_PYTHON=3.15 bash
```
### Reconfigure later — `/yas:config`
Run `/yas:config` any time to re-run the wizard against the already-installed
plugin — switch theme/glyph mode, toggle labels, change the soft limit, or move
to Python **3.15**. It re-wires `settings.json` without re-registering the
marketplace or reinstalling the plugin.
## Demo
A dummy session to demonstrate the layout:

## Widths
The statusline also renders differently according to available width
| mode | width | screenshot |
|------|-------|------------|
| "medium" | <=80 pixels |
|
| "narrow" | <=55 pixels |
|
---
## Configuration
Every configurable knob resolves through one fixed precedence chain (highest wins):
```
CLI flag → canonical YAS_* env var → legacy-alias env var → yas.toml → built-in default
```
The first source in that chain that is **present and valid** wins; an absent or
invalid source falls through to the next (an empty-string env var counts as
absent). Canonical `YAS_*` env vars always win over their deprecated legacy
aliases when both are set — the aliases keep working but are deprecated.
### Knobs
| Knob | Env var | Legacy alias | `yas.toml` key | Default |
|------|---------|--------------|----------------|---------|
| `max_width` | `YAS_MAX_WIDTH` | — | `[layout].max_width` | `140` |
| `full_width` | `YAS_FULL_WIDTH` | — | `[layout].full_width` | `false` |
| `soft_limit` | `YAS_SOFT_LIMIT` | — | `[tokens].soft_limit` | `150000` |
| `token_window` | `YAS_TOKEN_WINDOW` | `STATUSLINE_TOKEN_WINDOW` | `[tokens].token_window` | `60` |
| `theme` | `YAS_THEME` (also `--theme` CLI) | `CLAUDE_STATUSLINE_THEME` | `[appearance].theme` | `claude-dark` |
| `bg_shift` | `YAS_BG_SHIFT` (also `--bg-shift` CLI) | — | `[appearance].bg_shift` | `warm` |
| `glyph_mode` | `YAS_GLYPH_MODE` (also `--glyph-mode` CLI) | — | `[appearance.glyphs].mode` | `nerdfont` |
| `single_width` | `YAS_GLYPH_SINGLE_WIDTH` (also `--glyph-single-width` CLI) | — | `[appearance.glyphs].single_width` | `false` |
| `context_state` | `YAS_CONTEXT_STATE` | — | `[context].state` | `false` |
| `context_labels` | `YAS_CONTEXT_LABELS` | — | `[context].labels` | `Smart,Coasting,Foggy,Cooked,Dumb` |
| `context_thresholds` | `YAS_CONTEXT_THRESHOLDS` | — | `[context].thresholds` | `25,50,70,90` |
| `show_render_time` | `YAS_SHOW_RENDER_TIME` | — | `[layout].show_render_time` | `false` |
| `show_tool_uses` | `YAS_SHOW_TOOL_USES` | — | `[layout].show_tool_uses` | `true` |
#### Valid values
- **`theme`** — 14 built-in themes; unknown or unset falls back to `claude-dark`:
- **Dark:** `claude-dark`, `catppuccin-mocha`, `dracula`, `gruvbox-dark`, `nord`, `one-dark`, `solarized-dark`, `tokyo-night`, `palenight`
- **Light:** `claude-light`, `catppuccin-latte`, `gruvbox-light`, `one-light`, `solarized-light`
- **`bg_shift`** — `warm` or `cool`.
- **`glyph_mode`** — all four modes preserve column geometry; an unknown value falls back to `nerdfont`:
- `nerdfont` — default; full fidelity, needs a Nerd Font.
- `ascii` — every non-ASCII glyph → width-1 ASCII; maximum compatibility.
- `unicode` — only Nerd Font PUA icons → non-PUA Unicode; keeps box/block/arrow glyphs.
- `github` — GitHub-paste-safe: folds every glyph a browser renders double-wide (the box-drawing frame, block/sparkline ramp, and EAW-ambiguous punctuation/icons) to a width-1, EAW-narrow/ASCII stand-in, so a render stays column-aligned when pasted into a GitHub markdown code block.
#### Behaviour notes
- **`single_width`** — orthogonal boolean that folds double-width dynamic content (wide emoji, CJK in branch names/paths) to width-1; combinable with any `glyph_mode`. The statusline's own glyphs are already width-1, so column geometry is preserved.
- **`full_width`** — when `true`, makes the box fill the terminal and ignore `max_width`.
- **`show_render_time`** — when `true`, annotates the bottom-right border with the previous run's wall-clock render time (e.g. `…47.2ms──╯`). Off by default; each run shows the prior run's timing, so it is blank on a session's first render.
- **`show_tool_uses`** — when `true`, adds a row (wide layout only) under the tokens/cost band, listing per-tool `tool_use` counts.
- **CLI flags** — `--theme NAME` / `--bg-shift DIR` also accept the `--theme=NAME` / `--bg-shift=DIR` form. Pass them in the `statusLine.command` of your `~/.claude/settings.json`.
- **Legacy theme file** — `~/.claude/statusline-theme` (contents = a theme name) still works as the lowest-priority theme fallback, below `[appearance].theme`.
### Context state word
`context_state` adds a morphing word to the context line that names how full the
context window is — `Smart → Coasting → Foggy → Cooked → Dumb` — so you can read
the state at a glance instead of doing percentage math. It is **off by default**;
enable it with `YAS_CONTEXT_STATE=1` or `[context].state = true`.
The word is tinted with the same threshold colour as the context bar and sits
just before it; in a narrow box it sheds first (the bar and percentage stay).
- `context_labels` — exactly 5 comma-separated words (`Smart,Coasting,Foggy,Cooked,Dumb` by default).
- `context_thresholds` — exactly 4 strictly-ascending integers in `1..99` (`25,50,70,90` by default), the start percentage of bands 2–5.
The percentage driving the word is YAS's **soft-limit fill ratio** (the same
basis as the context bar), so the word and the bar always agree — the word turns
`Dumb` exactly as the bar fills toward the compaction `soft_limit`.
> Credit: the state-word idea and its default labels/thresholds are ported from
> [**Dumbometer**](https://github.com/MaximoCorrea1/dumbometer) by Maximo Correa
> Rosas (MIT). See [`NOTICE`](NOTICE). Dumbometer maps the word to the *full
> context window*; YAS maps it to the soft-limit fill ratio for bar consistency.
### `yas.toml`
`yas.toml` lives in `CLAUDE_CONFIG_DIR` (defaults to `~/.claude/`). It is **not**
auto-created — its absence simply means all-defaults — and `/yas:init` never
writes it. See [`yas.example.toml`](yas.example.toml) for a fully-commented
template; copy it to `~/.claude/yas.toml` and uncomment what you want.
```toml
[layout]
max_width = 140
[tokens]
soft_limit = 150000
token_window = 60
[appearance]
theme = "claude-dark"
bg_shift = "warm"
[appearance.glyphs]
mode = "nerdfont"
single_width = false
[context]
state = false
labels = ["Smart", "Coasting", "Foggy", "Cooked", "Dumb"]
thresholds = [25, 50, 70, 90]
```
> **`yas.toml` requires Python 3.11+** — it is parsed with the stdlib `tomllib`.
> On Python 3.10 the file is silently skipped; **environment variables work on
> every Python version**.
Bad config never crashes the statusline. A malformed `yas.toml` is ignored
wholesale, and a single bad / out-of-range / wrong-type value drops only that
one knob back to its default. When any `yas.toml` value is rejected, a compact
warning row — `⚠yas.toml: N values ignored (...)` — appears at the bottom of
the box listing the rejected knob names. Detailed per-value reasons go to stderr
only when `YAS_DEBUG` is set.
### Per-model `soft_limit` overrides
Beyond the global `[tokens].soft_limit`, you can declare per-model overrides as
an inline array under `[tokens]`:
```toml
[tokens]
model = [
{ match = "opus", soft_limit = 200000 }, # the whole Opus family
{ match = "opus-4-8[1m]", soft_limit = 1000000 }, # 1M-context variant (longer match wins)
]
```
- `match` is a **case-insensitive plain substring** (no glob/regex), tested
against the model's id and display name.
- When multiple entries match, the **longest** `match` wins; ties break by array
order (first wins). If no entry matches, the global `soft_limit` is used. So to
single out a variant from its family, give the variant the **longer, more
specific** `match` (above, `opus-4-8[1m]` outranks `opus` for the 1M model).
- **A matching per-model override beats the global `soft_limit` from _any_
source — including the `YAS_SOFT_LIMIT` environment variable.** This is the
one documented exception to the "env beats `yas.toml`" rule: specificity beats
source precedence (there is intentionally no per-model env var). It lets you
raise the compaction-risk threshold for a 1M-context model variant distinctly
from the rest of its family.
### Other environment variables
| var | default | description |
|-----|---------|-------------|
| `CLAUDE_CONFIG_DIR` | `~/.claude` | base dir for config/state files (`yas.toml`, theme file, width file, token-rate log, output payloads) |
| `YAS_DEBUG` | _(unset)_ | when set, prints detailed per-value config-rejection reasons to stderr |
| `COLUMNS` | _(unset)_ | terminal-width fallback when tmux / width-file detection fail |
### Terminal width
Width is detected by the first source that returns a positive value:
1. `tmux display-message -p '#{pane_width}'`
2. `~/.claude/terminal-width` file
3. `COLUMNS` env var
4. `shutil.get_terminal_size()` / `/dev/tty` ioctl
---
## Uninstalling
Remove the statusline config and uninstall the plugin in one step:
```bash
curl -fsSL https://raw.githubusercontent.com/tmck-code/yet-another-statusline/main/ops/install.sh | bash -s -- --uninstall --full
```
Or uninstall manually:
```bash
claude -p "/yas:uninstall"
claude plugin uninstall yas@yet-another-statusline
```
`claude plugin uninstall` only deletes the plugin cache — it leaves `statusLine.command`
in `~/.claude/settings.json` pointing at the now-missing script, so the statusline keeps
trying to run. Run the uninstall script (or `/yas:uninstall`) **first** to remove that
config, then uninstall the plugin. Reload Claude Code afterwards.
---
## Commands
```bash
make test # run pytest suite
make demo # animated demo at current terminal width
make statusline/test # same as demo — use during development
make demo/img # render snapshots into demo/
make mon/run # launch multi-session monitor TUI
```
## Contributing
Enable the git pre-commit hooks (runs `ruff` / `mypy` / `pytest` on staged Python before each commit):
```bash
make hooks
```
### Installing via git clone
Edits to the checkout take effect immediately — no reinstall step.
```bash
git clone https://github.com/tmck-code/yet-another-statusline
cd yet-another-statusline
```
Wire `statusLine.command` in `~/.claude/settings.json` to point at the checkout:
```json
"statusLine": {
"async": true,
"command": "python3 \"/path/to/yet-another-statusline/claude/statusline_command.py\"",
"type": "command"
}
```
> **Note:** if you also have the plugin installed, `claude plugin install` will overwrite
> `statusLine.command` back to the plugin cache path. Either uninstall the plugin or bump
> the version in `.claude-plugin/plugin.json` before reinstalling to keep your local path.
For accurate turn-scoped subagent display, also wire the `UserPromptSubmit` hook
(`hooks/yas-prompt-hook.py`) so per-session prompt timestamps are recorded:
```json
"hooks": {
"UserPromptSubmit": [
{ "matcher": "", "hooks": [
{ "type": "command", "command": "python3 \"/path/to/yet-another-statusline/hooks/yas-prompt-hook.py\"" }
] }
]
}
```
> The `curl … | bash` installer wires this hook for you (upserted idempotently —
> foreign hooks preserved, stale paths rewritten on upgrade — and removed on
> uninstall); it is only a manual step for a git-clone install. Without it, the
> statusline falls back to a time-window heuristic for which subagents to show.
This prompts before setting `core.hooksPath`. CI runs the same checks on every push, so the hook is fast local feedback rather than the gate.