Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make progress use lipgloss styles #543

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: make progress use lipgloss styles
nervo committed Jun 24, 2024
commit 0f8371e403f0ceda5632750cc5390ae95e765522
61 changes: 39 additions & 22 deletions progress/progress.go
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/harmonica"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/termenv"
)
@@ -72,13 +71,22 @@ func WithScaledGradient(colorA, colorB string) Option {
}

// WithSolidFill sets the progress to use a solid fill with the given color.
// Deprecated: use WithFillStyles.
func WithSolidFill(color string) Option {
return func(m *Model) {
m.FullColor = color
m.FullStyle = m.FullStyle.Foreground(lipgloss.Color(color))
m.useRamp = false
}
}

// WithFillStyles sets the styles used to construct the full and empty components of the progress bar.
func WithFillStyles(full lipgloss.Style, empty lipgloss.Style) Option {
return func(m *Model) {
m.FullStyle = full
m.EmptyStyle = empty
}
}

// WithFillCharacters sets the characters used to construct the full and empty components of the progress bar.
func WithFillCharacters(full rune, empty rune) Option {
return func(m *Model) {
@@ -116,9 +124,13 @@ func WithSpringOptions(frequency, damping float64) Option {
}

// WithColorProfile sets the color profile to use for the progress bar.
// Deprecated: use WithFillStyles with style's embedded color profile.
func WithColorProfile(p termenv.Profile) Option {
return func(m *Model) {
m.colorProfile = p
r := lipgloss.DefaultRenderer()
r.SetColorProfile(p)
m.FullStyle = r.NewStyle().Inherit(m.FullStyle)
m.EmptyStyle = r.NewStyle().Inherit(m.EmptyStyle)
}
}

@@ -141,12 +153,16 @@ type Model struct {
Width int

// "Filled" sections of the progress bar.
Full rune
Full rune
// Deprecated: use FullStyle with style's color
FullColor string
FullStyle lipgloss.Style

// "Empty" sections of the progress bar.
Empty rune
Empty rune
// Deprecated: use FullStyle with style's color
EmptyColor string
EmptyStyle lipgloss.Style

// Settings for rendering the numeric percentage.
ShowPercentage bool
@@ -169,9 +185,6 @@ type Model struct {
// of the progress bar. When false, the width of the gradient will be set
// to the full width of the progress bar.
scaleRamp bool

// Color profile for the progress bar.
colorProfile termenv.Profile
}

// New returns a model with default values.
@@ -180,12 +193,13 @@ func New(opts ...Option) Model {
id: nextID(),
Width: defaultWidth,
Full: '█',
FullColor: "#7571F9",
FullColor: "",
FullStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#7571F9")),
Empty: '░',
EmptyColor: "#606060",
EmptyColor: "",
EmptyStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#606060")),
ShowPercentage: true,
PercentFormat: " %3.0f%%",
colorProfile: termenv.ColorProfile(),
}

for _, opt := range opts {
@@ -285,7 +299,7 @@ func (m Model) View() string {
func (m Model) ViewAs(percent float64) string {
b := strings.Builder{}
percentView := m.percentageView(percent)
m.barView(&b, percent, ansi.StringWidth(percentView))
m.barView(&b, percent, lipgloss.Width(percentView))
b.WriteString(percentView)
return b.String()
}
@@ -319,20 +333,27 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
p = float64(i) / float64(tw-1)
}
c := m.rampColorA.BlendLuv(m.rampColorB, p).Hex()
b.WriteString(termenv.
String(string(m.Full)).
Foreground(m.color(c)).
String(),
b.WriteString(m.FullStyle.
Foreground(lipgloss.Color(c)).
Render(string(m.Full)),
)
}
} else {
// Solid fill
s := termenv.String(string(m.Full)).Foreground(m.color(m.FullColor)).String()
style := m.FullStyle
if m.FullColor != "" {
style = style.Foreground(lipgloss.Color(m.FullColor))
}
s := style.Render(string(m.Full))
b.WriteString(strings.Repeat(s, fw))
}

// Empty fill
e := termenv.String(string(m.Empty)).Foreground(m.color(m.EmptyColor)).String()
style := m.EmptyStyle
if m.EmptyColor != "" {
style = style.Foreground(lipgloss.Color(m.EmptyColor))
}
e := style.Render(string(m.Empty))
n := max(0, tw-fw)
b.WriteString(strings.Repeat(e, n))
}
@@ -360,10 +381,6 @@ func (m *Model) setRamp(colorA, colorB string, scaled bool) {
m.rampColorB = b
}

func (m Model) color(c string) termenv.Color {
return m.colorProfile.Color(c)
}

func max(a, b int) int {
if a > b {
return a
61 changes: 57 additions & 4 deletions progress/progress_test.go
Original file line number Diff line number Diff line change
@@ -4,15 +4,64 @@ import (
"strings"
"testing"

"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)

const (
AnsiReset = "\x1b[0m"
)

func TestSolid(t *testing.T) {
r := lipgloss.DefaultRenderer()
r.SetColorProfile(termenv.TrueColor)

tests := []struct {
name string
width int
expected string
}{
{
name: "width 3",
width: 3,
expected: `██░`,
},
{
name: "width 5",
width: 5,
expected: `███░░`,
},
{
name: "width 50",
width: 50,
expected: `█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░`,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p := New(
WithFillStyles(
r.NewStyle().Foreground(lipgloss.Color("#7571F9")),
r.NewStyle().Foreground(lipgloss.Color("#606060")),
),
WithoutPercentage(),
)
p.Width = test.width
res := p.ViewAs(0.5)

if res != test.expected {
t.Errorf("expected view %q, instead got %q", test.expected, res)
}
})
}
}

func TestGradient(t *testing.T) {

r := lipgloss.DefaultRenderer()
r.SetColorProfile(termenv.TrueColor)

colA := "#FF0000"
colB := "#00FF00"

@@ -21,7 +70,11 @@ func TestGradient(t *testing.T) {

for _, scale := range []bool{false, true} {
opts := []Option{
WithColorProfile(termenv.TrueColor), WithoutPercentage(),
WithFillStyles(
r.NewStyle().Foreground(lipgloss.Color("#7571F9")),
r.NewStyle().Foreground(lipgloss.Color("#606060")),
),
WithoutPercentage(),
}
if scale {
descr = "progress bar with scaled gradient"
@@ -36,17 +89,17 @@ func TestGradient(t *testing.T) {

// build the expected colors by colorizing an empty string and then cutting off the following reset sequence
sb := strings.Builder{}
sb.WriteString(termenv.String("").Foreground(p.color(colA)).String())
sb.WriteString(r.NewStyle().Foreground(lipgloss.Color(colA)).Render(""))
expFirst := strings.Split(sb.String(), AnsiReset)[0]
sb.Reset()
sb.WriteString(termenv.String("").Foreground(p.color(colB)).String())
sb.WriteString(r.NewStyle().Foreground(lipgloss.Color(colB)).Render(""))
expLast := strings.Split(sb.String(), AnsiReset)[0]

for _, width := range []int{3, 5, 50} {
p.Width = width
res := p.ViewAs(1.0)

// extract colors from the progrss bar by splitting at p.Full+AnsiReset, leaving us with just the color sequences
// extract colors from the progress bar by splitting at p.Full+AnsiReset, leaving us with just the color sequences
colors := strings.Split(res, string(p.Full)+AnsiReset)

// discard the last color, because it is empty (no new color comes after the last char of the bar)