Skip to content

Commit

Permalink
fix: output race condition
Browse files Browse the repository at this point in the history
Make output profile accessible through `ColorProfile`.
Use `SetColorProfile` to change the output color profile.
Use a mutex to guard output writes.

Fixes: charmbracelet/lipgloss#210
Fixes: #145
  • Loading branch information
aymanbagabas committed Jul 28, 2023
1 parent 3466887 commit 007eb06
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 30 deletions.
4 changes: 2 additions & 2 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

// Copy copies text to clipboard using OSC 52 escape sequence.
func (o Output) Copy(str string) {
func (o *Output) Copy(str string) {
s := osc52.New(str)
if strings.HasPrefix(o.environ.Getenv("TERM"), "screen") {
s = s.Screen()
Expand All @@ -17,7 +17,7 @@ func (o Output) Copy(str string) {

// CopyPrimary copies text to primary clipboard (X11) using OSC 52 escape
// sequence.
func (o Output) CopyPrimary(str string) {
func (o *Output) CopyPrimary(str string) {
s := osc52.New(str).Primary()
if strings.HasPrefix(o.environ.Getenv("TERM"), "screen") {
s = s.Screen()
Expand Down
35 changes: 27 additions & 8 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type OutputOption = func(*Output)

// Output is a terminal output.
type Output struct {
Profile
profile Profile
tty io.Writer
environ Environ

Expand All @@ -33,6 +33,8 @@ type Output struct {
fgColor Color
bgSync *sync.Once
bgColor Color

mtx sync.Mutex
}

// Environ is an interface for getting environment variables.
Expand Down Expand Up @@ -66,7 +68,7 @@ func NewOutput(tty io.Writer, opts ...OutputOption) *Output {
o := &Output{
tty: tty,
environ: &osEnviron{},
Profile: -1,
profile: -1,
fgSync: &sync.Once{},
fgColor: NoColor{},
bgSync: &sync.Once{},
Expand All @@ -79,8 +81,8 @@ func NewOutput(tty io.Writer, opts ...OutputOption) *Output {
for _, opt := range opts {
opt(o)
}
if o.Profile < 0 {
o.Profile = o.EnvColorProfile()
if o.profile < 0 {
o.profile = o.EnvColorProfile()
}

return o
Expand All @@ -96,7 +98,7 @@ func WithEnvironment(environ Environ) OutputOption {
// WithProfile returns a new OutputOption for the given profile.
func WithProfile(profile Profile) OutputOption {
return func(o *Output) {
o.Profile = profile
o.profile = profile
}
}

Expand Down Expand Up @@ -133,13 +135,28 @@ func WithUnsafe() OutputOption {
}
}

// ColorProfile returns the color profile.
// Ascii, ANSI, ANSI256, or TrueColor.
func (o *Output) ColorProfile() Profile {
return o.colorProfile()
}

// SetColorProfile sets the color profile.
func (o *Output) SetColorProfile(profile Profile) {
o.mtx.Lock()
defer o.mtx.Unlock()
o.profile = profile
}

// ForegroundColor returns the terminal's default foreground color.
func (o *Output) ForegroundColor() Color {
f := func() {
if !o.isTTY() {
return
}

o.mtx.Lock()
defer o.mtx.Unlock()
o.fgColor = o.foregroundColor()
}

Expand All @@ -159,6 +176,8 @@ func (o *Output) BackgroundColor() Color {
return
}

o.mtx.Lock()
defer o.mtx.Unlock()
o.bgColor = o.backgroundColor()
}

Expand All @@ -180,18 +199,18 @@ func (o *Output) HasDarkBackground() bool {

// TTY returns the terminal's file descriptor. This may be nil if the output is
// not a terminal.
func (o Output) TTY() File {
func (o *Output) TTY() File {
if f, ok := o.tty.(File); ok {
return f
}
return nil
}

func (o Output) Write(p []byte) (int, error) {
func (o *Output) Write(p []byte) (int, error) {
return o.tty.Write(p)
}

// WriteString writes the given string to the output.
func (o Output) WriteString(s string) (int, error) {
func (o *Output) WriteString(s string) (int, error) {
return o.Write([]byte(s))
}
22 changes: 22 additions & 0 deletions output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package termenv

import (
"io"
"testing"
)

func TestOutputRace(t *testing.T) {
o := NewOutput(io.Discard)
for i := 0; i < 100; i++ {
t.Run("Test race", func(t *testing.T) {
t.Parallel()
o.Write([]byte("test"))
o.SetColorProfile(ANSI)
o.ColorProfile()
o.HasDarkBackground()
o.TTY()
o.ForegroundColor()
o.BackgroundColor()
})
}
}
2 changes: 1 addition & 1 deletion templatehelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

// TemplateFuncs returns template helpers for the given output.
func (o Output) TemplateFuncs() template.FuncMap {
return TemplateFuncs(o.Profile)
return TemplateFuncs(o.colorProfile())
}

// TemplateFuncs contains a few useful template helpers.
Expand Down
7 changes: 6 additions & 1 deletion termenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func ColorProfile() Profile {
return output.ColorProfile()
}

// SetColorProfile sets the color profile.
func SetColorProfile(profile Profile) {
output.SetColorProfile(profile)
}

// ForegroundColor returns the terminal's default foreground color.
func ForegroundColor() Color {
return output.ForegroundColor()
Expand Down Expand Up @@ -99,7 +104,7 @@ func (o *Output) EnvColorProfile() Profile {
if o.EnvNoColor() {
return Ascii
}
p := o.ColorProfile()
p := o.colorProfile()
if o.cliColorForced() && p == Ascii {
return ANSI
}
Expand Down
8 changes: 4 additions & 4 deletions termenv_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ package termenv

import "io"

// ColorProfile returns the supported color profile:
// colorProfile returns the supported color profile:
// ANSI256
func (o Output) ColorProfile() Profile {
func (o *Output) colorProfile() Profile {
return ANSI256
}

func (o Output) foregroundColor() Color {
func (o *Output) foregroundColor() Color {
// default gray
return ANSIColor(7)
}

func (o Output) backgroundColor() Color {
func (o *Output) backgroundColor() Color {
// default black
return ANSIColor(0)
}
Expand Down
14 changes: 8 additions & 6 deletions termenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ func TestLegacyTermEnv(t *testing.T) {

func TestTermEnv(t *testing.T) {
o := NewOutput(os.Stdout)
if o.Profile != TrueColor && o.Profile != Ascii {
t.Errorf("Expected %d, got %d", TrueColor, o.Profile)
p := o.colorProfile()
if p != TrueColor && p != Ascii {
t.Errorf("Expected %d, got %d", TrueColor, p)
}

fg := o.ForegroundColor()
Expand Down Expand Up @@ -360,8 +361,9 @@ func TestEnvNoColor(t *testing.T) {
func TestPseudoTerm(t *testing.T) {
buf := &bytes.Buffer{}
o := NewOutput(buf)
if o.Profile != Ascii {
t.Errorf("Expected %d, got %d", Ascii, o.Profile)
p := o.colorProfile()
if p != Ascii {
t.Errorf("Expected %d, got %d", Ascii, p)
}

fg := o.ForegroundColor()
Expand All @@ -377,8 +379,8 @@ func TestPseudoTerm(t *testing.T) {
}

exp := "foobar"
out := o.String(exp)
out = out.Foreground(o.Color("#abcdef"))
out := p.String(exp)
out = out.Foreground(p.Color("#abcdef"))
o.Write([]byte(out.String()))

if buf.String() != exp {
Expand Down
10 changes: 5 additions & 5 deletions termenv_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const (
OSCTimeout = 5 * time.Second
)

// ColorProfile returns the supported color profile:
// colorProfile returns the supported color profile:
// Ascii, ANSI, ANSI256, or TrueColor.
func (o *Output) ColorProfile() Profile {
func (o *Output) colorProfile() Profile {
if !o.isTTY() {
return Ascii
}
Expand Down Expand Up @@ -69,7 +69,7 @@ func (o *Output) ColorProfile() Profile {
return Ascii
}

func (o Output) foregroundColor() Color {
func (o *Output) foregroundColor() Color {
s, err := o.termStatusReport(10)
if err == nil {
c, err := xTermColor(s)
Expand All @@ -91,7 +91,7 @@ func (o Output) foregroundColor() Color {
return ANSIColor(7)
}

func (o Output) backgroundColor() Color {
func (o *Output) backgroundColor() Color {
s, err := o.termStatusReport(11)
if err == nil {
c, err := xTermColor(s)
Expand Down Expand Up @@ -223,7 +223,7 @@ func (o *Output) readNextResponse() (response string, isOSC bool, err error) {
return "", false, ErrStatusReport
}

func (o Output) termStatusReport(sequence int) (string, error) {
func (o *Output) termStatusReport(sequence int) (string, error) {
// screen/tmux can't support OSC, because they can be connected to multiple
// terminals concurrently.
term := o.environ.Getenv("TERM")
Expand Down
6 changes: 3 additions & 3 deletions termenv_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"golang.org/x/sys/windows"
)

func (o *Output) ColorProfile() Profile {
func (o *Output) colorProfile() Profile {
if !o.isTTY() {
return Ascii
}
Expand Down Expand Up @@ -43,12 +43,12 @@ func (o *Output) ColorProfile() Profile {
return TrueColor
}

func (o Output) foregroundColor() Color {
func (o *Output) foregroundColor() Color {
// default gray
return ANSIColor(7)
}

func (o Output) backgroundColor() Color {
func (o *Output) backgroundColor() Color {
// default black
return ANSIColor(0)
}
Expand Down

0 comments on commit 007eb06

Please sign in to comment.