feat: line-spanning foreground gradients via overrideForegroundColor#406
feat: line-spanning foreground gradients via overrideForegroundColor#406cameronsjo wants to merge 3 commits into
Conversation
Add a `gradient:<stop>,<stop>,...` form for `overrideForegroundColor` that paints the whole status line with a continuous gradient — each visible character is colored by its column position, so the gradient spans the line rather than restarting per widget. Applies to standard (non-powerline) lines; powerline separators derive their color from adjacent backgrounds, so a foreground gradient is intentionally not applied there. - src/utils/gradient.ts: parse hex stops (`hex:RRGGBB` / `#RRGGBB` / bare), interpolate in OKLab for perceptually even, non-muddy blends, and map to truecolor or the nearest ansi256 index. - src/utils/ansi.ts: applyLineGradient walks the assembled line with the existing escape/cluster tokenizer, so SGR styling and OSC-8 hyperlinks pass through untouched and visible width is unchanged (flex layout unaffected). - src/utils/renderer.ts: applied after assembly and before truncation in the standard path. `overrideForegroundColor` accepts the new `gradient:` form alongside the existing `hex:` / `ansi256:` / named tagged-string forms; a gradient spec is not treated as a per-widget solid color and degrades to a no-op at ansi16 (keeping widgets' own colors). Tests cover spec parsing, OKLab sampling, ansi256 quantization, width-invariance, OSC-8 passthrough, and a renderer integration case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cdc537d to
d95516b
Compare
…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>
|
Nice work converging both scopes onto a single dependency-free OKLab core — the line-span + per-widget split reads really cleanly. 👏 Replied on #404 re: how we land the attribution. tl;dr: I'd love to stay a real contributor on the merged history — ideally by preserving my original #404 commit, or at minimum 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>
…n, document Pre-PR polish pass over the gradient feature. Fix (correctness): - renderStatusLine now applies the line-span gradient AFTER truncation, not before. truncateStyledText cuts from the right and appends a raw "..." with no trailing reset, so a gradient applied earlier had its closing \x1b[39m sliced off, leaking the last color past the status line. Gradient codes are zero-width, so the truncation measurement is unaffected by deferring. Regression test added. Simplify: - Add exported isGradientSpec(); reuse it across colors.ts and renderer.ts instead of duplicating the 'gradient:' startsWith check. Drop the redundant prefix guard in applyColors (parseGradientSpec already self-guards). Docs: - Document applyGradientToText's code-point (vs grapheme-cluster) iteration as a known limitation, with the ZWJ/variation-selector consequence and the circular-import reason it isn't unified with applyLineGradient yet. - Note that hex:/# /bare stops are valid in both comma and dash parse forms. - Note getColorAnsiCode's ansi16 branch intentionally emits a truecolor first-stop escape (caller-degraded; never reached at a true ansi16 terminal). - README + docs/USAGE.md: gradient color options (both scopes, presets, forms). bun test 1433 pass / 0 fail; lint + tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
803267c to
24edb57
Compare
|
Thanks for the steer, @sirmalloc. Just force-pushed: the per-widget mesh commit ( |
|
Heads up on a possible follow-up. I have rebuilt the value-driven dynamic color feature from my fork on top of this gradient engine. Instead of a separate It covers the context, session and weekly usage bars plus the reset timers, with a |
What
Adds foreground gradients to ccstatusline at two scopes, on one shared OKLab engine:
overrideForegroundColor: "gradient:…"paints the whole status line with one continuous sweep; each visible character is colored by its column across the line.widget.color: "gradient:…"gives a single widget its own self-contained sweep, alongside the existing solid colors. Configurable in the TUI (gin the color menu).Three spec forms: a named preset (
gradient:atlas,gradient:rainbow, …), dash stops (gradient:RRGGBB-RRGGBB), or comma stops (gradient:hex:…,#…,…). Two or more stops.Why one core
This converges two same-day PRs. #404 by @akkaz (opened first) added gradient as a per-widget color with named presets (reproduced from
gradient-string, MIT) and a ColorMenu picker; this PR added the line-spanning form. Both createsrc/utils/gradient.ts, so they'd conflict. Rather than compete, they're meshed onto a single engine that offers both scopes.Colors interpolate in OKLab for perceptually even blends — no muddy mid-tones, no
tinygradient/HSV dependency.rainbow/pastel(originally HSV hue-spins) are re-expressed as explicit multi-stop hue wheels so OKLab reproduces them. Zero new dependencies.How
gradient.ts—parseGradientSpec(presets + dash + comma →Rgb[]), OKLabsampleGradient,rgbToAnsi256,gradientCodeAt,applyGradientToText(per-widget), plusGRADIENT_PRESETS.colors.ts— per-widget gradient inapplyColors;getColorAnsiCodecollapses a gradient to its first stop as a solid (what the powerline renderer and the ansi16 path see).ansi.ts—applyLineGradientreuses the existingparseEscapeSequence/consumeDisplayClusterwalkers, so OSC-8 hyperlinks pass through and visible width is unchanged (flexModeunaffected).renderer.ts— line-span pass after assembly, before truncation; suppresses a widget's solid fg when a line-span gradient owns the line.ColorMenu.tsx—gopens a gradient picker (preset list + custom hex), foreground-only, colorLevel ≥ 2.Precedence: line-span override > per-widget
color: gradient:> solid.Color levels & scope
38;2;r;g;b; ansi256 → nearest 6×6×6 / grayscale; ansi16 → per-widget degrades to a solid first stop, line-span is a no-op.color-sanitizeleaves them untouched at every level.Tests
bun testgreen (1426 pass). Covers preset/dash/comma parsing, OKLab sampling, ansi256 mapping, per-widgetapplyGradientToText(whitespace-skipped, restart-per-call), line-span width-invariance, OSC-8 passthrough, the per-widgetapplyColorspath at all levels, first-stop fallback, and the self-degrade-across-levels sanitize behavior.bun run lint+bun tsc --noEmitclean.Credit
Per-widget gradients, the preset list, and the TUI picker come from #404 by @akkaz — this PR carries that design forward on the shared OKLab core. 🙏
Follow-ups (not in this PR)
overrideForegroundColorgradient🤖 Generated with Claude Code