~rockorager/tcell-ansi

1ca28631741fd6791e3f0f2fa77133e7ecfa5d77 — Tim Culverhouse 1 year, 4 months ago
initial commit

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
7 files changed, 656 insertions(+), 0 deletions(-)

A LICENSE
A ansi.go
A ansi_test.go
A go.mod
A go.sum
A parse.go
A parse_test.go
A  => LICENSE +21 -0
@@ 1,21 @@
The MIT License (MIT)

Copyright (c) 2023 Tim Culverhouse

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

A  => ansi.go +130 -0
@@ 1,130 @@
package ansi

import (
	"strings"

	"github.com/gdamore/tcell/v2"
	"github.com/gdamore/tcell/v2/terminfo"
	"github.com/mattn/go-runewidth"
)

// StyledRune is a rune and it's associated style. The rune has already been
// measured using go-runewidth
type StyledRune struct {
	Value rune
	Width int
	Style tcell.Style
}

// RuneBuffer is a buffer of runes styled with tcell.Style objects
type RuneBuffer struct {
	buf []*StyledRune
}

// Returns the internal slice of styled runes
func (rb *RuneBuffer) Runes() []*StyledRune {
	return rb.buf
}

// Write writes a rune and it's associated style to the RuneBuffer
func (rb *RuneBuffer) Write(r rune, style tcell.Style) {
	w := runewidth.RuneWidth(r)
	rb.buf = append(rb.buf, &StyledRune{r, w, style})
}

// String outputs a styled-string using TERM=xterm-256color
func (rb *RuneBuffer) String() string {
	ti, err := terminfo.LookupTerminfo("xterm-256color")
	if err != nil {
		// Who knows what happened
		return ""
	}
	s := strings.Builder{}
	style := tcell.StyleDefault
	hasStyle := false
	for _, r := range rb.buf {
		if style != r.Style {
			hasStyle = true
			style = r.Style
			s.WriteString(ti.AttrOff)
			fg, bg, attrs := style.Decompose()

			switch {
			case fg.IsRGB() && bg.IsRGB() && ti.SetFgBgRGB != "":
				fr, fg, fb := fg.RGB()
				br, bg, bb := bg.RGB()
				s.WriteString(ti.TParm(
					ti.SetFgBgRGB,
					int(fr),
					int(fg),
					int(fb),
					int(br),
					int(bg),
					int(bb),
				))
			case fg.IsRGB() && ti.SetFgRGB != "":
				// RGB
				r, g, b := fg.RGB()
				s.WriteString(ti.TParm(ti.SetFgRGB, int(r), int(g), int(b)))
			case bg.IsRGB() && ti.SetBgRGB != "":
				// RGB
				r, g, b := bg.RGB()
				s.WriteString(ti.TParm(ti.SetBgRGB, int(r), int(g), int(b)))

				// Indexed
			case fg.Valid() && bg.Valid() && ti.SetFgBg != "":
				s.WriteString(ti.TParm(ti.SetFgBg, int(fg&0xff), int(bg&0xff)))
			case fg.Valid() && ti.SetFg != "":
				s.WriteString(ti.TParm(ti.SetFg, int(fg&0xff)))
			case bg.Valid() && ti.SetBg != "":
				s.WriteString(ti.TParm(ti.SetBg, int(bg&0xff)))
			}

			if attrs&tcell.AttrBold != 0 {
				s.WriteString(ti.Bold)
			}
			if attrs&tcell.AttrUnderline != 0 {
				s.WriteString(ti.Underline)
			}
			if attrs&tcell.AttrReverse != 0 {
				s.WriteString(ti.Reverse)
			}
			if attrs&tcell.AttrBlink != 0 {
				s.WriteString(ti.Blink)
			}
			if attrs&tcell.AttrDim != 0 {
				s.WriteString(ti.Dim)
			}
			if attrs&tcell.AttrItalic != 0 {
				s.WriteString(ti.Italic)
			}
			if attrs&tcell.AttrStrikeThrough != 0 {
				s.WriteString(ti.StrikeThrough)
			}
		}
		s.WriteRune(r.Value)
	}
	if hasStyle {
		s.WriteString(ti.AttrOff)
	}
	return s.String()
}

func (rb *RuneBuffer) Len() int {
	l := 0
	for _, r := range rb.buf {
		l += r.Width
	}
	return l
}

// Applies a style to a string. Any currently applied styles will not be overwritten
func ApplyStyle(style tcell.Style, str string) string {
	rb := NewParser().Parse(str)
	for _, sr := range rb.buf {
		if sr.Style == tcell.StyleDefault {
			sr.Style = style
		}
	}
	return rb.String()
}

A  => ansi_test.go +19 -0
@@ 1,19 @@
package ansi_test

import (
	"fmt"
	"testing"

	ansi "git.sr.ht/~rockorager/tcell-ansi"
	"github.com/gdamore/tcell/v2"
)

func TestAnsi(t *testing.T) {
	style := tcell.StyleDefault.Foreground(tcell.ColorRed)
	s := "This is red."
	out := ansi.ApplyStyle(style, s)
	fmt.Println(out)
	style = tcell.StyleDefault.Foreground(tcell.ColorBlue)
	next := ansi.ApplyStyle(style, "This is blue." + out)
	fmt.Println(next)
}

