package spinner import ( "sync" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // 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 FPS time.Duration } // Some spinners to choose from. You could also make your own. var ( Line = Spinner{ Frames: []string{"|", "/", "-", "\\"}, FPS: time.Second / 10, //nolint:gomnd } Dot = Spinner{ Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "}, FPS: time.Second / 10, //nolint:gomnd } MiniDot = Spinner{ Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, FPS: time.Second / 12, //nolint:gomnd } Jump = Spinner{ Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"}, FPS: time.Second / 10, //nolint:gomnd } Pulse = Spinner{ Frames: []string{"█", "▓", "▒", "░"}, FPS: time.Second / 8, //nolint:gomnd } Points = Spinner{ Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"}, FPS: time.Second / 7, //nolint:gomnd } Globe = Spinner{ Frames: []string{"🌍", "🌎", "🌏"}, FPS: time.Second / 4, //nolint:gomnd } Moon = Spinner{ Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"}, FPS: time.Second / 8, //nolint:gomnd } Monkey = Spinner{ Frames: []string{"🙈", "🙉", "🙊"}, FPS: time.Second / 3, //nolint:gomnd } ) // Model contains the state for the spinner. Use NewModel to create new models // rather than using Model as a struct literal. type Model struct { // Spinner settings to use. See type Spinner. Spinner Spinner // 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 frame int id int tag int } // ID returns the spinner's unique ID. func (m Model) ID() int { return m.id } // New returns a model with default values. func New() Model { return Model{ Spinner: Line, id: nextID(), } } // NewModel returns a model with default values. // // Deprecated. Use New instead. var NewModel = New // 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 // every time it's called, regardless the message passed, so be sure the logic // is setup so as not to call this Update needlessly. 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. if msg.tag > 0 && msg.tag != m.tag { return m, nil } m.frame++ if m.frame >= len(m.Spinner.Frames) { m.frame = 0 } m.tag++ return m, m.tick(m.id, m.tag) default: return m, nil } } // View renders the model's view. func (m Model) View() string { if m.frame >= len(m.Spinner.Frames) { return "(error)" } return m.Style.Render(m.Spinner.Frames[m.frame]) } // Tick is the command used to advance the spinner one frame. Use this command // to effectively start the spinner. 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(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()} }