2020-02-10 19:40:52 +03:00
|
|
|
package spinner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"time"
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
2020-03-27 21:10:09 +03:00
|
|
|
"github.com/muesli/termenv"
|
2020-02-10 19:40:52 +03:00
|
|
|
)
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
const (
|
2020-06-22 21:49:21 +03:00
|
|
|
defaultFPS = time.Second / 10
|
2020-05-26 02:57:58 +03:00
|
|
|
)
|
|
|
|
|
2020-07-15 01:21:48 +03:00
|
|
|
// Spinner is a set of frames used in animating the spinner.
|
|
|
|
type Spinner = []string
|
|
|
|
|
2020-02-10 19:40:52 +03:00
|
|
|
var (
|
2020-07-15 01:21:48 +03:00
|
|
|
// Some spinners to choose from. You could also make your own.
|
|
|
|
Line = Spinner([]string{"|", "/", "-", "\\"})
|
|
|
|
Dot = Spinner([]string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "})
|
2020-02-10 19:51:08 +03:00
|
|
|
|
2020-03-27 21:10:09 +03:00
|
|
|
color = termenv.ColorProfile().Color
|
2020-02-10 19:40:52 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// Model contains the state for the spinner. Use NewModel to create new models
|
|
|
|
// rather than using Model as a struct literal.
|
|
|
|
type Model struct {
|
2020-05-26 02:57:58 +03:00
|
|
|
|
|
|
|
// Type is the set of frames to use. See Spinner.
|
2020-07-15 01:21:48 +03:00
|
|
|
Frames Spinner
|
2020-05-26 02:57:58 +03:00
|
|
|
|
2020-08-25 06:14:17 +03:00
|
|
|
// FPS is the speed at which the ticker should tick.
|
2020-06-22 21:49:21 +03:00
|
|
|
FPS time.Duration
|
2020-05-26 02:57:58 +03:00
|
|
|
|
|
|
|
// ForegroundColor sets the background color of the spinner. It can be a
|
|
|
|
// hex code or one of the 256 ANSI colors. If the terminal emulator can't
|
|
|
|
// doesn't support the color specified it will automatically degrade
|
|
|
|
// (per github.com/muesli/termenv).
|
2020-03-27 21:10:09 +03:00
|
|
|
ForegroundColor string
|
2020-05-26 02:57:58 +03:00
|
|
|
|
|
|
|
// BackgroundColor sets the background color of the spinner. It can be a
|
|
|
|
// hex code or one of the 256 ANSI colors. If the terminal emulator can't
|
|
|
|
// doesn't support the color specified it will automatically degrade
|
|
|
|
// (per github.com/muesli/termenv).
|
2020-03-27 21:10:09 +03:00
|
|
|
BackgroundColor string
|
|
|
|
|
2020-08-25 04:07:45 +03:00
|
|
|
// MinimumLifetime is the minimum amount of time the spinner can run. Any
|
|
|
|
// logic around this can be implemented in view that implements this
|
|
|
|
// spinner. If HideFor is set MinimumLifetime will be added on top of
|
|
|
|
// HideFor. In other words, if HideFor is 100ms and MinimumLifetime is
|
|
|
|
// 200ms then MinimumLifetime will expire after 300ms.
|
|
|
|
//
|
|
|
|
// MinimumLifetime is optional.
|
2020-08-25 06:14:17 +03:00
|
|
|
//
|
|
|
|
// This is considered experimental and may not appear in future versions of
|
|
|
|
// this library.
|
2020-08-25 03:03:54 +03:00
|
|
|
MinimumLifetime time.Duration
|
|
|
|
|
|
|
|
// HideFor can be used to wait to show the spinner until a certain amount
|
|
|
|
// of time has passed. This can be useful for preventing flicking when load
|
|
|
|
// times are very fast. The hidden state can be set with HiddenState.
|
|
|
|
// Optional.
|
2020-08-25 06:14:17 +03:00
|
|
|
//
|
|
|
|
// This is considered experimental and may not appear in future versions of
|
|
|
|
// this library.
|
2020-08-25 03:03:54 +03:00
|
|
|
HideFor time.Duration
|
|
|
|
|
|
|
|
frame int
|
|
|
|
startTime time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start resets resets the spinner start time. For use with MinimumLifetime and
|
|
|
|
// MinimumStartTime. Optional.
|
2020-08-25 06:14:17 +03:00
|
|
|
//
|
|
|
|
// This is considered experimental and may not appear in future versions of
|
|
|
|
// this library.
|
2020-08-25 03:03:54 +03:00
|
|
|
func (m *Model) Start() {
|
|
|
|
m.startTime = time.Now()
|
|
|
|
}
|
|
|
|
|
2020-08-25 06:14:17 +03:00
|
|
|
// hidden returns whether or not Model.HideFor is in effect.
|
|
|
|
func (m Model) hidden() bool {
|
|
|
|
if m.startTime.IsZero() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if m.HideFor == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return m.startTime.Add(m.HideFor).After(time.Now())
|
|
|
|
}
|
|
|
|
|
|
|
|
// finished returns whether Model.MinimumLifetimeReached has been met.
|
|
|
|
func (m Model) finished() bool {
|
2020-08-25 03:03:54 +03:00
|
|
|
if m.startTime.IsZero() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if m.MinimumLifetime == 0 {
|
|
|
|
return true
|
|
|
|
}
|
2020-08-25 04:07:45 +03:00
|
|
|
return m.startTime.Add(m.HideFor).Add(m.MinimumLifetime).Before(time.Now())
|
2020-08-25 03:03:54 +03:00
|
|
|
}
|
|
|
|
|
2020-08-25 06:14:17 +03:00
|
|
|
// Visible returns whether or not the view should be rendered. Works in
|
|
|
|
// conjunction with Model.HideFor and Model.MinimumLifetimeReached. You should
|
|
|
|
// use this message directly to determine whether or not to render this view in
|
|
|
|
// the parent view and whether to continue sending spin messaging in the
|
|
|
|
// parent update function.
|
|
|
|
//
|
|
|
|
// Also note that using this function is optional and generally considered for
|
|
|
|
// advanced use only. Most of the time your application logic will determine
|
|
|
|
// whether or not this view should be used.
|
|
|
|
//
|
|
|
|
// This is considered experimental and may not appear in future versions of
|
|
|
|
// this library.
|
|
|
|
func (m Model) Visible() bool {
|
|
|
|
return !m.hidden() && !m.finished()
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
// NewModel returns a model with default values.
|
2020-02-10 19:40:52 +03:00
|
|
|
func NewModel() Model {
|
|
|
|
return Model{
|
2020-07-15 01:21:48 +03:00
|
|
|
Frames: Line,
|
|
|
|
FPS: defaultFPS,
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
// TickMsg indicates that the timer has ticked and we should render a frame.
|
2020-08-25 03:03:54 +03:00
|
|
|
type TickMsg struct {
|
2020-08-25 04:09:28 +03:00
|
|
|
Time time.Time
|
2020-08-25 03:03:54 +03:00
|
|
|
}
|
2020-02-10 19:40:52 +03:00
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
// Update is the Tea update function. This will advance the spinner one frame
|
|
|
|
// every time it's called, regardless the message passed, so be sure the logic
|
|
|
|
// is setup so as not to call this Update needlessly.
|
2020-02-10 19:40:52 +03:00
|
|
|
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
|
2020-06-22 21:49:21 +03:00
|
|
|
if _, ok := msg.(TickMsg); ok {
|
|
|
|
m.frame++
|
2020-07-15 01:21:48 +03:00
|
|
|
if m.frame >= len(m.Frames) {
|
2020-06-22 21:49:21 +03:00
|
|
|
m.frame = 0
|
|
|
|
}
|
2020-05-26 02:57:58 +03:00
|
|
|
return m, Tick(m)
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
2020-06-22 21:49:21 +03:00
|
|
|
return m, nil
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
// View renders the model's view.
|
2020-02-10 19:40:52 +03:00
|
|
|
func View(model Model) string {
|
2020-07-15 01:21:48 +03:00
|
|
|
if model.frame >= len(model.Frames) {
|
|
|
|
return "error"
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
2020-03-27 21:10:09 +03:00
|
|
|
|
2020-07-15 01:21:48 +03:00
|
|
|
frame := model.Frames[model.frame]
|
2020-03-27 21:10:09 +03:00
|
|
|
|
|
|
|
if model.ForegroundColor != "" || model.BackgroundColor != "" {
|
|
|
|
return termenv.
|
2020-07-15 01:21:48 +03:00
|
|
|
String(frame).
|
2020-03-27 21:10:09 +03:00
|
|
|
Foreground(color(model.ForegroundColor)).
|
|
|
|
Background(color(model.BackgroundColor)).
|
|
|
|
String()
|
|
|
|
}
|
|
|
|
|
2020-07-15 01:21:48 +03:00
|
|
|
return frame
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
// Tick is the command used to advance the spinner one frame.
|
2020-06-22 21:49:21 +03:00
|
|
|
func Tick(m Model) tea.Cmd {
|
2020-08-25 03:03:54 +03:00
|
|
|
return tea.Tick(m.FPS, func(t time.Time) tea.Msg {
|
|
|
|
return TickMsg{
|
2020-08-25 04:09:28 +03:00
|
|
|
Time: t,
|
2020-08-25 03:03:54 +03:00
|
|
|
}
|
2020-06-22 21:49:21 +03:00
|
|
|
})
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|