mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 04:36:07 +00:00
feat(progress): support multiple stops and improved blend algorithm (#838)
This commit is contained in:
@@ -11,7 +11,6 @@ require (
|
|||||||
github.com/charmbracelet/x/ansi v0.11.0
|
github.com/charmbracelet/x/ansi v0.11.0
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
|
||||||
github.com/mattn/go-runewidth v0.0.19
|
github.com/mattn/go-runewidth v0.0.19
|
||||||
github.com/rivo/uniseg v0.4.7
|
github.com/rivo/uniseg v0.4.7
|
||||||
github.com/sahilm/fuzzy v0.1.1
|
github.com/sahilm/fuzzy v0.1.1
|
||||||
@@ -28,6 +27,7 @@ require (
|
|||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
|||||||
+159
-74
@@ -13,9 +13,14 @@ import (
|
|||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/charmbracelet/harmonica"
|
"github.com/charmbracelet/harmonica"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ColorFunc is a function that can be used to dynamically fill the progress
|
||||||
|
// bar based on the current percentage. total is the total filled percentage,
|
||||||
|
// and current is the current percentage that is actively being filled with a
|
||||||
|
// color.
|
||||||
|
type ColorFunc func(total, current float64) color.Color
|
||||||
|
|
||||||
// Internal ID management. Used during animating to assure that frame messages
|
// Internal ID management. Used during animating to assure that frame messages
|
||||||
// can only be received by progress components that sent them.
|
// can only be received by progress components that sent them.
|
||||||
var lastID int64
|
var lastID int64
|
||||||
@@ -25,55 +30,107 @@ func nextID() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// DefaultFullCharHalfBlock is the default character used to fill the progress
|
||||||
|
// bar. It is a half block, which allows more granular color blending control,
|
||||||
|
// by having a different foreground and background color, doubling blending
|
||||||
|
// resolution.
|
||||||
|
DefaultFullCharHalfBlock = '▌'
|
||||||
|
|
||||||
|
// DefaultFullCharFullBlock can also be used as a fill character for the
|
||||||
|
// progress bar. Use this to disable the higher resolution blending which is
|
||||||
|
// enabled when using [DefaultFullCharHalfBlock].
|
||||||
|
DefaultFullCharFullBlock = '█'
|
||||||
|
|
||||||
|
// DefaultEmptyCharBlock is the default character used to fill the empty
|
||||||
|
// portion of the progress bar.
|
||||||
|
DefaultEmptyCharBlock = '░'
|
||||||
|
|
||||||
fps = 60
|
fps = 60
|
||||||
defaultWidth = 40
|
defaultWidth = 40
|
||||||
defaultFrequency = 18.0
|
defaultFrequency = 18.0
|
||||||
defaultDamping = 1.0
|
defaultDamping = 1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option is used to set options in New. For example:
|
var (
|
||||||
|
defaultBlendStart = lipgloss.Color("#5A56E0") // Purple haze.
|
||||||
|
defaultBlendEnd = lipgloss.Color("#EE6FF8") // Neon pink.
|
||||||
|
defaultFullColor = lipgloss.Color("#7571F9") // Blueberry.
|
||||||
|
defaultEmptyColor = lipgloss.Color("#606060") // Slate gray.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option is used to set options in [New]. For example:
|
||||||
//
|
//
|
||||||
// progress := New(
|
// progress := New(
|
||||||
// WithRamp("#ff0000", "#0000ff"),
|
// WithColors(
|
||||||
// WithoutPercentage(),
|
// lipgloss.Color("#5A56E0"),
|
||||||
// )
|
// lipgloss.Color("#EE6FF8"),
|
||||||
|
// ),
|
||||||
|
// WithoutPercentage(),
|
||||||
|
// )
|
||||||
type Option func(*Model)
|
type Option func(*Model)
|
||||||
|
|
||||||
// WithDefaultGradient sets a gradient fill with default colors.
|
// WithDefaultBlend sets a default blend of colors, which is a blend of purple
|
||||||
func WithDefaultGradient() Option {
|
// haze to neon pink.
|
||||||
return WithGradient("#5A56E0", "#EE6FF8")
|
func WithDefaultBlend() Option {
|
||||||
|
return WithColors(
|
||||||
|
defaultBlendStart,
|
||||||
|
defaultBlendEnd,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithGradient sets a gradient fill blending between two colors.
|
// WithColors sets the colors to use to fill the progress bar. Depending on the
|
||||||
func WithGradient(colorA, colorB string) Option {
|
// number of colors passed in, will determine whether to use a solid fill or a
|
||||||
|
// blend of colors.
|
||||||
|
//
|
||||||
|
// - 0 colors: clears all previously set colors, setting them back to defaults.
|
||||||
|
// - 1 color: uses a solid fill with the given color.
|
||||||
|
// - 2+ colors: uses a blend of the provided colors.
|
||||||
|
func WithColors(colors ...color.Color) Option {
|
||||||
|
if len(colors) == 0 {
|
||||||
|
return func(m *Model) {
|
||||||
|
m.FullColor = defaultFullColor
|
||||||
|
m.blend = nil
|
||||||
|
m.colorFunc = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(colors) == 1 {
|
||||||
|
return func(m *Model) {
|
||||||
|
m.FullColor = colors[0]
|
||||||
|
m.colorFunc = nil
|
||||||
|
m.blend = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return func(m *Model) {
|
return func(m *Model) {
|
||||||
m.setRamp(colorA, colorB, false)
|
m.blend = colors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDefaultScaledGradient sets a gradient with default colors, and scales the
|
// WithColorFunc sets a function that can be used to dynamically fill the progress
|
||||||
// gradient to fit the filled portion of the ramp.
|
// bar based on the current percentage. total is the total filled percentage, and
|
||||||
func WithDefaultScaledGradient() Option {
|
// current is the current percentage that is actively being filled with a color.
|
||||||
return WithScaledGradient("#5A56E0", "#EE6FF8")
|
// When specified, this overrides any other defined colors and scaling.
|
||||||
}
|
//
|
||||||
|
// Example: A progress bar that changes color based on the total completed
|
||||||
// WithScaledGradient scales the gradient to fit the width of the filled portion of
|
// percentage:
|
||||||
// the progress bar.
|
//
|
||||||
func WithScaledGradient(colorA, colorB string) Option {
|
// WithColorFunc(func(total, current float64) color.Color {
|
||||||
|
// if total <= 0.3 {
|
||||||
|
// return lipgloss.Color("#FF0000")
|
||||||
|
// }
|
||||||
|
// if total <= 0.7 {
|
||||||
|
// return lipgloss.Color("#00FF00")
|
||||||
|
// }
|
||||||
|
// return lipgloss.Color("#0000FF")
|
||||||
|
// }),
|
||||||
|
func WithColorFunc(fn ColorFunc) Option {
|
||||||
return func(m *Model) {
|
return func(m *Model) {
|
||||||
m.setRamp(colorA, colorB, true)
|
m.colorFunc = fn
|
||||||
|
m.blend = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSolidFill sets the progress to use a solid fill with the given color.
|
// WithFillCharacters sets the characters used to construct the full and empty
|
||||||
func WithSolidFill(color color.Color) Option {
|
// components of the progress bar.
|
||||||
return func(m *Model) {
|
|
||||||
m.FullColor = color
|
|
||||||
m.useRamp = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFillCharacters sets the characters used to construct the full and empty components of the progress bar.
|
|
||||||
func WithFillCharacters(full rune, empty rune) Option {
|
func WithFillCharacters(full rune, empty rune) Option {
|
||||||
return func(m *Model) {
|
return func(m *Model) {
|
||||||
m.Full = full
|
m.Full = full
|
||||||
@@ -93,7 +150,7 @@ func WithoutPercentage() Option {
|
|||||||
// waiting for a tea.WindowSizeMsg.
|
// waiting for a tea.WindowSizeMsg.
|
||||||
func WithWidth(w int) Option {
|
func WithWidth(w int) Option {
|
||||||
return func(m *Model) {
|
return func(m *Model) {
|
||||||
m.width = w
|
m.SetWidth(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +166,17 @@ func WithSpringOptions(frequency, damping float64) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithScaled sets whether to scale the blend/gradient to fit the width of only
|
||||||
|
// the filled portion of the progress bar. The default is false, which means the
|
||||||
|
// percentage must be 100% to see the full color blend/gradient.
|
||||||
|
//
|
||||||
|
// This is ignored when not using blending/multiple colors.
|
||||||
|
func WithScaled(enabled bool) Option {
|
||||||
|
return func(m *Model) {
|
||||||
|
m.scaleBlend = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FrameMsg indicates that an animation step should occur.
|
// FrameMsg indicates that an animation step should occur.
|
||||||
type FrameMsg struct {
|
type FrameMsg struct {
|
||||||
id int
|
id int
|
||||||
@@ -147,15 +215,17 @@ type Model struct {
|
|||||||
targetPercent float64 // percent to which we're animating
|
targetPercent float64 // percent to which we're animating
|
||||||
velocity float64
|
velocity float64
|
||||||
|
|
||||||
// Gradient settings
|
// Blend of colors to use. When len < 1, we use FullColor.
|
||||||
useRamp bool
|
blend []color.Color
|
||||||
rampColorA colorful.Color
|
|
||||||
rampColorB colorful.Color
|
|
||||||
|
|
||||||
// When true, we scale the gradient to fit the width of the filled section
|
// When true, we scale the blended colors to fit the width of the filled
|
||||||
// of the progress bar. When false, the width of the gradient will be set
|
// section of the progress bar. When false, the width of the blend will be
|
||||||
// to the full width of the progress bar.
|
// set to the full width of the progress bar.
|
||||||
scaleRamp bool
|
scaleBlend bool
|
||||||
|
|
||||||
|
// colorFunc is used to dynamically fill the progress bar based on the
|
||||||
|
// current percentage.
|
||||||
|
colorFunc ColorFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a model with default values.
|
// New returns a model with default values.
|
||||||
@@ -163,10 +233,10 @@ func New(opts ...Option) Model {
|
|||||||
m := Model{
|
m := Model{
|
||||||
id: nextID(),
|
id: nextID(),
|
||||||
width: defaultWidth,
|
width: defaultWidth,
|
||||||
Full: '█',
|
Full: DefaultFullCharHalfBlock,
|
||||||
FullColor: lipgloss.Color("#7571F9"),
|
FullColor: defaultFullColor,
|
||||||
Empty: '░',
|
Empty: DefaultEmptyCharBlock,
|
||||||
EmptyColor: lipgloss.Color("#606060"),
|
EmptyColor: defaultEmptyColor,
|
||||||
ShowPercentage: true,
|
ShowPercentage: true,
|
||||||
PercentFormat: " %3.0f%%",
|
PercentFormat: " %3.0f%%",
|
||||||
}
|
}
|
||||||
@@ -288,35 +358,62 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
|
|||||||
var (
|
var (
|
||||||
tw = max(0, m.width-textWidth) // total width
|
tw = max(0, m.width-textWidth) // total width
|
||||||
fw = int(math.Round((float64(tw) * percent))) // filled width
|
fw = int(math.Round((float64(tw) * percent))) // filled width
|
||||||
p float64
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fw = max(0, min(tw, fw))
|
fw = max(0, min(tw, fw))
|
||||||
|
|
||||||
if m.useRamp {
|
isHalfBlock := m.Full == DefaultFullCharHalfBlock
|
||||||
// Gradient fill
|
|
||||||
|
if m.colorFunc != nil { //nolint:nestif
|
||||||
|
var style lipgloss.Style
|
||||||
|
var current float64
|
||||||
|
halfBlockPerc := 0.5 / float64(tw)
|
||||||
for i := range fw {
|
for i := range fw {
|
||||||
if fw == 1 {
|
current = float64(i) / float64(tw)
|
||||||
// this is up for debate: in a gradient of width=1, should the
|
style = style.Foreground(m.colorFunc(percent, current))
|
||||||
// single character rendered be the first color, the last color
|
if isHalfBlock {
|
||||||
// or exactly 50% in between? I opted for 50%
|
style = style.Background(m.colorFunc(percent, min(current+halfBlockPerc, 1)))
|
||||||
p = 0.5
|
|
||||||
} else if m.scaleRamp {
|
|
||||||
p = float64(i) / float64(fw-1)
|
|
||||||
} else {
|
|
||||||
p = float64(i) / float64(tw-1)
|
|
||||||
}
|
}
|
||||||
c := m.rampColorA.BlendLuv(m.rampColorB, p)
|
b.WriteString(style.Render(string(m.Full)))
|
||||||
b.WriteString(lipgloss.NewStyle().Foreground(c).Render(string(m.Full)))
|
}
|
||||||
|
} else if len(m.blend) > 0 {
|
||||||
|
var blend []color.Color
|
||||||
|
|
||||||
|
multiplier := 1
|
||||||
|
if isHalfBlock {
|
||||||
|
multiplier = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.scaleBlend {
|
||||||
|
blend = lipgloss.Blend1D(fw*multiplier, m.blend...)
|
||||||
|
} else {
|
||||||
|
blend = lipgloss.Blend1D(tw*multiplier, m.blend...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blend fill.
|
||||||
|
var blendIndex int
|
||||||
|
for i := range fw {
|
||||||
|
if !isHalfBlock {
|
||||||
|
b.WriteString(lipgloss.NewStyle().
|
||||||
|
Foreground(blend[i]).
|
||||||
|
Render(string(m.Full)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(lipgloss.NewStyle().
|
||||||
|
Foreground(blend[blendIndex]).
|
||||||
|
Background(blend[blendIndex+1]).
|
||||||
|
Render(string(m.Full)))
|
||||||
|
blendIndex += 2
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Solid fill
|
// Solid fill.
|
||||||
b.WriteString(lipgloss.NewStyle().
|
b.WriteString(lipgloss.NewStyle().
|
||||||
Foreground(m.FullColor).
|
Foreground(m.FullColor).
|
||||||
Render(strings.Repeat(string(m.Full), fw)))
|
Render(strings.Repeat(string(m.Full), fw)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty fill
|
// Empty fill.
|
||||||
n := max(0, tw-fw)
|
n := max(0, tw-fw)
|
||||||
b.WriteString(lipgloss.NewStyle().
|
b.WriteString(lipgloss.NewStyle().
|
||||||
Foreground(m.EmptyColor).
|
Foreground(m.EmptyColor).
|
||||||
@@ -333,20 +430,8 @@ func (m Model) percentageView(percent float64) string {
|
|||||||
return percentage
|
return percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) setRamp(colorA, colorB string, scaled bool) {
|
// IsAnimating returns false if the progress bar reached equilibrium and is no
|
||||||
// In the event of an error colors here will default to black. For
|
// longer animating.
|
||||||
// usability's sake, and because such an error is only cosmetic, we're
|
|
||||||
// ignoring the error.
|
|
||||||
a, _ := colorful.Hex(colorA)
|
|
||||||
b, _ := colorful.Hex(colorB)
|
|
||||||
|
|
||||||
m.useRamp = true
|
|
||||||
m.scaleRamp = scaled
|
|
||||||
m.rampColorA = a
|
|
||||||
m.rampColorB = b
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAnimating returns false if the progress bar reached equilibrium and is no longer animating.
|
|
||||||
func (m *Model) IsAnimating() bool {
|
func (m *Model) IsAnimating() bool {
|
||||||
dist := math.Abs(m.percentShown - m.targetPercent)
|
dist := math.Abs(m.percentShown - m.targetPercent)
|
||||||
return !(dist < 0.001 && m.velocity < 0.01)
|
return !(dist < 0.001 && m.velocity < 0.01)
|
||||||
|
|||||||
+82
-52
@@ -1,64 +1,94 @@
|
|||||||
package progress
|
package progress
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"image/color"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/charmbracelet/x/exp/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func TestBlend(t *testing.T) {
|
||||||
AnsiReset = "\x1b[m"
|
tests := []struct {
|
||||||
)
|
name string
|
||||||
|
options []Option
|
||||||
|
width int
|
||||||
|
percent float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "10w-red-to-green-50perc",
|
||||||
|
options: []Option{
|
||||||
|
WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
|
||||||
|
WithScaled(false),
|
||||||
|
WithoutPercentage(),
|
||||||
|
},
|
||||||
|
width: 10,
|
||||||
|
percent: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "10w-red-to-green-50perc-full-block",
|
||||||
|
options: []Option{
|
||||||
|
WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
|
||||||
|
WithFillCharacters('█', DefaultEmptyCharBlock),
|
||||||
|
WithoutPercentage(),
|
||||||
|
},
|
||||||
|
width: 10,
|
||||||
|
percent: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "30w-red-to-green-100perc",
|
||||||
|
options: []Option{
|
||||||
|
WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
|
||||||
|
WithScaled(false),
|
||||||
|
WithoutPercentage(),
|
||||||
|
},
|
||||||
|
width: 30,
|
||||||
|
percent: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "10w-red-to-green-scaled-50perc",
|
||||||
|
options: []Option{
|
||||||
|
WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
|
||||||
|
WithScaled(true),
|
||||||
|
WithoutPercentage(),
|
||||||
|
},
|
||||||
|
width: 10,
|
||||||
|
percent: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "30w-red-to-green-scaled-100perc",
|
||||||
|
options: []Option{
|
||||||
|
WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#00FF00")),
|
||||||
|
WithScaled(true),
|
||||||
|
WithoutPercentage(),
|
||||||
|
},
|
||||||
|
width: 30,
|
||||||
|
percent: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "30w-colorfunc-rgb-100perc",
|
||||||
|
options: []Option{
|
||||||
|
WithColorFunc(func(_, current float64) color.Color {
|
||||||
|
if current <= 0.3 {
|
||||||
|
return lipgloss.Color("#FF0000")
|
||||||
|
}
|
||||||
|
if current <= 0.7 {
|
||||||
|
return lipgloss.Color("#00FF00")
|
||||||
|
}
|
||||||
|
return lipgloss.Color("#0000FF")
|
||||||
|
}),
|
||||||
|
WithoutPercentage(),
|
||||||
|
},
|
||||||
|
width: 30,
|
||||||
|
percent: 1.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func TestGradient(t *testing.T) {
|
for _, tt := range tests {
|
||||||
colA := "#FF0000"
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
colB := "#00FF00"
|
p := New(tt.options...)
|
||||||
|
p.SetWidth(tt.width)
|
||||||
var p Model
|
golden.RequireEqual(t, []byte(p.ViewAs(tt.percent)))
|
||||||
var descr string
|
|
||||||
|
|
||||||
for _, scale := range []bool{false, true} {
|
|
||||||
opts := []Option{
|
|
||||||
WithoutPercentage(),
|
|
||||||
}
|
|
||||||
if scale {
|
|
||||||
descr = "progress bar with scaled gradient"
|
|
||||||
opts = append(opts, WithScaledGradient(colA, colB))
|
|
||||||
} else {
|
|
||||||
descr = "progress bar with gradient"
|
|
||||||
opts = append(opts, WithGradient(colA, colB))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(descr, func(t *testing.T) {
|
|
||||||
p = New(opts...)
|
|
||||||
|
|
||||||
// build the expected colors by colorizing an empty string and then cutting off the following reset sequence
|
|
||||||
sb := strings.Builder{}
|
|
||||||
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colA)).String())
|
|
||||||
expFirst := strings.Split(sb.String(), AnsiReset)[0]
|
|
||||||
sb.Reset()
|
|
||||||
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colB)).String())
|
|
||||||
expLast := strings.Split(sb.String(), AnsiReset)[0]
|
|
||||||
|
|
||||||
for _, width := range []int{3, 5, 50} {
|
|
||||||
p.SetWidth(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
|
|
||||||
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)
|
|
||||||
colors = colors[0 : len(colors)-1]
|
|
||||||
|
|
||||||
if expFirst != colors[0] {
|
|
||||||
t.Errorf("expected first color of bar to be first gradient color %q, instead got %q", expFirst, colors[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if expLast != colors[len(colors)-1] {
|
|
||||||
t.Errorf("expected last color of bar to be second gradient color %q, instead got %q", expLast, colors[len(colors)-1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
[38;2;255;0;0m█[m[38;2;246;78;0m█[m[38;2;236;111;0m█[m[38;2;223;138;0m█[m[38;2;209;161;0m█[m[38;2;96;96;96m░░░░░[m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[38;2;255;0;0;48;2;251;52;0m▌[m[38;2;247;76;0;48;2;242;93;0m▌[m[38;2;237;108;0;48;2;231;122;0m▌[m[38;2;225;134;0;48;2;219;145;0m▌[m[38;2;212;156;0;48;2;205;166;0m▌[m[38;2;96;96;96m░░░░░[m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[38;2;255;0;0;48;2;246;78;0m▌[m[38;2;236;111;0;48;2;223;138;0m▌[m[38;2;209;161;0;48;2;192;181;0m▌[m[38;2;171;201;0;48;2;144;220;0m▌[m[38;2;106;238;0;48;2;0;255;0m▌[m[38;2;96;96;96m░░░░░[m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;255;0;0m▌[m[38;2;255;0;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;255;0m▌[m[38;2;0;255;0;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;0;0;255;48;2;0;0;255m▌[m[38;2;96;96;96m[m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[38;2;255;0;0;48;2;254;27;0m▌[m[38;2;253;41;0;48;2;251;51;0m▌[m[38;2;250;60;0;48;2;248;68;0m▌[m[38;2;247;74;0;48;2;245;81;0m▌[m[38;2;244;86;0;48;2;242;92;0m▌[m[38;2;241;97;0;48;2;239;102;0m▌[m[38;2;237;106;0;48;2;236;111;0m▌[m[38;2;234;115;0;48;2;232;119;0m▌[m[38;2;230;124;0;48;2;228;128;0m▌[m[38;2;227;131;0;48;2;225;135;0m▌[m[38;2;223;139;0;48;2;221;143;0m▌[m[38;2;218;146;0;48;2;216;150;0m▌[m[38;2;214;153;0;48;2;212;156;0m▌[m[38;2;209;160;0;48;2;207;163;0m▌[m[38;2;205;166;0;48;2;202;170;0m▌[m[38;2;200;173;0;48;2;197;176;0m▌[m[38;2;194;179;0;48;2;191;182;0m▌[m[38;2;188;185;0;48;2;185;188;0m▌[m[38;2;182;191;0;48;2;179;194;0m▌[m[38;2;176;197;0;48;2;172;200;0m▌[m[38;2;169;203;0;48;2;165;206;0m▌[m[38;2;161;209;0;48;2;157;212;0m▌[m[38;2;153;214;0;48;2;149;217;0m▌[m[38;2;144;220;0;48;2;139;223;0m▌[m[38;2;134;226;0;48;2;128;228;0m▌[m[38;2;123;231;0;48;2;116;234;0m▌[m[38;2;109;237;0;48;2;102;239;0m▌[m[38;2;94;242;0;48;2;84;245;0m▌[m[38;2;73;247;0;48;2;60;250;0m▌[m[38;2;41;253;0;48;2;0;255;0m▌[m[38;2;96;96;96m[m
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[38;2;255;0;0;48;2;254;27;0m▌[m[38;2;253;41;0;48;2;251;51;0m▌[m[38;2;250;60;0;48;2;248;68;0m▌[m[38;2;247;74;0;48;2;245;81;0m▌[m[38;2;244;86;0;48;2;242;92;0m▌[m[38;2;241;97;0;48;2;239;102;0m▌[m[38;2;237;106;0;48;2;236;111;0m▌[m[38;2;234;115;0;48;2;232;119;0m▌[m[38;2;230;124;0;48;2;228;128;0m▌[m[38;2;227;131;0;48;2;225;135;0m▌[m[38;2;223;139;0;48;2;221;143;0m▌[m[38;2;218;146;0;48;2;216;150;0m▌[m[38;2;214;153;0;48;2;212;156;0m▌[m[38;2;209;160;0;48;2;207;163;0m▌[m[38;2;205;166;0;48;2;202;170;0m▌[m[38;2;200;173;0;48;2;197;176;0m▌[m[38;2;194;179;0;48;2;191;182;0m▌[m[38;2;188;185;0;48;2;185;188;0m▌[m[38;2;182;191;0;48;2;179;194;0m▌[m[38;2;176;197;0;48;2;172;200;0m▌[m[38;2;169;203;0;48;2;165;206;0m▌[m[38;2;161;209;0;48;2;157;212;0m▌[m[38;2;153;214;0;48;2;149;217;0m▌[m[38;2;144;220;0;48;2;139;223;0m▌[m[38;2;134;226;0;48;2;128;228;0m▌[m[38;2;123;231;0;48;2;116;234;0m▌[m[38;2;109;237;0;48;2;102;239;0m▌[m[38;2;94;242;0;48;2;84;245;0m▌[m[38;2;73;247;0;48;2;60;250;0m▌[m[38;2;41;253;0;48;2;0;255;0m▌[m[38;2;96;96;96m[m
|
||||||
Reference in New Issue
Block a user