diff --git a/hue.go b/hue.go index b91c884..2a73530 100644 --- a/hue.go +++ b/hue.go @@ -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() { diff --git a/hue_test.go b/hue_test.go index bbb942d..fae425d 100644 --- a/hue_test.go +++ b/hue_test.go @@ -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) { @@ -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]. //