Skip to content

Commit b8e6cbb

Browse files
committed
fix: renderer race condition
Guard accessing the underlying Termenv output behind a mutex. Multiple goroutines can set/get the color profile causing a race condition. Needs: muesli/termenv#146
1 parent c43b22e commit b8e6cbb

File tree

6 files changed

+60
-18
lines changed

6 files changed

+60
-18
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ go 1.17
55
require (
66
github.com/mattn/go-runewidth v0.0.14
77
github.com/muesli/reflow v0.3.0
8-
github.com/muesli/termenv v0.15.1
8+
github.com/muesli/termenv v0.15.3-0.20230728171558-3c898b2ce7c3
99
)
1010

1111
require (
1212
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
1313
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
14-
github.com/mattn/go-isatty v0.0.17 // indirect
14+
github.com/mattn/go-isatty v0.0.19 // indirect
1515
github.com/rivo/uniseg v0.2.0 // indirect
16-
golang.org/x/sys v0.6.0 // indirect
16+
golang.org/x/sys v0.10.0 // indirect
1717
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
44
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
55
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
66
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
7+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
8+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
79
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
810
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
911
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
1012
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
1113
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
1214
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
1315
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
16+
github.com/muesli/termenv v0.15.3-0.20230728162136-de1ed946b031 h1:mSQxHDNYTlwIFgiPPz8W+rulsHVCFzFAbbc/XQo7BfI=
17+
github.com/muesli/termenv v0.15.3-0.20230728162136-de1ed946b031/go.mod h1:cTIDIpWz9VkemD0+7lFZuZ8my3zF4iDi115tBmAocz0=
18+
github.com/muesli/termenv v0.15.3-0.20230728164039-3cf3563b77d7 h1:FNxK5hfhxljbUS5MbiZs6iAyo6dMI2qLI+G8q0GJhVE=
19+
github.com/muesli/termenv v0.15.3-0.20230728164039-3cf3563b77d7/go.mod h1:cTIDIpWz9VkemD0+7lFZuZ8my3zF4iDi115tBmAocz0=
20+
github.com/muesli/termenv v0.15.3-0.20230728171558-3c898b2ce7c3 h1:9Sx5BlHeicxZHFBqfA4zcPtcTpmSLdxW1l4DgNszt+U=
21+
github.com/muesli/termenv v0.15.3-0.20230728171558-3c898b2ce7c3/go.mod h1:cTIDIpWz9VkemD0+7lFZuZ8my3zF4iDi115tBmAocz0=
1422
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
1523
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
1624
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
1725
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1826
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
1927
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28+
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
29+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

renderer.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package lipgloss
22

33
import (
44
"io"
5+
"sync"
56

67
"github.com/muesli/termenv"
78
)
@@ -16,6 +17,7 @@ var renderer = &Renderer{
1617
type Renderer struct {
1718
output *termenv.Output
1819
hasDarkBackground *bool
20+
mtx sync.RWMutex
1921
}
2022

2123
// RendererOption is a function that can be used to configure a [Renderer].
@@ -43,17 +45,21 @@ func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer {
4345

4446
// Output returns the termenv output.
4547
func (r *Renderer) Output() *termenv.Output {
48+
r.mtx.RLock()
49+
defer r.mtx.RUnlock()
4650
return r.output
4751
}
4852

4953
// SetOutput sets the termenv output.
5054
func (r *Renderer) SetOutput(o *termenv.Output) {
55+
r.mtx.Lock()
56+
defer r.mtx.Unlock()
5157
r.output = o
5258
}
5359

5460
// ColorProfile returns the detected termenv color profile.
5561
func (r *Renderer) ColorProfile() termenv.Profile {
56-
return r.output.Profile
62+
return r.output.ColorProfile()
5763
}
5864

5965
// ColorProfile returns the detected termenv color profile.
@@ -78,7 +84,9 @@ func ColorProfile() termenv.Profile {
7884
//
7985
// This function is thread-safe.
8086
func (r *Renderer) SetColorProfile(p termenv.Profile) {
81-
r.output.Profile = p
87+
r.mtx.Lock()
88+
defer r.mtx.Unlock()
89+
r.output.SetColorProfile(p)
8290
}
8391

8492
// SetColorProfile sets the color profile on the default renderer. This
@@ -110,6 +118,8 @@ func HasDarkBackground() bool {
110118
// background. A dark background can either be auto-detected, or set explicitly
111119
// on the renderer.
112120
func (r *Renderer) HasDarkBackground() bool {
121+
r.mtx.RLock()
122+
defer r.mtx.RUnlock()
113123
if r.hasDarkBackground != nil {
114124
return *r.hasDarkBackground
115125
}
@@ -139,5 +149,7 @@ func SetHasDarkBackground(b bool) {
139149
//
140150
// This function is thread-safe.
141151
func (r *Renderer) SetHasDarkBackground(b bool) {
152+
r.mtx.Lock()
153+
defer r.mtx.Unlock()
142154
r.hasDarkBackground = &b
143155
}

renderer_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lipgloss
22

33
import (
4+
"io"
45
"os"
56
"testing"
67

@@ -29,7 +30,24 @@ func TestRendererWithOutput(t *testing.T) {
2930
defer os.Remove(f.Name())
3031
r := NewRenderer(f)
3132
r.SetColorProfile(termenv.TrueColor)
32-
if r.output.Profile != termenv.TrueColor {
33+
if r.output.ColorProfile() != termenv.TrueColor {
3334
t.Error("Expected renderer to use true color")
3435
}
3536
}
37+
38+
func TestRace(t *testing.T) {
39+
r := NewRenderer(io.Discard)
40+
o := r.Output()
41+
42+
for i := 0; i < 100; i++ {
43+
t.Run("SetColorProfile", func(t *testing.T) {
44+
t.Parallel()
45+
r.SetHasDarkBackground(false)
46+
r.HasDarkBackground()
47+
r.SetOutput(o)
48+
r.SetColorProfile(termenv.ANSI256)
49+
r.SetHasDarkBackground(true)
50+
r.Output()
51+
})
52+
}
53+
}

style.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,10 @@ func (s Style) Render(strs ...string) string {
182182
var (
183183
str = joinString(strs...)
184184

185-
te = s.r.ColorProfile().String()
186-
teSpace = s.r.ColorProfile().String()
187-
teWhitespace = s.r.ColorProfile().String()
185+
p = s.r.ColorProfile()
186+
te = p.String()
187+
teSpace = p.String()
188+
teWhitespace = p.String()
188189

189190
bold = s.getAsBool(boldKey, false)
190191
italic = s.getAsBool(italicKey, false)

style_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,41 @@ import (
99
)
1010

1111
func TestStyleRender(t *testing.T) {
12-
renderer.SetColorProfile(termenv.TrueColor)
13-
renderer.SetHasDarkBackground(true)
12+
r := NewRenderer(ioutil.Discard)
13+
r.SetColorProfile(termenv.TrueColor)
14+
r.SetHasDarkBackground(true)
1415
t.Parallel()
1516

1617
tt := []struct {
1718
style Style
1819
expected string
1920
}{
2021
{
21-
NewStyle().Foreground(Color("#5A56E0")),
22+
r.NewStyle().Foreground(Color("#5A56E0")),
2223
"\x1b[38;2;89;86;224mhello\x1b[0m",
2324
},
2425
{
25-
NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
26+
r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
2627
"\x1b[38;2;89;86;224mhello\x1b[0m",
2728
},
2829
{
29-
NewStyle().Bold(true),
30+
r.NewStyle().Bold(true),
3031
"\x1b[1mhello\x1b[0m",
3132
},
3233
{
33-
NewStyle().Italic(true),
34+
r.NewStyle().Italic(true),
3435
"\x1b[3mhello\x1b[0m",
3536
},
3637
{
37-
NewStyle().Underline(true),
38+
r.NewStyle().Underline(true),
3839
"\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",
3940
},
4041
{
41-
NewStyle().Blink(true),
42+
r.NewStyle().Blink(true),
4243
"\x1b[5mhello\x1b[0m",
4344
},
4445
{
45-
NewStyle().Faint(true),
46+
r.NewStyle().Faint(true),
4647
"\x1b[2mhello\x1b[0m",
4748
},
4849
}

0 commit comments

Comments
 (0)