2020-02-10 19:40:52 +03:00
|
|
|
package spinner
|
|
|
|
|
|
|
|
import (
|
2020-12-11 22:51:26 +03:00
|
|
|
"strings"
|
2020-02-10 19:40:52 +03:00
|
|
|
"time"
|
|
|
|
|
2020-05-26 02:57:58 +03:00
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
2021-04-12 22:54:56 +03:00
|
|
|
"github.com/charmbracelet/lipgloss"
|
2020-12-11 22:51:26 +03:00
|
|
|
"github.com/muesli/reflow/ansi"
|
2020-02-10 19:40:52 +03:00
|
|
|
)
|
|
|
|
|
2020-07-15 01:21:48 +03:00
|
|
|
// Spinner is a set of frames used in animating the spinner.
|
2020-11-11 19:48:47 +03:00
|
|
|
type Spinner struct {
|
|
|
|
Frames []string
|
|
|
|
FPS time.Duration
|
|
|
|
}
|
2020-07-15 01:21:48 +03:00
|
|
|
|
2021-04-13 02:33:16 +03:00
|
|
|
// Some spinners to choose from. You could also make your own.
|
2020-02-10 19:40:52 +03:00
|
|
|
var (
|
2020-11-11 19:48:47 +03:00
|
|
|
Line = Spinner{
|
|
|
|
Frames: []string{"|", "/", "-", "\\"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 10, //nolint:gomnd
|
2020-11-11 19:48:47 +03:00
|
|
|
}
|
|
|
|
Dot = Spinner{
|
|
|
|
Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 10, //nolint:gomnd
|
2020-11-11 19:48:47 +03:00
|
|
|
}
|
2020-11-11 19:59:06 +03:00
|
|
|
MiniDot = Spinner{
|
|
|
|
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 12, //nolint:gomnd
|
2020-11-11 19:59:06 +03:00
|
|
|
}
|
2020-11-11 19:48:47 +03:00
|
|
|
Jump = Spinner{
|
|
|
|
Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 10, //nolint:gomnd
|
2020-11-11 19:48:47 +03:00
|
|
|
}
|
2020-11-11 20:06:03 +03:00
|
|
|
Pulse = Spinner{
|
2020-11-11 20:05:41 +03:00
|
|
|
Frames: []string{"█", "▓", "▒", "░"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 8, //nolint:gomnd
|
2020-11-11 20:05:41 +03:00
|
|
|
}
|
2020-11-11 20:19:37 +03:00
|
|
|
Points = Spinner{
|
|
|
|
Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 7, //nolint:gomnd
|
2020-11-11 20:19:37 +03:00
|
|
|
}
|
2020-11-11 19:48:47 +03:00
|
|
|
Globe = Spinner{
|
2020-11-11 22:19:54 +03:00
|
|
|
Frames: []string{"🌍", "🌎", "🌏"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 4, //nolint:gomnd
|
2020-11-11 19:48:47 +03:00
|
|
|
}
|
|
|
|
Moon = Spinner{
|
2020-11-11 22:19:54 +03:00
|
|
|
Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 8, //nolint:gomnd
|
2020-11-11 19:48:47 +03:00
|
|
|
}
|
|
|
|
Monkey = Spinner{
|
2020-11-11 22:19:54 +03:00
|
|
|
Frames: []string{"🙈", "🙉", "🙊"},
|
2021-03-12 03:43:13 +03:00
|
|
|
FPS: time.Second / 3, //nolint:gomnd
|
2020-11-11 19:48:47 +03:00
|
|
|
}
|
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
|
|
|
|
2020-11-11 19:48:47 +03:00
|
|
|
// Spinner settings to use. See type Spinner.
|
|
|
|
Spinner Spinner
|
2020-05-26 02:57:58 +03:00
|
|
|
|
2021-04-12 22:54:56 +03:00
|
|
|
// Style sets the styling for the spinner. Most of the time you'll just
|
|
|
|
// want foreground and background coloring, and potentially some padding.
|
|
|
|
//
|
|
|
|
// For an introduction to styling with Lip Gloss see:
|
|
|
|
// https://github.com/charmbracelet/lipgloss
|
|
|
|
Style lipgloss.Style
|
2020-03-27 21:10:09 +03:00
|
|
|
|
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
|
2020-10-02 20:03:04 +03:00
|
|
|
// times are very fast.
|
2020-08-25 03:03:54 +03:00
|
|
|
// 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
|
2020-12-11 20:00:07 +03:00
|
|
|
tag int
|
2020-08-25 03:03:54 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start resets resets the spinner start time. For use with MinimumLifetime and
|
|
|
|
// MinimumStartTime. Optional.
|
2020-08-25 06:14:17 +03:00
|
|
|
//
|
2020-12-11 22:51:26 +03:00
|
|
|
// This function is optional and generally considered for advanced use only.
|
|
|
|
// Most of the time your application logic will obviate the need for this
|
|
|
|
// method.
|
|
|
|
//
|
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-12-11 22:51:26 +03:00
|
|
|
// Finish sets the internal timer to a completed state so long as the spinner
|
|
|
|
// isn't flagged to be showing. If it is showing, finish has no effect. The
|
|
|
|
// idea here is that you call Finish if your operation has completed and, if
|
|
|
|
// the spinner isn't showing yet (by virtue of HideFor) then Visible() doesn't
|
|
|
|
// show the spinner at all.
|
|
|
|
//
|
|
|
|
// This is intended to work in conjunction with MinimumLifetime and
|
|
|
|
// MinimumStartTime, is completely optional.
|
|
|
|
//
|
|
|
|
// This function is optional and generally considered for advanced use only.
|
|
|
|
// Most of the time your application logic will obviate the need for this
|
|
|
|
// method.
|
|
|
|
//
|
|
|
|
// This is considered experimental and may not appear in future versions of
|
|
|
|
// this library.
|
|
|
|
func (m *Model) Finish() {
|
|
|
|
if m.hidden() {
|
|
|
|
m.startTime = time.Time{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// advancedMode returns whether or not the user is making use of HideFor and
|
|
|
|
// MinimumLifetime properties.
|
|
|
|
func (m Model) advancedMode() bool {
|
|
|
|
return m.HideFor > 0 && m.MinimumLifetime > 0
|
|
|
|
}
|
|
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2020-12-11 20:00:07 +03:00
|
|
|
// finished returns whether the minimum lifetime of this spinner has been
|
|
|
|
// exceeded.
|
2020-08-25 06:14:17 +03:00
|
|
|
func (m Model) finished() bool {
|
2020-12-11 22:51:26 +03:00
|
|
|
if m.startTime.IsZero() || m.MinimumLifetime == 0 {
|
2020-08-25 03:03:54 +03:00
|
|
|
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
|
2020-12-10 19:36:22 +03:00
|
|
|
// use this method directly to determine whether or not to render this view in
|
2020-08-25 06:14:17 +03:00
|
|
|
// the parent view and whether to continue sending spin messaging in the
|
|
|
|
// parent update function.
|
|
|
|
//
|
2020-12-11 22:51:26 +03:00
|
|
|
// This function is optional and generally considered for advanced use only.
|
|
|
|
// Most of the time your application logic will obviate the need for this
|
|
|
|
// method.
|
2020-08-25 06:14:17 +03:00
|
|
|
//
|
|
|
|
// 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 {
|
2020-11-11 19:48:47 +03:00
|
|
|
return Model{Spinner: Line}
|
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-12-11 20:00:07 +03:00
|
|
|
tag int
|
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-10-27 23:36:29 +03:00
|
|
|
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
2020-12-11 20:00:07 +03:00
|
|
|
switch msg := msg.(type) {
|
2020-10-27 23:36:29 +03:00
|
|
|
case TickMsg:
|
2020-12-11 20:00:07 +03:00
|
|
|
// If a tag is set, and it's not the one we expect, reject the message.
|
|
|
|
// This prevents the spinner from receiving too many messages and
|
2021-06-01 23:30:33 +03:00
|
|
|
// thus spinning too fast.
|
2020-12-11 20:00:07 +03:00
|
|
|
if msg.tag > 0 && msg.tag != m.tag {
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
2020-06-22 21:49:21 +03:00
|
|
|
m.frame++
|
2020-11-11 19:48:47 +03:00
|
|
|
if m.frame >= len(m.Spinner.Frames) {
|
2020-06-22 21:49:21 +03:00
|
|
|
m.frame = 0
|
|
|
|
}
|
2020-12-11 20:00:07 +03:00
|
|
|
|
|
|
|
m.tag++
|
|
|
|
return m, m.tick(m.tag)
|
2020-10-27 23:36:29 +03:00
|
|
|
default:
|
|
|
|
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-10-27 23:36:29 +03:00
|
|
|
func (m Model) View() string {
|
2020-11-11 19:48:47 +03:00
|
|
|
if m.frame >= len(m.Spinner.Frames) {
|
|
|
|
return "(error)"
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
2020-03-27 21:10:09 +03:00
|
|
|
|
2020-11-11 19:48:47 +03:00
|
|
|
frame := m.Spinner.Frames[m.frame]
|
2020-03-27 21:10:09 +03:00
|
|
|
|
2020-12-11 22:51:26 +03:00
|
|
|
// If we're using the fine-grained hide/show spinner rules and those rules
|
|
|
|
// deem that the spinner should be hidden, draw an empty space in place of
|
|
|
|
// the spinner.
|
|
|
|
if m.advancedMode() && !m.Visible() {
|
|
|
|
frame = strings.Repeat(" ", ansi.PrintableRuneWidth(frame))
|
|
|
|
}
|
|
|
|
|
2021-04-12 22:54:56 +03:00
|
|
|
return m.Style.Render(frame)
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|
|
|
|
|
2020-12-11 20:00:07 +03:00
|
|
|
// Tick is the command used to advance the spinner one frame. Use this command
|
|
|
|
// to effectively start the spinner.
|
2020-10-27 23:36:29 +03:00
|
|
|
func Tick() tea.Msg {
|
2020-11-10 04:39:04 +03:00
|
|
|
return TickMsg{Time: time.Now()}
|
2020-10-27 23:36:29 +03:00
|
|
|
}
|
|
|
|
|
2020-12-11 20:00:07 +03:00
|
|
|
func (m Model) tick(tag int) tea.Cmd {
|
2020-11-11 19:48:47 +03:00
|
|
|
return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
|
2020-08-25 03:03:54 +03:00
|
|
|
return TickMsg{
|
2020-08-25 04:09:28 +03:00
|
|
|
Time: t,
|
2020-12-11 20:00:07 +03:00
|
|
|
tag: tag,
|
2020-08-25 03:03:54 +03:00
|
|
|
}
|
2020-06-22 21:49:21 +03:00
|
|
|
})
|
2020-02-10 19:40:52 +03:00
|
|
|
}
|