feat!: introduce color profiles and use term/ansi for styles

This introduces color profiles similar to Termenv color profiles.
Plus, it switches to using Charmbracelet term & term/ansi to query
terminal background color for the default renderer, and construct styles.

However, it breaks the renderer API since it doesn't depend on Termenv
anymore.
This commit is contained in:
Ayman Bagabas 2024-03-14 10:56:02 -04:00
parent e87acc3c4a
commit 05affa7bd0
No known key found for this signature in database
GPG Key ID: C8D51D2157C919AC
18 changed files with 863 additions and 249 deletions

View File

@ -4,13 +4,12 @@ import (
"strings"
"github.com/charmbracelet/x/exp/term/ansi"
"github.com/muesli/termenv"
)
// 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 +58,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

View File

@ -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
}
}
})
}

View File

@ -4,7 +4,6 @@ import (
"strings"
"github.com/charmbracelet/x/exp/term/ansi"
"github.com/muesli/termenv"
"github.com/rivo/uniseg"
)
@ -407,13 +406,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)

View File

@ -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
}

View File

@ -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"},

120
env.go Normal file
View File

@ -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
}

View File

@ -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/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // 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.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.17.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.19.0 // indirect
)

View File

@ -17,6 +17,7 @@ github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBER
github.com/charmbracelet/wish v0.5.0 h1:FkkdNBFqrLABR1ciNrAL2KCxoyWfKhXnIGZw6GfAtPg=
github.com/charmbracelet/wish v0.5.0/go.mod h1:5GAn5SrDSZ7cgKjnC+3kDmiIo7I6k4/AYiRzC4+tpCk=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE=
github.com/charmbracelet/x/exp/term v0.0.0-20240422203001-5cc5941b761c h1:MF4XzYBazvaM6g2IlOwgnsIuteW5q8tRfldetAHk2yg=
github.com/charmbracelet/x/exp/term v0.0.0-20240422203001-5cc5941b761c/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
@ -24,6 +25,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=
@ -54,17 +56,16 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/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=
@ -86,6 +87,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=
@ -94,23 +96,19 @@ 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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -129,32 +127,25 @@ 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.5.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

13
go.mod
View File

@ -5,15 +5,14 @@ retract v0.7.0 // v0.7.0 introduces a bug that causes some apps to freeze.
go 1.18
require (
github.com/charmbracelet/x/exp/term v0.0.0-20240422203001-5cc5941b761c
github.com/muesli/termenv v0.15.2
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/rivo/uniseg v0.4.7
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.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
golang.org/x/sys v0.19.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
)

26
go.sum
View File

@ -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-20240422203001-5cc5941b761c h1:MF4XzYBazvaM6g2IlOwgnsIuteW5q8tRfldetAHk2yg=
github.com/charmbracelet/x/exp/term v0.0.0-20240422203001-5cc5941b761c/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/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=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=

View File

@ -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

417
profile.go Normal file
View File

@ -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",
}

View File

@ -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
}
// 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 termenv color profile.
func (r *Renderer) ColorProfile() termenv.Profile {
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.colorProfile
}
// ColorProfile returns the detected termenv color profile.
func ColorProfile() termenv.Profile {
return renderer.ColorProfile()
// ColorProfile returns the detected color profile.
func ColorProfile() Profile {
return DefaultRenderer().ColorProfile()
}
// 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()
return r.isatty
}
// IsTerminal returns whether or not the default renderer is thinking it's
// writing to a terminal.
func IsTerminal() bool {
return DefaultRenderer().IsTerminal()
}
// 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
}

View File

@ -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()
})
}
}

View File

@ -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",
},
}

101
style.go
View File

@ -5,7 +5,6 @@ import (
"unicode"
"github.com/charmbracelet/x/exp/term/ansi"
"github.com/muesli/termenv"
)
const tabWidthDefault = 4
@ -83,7 +82,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 +175,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 +185,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 +236,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 +252,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 +282,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
@ -340,7 +335,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
}
@ -348,7 +343,7 @@ func (s Style) Render(strs ...string) string {
}
if rightPadding > 0 {
var st *termenv.Style
var st *style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
@ -376,7 +371,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
}
@ -434,12 +429,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
@ -463,19 +458,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
}
@ -529,3 +524,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}
}

View File

@ -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)
}

View File

@ -4,14 +4,13 @@ import (
"strings"
"github.com/charmbracelet/x/exp/term/ansi"
"github.com/muesli/termenv"
)
// 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 +19,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 +63,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))
}
}