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
Show file tree
Hide file tree
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
61 changes: 39 additions & 22 deletions progress/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
Expand Down
61 changes: 57 additions & 4 deletions progress/progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"
Expand All @@ -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)
Expand Down
Loading