Skip to content

AnsiColorMap — OKLab to terminal color escape sequences #40

@wow-miley

Description

@wow-miley

Context

Wave 2 introduces ASCII projection of the Lumos voxel orb. Each projected cell carries an OKLab color sampled from its source voxel, but terminals consume ANSI escape sequences — either the 256-color palette (\u001B[38;5;{n}m) or truecolor (\u001B[38;2;{r};{g};{b}m). This ticket provides the primitive that bridges those two worlds.

phosphor-core already contains CognitiveColorRamp, a luminance-driven ANSI ramping helper. AnsiColorMap is a complementary primitive that takes a full OKLab color (not a scalar luminance) and produces a terminal-ready escape sequence. Both belong in phosphor-core — ANSI is a wire format, not a UI framework, so it doesn't violate the pure-core principle.

Objective

A pure-function OKLab → ANSI escape sequence primitive supporting both 256-color and truecolor modes.

Expected Outcomes

  • AnsiColorMode enum with ANSI_256 and TRUECOLOR members
  • AnsiColorMap object with:
    • escape(color: OklabColor, mode: AnsiColorMode): String — returns the foreground escape sequence
    • backgroundEscape(color: OklabColor, mode: AnsiColorMode): String — returns the background escape sequence
    • nearestPaletteIndex(color: OklabColor): Int — exposed for testing and for callers that want the raw palette index
  • Quantization uses the standard 256-color palette structure: 16 base colors (indices 0–15), 6×6×6 RGB cube (16–231), 24-step grayscale ramp (232–255). Nearest-color search goes OKLab → linear sRGB → sRGB → cube lookup.
  • Unit tests covering: pure red/green/blue snap to expected cube indices, near-grayscale snaps to grayscale ramp not cube, truecolor mode emits raw 24-bit values.

Technical Constraints

  • Lives in phosphor-core at src/commonMain/kotlin/link/socket/phosphor/color/AnsiColorMap.kt.
  • Uses Wave 0's OklabColor and LinearRgbColor from color/OklabColor.kt. No new color types.
  • Pure Kotlin, no platform-specific code.
  • No allocations in the hot path — the 256-color cube lookup table is computed once at object init.

API Surface Verification (before starting)

Confirm in socket-link/phosphor:

  • OklabColor and LinearRgbColor data class definitions and the conversion path between them.
  • Whether CognitiveColorRamp already exposes any escape-sequence helpers worth reusing (likely not — it's luminance-only — but worth verifying so we don't duplicate).

Tasks

  1. Implement linear sRGB → sRGB gamma encoding helper (if not already present in OklabColor.kt). Validate: round-trip linearToSrgb(srgbToLinear(0.5)) ≈ 0.5.
  2. Implement 256-color palette table generation (lazy val initialized once). Validate: table has exactly 256 entries, index 16 maps to RGB (0,0,0), index 231 maps to RGB (255,255,255).
  3. Implement nearestPaletteIndex. Validate: pure red OKLab snaps to palette index in the red region of the cube (e.g., index 196 or similar — verify exact expected value with a one-off test).
  4. Implement escape and backgroundEscape for both modes. Validate: pure red truecolor returns exactly "\u001B[38;2;255;0;0m"; pure red ANSI_256 returns "\u001B[38;5;{expected_index}m".
  5. Add unit tests covering edge cases: pure white, pure black, mid-gray, near-saturated red/green/blue/purple/amber (matches Lumos atmosphere palette).

Out of Scope

  • Background-color compositing logic. LumosTerminalFrame.TerminalCell.background is set elsewhere (sibling ticket on LumosTerminalFrame DTO).
  • Differential update / escape sequence diffing across frames. Lives in the CliOrb renderer ticket.
  • Color cycling animations. Atmospheres handle that upstream.
  • Windows-specific palette quirks (legacy 16-color mode). Truecolor is required on any host running Lumos.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions