Skip to content

feat: add gradient as a first-class widget color#404

Open
akkaz wants to merge 1 commit into
sirmalloc:mainfrom
akkaz:gradient-pr
Open

feat: add gradient as a first-class widget color#404
akkaz wants to merge 1 commit into
sirmalloc:mainfrom
akkaz:gradient-pr

Conversation

@akkaz
Copy link
Copy Markdown

@akkaz akkaz commented May 30, 2026

What

Adds gradient as a selectable widget text color, alongside the existing solid colors. A widget's text can now be painted with a multi-stop color gradient spread across its characters.

Two forms are supported:

  • Named presetsgradient:atlas, gradient:rainbow, gradient:vice, … (the named gradients shipped by gradient-string, reproduced as raw stops).
  • Custom stopsgradient:RRGGBB-RRGGBB[-RRGGBB...] (2+ hex stops).

The ColorMenu TUI gains a gradient picker so it can be configured interactively.

How

  • New src/utils/gradient.ts: parses a gradient: color spec and interpolates it across the visible characters of the text, emitting one ANSI color code per character. Whitespace is skipped (matching gradient-string behavior).
  • Color-level aware: truecolor → 38;2;r;g;b, ansi256 → nearest xterm-256 index, ansi16 → falls back to a solid first-stop color (gradients can't be represented at 16 colors).
  • colors.ts / renderer.ts hook the gradient path into the existing color application flow; non-gradient colors are unaffected.

Why tinygradient (not gradient-string directly)

gradient-string (already present transitively via ink-gradient) is built to emit chalk/ink-styled strings at the render layer. The status line renderer needs raw, per-character ANSI codes at a specific color level (with 256-color downsampling and whitespace handling) outside the React/Ink layer, so this uses tinygradient — the underlying interpolation primitive — directly. Preset stops are reproduced from gradient-string (MIT) with attribution in the source.

Tests

Adds focused tests for gradient parsing, color interpolation, 256-color downsampling, sanitization, and renderer integration. Full suite green.

Add a 'gradient:' color prefix (alongside hex:/ansi256:) that paints a
per-character truecolor gradient across a widget's text. Supports named
presets (retro, rainbow, vice, ...) and custom start/end stops
(gradient:RRGGBB-RRGGBB). Selectable from the TUI color menu via 'g'.

- src/utils/gradient.ts: presets, parseGradientSpec, rgbToAnsi256, applyGradientToText
- src/utils/colors.ts: applyColors per-char gradient + getColorAnsiCode first-stop fallback (powerline/ansi16)
- src/tui/components/ColorMenu.tsx: gradient picker UI
- tests for parsing, rendering and color-level sanitize passthrough
cameronsjo added a commit to cameronsjo/ccstatusline that referenced this pull request May 30, 2026
…n core

Converges the two same-day gradient PRs into one coherent feature. sirmalloc#406
added line-spanning gradients via overrideForegroundColor (OKLab, zero-dep);
sirmalloc#404 (@akkaz) added gradient as a per-widget color with named presets and a
ColorMenu picker. Both created src/utils/gradient.ts and would conflict, so
this meshes them onto a single shared OKLab engine:

- gradient.ts: GRADIENT_PRESETS (akkaz's stops, gradient-string MIT;
  rainbow/pastel re-expressed as multi-stop hue wheels for OKLab); unified
  parseGradientSpec accepting presets, dash (RRGGBB-RRGGBB), and comma
  (hex:..,..) forms; applyGradientToText for the per-widget sweep.
- colors.ts: per-widget gradient hook in applyColors; getColorAnsiCode
  collapses a gradient to its first stop (powerline / ansi16 degrade).
- ColorMenu.tsx: 'g' opens a gradient picker (preset list + custom hex),
  foreground-only, colorLevel >= 2.
- No new dependency (tinygradient dropped in favor of OKLab).

Precedence: overrideForegroundColor gradient (line-span) > widget.color
gradient (per-widget) > solid. Gradients self-degrade at render time, so
color-sanitize leaves them untouched at every level.

Builds on the per-widget design from sirmalloc#404 by @akkaz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cameronsjo
Copy link
Copy Markdown

Hey @akkaz — looks like we landed on the same idea the same day. 👋 You opened this (#404) a few hours before I opened #406 — yours adds gradient as a per-widget color with named presets and a ColorMenu picker; mine started as a line-spanning gradient (overrideForegroundColor, OKLab, no deps). They're complementary scopes, but both create src/utils/gradient.ts, so they can't both merge as-is.

Rather than have them compete, I've meshed the two onto one shared OKLab core in #406:

  • kept your per-widget model, presets, and TUI picker (credited you in the PR + commit);
  • unified the parser to accept presets, dash (RRGGBB-RRGGBB), and comma forms;
  • swapped interpolation to OKLab so it's dependency-free — rainbow/pastel are re-expressed as multi-stop hue wheels (OKLab can't do an HSV hue-spin);
  • both scopes coexist: overrideForegroundColor (whole line) and widget.color (one widget).

Since you were here first, I don't want to step on your PR — happy to fold this into #404 instead if you'd prefer, or have you review #406. Your call on how we converge, and credit is yours to share. Thanks for the picker work especially. 🙏

@akkaz
Copy link
Copy Markdown
Author

akkaz commented May 30, 2026

Hey @cameronsjo — thanks for meshing these together so thoughtfully, and for the credit note. 🙏 The OKLab core and the line-span work are genuinely nice, and I'm glad the per-widget model + presets + TUI picker live on in #406.

One thing that does matter to me: I'd like to end up a real contributor on the merged history, not just a mention in the description. Two ways, in order of preference:

  1. Preserve my original feat: add gradient as a first-class widget color #404 commit (cherry-pick / rebase it in) so I'm the author of record on the per-widget parts — cleanest, and it shows up everywhere including the repo's Contributors graph.
  2. If that's awkward given how you've restructured things, at minimum add a co-author trailer to the per-widget commit:
    Co-authored-by: akkaz <giomarco@cleversoft.it>
    

Happy to review #406 and converge there — just want the attribution set up properly before it merges. Thanks again! 🙌

@sirmalloc
Copy link
Copy Markdown
Owner

@akkaz @cameronsjo I'll work on getting #406 reviewed if that's what you both have decided on. I typically squash merge these to main, but I believe if there is a commit authored by @akkaz in #406's history then it should attribute both. If it doesn't I can roll it back and we can try again. Let me know what the final decision is and I'll review that PR.

@akkaz
Copy link
Copy Markdown
Author

akkaz commented May 30, 2026

Thanks @sirmalloc! Yes, let's go with #406, that's the decision.

One thing to flag since you squash-merge: right now none of #406's commits have a Co-authored-by trailer, so a squash would credit it only to @cameronsjo. @cameronsjo could you add this to one of the per-widget commits?

Co-authored-by: akkaz <giomarco@cleversoft.it>

With that in the history the squashed commit should credit both of us. Happy to review #406 too.

Also, once this lands, in my fork I built dynamic value-based widget colors (a dynamic: ramp on top of the same gradient engine) that I'd be glad to send as a separate PR if there's interest.

@akkaz
Copy link
Copy Markdown
Author

akkaz commented May 30, 2026

Here's the dynamic colors I mentioned, in case it helps picture it:

dynamic value-based widget colors

The Context bar/percentage warms toward red as it fills (73% here), while Session and Week stay cooler at lower fill. Same gradient engine, just sampled by each widget's fill ratio.

cameronsjo added a commit to cameronsjo/ccstatusline that referenced this pull request May 30, 2026
…n core

Converges the two same-day gradient PRs into one coherent feature. sirmalloc#406
added line-spanning gradients via overrideForegroundColor (OKLab, zero-dep);
sirmalloc#404 (@akkaz) added gradient as a per-widget color with named presets and a
ColorMenu picker. Both created src/utils/gradient.ts and would conflict, so
this meshes them onto a single shared OKLab engine:

- gradient.ts: GRADIENT_PRESETS (akkaz's stops, gradient-string MIT;
  rainbow/pastel re-expressed as multi-stop hue wheels for OKLab); unified
  parseGradientSpec accepting presets, dash (RRGGBB-RRGGBB), and comma
  (hex:..,..) forms; applyGradientToText for the per-widget sweep.
- colors.ts: per-widget gradient hook in applyColors; getColorAnsiCode
  collapses a gradient to its first stop (powerline / ansi16 degrade).
- ColorMenu.tsx: 'g' opens a gradient picker (preset list + custom hex),
  foreground-only, colorLevel >= 2.
- No new dependency (tinygradient dropped in favor of OKLab).

Precedence: overrideForegroundColor gradient (line-span) > widget.color
gradient (per-widget) > solid. Gradients self-degrade at render time, so
color-sanitize leaves them untouched at every level.

Builds on the per-widget design from sirmalloc#404 by @akkaz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: akkaz <giomarco@cleversoft.it>
@cameronsjo
Copy link
Copy Markdown

Thanks for the thoughtful note, @akkaz, and for offering to carry the per-widget design forward. Just force-pushed: the mesh commit (98f384f) now carries Co-authored-by: akkaz <giomarco@cleversoft.it>, so the squash-merge should credit you on the Contributors graph.

On the cherry-pick option — I genuinely considered it, but the branches diverged on gradient.ts itself: your parseGradientSpec returns a GradientSpec with HSV flags and uses tinygradient, mine returns Rgb[] and interpolates in OKLab with zero deps. Cherry-picking would hard-conflict on every file we both touched (gradient.ts, colors.ts, ColorMenu.tsx) and the conflict resolution would mostly walk back toward what's already on the branch — an author=akkaz commit whose content has been edited away from your original. The trailer felt like the truer attribution for what actually shipped: your design (per-widget hook, presets verbatim with the gradient-string MIT attribution, the TUI picker logic) carried forward on a different engine.

The dynamic value-based widget colors look great — the context bar warming toward red is exactly the kind of thing a shared gradient engine should make easy. Please do open a PR on top of this once it lands; happy to review.

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.

3 participants