feat: instantiate lipgloss renderers

* Use lipgloss instances instead of a singleton global instance
* Deprecate (s Style).Render & (s Style).String
* Deprecate the Stringer interface
* Update README and godocs
* Update example
This commit is contained in:
Ayman Bagabas 2022-06-28 13:28:35 -04:00
parent f754c404f6
commit 31ab45f810
11 changed files with 569 additions and 423 deletions

View File

@ -27,7 +27,7 @@ var style = lipgloss.NewStyle().
PaddingLeft(4). PaddingLeft(4).
Width(22) Width(22)
fmt.Println(style.Render("Hello, kitty.")) fmt.Println(lipgloss.Render(style, "Hello, kitty."))
``` ```
@ -151,11 +151,12 @@ var style = lipgloss.NewStyle().
Setting a minimum width and height is simple and straightforward. Setting a minimum width and height is simple and straightforward.
```go ```go
var str = lipgloss.NewStyle(). var style = lipgloss.NewStyle().
Width(24). Width(24).
Height(32). Height(32).
Foreground(lipgloss.Color("63")). Foreground(lipgloss.Color("63"))
Render("Whats for lunch?")
var str = lipgloss.Render(style, "Whats for lunch?")
``` ```
@ -263,29 +264,35 @@ and `MaxWidth`, and `MaxHeight` come in:
```go ```go
// Force rendering onto a single line, ignoring margins, padding, and borders. // Force rendering onto a single line, ignoring margins, padding, and borders.
someStyle.Inline(true).Render("yadda yadda") lipgloss.Render(someStyle.Inline(true), "yadda yadda")
// Also limit rendering to five cells // Also limit rendering to five cells
someStyle.Inline(true).MaxWidth(5).Render("yadda yadda") lipgloss.Render(someStyle.Inline(true).MaxWidth(5), "yadda yadda")
// Limit rendering to a 5x5 cell block // Limit rendering to a 5x5 cell block
someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda") lipgloss.Render(someStyle.MaxWidth(5).MaxHeight(5), "yadda yadda")
``` ```
## Rendering ## Rendering
Generally, you just call the `Render(string)` method on a `lipgloss.Style`: Generally, you just pass a style and string to the default renderer:
```go ```go
fmt.Println(lipgloss.NewStyle().Bold(true).Render("Hello, kitty.")) style := lipgloss.NewStyle().Bold(true)
fmt.Println(lipgloss.Render(style, "Hello, kitty."))
``` ```
But you could also use the Stringer interface: But you can also use a custom renderer:
```go ```go
// Render to stdout and force dark background mode.
var r = lipgloss.NewRenderer(
lipgloss.WithOutput(termenv.NewOutput(os.Stdout)),
lipgloss.WithDarkBackground(),
)
var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true) var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true)
fmt.Printf("%s\n", style) fmt.Println(r.Render(style))
``` ```
@ -318,10 +325,11 @@ Sometimes youll want to know the width and height of text blocks when buildin
your layouts. your layouts.
```go ```go
var block string = lipgloss.NewStyle(). // Render a block of text.
var style = lipgloss.NewStyle().
Width(40). Width(40).
Padding(2). Padding(2)
Render(someLongString) var block string = lipgloss.Render(style, someLongString)
// Get the actual, physical dimensions of the text block. // Get the actual, physical dimensions of the text block.
width := lipgloss.Width(block) width := lipgloss.Width(block)

View File

