From 615e11bc78971f2952b5c5fb05cac6496853a0ac Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Sun, 19 Jul 2026 05:43:42 -0400 Subject: [PATCH] v0.1.2 xterm 256 color addition, examples in cmd dir --- cmd/example-256/main.go | 145 +++++++++++++++++++ {examples/cmd => cmd/example-tc}/main.go | 0 xterm.go | 171 +++++++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 cmd/example-256/main.go rename {examples/cmd => cmd/example-tc}/main.go (100%) create mode 100644 xterm.go diff --git a/cmd/example-256/main.go b/cmd/example-256/main.go new file mode 100644 index 0000000..171da77 --- /dev/null +++ b/cmd/example-256/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/lixenwraith/color" +) + +const reset = "\x1b[0m" + +// bg256 returns the SGR sequence for an 8-bit xterm-256 background. +// This is the sequence used by naked terminals (TTY, basic SSH). +func bg256(idx uint8) string { return fmt.Sprintf("\x1b[48;5;%dm", idx) } + +// bgRGB returns the SGR sequence for a 24-bit truecolor background. +func bgRGB(c color.RGB) string { return fmt.Sprintf("\x1b[48;2;%d;%d;%dm", c.R, c.G, c.B) } + +// block256 renders n background-colored spaces using the 256-color palette. +func block256(idx uint8, n int) string { return bg256(idx) + strings.Repeat(" ", n) + reset } + +// blockRGB renders n background-colored spaces using truecolor. +func blockRGB(c color.RGB, n int) string { return bgRGB(c) + strings.Repeat(" ", n) + reset } + +// ===================================================================== +// PART 1: 256 Color Mechanics +// ===================================================================== + +func show256Cube() { + fmt.Println("── Part 1: xterm-256 Color Cube (6x6x6) ──") + fmt.Println("Displaying the 216-color cube layout (Indices 16-231).") + + // The standard xterm-256 cube consists of 6 "slices" of red, + // containing 6x6 grids of green and blue. + for r := uint8(0); r < 6; r++ { + for g := uint8(0); g < 6; g++ { + for b := uint8(0); b < 6; b++ { + // Get exact index using the pure math function + idx := color.Cube256(r, g, b) + fmt.Print(block256(idx, 3)) + } + fmt.Print(" ") // Space between green columns + } + fmt.Println() + } + fmt.Println() +} + +func showGrayscaleRamp() { + fmt.Println("── Part 1: Grayscale Ramp ──") + fmt.Println("Displaying the 24-step grayscale ramp (Indices 232-255).") + + for step := uint8(0); step < 24; step++ { + idx := color.Gray256(step) + fmt.Print(block256(idx, 2)) + } + fmt.Println("\n") +} + +// ===================================================================== +// PART 2: Naked Terminal (No Desktop Environment) +// ===================================================================== + +func showNakedTerminalDegradation() { + fmt.Println("── Part 2: Naked Terminal (Quantization Fallback) ──") + fmt.Println("Simulating how 24-bit Truecolor gracefully degrades in environments") + fmt.Println("without a modern desktop compositor (e.g., bare TTY, tmux, legacy SSH).") + fmt.Println() + + // 1. Force the lazy evaluation of the 262KB Redmean LUT. + // This makes RGBTo256 O(1) during the render loop. + color.WarmXterm256() + + // 2. Render a smooth gradient in both truecolor and degraded 256-color + const steps = 40 + start, end := color.ElectricViolet, color.LimeGreen + + var truecolor strings.Builder + var naked256 strings.Builder + + for i := 0; i < steps; i++ { + t := float64(i) / float64(steps-1) + c := color.Lerp(start, end, t) + + // Desktop Environment (24-bit) + truecolor.WriteString(blockRGB(c, 2)) + + // Naked Terminal (8-bit Quantized via Redmean perceptual distance) + idx := color.RGBTo256(c) + naked256.WriteString(block256(idx, 2)) + } + + fmt.Println("Desktop Environment (24-bit smooth interpolation):") + fmt.Println(truecolor.String()) + fmt.Println("Naked Terminal (8-bit perceptual quantization via RGBTo256):") + fmt.Println(naked256.String()) + fmt.Println() + + // 3. Show exact index mapping for specific named colors + fmt.Println("Nearest xterm-256 mappings for Named RGB colors:") + named := []struct { + name string + c color.RGB + }{ + {"HotPink", color.HotPink}, + {"BurntOrange", color.BurntOrange}, + {"MintGreen", color.MintGreen}, + {"DeepNavy", color.DeepNavy}, + {"SlateGray", color.SlateGray}, + } + + for _, n := range named { + // Calculate nearest palette index + idx := color.RGBTo256(n.c) + + // Map index back to mathematical coordinates to see where it landed + cubeStr := "" + if idx >= 16 && idx <= 231 { + r, g, b := color.CubeRGB256(idx) + cubeStr = fmt.Sprintf("Cube(r:%d, g:%d, b:%d)", r, g, b) + } else if idx >= 232 { + cubeStr = fmt.Sprintf("GrayStep(%d)", idx-232) + } else { + cubeStr = "System(0-15)" + } + + fmt.Printf(" %-12s %s (Truecolor) -> %s (Index %3d) %s\n", + n.name, + blockRGB(n.c, 4), + block256(idx, 4), + idx, + cubeStr, + ) + } + fmt.Println() +} + +func main() { + // Execute Part 1 + show256Cube() + showGrayscaleRamp() + + // Execute Part 2 + showNakedTerminalDegradation() +} diff --git a/examples/cmd/main.go b/cmd/example-tc/main.go similarity index 100% rename from examples/cmd/main.go rename to cmd/example-tc/main.go diff --git a/xterm.go b/xterm.go new file mode 100644 index 0000000..7cd9437 --- /dev/null +++ b/xterm.go @@ -0,0 +1,171 @@ +package color + +import ( + "sync" + "sync/atomic" +) + +// Generic xterm 256-color palette indices +// Color cube: index = 16 + 36*r + 6*g + b where r,g,b ∈ [0,5] +// Grayscale ramp: indices 232-255, level = 8 + 10*(index-232) +// Ordered dark-to-light within each hue group + +const ( + P256DeepNavy uint8 = 17 // (0,0,1) + P256DarkBlue uint8 = 18 // (0,0,2) + P256SteelBlue uint8 = 75 // (1,3,5) + P256LightBlue uint8 = 81 // (1,4,5) + + P256DeepTeal uint8 = 23 // (0,1,1) + P256Teal uint8 = 44 // (0,4,4) + P256Green uint8 = 46 // (0,5,0) + P256Cyan uint8 = 51 // (0,5,5) + P256LightCyan uint8 = 87 // (1,5,5) + + P256CobaltBlue uint8 = 33 // (0,2,5) + P256DarkPurpleBlue uint8 = 54 // (1,0,2) + P256Indigo uint8 = 63 // (1,1,5) + P256Purple uint8 = 129 // (3,0,5) + P256Violet uint8 = 134 // (3,1,4) + P256MediumPurple uint8 = 135 // (3,1,5) + P256Orchid uint8 = 176 // (4,2,4) + + P256YellowGreen uint8 = 154 // (3,5,0) + + P256Maroon uint8 = 52 // (1,0,0) + P256DarkCrimson uint8 = 88 // (2,0,0) + P256Crimson uint8 = 160 // (4,0,0) + + P256Red uint8 = 196 // (5,0,0) + P256Rose uint8 = 198 // (5,0,2) + P256RedOrange uint8 = 202 // (5,1,0) + P256Orange uint8 = 208 // (5,2,0) + P256Amber uint8 = 214 // (5,3,0) + P256Gold uint8 = 220 // (5,4,0) + P256Yellow uint8 = 226 // (5,5,0) + + P256DarkAmber uint8 = 94 // (2,1,0) + + P256Gray uint8 = 240 // Grayscale step 8, level ~88 +) + +// 6-bit quantized LUT for Redmean-based 256-color mapping +// 64×64×64 = 262,144 bytes, fits in L2 cache +const lut256Size = 64 * 64 * 64 + +var ( + lut256Ptr atomic.Pointer[[lut256Size]uint8] + lut256Once sync.Once +) + +// lut256 returns the palette LUT, building it on first use. +func lut256() *[lut256Size]uint8 { + if p := lut256Ptr.Load(); p != nil { + return p + } + return lut256Build() +} + +//go:noinline +func lut256Build() *[lut256Size]uint8 { + lut256Once.Do(func() { + t := new([lut256Size]uint8) + for r := range 64 { + for g := range 64 { + for b := range 64 { + c := RGB{ + R: uint8(r<<2 | 2), + G: uint8(g<<2 | 2), + B: uint8(b<<2 | 2), + } + t[r<<12|g<<6|b] = computeRedmean256(c) + } + } + } + lut256Ptr.Store(t) + }) + return lut256Ptr.Load() +} + +// WarmXterm256 forces LUT construction. Idempotent, safe for concurrent use. +// Optional, prevents latency spike on first RGBTo256 render in the terminal loop. +func WarmXterm256() { _ = lut256() } + +func computeRedmean256(c RGB) uint8 { + if c.R == c.G && c.G == c.B { + if c.R < 8 { + return 16 + } + if c.R > 238 { + return 231 + } + return uint8(232 + (int(c.R)-8)/10) + } + + bestIdx := uint8(16) + minDist := 1 << 30 + + for i := range 216 { + cand := RGB{ + R: cubeValues[i/36], + G: cubeValues[(i/6)%6], + B: cubeValues[i%6], + } + if d := RedmeanDistance(c, cand); d < minDist { + minDist = d + bestIdx = uint8(16 + i) + } + } + + for i := range 24 { + g := uint8(8 + i*10) + if d := RedmeanDistance(c, RGB{R: g, G: g, B: g}); d < minDist { + minDist = d + bestIdx = uint8(232 + i) + } + } + + return bestIdx +} + +var cubeValues = [6]uint8{0, 95, 135, 175, 215, 255} + +// 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 { + return lut256()[int(c.R>>2)<<12|int(c.G>>2)<<6|int(c.B>>2)] +} + +// Cube256 returns the xterm 256-palette index for an RGB cube coordinate. +func Cube256(r, g, b uint8) uint8 { + if r > 5 { + r = 5 + } + if g > 5 { + g = 5 + } + if b > 5 { + b = 5 + } + return 16 + 36*r + 6*g + b +} + +// CubeRGB256 returns the (r, g, b) cube coordinates for a 256-palette color cube index. +func CubeRGB256(index uint8) (r, g, b uint8) { + if index < 16 || index > 231 { + return 0, 0, 0 + } + n := index - 16 + r = n / 36 + g = (n % 36) / 6 + b = n % 6 + return r, g, b +} + +// Gray256 returns the xterm 256-palette index for a grayscale step. +func Gray256(step uint8) uint8 { + if step > 23 { + step = 23 + } + return 232 + step +}