@@ -13,6 +13,7 @@ const (
1313 SpinnerBraille SpinnerStyle = "braille"
1414 SpinnerBrailleWave SpinnerStyle = "braillewave"
1515 SpinnerHawk SpinnerStyle = "hawk"
16+ SpinnerHawkQuad SpinnerStyle = "hawkquad"
1617 SpinnerDNA SpinnerStyle = "dna"
1718 SpinnerScan SpinnerStyle = "scan"
1819 SpinnerPulse SpinnerStyle = "pulse"
@@ -23,7 +24,10 @@ const (
2324 SpinnerRandom SpinnerStyle = "random"
2425)
2526
26- // hawkQuadBlockGlyphs is the unicode.framer.website QUADBLOCK spinner (4 frames).
27+ // hawkSpinnerGlyphs is the default TUI spinner — partial-circle compass (smooth, readable).
28+ var hawkSpinnerGlyphs = []string {"◐" , "◓" , "◑" , "◒" }
29+
30+ // hawkQuadBlockGlyphs is the legacy QUADBLOCK animation (kept for tests / bubbles compat).
2731var hawkQuadBlockGlyphs = []string {"▛" , "▜" , "▟" , "▙" }
2832
2933// hawkSpinnerBG is the chat viewport background (chat_view.go).
@@ -33,7 +37,8 @@ var hawkSpinnerBG = [3]int{30, 30, 40}
3337var spinnerFrames = map [SpinnerStyle ][]string {
3438 SpinnerBraille : {"⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" },
3539 SpinnerBrailleWave : {"⠁⠂⠄⡀" , "⠂⠄⡀⢀" , "⠄⡀⢀⠠" , "⡀⢀⠠⠐" , "⢀⠠⠐⠈" , "⠠⠐⠈⠁" , "⠐⠈⠁⠂" , "⠈⠁⠂⠄" },
36- SpinnerHawk : hawkQuadBlockGlyphs ,
40+ SpinnerHawk : hawkSpinnerGlyphs ,
41+ SpinnerHawkQuad : hawkQuadBlockGlyphs ,
3742 SpinnerDNA : {"⠋⠉⠙⠚" , "⠉⠙⠚⠒" , "⠙⠚⠒⠂" , "⠚⠒⠂⠂" , "⠒⠂⠂⠒" , "⠂⠂⠒⠲" , "⠂⠒⠲⠴" , "⠒⠲⠴⠤" , "⠲⠴⠤⠄" , "⠴⠤⠄⠋" , "⠤⠄⠋⠉" , "⠄⠋⠉⠙" },
3843 SpinnerScan : {"⡇⠀⠀⠀" , "⣿⠀⠀⠀" , "⢸⡇⠀⠀" , "⠀⣿⠀⠀" , "⠀⢸⡇⠀" , "⠀⠀⣿⠀" , "⠀⠀⢸⡇" , "⠀⠀⠀⣿" , "⠀⠀⠀⢸" , "⠀⠀⠀⠀" },
3944 SpinnerPulse : {"⠀" , "⠄" , "⠆" , "⠇" , "⡇" , "⣇" , "⣧" , "⣷" , "⣿" , "⣷" , "⣧" , "⣇" , "⡇" , "⠇" , "⠆" , "⠄" },
@@ -46,29 +51,18 @@ var spinnerFrames = map[SpinnerStyle][]string{
4651// hawkTypingDots is the number of trailing typing-indicator dots.
4752const hawkTypingDots = 3
4853
49- // colorSpinnerGlyph renders a single glyph in hawk brand orange — the
50- // spinner is the visual hero of the line and the brand color. The ANSI
51- // escape constants (ansiOrange, ansiReset, etc.) and the icon glyphs
52- // (iconDotFilled, iconDotEmpty) live in theme.go so the entire palette
53- // is editable from one place.
54- func colorSpinnerGlyph (glyph string ) string {
55- if glyph == "" {
56- return ""
57- }
58- return ansiOrange + glyph + ansiReset
59- }
60-
61- // BrailleSpinner renders animated spinners with a single accent color
62- // (cyan) for both the glyph and the label.
54+ // BrailleSpinner renders the glyph frame (◐◓◑◒) and a 20-color wave on the
55+ // whole status strip: glyph → verb → ▪▫▫.
6356type BrailleSpinner struct {
64- mu sync.Mutex
65- style SpinnerStyle
66- frames []string
67- frame int
68- text string
69- dots int // 0..hawkTypingDots-1 — position of the highlighted dot
70- running bool
71- stopCh chan struct {}
57+ mu sync.Mutex
58+ style SpinnerStyle
59+ frames []string
60+ frame int // glyph animation frame (mod len(frames))
61+ wavePhase int // 0..19 flowing color wave (glyph + verb + dots)
62+ text string
63+ dots int // 0..hawkTypingDots-1 — position of the highlighted dot
64+ running bool
65+ stopCh chan struct {}
7266}
7367
7468// NewBrailleSpinner creates a spinner with the given style and label text.
@@ -96,63 +90,23 @@ func (s *BrailleSpinner) SetLabel(text string) {
9690 s .text = text
9791}
9892
99- // SetWave is kept for backwards compatibility with existing call sites —
100- // the line no longer uses a color wave so this is a no-op.
101- func (s * BrailleSpinner ) SetWave (_ bool ) {
102- s .mu .Lock ()
103- defer s .mu .Unlock ()
104- }
105-
106- func (s * BrailleSpinner ) renderGlyphLocked (glyph string ) string {
107- return colorSpinnerGlyph (glyph )
108- }
93+ // SetWave is kept for backwards compatibility with existing call sites.
94+ // The verb and typing dots always use the 20-color wave; this is a no-op.
95+ func (s * BrailleSpinner ) SetWave (_ bool ) {}
10996
110- // renderLabelLocked returns the label in green followed by the trailing
111- // animated dots (one yellow dot, two dim dots). The whole group is the
112- // "alive" part of the spinner line.
113- func (s * BrailleSpinner ) renderLabelLocked () string {
114- if s .text == "" {
115- return ""
116- }
117- out := ansiGreen + s .text + ansiReset
118- out += " " + s .renderAnimatedDotsLocked ()
119- return out
120- }
121-
122- // renderAnimatedDotsLocked returns hawkTypingDots plain circles, with the
123- // current position rendered in yellow and the rest dim. The filled and
124- // empty glyphs come from theme.go (iconDotFilled / iconDotEmpty) so they
125- // stay in sync with the rest of the TUI.
126- func (s * BrailleSpinner ) renderAnimatedDotsLocked () string {
127- idx := s .dots % hawkTypingDots
128- out := ""
129- for i := 0 ; i < hawkTypingDots ; i ++ {
130- if i == idx {
131- out += ansiYellow + iconDotFilled + ansiReset
132- } else {
133- out += ansiDim + iconDotEmpty + ansiReset
134- }
135- }
136- return out
137- }
138-
139- // Frame returns the current rendered frame (spinner + label).
97+ // Frame returns the current rendered frame (glyph + verb + dots, all in the wave).
14098func (s * BrailleSpinner ) Frame () string {
14199 s .mu .Lock ()
142100 defer s .mu .Unlock ()
143101 glyph := s .frames [s .frame % len (s .frames )]
144- spinner := s .renderGlyphLocked (glyph )
145- label := s .renderLabelLocked ()
146- if label == "" {
147- return spinner
148- }
149- return spinner + " " + label
102+ return renderSpinnerWaveLine (glyph , s .text , s .wavePhase , s .dots )
150103}
151104
152105// Tick advances to the next frame and cycles the dot highlight.
153106func (s * BrailleSpinner ) Tick () string {
154107 s .mu .Lock ()
155108 s .frame ++
109+ s .wavePhase = (s .wavePhase + 1 ) % spinnerWaveLen
156110 s .dots = (s .dots + 1 ) % hawkTypingDots
157111 s .mu .Unlock ()
158112 return s .Frame ()
0 commit comments