fix: renderer race condition (#210)

Guard accessing the underlying Termenv output behind a mutex. Multiple goroutines can set/get the dark background color causing a race condition.

Needs: https://github.com/muesli/termenv/pull/146
This commit is contained in:
Ayman Bagabas 2023-08-01 05:23:15 -07:00 committed by GitHub
parent b3bce2366a
commit ac8231edce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 13 deletions

View File

@ -2,6 +2,7 @@ package lipgloss
import ( import (
"io" "io"
"sync"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
@ -16,6 +17,7 @@ var renderer = &Renderer{
type Renderer struct { type Renderer struct {
output *termenv.Output output *termenv.Output
hasDarkBackground *bool hasDarkBackground *bool
mtx sync.RWMutex
} }
// RendererOption is a function that can be used to configure a [Renderer]. // RendererOption is a function that can be used to configure a [Renderer].
@ -43,11 +45,15 @@ func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer {
// Output returns the termenv output. // Output returns the termenv output.
func (r *Renderer) Output() *termenv.Output { func (r *Renderer) Output() *termenv.Output {
r.mtx.RLock()
defer r.mtx.RUnlock()
return r.output return r.output
} }
// SetOutput sets the termenv output. // SetOutput sets the termenv output.
func (r *Renderer) SetOutput(o *termenv.Output) { func (r *Renderer) SetOutput(o *termenv.Output) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.output = o r.output = o
} }
@ -78,6 +84,8 @@ func ColorProfile() termenv.Profile {
// //
// This function is thread-safe. // This function is thread-safe.
func (r *Renderer) SetColorProfile(p termenv.Profile) { func (r *Renderer) SetColorProfile(p termenv.Profile) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.output.Profile = p r.output.Profile = p
} }
@ -110,6 +118,8 @@ func HasDarkBackground() bool {
// background. A dark background can either be auto-detected, or set explicitly // background. A dark background can either be auto-detected, or set explicitly
// on the renderer. // on the renderer.
func (r *Renderer) HasDarkBackground() bool { func (r *Renderer) HasDarkBackground() bool {
r.mtx.RLock()
defer r.mtx.RUnlock()
if r.hasDarkBackground != nil { if r.hasDarkBackground != nil {
return *r.hasDarkBackground return *r.hasDarkBackground
} }
@ -139,5 +149,7 @@ func SetHasDarkBackground(b bool) {
// //
// This function is thread-safe. // This function is thread-safe.
func (r *Renderer) SetHasDarkBackground(b bool) { func (r *Renderer) SetHasDarkBackground(b bool) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.hasDarkBackground = &b r.hasDarkBackground = &b
} }

View File

@ -1,6 +1,7 @@
package lipgloss package lipgloss
import ( import (
"io"
"os" "os"
"testing" "testing"
@ -29,7 +30,24 @@ func TestRendererWithOutput(t *testing.T) {
defer os.Remove(f.Name()) defer os.Remove(f.Name())
r := NewRenderer(f) r := NewRenderer(f)
r.SetColorProfile(termenv.TrueColor) r.SetColorProfile(termenv.TrueColor)
if r.output.Profile != termenv.TrueColor { if r.ColorProfile() != termenv.TrueColor {
t.Error("Expected renderer to use true color") t.Error("Expected renderer to use true color")
} }
} }
func TestRace(t *testing.T) {
r := NewRenderer(io.Discard)
o := r.Output()
for i := 0; i < 100; i++ {
t.Run("SetColorProfile", func(t *testing.T) {
t.Parallel()
r.SetHasDarkBackground(false)
r.HasDarkBackground()
r.SetOutput(o)
r.SetColorProfile(termenv.ANSI256)
r.SetHasDarkBackground(true)
r.Output()
})
}
}

View File

@ -185,9 +185,10 @@ func (s Style) Render(strs ...string) string {
var ( var (
str = joinString(strs...) str = joinString(strs...)
te = s.r.ColorProfile().String() p = s.r.ColorProfile()
teSpace = s.r.ColorProfile().String() te = p.String()
teWhitespace = s.r.ColorProfile().String() teSpace = p.String()
teWhitespace = p.String()
bold = s.getAsBool(boldKey, false) bold = s.getAsBool(boldKey, false)
italic = s.getAsBool(italicKey, false) italic = s.getAsBool(italicKey, false)

View File

@ -9,8 +9,9 @@ import (
) )
func TestStyleRender(t *testing.T) { func TestStyleRender(t *testing.T) {
renderer.SetColorProfile(termenv.TrueColor) r := NewRenderer(io.Discard)
renderer.SetHasDarkBackground(true) r.SetColorProfile(termenv.TrueColor)
r.SetHasDarkBackground(true)
t.Parallel() t.Parallel()
tt := []struct { tt := []struct {
@ -18,31 +19,31 @@ func TestStyleRender(t *testing.T) {
expected string expected string
}{ }{
{ {
NewStyle().Foreground(Color("#5A56E0")), r.NewStyle().Foreground(Color("#5A56E0")),
"\x1b[38;2;89;86;224mhello\x1b[0m", "\x1b[38;2;89;86;224mhello\x1b[0m",
}, },
{ {
NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
"\x1b[38;2;89;86;224mhello\x1b[0m", "\x1b[38;2;89;86;224mhello\x1b[0m",
}, },
{ {
NewStyle().Bold(true), r.NewStyle().Bold(true),
"\x1b[1mhello\x1b[0m", "\x1b[1mhello\x1b[0m",
}, },
{ {
NewStyle().Italic(true), r.NewStyle().Italic(true),
"\x1b[3mhello\x1b[0m", "\x1b[3mhello\x1b[0m",
}, },
{ {
NewStyle().Underline(true), r.NewStyle().Underline(true),
"\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m", "\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m",
}, },
{ {
NewStyle().Blink(true), r.NewStyle().Blink(true),
"\x1b[5mhello\x1b[0m", "\x1b[5mhello\x1b[0m",
}, },
{ {
NewStyle().Faint(true), r.NewStyle().Faint(true),
"\x1b[2mhello\x1b[0m", "\x1b[2mhello\x1b[0m",
}, },
} }