Progress can now animate itself

This commit is contained in:
Christian Rocha 2021-07-08 13:02:52 -04:00
parent 09a4cf419d
commit 20ead8fb7d
3 changed files with 113 additions and 17 deletions

1
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.13.1 github.com/charmbracelet/bubbletea v0.13.1
github.com/charmbracelet/lipgloss v0.1.2 github.com/charmbracelet/lipgloss v0.1.2
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.12 github.com/mattn/go-runewidth v0.0.12
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68

2
go.sum
View File

@ -6,6 +6,8 @@ github.com/charmbracelet/lipgloss v0.1.2 h1:D+LUMg34W7n2pkuMrevKVxT7HXqnoRHm7Ioo
github.com/charmbracelet/lipgloss v0.1.2/go.mod h1:5D8zradw52m7QmxRF6QgwbwJi9je84g8MkWiGN07uKg= github.com/charmbracelet/lipgloss v0.1.2/go.mod h1:5D8zradw52m7QmxRF6QgwbwJi9je84g8MkWiGN07uKg=
github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA=
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=

View File

@ -3,13 +3,36 @@ package progress
import ( import (
"fmt" "fmt"
"strings" "strings"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/fogleman/ease"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"github.com/muesli/reflow/ansi" "github.com/muesli/reflow/ansi"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const defaultWidth = 40 // Internal ID management. Used during animating to assure that frame messages
// can only be received by progress components that sent them.
var (
lastID int
idMtx sync.Mutex
)
// Return the next ID we should use on the model.
func nextID() int {
idMtx.Lock()
defer idMtx.Unlock()
lastID++
return lastID
}
const (
defaultWidth = 40
defaultFPS = time.Second / 60
defaultTransitionDuration = time.Millisecond * 350
)
var color func(string) termenv.Color = termenv.ColorProfile().Color var color func(string) termenv.Color = termenv.ColorProfile().Color
@ -19,6 +42,7 @@ var color func(string) termenv.Color = termenv.ColorProfile().Color
// WithRamp("#ff0000", "#0000ff"), // WithRamp("#ff0000", "#0000ff"),
// WithoutPercentage(), // WithoutPercentage(),
// ) // )
//
type Option func(*Model) error type Option func(*Model) error
// WithDefaultGradient sets a gradient fill with default colors. // WithDefaultGradient sets a gradient fill with default colors.
@ -74,25 +98,43 @@ func WithWidth(w int) Option {
} }
} }
// FrameMsg indicates that an animation step should occur.
type FrameMsg struct {
id int
}
// Model stores values we'll use when rendering the progress bar. // Model stores values we'll use when rendering the progress bar.
type Model struct { type Model struct {
// The internal identifier for this model.
id int
// Total width of the progress bar, including percentage, if set. // Total width of the progress bar, including percentage, if set.
Width int Width int
// "Filled" sections of the progress bar // "Filled" sections of the progress bar.
Full rune Full rune
FullColor string FullColor string
// "Empty" sections of progress bar // "Empty" sections of progress bar.
Empty rune Empty rune
EmptyColor string EmptyColor string
// Settings for rendering the numeric percentage // Settings for rendering the numeric percentage.
ShowPercentage bool ShowPercentage bool
PercentFormat string // a fmt string for a float PercentFormat string // a fmt string for a float
PercentageStyle *termenv.Style PercentageStyle *termenv.Style
// Animation options.
FPS time.Duration
TransitionDuration time.Duration
// Values for the internal animation state.
progress float64
startPercent float64
endPercent float64
transitionStart time.Time
// Gradient settings
useRamp bool useRamp bool
rampColorA colorful.Color rampColorA colorful.Color
rampColorB colorful.Color rampColorB colorful.Color
@ -104,28 +146,37 @@ type Model struct {
} }
// NewModel returns a model with default values. // NewModel returns a model with default values.
func NewModel(opts ...Option) (*Model, error) { func NewModel(opts ...Option) (Model, error) {
m := &Model{ m := Model{
Width: defaultWidth, id: nextID(),
Full: '█', Width: defaultWidth,
FullColor: "#7571F9", Full: '█',
Empty: '░', FullColor: "#7571F9",
EmptyColor: "#606060", Empty: '░',
ShowPercentage: true, EmptyColor: "#606060",
PercentFormat: " %3.0f%%", ShowPercentage: true,
PercentFormat: " %3.0f%%",
FPS: defaultFPS,
TransitionDuration: defaultTransitionDuration,
} }
for _, opt := range opts { for _, opt := range opts {
if err := opt(m); err != nil { if err := opt(&m); err != nil {
return nil, err return Model{}, err
} }
} }
return m, nil return m, nil
} }
// View renders the progress bar as a given percentage. // View renders the an animated progress bar in its current state. To render
func (m Model) View(percent float64) string { // a static progress bar based on your own calculations use ViewAs instead.
func (m Model) View() string {
return m.ViewAs(m.progress)
}
// ViewAs renders the progress bar with a given percentage.
func (m Model) ViewAs(percent float64) string {
b := strings.Builder{} b := strings.Builder{}
if m.ShowPercentage { if m.ShowPercentage {
percentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:gomnd percentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:gomnd
@ -140,6 +191,48 @@ func (m Model) View(percent float64) string {
return b.String() return b.String()
} }
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case FrameMsg:
if msg.id != m.id {
return m, nil
}
elapsed := time.Since(m.transitionStart)
totalDuration := m.transitionStart.
Add(m.TransitionDuration).
Sub(m.transitionStart)
f := func(d time.Duration) float64 { return float64(int64(d)) }
segmentProgress := f(elapsed) / f(totalDuration)
if segmentProgress >= 1.0 {
m.progress = m.endPercent
return m, nil
}
segmentSize := m.endPercent - m.startPercent
m.progress = ease.OutQuad(segmentProgress)*segmentSize + m.startPercent
return m, m.nextFrame()
default:
return m, nil
}
}
func (m *Model) SetPercent(p float64) tea.Cmd {
m.startPercent = m.endPercent
m.endPercent = p
m.transitionStart = time.Now()
return m.nextFrame()
}
func (m Model) nextFrame() tea.Cmd {
return tea.Tick(m.FPS, func(time.Time) tea.Msg {
return FrameMsg{m.id}
})
}
func (m Model) bar(b *strings.Builder, percent float64, textWidth int) { func (m Model) bar(b *strings.Builder, percent float64, textWidth int) {
var ( var (
tw = m.Width - textWidth // total width tw = m.Width - textWidth // total width