diff --git a/align.go b/align.go index 4aec3717..6539001b 100644 --- a/align.go +++ b/align.go @@ -3,14 +3,22 @@ package lipgloss import ( "strings" +<<<<<<< HEAD "github.com/charmbracelet/x/exp/term/ansi" +======= +<<<<<<< HEAD + "github.com/muesli/reflow/ansi" +>>>>>>> dd49ed05492e (feat!: introduce color profiles and use term/ansi for styles) "github.com/muesli/termenv" +======= + "github.com/charmbracelet/x/exp/term/ansi" +>>>>>>> d3d126dc69c7 (feat!: introduce color profiles and use term/ansi for styles) ) // Perform text alignment. If the string is multi-lined, we also make all lines -// the same width by padding them with spaces. If a termenv style is passed, -// use that to style the spaces added. -func alignTextHorizontal(str string, pos Position, width int, style *termenv.Style) string { +// the same width by padding them with spaces. If a style is passed, use that +// to style the spaces added. +func alignTextHorizontal(str string, pos Position, width int, style *style) string { lines, widestLine := getLines(str) var b strings.Builder @@ -59,7 +67,7 @@ func alignTextHorizontal(str string, pos Position, width int, style *termenv.Sty return b.String() } -func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) string { +func alignTextVertical(str string, pos Position, height int, _ *style) string { strHeight := strings.Count(str, "\n") + 1 if height < strHeight { return str diff --git a/ansi_windows.go b/ansi_windows.go index 0cf56e4c..332c0c51 100644 --- a/ansi_windows.go +++ b/ansi_windows.go @@ -6,7 +6,7 @@ package lipgloss import ( "sync" - "github.com/muesli/termenv" + "golang.org/x/sys/windows" ) var enableANSI sync.Once @@ -17,6 +17,23 @@ var enableANSI sync.Once // by default. func enableLegacyWindowsANSI() { enableANSI.Do(func() { - _, _ = termenv.EnableWindowsANSIConsole() + handle, err := windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) + if err != nil { + return + } + + var mode uint32 + err = windows.GetConsoleMode(handle, &mode) + if err != nil { + return + } + + // See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING { + vtpmode := mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + if err := windows.SetConsoleMode(handle, vtpmode); err != nil { + return + } + } }) } diff --git a/borders.go b/borders.go index 38f875f1..289eb80f 100644 --- a/borders.go +++ b/borders.go @@ -3,8 +3,16 @@ package lipgloss import ( "strings" +<<<<<<< HEAD "github.com/charmbracelet/x/exp/term/ansi" +======= +<<<<<<< HEAD + "github.com/muesli/reflow/ansi" +>>>>>>> dd49ed05492e (feat!: introduce color profiles and use term/ansi for styles) "github.com/muesli/termenv" +======= + "github.com/charmbracelet/x/exp/term/ansi" +>>>>>>> d3d126dc69c7 (feat!: introduce color profiles and use term/ansi for styles) "github.com/rivo/uniseg" ) @@ -407,13 +415,13 @@ func (s Style) styleBorder(border string, fg, bg TerminalColor) string { return border } - style := termenv.Style{} + style := s.r.ColorProfile().string() if fg != noColor { - style = style.Foreground(fg.color(s.r)) + style = style.ForegroundColor(fg.color(s.r)) } if bg != noColor { - style = style.Background(bg.color(s.r)) + style = style.BackgroundColor(bg.color(s.r)) } return style.Styled(border) diff --git a/color.go b/color.go index 43f5b434..a9f18a70 100644 --- a/color.go +++ b/color.go @@ -3,12 +3,13 @@ package lipgloss import ( "strconv" - "github.com/muesli/termenv" + "github.com/charmbracelet/x/exp/term/ansi" + "github.com/lucasb-eyer/go-colorful" ) // TerminalColor is a color intended to be rendered in the terminal. type TerminalColor interface { - color(*Renderer) termenv.Color + color(*Renderer) ansi.Color RGBA() (r, g, b, a uint32) } @@ -23,8 +24,8 @@ var noColor = NoColor{} // var style = someStyle.Copy().Background(lipgloss.NoColor{}) type NoColor struct{} -func (NoColor) color(*Renderer) termenv.Color { - return termenv.NoColor{} +func (NoColor) color(*Renderer) ansi.Color { + return nil } // RGBA returns the RGBA value of this color. Because we have to return @@ -44,8 +45,8 @@ func (n NoColor) RGBA() (r, g, b, a uint32) { // hexColor := lipgloss.Color("#0000ff") type Color string -func (c Color) color(r *Renderer) termenv.Color { - return r.ColorProfile().Color(string(c)) +func (c Color) color(r *Renderer) ansi.Color { + return r.ColorProfile().color(string(c)) } // RGBA returns the RGBA value of this color. This satisfies the Go Color @@ -55,7 +56,7 @@ func (c Color) color(r *Renderer) termenv.Color { // // Deprecated. func (c Color) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(c.color(renderer)).RGBA() + return c.color(DefaultRenderer()).RGBA() } // ANSIColor is a color specified by an ANSI color value. It's merely syntactic @@ -69,7 +70,7 @@ func (c Color) RGBA() (r, g, b, a uint32) { // colorB := lipgloss.Color("21") type ANSIColor uint -func (ac ANSIColor) color(r *Renderer) termenv.Color { +func (ac ANSIColor) color(r *Renderer) ansi.Color { return Color(strconv.FormatUint(uint64(ac), 10)).color(r) } @@ -96,7 +97,7 @@ type AdaptiveColor struct { Dark string } -func (ac AdaptiveColor) color(r *Renderer) termenv.Color { +func (ac AdaptiveColor) color(r *Renderer) ansi.Color { if r.HasDarkBackground() { return Color(ac.Dark).color(r) } @@ -110,7 +111,7 @@ func (ac AdaptiveColor) color(r *Renderer) termenv.Color { // // Deprecated. func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(ac.color(renderer)).RGBA() + return ac.color(DefaultRenderer()).RGBA() } // CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color @@ -121,17 +122,17 @@ type CompleteColor struct { ANSI string } -func (c CompleteColor) color(r *Renderer) termenv.Color { +func (c CompleteColor) color(r *Renderer) ansi.Color { p := r.ColorProfile() switch p { //nolint:exhaustive - case termenv.TrueColor: - return p.Color(c.TrueColor) - case termenv.ANSI256: - return p.Color(c.ANSI256) - case termenv.ANSI: - return p.Color(c.ANSI) + case TrueColor: + return p.color(c.TrueColor) + case ANSI256: + return p.color(c.ANSI256) + case ANSI: + return p.color(c.ANSI) default: - return termenv.NoColor{} + return NoColor{} } } @@ -143,7 +144,7 @@ func (c CompleteColor) color(r *Renderer) termenv.Color { // // Deprecated. func (c CompleteColor) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(c.color(renderer)).RGBA() + return c.color(DefaultRenderer()).RGBA() } // CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color @@ -154,7 +155,7 @@ type CompleteAdaptiveColor struct { Dark CompleteColor } -func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color { +func (cac CompleteAdaptiveColor) color(r *Renderer) ansi.Color { if r.HasDarkBackground() { return cac.Dark.color(r) } @@ -168,5 +169,11 @@ func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color { // // Deprecated. func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(cac.color(renderer)).RGBA() + return cac.color(DefaultRenderer()).RGBA() +} + +// ConvertToRGB converts a Color to a colorful.Color. +func ConvertToRGB(c ansi.Color) colorful.Color { + ch, _ := colorful.MakeColor(c) + return ch } diff --git a/color_test.go b/color_test.go index 0881076c..1d4b17c8 100644 --- a/color_test.go +++ b/color_test.go @@ -3,38 +3,36 @@ package lipgloss import ( "image/color" "testing" - - "github.com/muesli/termenv" ) func TestSetColorProfile(t *testing.T) { - r := renderer + r := DefaultRenderer() input := "hello" tt := []struct { name string - profile termenv.Profile + profile Profile expected string }{ { "ascii", - termenv.Ascii, + Ascii, "hello", }, { "ansi", - termenv.ANSI, - "\x1b[94mhello\x1b[0m", + ANSI, + "\x1b[94mhello\x1b[m", }, { "ansi256", - termenv.ANSI256, - "\x1b[38;5;62mhello\x1b[0m", + ANSI256, + "\x1b[38;5;62mhello\x1b[m", }, { "truecolor", - termenv.TrueColor, - "\x1b[38;2;89;86;224mhello\x1b[0m", + TrueColor, + "\x1b[38;2;89;86;224mhello\x1b[m", }, } @@ -89,76 +87,76 @@ func TestHexToColor(t *testing.T) { func TestRGBA(t *testing.T) { tt := []struct { - profile termenv.Profile + profile Profile darkBg bool input TerminalColor expected uint }{ // lipgloss.Color { - termenv.TrueColor, + TrueColor, true, Color("#FF0000"), 0xFF0000, }, { - termenv.TrueColor, + TrueColor, true, Color("9"), 0xFF0000, }, { - termenv.TrueColor, + TrueColor, true, Color("21"), 0x0000FF, }, // lipgloss.AdaptiveColor { - termenv.TrueColor, + TrueColor, true, AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"}, 0xFF0000, }, { - termenv.TrueColor, + TrueColor, false, AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"}, 0x0000FF, }, { - termenv.TrueColor, + TrueColor, true, AdaptiveColor{Light: "21", Dark: "9"}, 0xFF0000, }, { - termenv.TrueColor, + TrueColor, false, AdaptiveColor{Light: "21", Dark: "9"}, 0x0000FF, }, // lipgloss.CompleteColor { - termenv.TrueColor, + TrueColor, true, CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, 0xFF0000, }, { - termenv.ANSI256, + ANSI256, true, CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, 0xFFFFFF, }, { - termenv.ANSI, + ANSI, true, CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, 0x0000FF, }, { - termenv.TrueColor, + TrueColor, true, CompleteColor{TrueColor: "", ANSI256: "231", ANSI: "12"}, 0x000000, @@ -166,7 +164,7 @@ func TestRGBA(t *testing.T) { // lipgloss.CompleteAdaptiveColor // dark { - termenv.TrueColor, + TrueColor, true, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"}, @@ -175,7 +173,7 @@ func TestRGBA(t *testing.T) { 0xFF0000, }, { - termenv.ANSI256, + ANSI256, true, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"}, @@ -184,7 +182,7 @@ func TestRGBA(t *testing.T) { 0xFFFFFF, }, { - termenv.ANSI, + ANSI, true, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"}, @@ -194,7 +192,7 @@ func TestRGBA(t *testing.T) { }, // light { - termenv.TrueColor, + TrueColor, false, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"}, @@ -203,7 +201,7 @@ func TestRGBA(t *testing.T) { 0x0000FF, }, { - termenv.ANSI256, + ANSI256, false, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"}, @@ -212,7 +210,7 @@ func TestRGBA(t *testing.T) { 0x0000FF, }, { - termenv.ANSI, + ANSI, false, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"}, diff --git a/env.go b/env.go new file mode 100644 index 00000000..387a1201 --- /dev/null +++ b/env.go @@ -0,0 +1,120 @@ +package lipgloss + +import ( + "strconv" + "strings" +) + +// envNoColor returns true if the environment variables explicitly disable color output +// by setting NO_COLOR (https://no-color.org/) +// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) +// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE +// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset. +func (o *Renderer) envNoColor() bool { + return o.environ["NO_COLOR"] != "" || (o.environ["CLICOLOR"] == "0" && !o.cliColorForced()) +} + +// envColorProfile returns the color profile based on environment variables set +// Supports NO_COLOR (https://no-color.org/) +// and CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) +// If none of these environment variables are set, this behaves the same as ColorProfile() +// It will return the Ascii color profile if EnvNoColor() returns true +// If the terminal does not support any colors, but CLICOLOR_FORCE is set and not "0" +// then the ANSI color profile will be returned. +func (o *Renderer) envColorProfile() Profile { + if o.envNoColor() { + return Ascii + } + p := o.detectColorProfile() + if o.cliColorForced() && p == Ascii { + return ANSI + } + return p +} + +func (o *Renderer) cliColorForced() bool { + if forced := o.environ["CLICOLOR_FORCE"]; forced != "" { + return !isTrue(forced) + } + return false +} + +// detectColorProfile returns the supported color profile: +// Ascii, ANSI, ANSI256, or TrueColor. +func (o *Renderer) detectColorProfile() (p Profile) { + if !o.isatty { + return Ascii + } + + setProfile := func(profile Profile) { + if profile > p { + p = profile + } + } + + if isTrue(o.environ["GOOGLE_CLOUD_SHELL"]) { + setProfile(TrueColor) + } + + term := o.environ["TERM"] + colorTerm := o.environ["COLORTERM"] + + switch strings.ToLower(colorTerm) { + case "24bit": + fallthrough + case "truecolor": + if strings.HasPrefix(term, "screen") { + // tmux supports TrueColor, screen only ANSI256 + if o.environ["TERM_PROGRAM"] != "tmux" { + setProfile(ANSI256) + } + } + setProfile(TrueColor) + case "yes": + fallthrough + case "true": + setProfile(TrueColor) + } + + switch term { + case "xterm-kitty", "wezterm", "xterm-ghostty": + setProfile(TrueColor) + case "linux": + setProfile(ANSI) + } + + if strings.Contains(term, "256color") { + setProfile(ANSI256) + } + if strings.Contains(term, "color") { + setProfile(ANSI) + } + if strings.Contains(term, "ansi") { + setProfile(ANSI) + } + + return +} + +// isTrue returns true if the string is a truthy value. +func isTrue(s string) bool { + if s == "" { + return false + } + v, _ := strconv.ParseBool(strings.ToLower(s)) + return v +} + +// environMap converts an environment slice to a map. +func environMap(environ []string) map[string]string { + m := make(map[string]string, len(environ)) + for _, e := range environ { + parts := strings.SplitN(e, "=", 2) + var value string + if len(parts) == 2 { + value = parts[1] + } + m[parts[0]] = value + } + return m +} diff --git a/examples/go.mod b/examples/go.mod index 1eb982f9..0085cc81 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -20,10 +20,13 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/caarlos0/sshmarshal v0.1.0 // indirect github.com/charmbracelet/keygen v0.3.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/mattn/go-isatty v0.0.18 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/crypto v0.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect golang.org/x/sys v0.18.0 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 0e38eb5a..c8a9995b 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -24,6 +24,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -58,12 +59,12 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9 github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= @@ -85,6 +86,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -93,9 +95,9 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -103,7 +105,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -125,7 +126,6 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= @@ -139,7 +139,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/go.mod b/go.mod index 5b4599ef..04011110 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,13 @@ go 1.18 require ( github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd - github.com/muesli/termenv v0.15.2 - github.com/rivo/uniseg v0.4.7 + github.com/lucasb-eyer/go-colorful v1.2.0 + golang.org/x/sys v0.18.0 ) require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - golang.org/x/sys v0.18.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) diff --git a/go.sum b/go.sum index a313ebc7..998cd191 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,16 @@ -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U= github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/position.go b/position.go index f57b7bb9..d872b286 100644 --- a/position.go +++ b/position.go @@ -34,7 +34,7 @@ const ( // Place places a string or text block vertically in an unstyled box of a given // width or height. func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { - return renderer.Place(width, height, hPos, vPos, str, opts...) + return DefaultRenderer().Place(width, height, hPos, vPos, str, opts...) } // Place places a string or text block vertically in an unstyled box of a given @@ -47,7 +47,7 @@ func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opt // block of a given width. If the given width is shorter than the max width of // the string (measured by its longest line) this will be a noop. func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { - return renderer.PlaceHorizontal(width, pos, str, opts...) + return DefaultRenderer().PlaceHorizontal(width, pos, str, opts...) } // PlaceHorizontal places a string or text block horizontally in an unstyled @@ -101,7 +101,7 @@ func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ... // of a given height. If the given height is shorter than the height of the // string (measured by its newlines) then this will be a noop. func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { - return renderer.PlaceVertical(height, pos, str, opts...) + return DefaultRenderer().PlaceVertical(height, pos, str, opts...) } // PlaceVertical places a string or text block vertically in an unstyled block diff --git a/profile.go b/profile.go new file mode 100644 index 00000000..43880a4d --- /dev/null +++ b/profile.go @@ -0,0 +1,417 @@ +package lipgloss + +import ( + "image/color" + "math" + "strconv" + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" + "github.com/lucasb-eyer/go-colorful" +) + +// Profile is a color profile: Ascii, ANSI, ANSI256, or TrueColor. +type Profile int + +const ( + // Ascii, uncolored profile + Ascii Profile = iota //nolint:revive + // ANSI, 4-bit color profile + ANSI + // ANSI256, 8-bit color profile + ANSI256 + // TrueColor, 24-bit color profile + TrueColor +) + +func (p Profile) string() style { + return style{Profile: p} +} + +// convert transforms a given Color to a Color supported within the Profile. +func (p Profile) convert(c ansi.Color) ansi.Color { + if p == Ascii { + return NoColor{} + } + + switch v := c.(type) { + case ansi.BasicColor: + return v + + case ansi.ExtendedColor: + if p == ANSI { + return ansi256ToANSIColor(v) + } + return v + + case ansi.TrueColor, color.Color: + h, ok := colorful.MakeColor(v) + if !ok { + return nil + } + if p != TrueColor { + ac := hexToANSI256Color(h) + if p == ANSI { + return ansi256ToANSIColor(ac) + } + return ac + } + return v + } + + return c +} + +// color creates a color from a string. Valid inputs are hex colors, as well as +// ANSI color codes (0-15, 16-255). +func (p Profile) color(s string) ansi.Color { + if len(s) == 0 { + return color.Black + } + + var c ansi.Color + if strings.HasPrefix(s, "#") { + h, err := colorful.Hex(s) + if err != nil { + return nil + } + tc := uint32(h.R*255)<<16 + uint32(h.G*255)<<8 + uint32(h.B*255) + c = ansi.TrueColor(tc) + } else { + i, err := strconv.Atoi(s) + if err != nil { + return nil + } + + if i < 16 { + c = ansi.BasicColor(i) + } else { + c = ansi.ExtendedColor(i) + } + } + + return p.convert(c) +} + +func hexToANSI256Color(c colorful.Color) ansi.ExtendedColor { + v2ci := func(v float64) int { + if v < 48 { + return 0 + } + if v < 115 { + return 1 + } + return int((v - 35) / 40) + } + + // Calculate the nearest 0-based color index at 16..231 + r := v2ci(c.R * 255.0) // 0..5 each + g := v2ci(c.G * 255.0) + b := v2ci(c.B * 255.0) + ci := 36*r + 6*g + b /* 0..215 */ + + // Calculate the represented colors back from the index + i2cv := [6]int{0, 0x5f, 0x87, 0xaf, 0xd7, 0xff} + cr := i2cv[r] // r/g/b, 0..255 each + cg := i2cv[g] + cb := i2cv[b] + + // Calculate the nearest 0-based gray index at 232..255 + var grayIdx int + average := (r + g + b) / 3 + if average > 238 { + grayIdx = 23 + } else { + grayIdx = (average - 3) / 10 // 0..23 + } + gv := 8 + 10*grayIdx // same value for r/g/b, 0..255 + + // Return the one which is nearer to the original input rgb value + c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0} + g2 := colorful.Color{R: float64(gv) / 255.0, G: float64(gv) / 255.0, B: float64(gv) / 255.0} + colorDist := c.DistanceHSLuv(c2) + grayDist := c.DistanceHSLuv(g2) + + if colorDist <= grayDist { + return ansi.ExtendedColor(16 + ci) + } + return ansi.ExtendedColor(232 + grayIdx) +} + +func ansi256ToANSIColor(c ansi.ExtendedColor) ansi.BasicColor { + var r int + md := math.MaxFloat64 + + h, _ := colorful.Hex(ansiHex[c]) + for i := 0; i <= 15; i++ { + hb, _ := colorful.Hex(ansiHex[i]) + d := h.DistanceHSLuv(hb) + + if d < md { + md = d + r = i + } + } + + return ansi.BasicColor(r) +} + +// RGB values of ANSI colors (0-255). +var ansiHex = []string{ + "#000000", + "#800000", + "#008000", + "#808000", + "#000080", + "#800080", + "#008080", + "#c0c0c0", + "#808080", + "#ff0000", + "#00ff00", + "#ffff00", + "#0000ff", + "#ff00ff", + "#00ffff", + "#ffffff", + "#000000", + "#00005f", + "#000087", + "#0000af", + "#0000d7", + "#0000ff", + "#005f00", + "#005f5f", + "#005f87", + "#005faf", + "#005fd7", + "#005fff", + "#008700", + "#00875f", + "#008787", + "#0087af", + "#0087d7", + "#0087ff", + "#00af00", + "#00af5f", + "#00af87", + "#00afaf", + "#00afd7", + "#00afff", + "#00d700", + "#00d75f", + "#00d787", + "#00d7af", + "#00d7d7", + "#00d7ff", + "#00ff00", + "#00ff5f", + "#00ff87", + "#00ffaf", + "#00ffd7", + "#00ffff", + "#5f0000", + "#5f005f", + "#5f0087", + "#5f00af", + "#5f00d7", + "#5f00ff", + "#5f5f00", + "#5f5f5f", + "#5f5f87", + "#5f5faf", + "#5f5fd7", + "#5f5fff", + "#5f8700", + "#5f875f", + "#5f8787", + "#5f87af", + "#5f87d7", + "#5f87ff", + "#5faf00", + "#5faf5f", + "#5faf87", + "#5fafaf", + "#5fafd7", + "#5fafff", + "#5fd700", + "#5fd75f", + "#5fd787", + "#5fd7af", + "#5fd7d7", + "#5fd7ff", + "#5fff00", + "#5fff5f", + "#5fff87", + "#5fffaf", + "#5fffd7", + "#5fffff", + "#870000", + "#87005f", + "#870087", + "#8700af", + "#8700d7", + "#8700ff", + "#875f00", + "#875f5f", + "#875f87", + "#875faf", + "#875fd7", + "#875fff", + "#878700", + "#87875f", + "#878787", + "#8787af", + "#8787d7", + "#8787ff", + "#87af00", + "#87af5f", + "#87af87", + "#87afaf", + "#87afd7", + "#87afff", + "#87d700", + "#87d75f", + "#87d787", + "#87d7af", + "#87d7d7", + "#87d7ff", + "#87ff00", + "#87ff5f", + "#87ff87", + "#87ffaf", + "#87ffd7", + "#87ffff", + "#af0000", + "#af005f", + "#af0087", + "#af00af", + "#af00d7", + "#af00ff", + "#af5f00", + "#af5f5f", + "#af5f87", + "#af5faf", + "#af5fd7", + "#af5fff", + "#af8700", + "#af875f", + "#af8787", + "#af87af", + "#af87d7", + "#af87ff", + "#afaf00", + "#afaf5f", + "#afaf87", + "#afafaf", + "#afafd7", + "#afafff", + "#afd700", + "#afd75f", + "#afd787", + "#afd7af", + "#afd7d7", + "#afd7ff", + "#afff00", + "#afff5f", + "#afff87", + "#afffaf", + "#afffd7", + "#afffff", + "#d70000", + "#d7005f", + "#d70087", + "#d700af", + "#d700d7", + "#d700ff", + "#d75f00", + "#d75f5f", + "#d75f87", + "#d75faf", + "#d75fd7", + "#d75fff", + "#d78700", + "#d7875f", + "#d78787", + "#d787af", + "#d787d7", + "#d787ff", + "#d7af00", + "#d7af5f", + "#d7af87", + "#d7afaf", + "#d7afd7", + "#d7afff", + "#d7d700", + "#d7d75f", + "#d7d787", + "#d7d7af", + "#d7d7d7", + "#d7d7ff", + "#d7ff00", + "#d7ff5f", + "#d7ff87", + "#d7ffaf", + "#d7ffd7", + "#d7ffff", + "#ff0000", + "#ff005f", + "#ff0087", + "#ff00af", + "#ff00d7", + "#ff00ff", + "#ff5f00", + "#ff5f5f", + "#ff5f87", + "#ff5faf", + "#ff5fd7", + "#ff5fff", + "#ff8700", + "#ff875f", + "#ff8787", + "#ff87af", + "#ff87d7", + "#ff87ff", + "#ffaf00", + "#ffaf5f", + "#ffaf87", + "#ffafaf", + "#ffafd7", + "#ffafff", + "#ffd700", + "#ffd75f", + "#ffd787", + "#ffd7af", + "#ffd7d7", + "#ffd7ff", + "#ffff00", + "#ffff5f", + "#ffff87", + "#ffffaf", + "#ffffd7", + "#ffffff", + "#080808", + "#121212", + "#1c1c1c", + "#262626", + "#303030", + "#3a3a3a", + "#444444", + "#4e4e4e", + "#585858", + "#626262", + "#6c6c6c", + "#767676", + "#808080", + "#8a8a8a", + "#949494", + "#9e9e9e", + "#a8a8a8", + "#b2b2b2", + "#bcbcbc", + "#c6c6c6", + "#d0d0d0", + "#dadada", + "#e4e4e4", + "#eeeeee", +} diff --git a/renderer.go b/renderer.go index 85ffd254..2f8ce9d2 100644 --- a/renderer.go +++ b/renderer.go @@ -1,31 +1,27 @@ package lipgloss import ( - "io" + "os" "sync" - "github.com/muesli/termenv" + "github.com/charmbracelet/x/exp/term" + "github.com/lucasb-eyer/go-colorful" ) // We're manually creating the struct here to avoid initializing the output and // query the terminal multiple times. -var renderer = &Renderer{ - output: termenv.DefaultOutput(), -} +var ( + renderer *Renderer + rendererOnce sync.Once +) // Renderer is a lipgloss terminal renderer. type Renderer struct { - output *termenv.Output - colorProfile termenv.Profile + environ map[string]string + colorProfile Profile + mtx sync.RWMutex hasDarkBackground bool - - getColorProfile sync.Once - explicitColorProfile bool - - getBackgroundColor sync.Once - explicitBackgroundColor bool - - mtx sync.RWMutex + isatty bool } // RendererOption is a function that can be used to configure a [Renderer]. @@ -33,6 +29,32 @@ type RendererOption func(r *Renderer) // DefaultRenderer returns the default renderer. func DefaultRenderer() *Renderer { + rendererOnce.Do(func() { + if renderer != nil { + // Alredy set by SetDefaultRenderer + return + } + hasDarkBackground := true // Assume dark background by default + isatty := term.IsTerminal(os.Stdout.Fd()) + if isatty { + if bg := term.BackgroundColor(os.Stdin, os.Stdout); bg != nil { + c, ok := colorful.MakeColor(bg) + if ok { + _, _, l := c.Hsl() + hasDarkBackground = l < 0.5 + } + } + + // Enable support for ANSI on the legacy Windows cmd.exe console. This is a + // no-op on non-Windows systems and on Windows runs only once. + // When using a custom renderer, this should be called manually. + enableLegacyWindowsANSI() + } + // we already know whether the terminal isatty and we want to use + // os.Environ() by default + renderer = NewRenderer(nil, nil, hasDarkBackground) + renderer.SetIsTerminal(isatty) + }) return renderer } @@ -43,47 +65,63 @@ func SetDefaultRenderer(r *Renderer) { // NewRenderer creates a new Renderer. // -// w will be used to determine the terminal's color capabilities. -func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer { +// The stdout argument is used to detect if the renderer is writing to a +// terminal. If it is nil, the renderer will assume it's not writing to a +// terminal. +// The environ argument is used to detect the color profile based on the +// environment variables. If it's nil, os.Environ() will be used. +// Set hasDarkBackground to true if the terminal has a dark background. +func NewRenderer(stdout *os.File, environ []string, hasDarkBackground bool) *Renderer { r := &Renderer{ - output: termenv.NewOutput(w, opts...), + hasDarkBackground: hasDarkBackground, } + r.isatty = stdout != nil && term.IsTerminal(stdout.Fd()) + if environ == nil { + environ = os.Environ() + } + r.environ = environMap(environ) + r.colorProfile = r.envColorProfile() return r } -// Output returns the termenv output. -func (r *Renderer) Output() *termenv.Output { +// ColorProfile returns the detected color profile. +func (r *Renderer) ColorProfile() Profile { r.mtx.RLock() defer r.mtx.RUnlock() - return r.output + + return r.colorProfile } -// SetOutput sets the termenv output. -func (r *Renderer) SetOutput(o *termenv.Output) { - r.mtx.Lock() - defer r.mtx.Unlock() - r.output = o +// ColorProfile returns the detected color profile. +func ColorProfile() Profile { + return DefaultRenderer().ColorProfile() } -// ColorProfile returns the detected termenv color profile. -func (r *Renderer) ColorProfile() termenv.Profile { +// IsTerminal returns whether or not the renderer is thinking it's writing to a +// terminal. +func (r *Renderer) IsTerminal() bool { r.mtx.RLock() defer r.mtx.RUnlock() - if !r.explicitColorProfile { - r.getColorProfile.Do(func() { - // NOTE: we don't need to lock here because sync.Once provides its - // own locking mechanism. - r.colorProfile = r.output.EnvColorProfile() - }) - } + return r.isatty +} - return r.colorProfile +// IsTerminal returns whether or not the default renderer is thinking it's +// writing to a terminal. +func IsTerminal() bool { + return DefaultRenderer().IsTerminal() } -// ColorProfile returns the detected termenv color profile. -func ColorProfile() termenv.Profile { - return renderer.ColorProfile() +// SetIsTerminal sets whether or not the renderer is writing to a terminal. +func (r *Renderer) SetIsTerminal(b bool) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.isatty = b +} + +// SetIsTerminal sets whether or not the renderer is writing to a terminal. +func SetIsTerminal(b bool) { + DefaultRenderer().SetIsTerminal(b) } // SetColorProfile sets the color profile on the renderer. This function exists @@ -96,18 +134,17 @@ func ColorProfile() termenv.Profile { // // Available color profiles are: // -// termenv.Ascii // no color, 1-bit -// termenv.ANSI //16 colors, 4-bit -// termenv.ANSI256 // 256 colors, 8-bit -// termenv.TrueColor // 16,777,216 colors, 24-bit +// Ascii // no color, 1-bit +// ANSI //16 colors, 4-bit +// ANSI256 // 256 colors, 8-bit +// TrueColor // 16,777,216 colors, 24-bit // // This function is thread-safe. -func (r *Renderer) SetColorProfile(p termenv.Profile) { +func (r *Renderer) SetColorProfile(p Profile) { r.mtx.Lock() defer r.mtx.Unlock() r.colorProfile = p - r.explicitColorProfile = true } // SetColorProfile sets the color profile on the default renderer. This @@ -120,19 +157,19 @@ func (r *Renderer) SetColorProfile(p termenv.Profile) { // // Available color profiles are: // -// termenv.Ascii // no color, 1-bit -// termenv.ANSI //16 colors, 4-bit -// termenv.ANSI256 // 256 colors, 8-bit -// termenv.TrueColor // 16,777,216 colors, 24-bit +// Ascii // no color, 1-bit +// ANSI //16 colors, 4-bit +// ANSI256 // 256 colors, 8-bit +// TrueColor // 16,777,216 colors, 24-bit // // This function is thread-safe. -func SetColorProfile(p termenv.Profile) { - renderer.SetColorProfile(p) +func SetColorProfile(p Profile) { + DefaultRenderer().SetColorProfile(p) } // HasDarkBackground returns whether or not the terminal has a dark background. func HasDarkBackground() bool { - return renderer.HasDarkBackground() + return DefaultRenderer().HasDarkBackground() } // HasDarkBackground returns whether or not the renderer will render to a dark @@ -142,14 +179,6 @@ func (r *Renderer) HasDarkBackground() bool { r.mtx.RLock() defer r.mtx.RUnlock() - if !r.explicitBackgroundColor { - r.getBackgroundColor.Do(func() { - // NOTE: we don't need to lock here because sync.Once provides its - // own locking mechanism. - r.hasDarkBackground = r.output.HasDarkBackground() - }) - } - return r.hasDarkBackground } @@ -163,7 +192,7 @@ func (r *Renderer) HasDarkBackground() bool { // // This function is thread-safe. func SetHasDarkBackground(b bool) { - renderer.SetHasDarkBackground(b) + DefaultRenderer().SetHasDarkBackground(b) } // SetHasDarkBackground sets the background color detection value on the @@ -180,5 +209,4 @@ func (r *Renderer) SetHasDarkBackground(b bool) { defer r.mtx.Unlock() r.hasDarkBackground = b - r.explicitBackgroundColor = true } diff --git a/renderer_test.go b/renderer_test.go index 6c0b145f..a96acc5e 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -1,20 +1,17 @@ package lipgloss import ( - "io" "os" "testing" - - "github.com/muesli/termenv" ) func TestRendererHasDarkBackground(t *testing.T) { - r1 := NewRenderer(os.Stdout) + r1 := NewRenderer(os.Stdout, nil, true) r1.SetHasDarkBackground(false) if r1.HasDarkBackground() { t.Error("Expected renderer to have light background") } - r2 := NewRenderer(os.Stdout) + r2 := NewRenderer(os.Stdout, nil, false) r2.SetHasDarkBackground(true) if !r2.HasDarkBackground() { t.Error("Expected renderer to have dark background") @@ -28,26 +25,23 @@ func TestRendererWithOutput(t *testing.T) { } defer f.Close() defer os.Remove(f.Name()) - r := NewRenderer(f) - r.SetColorProfile(termenv.TrueColor) - if r.ColorProfile() != termenv.TrueColor { + r := NewRenderer(f, nil, true) + r.SetColorProfile(TrueColor) + if r.ColorProfile() != TrueColor { t.Error("Expected renderer to use true color") } } func TestRace(t *testing.T) { - r := NewRenderer(io.Discard) - o := r.Output() + r := NewRenderer(os.Stdout, nil, true) 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.SetColorProfile(ANSI256) r.SetHasDarkBackground(true) - r.Output() }) } } diff --git a/runes_test.go b/runes_test.go index 44f3963e..be36150d 100644 --- a/runes_test.go +++ b/runes_test.go @@ -19,31 +19,31 @@ func TestStyleRunes(t *testing.T) { "hello 0", "hello", []int{0}, - "\x1b[7mh\x1b[0mello", + "\x1b[7mh\x1b[mello", }, { "你好 1", "你好", []int{1}, - "你\x1b[7m好\x1b[0m", + "你\x1b[7m好\x1b[m", }, { "hello 你好 6,7", "hello 你好", []int{6, 7}, - "hello \x1b[7m你好\x1b[0m", + "hello \x1b[7m你好\x1b[m", }, { "hello 1,3", "hello", []int{1, 3}, - "h\x1b[7me\x1b[0ml\x1b[7ml\x1b[0mo", + "h\x1b[7me\x1b[ml\x1b[7ml\x1b[mo", }, { "你好 0,1", "你好", []int{0, 1}, - "\x1b[7m你好\x1b[0m", + "\x1b[7m你好\x1b[m", }, } diff --git a/style.go b/style.go index 43d1144a..29c773d8 100644 --- a/style.go +++ b/style.go @@ -4,8 +4,18 @@ import ( "strings" "unicode" +<<<<<<< HEAD "github.com/charmbracelet/x/exp/term/ansi" +======= +<<<<<<< HEAD + "github.com/muesli/reflow/truncate" + "github.com/muesli/reflow/wordwrap" + "github.com/muesli/reflow/wrap" +>>>>>>> dd49ed05492e (feat!: introduce color profiles and use term/ansi for styles) "github.com/muesli/termenv" +======= + "github.com/charmbracelet/x/exp/term/ansi" +>>>>>>> d3d126dc69c7 (feat!: introduce color profiles and use term/ansi for styles) ) const tabWidthDefault = 4 @@ -83,7 +93,7 @@ type rules map[propKey]interface{} // in case the underlying implementation changes. It takes an optional string // value to be set as the underlying string value for this style. func NewStyle() Style { - return renderer.NewStyle() + return DefaultRenderer().NewStyle() } // NewStyle returns a new, empty Style. While it's syntactic sugar for the @@ -176,7 +186,7 @@ func (s Style) Inherit(i Style) Style { // Render applies the defined style formatting to a given string. func (s Style) Render(strs ...string) string { if s.r == nil { - s.r = renderer + s.r = DefaultRenderer() } if s.value != "" { strs = append([]string{s.value}, strs...) @@ -186,9 +196,9 @@ func (s Style) Render(strs ...string) string { str = joinString(strs...) p = s.r.ColorProfile() - te = p.String() - teSpace = p.String() - teWhitespace = p.String() + te = p.string() + teSpace = p.string() + teWhitespace = p.string() bold = s.getAsBool(boldKey, false) italic = s.getAsBool(italicKey, false) @@ -237,10 +247,6 @@ func (s Style) Render(strs ...string) string { return s.maybeConvertTabs(str) } - // Enable support for ANSI on the legacy Windows cmd.exe console. This is a - // no-op on non-Windows systems and on Windows runs only once. - enableLegacyWindowsANSI() - if bold { te = te.Bold() } @@ -257,29 +263,29 @@ func (s Style) Render(strs ...string) string { te = te.Reverse() } if blink { - te = te.Blink() + te = te.SlowBlink() } if faint { te = te.Faint() } if fg != noColor { - te = te.Foreground(fg.color(s.r)) + te = te.ForegroundColor(fg.color(s.r)) if styleWhitespace { - teWhitespace = teWhitespace.Foreground(fg.color(s.r)) + teWhitespace = teWhitespace.ForegroundColor(fg.color(s.r)) } if useSpaceStyler { - teSpace = teSpace.Foreground(fg.color(s.r)) + teSpace = teSpace.ForegroundColor(fg.color(s.r)) } } if bg != noColor { - te = te.Background(bg.color(s.r)) + te = te.BackgroundColor(bg.color(s.r)) if colorWhitespace { - teWhitespace = teWhitespace.Background(bg.color(s.r)) + teWhitespace = teWhitespace.BackgroundColor(bg.color(s.r)) } if useSpaceStyler { - teSpace = teSpace.Background(bg.color(s.r)) + teSpace = teSpace.BackgroundColor(bg.color(s.r)) } } @@ -287,14 +293,14 @@ func (s Style) Render(strs ...string) string { te = te.Underline() } if strikethrough { - te = te.CrossOut() + te = te.Strikethrough() } if underlineSpaces { teSpace = teSpace.Underline() } if strikethroughSpaces { - teSpace = teSpace.CrossOut() + teSpace = teSpace.Strikethrough() } // Potentially convert tabs to spaces @@ -341,7 +347,7 @@ func (s Style) Render(strs ...string) string { // Padding if !inline { if leftPadding > 0 { - var st *termenv.Style + var st *style if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -349,7 +355,7 @@ func (s Style) Render(strs ...string) string { } if rightPadding > 0 { - var st *termenv.Style + var st *style if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -377,7 +383,7 @@ func (s Style) Render(strs ...string) string { numLines := strings.Count(str, "\n") if !(numLines == 0 && width == 0) { - var st *termenv.Style + var st *style if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -435,12 +441,12 @@ func (s Style) applyMargins(str string, inline bool) string { bottomMargin = s.getAsInt(marginBottomKey) leftMargin = s.getAsInt(marginLeftKey) - styler termenv.Style + styler = s.r.ColorProfile().string() ) bgc := s.getAsColor(marginBackgroundKey) if bgc != noColor { - styler = styler.Background(bgc.color(s.r)) + styler = styler.BackgroundColor(bgc.color(s.r)) } // Add left and right margin @@ -464,19 +470,19 @@ func (s Style) applyMargins(str string, inline bool) string { } // Apply left padding. -func padLeft(str string, n int, style *termenv.Style) string { +func padLeft(str string, n int, style *style) string { return pad(str, -n, style) } // Apply right padding. -func padRight(str string, n int, style *termenv.Style) string { +func padRight(str string, n int, style *style) string { return pad(str, n, style) } // pad adds padding to either the left or right side of a string. // Positive values add to the right side while negative values // add to the left side. -func pad(str string, n int, style *termenv.Style) string { +func pad(str string, n int, style *style) string { if n == 0 { return str } @@ -530,3 +536,55 @@ func abs(a int) int { return a } + +type style struct { + ansi.Style + Profile +} + +func (s style) Styled(str string) string { + if s.Profile == Ascii { + return str + } + return s.Style.Styled(str) +} + +func (s style) Bold() style { + return style{s.Style.Bold(), s.Profile} +} + +func (s style) Italic() style { + return style{s.Style.Italic(), s.Profile} +} + +func (s style) Underline() style { + return style{s.Style.Underline(), s.Profile} +} + +func (s style) Strikethrough() style { + return style{s.Style.Strikethrough(), s.Profile} +} + +func (s style) Reverse() style { + return style{s.Style.Reverse(), s.Profile} +} + +func (s style) SlowBlink() style { + return style{s.Style.SlowBlink(), s.Profile} +} + +func (s style) RapidBlink() style { + return style{s.Style.RapidBlink(), s.Profile} +} + +func (s style) Faint() style { + return style{s.Style.Faint(), s.Profile} +} + +func (s style) ForegroundColor(c ansi.Color) style { + return style{s.Style.ForegroundColor(c), s.Profile} +} + +func (s style) BackgroundColor(c ansi.Color) style { + return style{s.Style.BackgroundColor(c), s.Profile} +} diff --git a/style_test.go b/style_test.go index 84620e02..2014d32d 100644 --- a/style_test.go +++ b/style_test.go @@ -1,17 +1,15 @@ package lipgloss import ( - "io" + "os" "reflect" "strings" "testing" - - "github.com/muesli/termenv" ) func TestStyleRender(t *testing.T) { - r := NewRenderer(io.Discard) - r.SetColorProfile(termenv.TrueColor) + r := NewRenderer(os.Stdout, nil, true) + r.SetColorProfile(TrueColor) r.SetHasDarkBackground(true) t.Parallel() @@ -21,31 +19,31 @@ func TestStyleRender(t *testing.T) { }{ { r.NewStyle().Foreground(Color("#5A56E0")), - "\x1b[38;2;89;86;224mhello\x1b[0m", + "\x1b[38;2;89;86;224mhello\x1b[m", }, { r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), - "\x1b[38;2;89;86;224mhello\x1b[0m", + "\x1b[38;2;89;86;224mhello\x1b[m", }, { r.NewStyle().Bold(true), - "\x1b[1mhello\x1b[0m", + "\x1b[1mhello\x1b[m", }, { r.NewStyle().Italic(true), - "\x1b[3mhello\x1b[0m", + "\x1b[3mhello\x1b[m", }, { 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[m\x1b[4;4me\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4mo\x1b[m", }, { r.NewStyle().Blink(true), - "\x1b[5mhello\x1b[0m", + "\x1b[5mhello\x1b[m", }, { r.NewStyle().Faint(true), - "\x1b[2mhello\x1b[0m", + "\x1b[2mhello\x1b[m", }, } @@ -61,44 +59,44 @@ func TestStyleRender(t *testing.T) { } func TestStyleCustomRender(t *testing.T) { - r := NewRenderer(io.Discard) + r := NewRenderer(os.Stdout, nil, true) r.SetHasDarkBackground(false) - r.SetColorProfile(termenv.TrueColor) + r.SetColorProfile(TrueColor) tt := []struct { style Style expected string }{ { r.NewStyle().Foreground(Color("#5A56E0")), - "\x1b[38;2;89;86;224mhello\x1b[0m", + "\x1b[38;2;89;86;224mhello\x1b[m", }, { r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), - "\x1b[38;2;255;254;18mhello\x1b[0m", + "\x1b[38;2;255;254;18mhello\x1b[m", }, { r.NewStyle().Bold(true), - "\x1b[1mhello\x1b[0m", + "\x1b[1mhello\x1b[m", }, { r.NewStyle().Italic(true), - "\x1b[3mhello\x1b[0m", + "\x1b[3mhello\x1b[m", }, { 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[m\x1b[4;4me\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4mo\x1b[m", }, { r.NewStyle().Blink(true), - "\x1b[5mhello\x1b[0m", + "\x1b[5mhello\x1b[m", }, { r.NewStyle().Faint(true), - "\x1b[2mhello\x1b[0m", + "\x1b[2mhello\x1b[m", }, { NewStyle().Faint(true).Renderer(r), - "\x1b[2mhello\x1b[0m", + "\x1b[2mhello\x1b[m", }, } @@ -114,7 +112,7 @@ func TestStyleCustomRender(t *testing.T) { } func TestStyleRenderer(t *testing.T) { - r := NewRenderer(io.Discard) + r := NewRenderer(os.Stdout, nil, true) s1 := NewStyle().Bold(true) s2 := s1.Renderer(r) if s1.r == s2.r { @@ -349,7 +347,7 @@ func TestStyleValue(t *testing.T) { name: "set string with bold", text: "foo", style: NewStyle().SetString("bar").Bold(true), - expected: "\x1b[1mbar foo\x1b[0m", + expected: "\x1b[1mbar foo\x1b[m", }, { name: "new style with string", @@ -442,7 +440,7 @@ func TestStringTransform(t *testing.T) { }, } { res := NewStyle().Bold(true).Transform(tc.fn).Render(tc.input) - expected := "\x1b[1m" + tc.expected + "\x1b[0m" + expected := "\x1b[1m" + tc.expected + "\x1b[m" if res != expected { t.Errorf("Test #%d:\nExpected: %q\nGot: %q", i+1, expected, res) } diff --git a/whitespace.go b/whitespace.go index 19656871..58bfd593 100644 --- a/whitespace.go +++ b/whitespace.go @@ -3,15 +3,23 @@ package lipgloss import ( "strings" +<<<<<<< HEAD "github.com/charmbracelet/x/exp/term/ansi" +======= +<<<<<<< HEAD + "github.com/muesli/reflow/ansi" +>>>>>>> dd49ed05492e (feat!: introduce color profiles and use term/ansi for styles) "github.com/muesli/termenv" +======= + "github.com/charmbracelet/x/exp/term/ansi" +>>>>>>> d3d126dc69c7 (feat!: introduce color profiles and use term/ansi for styles) ) // whitespace is a whitespace renderer. type whitespace struct { re *Renderer - style termenv.Style chars string + style style } // newWhitespace creates a new whitespace renderer. The order of the options @@ -20,7 +28,7 @@ type whitespace struct { func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace { w := &whitespace{ re: r, - style: r.ColorProfile().String(), + style: r.ColorProfile().string(), } for _, opt := range opts { opt(w) @@ -64,14 +72,14 @@ type WhitespaceOption func(*whitespace) // WithWhitespaceForeground sets the color of the characters in the whitespace. func WithWhitespaceForeground(c TerminalColor) WhitespaceOption { return func(w *whitespace) { - w.style = w.style.Foreground(c.color(w.re)) + w.style = w.style.ForegroundColor(c.color(w.re)) } } // WithWhitespaceBackground sets the background color of the whitespace. func WithWhitespaceBackground(c TerminalColor) WhitespaceOption { return func(w *whitespace) { - w.style = w.style.Background(c.color(w.re)) + w.style = w.style.BackgroundColor(c.color(w.re)) } }