Skip to content

feat(tui): spinner breathes instead of ticking - #1421

Merged
dennisonbertram merged 1 commit into
mainfrom
feat/1420-breathing-spinner
Sep 8, 2026
Merged

feat(tui): spinner breathes instead of ticking#1421
dennisonbertram merged 1 commit into
mainfrom
feat/1420-breathing-spinner

Conversation

@dennisonbertram

Copy link
Copy Markdown
Owner

Closes #1420

The complaint

After #1415 stopped the word rotating, the spinner still felt wrong: "it still seems like it spins too fast", "slow it down so that it breaths."

The remaining problem was the glyph, and I had left it alone on a bad assumption — that because our six glyphs match the set independently reported for Claude Code, the glyph layer was already right. Matching their glyphs is not matching their cadence, and the research explicitly could not find their frame interval.

Two things were wrong:

Speed. Six frames at a flat 120ms is a full rotation every 720ms. For something the eye rests on during a long wait that is fast; Charm's own presets run 83–333ms and ours sat near the fast end.

No shape. Every frame held exactly as long as every other, so the cycle had no contour. And the order ✶ · ✻ ✽ ✳ ✢ was not monotonic in visual weight — it jumped from the heaviest glyph straight to the lightest and back, which reads as a stutter at any speed.

What it does now

The same six glyphs, reordered by weight and ping-ponged so the cycle grows and shrinks, with a per-step hold table:

  ·  ████████████   360ms
  ✢  ████████       240ms
  ✳  ████           120ms
  ✻  ████           120ms
  ✽  ████████       240ms
  ✶  ████████████   360ms
  ✽  ████████       240ms
  ✻  ████           120ms
  ✳  ████           120ms
  ✢  ████████       240ms
  cycle: 18 ticks = 2.16s

It lingers at the top and bottom of the breath and moves quickly through the middle. That unevenness is the easing — a flat cadence is precisely what made it read as mechanical. Three times slower than before, and symmetric.

The tick rate deliberately stays at 120ms. The same tick redraws the elapsed-time counter, so slowing the timer would slow the clock. Advance is gated instead, which slows only the glyph. The glyph set is unchanged; it was never the problem.

Verification

Red first, all three:

  • TestSpinnerCycleTakesAboutTwoSeconds — a cycle was 6 ticks, must be 18.
  • TestSpinnerEasesAtTheExtremes — every frame held exactly one tick, so no strict inequality between extremes and middle could exist.
  • TestSpinnerPulseGrowsThenShrinks — the old order made exactly the heaviest-to-lightest jump this forbids.

TestSpinnerStillAnimatesUnderEasing is the control: a hold table that never released would satisfy "slower" while freezing the spinner outright.

TestTUI024_SpinnerCyclesFrames pinned one-frame-per-tick, the behavior being replaced, so it became TestTUI024_SpinnerAdvancesThroughThePulse. The #1415 glyph guard widened its bound to the longest hold.

Full TUI suite green under -race. Snapshots regenerated and reviewed — glyph changes only, to , as expected from the new step ordering.

What is not yet verified

The hold table is a considered starting point, not a proven result. "Breathes" is a perceptual claim and no unit test can judge it — the numbers above are what I chose, and whether they feel right is the owner's call after watching a live run. If 2.16s reads as sluggish rather than calm, the table is one line to retune.

Also worth stating plainly: the claim that Claude Code eases its frames comes from third parties hand-timing screen recordings, not from reading source. This is our own easing, informed by that description rather than reproducing a measurement.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5

Six frames at a flat 120ms is a full rotation every 720ms with every
frame weighted the same, which reads as a mechanical tick. The frame
order also jumped from the heaviest glyph straight to the lightest, a
stutter at any speed.

Same six glyphs, reordered by weight and ping-ponged into a pulse, with a
per-step hold table so advance is gated rather than one-per-tick. The
extremes hold 360ms and the middle passes in 120ms; a cycle is now about
2.16s. The uneven hold is the easing — a flat cadence is exactly what
made it feel mechanical.

The tick rate stays at 120ms on purpose: the same tick redraws the
elapsed-time counter, so slowing the timer would slow the clock.

Closes #1420

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tui): spinner should breathe — slower cycle with easing instead of a flat 120ms tick

1 participant