diff --git a/color.go b/color.go index d5f39838..d21c92b8 100644 --- a/color.go +++ b/color.go @@ -1,6 +1,7 @@ package lipgloss import ( + "fmt" "image/color" "strconv" @@ -54,32 +55,36 @@ func (n NoColor) RGBA() (r, g, b, a uint32) { // ansiColor := lipgloss.Color(21) // hexColor := lipgloss.Color("#0000ff") // uint32Color := lipgloss.Color(0xff0000) -func Color[T string | int](c T) color.Color { - var col color.Color = noColor - switch c := any(c).(type) { +func Color(c any) color.Color { + switch c := c.(type) { + case nil: + return noColor case string: if len(c) == 0 { - return col + return noColor } if h, err := colorful.Hex(c); err == nil { return h } else if i, err := strconv.Atoi(c); err == nil { - if i < 16 { - return ansi.BasicColor(i) - } else if i < 256 { - return ansi.ExtendedColor(i) + if i < 16 { //nolint:gomnd + return ansi.BasicColor(i) //nolint:gosec + } else if i < 256 { //nolint:gomnd + return ansi.ExtendedColor(i) //nolint:gosec } - return ansi.TrueColor(i) + return ansi.TrueColor(i) //nolint:gosec } + return noColor case int: if c < 16 { - return ansi.BasicColor(c) + return ansi.BasicColor(c) //nolint:gosec } else if c < 256 { - return ansi.ExtendedColor(c) + return ansi.ExtendedColor(c) //nolint:gosec } - return ansi.TrueColor(c) + return ansi.TrueColor(c) //nolint:gosec + case color.Color: + return c } - return col + return Color(fmt.Sprint(c)) } // RGBColor is a color specified by red, green, and blue values. @@ -92,9 +97,10 @@ type RGBColor struct { // RGBA returns the RGBA value of this color. This satisfies the Go Color // interface. func (c RGBColor) RGBA() (r, g, b, a uint32) { - r |= uint32(c.R) << 8 - g |= uint32(c.G) << 8 - b |= uint32(c.B) << 8 + const shift = 8 + r |= uint32(c.R) << shift + g |= uint32(c.G) << shift + b |= uint32(c.B) << shift a = 0xFFFF //nolint:gomnd return } @@ -107,15 +113,62 @@ func (c RGBColor) RGBA() (r, g, b, a uint32) { // colorB := lipgloss.ANSIColor(134) type ANSIColor = ansi.ExtendedColor -// IsDarkColor returns whether the given color is dark. +// LightDarkFunc is a function that returns a color based on whether the +// terminal has a light or dark background. You can create one of these with +// [LightDark]. +// +// Example: +// +// lightDark := lipgloss.LightDark(hasDarkBackground) +// myHotColor := lightDark("#ff0000", "#0000ff") +// +// For more info see [LightDark]. +type LightDarkFunc func(light, dark any) color.Color + +// LightDark is a simple helper type that can be used to choose the appropriate +// color based on whether the terminal has a light or dark background. +// +// lightDark := lipgloss.LightDark(hasDarkBackground) +// theRightColor := lightDark("#0000ff", "#ff0000") +// +// In practice, there are slightly different workflows between Bubble Tea and +// Lip Gloss standalone. +// +// In Bubble Tea listen for tea.BackgroundColorMsg, which automatically +// flows through Update on start, and whenever the background color changes: +// +// case tea.BackgroundColorMsg: +// m.hasDarkBackground = msg.IsDark() +// +// Later, when you're rendering: +// +// lightDark := lipgloss.LightDark(m.hasDarkBackground) +// myHotColor := lightDark("#ff0000", "#0000ff") +// +// In standalone Lip Gloss, the workflow is simpler: +// +// hasDarkBG, _ := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) +// lightDark := lipgloss.LightDark(hasDarkBG) +// myHotColor := lightDark("#ff0000", "#0000ff") +func LightDark(isDark bool) LightDarkFunc { + return func(light, dark any) color.Color { + if isDark { + return Color(dark) + } + return Color(light) + } +} + +// IsDarkColor returns whether the given color is dark (based on the luminance +// portion of the color as interpreted as HSL). // // Example usage: // // color := lipgloss.Color("#0000ff") // if lipgloss.IsDarkColor(color) { -// fmt.Println("It's dark!") +// fmt.Println("It's dark! I love darkness!") // } else { -// fmt.Println("It's light!") +// fmt.Println("It's light! Cover your eyes!") // } func IsDarkColor(c color.Color) bool { col, ok := colorful.MakeColor(c) diff --git a/color_test.go b/color_test.go index 7cec7af7..519e5821 100644 --- a/color_test.go +++ b/color_test.go @@ -1,7 +1,6 @@ package lipgloss import ( - "image/color" "testing" ) @@ -31,8 +30,8 @@ func TestHexToColor(t *testing.T) { } for i, tc := range tt { - h := hexToColor(tc.input) - o := uint(h.R)<<16 + uint(h.G)<<8 + uint(h.B) + r, g, b, _ := Color(tc.input).RGBA() + o := uint(r>>8)<<16 + uint(g>>8)<<8 + uint(b>>8) if o != tc.expected { t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1) } @@ -41,145 +40,26 @@ func TestHexToColor(t *testing.T) { func TestRGBA(t *testing.T) { tt := []struct { - profile Profile - darkBg bool - input TerminalColor + input string expected uint }{ // lipgloss.Color { - TrueColor, - true, - Color("#FF0000"), - 0xFF0000, - }, - { - TrueColor, - true, - Color("9"), - 0xFF0000, - }, - { - TrueColor, - true, - Color("21"), - 0x0000FF, - }, - // lipgloss.AdaptiveColor - { - TrueColor, - true, - AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"}, - 0xFF0000, - }, - { - TrueColor, - false, - AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"}, - 0x0000FF, - }, - { - TrueColor, - true, - AdaptiveColor{Light: "21", Dark: "9"}, + "#FF0000", 0xFF0000, }, { - TrueColor, - false, - AdaptiveColor{Light: "21", Dark: "9"}, - 0x0000FF, - }, - // lipgloss.CompleteColor - { - TrueColor, - true, - CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, + "9", 0xFF0000, }, { - ANSI256, - true, - CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - 0xFFFFFF, - }, - { - ANSI, - true, - CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, + "21", 0x0000FF, }, - { - TrueColor, - true, - CompleteColor{TrueColor: "", ANSI256: "231", ANSI: "12"}, - 0x000000, - }, - // lipgloss.CompleteAdaptiveColor - // dark - { - TrueColor, - true, - CompleteAdaptiveColor{ - Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"}, - Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - }, - 0xFF0000, - }, - { - ANSI256, - true, - CompleteAdaptiveColor{ - Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"}, - Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - }, - 0xFFFFFF, - }, - { - ANSI, - true, - CompleteAdaptiveColor{ - Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"}, - Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - }, - 0x0000FF, - }, - // light - { - TrueColor, - false, - CompleteAdaptiveColor{ - Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"}, - Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - }, - 0x0000FF, - }, - { - ANSI256, - false, - CompleteAdaptiveColor{ - Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"}, - Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - }, - 0x0000FF, - }, - { - ANSI, - false, - CompleteAdaptiveColor{ - Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"}, - Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, - }, - 0xFF0000, - }, } - r := DefaultRenderer() for i, tc := range tt { - r.SetColorProfile(tc.profile) - r.SetHasDarkBackground(tc.darkBg) - - r, g, b, _ := tc.input.color(r).RGBA() + r, g, b, _ := Color(tc.input).RGBA() o := uint(r/256)<<16 + uint(g/256)<<8 + uint(b/256) if o != tc.expected { @@ -187,48 +67,3 @@ func TestRGBA(t *testing.T) { } } } - -// hexToColor translates a hex color string (#RRGGBB or #RGB) into a color.RGB, -// which satisfies the color.Color interface. If an invalid string is passed -// black with 100% opacity will be returned: or, in hex format, 0x000000FF. -func hexToColor(hex string) (c color.RGBA) { - c.A = 0xFF - - if hex == "" || hex[0] != '#' { - return c - } - - const ( - fullFormat = 7 // #RRGGBB - shortFormat = 4 // #RGB - ) - - switch len(hex) { - case fullFormat: - const offset = 4 - c.R = hexToByte(hex[1])<= '0' && b <= '9': - return b - '0' - case b >= 'a' && b <= 'f': - return b - 'a' + offset - case b >= 'A' && b <= 'F': - return b - 'A' + offset - } - // Invalid, but just return 0. - return 0 -} diff --git a/examples/color/bubbletea/main.go b/examples/color/bubbletea/main.go new file mode 100644 index 00000000..a144f5c7 --- /dev/null +++ b/examples/color/bubbletea/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss" +) + +// Style definitions. +type styles struct { + frame, + paragraph, + text, + keyword, + activeButton, + inactiveButton lipgloss.Style +} + +// Styles are initialized based on the background color of the terminal. +func newStyles(backgroundIsDark bool) (s *styles) { + s = new(styles) + + // Create a new helper function for choosing either a light or dark color + // based on the detected background color. + lightDark := lipgloss.LightDark(backgroundIsDark) + + // Define some styles. adaptive.Color() can be used to choose the + // appropriate light or dark color based on the detected background color. + s.frame = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lightDark("#C5ADF9", "#864EFF")). + Padding(1, 3). + Margin(1, 3) + s.paragraph = lipgloss.NewStyle(). + Width(40). + MarginBottom(1). + Align(lipgloss.Center) + s.text = lipgloss.NewStyle(). + Foreground(lightDark("#696969", "#bdbdbd")) + s.keyword = lipgloss.NewStyle(). + Foreground(lightDark("#37CD96", "#22C78A")). + Bold(true) + + s.activeButton = lipgloss.NewStyle(). + Padding(0, 3). + Background(lipgloss.Color(0xFF6AD2)). // you can also use octal format for colors, i.e 0xff38ec. + Foreground(lipgloss.Color(0xFFFCC2)) + s.inactiveButton = s.activeButton. + Background(lightDark(0x988F95, 0x978692)). + Foreground(lightDark(0xFDFCE3, 0xFBFAE7)) + return s +} + +type model struct { + styles *styles + yes bool + chosen bool + aborted bool +} + +func (m model) Init() (tea.Model, tea.Cmd) { + // Query for the background color on start. + m.yes = true + return m, tea.BackgroundColor +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + // Bubble Tea automatically detects the background color on start. We + // listen for the response here, then initialize our styles accordingly. + case tea.BackgroundColorMsg: + m.styles = newStyles(msg.IsDark()) + return m, nil + + case tea.KeyPressMsg: + switch msg.String() { + case "q", "esc", "ctrl+c": + m.aborted = true + return m, tea.Quit + case "enter": + m.chosen = true + return m, tea.Quit + case "left", "right", "h", "l": + m.yes = !m.yes + case "y": + m.yes = true + m.chosen = true + return m, tea.Quit + case "n": + m.yes = false + m.chosen = true + return m, tea.Quit + } + } + + return m, nil +} + +func (m model) View() string { + if m.styles == nil { + // We haven't received tea.BackgroundColorMsg yet. Don't worry, it'll + // be here in a flash. + return "" + } + if m.chosen || m.aborted { + // We're about to exit, so wipe the UI. + return "" + } + + var ( + s = m.styles + y = "Yes" + n = "No" + ) + + if m.yes { + y = s.activeButton.Render(y) + n = s.inactiveButton.Render(n) + } else { + y = s.inactiveButton.Render(y) + n = s.activeButton.Render(n) + } + + return s.frame.Render( + lipgloss.JoinVertical(lipgloss.Center, + s.paragraph.Render( + s.text.Render("Are you sure you want to eat that ")+ + s.keyword.Render("moderatly ripe")+ + s.text.Render(" banana?"), + ), + y+" "+n, + ), + ) +} + +func main() { + m, err := tea.NewProgram(model{}).Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Uh oh: %v", err) + os.Exit(1) + } + + if m := m.(model); m.chosen { + if m.yes { + fmt.Println("Are you sure? It's not ripe yet.") + } else { + fmt.Println("Well, alright. It was probably good, though.") + } + } +} diff --git a/examples/color/standalone/main.go b/examples/color/standalone/main.go new file mode 100644 index 00000000..b71c4308 --- /dev/null +++ b/examples/color/standalone/main.go @@ -0,0 +1,74 @@ +// This example illustrates how to detect the terminal's background color and +// choose either light or dark colors accordingly when using Lip Gloss in a. +// standalone fashion, i.e. independent of Bubble Tea. +// +// For an example of how to do this in a Bubble Tea program, see the +// 'bubbletea' example. +package main + +import ( + "fmt" + "os" + + "github.com/charmbracelet/lipgloss" +) + +func main() { + // Query for the background color. We only need to do this once, and only + // when using Lip Gloss standalone. + // + // In Bubble Tea listen for tea.BackgroundColorMsg in your Update. + hasDarkBG, err := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not detect background color: %v\n", err) + os.Exit(1) + } + + // Create a new helper function for choosing either a light or dark color + // based on the detected background color. + lightDark := lipgloss.LightDark(hasDarkBG) + + // Define some styles. adaptive.Color() can be used to choose the + // appropriate light or dark color based on the detected background color. + frameStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lightDark("#C5ADF9", "#864EFF")). + Padding(1, 3). + Margin(1, 3) + paragraphStyle := lipgloss.NewStyle(). + Width(40). + MarginBottom(1). + Align(lipgloss.Center) + textStyle := lipgloss.NewStyle(). + Foreground(lightDark("#696969", "#bdbdbd")) + keywordStyle := lipgloss.NewStyle(). + Foreground(lightDark("#37CD96", "#22C78A")). + Bold(true) + + activeButton := lipgloss.NewStyle(). + Padding(0, 3). + Background(lipgloss.Color(0xFF6AD2)). // you can also use octal format for colors, i.e 0xff38ec. + Foreground(lipgloss.Color(0xFFFCC2)) + inactiveButton := activeButton. + Background(lightDark(0x988F95, 0x978692)). + Foreground(lightDark(0xFDFCE3, 0xFBFAE7)) + + // Build layout. + text := paragraphStyle.Render( + textStyle.Render("Are you sure you want to eat that ") + + keywordStyle.Render("moderatly ripe") + + textStyle.Render(" banana?"), + ) + buttons := activeButton.Render("Yes") + " " + inactiveButton.Render("No") + block := frameStyle.Render( + lipgloss.JoinVertical(lipgloss.Center, text, buttons), + ) + + // Print the block to stdout. It's important to use Lip Gloss's print + // functions to ensure that colors are downsampled correctly. If output + // isn't a TTY (i.e. we're logging to a file) colors will be stripped + // entirely. + // + // Note that in Bubble Tea downsampling happens automatically. + lipgloss.Println(block) +} diff --git a/examples/go.mod b/examples/go.mod index 63961127..b864d844 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -6,15 +6,17 @@ replace github.com/charmbracelet/lipgloss => ../ replace github.com/charmbracelet/lipgloss/list => ../list +replace github.com/charmbracelet/lipgloss/table => ../table + require ( - github.com/charmbracelet/lipgloss v0.11.0 + github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1.0.20241016204941-d95e2750ec12 + github.com/charmbracelet/colorprofile v0.1.2 + github.com/charmbracelet/lipgloss v0.13.1-0.20240822211938-b89f1a3db2a4 github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 github.com/charmbracelet/wish v1.4.0 - github.com/creack/pty v1.1.21 + github.com/charmbracelet/x/term v0.2.0 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/muesli/gamut v0.3.1 - github.com/muesli/termenv v0.15.2 - golang.org/x/term v0.21.0 ) require ( @@ -26,21 +28,31 @@ require ( github.com/charmbracelet/x/ansi v0.3.2 // indirect github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect + github.com/charmbracelet/x/input v0.2.0 // indirect + github.com/charmbracelet/x/windows v0.2.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/creack/pty v1.1.21 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect github.com/muesli/kmeans v0.3.1 // indirect github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.14.0 // indirect ) + +// replace with log v2 +replace github.com/charmbracelet/log => github.com/charmbracelet/log v0.4.1-0.20241010222913-47ce960d4847 diff --git a/examples/go.sum b/examples/go.sum index 69e7f0a5..f66420e3 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -5,10 +5,14 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1.0.20241016204941-d95e2750ec12 h1:w1Dt/pl0gXE6PO2U+EUMoeMiSHe07/RR1Slcy9cH6kQ= +github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1.0.20241016204941-d95e2750ec12/go.mod h1:8vhUTCIihfP3GfyyfznbOU7XyFe/ID1n7pdh/uT5Rug= +github.com/charmbracelet/colorprofile v0.1.2 h1:nuB1bd/yAExT4fkcZvpqtQ2N5/8cJHSRIKb6CzT7lAM= +github.com/charmbracelet/colorprofile v0.1.2/go.mod h1:1htIKZYeI4TQs+OykPvpuBTUbUJxBYeSYBDIZuejMj0= github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc= github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8= -github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= -github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/log v0.4.1-0.20241010222913-47ce960d4847 h1:3uto8OPDaseTS92giSw/8GfVQ+TCjcVE+QOaf8lT0Zc= +github.com/charmbracelet/log v0.4.1-0.20241010222913-47ce960d4847/go.mod h1:3UCs04fasl8pA1HoNNJ8HCYCaKoeMBZrNXo9iy5F0vY= github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 h1:NZKjJ7d/pzk/AfcJYEzmF8M48JlIrrY00RR5JdDc3io= github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U= github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc= @@ -20,11 +24,19 @@ github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2 github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 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/charmbracelet/x/input v0.2.0 h1:1Sv+y/flcqUfUH2PXNIDKDIdT2G8smOnGOgawqhwy8A= +github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= +github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +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/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -35,10 +47,10 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -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/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 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/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= @@ -59,6 +71,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +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/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= @@ -66,12 +80,13 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/examples/layout/main.go b/examples/layout/main.go index 6ff82c97..811fbc88 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -9,10 +9,9 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/adaptive" + "github.com/charmbracelet/x/term" "github.com/lucasb-eyer/go-colorful" "github.com/muesli/gamut" - "golang.org/x/term" ) const ( @@ -22,179 +21,205 @@ const ( // wrapping. width = 96 + // How wide to render various columns in the layout. columnWidth = 30 ) -// Style definitions. var ( + // Whether the detected background color is dark. We detect this in init(). + hasDarkBG bool - // General. + // A helper function for choosing either a light or dark color based on the + // detected background color. We create this in init(). + lightDark lipgloss.LightDarkFunc +) - subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} - highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} - special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} - blends = gamut.Blends(lipgloss.Color("#F25D94"), lipgloss.Color("#EDFF82"), 50) +func init() { + var err error - divider = lipgloss.NewStyle(). - SetString("•"). - Padding(0, 1). - Foreground(subtle). - String() + // Detect the background color. + hasDarkBG, err = lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not detect background color: %v\n", err) + os.Exit(1) + } - url = lipgloss.NewStyle().Foreground(special).Render + // Create a new helper function for choosing either a light or dark color + // based on the detected background color. + lightDark = lipgloss.LightDark(hasDarkBG) +} - // Tabs. +func main() { - activeTabBorder = lipgloss.Border{ - Top: "─", - Bottom: " ", - Left: "│", - Right: "│", - TopLeft: "╭", - TopRight: "╮", - BottomLeft: "┘", - BottomRight: "└", - } + // Style definitions. + var ( - tabBorder = lipgloss.Border{ - Top: "─", - Bottom: "─", - Left: "│", - Right: "│", - TopLeft: "╭", - TopRight: "╮", - BottomLeft: "┴", - BottomRight: "┴", - } + // General. - tab = lipgloss.NewStyle(). - Border(tabBorder, true). - BorderForeground(highlight). - Padding(0, 1) + subtle = lightDark("#D9DCCF", "#383838") + highlight = lightDark("#874BFD", "#7D56F4") + special = lightDark("#43BF6D", "#73F59F") + blends = gamut.Blends(lipgloss.Color("#F25D94"), lipgloss.Color("#EDFF82"), 50) - activeTab = tab.Border(activeTabBorder, true) + divider = lipgloss.NewStyle(). + SetString("•"). + Padding(0, 1). + Foreground(subtle). + String() - tabGap = tab. - BorderTop(false). - BorderLeft(false). - BorderRight(false) + url = lipgloss.NewStyle().Foreground(special).Render - // Title. + // Tabs. - titleStyle = lipgloss.NewStyle(). - MarginLeft(1). - MarginRight(5). - Padding(0, 1). - Italic(true). - Foreground(lipgloss.Color("#FFF7DB")). - SetString("Lip Gloss") + activeTabBorder = lipgloss.Border{ + Top: "─", + Bottom: " ", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "┘", + BottomRight: "└", + } - descStyle = lipgloss.NewStyle().MarginTop(1) + tabBorder = lipgloss.Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "┴", + BottomRight: "┴", + } - infoStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderTop(true). - BorderForeground(subtle) + tab = lipgloss.NewStyle(). + Border(tabBorder, true). + BorderForeground(highlight). + Padding(0, 1) - // Dialog. + activeTab = tab.Border(activeTabBorder, true) - dialogBoxStyle = lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#874BFD")). - Padding(1, 0). - BorderTop(true). - BorderLeft(true). - BorderRight(true). - BorderBottom(true) - - buttonStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFF7DB")). - Background(lipgloss.Color("#888B7E")). - Padding(0, 3). - MarginTop(1) - - activeButtonStyle = buttonStyle. + tabGap = tab. + BorderTop(false). + BorderLeft(false). + BorderRight(false) + + // Title. + + titleStyle = lipgloss.NewStyle(). + MarginLeft(1). + MarginRight(5). + Padding(0, 1). + Italic(true). Foreground(lipgloss.Color("#FFF7DB")). - Background(lipgloss.Color("#F25D94")). - MarginRight(2). - Underline(true) + SetString("Lip Gloss") + + descStyle = lipgloss.NewStyle().MarginTop(1) + + infoStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderTop(true). + BorderForeground(subtle) - // List. + // Dialog. - list = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder(), false, true, false, false). - BorderForeground(subtle). - MarginRight(2). - Height(8). - Width(columnWidth + 1) + dialogBoxStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#874BFD")). + Padding(1, 0). + BorderTop(true). + BorderLeft(true). + BorderRight(true). + BorderBottom(true) - listHeader = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderBottom(true). + buttonStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFF7DB")). + Background(lipgloss.Color("#888B7E")). + Padding(0, 3). + MarginTop(1) + + activeButtonStyle = buttonStyle. + Foreground(lipgloss.Color("#FFF7DB")). + Background(lipgloss.Color("#F25D94")). + MarginRight(2). + Underline(true) + + // List. + + list = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, true, false, false). BorderForeground(subtle). MarginRight(2). - Render + Height(8). + Width(columnWidth + 1) - listItem = lipgloss.NewStyle().PaddingLeft(2).Render + listHeader = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderBottom(true). + BorderForeground(subtle). + MarginRight(2). + Render - checkMark = lipgloss.NewStyle().SetString("✓"). - Foreground(special). - PaddingRight(1). - String() + listItem = lipgloss.NewStyle().PaddingLeft(2).Render - listDone = func(s string) string { - return checkMark + lipgloss.NewStyle(). - Strikethrough(true). - Foreground(adaptive.AdaptiveColor("#969B86", "#696969")). - Render(s) - } + checkMark = lipgloss.NewStyle().SetString("✓"). + Foreground(special). + PaddingRight(1). + String() - // Paragraphs/History. + listDone = func(s string) string { + return checkMark + lipgloss.NewStyle(). + Strikethrough(true). + Foreground(lightDark("#969B86", "#696969")). + Render(s) + } - historyStyle = lipgloss.NewStyle(). - Align(lipgloss.Left). - Foreground(lipgloss.Color("#FAFAFA")). - Background(highlight). - Margin(1, 3, 0, 0). - Padding(1, 2). - Height(19). - Width(columnWidth) + // Paragraphs/History. - // Status Bar. + historyStyle = lipgloss.NewStyle(). + Align(lipgloss.Left). + Foreground(lipgloss.Color("#FAFAFA")). + Background(highlight). + Margin(1, 3, 0, 0). + Padding(1, 2). + Height(19). + Width(columnWidth) - statusNugget = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFDF5")). - Padding(0, 1) + // Status Bar. - statusBarStyle = lipgloss.NewStyle(). - Foreground(adaptive.AdaptiveColor("#343433", "#C1C6B2")). - Background(adaptive.AdaptiveColor("#D9DCCF", "#353533")) + statusNugget = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFDF5")). + Padding(0, 1) - statusStyle = lipgloss.NewStyle(). - Inherit(statusBarStyle). - Foreground(lipgloss.Color("#FFFDF5")). - Background(lipgloss.Color("#FF5F87")). - Padding(0, 1). - MarginRight(1) + statusBarStyle = lipgloss.NewStyle(). + Foreground(lightDark("#343433", "#C1C6B2")). + Background(lightDark("#D9DCCF", "#353533")) - encodingStyle = statusNugget. - Background(lipgloss.Color("#A550DF")). - Align(lipgloss.Right) + statusStyle = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(lipgloss.Color("#FFFDF5")). + Background(lipgloss.Color("#FF5F87")). + Padding(0, 1). + MarginRight(1) - statusText = lipgloss.NewStyle().Inherit(statusBarStyle) + encodingStyle = statusNugget. + Background(lipgloss.Color("#A550DF")). + Align(lipgloss.Right) - fishCakeStyle = statusNugget.Background(lipgloss.Color("#6124DF")) + statusText = lipgloss.NewStyle().Inherit(statusBarStyle) - // Page. + fishCakeStyle = statusNugget.Background(lipgloss.Color("#6124DF")) - docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) -) + // Page. -func main() { - physicalWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) + docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) + ) + + physicalWidth, _, _ := term.GetSize(os.Stdout.Fd()) doc := strings.Builder{} - // Tabs + // Tabs. { row := lipgloss.JoinHorizontal( lipgloss.Top, @@ -209,7 +234,7 @@ func main() { doc.WriteString(row + "\n\n") } - // Title + // Title. { var ( colors = colorGrid(1, 5) @@ -234,7 +259,7 @@ func main() { doc.WriteString(row + "\n\n") } - // Dialog + // Dialog. { okButton := activeButtonStyle.Render("Yes") cancelButton := buttonStyle.Render("Maybe") @@ -253,7 +278,7 @@ func main() { doc.WriteString(dialog + "\n\n") } - // Color grid + // Color grid. colors := func() string { colors := colorGrid(14, 8) @@ -294,7 +319,7 @@ func main() { doc.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, lists, colors)) - // Marmalade history + // Marmalade history. { const ( historyA = "The Romans learned from the Greeks that quinces slowly cooked with honey would “set” when cool. The Apicius gives a recipe for preserving whole quinces, stems and leaves attached, in a bath of honey diluted with defrutum: Roman marmalade. Preserves of quince and lemon appear (along with rose, apple, plum and pear) in the Book of ceremonies of the Byzantine Emperor Constantine VII Porphyrogennetos." @@ -312,16 +337,21 @@ func main() { doc.WriteString("\n\n") } - // Status bar + // Status bar. { w := lipgloss.Width + lightDarkState := "Light" + if hasDarkBG { + lightDarkState = "Dark" + } + statusKey := statusStyle.Render("STATUS") encoding := encodingStyle.Render("UTF-8") fishCake := fishCakeStyle.Render("🍥 Fish Cake") statusVal := statusText. Width(width - w(statusKey) - w(encoding) - w(fishCake)). - Render("Ravishing") + Render("Ravishingly " + lightDarkState + "!") bar := lipgloss.JoinHorizontal(lipgloss.Top, statusKey, @@ -337,7 +367,9 @@ func main() { docStyle = docStyle.MaxWidth(physicalWidth) } - // Okay, let's print it + // Okay, let's print it. We use a special Lipgloss writer to downsample + // colors to the terminal's color palette. And, if output's not a TTY, we + // will remove color entirely. lipgloss.Println(docStyle.Render(doc.String())) } diff --git a/examples/list/duckduckgoose/main.go b/examples/list/duckduckgoose/main.go index 7ff6eb5e..1f5b03f9 100644 --- a/examples/list/duckduckgoose/main.go +++ b/examples/list/duckduckgoose/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" ) @@ -22,5 +20,6 @@ func main() { ItemStyle(itemStyle). EnumeratorStyle(enumStyle). Enumerator(duckDuckGooseEnumerator) - fmt.Println(l) + + lipgloss.Println(l) } diff --git a/examples/list/glow/main.go b/examples/list/glow/main.go index d2f1d48d..a004ebf1 100644 --- a/examples/list/glow/main.go +++ b/examples/list/glow/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" ) @@ -59,6 +57,5 @@ func main() { l.Item(d.String()) } - fmt.Println() - fmt.Println(l) + lipgloss.Print("\n", l, "\n") } diff --git a/examples/list/grocery/main.go b/examples/list/grocery/main.go index 548a8735..218b6223 100644 --- a/examples/list/grocery/main.go +++ b/examples/list/grocery/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" ) @@ -71,5 +69,5 @@ func main() { EnumeratorStyleFunc(enumStyleFunc). ItemStyleFunc(itemStyleFunc) - fmt.Println(l) + lipgloss.Println(l) } diff --git a/examples/list/roman/main.go b/examples/list/roman/main.go index 791b8e8e..e4b82ef3 100644 --- a/examples/list/roman/main.go +++ b/examples/list/roman/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" ) @@ -22,5 +20,5 @@ func main() { EnumeratorStyle(enumeratorStyle). ItemStyle(itemStyle) - fmt.Println(l) + lipgloss.Println(l) } diff --git a/examples/list/simple/main.go b/examples/list/simple/main.go index 3a6da490..4a0c17b3 100644 --- a/examples/list/simple/main.go +++ b/examples/list/simple/main.go @@ -1,8 +1,7 @@ package main import ( - "fmt" - + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" ) @@ -18,5 +17,5 @@ func main() { ).Enumerator(list.Roman), "G", ) - fmt.Println(l) + lipgloss.Println(l) } diff --git a/examples/list/sublist/main.go b/examples/list/sublist/main.go index 3f0a48cd..fd3f7086 100644 --- a/examples/list/sublist/main.go +++ b/examples/list/sublist/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" @@ -10,6 +11,14 @@ import ( ) func main() { + hasDarkBG, err := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not detect background color: %v\n", err) + os.Exit(1) + } + + lightDark := lipgloss.LightDark(hasDarkBG) + purple := lipgloss.NewStyle(). Foreground(lipgloss.Color("99")). MarginRight(1) @@ -27,7 +36,7 @@ func main() { dim := lipgloss.Color("250") highlight := lipgloss.Color("#EE6FF8") - special := lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} + special := lightDark("#43BF6D", "#73F59F") checklistEnumStyle := func(items list.Items, index int) lipgloss.Style { switch index { @@ -54,7 +63,7 @@ func main() { case 1, 2, 4: return lipgloss.NewStyle(). Strikethrough(true). - Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"}) + Foreground(lightDark("#969B86", "#696969")) default: return lipgloss.NewStyle() } @@ -210,7 +219,7 @@ func main() { ). Item("xoxo, Charm_™") - fmt.Println(l) + lipgloss.Println(l) } func colorGrid(xSteps, ySteps int) [][]string { diff --git a/examples/ssh/main.go b/examples/ssh/main.go index 6a6dbf33..fd8383d1 100644 --- a/examples/ssh/main.go +++ b/examples/ssh/main.go @@ -14,11 +14,10 @@ import ( "log" "strings" + "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/adaptive" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" - "github.com/lucasb-eyer/go-colorful" ) // Available styles. @@ -66,7 +65,7 @@ func handler(next ssh.Handler) ssh.Handler { environ := sess.Environ() environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term)) - output := adaptive.NewOutput(pty.Slave, pty.Slave, environ) + output := colorprofile.NewWriter(pty.Slave, environ) width := pty.Window.Width // Initialize new styles against the renderer. @@ -75,7 +74,7 @@ func handler(next ssh.Handler) ssh.Handler { str := strings.Builder{} fmt.Fprintf(&str, "\n\nProfile: %s\n%s %s %s %s %s", - output.ColorProfile().String(), + colorprofile.Detect(pty.Slave, environ), styles.bold, styles.faint, styles.italic, @@ -103,19 +102,32 @@ func handler(next ssh.Handler) ssh.Handler { styles.gray, ) - col, _ := colorful.MakeColor(output.BackgroundColor) - fmt.Fprintf(&str, "%s %t %s\n\n", styles.bold.UnsetString().Render("Has dark background?"), - output.HasDarkBackground(), - col.Hex()) + hasDarkBG, err := lipgloss.HasDarkBackground(pty.Slave, pty.Slave) + if err != nil { + log.Print("Could not detect background color: %w", err) + return + } + + lightDark := lipgloss.LightDark(hasDarkBG) + + fmt.Fprintf(&str, "%s %s\n\n", + styles.bold.UnsetString().Render("Has dark background?"), + func() string { + if hasDarkBG { + return "Yep." + } + return "Nope!" + }(), + ) block := lipgloss.Place(width, lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(), lipgloss.WithWhitespaceChars("/"), - lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(output.AdaptiveColor(lipgloss.Color(250), lipgloss.Color(236)))), + lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(lightDark(250, 236))), ) // Render to client. - output.Println(block) + output.WriteString(block) next(sess) } diff --git a/examples/table/ansi/main.go b/examples/table/ansi/main.go index 66d457d5..092bed32 100644 --- a/examples/table/ansi/main.go +++ b/examples/table/ansi/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" ) @@ -14,5 +12,5 @@ func main() { t.Row("Bubble Tea", s("Milky")) t.Row("Milk Tea", s("Also milky")) t.Row("Actual milk", s("Milky as well")) - fmt.Println(t.Render()) + lipgloss.Println(t.Render()) } diff --git a/examples/table/chess/main.go b/examples/table/chess/main.go index a69a9bc4..d49025d4 100644 --- a/examples/table/chess/main.go +++ b/examples/table/chess/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "os" "strings" "github.com/charmbracelet/lipgloss" @@ -10,8 +8,7 @@ import ( ) func main() { - re := lipgloss.NewRenderer(os.Stdout) - labelStyle := re.NewStyle().Foreground(lipgloss.Color("241")) + labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")) board := [][]string{ {"♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜"}, @@ -36,5 +33,11 @@ func main() { ranks := labelStyle.Render(strings.Join([]string{" A", "B", "C", "D", "E", "F", "G", "H "}, " ")) files := labelStyle.Render(strings.Join([]string{" 1", "2", "3", "4", "5", "6", "7", "8 "}, "\n\n ")) - fmt.Println(lipgloss.JoinVertical(lipgloss.Right, lipgloss.JoinHorizontal(lipgloss.Center, files, t.Render()), ranks) + "\n") + lipgloss.Println( + lipgloss.JoinVertical( + lipgloss.Right, + lipgloss.JoinHorizontal(lipgloss.Center, files, t.Render()), + ranks, + ) + "\n", + ) } diff --git a/examples/table/languages/main.go b/examples/table/languages/main.go index aa8a10cf..821ff87d 100644 --- a/examples/table/languages/main.go +++ b/examples/table/languages/main.go @@ -1,33 +1,28 @@ package main import ( - "fmt" - "os" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" ) const ( - purple = lipgloss.Color("99") - gray = lipgloss.Color("245") - lightGray = lipgloss.Color("241") + purple = "99" + gray = "245" + lightGray = "241" ) func main() { - re := lipgloss.NewRenderer(os.Stdout) - var ( // HeaderStyle is the lipgloss style used for the table headers. - HeaderStyle = re.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center) + HeaderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(purple)).Bold(true).Align(lipgloss.Center) // CellStyle is the base lipgloss style used for the table rows. - CellStyle = re.NewStyle().Padding(0, 1).Width(14) + CellStyle = lipgloss.NewStyle().Padding(0, 1).Width(14) // OddRowStyle is the lipgloss style used for odd-numbered table rows. - OddRowStyle = CellStyle.Foreground(gray) + OddRowStyle = CellStyle.Foreground(lipgloss.Color(gray)) // EvenRowStyle is the lipgloss style used for even-numbered table rows. - EvenRowStyle = CellStyle.Foreground(lightGray) + EvenRowStyle = CellStyle.Foreground(lipgloss.Color(lightGray)) // BorderStyle is the lipgloss style used for the table border. - BorderStyle = lipgloss.NewStyle().Foreground(purple) + BorderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(purple)) ) rows := [][]string{ @@ -70,5 +65,5 @@ func main() { t.Row("English", "You look absolutely fabulous.", "How's it going?") - fmt.Println(t) + lipgloss.Println(t) } diff --git a/examples/table/mindy/main.go b/examples/table/mindy/main.go index 9f97c9af..757456d0 100644 --- a/examples/table/mindy/main.go +++ b/examples/table/mindy/main.go @@ -2,16 +2,14 @@ package main import ( "fmt" - "os" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" ) func main() { - re := lipgloss.NewRenderer(os.Stdout) - labelStyle := re.NewStyle().Width(3).Align(lipgloss.Right) - swatchStyle := re.NewStyle().Width(6) + labelStyle := lipgloss.NewStyle().Width(3).Align(lipgloss.Right) + swatchStyle := lipgloss.NewStyle().Width(6) data := [][]string{} for i := 0; i < 13; i += 8 { @@ -43,7 +41,7 @@ func main() { } }) - fmt.Println(t) + lipgloss.Println(t) } const rowLength = 12 diff --git a/examples/table/pokemon/main.go b/examples/table/pokemon/main.go index ff3d7bad..8b74878e 100644 --- a/examples/table/pokemon/main.go +++ b/examples/table/pokemon/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "os" + "image/color" "strings" "github.com/charmbracelet/lipgloss" @@ -10,11 +10,10 @@ import ( ) func main() { - re := lipgloss.NewRenderer(os.Stdout) - baseStyle := re.NewStyle().Padding(0, 1) + baseStyle := lipgloss.NewStyle().Padding(0, 1) headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true) selectedStyle := baseStyle.Foreground(lipgloss.Color("#01BE85")).Background(lipgloss.Color("#00432F")) - typeColors := map[string]lipgloss.Color{ + typeColors := map[string]color.Color{ "Bug": lipgloss.Color("#D7FF87"), "Electric": lipgloss.Color("#FDFF90"), "Fire": lipgloss.Color("#FF7698"), @@ -25,7 +24,7 @@ func main() { "Poison": lipgloss.Color("#7D5AFC"), "Water": lipgloss.Color("#00E2C7"), } - dimTypeColors := map[string]lipgloss.Color{ + dimTypeColors := map[string]color.Color{ "Bug": lipgloss.Color("#97AD64"), "Electric": lipgloss.Color("#FCFF5F"), "Fire": lipgloss.Color("#BA5F75"), @@ -78,7 +77,7 @@ func main() { t := table.New(). Border(lipgloss.NormalBorder()). - BorderStyle(re.NewStyle().Foreground(lipgloss.Color("238"))). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))). Headers(CapitalizeHeaders(headers)...). Width(80). Rows(data...). @@ -109,5 +108,6 @@ func main() { } return baseStyle.Foreground(lipgloss.Color("252")) }) - fmt.Println(t) + + lipgloss.Println(t) } diff --git a/examples/tree/background/main.go b/examples/tree/background/main.go index a38ca626..e87cfb6f 100644 --- a/examples/tree/background/main.go +++ b/examples/tree/background/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" ) @@ -35,5 +33,5 @@ func main() { Child("Chapter 2.2"), ) - fmt.Println(t) + lipgloss.Println(t) } diff --git a/examples/tree/files/main.go b/examples/tree/files/main.go index 4435c5d1..58d40ca5 100644 --- a/examples/tree/files/main.go +++ b/examples/tree/files/main.go @@ -68,5 +68,5 @@ func main() { os.Exit(1) } - fmt.Println(t) + lipgloss.Println(t) } diff --git a/examples/tree/makeup/main.go b/examples/tree/makeup/main.go index b6dca57d..0da00394 100644 --- a/examples/tree/makeup/main.go +++ b/examples/tree/makeup/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" ) @@ -30,5 +28,5 @@ func main() { RootStyle(rootStyle). ItemStyle(itemStyle) - fmt.Println(t) + lipgloss.Println(t) } diff --git a/examples/tree/rounded/main.go b/examples/tree/rounded/main.go index c32dae3a..10f8f4b5 100644 --- a/examples/tree/rounded/main.go +++ b/examples/tree/rounded/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" ) @@ -33,5 +31,5 @@ func main() { ), ).ItemStyle(itemStyle).EnumeratorStyle(enumeratorStyle).Enumerator(tree.RoundedEnumerator) - fmt.Println(t) + lipgloss.Println(t) } diff --git a/examples/tree/simple/main.go b/examples/tree/simple/main.go index 3969f156..2cf8c21e 100644 --- a/examples/tree/simple/main.go +++ b/examples/tree/simple/main.go @@ -1,8 +1,7 @@ package main import ( - "fmt" - + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" ) @@ -23,5 +22,5 @@ func main() { Child("OpenBSD"), ) - fmt.Println(t) + lipgloss.Println(t) } diff --git a/examples/tree/styles/main.go b/examples/tree/styles/main.go index 9950b1f4..8859c8ff 100644 --- a/examples/tree/styles/main.go +++ b/examples/tree/styles/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" ) @@ -22,5 +20,6 @@ func main() { "Milk", ). EnumeratorStyle(purple) - fmt.Println(t) + + lipgloss.Println(t) } diff --git a/examples/tree/toggle/main.go b/examples/tree/toggle/main.go index 0664e98f..255b4711 100644 --- a/examples/tree/toggle/main.go +++ b/examples/tree/toggle/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" ) @@ -88,5 +86,5 @@ func main() { dir{"maas", false, s}, ) - fmt.Println(s.block.Render(t.String())) + lipgloss.Println(s.block.Render(t.String())) } diff --git a/go.mod b/go.mod index 6ca85aed..9d3919c7 100644 --- a/go.mod +++ b/go.mod @@ -8,16 +8,18 @@ go 1.18 require ( github.com/aymanbagabas/go-udiff v0.2.0 + github.com/charmbracelet/colorprofile v0.1.2 github.com/charmbracelet/x/ansi v0.3.2 github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a + github.com/charmbracelet/x/input v0.2.0 + github.com/charmbracelet/x/term v0.2.0 github.com/lucasb-eyer/go-colorful v1.2.0 - github.com/muesli/termenv v0.15.2 github.com/rivo/uniseg v0.4.7 - golang.org/x/sys v0.19.0 + golang.org/x/sys v0.24.0 ) require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/muesli/cancelreader v0.2.2 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) diff --git a/go.sum b/go.sum index e03a2524..cd526952 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,26 @@ -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/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/colorprofile v0.1.2 h1:nuB1bd/yAExT4fkcZvpqtQ2N5/8cJHSRIKb6CzT7lAM= +github.com/charmbracelet/colorprofile v0.1.2/go.mod h1:1htIKZYeI4TQs+OykPvpuBTUbUJxBYeSYBDIZuejMj0= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/input v0.2.0 h1:1Sv+y/flcqUfUH2PXNIDKDIdT2G8smOnGOgawqhwy8A= +github.com/charmbracelet/x/input v0.2.0/go.mod h1:KUSFIS6uQymtnr5lHVSOK9j8RvwTD4YHnWnzJUYnd/M= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +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-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/list/list_test.go b/list/list_test.go index b5a0a947..8a20444a 100644 --- a/list/list_test.go +++ b/list/list_test.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/list" "github.com/charmbracelet/lipgloss/tree" + "github.com/charmbracelet/x/ansi" ) // XXX: can't write multi-line examples if the underlying string uses @@ -194,7 +195,7 @@ func TestComplexSublist(t *testing.T) { C. bar • Baz ` - assertEqual(t, expected, l.String()) + assertEqual(t, expected, ansi.Strip(l.String())) } func TestMultiline(t *testing.T) { diff --git a/query.go b/query.go new file mode 100644 index 00000000..3dd68099 --- /dev/null +++ b/query.go @@ -0,0 +1,60 @@ +package lipgloss + +import ( + "errors" + "fmt" + "image/color" + "os" + + "github.com/charmbracelet/x/term" +) + +// BackgroundColor queries the terminal's background color. Typically, you'll +// want to query against stdin and either stdout or stderr, depending on what +// you're writing to. +// +// This function is intended for standalone Lip Gloss use only. If you're using +// Bubble Tea, listen for tea.BackgroundColorMsg in your update function. +func BackgroundColor(in *os.File, out *os.File) (color.Color, error) { + state, err := term.MakeRaw(in.Fd()) + if err != nil { + return nil, fmt.Errorf("error setting raw state to detect background color: %w", err) + } + + defer term.Restore(in.Fd(), state) //nolint:errcheck + + bg, err := queryBackgroundColor(in, out) + if err != nil { + return nil, err + } + + return bg, nil +} + +// HasDarkBackground detects whether the terminal has a light or dark +// background. It's a convenience function that wraps [BackgroundColor] and +// [lipgloss.IsDarkColor]. +// +// Typically, you'll want to query against stdin and either stdout or stderr +// depending on what you're writing to. +// +// hasDarkBG, _ := HasDarkBackground(os.Stdin, os.Stdout) +// adaptive := Adapt(hasDarkBG) +// myHotColor := Color("#ff0000", "#0000ff") +// +// This is intedded for use in standalone Lip Gloss only. In Bubble Tea, listen +// for tea.BackgroundColorMsg in your update function. +// +// case tea.BackgroundColorMsg: +// hasDarkBackground = msg.IsDark() +func HasDarkBackground(in *os.File, out *os.File) (bool, error) { + bg, err := BackgroundColor(in, out) + if err != nil { + return true, fmt.Errorf("could not detect background color: %w", err) + } + if bg == nil { + return true, errors.New("detected background color is nil") + } + + return IsDarkColor(bg), nil +} diff --git a/runes_test.go b/runes_test.go index be36150d..bab31a8a 100644 --- a/runes_test.go +++ b/runes_test.go @@ -1,7 +1,6 @@ package lipgloss import ( - "strings" "testing" ) @@ -55,14 +54,10 @@ func TestStyleRunes(t *testing.T) { t.Run(tc.name, func(t *testing.T) { res := fn(tc.input, tc.indices) if res != tc.expected { - t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual Output:\n\n`%s`\n`%s`\n\n", - tc.expected, formatEscapes(tc.expected), - res, formatEscapes(res)) + t.Errorf("Expected:\n\n`%q`\n`%q`\n\nActual Output:\n\n`%q`\n`%q`\n\n", + tc.expected, tc.expected, + res, res) } }) } } - -func formatEscapes(str string) string { - return strings.ReplaceAll(str, "\x1b", "\\x1b") -} diff --git a/style_test.go b/style_test.go index 3940f982..c660b907 100644 --- a/style_test.go +++ b/style_test.go @@ -16,19 +16,19 @@ func TestUnderline(t *testing.T) { }{ { NewStyle().Underline(true), - "\x1b[4;4ma\x1b[0m\x1b[4;4mb\x1b[0m\x1b[4m \x1b[0m\x1b[4;4mc\x1b[0m", + "\x1b[4;4ma\x1b[m\x1b[4;4mb\x1b[m\x1b[4m \x1b[m\x1b[4;4mc\x1b[m", }, { NewStyle().Underline(true).UnderlineSpaces(true), - "\x1b[4;4ma\x1b[0m\x1b[4;4mb\x1b[0m\x1b[4m \x1b[0m\x1b[4;4mc\x1b[0m", + "\x1b[4;4ma\x1b[m\x1b[4;4mb\x1b[m\x1b[4m \x1b[m\x1b[4;4mc\x1b[m", }, { NewStyle().Underline(true).UnderlineSpaces(false), - "\x1b[4;4ma\x1b[0m\x1b[4;4mb\x1b[0m \x1b[4;4mc\x1b[0m", + "\x1b[4;4ma\x1b[m\x1b[4;4mb\x1b[m \x1b[4;4mc\x1b[m", }, { NewStyle().UnderlineSpaces(true), - "ab\x1b[4m \x1b[0mc", + "ab\x1b[4m \x1b[mc", }, } @@ -36,9 +36,9 @@ func TestUnderline(t *testing.T) { s := tc.style.SetString("ab c") res := s.Render() if res != tc.expected { - t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n", - i, tc.expected, formatEscapes(tc.expected), - res, formatEscapes(res)) + t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", + i, tc.expected, + res) } } } @@ -52,19 +52,19 @@ func TestStrikethrough(t *testing.T) { }{ { NewStyle().Strikethrough(true), - "\x1b[9ma\x1b[0m\x1b[9mb\x1b[0m\x1b[9m \x1b[0m\x1b[9mc\x1b[0m", + "\x1b[9ma\x1b[m\x1b[9mb\x1b[m\x1b[9m \x1b[m\x1b[9mc\x1b[m", }, { NewStyle().Strikethrough(true).StrikethroughSpaces(true), - "\x1b[9ma\x1b[0m\x1b[9mb\x1b[0m\x1b[9m \x1b[0m\x1b[9mc\x1b[0m", + "\x1b[9ma\x1b[m\x1b[9mb\x1b[m\x1b[9m \x1b[m\x1b[9mc\x1b[m", }, { NewStyle().Strikethrough(true).StrikethroughSpaces(false), - "\x1b[9ma\x1b[0m\x1b[9mb\x1b[0m \x1b[9mc\x1b[0m", + "\x1b[9ma\x1b[m\x1b[9mb\x1b[m \x1b[9mc\x1b[m", }, { NewStyle().StrikethroughSpaces(true), - "ab\x1b[9m \x1b[0mc", + "ab\x1b[9m \x1b[mc", }, } @@ -72,9 +72,9 @@ func TestStrikethrough(t *testing.T) { s := tc.style.SetString("ab c") res := s.Render() if res != tc.expected { - t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n", - i, tc.expected, formatEscapes(tc.expected), - res, formatEscapes(res)) + t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", + i, tc.expected, + res) } } } @@ -88,11 +88,7 @@ func TestStyleRender(t *testing.T) { }{ { NewStyle().Foreground(Color("#5A56E0")), - "\x1b[38;2;89;86;224mhello\x1b[m", - }, - { - NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), - "\x1b[38;2;89;86;224mhello\x1b[m", + "\x1b[38;2;90;86;224mhello\x1b[m", }, { NewStyle().Bold(true), @@ -120,59 +116,9 @@ func TestStyleRender(t *testing.T) { s := tc.style.SetString("hello") res := s.Render() if res != tc.expected { - t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n", - i, tc.expected, formatEscapes(tc.expected), - res, formatEscapes(res)) - } - } -} - -func TestStyleCustomRender(t *testing.T) { - tt := []struct { - style Style - expected string - }{ - { - NewStyle().Foreground(Color("#5A56E0")), - "\x1b[38;2;89;86;224mhello\x1b[m", - }, - { - NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), - "\x1b[38;2;255;254;18mhello\x1b[m", - }, - { - NewStyle().Bold(true), - "\x1b[1mhello\x1b[m", - }, - { - NewStyle().Italic(true), - "\x1b[3mhello\x1b[m", - }, - { - NewStyle().Underline(true), - "\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", - }, - { - NewStyle().Blink(true), - "\x1b[5mhello\x1b[m", - }, - { - NewStyle().Faint(true), - "\x1b[2mhello\x1b[m", - }, - { - NewStyle().Faint(true), - "\x1b[2mhello\x1b[m", - }, - } - - for i, tc := range tt { - s := tc.style.SetString("hello") - res := s.Render() - if res != tc.expected { - t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n", - i, tc.expected, formatEscapes(tc.expected), - res, formatEscapes(res)) + t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", + i, tc.expected, + res) } } } @@ -455,9 +401,9 @@ func TestStyleValue(t *testing.T) { for i, tc := range tt { res := tc.style.Render(tc.text) if res != tc.expected { - t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n", - i, tc.expected, formatEscapes(tc.expected), - res, formatEscapes(res)) + t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", + i, tc.expected, + res) } } } diff --git a/table/table_test.go b/table/table_test.go index 09175eb2..55809a56 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -5,6 +5,7 @@ import ( "testing" "unicode" + "github.com/aymanbagabas/go-udiff" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" @@ -1344,3 +1345,28 @@ func stripString(str string) string { return strings.Join(lines, "\n") } + +// assertEqual verifies the strings are equal, assuming its terminal output. +func assertEqual(tb testing.TB, want, got string) { + tb.Helper() + + want = trimSpace(want) + got = trimSpace(got) + + diff := udiff.Unified("want", "got", want, got) + if diff != "" { + tb.Fatalf("\nwant:\n\n%s\n\ngot:\n\n%s\n\ndiff:\n\n%s\n\n", want, got, diff) + } +} + +func trimSpace(s string) string { + var result []string //nolint: prealloc + ss := strings.Split(s, "\n") + for i, line := range ss { + if strings.TrimSpace(line) == "" && (i == 0 || i == len(ss)-1) { + continue + } + result = append(result, strings.TrimRightFunc(line, unicode.IsSpace)) + } + return strings.Join(result, "\n") +} diff --git a/terminal.go b/terminal.go new file mode 100644 index 00000000..3ddf8a87 --- /dev/null +++ b/terminal.go @@ -0,0 +1,96 @@ +package lipgloss + +import ( + "fmt" + "image/color" + "io" + "time" + + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/input" +) + +// queryBackgroundColor queries the terminal for the background color. +// If the terminal does not support querying the background color, nil is +// returned. +// +// Note: you will need to set the input to raw mode before calling this +// function. +// +// state, _ := term.MakeRaw(in.Fd()) +// defer term.Restore(in.Fd(), state) +// +// copied from x/term@v0.1.3. +func queryBackgroundColor(in io.Reader, out io.Writer) (c color.Color, err error) { + // nolint: errcheck + err = queryTerminal(in, out, defaultQueryTimeout, + func(events []input.Event) bool { + for _, e := range events { + switch e := e.(type) { + case input.BackgroundColorEvent: + c = e.Color + continue // we need to consume the next DA1 event + case input.PrimaryDeviceAttributesEvent: + return false + } + } + return true + }, ansi.RequestBackgroundColor+ansi.RequestPrimaryDeviceAttributes) + return +} + +const defaultQueryTimeout = time.Second * 2 + +// queryTerminalFilter is a function that filters input events using a type +// switch. If false is returned, the QueryTerminal function will stop reading +// input. +type queryTerminalFilter func(events []input.Event) bool + +// queryTerminal queries the terminal for support of various features and +// returns a list of response events. +// Most of the time, you will need to set stdin to raw mode before calling this +// function. +// Note: This function will block until the terminal responds or the timeout +// is reached. +// copied from x/term@v0.1.3. +func queryTerminal( + in io.Reader, + out io.Writer, + timeout time.Duration, + filter queryTerminalFilter, + query string, +) error { + rd, err := input.NewDriver(in, "", 0) + if err != nil { + return fmt.Errorf("could not create driver: %w", err) + } + + defer rd.Close() //nolint: errcheck + + done := make(chan struct{}, 1) + defer close(done) + go func() { + select { + case <-done: + case <-time.After(timeout): + rd.Cancel() + } + }() + + if _, err := io.WriteString(out, query); err != nil { + return fmt.Errorf("could not write query: %w", err) + } + + for { + events, err := rd.ReadEvents() + if err != nil { + return err + } + + if !filter(events) { + break + } + } + + return nil +} diff --git a/tree/testdata/TestRootStyle.golden b/tree/testdata/TestRootStyle.golden index feacf4cd..f6b11fc5 100644 --- a/tree/testdata/TestRootStyle.golden +++ b/tree/testdata/TestRootStyle.golden @@ -1,3 +1,3 @@ -Root -├── Foo -└── Baz \ No newline at end of file +Root +├── Foo +└── Baz \ No newline at end of file diff --git a/tree/tree_test.go b/tree/tree_test.go index 3ee1fd34..3f4cca3f 100644 --- a/tree/tree_test.go +++ b/tree/tree_test.go @@ -10,8 +10,8 @@ import ( "github.com/charmbracelet/lipgloss/list" "github.com/charmbracelet/lipgloss/table" "github.com/charmbracelet/lipgloss/tree" + "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" - "github.com/muesli/termenv" ) func TestTree(t *testing.T) { @@ -272,7 +272,7 @@ func TestTreeCustom(t *testing.T) { -> -> Quuux -> Baz ` - assertEqual(t, want, tree.String()) + assertEqual(t, want, ansi.Strip(tree.String())) } func TestTreeMultilineNode(t *testing.T) { @@ -413,7 +413,6 @@ Root } func TestRootStyle(t *testing.T) { - lipgloss.SetColorProfile(termenv.TrueColor) tree := tree.New(). Root("Root"). Child( @@ -423,7 +422,7 @@ func TestRootStyle(t *testing.T) { RootStyle(lipgloss.NewStyle().Background(lipgloss.Color("#5A56E0"))). ItemStyle(lipgloss.NewStyle().Background(lipgloss.Color("#04B575"))) - golden.RequireEqual(t, []byte(tree.String())) + golden.RequireEqual(t, []byte(ansi.Strip(tree.String()))) } func TestAt(t *testing.T) { diff --git a/writer.go b/writer.go new file mode 100644 index 00000000..ba3e25ca --- /dev/null +++ b/writer.go @@ -0,0 +1,96 @@ +package lipgloss + +import ( + "fmt" + "io" + "os" + + "github.com/charmbracelet/colorprofile" +) + +// Writer is the default writer that prints to stdout, automatically +// downsampling colors when necessary. +var Writer = colorprofile.NewWriter(os.Stdout, os.Environ()) + +// Println to stdout, automatically downsampling colors when necessary, ending +// with a trailing newline. +// +// Example: +// +// str := NewStyle(). +// Foreground(lipgloss.Color("#6a00ff")). +// Render("breakfast") +// +// Println("Time for a", str, "sandwich!") +func Println(v ...interface{}) (int, error) { + return fmt.Fprintln(Writer, v...) //nolint:wrapcheck +} + +// Print formatted text to stdout, automatically downsampling colors when +// necessary. +// +// Example: +// +// str := NewStyle(). +// Foreground(lipgloss.Color("#6a00ff")). +// Render("knuckle") +// +// Printf("Time for a %s sandwich!\n", str) +func Printf(format string, v ...interface{}) (int, error) { + return fmt.Fprintf(Writer, format, v...) //nolint:wrapcheck +} + +// Print to stdout, automatically downsampling colors when necessary. +// +// Example: +// +// str := NewStyle(). +// Foreground(lipgloss.Color("#6a00ff")). +// Render("Who wants marmalade?\n") +// +// Print(str) +func Print(v ...interface{}) (int, error) { + return fmt.Fprint(Writer, v...) //nolint:wrapcheck +} + +// Fprint pritnts to the given writer, automatically downsampling colors when +// necessary. +// +// Example: +// +// str := NewStyle(). +// Foreground(lipgloss.Color("#6a00ff")). +// Render("guzzle") +// +// Fprint(os.Stderr, "I %s horchata pretty much all the time.\n", str) +func Fprint(w io.Writer, v ...interface{}) (int, error) { + return fmt.Fprint(colorprofile.NewWriter(w, os.Environ()), v...) //nolint:wrapcheck +} + +// Fprint pritnts to the given writer, automatically downsampling colors when +// necessary, and ending with a trailing newline. +// +// Example: +// +// str := NewStyle(). +// Foreground(lipgloss.Color("#6a00ff")). +// Render("Sandwich time!") +// +// Fprintln(os.Stderr, str) +func Fprintln(w io.Writer, v ...interface{}) (int, error) { + return fmt.Fprintln(colorprofile.NewWriter(w, os.Environ()), v...) //nolint:wrapcheck +} + +// Fprintf prints text to a writer, against the given format, automatically +// downsampling colors when necessary. +// +// Example: +// +// str := NewStyle(). +// Foreground(lipgloss.Color("#6a00ff")). +// Render("artichokes") +// +// Fprintf(os.Stderr, "I really love %s!\n", food) +func Fprintf(w io.Writer, format string, v ...interface{}) (int, error) { + return fmt.Fprintf(colorprofile.NewWriter(w, os.Environ()), format, v...) //nolint:wrapcheck +}