diff --git a/compat/color.go b/compat/color.go new file mode 100644 index 00000000..1cc67a5b --- /dev/null +++ b/compat/color.go @@ -0,0 +1,80 @@ +package compat + +import ( + "image/color" + "os" + + "github.com/charmbracelet/colorprofile" + "github.com/charmbracelet/lipgloss/v2" +) + +var ( + // HasDarkBackground is true if the terminal has a dark background. + HasDarkBackground = func() bool { + hdb, _ := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + return hdb + }() + + // Profile is the color profile of the terminal. + Profile = colorprofile.Detect(os.Stdout, os.Environ()) +) + +// AdaptiveColor provides color options for light and dark backgrounds. The +// appropriate color will be returned at runtime based on the darkness of the +// terminal background color. +// +// Example usage: +// +// color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} +type AdaptiveColor struct { + Light color.Color + Dark color.Color +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. +func (c AdaptiveColor) RGBA() (uint32, uint32, uint32, uint32) { + if HasDarkBackground { + return c.Dark.RGBA() + } + return c.Light.RGBA() +} + +// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color +// profiles. Automatic color degradation will not be performed. +type CompleteColor struct { + TrueColor color.Color + ANSI256 color.Color + ANSI color.Color +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. +func (c CompleteColor) RGBA() (uint32, uint32, uint32, uint32) { + switch Profile { + case colorprofile.TrueColor: + return c.TrueColor.RGBA() + case colorprofile.ANSI256: + return c.ANSI256.RGBA() + case colorprofile.ANSI: + return c.ANSI.RGBA() + } + return lipgloss.NoColor{}.RGBA() +} + +// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color +// profiles, with separate options for light and dark backgrounds. Automatic +// color degradation will not be performed. +type CompleteAdaptiveColor struct { + Light CompleteColor + Dark CompleteColor +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. +func (c CompleteAdaptiveColor) RGBA() (uint32, uint32, uint32, uint32) { + if HasDarkBackground { + return c.Dark.RGBA() + } + return c.Light.RGBA() +} diff --git a/compat/doc.go b/compat/doc.go new file mode 100644 index 00000000..f2af50d0 --- /dev/null +++ b/compat/doc.go @@ -0,0 +1,21 @@ +// Package compat is a compatibility layer for Lip Gloss that provides a way to +// deal with the hassle of setting up a writer. It's impure because it uses +// global variables, is not thread-safe, and only works with the default +// standard I/O streams. +// +// In case you want [os.Stderr] to be used as the default writer, you can set +// both [Writer] and [HasDarkBackground] to use [os.Stderr] with +// the following code: +// +// import ( +// "os" +// +// "github.com/charmbracelet/colorprofile" +// "github.com/charmbracelet/lipgloss/v2/impure" +// ) +// +// func init() { +// impure.Writer = colorprofile.NewWriter(os.Stderr, os.Environ()) +// impure.HasDarkBackground, _ = lipgloss.HasDarkBackground(os.Stdin, os.Stderr) +// } +package compat diff --git a/examples/color/bubbletea/main.go b/examples/color/bubbletea/main.go index d09368f2..27e10e18 100644 --- a/examples/color/bubbletea/main.go +++ b/examples/color/bubbletea/main.go @@ -5,7 +5,7 @@ import ( "os" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/v2" ) // Style definitions. diff --git a/examples/color/standalone/main.go b/examples/color/standalone/main.go index b71c4308..d2641aa6 100644 --- a/examples/color/standalone/main.go +++ b/examples/color/standalone/main.go @@ -10,7 +10,7 @@ import ( "fmt" "os" - "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/v2" ) func main() { diff --git a/examples/compat/bubbletea/main.go b/examples/compat/bubbletea/main.go new file mode 100644 index 00000000..6cfc2044 --- /dev/null +++ b/examples/compat/bubbletea/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/lipgloss/v2/compat" +) + +var ( + frameColor = compat.AdaptiveColor{Light: lipgloss.Color("#C5ADF9"), Dark: lipgloss.Color("#864EFF")} + textColor = compat.AdaptiveColor{Light: lipgloss.Color("#696969"), Dark: lipgloss.Color("#bdbdbd")} + keywordColor = compat.AdaptiveColor{Light: lipgloss.Color("#37CD96"), Dark: lipgloss.Color("#22C78A")} + inactiveBgColor = compat.AdaptiveColor{Light: lipgloss.Color(0x988F95), Dark: lipgloss.Color(0x978692)} + inactiveFgColor = compat.AdaptiveColor{Light: lipgloss.Color(0xFDFCE3), Dark: lipgloss.Color(0xFBFAE7)} +) + +// 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() (s styles) { + // 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(frameColor). + Padding(1, 3). + Margin(1, 3) + s.paragraph = lipgloss.NewStyle(). + Width(40). + MarginBottom(1). + Align(lipgloss.Center) + s.text = lipgloss.NewStyle(). + Foreground(textColor) + s.keyword = lipgloss.NewStyle(). + Foreground(keywordColor). + 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(inactiveBgColor). + Foreground(inactiveFgColor) + return s +} + +type model struct { + styles styles + yes bool + chosen bool + aborted bool +} + +func (m model) Init() (tea.Model, tea.Cmd) { + m.yes = true + m.styles = newStyles() + return m, nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + 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.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/compat/standalone/main.go b/examples/compat/standalone/main.go new file mode 100644 index 00000000..92808906 --- /dev/null +++ b/examples/compat/standalone/main.go @@ -0,0 +1,66 @@ +// 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 ( + "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/lipgloss/v2/compat" +) + +var ( + frameColor = compat.AdaptiveColor{Light: lipgloss.Color("#C5ADF9"), Dark: lipgloss.Color("#864EFF")} + textColor = compat.AdaptiveColor{Light: lipgloss.Color("#696969"), Dark: lipgloss.Color("#bdbdbd")} + keywordColor = compat.AdaptiveColor{Light: lipgloss.Color("#37CD96"), Dark: lipgloss.Color("#22C78A")} + inactiveBgColor = compat.AdaptiveColor{Light: lipgloss.Color(0x988F95), Dark: lipgloss.Color(0x978692)} + inactiveFgColor = compat.AdaptiveColor{Light: lipgloss.Color(0xFDFCE3), Dark: lipgloss.Color(0xFBFAE7)} +) + +func main() { + // 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(frameColor). + Padding(1, 3). + Margin(1, 3) + paragraphStyle := lipgloss.NewStyle(). + Width(40). + MarginBottom(1). + Align(lipgloss.Center) + textStyle := lipgloss.NewStyle(). + Foreground(textColor) + keywordStyle := lipgloss.NewStyle(). + Foreground(keywordColor). + 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(inactiveBgColor). + Foreground(inactiveFgColor) + + // 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 61d1e6cf..2b7917e4 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -8,10 +8,11 @@ replace github.com/charmbracelet/lipgloss/v2/list => ../list replace github.com/charmbracelet/lipgloss/v2/table => ../table +replace github.com/charmbracelet/lipgloss/v2/compat => ../compat + require ( github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1.0.20241031200731-4f70d4c680b8 github.com/charmbracelet/colorprofile v0.1.6 - github.com/charmbracelet/lipgloss v0.13.1-0.20240822211938-b89f1a3db2a4 github.com/charmbracelet/lipgloss/v2 v2.0.0-20241101153040-904e60506df7 github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 github.com/charmbracelet/wish v1.4.0 @@ -25,6 +26,7 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/keygen v0.5.0 // indirect + github.com/charmbracelet/lipgloss v0.13.1-0.20240822211938-b89f1a3db2a4 // indirect github.com/charmbracelet/log v0.4.0 // indirect github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect diff --git a/examples/layout/main.go b/examples/layout/main.go index 02e12ddb..7ef8ce85 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -8,7 +8,7 @@ import ( "os" "strings" - "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/term" "github.com/lucasb-eyer/go-colorful" "github.com/rivo/uniseg" @@ -50,7 +50,6 @@ func init() { } func main() { - // Style definitions. var ( @@ -427,7 +426,7 @@ func applyGradient(base lipgloss.Style, input string, from, to color.Color) stri // bytes. The rune count would get us closer but there are times, like with // emojis, where the rune count is greater than the number of actual // characters. - var g = uniseg.NewGraphemes(input) + g := uniseg.NewGraphemes(input) var chars []string for g.Next() { chars = append(chars, g.Str()) diff --git a/examples/table/ansi/main.go b/examples/table/ansi/main.go index 9ed8138e..c1c35faf 100644 --- a/examples/table/ansi/main.go +++ b/examples/table/ansi/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2/table" )