Do you think you can handle Lip Gloss v2?
We’re really excited for you to try Lip Gloss v2! Read on for new features and a guide to upgrading.
If you (or your LLM) just want the technical details, take a look at Upgrade Guide.
Note
We take API changes seriously and strive to make the upgrade process as simple as possible. We believe the changes bring necessary improvements as well as pave the way for the future. If something feels way off, let us know.
What’s new?
The big changes are that Styles are now deterministic (λipgloss!) and you can be much more intentional with your inputs and outputs. Why does this matter?
Playing nicely with others
v2 gives you precise control over I/O. One of the issues we saw with the Lip Gloss and Bubble Tea v1s is that they could fight over the same inputs and outputs, producing lock-ups. The v2s now operate in lockstep.
Querying the right inputs and outputs
In v1, Lip Gloss defaulted to looking at stdin and stdout when downsampling colors and querying for the background color. This was not always necessarily what you wanted. For example, if your application was writing to stderr while redirecting stdout to a file, the program would erroneously think output was not a TTY and strip colors. Lip Gloss v2 gives you control over this.
Going beyond localhost
Did you know TUIs and CLIs can be served over the network? For example, Wish allows you to serve Bubble Tea and Lip Gloss over SSH. In these cases, you need to work with the input and output of the connected clients as opposed to stdin and stdout, which belong to the server. Lip Gloss v2 gives you flexibility around this in a more natural way.
🧋 Using Lip Gloss with Bubble Tea?
Make sure you get all the latest v2s as they’ve been designed to work together.
# Collect the whole set.
go get charm.land/bubbletea/v2
go get charm.land/bubbles/v2
go get charm.land/lipgloss/v2🐇 Quick upgrade
If you don't have time for changes and just want to upgrade to Lip Gloss v2 as fast as possible? Here’s a quick guide:
Use the compat package
The compat package provides adaptive colors, complete colors, and complete adaptive colors:
import "charm.land/lipgloss/v2/compat"
// Before
color := lipgloss.AdaptiveColor{Light: "#f1f1f1", Dark: "#cccccc"}
// After
color := compat.AdaptiveColor{Light: lipgloss.Color("#f1f1f1"), Dark: lipgloss.Color("#cccccc")}compat works by looking at stdin and stdout on a global basis. Want to change the inputs and outputs? Knock yourself out:
import (
"charm.land/lipgloss/v2/compat"
"github.com/charmbracelet/colorprofile"
)
func init() {
// Let’s use stderr instead of stdout.
compat.HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
compat.Profile = colorprofile.Detect(os.Stderr, os.Environ())
}Use the new Lip Gloss writer
If you’re using Bubble Tea with Lip Gloss you can skip this step. If you're using Lip Gloss in a standalone fashion, however, you'll want to use lipgloss.Println (and lipgloss.Printf and so on) when printing your output:
s := someStyle.Render("Fancy Lip Gloss Output")
// Before
fmt.Println(s)
// After
lipgloss.Println(s)Why? Because lipgloss.Println will automatically downsample colors based on the environment.
That’s it!
Yep, you’re done. All this said, we encourage you to read on to get the full benefit of v2.
👀 What’s changing?
Only a couple main things that are changing in Lip Gloss v2:
- Color downsampling in non-Bubble-Tea uses cases is now a manual proccess (don't worry, it's easy)
- Background color detection and adaptive colors are manual, and intentional (but optional)
🪄 Downsampling colors with a writer
One of the best things about Lip Gloss is that it can automatically downsample colors to the best available profile, stripping colors (and ANSI) entirely when output is not a TTY.
If you're using Lip Gloss with Bubble Tea there's nothing to do here: downsampling is built into Bubble Tea v2. If you're not using Bubble Tea you now need to use a writer to downsample colors. Lip Gloss writers are a drop-in replacement for the usual functions found in the fmt package:
s := someStyle.Render("Hello!")
// Downsample and print to stdout.
lipgloss.Println(s)
// Render to a variable.
downsampled := lipgloss.Sprint(s)
// Print to stderr.
lipgloss.Fprint(os.Stderr, s)🌛 Background color detection and adaptive colors
Rendering different colors depending on whether the terminal has a light or dark background is an awesome power. Lip Gloss v2 gives you more control over this progress. This especially matters when input and output are not stdin and stdout.
If that doesn’t matter to you and you're only working with stdout you skip this via compat above, though we encourage you to explore this new functionality.
With Bubble Tea
In Bubble Tea, request the background color, listen for a BackgroundColorMsg in your update, and respond accordingly.
// Query for the background color.
func (m model) Init() tea.Cmd {
return tea.RequestBackgroundColor
}
// Listen for the response and initialize your styles accordigly.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
// Initialize your styles now that you know the background color.
m.styles = newStyles(msg.IsDark())
return m, nil
}
}
type styles {
myHotStyle lipgloss.Style
}
func newStyles(bgIsDark bool) (s styles) {
lightDark := lipgloss.LightDark(bgIsDark) // just a helper function
return styles{
myHotStyle := lipgloss.NewStyle().Foreground(lightDark("#f1f1f1", "#333333"))
}
}Standalone
If you're not using Bubble Tea you simply can perform the query manually:
// Detect the background color. Notice we're writing to stderr.
hasDarkBG, err := lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
if err != nil {
log.Fatal("Oof:", err)
}
// Create a helper for choosing the appropriate color.
lightDark := lipgloss.LightDark(hasDarkBG)
// Declare some colors.
thisColor := lightDark("#C5ADF9", "#864EFF")
thatColor := lightDark("#37CD96", "#22C78A")
// Render some styles.
a := lipgloss.NewStyle().Foreground(thisColor).Render("this")
b := lipgloss.NewStyle().Foreground(thatColor).Render("that")
// Print to stderr.
lipgloss.Fprintf(os.Stderr, "my fave colors are %s and %s...for now.", a, b)🥕 Other stuff
Colors are now color.Color
lipgloss.Color() now produces an idiomatic color.Color, whereas before colors were type lipgloss.TerminalColor. Generally speaking, this is more of an implementation detail, but it’s worth noting the structural differences.
// Before
type TerminalColor interface{/* ... */}
type Color string
// After
func Color(string) color.Color
type RGBColor struct{R, G, B uint8}
func LightDark(isDark bool) LightDarkFunc
type LightDarkFunc func(light, dark color.Color) color.Color
func Complete(colorprofile.Profile) CompleteFunc
type CompleteFunc func(ansi, ansi256, truecolor color.Color) color.Color
Changelog
New!
- b259725: feat(blending): early return when steps <= num stops (#566) (@lrstanley)
- 71dd8ee: feat(borders): initial border blend implementation (#560) (@lrstanley)
- 2166ce8: feat(canvas): accept any type as layer content (@aymanbagabas)
- 0303864: feat(colors): refactor colors sub-package into root package (@lrstanley)
- 9c86c1f: feat(colors): switch from int to float64 for inputs (@lrstanley)
- 0334bb4: feat(tree): support width and indenter styling (#446) (@dlvhdr)
- 9a771f5: feat: BlendLinear* -> Blend* (@lrstanley)
- 34443e8: feat: add brightness example, misc example tweaks (@lrstanley)
- c95c5f3: feat: add hyperlink support (#473) (@aymanbagabas)
- 5e542b8: feat: add underline style and color (@aymanbagabas)
- d303260: feat: add wrap implementation preserving styles and links (#582) (@aymanbagabas)
- 7bf1844: feat: further simplify colors in examples (@lrstanley)
- 27a8cf9: feat: implement uv Drawable for Canvas and Layer (@aymanbagabas)
- c4c08fc: feat: implement uv.Drawable for *Layer (#607) (@ayn2op)
- 18b4bb8: feat: initial implementation of color blending & brightness helpers (@lrstanley)
- 6361009: feat: update examples/layout to use colors.BlendLinear1D (@lrstanley)
- de4521b: feat: update examples/list/sublist to use colors.BlendLinear1D (@lrstanley)
- 1b3716c: feat: use custom hex parsing for increased perf (@lrstanley)
Fixed
- 06ca257: fix(canvas): Hit method should return Layer ID as string instead of *Layer (@aymanbagabas)
- d1fa879: fix(canvas): handle misc edge cases (#588) (@lrstanley)
- 7869489: fix(canvas): simplify Render handling (@aymanbagabas)
- 68f38bd: fix(ci): use local golangci config (@aymanbagabas)
- ff11224: fix(color): update deprecated types (@aymanbagabas)
- 3f659a8: fix(colors): update examples to use new method locations (@lrstanley)
- 3248589: fix(layers): allow recursive rendering for layers that only contain children (#589) (@lrstanley)
- 6c33b19: fix(lint): remove nolint:exhaustive comments and ignore var-naming rule for revive (@aymanbagabas)
- d267651: fix(style): use alias for Underline type from ansi package (@aymanbagabas)
- 76690c6: fix(table): fix wrong behavior of headers regarding margins (#513) (@andreynering)
- 41ff0bf: fix(terminal): switch to uv.NewCancelReader for Windows compatibility (@aymanbagabas)
- 5d69c0e: fix: ensure we strip out \r\n from strings when getting lines (@aymanbagabas)
- 2e570c2: fix: linear-2d example (@lrstanley)
- 0d6a022: fix: lint issues (@aymanbagabas)
- 832bc9d: fix: prevent infinite loop with zero-width whitespace chars (#108) (#604) (@calobozan)
- 354e70d: fix: rename underline constants to be consistent with other style properties (@raphamorim)
Docs
- 60df47f: docs(readme): cleanup badges (@meowgorithm)
- 881a1ff: docs(readme): update art (@meowgorithm)
- ee74a03: docs(readme): update footer art and copyright date (@meowgorithm)
- 8863cc0: docs(readme): update header image (@meowgorithm)
- 4e8ca2d: docs: add underline styles and colors caveats (@aymanbagabas)
- a8cfc26: docs: add v2 upgrade and changes guide (#611) (@aymanbagabas)
- 454007a: docs: update comments in for GetPaddingChar and GetMarginChar (@aymanbagabas)
- 95f30db: docs: update mascot header image (@aymanbagabas)
- a06a847: docs: update readme in prep for v2 (#613) (@aymanbagabas)
Other stuff
- 5ca0343: Fix(table): BorderRow (#514) (@bashbunni)
- f2d1864: Improve performance of maxRuneWidth (#592) (@clipperhouse)
- 10c048e: Merge v2-uv-canvas into v2-exp (@aymanbagabas)
- d02a007: ci: sync dependabot config (#521) (@charmcli)
- 8708a89: ci: sync dependabot config (#561) (@charmcli)
- 7d1b622: ci: sync dependabot config (#572) (@charmcli)
- 19a4b99: ci: sync golangci-lint config (@aymanbagabas)
- a6c079d: ci: sync golangci-lint config (@aymanbagabas)
- 350edde: ci: sync golangci-lint config (#553) (@github-actions[bot])
- 1e3ee34: ci: sync golangci-lint config (#598) (@github-actions[bot])
- e729228: lint: fix lint for newer go versions (#540) (@andreynering)
- 66093c8: perf: remove allocations from getFirstRuneAsString (#578) (@clipperhouse)
- ad876c4: refactor: new Canvas, Compositor, and Layer API (#591) (@aymanbagabas)
- 3aae286: refactor: update imports to use charm.land domain (@aymanbagabas)
🌈 Feedback
That's a wrap! Feel free to reach out, ask questions, and let us know how it's going. We'd love to know what you think.
Part of Charm.
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
