Skip to content

Commit

Permalink
feat(ansi): implement DecodeColor function (#327)
Browse files Browse the repository at this point in the history
* feat(ansi): implement ReadColor function

This implements reading SGR colors according to the ITU T.416 and ECMA
standards. It supports reading colors in the following formats: RGB, CMY,
CMYK, and indexed colors. The function also supports reading color values
separated by semicolons (;) and colons (:).

* feat(ansi): color: support wezterm RGBA extension

See https://wezfurlong.org/wezterm/escape-sequences.html#graphic-rendition-sgr

* refactor(ansi): color: rename ReadColor to DecodeColor

* Update color.go

* Update color.go

* fix: tests
  • Loading branch information
aymanbagabas authored Jan 15, 2025
1 parent 1814328 commit 62b1fae
Show file tree
Hide file tree
Showing 2 changed files with 540 additions and 0 deletions.
170 changes: 170 additions & 0 deletions ansi/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,173 @@ func toRGBA(r, g, b uint32) (uint32, uint32, uint32, uint32) {
b |= b << 8
return r, g, b, 0xffff
}

// DecodeColor decodes a color from a slice of parameters. It returns the
// number of parameters read and the color. This function is used to read SGR
// color parameters following the ITU T.416 standard.
//
// It supports reading the following color types:
// - 0: implementation defined
// - 1: transparent
// - 2: RGB direct color
// - 3: CMY direct color
// - 4: CMYK direct color
// - 5: indexed color
// - 6: RGBA direct color (WezTerm extension)
//
// The parameters can be separated by semicolons (;) or colons (:). Mixing
// separators is not allowed.
//
// The specs supports defining a color space id, a color tolerance value, and a
// tolerance color space id. However, these values have no effect on the
// returned color and will be ignored.
//
// This implementation includes a few modifications to the specs:
// 1. Support for legacy color values separated by semicolons (;) with respect to RGB, and indexed colors
// 2. Support ignoring and omitting the color space id (second parameter) with respect to RGB colors
// 3. Support ignoring and omitting the 6th parameter with respect to RGB and CMY colors
// 4. Support reading RGBA colors
func DecodeColor(params []Parameter) (n int, co Color) {
if len(params) < 2 { // Need at least SGR type and color type
return 0, nil
}

// First parameter indicates one of 38, 48, or 58 (foreground, background, or underline)
s := params[0]
p := params[1]
colorType := p.Param(0)
n = 2

paramsfn := func() (p1, p2, p3, p4 int) {
// Where should we start reading the color?
switch {
case s.HasMore() && p.HasMore() && len(params) > 8 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && params[5].HasMore() && params[6].HasMore() && params[7].HasMore():
// We have color space id, a 6th parameter, a tolerance value, and a tolerance color space
n += 7
return params[3].Param(0), params[4].Param(0), params[5].Param(0), params[6].Param(0)
case s.HasMore() && p.HasMore() && len(params) > 7 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && params[5].HasMore() && params[6].HasMore():
// We have color space id, a 6th parameter, and a tolerance value
n += 6
return params[3].Param(0), params[4].Param(0), params[5].Param(0), params[6].Param(0)
case s.HasMore() && p.HasMore() && len(params) > 6 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && params[5].HasMore():
// We have color space id and a 6th parameter
// 48 : 4 : : 1 : 2 : 3 :4
n += 5
return params[3].Param(0), params[4].Param(0), params[5].Param(0), params[6].Param(0)
case s.HasMore() && p.HasMore() && len(params) > 5 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && !params[5].HasMore():
// We have color space
// 48 : 3 : : 1 : 2 : 3
n += 4
return params[3].Param(0), params[4].Param(0), params[5].Param(0), -1
case s.HasMore() && p.HasMore() && p.Param(0) == 2 && params[2].HasMore() && params[3].HasMore() && !params[4].HasMore():
// We have color values separated by colons (:)
// 48 : 2 : 1 : 2 : 3
fallthrough
case !s.HasMore() && !p.HasMore() && p.Param(0) == 2 && !params[2].HasMore() && !params[3].HasMore() && !params[4].HasMore():
// Support legacy color values separated by semicolons (;)
// 48 ; 2 ; 1 ; 2 ; 3
n += 3
return params[2].Param(0), params[3].Param(0), params[4].Param(0), -1
}
// Ambiguous SGR color
return -1, -1, -1, -1
}

switch colorType {
case 0: // implementation defined
return 2, nil
case 1: // transparent
return 2, color.Transparent
case 2: // RGB direct color
if len(params) < 5 {
return 0, nil
}

r, g, b, _ := paramsfn()
if r == -1 || g == -1 || b == -1 {
return 0, nil
}

co = color.RGBA{
R: uint8(r), //nolint:gosec
G: uint8(g), //nolint:gosec
B: uint8(b), //nolint:gosec
A: 0xff,
}
return

case 3: // CMY direct color
if len(params) < 5 {
return 0, nil
}

c, m, y, _ := paramsfn()
if c == -1 || m == -1 || y == -1 {
return 0, nil
}

co = color.CMYK{
C: uint8(c), //nolint:gosec
M: uint8(m), //nolint:gosec
Y: uint8(y), //nolint:gosec
K: 0,
}
return

case 4: // CMYK direct color
if len(params) < 6 {
return 0, nil
}

c, m, y, k := paramsfn()
if c == -1 || m == -1 || y == -1 || k == -1 {
return 0, nil
}

co = color.CMYK{
C: uint8(c), //nolint:gosec
M: uint8(m), //nolint:gosec
Y: uint8(y), //nolint:gosec
K: uint8(k), //nolint:gosec
}
return

case 5: // indexed color
if len(params) < 3 {
return 0, nil
}
switch {
case s.HasMore() && p.HasMore() && !params[2].HasMore():
// Colon separated indexed color
// 38 : 5 : 234
case !s.HasMore() && !p.HasMore() && !params[2].HasMore():
// Legacy semicolon indexed color
// 38 ; 5 ; 234
default:
return 0, nil
}
co = ExtendedColor(params[2].Param(0)) //nolint:gosec
return 3, co

case 6: // RGBA direct color
if len(params) < 6 {
return 0, nil
}

r, g, b, a := paramsfn()
if r == -1 || g == -1 || b == -1 || a == -1 {
return 0, nil
}

co = color.RGBA{
R: uint8(r), //nolint:gosec
G: uint8(g), //nolint:gosec
B: uint8(b), //nolint:gosec
A: uint8(a), //nolint:gosec
}
return

default:
return 0, nil
}
}
Loading

0 comments on commit 62b1fae

Please sign in to comment.