Files
ollama_ollama/cmd/tui/spinner.go
T

111 lines
3.1 KiB
Go

package tui
import (
"os"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/ollama/ollama/cmd/launch"
"golang.org/x/term"
)
// spinnerStyle dims the spinner so it reads as ancillary status text, matching
// the sign-in/upgrade spinners in signin.go.
var spinnerStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "242", Dark: "246"})
type spinnerTickMsg struct{}
// spinnerQuitMsg is sent by Stop to ask the program to quit cleanly.
type spinnerQuitMsg struct{}
type spinnerModel struct {
message string
frame int
quitting bool
cancelled chan struct{}
once sync.Once
}
func (m *spinnerModel) Init() tea.Cmd {
return tea.Tick(100*time.Millisecond, func(time.Time) tea.Msg { return spinnerTickMsg{} })
}
func (m *spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case spinnerTickMsg:
if m.quitting {
return m, nil
}
m.frame++
return m, tea.Tick(100*time.Millisecond, func(time.Time) tea.Msg { return spinnerTickMsg{} })
case tea.KeyMsg:
// bubbletea runs the terminal in raw mode, so Ctrl+C is delivered here
// as a key rather than as a SIGINT. Treat it as a user cancellation:
// close the cancelled channel (so the caller's wait loop can abort) and
// quit the program so bubbletea restores the terminal before control
// returns to the caller.
if msg.String() == "ctrl+c" {
m.once.Do(func() { close(m.cancelled) })
m.quitting = true
return m, tea.Quit
}
case spinnerQuitMsg:
m.quitting = true
// Returning "" from View on quit clears the spinner line, mirroring how
// confirm.go blanks its view when it quits.
return m, tea.Quit
}
return m, nil
}
func (m *spinnerModel) View() string {
if m.quitting {
return ""
}
frame := launch.SpinnerFrames[m.frame%len(launch.SpinnerFrames)]
return spinnerStyle.Render(frame + " " + m.message)
}
// RunSpinner runs a bubbletea spinner displaying message until the returned
// Spinner's Stop is called. Stop signals the program to quit and blocks until
// it has exited and cleared its line. If the user presses Ctrl+C while the
// spinner is running, Spinner.Cancelled() is closed so the caller can abort
// its wait; the program quits and the terminal is restored before Stop
// returns. RunSpinner returns nil when there is no interactive terminal, so
// launch.StartSpinner can fall back to its ANSI spinner for headless/--yes
// runs.
func RunSpinner(message string) *launch.Spinner {
if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stderr.Fd())) {
return nil
}
cancelled := make(chan struct{})
m := &spinnerModel{message: message, cancelled: cancelled}
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
done := make(chan struct{})
go func() {
_, _ = p.Run()
close(done)
}()
var once sync.Once
stop := func() {
once.Do(func() {
select {
case <-done:
// Program already finished (e.g. the user cancelled), so don't
// send to it; just ensure it has exited.
return
default:
}
p.Send(spinnerQuitMsg{})
<-done
})
}
return launch.NewSpinner(stop, cancelled)
}