A  => go.mod +21 -0
@@ 1,21 @@
module git.sr.ht/~rockorager/tcell-ansi

go 1.19

require (
	github.com/gdamore/tcell/v2 v2.5.4
	github.com/mattn/go-runewidth v0.0.14
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/gdamore/encoding v1.0.0 // indirect
	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/rivo/uniseg v0.2.0 // indirect
	github.com/stretchr/testify v1.8.1 // indirect
	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
	golang.org/x/text v0.5.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

A  => go.sum +53 -0
@@ 1,53 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

A  => parse.go +205 -0
@@ 1,205 @@
package ansi

import (
	"io"
	"strconv"
	"strings"

	"github.com/gdamore/tcell/v2"
)

// A Parser parses a string into a RuneBuffer
type Parser struct {
	buf      *RuneBuffer
	curStyle tcell.Style
}

func NewParser() *Parser {
	return &Parser{
		buf:      &RuneBuffer{},
		curStyle: tcell.StyleDefault,
	}
}

// Parses a styled string into a RuneBuffer
func (p *Parser) Parse(s string) *RuneBuffer {
	rdr := strings.NewReader(s)

	for {
		r, _, err := rdr.ReadRune()
		if err == io.EOF {
			break
		}
		switch r {
		case 0x1b:
			p.handleSeq(rdr)
		default:
			p.buf.Write(r, p.curStyle)
		}
	}
	return p.buf
}

func (p *Parser) handleSeq(rdr io.RuneReader) {
	r, _, err := rdr.ReadRune()
	if err == io.EOF {
		return
	}
	switch r {
	case '[': // CSI
		p.handleCSI(rdr)
	case ']': // OSC
	case '(': // Designate G0 charset
		p.swallow(rdr, 1)
	}
}

func (p *Parser) handleCSI(rdr io.RuneReader) {
	var (
		params []int
		param  []rune
		hasErr bool
		er     error
	)
outer:
	for {
		r, _, err := rdr.ReadRune()
		if err == io.EOF {
			return
		}
		switch true {
		case r >= 0x30 && r <= 0x39:
			param = append(param, r)
		case r == ':' || r == ';':
			var ps int
			if len(param) > 0 {
				ps, er = strconv.Atoi(string(param))
				if er != nil {
					hasErr = true
					continue
				}
			}
			params = append(params, ps)
			param = []rune{}
		case r == 'm':
			var ps int
			if len(param) > 0 {
				ps, er = strconv.Atoi(string(param))
				if er != nil {
					err = er
					hasErr = true
					continue
				}
			}
			params = append(params, ps)
			break outer
		}
	}
	if hasErr {
		// leave the cursor unchanged
		return
	}
	for i := 0; i < len(params); i++ {
		param := params[i]
		switch param {
		case 0:
			p.curStyle = tcell.StyleDefault
		case 1:
			p.curStyle = p.curStyle.Bold(true)
		case 2:
			p.curStyle = p.curStyle.Dim(true)
		case 3:
			p.curStyle = p.curStyle.Italic(true)
		case 4:
			p.curStyle = p.curStyle.Underline(true)
		case 5:
			p.curStyle = p.curStyle.Blink(true)
		case 6:
			// rapid blink, not supported by tcell. fallback to slow
			// blink
			p.curStyle = p.curStyle.Blink(true)
		case 7:
			p.curStyle = p.curStyle.Reverse(true)
		case 8:
			// Hidden. not supported by tcell
		case 9:
			p.curStyle = p.curStyle.StrikeThrough(true)
		case 21:
			p.curStyle = p.curStyle.Bold(false)
		case 22:
			p.curStyle = p.curStyle.Dim(false)
		case 23:
			p.curStyle = p.curStyle.Italic(false)
		case 24:
			p.curStyle = p.curStyle.Underline(false)
		case 25:
			p.curStyle = p.curStyle.Blink(false)
		case 26:
			// rapid blink, not supported by tcell. fallback to slow
			// blink
			p.curStyle = p.curStyle.Blink(false)
		case 27:
			p.curStyle = p.curStyle.Reverse(false)
		case 28:
			// Hidden. unsupported by tcell
		case 29:
			p.curStyle = p.curStyle.StrikeThrough(false)
		case 30, 31, 32, 33, 34, 35, 36, 37:
			p.curStyle = p.curStyle.Foreground(tcell.PaletteColor(param - 30))
		case 38:
			if i+2 < len(params) && params[i+1] == 5 {
				p.curStyle = p.curStyle.Foreground(tcell.PaletteColor(params[i+2]))
				i += 2
			}
			if i+4 < len(params) && params[i+1] == 2 {
				switch len(params) {
				case 6:
					r := int32(params[i+3])
					g := int32(params[i+4])
					b := int32(params[i+5])
					p.curStyle = p.curStyle.Foreground(tcell.NewRGBColor(r, g, b))
					i += 5
				default:
					r := int32(params[i+2])
					g := int32(params[i+3])
					b := int32(params[i+4])
					p.curStyle = p.curStyle.Foreground(tcell.NewRGBColor(r, g, b))
					i += 4
				}
			}
		case 40, 41, 42, 43, 44, 45, 46, 47:
			p.curStyle = p.curStyle.Background(tcell.PaletteColor(param - 40))
		case 48:
			if i+2 < len(params) && params[i+1] == 5 {
				p.curStyle = p.curStyle.Background(tcell.PaletteColor(params[i+2]))
				i += 2
			}
			if i+4 < len(params) && params[i+1] == 2 {
				switch len(params) {
				case 6:
					r := int32(params[i+3])
					g := int32(params[i+4])
					b := int32(params[i+5])
					p.curStyle = p.curStyle.Background(tcell.NewRGBColor(r, g, b))
					i += 5
				default:
					r := int32(params[i+2])
					g := int32(params[i+3])
					b := int32(params[i+4])
					p.curStyle = p.curStyle.Background(tcell.NewRGBColor(r, g, b))
					i += 4
				}
			}
		case 90, 91, 92, 93, 94, 95, 96, 97:
			p.curStyle = p.curStyle.Foreground(tcell.PaletteColor(param - 90 + 8))
		case 100, 101, 102, 103, 104, 105, 106, 107:
			p.curStyle = p.curStyle.Background(tcell.PaletteColor(param - 100 + 8))
		}
	}
}

func (p *Parser) swallow(rdr io.RuneReader, n int) {
	for i := 0; i < n; i++ {
		rdr.ReadRune()
	}
}

A  => parse_test.go +207 -0
@@ 1,207 @@
package ansi_test

import (
	"testing"

	ansi "git.sr.ht/~rockorager/tcell-ansi"
	"github.com/stretchr/testify/assert"
)

func TestParser(t *testing.T) {
	tests := []struct {
		name           string
		input          string
		expectedString string
		expectedLen    int
	}{
		{
			name:           "no style",
			input:          "hello, world",
			expectedString: "hello, world",
			expectedLen:    12,
		},
		{
			name:           "bold",
			input:          "\x1b[1mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[1mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "dim",
			input:          "\x1b[2mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[2mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "bold and dim",
			input:          "\x1b[1;2mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[1m\x1b[2mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "italic",
			input:          "\x1b[3mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[3mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "underline",
			input:          "\x1b[4mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[4mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "blink",
			input:          "\x1b[5mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[5mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "fast blink",
			input:          "\x1b[6mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[5mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "reverse",
			input:          "\x1b[7mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[7mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "hidden",
			input:          "\x1b[8mhello, world",
			expectedString: "hello, world",
			expectedLen:    12,
		},
		{
			name:           "strikethrough",
			input:          "\x1b[9mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[9mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "bold hello, normal world",
			input:          "\x1b[1mhello, \x1b[21mworld",
			expectedString: "\x1b(B\x1b[m\x1b[1mhello, \x1b(B\x1b[mworld\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "bold hello, normal world v2",
			input:          "\x1b[1mhello, \x1b[mworld",
			expectedString: "\x1b(B\x1b[m\x1b[1mhello, \x1b(B\x1b[mworld\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "8 bit color: foreground",
			input:          "\x1b[30mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[30mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "8 bit color: background",
			input:          "\x1b[41mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[41mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "8 bit color: foreground and background",
			input:          "\x1b[31;41mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[31;41mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "16 bit color: foreground",
			input:          "\x1b[90mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[90mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "16 bit color: background",
			input:          "\x1b[101mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[101mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "16 bit color: foreground and background",
			input:          "\x1b[91;101mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[91;101mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "256 color: foreground",
			input:          "\x1b[38;5;2mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[32mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "256 color: foreground",
			input:          "\x1b[38;5;132mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[38;5;132mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "256 color: background",
			input:          "\x1b[48;5;132mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[48;5;132mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "256 color: foreground and background",
			input:          "\x1b[38;5;20;48;5;20mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[38;5;20;48;5;20mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "256 color: background",
			input:          "\x1b[48;5;2mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[42mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "true color: foreground",
			input:          "\x1b[38;2;0;0;0mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[38;2;0;0;0mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "true color: foreground with color space",
			input:          "\x1b[38;2;;0;0;0mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[38;2;0;0;0mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "true color: foreground with color space and colons",
			input:          "\x1b[38:2::0:0:0mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[38;2;0;0;0mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "true color: background",
			input:          "\x1b[48;2;0;0;0mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[48;2;0;0;0mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "true color: background with color space",
			input:          "\x1b[48;2;;0;0;0mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[48;2;0;0;0mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
		{
			name:           "true color: foreground and background",
			input:          "\x1b[38;2;200;200;200;48;2;0;0;0mhello, world",
			expectedString: "\x1b(B\x1b[m\x1b[38;2;200;200;200;48;2;0;0;0mhello, world\x1b(B\x1b[m",
			expectedLen:    12,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			parser := ansi.NewParser()
			buf := parser.Parse(test.input)
			assert.Equal(t, test.expectedString, buf.String())
			assert.Equal(t, test.expectedLen, buf.Len())
		})
	}
}