Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3014273
feat: add adaptive color package
aymanbagabas Aug 30, 2024
dbc5538
chore: miscellaneous lint work
meowgorithm Oct 11, 2024
c0da46c
chore: rename LightDark to Adapt per @bashbunni's acute suggestion
meowgorithm Oct 11, 2024
7e7e96a
chore(lint): mute integer conversion warnings (G115)
meowgorithm Oct 11, 2024
5d63b45
chore: make adaptive color workflow more explicit
meowgorithm Oct 11, 2024
ea8092f
fix(adaptive): return dark background by default
meowgorithm Oct 16, 2024
d6e77aa
fix(adaptive): fix GoDoc
meowgorithm Oct 16, 2024
790b508
fix: background color detect
meowgorithm Oct 16, 2024
d8ccecc
chore: improve errors in background color query
meowgorithm Oct 16, 2024
01159e4
chore: add functions for printing colors with automatic downsampling
meowgorithm Oct 16, 2024
dfd38ce
chore: remove rogue logfile
meowgorithm Oct 16, 2024
9551662
docs(examples): add v2 adaptive color examples
meowgorithm Oct 16, 2024
b423a32
chore: cleanup writer implementations
meowgorithm Oct 16, 2024
c143751
docs(examples): move adaptive color examples
meowgorithm Oct 16, 2024
78be932
docs(examples): update layout example for Lip Gloss v2
meowgorithm Oct 16, 2024
ebe9e2a
chore: update tests and remove unused code
aymanbagabas Oct 8, 2024
e36ce87
wip(examples): various updates for Lip Gloss v2
bashbunni Oct 10, 2024
d724ce6
docs(examples): fixes for Lip Gloss v2
bashbunni Oct 10, 2024
9cb1f6b
docs(examples): use Lip Gloss (colorprofile) writer in layout example
meowgorithm Oct 16, 2024
5d44e47
docs(examples): show background detection status in layout example
meowgorithm Oct 16, 2024
805e5a4
docs(examples): fix background detection in layout example
meowgorithm Oct 16, 2024
54e84cd
docs(examples): re-add background color detect to ssh; fix profile
meowgorithm Oct 16, 2024
067d085
feat: move writers to top level package
meowgorithm Oct 17, 2024
6fa7bb6
docs(examples): use colorprofile writer in all examples
meowgorithm Oct 17, 2024
8dc8bdc
chore(lint): ignore error wrap checks in writers
meowgorithm Oct 17, 2024
70d7cb6
chore: rename Adapt back to LightDark
meowgorithm Oct 17, 2024
bfae28f
docs(examples): improve light/dark colors in color examples
meowgorithm Oct 17, 2024
83dd196
chore(deps): use current v2-exp branch in Bubble Tea in examples
meowgorithm Oct 17, 2024
6faf40a
chore: simplify LightDark API
meowgorithm Oct 17, 2024
0d5d57a
docs(examples): restore removed light-dark (formerly adaptive) colors
meowgorithm Oct 17, 2024
4b0b6a2
docs(examples): remove cool FP-like expression
meowgorithm Oct 18, 2024
7256ef2
chore(lint): wrap error in BackgroundColor()
meowgorithm Oct 18, 2024
125420d
chore(merge): branch 'v2-exp' into v2-adaptive-standalone
meowgorithm Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 72 additions & 19 deletions color.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lipgloss

import (
"fmt"
"image/color"
"strconv"

Expand Down Expand Up @@ -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.
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
179 changes: 7 additions & 172 deletions color_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package lipgloss

import (
"image/color"
"testing"
)

Expand Down Expand Up @@ -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)
}
Expand All @@ -41,194 +40,30 @@ 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 {
t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1)
}
}
}

// 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])<<offset + hexToByte(hex[2])
c.G = hexToByte(hex[3])<<offset + hexToByte(hex[4])
c.B = hexToByte(hex[5])<<offset + hexToByte(hex[6])
case shortFormat:
const offset = 0x11
c.R = hexToByte(hex[1]) * offset
c.G = hexToByte(hex[2]) * offset
c.B = hexToByte(hex[3]) * offset
}

return c
}

func hexToByte(b byte) byte {
const offset = 10
switch {
case b >= '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
}
Loading
Loading