Skip to content

feat: add $fill layout token (Starship-style right-alignment)#189

Open
tfenne wants to merge 3 commits into
stephenleo:mainfrom
tfenne:feat/fill
Open

feat: add $fill layout token (Starship-style right-alignment)#189
tfenne wants to merge 3 commits into
stephenleo:mainfrom
tfenne:feat/fill

Conversation

@tfenne

@tfenne tfenne commented May 25, 2026

Copy link
Copy Markdown
Contributor

Adds a Starship-style $fill token (configured via [cship.fill]) that expands to fill the remaining width; multiple $fill on a line split the space evenly, so trailing content is right-aligned. Mirrors Starship's fill module (symbol / style / disabled; defaults . / bold black). Replaces the old inert $cship.flex stub.

The hard part: terminal width

Claude Code doesn't give statusline commands the terminal width — no $COLUMNS, no controlling tty on the child process, and no width field in the JSON (anthropics/claude-code#22115). So $fill recovers it best-effort on macOS/Linux by walking up the parent process chain to an ancestor's controlling terminal and reading its window size. Resolution order:

detected terminal width  →  $COLUMNS  →  [cship] width  →  80

…then minus [cship] width_offset (default 3, ≈ the left/right margin Claude Code reserves).

Honest limitations (documented in configuration.md + FAQ)

  • Windows / web / desktop apps have no controlling tty to walk → fall back to [cship] width or 80.
  • Relies on Claude Code spawning the statusline under a tty-holding ancestor (true in a terminal; an implementation detail that could change).
  • Nerd Font glyphs can be undercounted by unicode-width, nudging alignment by a column (a limitation shared with Starship).

Dependencies — and a leaner alternative I'm happy to switch to

This PR's width walk uses libproc (macOS) + procfs (Linux) + ctty (dev→path) + terminal_size (winsize). Two of those carry weight worth flagging:

  • libproc pulls bindgenclang-sys as a build dependency — it requires libclang at build time, adds ~390 lockfile lines, and slows macOS builds.
  • ctty (2019, single release) adds a duplicate thiserror v1 (the crate already uses v2) and a cc build step, and does a glob+stat scan per fill-line on Linux.

terminal_size is light (pure-Rust rustix) and worth keeping either way.

If you'd prefer a minimal dependency tree, I can drop libproc + ctty + bindgen/clang-sys entirely in favour of a raw libc + std implementation (keeping only libc, already a dep, + terminal_size):

  • macOS: sysctl(KERN_PROC_PID)kinfo_proc.e_ppid / e_tdev; libc::devname for the device path (~30 lines, about half unsafe).
  • Linux: parse /proc/<pid>/stat for ppid / tty_nr; readlink /proc/<pid>/fd/0 for the device path (~25 lines std, no glob).

I went with the crate-based version here for readability and to keep unsafe out of the tree — but I'm glad to swap to the lean version if you'd rather not take on bindgen/ctty. Just say which you prefer.

Tests & verification

  • Unit tests for the layout math: distribute, build_filled_line (including multi-column symbols, overflow, content-less suppression, and Unicode width), and apply_offset.
  • Integration tests for fill expansion and disabled-collapse.
  • Verified on macOS and Linux: cargo fmt --check, cargo clippy -- -D warnings, full test suite, and release build.

Showcase

cship.toml demonstrates fills around the context bar. The README / docs/showcase.md Hero examples are intentionally left unchanged pending your call on whether to feature $fill in the flagship recommended config.

tfenne and others added 3 commits May 24, 2026 18:20
Add a Starship-style $fill token (configured via [cship.fill]) that
expands to fill the remaining width; multiple $fill on a line split the
space evenly, right-aligning trailing content.

