diff --git a/NOTICE b/NOTICE index 922dd1fd79a..dd1e4c0c1b6 100644 --- a/NOTICE +++ b/NOTICE @@ -155,6 +155,10 @@ mattn/go-isatty - https://github.com/mattn/go-isatty Copyright (c) Yasuhiro MATSUMOTO License - https://github.com/mattn/go-isatty/blob/master/LICENSE +muesli/termenv - https://github.com/muesli/termenv +Copyright (c) 2019 Christian Muehlhaeuser +License - https://github.com/muesli/termenv/blob/master/LICENSE + sabhiram/go-gitignore - https://github.com/sabhiram/go-gitignore Copyright (c) 2015 Shaba Abhiram License - https://github.com/sabhiram/go-gitignore/blob/master/LICENSE diff --git a/go.mod b/go.mod index 91f788b62f5..a6e3179d04e 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/hexops/gotextdiff v1.0.3 // BSD-3-Clause github.com/jackc/pgx/v5 v5.10.0 // MIT github.com/mattn/go-isatty v0.0.22 // MIT + github.com/muesli/termenv v0.16.0 // MIT github.com/palantir/pkg/yamlpatch v1.5.0 // BSD-3-Clause github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // BSD-2-Clause github.com/quasilyte/go-ruleguard/dsl v0.3.22 // BSD-3-Clause @@ -85,7 +86,6 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect diff --git a/libs/cmdio/color.go b/libs/cmdio/color.go index 60fb3a15fa0..4549b31feff 100644 --- a/libs/cmdio/color.go +++ b/libs/cmdio/color.go @@ -3,10 +3,12 @@ package cmdio import ( "context" "fmt" + "io" "strings" "text/template" "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" ) // SGR (Select Graphic Rendition) escapes; see @@ -138,3 +140,17 @@ func PadLeft(s string, n int) string { } return s } + +// NewRenderer returns a lipgloss renderer targeting w, along with whether w +// supports color. When it does not (NO_COLOR, TERM=dumb, or w is piped or +// redirected) the renderer is forced to the Ascii profile so every Style it +// mints emits no SGR escapes. Callers still write rendered strings to w +// themselves; the renderer only carries the color profile. +func NewRenderer(ctx context.Context, w io.Writer) (*lipgloss.Renderer, bool) { + color := SupportsColor(ctx, w) + r := lipgloss.NewRenderer(w) + if !color { + r.SetColorProfile(termenv.Ascii) + } + return r, color +} diff --git a/libs/cmdio/color_test.go b/libs/cmdio/color_test.go index 9e51dd98bb9..404f0c12cdb 100644 --- a/libs/cmdio/color_test.go +++ b/libs/cmdio/color_test.go @@ -1,10 +1,12 @@ package cmdio_test import ( + "bytes" "context" "testing" "github.com/databricks/cli/libs/cmdio" + "github.com/muesli/termenv" "github.com/stretchr/testify/assert" ) @@ -126,6 +128,17 @@ func TestPadLeft(t *testing.T) { } } +func TestNewRendererForcesAsciiWhenNoColor(t *testing.T) { + ctx := noColorContext(t) + + // A bytes.Buffer is never a TTY, so the renderer must fall back to Ascii and + // its styles must render without SGR escapes. + r, color := cmdio.NewRenderer(ctx, &bytes.Buffer{}) + assert.False(t, color) + assert.Equal(t, termenv.Ascii, r.ColorProfile()) + assert.Equal(t, "hi", r.NewStyle().Bold(true).Render("hi")) +} + func TestRenderFuncMap(t *testing.T) { ctx := ttyContext(t) fm := cmdio.RenderFuncMap(ctx)