Add perceptual 16-colour mapping

Dark shades map to black and greys to the grey/white entries while
saturated category colours keep their hue, instead of the stock
conversion's bright yellow/cyan shadows.
This commit is contained in:
Andy Dixon
2026-10-02 08:38:36 +01:00
parent d7e1d3e620
commit cb8ae54258
4 changed files with 120 additions and 12 deletions
+31 -10
View File
@@ -232,7 +232,16 @@ func (cv *Canvas) Line(y int) string {
return b.String()
}
func sgr(b *strings.Builder, st Style, p256 bool) {
// colorDepth selects how colours are serialised.
type colorDepth uint8
const (
depthTrue colorDepth = iota
depth256
depth16
)
func sgr(b *strings.Builder, st Style, depth colorDepth) {
b.WriteString("\x1b[0")
if st.Attr&Bold != 0 {
b.WriteString(";1")
@@ -253,10 +262,22 @@ func sgr(b *strings.Builder, st Style, p256 bool) {
if c == 0 {
return
}
if p256 {
switch depth {
case depth256:
b.WriteString(";" + base + ";5;")
b.WriteString(strconv.Itoa(int(quant256(c))))
return
case depth16:
n := int(quant16(c))
code := 30 + n
if n >= 8 {
code = 90 + n - 8
}
if base == "48" {
code += 10
}
b.WriteString(";" + strconv.Itoa(code))
return
}
b.WriteString(";" + base + ";2;")
b.WriteString(strconv.Itoa(int(c >> 16 & 0xff)))
@@ -270,14 +291,14 @@ func sgr(b *strings.Builder, st Style, p256 bool) {
b.WriteByte('m')
}
// String serialises the canvas as lines of text with SGR styling. Colours
// are emitted as 24-bit, or as 256-colour indices from our own quantizer
// when p256 is set; Bubble Tea downsamples further (16 colours, none) to
// the terminal's profile.
func (cv *Canvas) String() string { return cv.Render(false) }
// String serialises the canvas as lines of text with SGR styling and
// 24-bit colours.
func (cv *Canvas) String() string { return cv.Render(depthTrue) }
// Render serialises the canvas; see String.
func (cv *Canvas) Render(p256 bool) string {
// Render serialises the canvas. For 256 and 16 colours it quantizes with
// its own perceptual mapping (see color256.go); Bubble Tea strips colour
// entirely for colourless terminals.
func (cv *Canvas) Render(depth colorDepth) string {
var b strings.Builder
b.Grow(cv.W * cv.H * 4)
for y := 0; y < cv.H; y++ {
@@ -292,7 +313,7 @@ func (cv *Canvas) Render(p256 bool) string {
continue
}
if first || c.st != cur {
sgr(&b, c.st, p256)
sgr(&b, c.st, depth)
cur, first = c.st, false
}
b.WriteString(c.s)