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

Spin Safety #91

Merged
merged 3 commits into from Jan 10, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 56 additions & 5 deletions spinner/spinner.go
Expand Up @@ -2,13 +2,29 @@ package spinner

import (
"strings"
"sync"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/ansi"
)

// Internal ID management for text inputs. Necessary for blink integrity when
// multiple text inputs are involved.
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
}

// Spinner is a set of frames used in animating the spinner.
type Spinner struct {
Frames []string
Expand Down Expand Up @@ -92,6 +108,7 @@ type Model struct {

frame int
startTime time.Time
id int
tag int
}

Expand Down Expand Up @@ -129,6 +146,11 @@ func (m *Model) Finish() {
}
}

// ID returns the spinner's unique ID.
func (m Model) ID() int {
return m.id
}

// advancedMode returns whether or not the user is making use of HideFor and
// MinimumLifetime properties.
func (m Model) advancedMode() bool {
Expand Down Expand Up @@ -173,13 +195,17 @@ func (m Model) Visible() bool {

// NewModel returns a model with default values.
func NewModel() Model {
return Model{Spinner: Line}
return Model{
Spinner: Line,
id: nextID(),
}
}

// TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct {
Time time.Time
tag int
ID int
}

// Update is the Tea update function. This will advance the spinner one frame
Expand All @@ -188,6 +214,12 @@ type TickMsg struct {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case TickMsg:
// If an ID is set, and the ID doesn't belong to this spinner, reject
// the message.
if msg.ID > 0 && msg.ID != m.id {
return m, nil
}

// 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
// thus spinning too fast.
Expand All @@ -201,7 +233,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}

m.tag++
return m, m.tick(m.tag)
return m, m.tick(m.id, m.tag)
default:
return m, nil
}
Expand All @@ -227,15 +259,34 @@ func (m Model) View() string {

// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
func Tick() tea.Msg {
return TickMsg{Time: time.Now()}
func (m Model) Tick() tea.Msg {
return TickMsg{
// The time at which the tick occurred.
Time: time.Now(),

// The ID of the spinner that this message belongs to. This can be
// helpful when routing messages, however bear in mind that spinners
// will ignore messages that don't contain ID by default.
ID: m.id,

tag: m.tag,
}
}

func (m Model) tick(tag int) tea.Cmd {
func (m Model) tick(id, tag int) tea.Cmd {
return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
return TickMsg{
Time: t,
ID: id,
tag: tag,
}
})
}

// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
//
// This method is deprecated. Use Model.Tick instead.
func Tick() tea.Msg {
return TickMsg{Time: time.Now()}
}