From a2c12f33703dda93a600ca9767a1dc104ddbb4b4 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 23 Jan 2025 18:16:02 -0500 Subject: [PATCH 1/3] feat(cellbuf): add common sequence decoding utilities This abstracts the decoding of Select Graphic Rendition (SGR) escape sequences and hyperlink escape sequences from the cellbuf package. This will allow other packages to use these utilities without having to duplicate the code. --- cellbuf/sequence.go | 301 ++++++++++++++++++++++++++++++++++++++++++++ cellbuf/window.go | 124 +----------------- 2 files changed, 303 insertions(+), 122 deletions(-) create mode 100644 cellbuf/sequence.go diff --git a/cellbuf/sequence.go b/cellbuf/sequence.go new file mode 100644 index 00000000..0f3240f9 --- /dev/null +++ b/cellbuf/sequence.go @@ -0,0 +1,301 @@ +package cellbuf + +import ( + "bytes" + "image/color" + + "github.com/charmbracelet/x/ansi" +) + +// ReadStyle reads a Select Graphic Rendition (SGR) escape sequences from a +// list of parameters. +func ReadStyle(params []ansi.Parameter, pen *Style) { + if len(params) == 0 { + pen.Reset() + return + } + + for i := 0; i < len(params); i++ { + r := params[i] + param, hasMore := r.Param(0), r.HasMore() // Are there more subparameters i.e. separated by ":"? + switch param { + case 0: // Reset + pen.Reset() + case 1: // Bold + pen.Bold(true) + case 2: // Dim/Faint + pen.Faint(true) + case 3: // Italic + pen.Italic(true) + case 4: // Underline + if hasMore { // Only accept subparameters i.e. separated by ":" + nextParam := params[i+1].Param(0) + switch nextParam { + case 0, 1, 2, 3, 4, 5: + i++ + switch nextParam { + case 0: // No Underline + pen.UnderlineStyle(NoUnderline) + case 1: // Single Underline + pen.UnderlineStyle(SingleUnderline) + case 2: // Double Underline + pen.UnderlineStyle(DoubleUnderline) + case 3: // Curly Underline + pen.UnderlineStyle(CurlyUnderline) + case 4: // Dotted Underline + pen.UnderlineStyle(DottedUnderline) + case 5: // Dashed Underline + pen.UnderlineStyle(DashedUnderline) + } + } + } else { + // Single Underline + pen.Underline(true) + } + case 5: // Slow Blink + pen.SlowBlink(true) + case 6: // Rapid Blink + pen.RapidBlink(true) + case 7: // Reverse + pen.Reverse(true) + case 8: // Conceal + pen.Conceal(true) + case 9: // Crossed-out/Strikethrough + pen.Strikethrough(true) + case 22: // Normal Intensity (not bold or faint) + pen.Bold(false).Faint(false) + case 23: // Not italic, not Fraktur + pen.Italic(false) + case 24: // Not underlined + pen.Underline(false) + case 25: // Blink off + pen.SlowBlink(false).RapidBlink(false) + case 27: // Positive (not reverse) + pen.Reverse(false) + case 28: // Reveal + pen.Conceal(false) + case 29: // Not crossed out + pen.Strikethrough(false) + case 30, 31, 32, 33, 34, 35, 36, 37: // Set foreground + pen.Foreground(ansi.Black + ansi.BasicColor(param-30)) //nolint:gosec + case 38: // Set foreground 256 or truecolor + var c color.Color + n := ReadStyleColor(params[i:], &c) + if n > 0 { + pen.Foreground(c) + i += n - 1 + } + case 39: // Default foreground + pen.Foreground(nil) + case 40, 41, 42, 43, 44, 45, 46, 47: // Set background + pen.Background(ansi.Black + ansi.BasicColor(param-40)) //nolint:gosec + case 48: // Set background 256 or truecolor + var c color.Color + n := ReadStyleColor(params[i:], &c) + if n > 0 { + pen.Background(c) + i += n - 1 + } + case 49: // Default Background + pen.Background(nil) + case 58: // Set underline color + var c color.Color + n := ReadStyleColor(params[i:], &c) + if n > 0 { + pen.UnderlineColor(c) + i += n - 1 + } + case 59: // Default underline color + pen.UnderlineColor(nil) + case 90, 91, 92, 93, 94, 95, 96, 97: // Set bright foreground + pen.Foreground(ansi.BrightBlack + ansi.BasicColor(param-90)) //nolint:gosec + case 100, 101, 102, 103, 104, 105, 106, 107: // Set bright background + pen.Background(ansi.BrightBlack + ansi.BasicColor(param-100)) //nolint:gosec + } + } +} + +// ReadLink reads a hyperlink escape sequence from a data buffer. +func ReadLink(p []byte, link *Link) { + params := bytes.Split(p, []byte{';'}) + if len(params) != 3 { + return + } + for _, param := range bytes.Split(params[1], []byte{':'}) { + if bytes.HasPrefix(param, []byte("id=")) { + link.URLID = string(param) + } + } + link.URL = string(params[2]) +} + +// ReadStyleColor 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 ReadStyleColor(params []ansi.Parameter, co *color.Color) (n int) { + if len(params) < 2 { // Need at least SGR type and color type + return 0 + } + + // 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 + case 1: // transparent + *co = color.Transparent + return 2 + case 2: // RGB direct color + if len(params) < 5 { + return 0 + } + + r, g, b, _ := paramsfn() + if r == -1 || g == -1 || b == -1 { + return 0 + } + + *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 + } + + c, m, y, _ := paramsfn() + if c == -1 || m == -1 || y == -1 { + return 0 + } + + *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 + } + + c, m, y, k := paramsfn() + if c == -1 || m == -1 || y == -1 || k == -1 { + return 0 + } + + *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 + } + 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 + } + *co = ansi.ExtendedColor(params[2].Param(0)) //nolint:gosec + return 3 + + case 6: // RGBA direct color + if len(params) < 6 { + return 0 + } + + r, g, b, a := paramsfn() + if r == -1 || g == -1 || b == -1 || a == -1 { + return 0 + } + + *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 + } +} diff --git a/cellbuf/window.go b/cellbuf/window.go index 3a846754..243e7ecd 100644 --- a/cellbuf/window.go +++ b/cellbuf/window.go @@ -1,7 +1,6 @@ package cellbuf import ( - "bytes" "fmt" "image/color" "strings" @@ -328,12 +327,12 @@ func (c *Window) drawString(s string, x, y int, opts *drawOpts) { case ansi.HasCsiPrefix(seq) && p.Cmd() != 0: switch p.Cmd() { case 'm': // SGR - Select Graphic Rendition - handleSgr(p, &c.cur.Style) + ReadStyle(p.Params(), &c.cur.Style) } case ansi.HasOscPrefix(seq) && p.Cmd() != 0: switch p.Cmd() { case 8: // Hyperlinks - handleHyperlinks(p, &c.cur.Link) + ReadLink(p.Data(), &c.cur.Link) } case ansi.Equal(seq, "\n"): if y+1 < c.y+c.h { @@ -351,122 +350,3 @@ func (c *Window) drawString(s string, x, y int, opts *drawOpts) { c.cur.X, c.cur.Y = x, y } - -// handleSgr handles Select Graphic Rendition (SGR) escape sequences. -func handleSgr(p *ansi.Parser, pen *Style) { - params := p.Params() - if len(params) == 0 { - pen.Reset() - return - } - - for i := 0; i < len(params); i++ { - r := params[i] - param, hasMore := r.Param(0), r.HasMore() // Are there more subparameters i.e. separated by ":"? - switch param { - case 0: // Reset - pen.Reset() - case 1: // Bold - pen.Bold(true) - case 2: // Dim/Faint - pen.Faint(true) - case 3: // Italic - pen.Italic(true) - case 4: // Underline - if hasMore { // Only accept subparameters i.e. separated by ":" - nextParam := params[i+1].Param(0) - switch nextParam { - case 0, 1, 2, 3, 4, 5: - i++ - switch nextParam { - case 0: // No Underline - pen.UnderlineStyle(NoUnderline) - case 1: // Single Underline - pen.UnderlineStyle(SingleUnderline) - case 2: // Double Underline - pen.UnderlineStyle(DoubleUnderline) - case 3: // Curly Underline - pen.UnderlineStyle(CurlyUnderline) - case 4: // Dotted Underline - pen.UnderlineStyle(DottedUnderline) - case 5: // Dashed Underline - pen.UnderlineStyle(DashedUnderline) - } - } - } else { - // Single Underline - pen.Underline(true) - } - case 5: // Slow Blink - pen.SlowBlink(true) - case 6: // Rapid Blink - pen.RapidBlink(true) - case 7: // Reverse - pen.Reverse(true) - case 8: // Conceal - pen.Conceal(true) - case 9: // Crossed-out/Strikethrough - pen.Strikethrough(true) - case 22: // Normal Intensity (not bold or faint) - pen.Bold(false).Faint(false) - case 23: // Not italic, not Fraktur - pen.Italic(false) - case 24: // Not underlined - pen.Underline(false) - case 25: // Blink off - pen.SlowBlink(false).RapidBlink(false) - case 27: // Positive (not reverse) - pen.Reverse(false) - case 28: // Reveal - pen.Conceal(false) - case 29: // Not crossed out - pen.Strikethrough(false) - case 30, 31, 32, 33, 34, 35, 36, 37: // Set foreground - pen.Foreground(ansi.Black + ansi.BasicColor(param-30)) //nolint:gosec - case 38: // Set foreground 256 or truecolor - n, c := ansi.DecodeColor(params[i:]) - if n > 0 { - pen.Foreground(c) - i += n - 1 - } - case 39: // Default foreground - pen.Foreground(nil) - case 40, 41, 42, 43, 44, 45, 46, 47: // Set background - pen.Background(ansi.Black + ansi.BasicColor(param-40)) //nolint:gosec - case 48: // Set background 256 or truecolor - n, c := ansi.DecodeColor(params[i:]) - if n > 0 { - pen.Background(c) - i += n - 1 - } - case 49: // Default Background - pen.Background(nil) - case 58: // Set underline color - n, c := ansi.DecodeColor(params[i:]) - if n > 0 { - pen.UnderlineColor(c) - i += n - 1 - } - case 59: // Default underline color - pen.UnderlineColor(nil) - case 90, 91, 92, 93, 94, 95, 96, 97: // Set bright foreground - pen.Foreground(ansi.BrightBlack + ansi.BasicColor(param-90)) //nolint:gosec - case 100, 101, 102, 103, 104, 105, 106, 107: // Set bright background - pen.Background(ansi.BrightBlack + ansi.BasicColor(param-100)) //nolint:gosec - } - } -} - -// handleHyperlinks handles hyperlink escape sequences. -func handleHyperlinks(p *ansi.Parser, link *Link) { - params := bytes.Split(p.Data(), []byte{';'}) - if len(params) != 3 { - return - } - for _, param := range bytes.Split(params[1], []byte{':'}) { - if bytes.HasPrefix(param, []byte("id=")) { - link.URLID = string(param) - } - } - link.URL = string(params[2]) -} From 19998d024cdbe9ceb2d2ee78c96ac394ef9ca699 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 24 Jan 2025 12:13:48 -0500 Subject: [PATCH 2/3] fix: add missing max util for go < 1.21 --- ansi/util_no121.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 ansi/util_no121.go diff --git a/ansi/util_no121.go b/ansi/util_no121.go new file mode 100644 index 00000000..e35c48c7 --- /dev/null +++ b/ansi/util_no121.go @@ -0,0 +1,18 @@ +//go:build !go1.21 +// +build !go1.21 + +package ansi + +type ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +func max[T ordered](a, b T) T { //nolint:predeclared + if a > b { + return a + } + return b +} From faf068faaf54b60f1750fb820d89f1f802817eec Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 24 Jan 2025 12:22:38 -0500 Subject: [PATCH 3/3] chore: remove unused --- ansi/color.go | 170 ------------------ ansi/color_test.go | 370 -------------------------------------- cellbuf/sequence_test.go | 378 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 378 insertions(+), 540 deletions(-) create mode 100644 cellbuf/sequence_test.go diff --git a/ansi/color.go b/ansi/color.go index 709b0ad7..77f8a08d 100644 --- a/ansi/color.go +++ b/ansi/color.go @@ -194,173 +194,3 @@ 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 - } -} diff --git a/ansi/color_test.go b/ansi/color_test.go index 5fa128b3..1ec42436 100644 --- a/ansi/color_test.go +++ b/ansi/color_test.go @@ -3,8 +3,6 @@ package ansi import ( "image/color" "testing" - - "github.com/charmbracelet/x/ansi/parser" ) func TestRGBAToHex(t *testing.T) { @@ -84,371 +82,3 @@ func TestHexToRGB(t *testing.T) { } } } - -func TestDecodeColor(t *testing.T) { - tests := []struct { - name string - params []Parameter - wantN int - wantColor Color - wantNil bool - }{ - { - name: "invalid - too few parameters", - params: []Parameter{38}, - wantN: 0, - wantNil: true, - }, - { - name: "implementation defined", - params: []Parameter{38, 0}, - wantN: 2, - wantNil: true, - }, - { - name: "transparent", - params: []Parameter{38, 1}, - wantN: 2, - wantColor: color.Transparent, - }, - { - name: "RGB semicolon separated", - params: []Parameter{38, 2, 100, 150, 200}, - wantN: 5, - wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, - }, - { - name: "RGB colon separated", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200, - }, - wantN: 5, - wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, - }, - { - name: "RGB with color space", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, - 1 | parser.HasMoreFlag, // color space id - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200, - }, - wantN: 6, - wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, - }, - // { - // name: "CMY semicolon separated", - // params: []Parameter{38, 3, 100, 150, 200}, - // wantN: 5, - // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 0}, - // }, - { - name: "CMY with color space", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 3 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, // color space id - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200, - }, - wantN: 6, - wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 0}, - }, - // { - // name: "CMY colon separated", - // params: []Parameter{ - // 38 | parser.HasMoreFlag, - // 3 | parser.HasMoreFlag, - // 100 | parser.HasMoreFlag, - // 150 | parser.HasMoreFlag, - // 200, - // }, - // wantN: 5, - // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 0}, - // }, - // { - // name: "CMYK semicolon separated", - // params: []Parameter{38, 4, 100, 150, 200, 50}, - // wantN: 6, - // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 50}, - // }, - { - name: "CMYK with color space", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 4 | parser.HasMoreFlag, - 1 | parser.HasMoreFlag, // color space id - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200 | parser.HasMoreFlag, - 50, - }, - wantN: 7, - wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 50}, - }, - // { - // name: "CMYK colon separated", - // params: []Parameter{ - // 38 | parser.HasMoreFlag, - // 4 | parser.HasMoreFlag, - // 100 | parser.HasMoreFlag, - // 150 | parser.HasMoreFlag, - // 200 | parser.HasMoreFlag, - // 50, - // }, - // wantN: 6, - // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 50}, - // }, - { - name: "indexed color semicolon", - params: []Parameter{38, 5, 123}, - wantN: 3, - wantColor: ExtendedColor(123), - }, - { - name: "indexed color colon", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 5 | parser.HasMoreFlag, - 123, - }, - wantN: 3, - wantColor: ExtendedColor(123), - }, - { - name: "invalid color type", - params: []Parameter{38, 99}, - wantN: 0, - wantNil: true, - }, - { - name: "RGB with tolerance and color space", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, - 1 | parser.HasMoreFlag, // color space id - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200 | parser.HasMoreFlag, - 0 | parser.HasMoreFlag, // tolerance value - 1, // tolerance color space - }, - wantN: 8, - wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, - }, - // Invalid cases - { - name: "empty params", - params: []Parameter{}, - wantN: 0, - wantNil: true, - }, - { - name: "single param", - params: []Parameter{38}, - wantN: 0, - wantNil: true, - }, - { - name: "nil params", - params: nil, - wantN: 0, - wantNil: true, - }, - // Mixed separator cases (should fail) - { - name: "RGB mixed separators", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2, // semicolon - 100 | parser.HasMoreFlag, // colon - 150, // semicolon - 200, - }, - wantN: 0, - wantNil: true, - }, - { - name: "CMYK mixed separators", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 4, // semicolon - 100 | parser.HasMoreFlag, // colon - 150, // semicolon - 200 | parser.HasMoreFlag, // colon - 50, - }, - wantN: 0, - wantNil: true, - }, - // Edge cases - { - name: "RGB with max values", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, - 255 | parser.HasMoreFlag, - 255 | parser.HasMoreFlag, - 255, - }, - wantN: 5, - wantColor: color.RGBA{R: 255, G: 255, B: 255, A: 255}, - }, - { - name: "RGB with negative values", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, - -1 | parser.HasMoreFlag, - -1 | parser.HasMoreFlag, - -1, - }, - wantN: 0, - wantNil: true, - }, - { - name: "indexed color with out of range index", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 5 | parser.HasMoreFlag, - 256, // out of range - }, - wantN: 3, - wantColor: ExtendedColor(0), - }, - { - name: "indexed color with negative index", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 5 | parser.HasMoreFlag, - -1, - }, - wantN: 0, - wantNil: true, - }, - { - name: "RGB truncated params", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 2 | parser.HasMoreFlag, - 100 | parser.HasMoreFlag, - 150, - }, - wantN: 0, - wantNil: true, - }, - { - name: "CMYK truncated params", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 4 | parser.HasMoreFlag, - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200, - }, - wantN: 0, - wantNil: true, - }, - // RGBA (type 6) test cases - // { - // name: "RGBA semicolon separated", - // params: []Parameter{38, 6, 100, 150, 200, 128}, - // wantN: 6, - // wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, - // }, - // { - // name: "RGBA colon separated", - // params: []Parameter{ - // 38 | parser.HasMoreFlag, - // 6 | parser.HasMoreFlag, - // 100 | parser.HasMoreFlag, - // 150 | parser.HasMoreFlag, - // 200 | parser.HasMoreFlag, - // 128, - // }, - // wantN: 6, - // wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, - // }, - { - name: "RGBA with color space", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 6 | parser.HasMoreFlag, - 1 | parser.HasMoreFlag, // color space id - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200 | parser.HasMoreFlag, - 128, - }, - wantN: 7, - wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, - }, - { - name: "RGBA with tolerance and color space", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 6 | parser.HasMoreFlag, - 1 | parser.HasMoreFlag, // color space id - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200 | parser.HasMoreFlag, - 128 | parser.HasMoreFlag, - 0 | parser.HasMoreFlag, // tolerance value - 1, // tolerance color space - }, - wantN: 9, - wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, - }, - { - name: "RGBA with max values", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 6 | parser.HasMoreFlag, - 0 | parser.HasMoreFlag, // color space id - 255 | parser.HasMoreFlag, - 255 | parser.HasMoreFlag, - 255 | parser.HasMoreFlag, - 255, - }, - wantN: 7, - wantColor: color.RGBA{R: 255, G: 255, B: 255, A: 255}, - }, - { - name: "RGBA truncated params", - params: []Parameter{ - 38 | parser.HasMoreFlag, - 6 | parser.HasMoreFlag, - 100 | parser.HasMoreFlag, - 150 | parser.HasMoreFlag, - 200, - }, - wantN: 0, - wantNil: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotN, gotColor := DecodeColor(tt.params) - if gotN != tt.wantN { - t.Errorf("ReadColor() gotN = %v, want %v", gotN, tt.wantN) - } - if tt.wantNil { - if gotColor != nil { - t.Errorf("ReadColor() gotColor = %v, want nil", gotColor) - } - return - } - if gotColor != tt.wantColor { - t.Errorf("ReadColor() gotColor = %v, want %v", gotColor, tt.wantColor) - } - }) - } -} diff --git a/cellbuf/sequence_test.go b/cellbuf/sequence_test.go new file mode 100644 index 00000000..8df76ad0 --- /dev/null +++ b/cellbuf/sequence_test.go @@ -0,0 +1,378 @@ +package cellbuf + +import ( + "image/color" + "testing" + + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/ansi/parser" +) + +func TestReadStyleColor(t *testing.T) { + tests := []struct { + name string + params []ansi.Parameter + wantN int + wantColor color.Color + wantNil bool + }{ + { + name: "invalid - too few parameters", + params: []ansi.Parameter{38}, + wantN: 0, + wantNil: true, + }, + { + name: "implementation defined", + params: []ansi.Parameter{38, 0}, + wantN: 2, + wantNil: true, + }, + { + name: "transparent", + params: []ansi.Parameter{38, 1}, + wantN: 2, + wantColor: color.Transparent, + }, + { + name: "RGB semicolon separated", + params: []ansi.Parameter{38, 2, 100, 150, 200}, + wantN: 5, + wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, + }, + { + name: "RGB colon separated", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200, + }, + wantN: 5, + wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, + }, + { + name: "RGB with color space", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, + 1 | parser.HasMoreFlag, // color space id + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200, + }, + wantN: 6, + wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, + }, + // { + // name: "CMY semicolon separated", + // params: []ansi.Parameter{38, 3, 100, 150, 200}, + // wantN: 5, + // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 0}, + // }, + { + name: "CMY with color space", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 3 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, // color space id + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200, + }, + wantN: 6, + wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 0}, + }, + // { + // name: "CMY colon separated", + // params: []ansi.Parameter{ + // 38 | parser.HasMoreFlag, + // 3 | parser.HasMoreFlag, + // 100 | parser.HasMoreFlag, + // 150 | parser.HasMoreFlag, + // 200, + // }, + // wantN: 5, + // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 0}, + // }, + // { + // name: "CMYK semicolon separated", + // params: []ansi.Parameter{38, 4, 100, 150, 200, 50}, + // wantN: 6, + // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 50}, + // }, + { + name: "CMYK with color space", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 4 | parser.HasMoreFlag, + 1 | parser.HasMoreFlag, // color space id + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200 | parser.HasMoreFlag, + 50, + }, + wantN: 7, + wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 50}, + }, + // { + // name: "CMYK colon separated", + // params: []ansi.Parameter{ + // 38 | parser.HasMoreFlag, + // 4 | parser.HasMoreFlag, + // 100 | parser.HasMoreFlag, + // 150 | parser.HasMoreFlag, + // 200 | parser.HasMoreFlag, + // 50, + // }, + // wantN: 6, + // wantColor: color.CMYK{C: 100, M: 150, Y: 200, K: 50}, + // }, + { + name: "indexed color semicolon", + params: []ansi.Parameter{38, 5, 123}, + wantN: 3, + wantColor: ansi.ExtendedColor(123), + }, + { + name: "indexed color colon", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 5 | parser.HasMoreFlag, + 123, + }, + wantN: 3, + wantColor: ansi.ExtendedColor(123), + }, + { + name: "invalid color type", + params: []ansi.Parameter{38, 99}, + wantN: 0, + wantNil: true, + }, + { + name: "RGB with tolerance and color space", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, + 1 | parser.HasMoreFlag, // color space id + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200 | parser.HasMoreFlag, + 0 | parser.HasMoreFlag, // tolerance value + 1, // tolerance color space + }, + wantN: 8, + wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 255}, + }, + // Invalid cases + { + name: "empty params", + params: []ansi.Parameter{}, + wantN: 0, + wantNil: true, + }, + { + name: "single param", + params: []ansi.Parameter{38}, + wantN: 0, + wantNil: true, + }, + { + name: "nil params", + params: nil, + wantN: 0, + wantNil: true, + }, + // Mixed separator cases (should fail) + { + name: "RGB mixed separators", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2, // semicolon + 100 | parser.HasMoreFlag, // colon + 150, // semicolon + 200, + }, + wantN: 0, + wantNil: true, + }, + { + name: "CMYK mixed separators", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 4, // semicolon + 100 | parser.HasMoreFlag, // colon + 150, // semicolon + 200 | parser.HasMoreFlag, // colon + 50, + }, + wantN: 0, + wantNil: true, + }, + // Edge cases + { + name: "RGB with max values", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, + 255 | parser.HasMoreFlag, + 255 | parser.HasMoreFlag, + 255, + }, + wantN: 5, + wantColor: color.RGBA{R: 255, G: 255, B: 255, A: 255}, + }, + { + name: "RGB with negative values", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, + -1 | parser.HasMoreFlag, + -1 | parser.HasMoreFlag, + -1, + }, + wantN: 0, + wantNil: true, + }, + { + name: "indexed color with out of range index", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 5 | parser.HasMoreFlag, + 256, // out of range + }, + wantN: 3, + wantColor: ansi.ExtendedColor(0), + }, + { + name: "indexed color with negative index", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 5 | parser.HasMoreFlag, + -1, + }, + wantN: 0, + wantNil: true, + }, + { + name: "RGB truncated params", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 2 | parser.HasMoreFlag, + 100 | parser.HasMoreFlag, + 150, + }, + wantN: 0, + wantNil: true, + }, + { + name: "CMYK truncated params", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 4 | parser.HasMoreFlag, + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200, + }, + wantN: 0, + wantNil: true, + }, + // RGBA (type 6) test cases + // { + // name: "RGBA semicolon separated", + // params: []Parameter{38, 6, 100, 150, 200, 128}, + // wantN: 6, + // wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, + // }, + // { + // name: "RGBA colon separated", + // params: []ansi.Parameter{ + // 38 | parser.HasMoreFlag, + // 6 | parser.HasMoreFlag, + // 100 | parser.HasMoreFlag, + // 150 | parser.HasMoreFlag, + // 200 | parser.HasMoreFlag, + // 128, + // }, + // wantN: 6, + // wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, + // }, + { + name: "RGBA with color space", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 6 | parser.HasMoreFlag, + 1 | parser.HasMoreFlag, // color space id + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200 | parser.HasMoreFlag, + 128, + }, + wantN: 7, + wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, + }, + { + name: "RGBA with tolerance and color space", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 6 | parser.HasMoreFlag, + 1 | parser.HasMoreFlag, // color space id + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200 | parser.HasMoreFlag, + 128 | parser.HasMoreFlag, + 0 | parser.HasMoreFlag, // tolerance value + 1, // tolerance color space + }, + wantN: 9, + wantColor: color.RGBA{R: 100, G: 150, B: 200, A: 128}, + }, + { + name: "RGBA with max values", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 6 | parser.HasMoreFlag, + 0 | parser.HasMoreFlag, // color space id + 255 | parser.HasMoreFlag, + 255 | parser.HasMoreFlag, + 255 | parser.HasMoreFlag, + 255, + }, + wantN: 7, + wantColor: color.RGBA{R: 255, G: 255, B: 255, A: 255}, + }, + { + name: "RGBA truncated params", + params: []ansi.Parameter{ + 38 | parser.HasMoreFlag, + 6 | parser.HasMoreFlag, + 100 | parser.HasMoreFlag, + 150 | parser.HasMoreFlag, + 200, + }, + wantN: 0, + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotColor color.Color + gotN := ReadStyleColor(tt.params, &gotColor) + if gotN != tt.wantN { + t.Errorf("ReadColor() gotN = %v, want %v", gotN, tt.wantN) + } + if tt.wantNil { + if gotColor != nil { + t.Errorf("ReadColor() gotColor = %v, want nil", gotColor) + } + return + } + if gotColor != tt.wantColor { + t.Errorf("ReadColor() gotColor = %v, want %v", gotColor, tt.wantColor) + } + }) + } +}