From d6ba8b210558c816a9d9fc7af7cc02f4be3cfa17 Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Sun, 4 Oct 2026 02:50:07 -0400 Subject: [PATCH] v0.1.3 ANSI 16 indices and RGB to 16 --- README.md | 15 +++++++++++---- cmd/example-256/main.go | 2 +- doc.go | 5 +++-- go.mod | 2 +- xterm.go | 41 +++++++++++++++++++++++++++++++++++++++++ xterm_test.go | 18 ++++++++++++++++++ 6 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 xterm_test.go diff --git a/README.md b/README.md index 1fe8303..6fcd6f9 100644 --- a/README.md +++ b/README.md @@ -77,15 +77,22 @@ var Accent = color.MustParseHex("#ff8800") accent = { r = 255, g = 136, b = 0 } ``` -## With terminal +## Terminal palettes -`terminal.Cell` carries `color.RGB` directly. Quantization stays device-side: +Quantization for terminals that cannot take 24-bit color: ```go -idx := terminal.RGBTo256(color.EmeraldGreen) // xterm-256 index +idx := color.RGBTo256(color.EmeraldGreen) // xterm-256 index, Redmean LUT +ansi := color.RGBTo16(color.EmeraldGreen) // ANSI 16 index, for text consoles ``` -Note: when `Cell.Attrs` sets `AttrFg256` / `AttrBg256`, `Cell.Fg.R` / `Cell.Bg.R` hold a palette index, not a channel. Such values are not colors and must not be passed to this package. +`ANSIBlack` .. `ANSIBrightWhite` name the 16 indices (SGR `30-37`/`90-97`). +The terminal's theme picks their shade, so they suit semantic colors such as +log levels. `RGBTo16` measures against xterm's default shades. + +`terminal.Cell` carries `color.RGB` directly. When `Cell.Attrs` sets +`AttrFg256` / `AttrBg256`, `Cell.Fg.R` / `Cell.Bg.R` hold a palette index, not +a channel. Such values are not colors and must not be passed to this package. ## Concurrency diff --git a/cmd/example-256/main.go b/cmd/example-256/main.go index 171da77..22abc8a 100644 --- a/cmd/example-256/main.go +++ b/cmd/example-256/main.go @@ -54,7 +54,7 @@ func showGrayscaleRamp() { idx := color.Gray256(step) fmt.Print(block256(idx, 2)) } - fmt.Println("\n") + fmt.Print("\n\n") } // ===================================================================== diff --git a/doc.go b/doc.go index 69d022f..3e5ffc3 100644 --- a/doc.go +++ b/doc.go @@ -6,8 +6,9 @@ // image/color.Color, so values pass directly into image, draw, and GUI // toolkit pipelines without conversion. // -// Device concerns — terminal capability detection, xterm-256 quantization, -// SGR emission, framebuffer formats — belong in the consuming package. +// Palette quantization (RGBTo256, RGBTo16) lives here; device concerns — +// terminal capability detection, SGR emission, framebuffer formats — belong +// in the consuming package. // // # Operations // diff --git a/go.mod b/go.mod index ca52a27..e4a952b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/lixenwraith/color -go 1.26.0 +go 1.27.1 diff --git a/xterm.go b/xterm.go index 7cd9437..92127be 100644 --- a/xterm.go +++ b/xterm.go @@ -130,6 +130,47 @@ func computeRedmean256(c RGB) uint8 { var cubeValues = [6]uint8{0, 95, 135, 175, 215, 255} +// ANSI 16 color indices, sent as SGR 30-37/90-97 (40-47/100-107 background). +// The terminal's theme picks the shade; a text console renders them too. +const ( + ANSIBlack uint8 = iota + ANSIRed + ANSIGreen + ANSIYellow + ANSIBlue + ANSIMagenta + ANSICyan + ANSIWhite + ANSIBrightBlack + ANSIBrightRed + ANSIBrightGreen + ANSIBrightYellow + ANSIBrightBlue + ANSIBrightMagenta + ANSIBrightCyan + ANSIBrightWhite +) + +// xterm's default shades for the ANSI 16 indices, the reference for RGBTo16 +var ansi16 = [16]RGB{ + {0, 0, 0}, {205, 0, 0}, {0, 205, 0}, {205, 205, 0}, + {0, 0, 238}, {205, 0, 205}, {0, 205, 205}, {229, 229, 229}, + {127, 127, 127}, {255, 0, 0}, {0, 255, 0}, {255, 255, 0}, + {92, 92, 255}, {255, 0, 255}, {0, 255, 255}, {255, 255, 255}, +} + +// RGBTo16 converts 24-bit RGB to the nearest ANSI 16 index (Redmean distance +// to xterm's default shades), for terminals limited to 16 colors. +func RGBTo16(c RGB) uint8 { + best, minDist := ANSIBlack, 1<<30 + for i, cand := range ansi16 { + if d := RedmeanDistance(c, cand); d < minDist { + best, minDist = uint8(i), d + } + } + return best +} + // RGBTo256 converts 24-bit RGB to the nearest xterm-256 palette index. // O(1) via the Redmean LUT; the first call builds it. func RGBTo256(c RGB) uint8 { diff --git a/xterm_test.go b/xterm_test.go new file mode 100644 index 0000000..2e3d9f3 --- /dev/null +++ b/xterm_test.go @@ -0,0 +1,18 @@ +package color + +import "testing" + +// Each reference shade maps to its own index, so the constants, the table and +// the search agree; the named primaries land on the bright indices like xterm. +func TestRGBTo16MapsReferenceShadesToTheirIndex(t *testing.T) { + for i, c := range ansi16 { + if got := RGBTo16(c); got != uint8(i) { + t.Errorf("shade %d %s: index %d", i, c.Hex(), got) + } + } + for c, want := range map[RGB]uint8{Red: ANSIBrightRed, Black: ANSIBlack, White: ANSIBrightWhite, {170, 0, 0}: ANSIRed} { + if got := RGBTo16(c); got != want { + t.Errorf("%s: index %d, want %d", c.Hex(), got, want) + } + } +}