feat: add gradient as a first-class widget color#404
Conversation
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
…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>
|
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 ( Rather than have them compete, I've meshed the two onto one shared OKLab core in #406:
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. 🙏 |
|
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:
Happy to review #406 and converge there — just want the attribution set up properly before it merges. Thanks again! 🙌 |
|
@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. |
|
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 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 |
…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>
|
Thanks for the thoughtful note, @akkaz, and for offering to carry the per-widget design forward. Just force-pushed: the mesh commit ( On the cherry-pick option — I genuinely considered it, but the branches diverged on 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. |

What
Adds
gradientas 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:
gradient:atlas,gradient:rainbow,gradient:vice, … (the named gradients shipped bygradient-string, reproduced as raw stops).gradient:RRGGBB-RRGGBB[-RRGGBB...](2+ hex stops).The
ColorMenuTUI gains a gradient picker so it can be configured interactively.How
src/utils/gradient.ts: parses agradient:color spec and interpolates it across the visible characters of the text, emitting one ANSI color code per character. Whitespace is skipped (matchinggradient-stringbehavior).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.tshook 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 viaink-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 usestinygradient— the underlying interpolation primitive — directly. Preset stops are reproduced fromgradient-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.