@ -196,7 +196,7 @@ func HiddenBorder() Border {
return hiddenBorder return hiddenBorder
} }
func (s Style) applyBorder(str string) string { func (s Style) applyBorder(re *Renderer, str string) string {
var ( var (
topSet = s.isSet(borderTopKey) topSet = s.isSet(borderTopKey)
rightSet = s.isSet(borderRightKey) rightSet = s.isSet(borderRightKey)
@ -298,7 +298,7 @@ func (s Style) applyBorder(str string) string {
// Render top // Render top
if hasTop { if hasTop {
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
top = styleBorder(top, topFG, topBG) top = styleBorder(re, top, topFG, topBG)
out.WriteString(top) out.WriteString(top)
out.WriteRune('\n') out.WriteRune('\n')
} }
@ -317,7 +317,7 @@ func (s Style) applyBorder(str string) string {
if leftIndex >= len(leftRunes) { if leftIndex >= len(leftRunes) {
leftIndex = 0 leftIndex = 0
} }
out.WriteString(styleBorder(r, leftFG, leftBG)) out.WriteString(styleBorder(re, r, leftFG, leftBG))
} }
out.WriteString(l) out.WriteString(l)
if hasRight { if hasRight {
@ -326,7 +326,7 @@ func (s Style) applyBorder(str string) string {
if rightIndex >= len(rightRunes) { if rightIndex >= len(rightRunes) {
rightIndex = 0 rightIndex = 0
} }
out.WriteString(styleBorder(r, rightFG, rightBG)) out.WriteString(styleBorder(re, r, rightFG, rightBG))
} }
if i < len(lines)-1 { if i < len(lines)-1 {
out.WriteRune('\n') out.WriteRune('\n')
@ -336,7 +336,7 @@ func (s Style) applyBorder(str string) string {
// Render bottom // Render bottom
if hasBottom { if hasBottom {
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width) bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
bottom = styleBorder(bottom, bottomFG, bottomBG) bottom = styleBorder(re, bottom, bottomFG, bottomBG)
out.WriteRune('\n') out.WriteRune('\n')
out.WriteString(bottom) out.WriteString(bottom)
} }
@ -376,7 +376,7 @@ func renderHorizontalEdge(left, middle, right string, width int) string {
} }
// Apply foreground and background styling to a border. // Apply foreground and background styling to a border.
func styleBorder(border string, fg, bg TerminalColor) string { func styleBorder(re *Renderer, border string, fg, bg TerminalColor) string {
if fg == noColor && bg == noColor { if fg == noColor && bg == noColor {
return border return border
} }
@ -384,10 +384,10 @@ func styleBorder(border string, fg, bg TerminalColor) string {
var style = termenv.Style{} var style = termenv.Style{}
if fg != noColor { if fg != noColor {
style = style.Foreground(ColorProfile().Color(fg.value())) style = style.Foreground(re.color(fg))
} }
if bg != noColor { if bg != noColor {
style = style.Background(ColorProfile().Color(bg.value())) style = style.Background(re.color(bg))
} }
return style.Styled(border) return style.Styled(border)

139
color.go
View File

@ -1,112 +1,13 @@
package lipgloss package lipgloss
import ( import (
"sync" "strconv"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
var ( // TerminalColor is a color intended to be rendered in the terminal.
output *termenv.Output
colorProfile termenv.Profile
getColorProfile sync.Once
explicitColorProfile bool
hasDarkBackground bool
getBackgroundColor sync.Once
explicitBackgroundColor bool
colorProfileMtx sync.RWMutex
)
// ColorProfile returns the detected termenv color profile. It will perform the
// actual check only once.
func ColorProfile() termenv.Profile {
colorProfileMtx.RLock()
defer colorProfileMtx.RUnlock()
if !explicitColorProfile {
getColorProfile.Do(func() {
if output != nil {
colorProfile = output.EnvColorProfile()
} else {
colorProfile = termenv.EnvColorProfile()
}
})
}
return colorProfile
}
// SetColorProfile sets the color profile on a package-wide context. This
// function exists mostly for testing purposes so that you can assure you're
// testing against a specific profile.
//
// Outside of testing you likely won't want to use this function as
// ColorProfile() will detect and cache the terminal's color capabilities
// and choose the best available profile.
//
// Available color profiles are:
//
// termenv.Ascii (no color, 1-bit)
// termenv.ANSI (16 colors, 4-bit)
// termenv.ANSI256 (256 colors, 8-bit)
// termenv.TrueColor (16,777,216 colors, 24-bit)
//
// This function is thread-safe.
func SetColorProfile(p termenv.Profile) {
colorProfileMtx.Lock()
defer colorProfileMtx.Unlock()
colorProfile = p
explicitColorProfile = true
}
// SetOutput sets the output to use for adaptive color detection.
func SetOutput(o *termenv.Output) {
output = o
}
// HasDarkBackground returns whether or not the terminal has a dark background.
func HasDarkBackground() bool {
colorProfileMtx.RLock()
defer colorProfileMtx.RUnlock()
if !explicitBackgroundColor {
getBackgroundColor.Do(func() {
if output != nil {
hasDarkBackground = output.HasDarkBackground()
} else {
hasDarkBackground = termenv.HasDarkBackground()
}
})
}
return hasDarkBackground
}
// SetHasDarkBackground sets the value of the background color detection on a
// package-wide context. This function exists mostly for testing purposes so
// that you can assure you're testing against a specific background color
// setting.
//
// Outside of testing you likely won't want to use this function as
// HasDarkBackground() will detect and cache the terminal's current background
// color setting.
//
// This function is thread-safe.
func SetHasDarkBackground(b bool) {
colorProfileMtx.Lock()
defer colorProfileMtx.Unlock()
hasDarkBackground = b
explicitBackgroundColor = true
}
// TerminalColor is a color intended to be rendered in the terminal. It
// satisfies the Go color.Color interface.
type TerminalColor interface { type TerminalColor interface {
value() string
color() termenv.Color
RGBA() (r, g, b, a uint32) RGBA() (r, g, b, a uint32)
} }
@ -119,13 +20,7 @@ type TerminalColor interface {
// var style = someStyle.Copy().Background(lipgloss.NoColor{}) // var style = someStyle.Copy().Background(lipgloss.NoColor{})
type NoColor struct{} type NoColor struct{}
func (n NoColor) value() string { var noColor = NoColor{}
return ""
}
func (n NoColor) color() termenv.Color {
return ColorProfile().Color("")
}
// RGBA returns the RGBA value of this color. Because we have to return // RGBA returns the RGBA value of this color. Because we have to return
// something, despite this color being the absence of color, we're returning // something, despite this color being the absence of color, we're returning
@ -136,18 +31,12 @@ func (n NoColor) RGBA() (r, g, b, a uint32) {
return 0x0, 0x0, 0x0, 0xFFFF return 0x0, 0x0, 0x0, 0xFFFF
} }
var noColor = NoColor{}
// Color specifies a color by hex or ANSI value. For example: // Color specifies a color by hex or ANSI value. For example:
// //
// ansiColor := lipgloss.Color("21") // ansiColor := lipgloss.Color("21")
// hexColor := lipgloss.Color("#0000ff") // hexColor := lipgloss.Color("#0000ff")
type Color string type Color string
func (c Color) value() string {
return string(c)
}
func (c Color) color() termenv.Color { func (c Color) color() termenv.Color {
return ColorProfile().Color(string(c)) return ColorProfile().Color(string(c))
} }
@ -160,6 +49,26 @@ func (c Color) RGBA() (r, g, b, a uint32) {
return termenv.ConvertToRGB(c.color()).RGBA() return termenv.ConvertToRGB(c.color()).RGBA()
} }
// ANSIColor is a color specified by an ANSI color value. It's merely syntactic
// sugar for the more general Color function. Invalid colors will render as
// black.
//
// Example usage:
//
// // These two statements are equivalent.
// colorA := lipgloss.ANSIColor(21)
// colorB := lipgloss.Color("21")
type ANSIColor uint
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface. Note that on error we return black with 100% opacity, or:
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
func (ac ANSIColor) RGBA() (r, g, b, a uint32) {
cf := Color(strconv.FormatUint(uint64(ac), 10))
return cf.RGBA()
}
// AdaptiveColor provides color options for light and dark backgrounds. The // AdaptiveColor provides color options for light and dark backgrounds. The
// appropriate color will be returned at runtime based on the darkness of the // appropriate color will be returned at runtime based on the darkness of the
// terminal background color. // terminal background color.
@ -213,7 +122,7 @@ func (c CompleteColor) value() string {
} }
func (c CompleteColor) color() termenv.Color { func (c CompleteColor) color() termenv.Color {
return colorProfile.Color(c.value()) return ColorProfile().Color(c.value())
} }
// RGBA returns the RGBA value of this color. This satisfies the Go Color // RGBA returns the RGBA value of this color. This satisfies the Go Color

View File

@ -8,6 +8,7 @@ import (
) )
func TestSetColorProfile(t *testing.T) { func TestSetColorProfile(t *testing.T) {
r := renderer
input := "hello" input := "hello"
tt := []struct { tt := []struct {
@ -39,9 +40,9 @@ func TestSetColorProfile(t *testing.T) {
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
SetColorProfile(tc.profile) r.SetColorProfile(tc.profile)
style := NewStyle().Foreground(Color("#5A56E0")) style := NewStyle().Foreground(Color("#5A56E0"))
res := style.Render(input) res := Render(style, input)
if res != tc.expected { if res != tc.expected {
t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n", t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",

View File

@ -29,13 +29,14 @@ var (
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}
divider = lipgloss.NewStyle(). divider = lipgloss.Render(
SetString("•"). lipgloss.NewStyle().
Padding(0, 1). Padding(0, 1).
Foreground(subtle). Foreground(subtle), "•")
String()
url = lipgloss.NewStyle().Foreground(special).Render url = func(s string) string {
return lipgloss.Render(lipgloss.NewStyle().Foreground(special), s)
}
// Tabs. // Tabs.
@ -122,14 +123,17 @@ var (
Height(8). Height(8).
Width(columnWidth + 1) Width(columnWidth + 1)
listHeader = lipgloss.NewStyle(). listHeader = func(s string) string {
return lipgloss.Render(lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()).
BorderBottom(true). BorderBottom(true).
BorderForeground(subtle). BorderForeground(subtle).
MarginRight(2). MarginRight(2), s)
Render }
listItem = lipgloss.NewStyle().PaddingLeft(2).Render listItem = func(s string) string {
return lipgloss.Render(lipgloss.NewStyle().PaddingLeft(2), s)
}
checkMark = lipgloss.NewStyle().SetString("✓"). checkMark = lipgloss.NewStyle().SetString("✓").
Foreground(special). Foreground(special).
@ -137,10 +141,11 @@ var (
String() String()
listDone = func(s string) string { listDone = func(s string) string {
return checkMark + lipgloss.NewStyle(). return checkMark + lipgloss.
Strikethrough(true). Render(lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"}). Strikethrough(true).
Render(s) Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"}),
s)
} }
// Paragraphs/History. // Paragraphs/History.
@ -192,13 +197,13 @@ func main() {
{ {
row := lipgloss.JoinHorizontal( row := lipgloss.JoinHorizontal(
lipgloss.Top, lipgloss.Top,
activeTab.Render("Lip Gloss"), lipgloss.Render(activeTab, "Lip Gloss"),
tab.Render("Blush"), lipgloss.Render(tab, "Blush"),
tab.Render("Eye Shadow"), lipgloss.Render(tab, "Eye Shadow"),
tab.Render("Mascara"), lipgloss.Render(tab, "Mascara"),
tab.Render("Foundation"), lipgloss.Render(tab, "Foundation"),
) )
gap := tabGap.Render(strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2))) gap := lipgloss.Render(tabGap, strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2)))
row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap) row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
doc.WriteString(row + "\n\n") doc.WriteString(row + "\n\n")
} }
@ -220,8 +225,8 @@ func main() {
} }
desc := lipgloss.JoinVertical(lipgloss.Left, desc := lipgloss.JoinVertical(lipgloss.Left,
descStyle.Render("Style Definitions for Nice Terminal Layouts"), lipgloss.Render(descStyle, "Style Definitions for Nice Terminal Layouts"),
infoStyle.Render("From Charm"+divider+url("https://github.com/charmbracelet/lipgloss")), lipgloss.Render(infoStyle, "From Charm"+divider+url("https://github.com/charmbracelet/lipgloss")),
) )
row := lipgloss.JoinHorizontal(lipgloss.Top, title.String(), desc) row := lipgloss.JoinHorizontal(lipgloss.Top, title.String(), desc)
@ -230,16 +235,16 @@ func main() {
// Dialog // Dialog
{ {
okButton := activeButtonStyle.Render("Yes") okButton := lipgloss.Render(activeButtonStyle, "Yes")
cancelButton := buttonStyle.Render("Maybe") cancelButton := lipgloss.Render(buttonStyle, "Maybe")
question := lipgloss.NewStyle().Width(50).Align(lipgloss.Center).Render("Are you sure you want to eat marmalade?") question := lipgloss.Render(lipgloss.NewStyle().Width(50).Align(lipgloss.Center), "Are you sure you want to eat marmalade?")
buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton) buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton)
ui := lipgloss.JoinVertical(lipgloss.Center, question, buttons) ui := lipgloss.JoinVertical(lipgloss.Center, question, buttons)
dialog := lipgloss.Place(width, 9, dialog := lipgloss.Place(width, 9,
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
dialogBoxStyle.Render(ui), lipgloss.Render(dialogBoxStyle, ui),
lipgloss.WithWhitespaceChars("猫咪"), lipgloss.WithWhitespaceChars("猫咪"),
lipgloss.WithWhitespaceForeground(subtle), lipgloss.WithWhitespaceForeground(subtle),
) )
@ -264,7 +269,7 @@ func main() {
}() }()
lists := lipgloss.JoinHorizontal(lipgloss.Top, lists := lipgloss.JoinHorizontal(lipgloss.Top,
list.Render( lipgloss.Render(list,
lipgloss.JoinVertical(lipgloss.Left, lipgloss.JoinVertical(lipgloss.Left,
listHeader("Citrus Fruits to Try"), listHeader("Citrus Fruits to Try"),
listDone("Grapefruit"), listDone("Grapefruit"),
@ -274,7 +279,7 @@ func main() {
listItem("Pomelo"), listItem("Pomelo"),
), ),
), ),
list.Copy().Width(columnWidth).Render( lipgloss.Render(list.Copy().Width(columnWidth),
lipgloss.JoinVertical(lipgloss.Left, lipgloss.JoinVertical(lipgloss.Left,
listHeader("Actual Lip Gloss Vendors"), listHeader("Actual Lip Gloss Vendors"),
listItem("Glossier"), listItem("Glossier"),
@ -298,9 +303,9 @@ func main() {
doc.WriteString(lipgloss.JoinHorizontal( doc.WriteString(lipgloss.JoinHorizontal(
lipgloss.Top, lipgloss.Top,
historyStyle.Copy().Align(lipgloss.Right).Render(historyA), lipgloss.Render(historyStyle.Copy().Align(lipgloss.Right), historyA),
historyStyle.Copy().Align(lipgloss.Center).Render(historyB), lipgloss.Render(historyStyle.Copy().Align(lipgloss.Center), historyB),
historyStyle.Copy().MarginRight(0).Render(historyC), lipgloss.Render(historyStyle.Copy().MarginRight(0), historyC),
)) ))
doc.WriteString("\n\n") doc.WriteString("\n\n")
@ -310,12 +315,13 @@ func main() {
{ {
w := lipgloss.Width w := lipgloss.Width
statusKey := statusStyle.Render("STATUS") statusKey := lipgloss.Render(statusStyle, "STATUS")
encoding := encodingStyle.Render("UTF-8") encoding := lipgloss.Render(encodingStyle, "UTF-8")
fishCake := fishCakeStyle.Render("🍥 Fish Cake") fishCake := lipgloss.Render(fishCakeStyle, "🍥 Fish Cake")
statusVal := statusText.Copy(). statusVal := lipgloss.Render(
Width(width - w(statusKey) - w(encoding) - w(fishCake)). statusText.Copy().
Render("Ravishing") Width(width-w(statusKey)-w(encoding)-w(fishCake)),
"Ravishing")
bar := lipgloss.JoinHorizontal(lipgloss.Top, bar := lipgloss.JoinHorizontal(lipgloss.Top,
statusKey, statusKey,
@ -324,7 +330,7 @@ func main() {
fishCake, fishCake,
) )
doc.WriteString(statusBarStyle.Width(width).Render(bar)) doc.WriteString(lipgloss.Render(statusBarStyle.Width(width), bar))
} }
if physicalWidth > 0 { if physicalWidth > 0 {
@ -332,7 +338,7 @@ func main() {
} }
// Okay, let's print it // Okay, let's print it
fmt.Println(docStyle.Render(doc.String())) fmt.Println(lipgloss.Render(docStyle, doc.String()))
} }
func colorGrid(xSteps, ySteps int) [][]string { func colorGrid(xSteps, ySteps int) [][]string {

2
go.mod
View File

@ -4,6 +4,6 @@ go 1.15
require ( require (
github.com/mattn/go-runewidth v0.0.14 github.com/mattn/go-runewidth v0.0.14
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.14.0 github.com/muesli/termenv v0.14.0
) )

6
go.sum
View File

@ -4,11 +4,11 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0= github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0=
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=

View File

@ -48,11 +48,7 @@ func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOpti
return str return str
} }
ws := &whitespace{} ws := NewWhitespace(opts...)
for _, opt := range opts {
opt(ws)
}
var b strings.Builder var b strings.Builder
for i, l := range lines { for i, l := range lines {
// Is this line shorter than the longest line? // Is this line shorter than the longest line?
@ -98,11 +94,7 @@ func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOptio
return str return str
} }
ws := &whitespace{} ws := NewWhitespace(opts...)
for _, opt := range opts {
opt(ws)
}
_, width := getLines(str) _, width := getLines(str)
emptyLine := ws.render(width) emptyLine := ws.render(width)
b := strings.Builder{} b := strings.Builder{}

426
renderer.go Normal file
View File

@ -0,0 +1,426 @@
package lipgloss
import (
"fmt"
"strings"
"unicode"
"github.com/muesli/reflow/truncate"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/reflow/wrap"
"github.com/muesli/termenv"
)
var renderer = NewRenderer()
// Renderer is a lipgloss terminal renderer.
type Renderer struct {
output *termenv.Output
hasDarkBackground bool
}
// RendererOption is a function that can be used to configure a Renderer.
type RendererOption func(r *Renderer)
// DefaultRenderer returns the default renderer.
func DefaultRenderer() *Renderer {
return renderer
}
// NewRenderer creates a new Renderer.
func NewRenderer(options ...RendererOption) *Renderer {
r := &Renderer{}
for _, option := range options {
option(r)
}
if r.output == nil {
r.output = termenv.DefaultOutput()
}
if !r.hasDarkBackground {
r.hasDarkBackground = r.output.HasDarkBackground()
}
return r
}
// WithOutput sets the termenv Output to use for rendering.
func WithOutput(output *termenv.Output) RendererOption {
return func(r *Renderer) {
r.output = output
}
}
// WithDarkBackground forces the renderer to use a dark background.
func WithDarkBackground() RendererOption {
return func(r *Renderer) {
r.SetHasDarkBackground(true)
}
}
// WithColorProfile sets the color profile on the renderer. This function is
// primarily intended for testing. For details, see the note on
// [Renderer.SetColorProfile].
func WithColorProfile(p termenv.Profile) RendererOption {
return func(r *Renderer) {
r.SetColorProfile(p)
}
}
// ColorProfile returns the detected termenv color profile.
func (r *Renderer) ColorProfile() termenv.Profile {
return r.output.Profile
}
// ColorProfile returns the detected termenv color profile.
func ColorProfile() termenv.Profile {
return renderer.ColorProfile()
}
// SetColorProfile sets the color profile on the renderer. This function exists
// mostly for testing purposes so that you can assure you're testing against
// a specific profile.
//
// Outside of testing you likely won't want to use this function as the color
// profile will detect and cache the terminal's color capabilities and choose
// the best available profile.
//
// Available color profiles are:
//
// termenv.Ascii (no color, 1-bit)
// termenv.ANSI (16 colors, 4-bit)
// termenv.ANSI256 (256 colors, 8-bit)
// termenv.TrueColor (16,777,216 colors, 24-bit)
//
// This function is thread-safe.
func (r *Renderer) SetColorProfile(p termenv.Profile) {
r.output.Profile = p
}
// SetColorProfile sets the color profile on the default renderer. This
// function exists mostly for testing purposes so that you can assure you're
// testing against a specific profile.
//
// Outside of testing you likely won't want to use this function as the color
// profile will detect and cache the terminal's color capabilities and choose
// the best available profile.
//
// Available color profiles are:
//
// termenv.Ascii (no color, 1-bit)
// termenv.ANSI (16 colors, 4-bit)
// termenv.ANSI256 (256 colors, 8-bit)
// termenv.TrueColor (16,777,216 colors, 24-bit)
//
// This function is thread-safe.
func SetColorProfile(p termenv.Profile) {
renderer.SetColorProfile(p)
}
// HasDarkBackground returns whether or not the terminal has a dark background.
func (r *Renderer) HasDarkBackground() bool {
return r.hasDarkBackground
}
// HasDarkBackground returns whether or not the terminal has a dark background.
func HasDarkBackground() bool {
return renderer.HasDarkBackground()
}
// SetHasDarkBackground sets the background color detection value on the
// renderer. This function exists mostly for testing purposes so that you can
// assure you're testing against a specific background color setting.
//
// Outside of testing you likely won't want to use this function as the
// backgrounds value will be automatically detected and cached against the
// terminal's current background color setting.
//
// This function is thread-safe.
func (r *Renderer) SetHasDarkBackground(b bool) {
r.hasDarkBackground = b
}
// SetHasDarkBackground sets the background color detection value for the
// default renderer. This function exists mostly for testing purposes so that
// you can assure you're testing against a specific background color setting.
//
// Outside of testing you likely won't want to use this function as the
// backgrounds value will be automatically detected and cached against the
// terminal's current background color setting.
//
// This function is thread-safe.
func SetHasDarkBackground(b bool) {
renderer.SetHasDarkBackground(b)
}
// Render formats a string according to the given style.
func (r *Renderer) Render(s Style, str string) string {
var (
te = r.ColorProfile().String()
teSpace = r.ColorProfile().String()
teWhitespace = r.ColorProfile().String()
bold = s.getAsBool(boldKey, false)
italic = s.getAsBool(italicKey, false)
underline = s.getAsBool(underlineKey, false)
strikethrough = s.getAsBool(strikethroughKey, false)
reverse = s.getAsBool(reverseKey, false)
blink = s.getAsBool(blinkKey, false)
faint = s.getAsBool(faintKey, false)
fg = s.getAsColor(foregroundKey)
bg = s.getAsColor(backgroundKey)
width = s.getAsInt(widthKey)
height = s.getAsInt(heightKey)
horizontalAlign = s.getAsPosition(alignHorizontalKey)
verticalAlign = s.getAsPosition(alignVerticalKey)
topPadding = s.getAsInt(paddingTopKey)
rightPadding = s.getAsInt(paddingRightKey)
bottomPadding = s.getAsInt(paddingBottomKey)
leftPadding = s.getAsInt(paddingLeftKey)
colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
inline = s.getAsBool(inlineKey, false)
maxWidth = s.getAsInt(maxWidthKey)
maxHeight = s.getAsInt(maxHeightKey)
underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true)
strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true)
// Do we need to style whitespace (padding and space outside
// paragraphs) separately?
styleWhitespace = reverse
// Do we need to style spaces separately?
useSpaceStyler = underlineSpaces || strikethroughSpaces
)
if len(s.rules) == 0 {
return str
}
// Enable support for ANSI on the legacy Windows cmd.exe console. This is a
// no-op on non-Windows systems and on Windows runs only once.
enableLegacyWindowsANSI()
if bold {
te = te.Bold()
}
if italic {
te = te.Italic()
}
if underline {
te = te.Underline()
}
if reverse {
if reverse {
teWhitespace = teWhitespace.Reverse()
}
te = te.Reverse()
}
if blink {
te = te.Blink()
}
if faint {
te = te.Faint()
}
if fg != noColor {
fgc := r.color(fg)
te = te.Foreground(fgc)
if styleWhitespace {
teWhitespace = teWhitespace.Foreground(fgc)
}
if useSpaceStyler {
teSpace = teSpace.Foreground(fgc)
}
}
if bg != noColor {
bgc := r.color(bg)
te = te.Background(bgc)
if colorWhitespace {
teWhitespace = teWhitespace.Background(bgc)
}
if useSpaceStyler {
teSpace = teSpace.Background(bgc)
}
}
if underline {
te = te.Underline()
}
if strikethrough {
te = te.CrossOut()
}
if underlineSpaces {
teSpace = teSpace.Underline()
}
if strikethroughSpaces {
teSpace = teSpace.CrossOut()
}
// Strip newlines in single line mode
if inline {
str = strings.ReplaceAll(str, "\n", "")
}
// Word wrap
if !inline && width > 0 {
wrapAt := width - leftPadding - rightPadding
str = wordwrap.String(str, wrapAt)
str = wrap.String(str, wrapAt) // force-wrap long strings
}
// Render core text
{
var b strings.Builder
l := strings.Split(str, "\n")
for i := range l {
if useSpaceStyler {
// Look for spaces and apply a different styler
for _, r := range l[i] {
if unicode.IsSpace(r) {
b.WriteString(teSpace.Styled(string(r)))
continue
}
b.WriteString(te.Styled(string(r)))
}
} else {
b.WriteString(te.Styled(l[i]))
}
if i != len(l)-1 {
b.WriteRune('\n')
}
}
str = b.String()
}
// Padding
if !inline {
if leftPadding > 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = padLeft(str, leftPadding, st)
}
if rightPadding > 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = padRight(str, rightPadding, st)
}
if topPadding > 0 {
str = strings.Repeat("\n", topPadding) + str
}
if bottomPadding > 0 {
str += strings.Repeat("\n", bottomPadding)
}
}
// Height
if height > 0 {
str = alignTextVertical(str, verticalAlign, height, nil)
}
// Set alignment. This will also pad short lines with spaces so that all
// lines are the same length, so we run it under a few different conditions
// beyond alignment.
{
numLines := strings.Count(str, "\n")
if !(numLines == 0 && width == 0) {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = alignTextHorizontal(str, horizontalAlign, width, st)
}
}
if !inline {
str = s.applyBorder(r, str)
str = s.applyMargins(r, str, inline)
}
// Truncate according to MaxWidth
if maxWidth > 0 {
lines := strings.Split(str, "\n")
for i := range lines {
lines[i] = truncate.String(lines[i], uint(maxWidth))
}
str = strings.Join(lines, "\n")
}
// Truncate according to MaxHeight
if maxHeight > 0 {
lines := strings.Split(str, "\n")
str = strings.Join(lines[:min(maxHeight, len(lines))], "\n")
}
return str
}
// Render formats a string according to the given style using the default
// renderer. This is syntactic sugar for rendering with a DefaultRenderer.
func Render(s Style, str string) string {
return renderer.Render(s, str)
}
func (r *Renderer) colorValue(c TerminalColor) string {
switch c := c.(type) {
case ANSIColor:
return fmt.Sprint(c)
case Color:
return string(c)
case AdaptiveColor:
if r.HasDarkBackground() {
return c.Dark
}
return c.Light
case CompleteColor:
switch r.ColorProfile() {
case termenv.TrueColor:
return c.TrueColor
case termenv.ANSI256:
return c.ANSI256
case termenv.ANSI:
return c.ANSI
default:
return ""
}
case CompleteAdaptiveColor:
col := c.Light
if r.HasDarkBackground() {
col = c.Dark
}
switch r.ColorProfile() {
case termenv.TrueColor:
return col.TrueColor
case termenv.ANSI256:
return col.ANSI256
case termenv.ANSI:
return col.ANSI
default:
return ""
}
default:
return ""
}
}
// color returns a termenv.color for the given TerminalColor.
func (r *Renderer) color(c TerminalColor) termenv.Color {
return r.ColorProfile().Color(r.colorValue(c))
}

229
style.go
View File

@ -2,11 +2,7 @@ package lipgloss
import ( import (
"strings" "strings"
"unicode"
"github.com/muesli/reflow/truncate"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/reflow/wrap"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
@ -106,6 +102,8 @@ func (s Style) Value() string {
// String implements stringer for a Style, returning the rendered result based // String implements stringer for a Style, returning the rendered result based
// on the rules in this style. An underlying string value must be set with // on the rules in this style. An underlying string value must be set with
// Style.SetString prior to using this method. // Style.SetString prior to using this method.
//
// Deprecated: Use Render(Style, string) instead.
func (s Style) String() string { func (s Style) String() string {
return s.Render(s.value) return s.Render(s.value)
} }
@ -153,226 +151,13 @@ func (s Style) Inherit(i Style) Style {
} }
// Render applies the defined style formatting to a given string. // Render applies the defined style formatting to a given string.
//
// Deprecated: Use Render(Style, string) instead.
func (s Style) Render(str string) string { func (s Style) Render(str string) string {
var ( return renderer.Render(s, str)
te = ColorProfile().String()
teSpace = ColorProfile().String()
teWhitespace = ColorProfile().String()
bold = s.getAsBool(boldKey, false)
italic = s.getAsBool(italicKey, false)
underline = s.getAsBool(underlineKey, false)
strikethrough = s.getAsBool(strikethroughKey, false)
reverse = s.getAsBool(reverseKey, false)
blink = s.getAsBool(blinkKey, false)
faint = s.getAsBool(faintKey, false)
fg = s.getAsColor(foregroundKey)
bg = s.getAsColor(backgroundKey)
width = s.getAsInt(widthKey)
height = s.getAsInt(heightKey)
horizontalAlign = s.getAsPosition(alignHorizontalKey)
verticalAlign = s.getAsPosition(alignVerticalKey)
topPadding = s.getAsInt(paddingTopKey)
rightPadding = s.getAsInt(paddingRightKey)
bottomPadding = s.getAsInt(paddingBottomKey)
leftPadding = s.getAsInt(paddingLeftKey)
colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
inline = s.getAsBool(inlineKey, false)
maxWidth = s.getAsInt(maxWidthKey)
maxHeight = s.getAsInt(maxHeightKey)
underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true)
strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true)
// Do we need to style whitespace (padding and space outside
// paragraphs) separately?
styleWhitespace = reverse
// Do we need to style spaces separately?
useSpaceStyler = underlineSpaces || strikethroughSpaces
)
if len(s.rules) == 0 {
return str
}
// Enable support for ANSI on the legacy Windows cmd.exe console. This is a
// no-op on non-Windows systems and on Windows runs only once.
enableLegacyWindowsANSI()
if bold {
te = te.Bold()
}
if italic {
te = te.Italic()
}
if underline {
te = te.Underline()
}
if reverse {
if reverse {
teWhitespace = teWhitespace.Reverse()
}
te = te.Reverse()
}
if blink {
te = te.Blink()
}
if faint {
te = te.Faint()
}
if fg != noColor {
fgc := fg.color()
te = te.Foreground(fgc)
if styleWhitespace {
teWhitespace = teWhitespace.Foreground(fgc)
}
if useSpaceStyler {
teSpace = teSpace.Foreground(fgc)
}
}
if bg != noColor {
bgc := bg.color()
te = te.Background(bgc)
if colorWhitespace {
teWhitespace = teWhitespace.Background(bgc)
}
if useSpaceStyler {
teSpace = teSpace.Background(bgc)
}
}
if underline {
te = te.Underline()
}
if strikethrough {
te = te.CrossOut()
}
if underlineSpaces {
teSpace = teSpace.Underline()
}
if strikethroughSpaces {
teSpace = teSpace.CrossOut()
}
// Strip newlines in single line mode
if inline {
str = strings.ReplaceAll(str, "\n", "")
}
// Word wrap
if !inline && width > 0 {
wrapAt := width - leftPadding - rightPadding
str = wordwrap.String(str, wrapAt)
str = wrap.String(str, wrapAt) // force-wrap long strings
}
// Render core text
{
var b strings.Builder
l := strings.Split(str, "\n")
for i := range l {
if useSpaceStyler {
// Look for spaces and apply a different styler
for _, r := range l[i] {
if unicode.IsSpace(r) {
b.WriteString(teSpace.Styled(string(r)))
continue
}
b.WriteString(te.Styled(string(r)))
}
} else {
b.WriteString(te.Styled(l[i]))
}
if i != len(l)-1 {
b.WriteRune('\n')
}
}
str = b.String()
}
// Padding
if !inline {
if leftPadding > 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = padLeft(str, leftPadding, st)
}
if rightPadding > 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = padRight(str, rightPadding, st)
}
if topPadding > 0 {
str = strings.Repeat("\n", topPadding) + str
}
if bottomPadding > 0 {
str += strings.Repeat("\n", bottomPadding)
}
}
// Height
if height > 0 {
str = alignTextVertical(str, verticalAlign, height, nil)
}
// Set alignment. This will also pad short lines with spaces so that all
// lines are the same length, so we run it under a few different conditions
// beyond alignment.
{
numLines := strings.Count(str, "\n")
if !(numLines == 0 && width == 0) {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = alignTextHorizontal(str, horizontalAlign, width, st)
}
}
if !inline {
str = s.applyBorder(str)
str = s.applyMargins(str, inline)
}
// Truncate according to MaxWidth
if maxWidth > 0 {
lines := strings.Split(str, "\n")
for i := range lines {
lines[i] = truncate.String(lines[i], uint(maxWidth))
}
str = strings.Join(lines, "\n")
}
// Truncate according to MaxHeight
if maxHeight > 0 {
lines := strings.Split(str, "\n")
str = strings.Join(lines[:min(maxHeight, len(lines))], "\n")
}
return str
} }
func (s Style) applyMargins(str string, inline bool) string { func (s Style) applyMargins(re *Renderer, str string, inline bool) string {
var ( var (
topMargin = s.getAsInt(marginTopKey) topMargin = s.getAsInt(marginTopKey)
rightMargin = s.getAsInt(marginRightKey) rightMargin = s.getAsInt(marginRightKey)
@ -384,7 +169,7 @@ func (s Style) applyMargins(str string, inline bool) string {
bgc := s.getAsColor(marginBackgroundKey) bgc := s.getAsColor(marginBackgroundKey)
if bgc != noColor { if bgc != noColor {
styler = styler.Background(bgc.color()) styler = styler.Background(re.color(bgc))
} }
// Add left and right margin // Add left and right margin

View File

@ -7,14 +7,26 @@ import (
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
// whitespace is a whitespace renderer. // Whitespace is a whitespace renderer.
type whitespace struct { type Whitespace struct {
re *Renderer
style termenv.Style style termenv.Style
chars string chars string
} }
// NewWhitespace creates a new whitespace renderer. The order of the options
// matters, it you'r using WithWhitespaceRenderer, make sure it comes first as
// other options might depend on it.
func NewWhitespace(opts ...WhitespaceOption) *Whitespace {
w := &Whitespace{re: renderer}
for _, opt := range opts {
opt(w)
}
return w
}
// Render whitespaces. // Render whitespaces.
func (w whitespace) render(width int) string { func (w Whitespace) render(width int) string {
if w.chars == "" { if w.chars == "" {
w.chars = " " w.chars = " "
} }
@ -44,25 +56,32 @@ func (w whitespace) render(width int) string {
} }
// WhitespaceOption sets a styling rule for rendering whitespace. // WhitespaceOption sets a styling rule for rendering whitespace.
type WhitespaceOption func(*whitespace) type WhitespaceOption func(*Whitespace)
// WithWhitespaceForeground sets the color of the characters in the whitespace. // WithWhitespaceForeground sets the color of the characters in the whitespace.
func WithWhitespaceForeground(c TerminalColor) WhitespaceOption { func WithWhitespaceForeground(c TerminalColor) WhitespaceOption {
return func(w *whitespace) { return func(w *Whitespace) {
w.style = w.style.Foreground(c.color()) w.style = w.style.Foreground(w.re.color(c))
} }
} }
// WithWhitespaceBackground sets the background color of the whitespace. // WithWhitespaceBackground sets the background color of the whitespace.
func WithWhitespaceBackground(c TerminalColor) WhitespaceOption { func WithWhitespaceBackground(c TerminalColor) WhitespaceOption {
return func(w *whitespace) { return func(w *Whitespace) {
w.style = w.style.Background(c.color()) w.style = w.style.Background(w.re.color(c))
} }
} }
// WithWhitespaceChars sets the characters to be rendered in the whitespace. // WithWhitespaceChars sets the characters to be rendered in the whitespace.
func WithWhitespaceChars(s string) WhitespaceOption { func WithWhitespaceChars(s string) WhitespaceOption {
return func(w *whitespace) { return func(w *Whitespace) {
w.chars = s w.chars = s
} }
} }
// WithWhitespaceRenderer sets the lipgloss renderer to be used for rendering.
func WithWhitespaceRenderer(r *Renderer) WhitespaceOption {
return func(w *Whitespace) {
w.re = r
}
}