Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions hue.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,29 @@ func (s Style) Text(text string) string {
return s.wrap(text)
}

// AppendText appends the styled form of text to dst and returns the extended slice.
// If colour is disabled, text is appended unchanged.
//
// AppendText is more performant than [Style.Text] as it avoids allocating an intermediate
// string for the result — useful in hot paths where the caller already maintains a []byte buffer.
func (s Style) AppendText(dst, text []byte) []byte {
if !enabled.Load() {
return append(dst, text...)
}

code, err := s.Code()
if err != nil {
return append(dst, text...)
}

dst = append(dst, escape...)
dst = append(dst, code...)
dst = append(dst, 'm')
dst = append(dst, text...)
dst = append(dst, reset...)
return dst
}

// wrap wraps text with the styles escape and reset sequences.
func (s Style) wrap(text string) string {
if !enabled.Load() {
Expand Down
106 changes: 106 additions & 0 deletions hue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,87 @@ func TestVisual(t *testing.T) {
}
}

func TestAppendText(t *testing.T) {
tests := []struct {
name string // Name of the test case
want string // Expected result including escape sequences
dst []byte // Existing destination buffer (may be nil)
input []byte // Text to style
style hue.Style // Style under test
enabled bool // Whether hue is enabled
}{
{
name: "basic",
dst: nil,
input: []byte("hello"),
style: hue.Green,
enabled: true,
want: "\x1b[32mhello\x1b[0m",
},
{
name: "many styles",
dst: nil,
input: []byte("hello"),
style: hue.Green | hue.BlueBackground | hue.Bold | hue.Underline,
enabled: true,
want: "\x1b[1;4;32;44mhello\x1b[0m",
},
{
name: "basic disabled",
dst: nil,
input: []byte("hello"),
style: hue.Green,
enabled: false,
want: "hello",
},
{
name: "many styles disabled",
dst: nil,
input: []byte("hello"),
style: hue.Green | hue.BlueBackground | hue.Bold | hue.Underline,
enabled: false,
want: "hello",
},
{
name: "appends to existing dst",
dst: []byte("prefix:"),
input: []byte("hello"),
style: hue.Red,
enabled: true,
want: "prefix:\x1b[31mhello\x1b[0m",
},
{
name: "appends to existing dst disabled",
dst: []byte("prefix:"),
input: []byte("hello"),
style: hue.Red,
enabled: false,
want: "prefix:hello",
},
{
name: "empty input",
dst: nil,
input: []byte(""),
style: hue.Cyan,
enabled: true,
want: "\x1b[36m\x1b[0m",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hue.Enabled(tt.enabled)

got := strconv.Quote(string(tt.style.AppendText(tt.dst, tt.input)))
want := strconv.Quote(tt.want)

if got != want {
t.Errorf("\nGot:\t%v\nWanted:\t%v\n", got, want)
}
})
}
}

func BenchmarkStyle(b *testing.B) {
hue.Enabled(true)
b.Run("simple", func(b *testing.B) {
Expand Down Expand Up @@ -819,6 +900,31 @@ func BenchmarkText(b *testing.B) {
})
}

func BenchmarkAppendText(b *testing.B) {
text := []byte("some text")
hue.Enabled(true)
b.Run("simple", func(b *testing.B) {
style := hue.Cyan
for b.Loop() {
style.AppendText(nil, text)
}
})

b.Run("composite fast", func(b *testing.B) {
style := hue.Cyan | hue.WhiteBackground | hue.Bold | hue.Strikethrough
for b.Loop() {
style.AppendText(nil, text)
}
})

b.Run("composite slow", func(b *testing.B) {
style := hue.Blue | hue.Red | hue.BlackBackground | hue.Italic | hue.Strikethrough | hue.Bold | hue.Underline | hue.GreenBackground | hue.Reverse
for b.Loop() {
style.AppendText(nil, text)
}
})
}

// captureOutput captures and returns data printed to [os.Stdout] and [os.Stderr] by the provided function fn, allowing
// you to test functions that write to those streams and do not have an option to pass in an [io.Writer].
//
Expand Down