Skip to content

Rewrite the statusline in Rust — single binary, cross-platform, near-zero cold start #27

Description

@ropdias

Summary

Replace the bash tokenline.sh with a Rust implementation compiled to a single per-platform binary.
This supersedes #6 (benchmark bash vs Node/TS) and #7 (TypeScript / cross-platform implementation): a benchmark only has meaning against a concrete candidate, and Rust — not TS — is the candidate that fits the constraints below.

This is a learning-motivated rewrite as much as a technical one. No rush — the bash + install.sh path keeps working throughout.

Why — the per-second cold-start constraint

The statusline is spawned as a fresh process on every update (~1×/s): the host pipes a JSON snapshot to stdin, the program prints up to three lines and exits. So the metric that matters is cold start (process boot + execution), not throughput.

Rough cold-start ranges for a spawn-every-second tool:

Option Cold start Note
Rust / Go (compiled binary) ~1–5 ms No runtime to boot.
Bun ~10–20 ms Adds a Bun dependency.
Node .js ~30–80 ms Pays V8 boot every second.
.ts via tsx/ts-node hundreds of ms Transpiles on the fly — non-starter.
bash + jq ~20–80 ms bash is fast, but every jq is a subprocess.

Rust wins because it has no runtime to boot — it jumps straight to main(), no V8/JVM/GC warmup. It also lets us drop jq entirely (parse JSON in-process).

Why not stay in bash — the cross-platform reality

"bash is portable" is mostly a myth: the language is portable, the ecosystem around it is not. The current script already carries the scars:

  • tokenline.sh:44-45 probe GNU vs BSD at runtime (date -d/stat -c vs date -v/stat -f) because macOS ships BSD coreutils.
  • tokenline.sh:54 uses mapfile (bash 4+); macOS ships bash 3.2, so users must brew install bash.
  • Windows has no native bash at all (WSL / Git Bash / Cygwin, each different).

A compiled binary makes cross-platform simpler, not harder: dates, file mtime and JSON are handled in-language with the same code everywhere. One binary per target (x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc, …), no jq, no coreutils-flag minefield.

Prior art / research

The pattern is clear — tools that prioritize raw speed + a single cross-platform binary chose Rust or Go; those that prioritize a config UI/ecosystem chose JS/TS; the one that stayed in shell had to bolt on a C++ daemon to hit its speed target.

Shell prompts (same conceptual pattern — a fast program run on every render):

Project Language Note
Starship Rust Cross-shell, ~5–15 ms render, "drop in and go".
Oh My Posh Go Cross-shell, single binary, low-latency.
Powerlevel10k Zsh + C++ daemon Zsh-only; needed gitstatusd (C++) for speed — evidence that pure shell hits a ceiling.

Claude Code statuslines (our direct neighbours):

Project Language
CCometixLine Rust
ccstatusline TypeScript (React + Ink)
ccusage statusline JS/TS (Node)
CCstatusline (Wzh0718) Python
tokenline (this repo) Bash

CCometixLine is the closest reference — a Claude Code statusline already in Rust; worth studying for Cargo project structure and per-platform binary distribution.

Trade-offs

We gain:

  • Near-zero cold start; drop the jq dependency.
  • Cross-platform as a single codebase, not runtime detection.
  • A normal multi-file source layout (src/parse.rs, src/render.rs, …) with first-class unit tests (cargo test). The single-file constraint exists only for bash copy-paste; the binary stays a single artifact.
  • Readable code (the bash syntax pain goes away).

We lose / pay:

  • "It's one readable text file you can audit" → an opaque binary.
  • A release pipeline: a CI matrix build (Linux/macOS/Windows runners), e.g. via cargo-dist, producing per-target binaries as release assets.
  • macOS Gatekeeper can complain about an unsigned binary — but binaries delivered via npm / curl | sh / Homebrew usually aren't quarantined, so it's typically a non-issue. Signing + notarization ($99/yr Apple Developer ID) is a later option if needed.

Impact on the npm installer

The TypeScript installer (src/cli.ts) stays useful — arguably more so. Its job is install-time: copy the artifact + safely patch settings.json (backup, merge-only, idempotent, --force). That logic is reused as-is. Changes:

  • Claude Code's statusLine runs any command (it just pipes JSON to stdin), so a binary works directly — and drops the wrapping bash process.
  • init copies the per-platform binary and writes command: <binary> instead of bash <script> (src/commands/init.ts:31).
  • doctor no longer checks for jq; it checks the binary exists / is executable.
  • The npm package becomes the distribution channel for prebuilt binaries (fetch the right target per platform, like esbuild).

(Optional future: fold the installer into a tokenline init subcommand of the Rust binary and drop Node — lower priority, higher risk; keep the tested TS installer for now.)

Plan — the benchmark is a go/no-go gate

  1. Minimal Rust prototype: read the same stdin JSON, render the first line (model / context / cache) with colors.
  2. Benchmark: current tokenline.sh vs the Rust prototype (cold start on Linux/WSL2). Record the numbers in this issue. ← go/no-go gate.
  3. Decide, with the data, here.
  4. Full implementation: port all three lines + the per-turn timestamp/TTL cache, cargo test coverage, then the CI matrix + installer changes.

Note

The bash path remains first-class throughout — no user is forced onto the binary until the Rust version is at parity and the installer handles distribution.

Supersedes #6 and #7.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:installerinstall.sh + npm CLI (src/cli.ts), settings.json patchingarea:statuslinetokenline.sh render path (model/context/cache, economics, rate limits)effort:LCross-cutting / multi-sessionhelp wantedExtra attention is neededplatform:macosmacOS supportplatform:windowsWindows supportrefactor

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions