Detailed changes
@@ -20,7 +20,7 @@ type Spinner struct {
type model struct {
cancel context.CancelFunc
- anim *anim.Anim
+ anim anim.Anim
}
func (m model) Init() tea.Cmd { return m.anim.Init() }
@@ -36,8 +36,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
}
- mm, cmd := m.anim.Update(msg)
- m.anim = mm.(*anim.Anim)
+ var cmd tea.Cmd
+ m.anim, cmd = m.anim.Update(msg)
return m, cmd
}
@@ -28,7 +28,7 @@ const (
//
// If the FPS is 20 (50 milliseconds) this means that the ellipsis will
// change every 8 frames (400 milliseconds).
- ellipsisAnimSpeed = 8
+ ellipsisanimSpeed = 8
// The maximum amount of time that can pass before a character appears.
// This is used to create a staggered entrance effect.
@@ -94,13 +94,11 @@ type Settings struct {
GradColorA color.Color
GradColorB color.Color
CycleColors bool
+ Static bool
}
-// Default settings.
-const ()
-
-// Anim is a Bubble for an animated spinner.
-type Anim struct {
+// anim is a Bubble for an animated spinner.
+type anim struct {
width int
cyclingCharWidth int
label *csync.Slice[string]
@@ -117,9 +115,8 @@ type Anim struct {
id int
}
-// New creates a new Anim instance with the specified width and label.
-func New(opts Settings) *Anim {
- a := &Anim{}
+// New creates a new anim instance with the specified width and label.
+func New(opts Settings) Anim {
// Validate settings.
if opts.Size < 1 {
opts.Size = defaultNumCyclingChars
@@ -134,6 +131,10 @@ func New(opts Settings) *Anim {
opts.LabelColor = defaultLabelColor
}
+ if opts.Static {
+ return newStatic(opts.Label, opts.LabelColor)
+ }
+ a := &anim{}
a.id = nextID()
a.startTime = time.Now()
a.cyclingCharWidth = opts.Size
@@ -254,7 +255,7 @@ func New(opts Settings) *Anim {
}
// SetLabel updates the label text and re-renders it.
-func (a *Anim) SetLabel(newLabel string) {
+func (a *anim) SetLabel(newLabel string) {
a.labelWidth = lipgloss.Width(newLabel)
// Update total width
@@ -268,7 +269,7 @@ func (a *Anim) SetLabel(newLabel string) {
}
// renderLabel renders the label with the current label color.
-func (a *Anim) renderLabel(label string) {
+func (a *anim) renderLabel(label string) {
if a.labelWidth > 0 {
// Pre-render the label.
labelRunes := []rune(label)
@@ -295,7 +296,7 @@ func (a *Anim) renderLabel(label string) {
}
// Width returns the total width of the animation.
-func (a *Anim) Width() (w int) {
+func (a *anim) Width() (w int) {
w = a.width
if a.labelWidth > 0 {
w += labelGapWidth + a.labelWidth
@@ -313,12 +314,12 @@ func (a *Anim) Width() (w int) {
}
// Init starts the animation.
-func (a *Anim) Init() tea.Cmd {
+func (a *anim) Init() tea.Cmd {
return a.Step()
}
// Update processes animation steps (or not).
-func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (a *anim) Update(msg tea.Msg) (Anim, tea.Cmd) {
switch msg := msg.(type) {
case StepMsg:
if msg.id != a.id {
@@ -334,7 +335,7 @@ func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.initialized.Load() && a.labelWidth > 0 {
// Manage the ellipsis animation.
ellipsisStep := a.ellipsisStep.Add(1)
- if int(ellipsisStep) >= ellipsisAnimSpeed*len(ellipsisFrames) {
+ if int(ellipsisStep) >= ellipsisanimSpeed*len(ellipsisFrames) {
a.ellipsisStep.Store(0)
}
} else if !a.initialized.Load() && time.Since(a.startTime) >= maxBirthOffset {
@@ -347,7 +348,7 @@ func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// View renders the current state of the animation.
-func (a *Anim) View() string {
+func (a *anim) View() string {
var b strings.Builder
step := int(a.step.Load())
for i := range a.width {
@@ -372,7 +373,7 @@ func (a *Anim) View() string {
// have been initialized.
if a.initialized.Load() && a.labelWidth > 0 {
ellipsisStep := int(a.ellipsisStep.Load())
- if ellipsisFrame, ok := a.ellipsisFrames.Get(ellipsisStep / ellipsisAnimSpeed); ok {
+ if ellipsisFrame, ok := a.ellipsisFrames.Get(ellipsisStep / ellipsisanimSpeed); ok {
b.WriteString(ellipsisFrame)
}
}
@@ -381,7 +382,7 @@ func (a *Anim) View() string {
}
// Step is a command that triggers the next step in the animation.
-func (a *Anim) Step() tea.Cmd {
+func (a *anim) Step() tea.Cmd {
return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg {
return StepMsg{id: a.id}
})
@@ -0,0 +1,34 @@
+package anim
+
+import (
+ "image/color"
+
+ tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/charmbracelet/lipgloss/v2"
+)
+
+type Anim interface {
+ Init() tea.Cmd
+ Update(tea.Msg) (Anim, tea.Cmd)
+ View() string
+ SetLabel(string)
+}
+
+type noAnim struct {
+ Color color.Color
+ rendered string
+}
+
+func newStatic(label string, foreground color.Color) Anim {
+ a := &noAnim{Color: foreground}
+ a.SetLabel(label)
+ return a
+}
+
+func (s *noAnim) SetLabel(label string) {
+ s.rendered = lipgloss.NewStyle().Foreground(s.Color).Render(label + ellipsisFrames[2])
+}
+
+func (s noAnim) Init() tea.Cmd { return nil }
+func (s *noAnim) Update(tea.Msg) (Anim, tea.Cmd) { return s, nil }
+func (s *noAnim) View() string { return s.rendered }
@@ -54,7 +54,7 @@ type messageCmp struct {
// Core message data and state
message message.Message // The underlying message content
spinning bool // Whether to show loading animation
- anim *anim.Anim // Animation component for loading states
+ anim anim.Anim // Animation component for loading states
// Thinking viewport for displaying reasoning content
thinkingViewport viewport.Model
@@ -99,8 +99,8 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case anim.StepMsg:
m.spinning = m.shouldSpin()
if m.spinning {
- u, cmd := m.anim.Update(msg)
- m.anim = u.(*anim.Anim)
+ var cmd tea.Cmd
+ m.anim, cmd = m.anim.Update(msg)
return m, cmd
}
case tea.KeyPressMsg:
@@ -61,8 +61,8 @@ type toolCallCmp struct {
permissionGranted bool
// Animation state for pending tool calls
- spinning bool // Whether to show loading animation
- anim util.Model // Animation component for pending states
+ spinning bool // Whether to show loading animation
+ anim anim.Anim // Animation component for pending states
nestedToolCalls []ToolCallCmp // Nested tool calls for hierarchical display
}
@@ -159,8 +159,8 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if m.spinning {
- u, cmd := m.anim.Update(msg)
- m.anim = u.(util.Model)
+ var cmd tea.Cmd
+ m.anim, cmd = m.anim.Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)