mirror of
https://github.com/charmbracelet/lipgloss.git
synced 2024-09-11 14:07:21 +03:00
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:
parent
f754c404f6
commit
31ab45f810
36
README.md
36
README.md
@ -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("What’s for lunch?")
|
|
||||||
|
var str = lipgloss.Render(style, "What’s 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 you’ll 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)
|
||||||
|
16
borders.go
16
borders.go
@ -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
139
color.go
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
2
go.mod
@ -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
6
go.sum
@ -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=
|
||||||
|
12
position.go
12
position.go
@ -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
426
renderer.go
Normal 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
229
style.go
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user