mirror of
https://github.com/charmbracelet/lipgloss.git
synced 2024-08-17 01:40:36 +03:00
feat(renderer): use style renderer
lipgloss.Style now takes a renderer instance to be used to detect styles and colors based on its termenv.Output and terminal
This commit is contained in:
parent
31ab45f810
commit
b9c2626fe7
16
borders.go
16
borders.go
@ -196,7 +196,7 @@ func HiddenBorder() Border {
|
||||
return hiddenBorder
|
||||
}
|
||||
|
||||
func (s Style) applyBorder(re *Renderer, str string) string {
|
||||
func (s Style) applyBorder(str string) string {
|
||||
var (
|
||||
topSet = s.isSet(borderTopKey)
|
||||
rightSet = s.isSet(borderRightKey)
|
||||
@ -298,7 +298,7 @@ func (s Style) applyBorder(re *Renderer, str string) string {
|
||||
// Render top
|
||||
if hasTop {
|
||||
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
|
||||
top = styleBorder(re, top, topFG, topBG)
|
||||
top = s.styleBorder(top, topFG, topBG)
|
||||
out.WriteString(top)
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
@ -317,7 +317,7 @@ func (s Style) applyBorder(re *Renderer, str string) string {
|
||||
if leftIndex >= len(leftRunes) {
|
||||
leftIndex = 0
|
||||
}
|
||||
out.WriteString(styleBorder(re, r, leftFG, leftBG))
|
||||
out.WriteString(s.styleBorder(r, leftFG, leftBG))
|
||||
}
|
||||
out.WriteString(l)
|
||||
if hasRight {
|
||||
@ -326,7 +326,7 @@ func (s Style) applyBorder(re *Renderer, str string) string {
|
||||
if rightIndex >= len(rightRunes) {
|
||||
rightIndex = 0
|
||||
}
|
||||
out.WriteString(styleBorder(re, r, rightFG, rightBG))
|
||||
out.WriteString(s.styleBorder(r, rightFG, rightBG))
|
||||
}
|
||||
if i < len(lines)-1 {
|
||||
out.WriteRune('\n')
|
||||
@ -336,7 +336,7 @@ func (s Style) applyBorder(re *Renderer, str string) string {
|
||||
// Render bottom
|
||||
if hasBottom {
|
||||
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
|
||||
bottom = styleBorder(re, bottom, bottomFG, bottomBG)
|
||||
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(bottom)
|
||||
}
|
||||
@ -376,7 +376,7 @@ func renderHorizontalEdge(left, middle, right string, width int) string {
|
||||
}
|
||||
|
||||
// Apply foreground and background styling to a border.
|
||||
func styleBorder(re *Renderer, border string, fg, bg TerminalColor) string {
|
||||
func (s Style) styleBorder(border string, fg, bg TerminalColor) string {
|
||||
if fg == noColor && bg == noColor {
|
||||
return border
|
||||
}
|
||||
@ -384,10 +384,10 @@ func styleBorder(re *Renderer, border string, fg, bg TerminalColor) string {
|
||||
var style = termenv.Style{}
|
||||
|
||||
if fg != noColor {
|
||||
style = style.Foreground(re.color(fg))
|
||||
style = style.Foreground(fg.color(s.r))
|
||||
}
|
||||
if bg != noColor {
|
||||
style = style.Background(re.color(bg))
|
||||
style = style.Background(bg.color(s.r))
|
||||
}
|
||||
|
||||
return style.Styled(border)
|
||||
|
95
color.go
95
color.go
@ -1,16 +1,18 @@
|
||||
package lipgloss
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"fmt"
|
||||
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
// TerminalColor is a color intended to be rendered in the terminal.
|
||||
type TerminalColor interface {
|
||||
RGBA() (r, g, b, a uint32)
|
||||
color(*Renderer) termenv.Color
|
||||
}
|
||||
|
||||
var noColor = NoColor{}
|
||||
|
||||
// NoColor is used to specify the absence of color styling. When this is active
|
||||
// foreground colors will be rendered with the terminal's default text color,
|
||||
// and background colors will not be drawn at all.
|
||||
@ -20,15 +22,8 @@ type TerminalColor interface {
|
||||
// var style = someStyle.Copy().Background(lipgloss.NoColor{})
|
||||
type NoColor struct{}
|
||||
|
||||
var noColor = NoColor{}
|
||||
|
||||
// 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
|
||||
// black with 100% opacity.
|
||||
//
|
||||
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||
func (n NoColor) RGBA() (r, g, b, a uint32) {
|
||||
return 0x0, 0x0, 0x0, 0xFFFF
|
||||
func (NoColor) color(*Renderer) termenv.Color {
|
||||
return termenv.NoColor{}
|
||||
}
|
||||
|
||||
// Color specifies a color by hex or ANSI value. For example:
|
||||
@ -37,16 +32,8 @@ func (n NoColor) RGBA() (r, g, b, a uint32) {
|
||||
// hexColor := lipgloss.Color("#0000ff")
|
||||
type Color string
|
||||
|
||||
func (c Color) color() termenv.Color {
|
||||
return ColorProfile().Color(string(c))
|
||||
}
|
||||
|
||||
// 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 (c Color) RGBA() (r, g, b, a uint32) {
|
||||
return termenv.ConvertToRGB(c.color()).RGBA()
|
||||
func (c Color) color(r *Renderer) termenv.Color {
|
||||
return r.ColorProfile().Color(string(c))
|
||||
}
|
||||
|
||||
// ANSIColor is a color specified by an ANSI color value. It's merely syntactic
|
||||
@ -60,13 +47,8 @@ func (c Color) RGBA() (r, g, b, a uint32) {
|
||||
// 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()
|
||||
func (ac ANSIColor) color(r *Renderer) termenv.Color {
|
||||
return Color(fmt.Sprintf("%d", ac)).color(r)
|
||||
}
|
||||
|
||||
// AdaptiveColor provides color options for light and dark backgrounds. The
|
||||
@ -81,23 +63,11 @@ type AdaptiveColor struct {
|
||||
Dark string
|
||||
}
|
||||
|
||||
func (ac AdaptiveColor) value() string {
|
||||
if HasDarkBackground() {
|
||||
return ac.Dark
|
||||
func (ac AdaptiveColor) color(r *Renderer) termenv.Color {
|
||||
if r.HasDarkBackground() {
|
||||
return Color(ac.Dark).color(r)
|
||||
}
|
||||
return ac.Light
|
||||
}
|
||||
|
||||
func (ac AdaptiveColor) color() termenv.Color {
|
||||
return ColorProfile().Color(ac.value())
|
||||
}
|
||||
|
||||
// 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 AdaptiveColor) RGBA() (r, g, b, a uint32) {
|
||||
return termenv.ConvertToRGB(ac.color()).RGBA()
|
||||
return Color(ac.Light).color(r)
|
||||
}
|
||||
|
||||
// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
|
||||
@ -108,19 +78,21 @@ type CompleteColor struct {
|
||||
ANSI string
|
||||
}
|
||||
|
||||
func (c CompleteColor) value() string {
|
||||
switch ColorProfile() {
|
||||
func (c CompleteColor) color(r *Renderer) termenv.Color {
|
||||
p := r.ColorProfile()
|
||||
switch p {
|
||||
case termenv.TrueColor:
|
||||
return c.TrueColor
|
||||
return p.Color(c.TrueColor)
|
||||
case termenv.ANSI256:
|
||||
return c.ANSI256
|
||||
return p.Color(c.ANSI256)
|
||||
case termenv.ANSI:
|
||||
return c.ANSI
|
||||
return p.Color(c.ANSI)
|
||||
default:
|
||||
return ""
|
||||
return termenv.NoColor{}
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
func (c CompleteColor) color() termenv.Color {
|
||||
return ColorProfile().Color(c.value())
|
||||
}
|
||||
@ -134,6 +106,9 @@ func (c CompleteColor) RGBA() (r, g, b, a uint32) {
|
||||
}
|
||||
|
||||
// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color
|
||||
=======
|
||||
// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
|
||||
>>>>>>> f22900b20f84 (feat(renderer): use style renderer)
|
||||
// profiles, with separate options for light and dark backgrounds. Automatic
|
||||
// color degradation will not be performed.
|
||||
type CompleteAdaptiveColor struct {
|
||||
@ -141,21 +116,9 @@ type CompleteAdaptiveColor struct {
|
||||
Dark CompleteColor
|
||||
}
|
||||
|
||||
func (cac CompleteAdaptiveColor) value() string {
|
||||
if HasDarkBackground() {
|
||||
return cac.Dark.value()
|
||||
func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color {
|
||||
if r.HasDarkBackground() {
|
||||
return cac.Dark.color(r)
|
||||
}
|
||||
return cac.Light.value()
|
||||
}
|
||||
|
||||
func (cac CompleteAdaptiveColor) color() termenv.Color {
|
||||
return ColorProfile().Color(cac.value())
|
||||
}
|
||||
|
||||
// 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 (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) {
|
||||
return termenv.ConvertToRGB(cac.color()).RGBA()
|
||||
return cac.Light.color(r)
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func TestSetColorProfile(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r.SetColorProfile(tc.profile)
|
||||
style := NewStyle().Foreground(Color("#5A56E0"))
|
||||
res := Render(style, input)
|
||||
res := style.Render(input)
|
||||
|
||||
if res != tc.expected {
|
||||
t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
|
||||
@ -117,25 +117,25 @@ func TestRGBA(t *testing.T) {
|
||||
{
|
||||
termenv.TrueColor,
|
||||
true,
|
||||
AdaptiveColor{Dark: "#FF0000", Light: "#0000FF"},
|
||||
AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"},
|
||||
0xFF0000,
|
||||
},
|
||||
{
|
||||
termenv.TrueColor,
|
||||
false,
|
||||
AdaptiveColor{Dark: "#FF0000", Light: "#0000FF"},
|
||||
AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"},
|
||||
0x0000FF,
|
||||
},
|
||||
{
|
||||
termenv.TrueColor,
|
||||
true,
|
||||
AdaptiveColor{Dark: "9", Light: "21"},
|
||||
AdaptiveColor{Light: "21", Dark: "9"},
|
||||
0xFF0000,
|
||||
},
|
||||
{
|
||||
termenv.TrueColor,
|
||||
false,
|
||||
AdaptiveColor{Dark: "9", Light: "21"},
|
||||
AdaptiveColor{Light: "21", Dark: "9"},
|
||||
0x0000FF,
|
||||
},
|
||||
// lipgloss.CompleteColor
|
||||
@ -163,8 +163,8 @@ func TestRGBA(t *testing.T) {
|
||||
termenv.TrueColor,
|
||||
true,
|
||||
CompleteAdaptiveColor{
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
},
|
||||
0xFF0000,
|
||||
},
|
||||
@ -172,8 +172,8 @@ func TestRGBA(t *testing.T) {
|
||||
termenv.ANSI256,
|
||||
true,
|
||||
CompleteAdaptiveColor{
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
},
|
||||
0xFFFFFF,
|
||||
},
|
||||
@ -181,8 +181,8 @@ func TestRGBA(t *testing.T) {
|
||||
termenv.ANSI,
|
||||
true,
|
||||
CompleteAdaptiveColor{
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
},
|
||||
0x0000FF,
|
||||
},
|
||||
@ -191,8 +191,8 @@ func TestRGBA(t *testing.T) {
|
||||
termenv.TrueColor,
|
||||
false,
|
||||
CompleteAdaptiveColor{
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
},
|
||||
0x0000FF,
|
||||
},
|
||||
@ -200,8 +200,8 @@ func TestRGBA(t *testing.T) {
|
||||
termenv.ANSI256,
|
||||
false,
|
||||
CompleteAdaptiveColor{
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
},
|
||||
0x0000FF,
|
||||
},
|
||||
@ -209,18 +209,19 @@ func TestRGBA(t *testing.T) {
|
||||
termenv.ANSI,
|
||||
false,
|
||||
CompleteAdaptiveColor{
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
|
||||
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
|
||||
},
|
||||
0xFF0000,
|
||||
},
|
||||
}
|
||||
|
||||
r := DefaultRenderer()
|
||||
for i, tc := range tt {
|
||||
SetColorProfile(tc.profile)
|
||||
SetHasDarkBackground(tc.darkBg)
|
||||
r.SetColorProfile(tc.profile)
|
||||
r.SetHasDarkBackground(tc.darkBg)
|
||||
|
||||
r, g, b, _ := tc.input.RGBA()
|
||||
r, g, b, _ := termenv.ConvertToRGB(tc.input.color(r)).RGBA()
|
||||
o := uint(r/256)<<16 + uint(g/256)<<8 + uint(b/256)
|
||||
|
||||
if o != tc.expected {
|
||||
|
@ -7,11 +7,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/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -29,13 +29,12 @@ var (
|
||||
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
|
||||
special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}
|
||||
|
||||
divider = lipgloss.Render(
|
||||
lipgloss.NewStyle().
|
||||
Padding(0, 1).
|
||||
Foreground(subtle), "•")
|
||||
divider = lipgloss.NewStyle().
|
||||
Padding(0, 1).
|
||||
Foreground(subtle).Render("•")
|
||||
|
||||
url = func(s string) string {
|
||||
return lipgloss.Render(lipgloss.NewStyle().Foreground(special), s)
|
||||
return lipgloss.NewStyle().Foreground(special).Render(s)
|
||||
}
|
||||
|
||||
// Tabs.
|
||||
@ -124,15 +123,15 @@ var (
|
||||
Width(columnWidth + 1)
|
||||
|
||||
listHeader = func(s string) string {
|
||||
return lipgloss.Render(lipgloss.NewStyle().
|
||||
return lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderBottom(true).
|
||||
BorderForeground(subtle).
|
||||
MarginRight(2), s)
|
||||
MarginRight(2).Render(s)
|
||||
}
|
||||
|
||||
listItem = func(s string) string {
|
||||
return lipgloss.Render(lipgloss.NewStyle().PaddingLeft(2), s)
|
||||
return lipgloss.NewStyle().PaddingLeft(2).Render(s)
|
||||
}
|
||||
|
||||
checkMark = lipgloss.NewStyle().SetString("✓").
|
||||
@ -141,11 +140,10 @@ var (
|
||||
String()
|
||||
|
||||
listDone = func(s string) string {
|
||||
return checkMark + lipgloss.
|
||||
Render(lipgloss.NewStyle().
|
||||
Strikethrough(true).
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"}),
|
||||
s)
|
||||
return checkMark + lipgloss.NewStyle().
|
||||
Strikethrough(true).
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"}).
|
||||
Render(s)
|
||||
}
|
||||
|
||||
// Paragraphs/History.
|
||||
@ -197,13 +195,13 @@ func main() {
|
||||
{
|
||||
row := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
lipgloss.Render(activeTab, "Lip Gloss"),
|
||||
lipgloss.Render(tab, "Blush"),
|
||||
lipgloss.Render(tab, "Eye Shadow"),
|
||||
lipgloss.Render(tab, "Mascara"),
|
||||
lipgloss.Render(tab, "Foundation"),
|
||||
activeTab.Render("Lip Gloss"),
|
||||
tab.Render("Blush"),
|
||||
tab.Render("Eye Shadow"),
|
||||
tab.Render("Mascara"),
|
||||
tab.Render("Foundation"),
|
||||
)
|
||||
gap := lipgloss.Render(tabGap, strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2)))
|
||||
gap := tabGap.Render(strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2)))
|
||||
row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
|
||||
doc.WriteString(row + "\n\n")
|
||||
}
|
||||
@ -225,8 +223,8 @@ func main() {
|
||||
}
|
||||
|
||||
desc := lipgloss.JoinVertical(lipgloss.Left,
|
||||
lipgloss.Render(descStyle, "Style Definitions for Nice Terminal Layouts"),
|
||||
lipgloss.Render(infoStyle, "From Charm"+divider+url("https://github.com/charmbracelet/lipgloss")),
|
||||
descStyle.Render("Style Definitions for Nice Terminal Layouts"),
|
||||
infoStyle.Render("From Charm"+divider+url("https://github.com/charmbracelet/lipgloss")),
|
||||
)
|
||||
|
||||
row := lipgloss.JoinHorizontal(lipgloss.Top, title.String(), desc)
|
||||
@ -235,16 +233,16 @@ func main() {
|
||||
|
||||
// Dialog
|
||||
{
|
||||
okButton := lipgloss.Render(activeButtonStyle, "Yes")
|
||||
cancelButton := lipgloss.Render(buttonStyle, "Maybe")
|
||||
okButton := activeButtonStyle.Render("Yes")
|
||||
cancelButton := buttonStyle.Render("Maybe")
|
||||
|
||||
question := lipgloss.Render(lipgloss.NewStyle().Width(50).Align(lipgloss.Center), "Are you sure you want to eat marmalade?")
|
||||
question := lipgloss.NewStyle().Width(50).Align(lipgloss.Center).Render("Are you sure you want to eat marmalade?")
|
||||
buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton)
|
||||
ui := lipgloss.JoinVertical(lipgloss.Center, question, buttons)
|
||||
|
||||
dialog := lipgloss.Place(width, 9,
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
lipgloss.Render(dialogBoxStyle, ui),
|
||||
dialogBoxStyle.Render(ui),
|
||||
lipgloss.WithWhitespaceChars("猫咪"),
|
||||
lipgloss.WithWhitespaceForeground(subtle),
|
||||
)
|
||||
@ -269,17 +267,16 @@ func main() {
|
||||
}()
|
||||
|
||||
lists := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
lipgloss.Render(list,
|
||||
lipgloss.JoinVertical(lipgloss.Left,
|
||||
listHeader("Citrus Fruits to Try"),
|
||||
listDone("Grapefruit"),
|
||||
listDone("Yuzu"),
|
||||
listItem("Citron"),
|
||||
listItem("Kumquat"),
|
||||
listItem("Pomelo"),
|
||||
),
|
||||
list.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||
listHeader("Citrus Fruits to Try"),
|
||||
listDone("Grapefruit"),
|
||||
listDone("Yuzu"),
|
||||
listItem("Citron"),
|
||||
listItem("Kumquat"),
|
||||
listItem("Pomelo"),
|
||||
),
|
||||
lipgloss.Render(list.Copy().Width(columnWidth),
|
||||
),
|
||||
list.Copy().Width(columnWidth).Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left,
|
||||
listHeader("Actual Lip Gloss Vendors"),
|
||||
listItem("Glossier"),
|
||||
@ -303,9 +300,9 @@ func main() {
|
||||
|
||||
doc.WriteString(lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
lipgloss.Render(historyStyle.Copy().Align(lipgloss.Right), historyA),
|
||||
lipgloss.Render(historyStyle.Copy().Align(lipgloss.Center), historyB),
|
||||
lipgloss.Render(historyStyle.Copy().MarginRight(0), historyC),
|
||||
historyStyle.Copy().Align(lipgloss.Right).Render(historyA),
|
||||
historyStyle.Copy().Align(lipgloss.Center).Render(historyB),
|
||||
historyStyle.Copy().MarginRight(0).Render(historyC),
|
||||
))
|
||||
|
||||
doc.WriteString("\n\n")
|
||||
@ -315,13 +312,12 @@ func main() {
|
||||
{
|
||||
w := lipgloss.Width
|
||||
|
||||
statusKey := lipgloss.Render(statusStyle, "STATUS")
|
||||
encoding := lipgloss.Render(encodingStyle, "UTF-8")
|
||||
fishCake := lipgloss.Render(fishCakeStyle, "🍥 Fish Cake")
|
||||
statusVal := lipgloss.Render(
|
||||
statusText.Copy().
|
||||
Width(width-w(statusKey)-w(encoding)-w(fishCake)),
|
||||
"Ravishing")
|
||||
statusKey := statusStyle.Render("STATUS")
|
||||
encoding := encodingStyle.Render("UTF-8")
|
||||
fishCake := fishCakeStyle.Render("🍥 Fish Cake")
|
||||
statusVal := statusText.Copy().
|
||||
Width(width - w(statusKey) - w(encoding) - w(fishCake)).
|
||||
Render("Ravishing")
|
||||
|
||||
bar := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
statusKey,
|
||||
@ -330,7 +326,7 @@ func main() {
|
||||
fishCake,
|
||||
)
|
||||
|
||||
doc.WriteString(lipgloss.Render(statusBarStyle.Width(width), bar))
|
||||
doc.WriteString(statusBarStyle.Width(width).Render(bar))
|
||||
}
|
||||
|
||||
if physicalWidth > 0 {
|
||||
@ -338,8 +334,8 @@ func main() {
|
||||
}
|
||||
|
||||
// Okay, let's print it
|
||||
fmt.Println(lipgloss.Render(docStyle, doc.String()))
|
||||
}
|
||||
fmt.Println(docStyle.Render(doc.String()))
|
||||
} //
|
||||
|
||||
func colorGrid(xSteps, ySteps int) [][]string {
|
||||
x0y0, _ := colorful.Hex("#F25D94")
|
||||
|
4
get.go
4
get.go
@ -415,12 +415,12 @@ func (s Style) getAsBool(k propKey, defaultVal bool) bool {
|
||||
func (s Style) getAsColor(k propKey) TerminalColor {
|
||||
v, ok := s.rules[k]
|
||||
if !ok {
|
||||
return NoColor{}
|
||||
return noColor
|
||||
}
|
||||
if c, ok := v.(TerminalColor); ok {
|
||||
return c
|
||||
}
|
||||
return NoColor{}
|
||||
return noColor
|
||||
}
|
||||
|
||||
func (s Style) getAsInt(k propKey) int {
|
||||
|
332
renderer.go
332
renderer.go
@ -1,13 +1,6 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@ -16,7 +9,7 @@ var renderer = NewRenderer()
|
||||
// Renderer is a lipgloss terminal renderer.
|
||||
type Renderer struct {
|
||||
output *termenv.Output
|
||||
hasDarkBackground bool
|
||||
hasDarkBackground *bool
|
||||
}
|
||||
|
||||
// RendererOption is a function that can be used to configure a Renderer.
|
||||
@ -28,17 +21,13 @@ func DefaultRenderer() *Renderer {
|
||||
}
|
||||
|
||||
// NewRenderer creates a new Renderer.
|
||||
func NewRenderer(options ...RendererOption) *Renderer {
|
||||
r := &Renderer{}
|
||||
func NewRenderer(options ...func(r *Renderer)) *Renderer {
|
||||
r := &Renderer{
|
||||
output: termenv.DefaultOutput(),
|
||||
}
|
||||
for _, option := range options {
|
||||
option(r)
|
||||
}
|
||||
if r.output == nil {
|
||||
r.output = termenv.DefaultOutput()
|
||||
}
|
||||
if !r.hasDarkBackground {
|
||||
r.hasDarkBackground = r.output.HasDarkBackground()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@ -49,10 +38,10 @@ func WithOutput(output *termenv.Output) RendererOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDarkBackground forces the renderer to use a dark background.
|
||||
func WithDarkBackground() RendererOption {
|
||||
// WithDarkBackground can force the renderer to use a light/dark background.
|
||||
func WithDarkBackground(dark bool) RendererOption {
|
||||
return func(r *Renderer) {
|
||||
r.SetHasDarkBackground(true)
|
||||
r.SetHasDarkBackground(dark)
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,27 +104,17 @@ 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
|
||||
// HasDarkBackground returns whether or not the terminal has a dark background.
|
||||
func (r *Renderer) HasDarkBackground() bool {
|
||||
if r.hasDarkBackground != nil {
|
||||
return *r.hasDarkBackground
|
||||
}
|
||||
return r.output.HasDarkBackground()
|
||||
}
|
||||
|
||||
// SetHasDarkBackground sets the background color detection value for the
|
||||
@ -151,276 +130,15 @@ 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))
|
||||
// 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
|
||||
}
|
||||
|
33
renderer_test.go
Normal file
33
renderer_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package lipgloss
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
func TestRendererHasDarkBackground(t *testing.T) {
|
||||
r1 := NewRenderer(WithDarkBackground(false))
|
||||
if r1.HasDarkBackground() {
|
||||
t.Error("Expected renderer to have light background")
|
||||
}
|
||||
r2 := NewRenderer(WithDarkBackground(true))
|
||||
if !r2.HasDarkBackground() {
|
||||
t.Error("Expected renderer to have dark background")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRendererWithOutput(t *testing.T) {
|
||||
f, err := os.Create(t.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
output := termenv.NewOutput(f, termenv.WithProfile(termenv.TrueColor))
|
||||
r := NewRenderer(WithOutput(output))
|
||||
if r.output.Profile != termenv.TrueColor {
|
||||
t.Error("Expected renderer to use true color")
|
||||
}
|
||||
}
|
260
style.go
260
style.go
@ -2,7 +2,11 @@ package lipgloss
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/muesli/reflow/wordwrap"
|
||||
"github.com/muesli/reflow/wrap"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
@ -71,15 +75,25 @@ const (
|
||||
// A set of properties.
|
||||
type rules map[propKey]interface{}
|
||||
|
||||
// NewStyle returns a new, empty Style. While it's syntactic sugar for the
|
||||
// NewStyle returns a new, empty Style. While it's syntactic sugar for the
|
||||
// Style{} primitive, it's recommended to use this function for creating styles
|
||||
// in case the underlying implementation changes.
|
||||
func NewStyle() Style {
|
||||
return Style{}
|
||||
// in case the underlying implementation changes. It takes an optional string
|
||||
// value to be set as the underlying string value for this style.
|
||||
func NewStyle(strs ...string) Style {
|
||||
return renderer.NewStyle(strs...)
|
||||
}
|
||||
|
||||
// NewStyle returns a new, empty Style. While it's syntactic sugar for the
|
||||
// Style{} primitive, it's recommended to use this function for creating styles
|
||||
// in case the underlying implementation changes. It takes an optional string
|
||||
// value to be set as the underlying string value for this style.
|
||||
func (r *Renderer) NewStyle(strs ...string) Style {
|
||||
return Style{r: r}.SetString(strs...)
|
||||
}
|
||||
|
||||
// Style contains a set of rules that comprise a style as a whole.
|
||||
type Style struct {
|
||||
r *Renderer
|
||||
rules map[propKey]interface{}
|
||||
value string
|
||||
}
|
||||
@ -89,8 +103,8 @@ type Style struct {
|
||||
// a convenience for cases when having a stringer implementation is handy, such
|
||||
// as when using fmt.Sprintf. You can also simply define a style and render out
|
||||
// strings directly with Style.Render.
|
||||
func (s Style) SetString(str string) Style {
|
||||
s.value = str
|
||||
func (s Style) SetString(strs ...string) Style {
|
||||
s.value = strings.Join(strs, " ")
|
||||
return s
|
||||
}
|
||||
|
||||
@ -102,10 +116,8 @@ func (s Style) Value() string {
|
||||
// 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
|
||||
// Style.SetString prior to using this method.
|
||||
//
|
||||
// Deprecated: Use Render(Style, string) instead.
|
||||
func (s Style) String() string {
|
||||
return s.Render(s.value)
|
||||
return s.Render()
|
||||
}
|
||||
|
||||
// Copy returns a copy of this style, including any underlying string values.
|
||||
@ -115,6 +127,7 @@ func (s Style) Copy() Style {
|
||||
for k, v := range s.rules {
|
||||
o.rules[k] = v
|
||||
}
|
||||
o.r = s.r
|
||||
o.value = s.value
|
||||
return o
|
||||
}
|
||||
@ -151,13 +164,230 @@ func (s Style) Inherit(i Style) Style {
|
||||
}
|
||||
|
||||
// Render applies the defined style formatting to a given string.
|
||||
//
|
||||
// Deprecated: Use Render(Style, string) instead.
|
||||
func (s Style) Render(str string) string {
|
||||
return renderer.Render(s, str)
|
||||
func (s Style) Render(strs ...string) string {
|
||||
if s.value != "" {
|
||||
strs = append([]string{s.value}, strs...)
|
||||
}
|
||||
|
||||
var (
|
||||
str = strings.Join(strs, " ")
|
||||
|
||||
te = s.r.ColorProfile().String()
|
||||
teSpace = s.r.ColorProfile().String()
|
||||
teWhitespace = s.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 {
|
||||
te = te.Foreground(fg.color(s.r))
|
||||
if styleWhitespace {
|
||||
teWhitespace = teWhitespace.Foreground(fg.color(s.r))
|
||||
}
|
||||
if useSpaceStyler {
|
||||
teSpace = teSpace.Foreground(fg.color(s.r))
|
||||
}
|
||||
}
|
||||
|
||||
if bg != noColor {
|
||||
te = te.Background(bg.color(s.r))
|
||||
if colorWhitespace {
|
||||
teWhitespace = teWhitespace.Background(bg.color(s.r))
|
||||
}
|
||||
if useSpaceStyler {
|
||||
teSpace = teSpace.Background(bg.color(s.r))
|
||||
}
|
||||
}
|
||||
|
||||
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(re *Renderer, str string, inline bool) string {
|
||||
func (s Style) applyMargins(str string, inline bool) string {
|
||||
var (
|
||||
topMargin = s.getAsInt(marginTopKey)
|
||||
rightMargin = s.getAsInt(marginRightKey)
|
||||
@ -169,7 +399,7 @@ func (s Style) applyMargins(re *Renderer, str string, inline bool) string {
|
||||
|
||||
bgc := s.getAsColor(marginBackgroundKey)
|
||||
if bgc != noColor {
|
||||
styler = styler.Background(re.color(bgc))
|
||||
styler = styler.Background(bgc.color(s.r))
|
||||
}
|
||||
|
||||
// Add left and right margin
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestStyleRender(t *testing.T) {
|
||||
renderer.SetColorProfile(termenv.TrueColor)
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
@ -40,9 +41,9 @@ func TestStyleRender(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
SetColorProfile(termenv.TrueColor)
|
||||
for i, tc := range tt {
|
||||
res := tc.style.Render("hello")
|
||||
s := tc.style.Copy().SetString("hello")
|
||||
res := s.Render()
|
||||
if res != tc.expected {
|
||||
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
|
||||
i, tc.expected, formatEscapes(tc.expected),
|
||||
@ -245,6 +246,47 @@ func TestStyleUnset(t *testing.T) {
|
||||
requireFalse(t, s.GetBorderLeft())
|
||||
}
|
||||
|
||||
func TestStyleValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
style Style
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
style: NewStyle(),
|
||||
expected: "foo",
|
||||
},
|
||||
{
|
||||
name: "set string",
|
||||
style: NewStyle().SetString("bar"),
|
||||
expected: "bar foo",
|
||||
},
|
||||
{
|
||||
name: "set string with bold",
|
||||
style: NewStyle().SetString("bar").Bold(true),
|
||||
expected: "\x1b[1mbar foo\x1b[0m",
|
||||
},
|
||||
{
|
||||
name: "new style with string",
|
||||
style: NewStyle("bar", "foobar"),
|
||||
expected: "bar foobar foo",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tt {
|
||||
res := tc.style.Render("foo")
|
||||
if res != tc.expected {
|
||||
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
|
||||
i, tc.expected, formatEscapes(tc.expected),
|
||||
res, formatEscapes(res))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkStyleRender(b *testing.B) {
|
||||
s := NewStyle().
|
||||
Bold(true).
|
||||
|
@ -58,17 +58,24 @@ func (w Whitespace) render(width int) string {
|
||||
// WhitespaceOption sets a styling rule for rendering whitespace.
|
||||
type WhitespaceOption func(*Whitespace)
|
||||
|
||||
// WithWhitespaceRenderer sets the lipgloss renderer to be used for rendering.
|
||||
func WithWhitespaceRenderer(r *Renderer) WhitespaceOption {
|
||||
return func(w *Whitespace) {
|
||||
w.re = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithWhitespaceForeground sets the color of the characters in the whitespace.
|
||||
func WithWhitespaceForeground(c TerminalColor) WhitespaceOption {
|
||||
return func(w *Whitespace) {
|
||||
w.style = w.style.Foreground(w.re.color(c))
|
||||
w.style = w.style.Foreground(c.color(w.re))
|
||||
}
|
||||
}
|
||||
|
||||
// WithWhitespaceBackground sets the background color of the whitespace.
|
||||
func WithWhitespaceBackground(c TerminalColor) WhitespaceOption {
|
||||
return func(w *Whitespace) {
|
||||
w.style = w.style.Background(w.re.color(c))
|
||||
w.style = w.style.Background(c.color(w.re))
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,10 +85,3 @@ func WithWhitespaceChars(s string) WhitespaceOption {
|
||||
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