The hard part is that Claude Code gives the statusline no terminal width
(no $COLUMNS, no tty, no JSON field — anthropics/claude-code#22115). We
recover it best-effort on macOS/Linux by walking up the parent process
chain to an ancestor with a controlling tty and reading its winsize
(libproc/procfs + ctty + terminal_size). Resolution order:
detected -> $COLUMNS -> [cship] width -> 80, minus [cship] width_offset
(default 3). Windows and the web/desktop apps fall back to config/80.

Honest limitations (documented in configuration.md + FAQ): relies on the
statusline being spawned under a tty-holding ancestor; Nerd Font glyph
widths may be off by a column; libproc pulls bindgen at build time on
macOS. The $cship.flex stub is removed in favor of $fill.

Showcased in the repo cship.toml (fills around the context bar).
…testable offset

- render_fill() sizes gaps by display columns, so multi-column `symbol`
  values align exactly (was repeating by count → overshoot). Remainder
  padded with spaces.
- A line whose only visible content is $fill now renders empty instead of
  a full-width rule, matching cship's empty-line dropping.
- Extract pure apply_offset() and test it unconditionally (the old
  statusline_width tests were skipped whenever a tty was found).
- Clarify that a disabled $fill leaves surrounding literal spaces; doc the
  bare-rule suppression.

From the opus/sonnet/haiku review of the branch.
# Conflicts:
#	CHANGELOG.md
#	Cargo.lock
#	Cargo.toml
#	cship.toml
#	docs/faq.md
#	src/config.rs
@stephenleo

Copy link
Copy Markdown
Owner

Thanks for this — it's a genuinely well-built PR, and I appreciate that you flagged the dependency trade-off yourself and pre-wrote the lean alternative. That made the review easy.

TL;DR: I'd love to merge the feature, but let's go with the lean libc + std version you offered.

What I verified

I checked the back-compat story carefully and it's clean at runtime:

  • Existing configs render byte-identically. The parser change is isolated to $fill (now Token::Fill instead of the old inert stub), and the renderer's fast path skips width detection entirely for any line without a fill — so there's zero added cost or behavior change for everyone not using $fill.
  • The new config fields (fill, width, width_offset) are all additive Option<T> on Default structs — deserializes fine against old configs.
  • fmt --check, clippy -D warnings, and the full suite (584 tests) all pass.

The layout math (distribute / build_filled_line / render_fill) is clean, and handling multi-column symbols + Unicode width exactly is a nice touch. No notes there.

On simplifying — yes please

You already diagnosed it, so I'll just confirm your instinct: the feature is great, but the dependency tree is disproportionate to what's a deliberately best-effort hack. The thing that tips it for me is the build-time libclang requirement that libproc → bindgen → clang-sys pulls in. cship's whole pitch is "one fast binary," and a hard libclang dep risks breaking cargo install cship on machines that don't have it (plus the duplicate thiserror v1 from ctty, and procfs being a full /proc library used to read two integers).

Your raw libc + std version does exactly the same work in ~55 lines, keeps only deps we already have (libc) plus the lightweight terminal_size, and deletes that whole subtree. On the "keep unsafe out" concern — I'm comfortable with a small, contained sysctl(KERN_PROC_PID) block here; that surface is far smaller than carrying a build-time toolchain dependency and a 2019 single-release crate.

So if you're up for it:

  • Swap to the lean libc + std width walk (drop libproc / ctty / procfs / bindgen).
  • Keep terminal_size + unicode-width.
  • On the Linux side, parse ppid / tty_nr from /proc/<pid>/stat after the last ) — the comm field can contain spaces/parens, which is the one parsing gotcha.

Everything else — docs, tests, the lazy fast path — is good as-is and shouldn't need to change.

One small thing

Let's leave the flagship cship.toml unchanged — please revert the $fill addition there. I'd rather not have the recommended/default config quietly opt everyone into the best-effort width detection on every render; $fill should stay opt-in for folks who add it themselves. Same reasoning you already applied to leaving the README Hero examples alone — let's keep both as-is for now. (And there's a now-trivial CHANGELOG.md conflict after the v1.8.0 release; I can resolve that on merge.)

Thanks again — this is close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants