Class: TUI::BrailleSpinner

Inherits:
Object
  • Object
show all
Defined in:
lib/tui/braille_spinner.rb

Overview

Animated braille spinner that communicates session state through distinct visual patterns. Each state gets its own animation — a user watching long enough starts feeling the difference between LLM thinking and tool execution without reading text.

The 2x4 braille grid (U+2800-U+28FF) encodes 8 dots in a single character cell. Dot positions map to bit flags:

┌───┬───┐
│ 0 │ 3 │   bit 0 = top-left,     bit 3 = top-right
│ 1 │ 4 │   bit 1 = mid-left,     bit 4 = mid-right
│ 2 │ 5 │   bit 2 = lower-left,   bit 5 = lower-right
│ 6 │ 7 │   bit 6 = bottom-left,  bit 7 = bottom-right
└───┴───┘

During LLM generation, a snake weaves through the grid — organic, unpredictable movement like watching a campfire. During tool execution, a fast staccato pulse signals mechanical work. Interrupting decelerates to a freeze.

Examples:

Basic usage

spinner = BrailleSpinner.new
spinner.state = "llm_generating"
char = spinner.tick  # => "⠋" (braille pattern)

Constant Summary collapse

SNAKE_FRAMES =

Clockwise traversal of the 8 dots in the braille grid. Produces a smooth rotating animation — one dot lit at a time.

[
  0x01, # ⠁ dot 0 (top-left)
  0x02, # ⠂ dot 1 (mid-left)
  0x04, # ⠄ dot 2 (lower-left)
  0x40, # ⡀ dot 6 (bottom-left)
  0x80, # ⢀ dot 7 (bottom-right)
  0x20, # ⠠ dot 5 (lower-right)
  0x10, # ⠐ dot 4 (mid-right)
  0x08  # ⠈ dot 3 (top-right)
].freeze
SNAKE_TRAIL_FRAMES =

Snake animation: 3 consecutive dots form a growing/moving tail. Each frame is the OR of 3 adjacent positions in SNAKE_FRAMES, creating a worm-like creature circling the grid.

SNAKE_FRAMES.each_index.map { |idx|
  SNAKE_FRAMES[idx] | SNAKE_FRAMES[(idx + 1) % 8] | SNAKE_FRAMES[(idx + 2) % 8]
}.freeze
TOOL_FRAMES =

Tool execution: alternating dot patterns for a staccato pulse. Fast, mechanical, clearly different from the smooth snake.

[
  0x09, # ⠉ dots 0+3 (top row)
  0x12, # ⠒ dots 1+4 (middle row)
  0x24, # ⠤ dots 2+5 (lower row)
  0xC0, # ⣀ dots 6+7 (bottom row)
  0x24, # ⠤ dots 2+5 (lower row)
  0x12  # ⠒ dots 1+4 (middle row)
].freeze
INTERRUPT_FRAMES =

Interrupting: rapid deceleration — full grid fading to empty.

[
  0xFF, # ⣿ all dots
  0xDB, # ⣛ most dots
  0x49, # ⡉ sparse
  0x00, # ⠀ empty
  0x49, # ⡉ sparse
  0xFF  # ⣿ all dots
].freeze
BRAILLE_BASE =

Braille Unicode block base codepoint.

0x2800
SPEED =

Ticks per frame for each state — controls animation speed. Higher = slower. At ~15fps render loop: 2 = ~7.5fps, 4 = ~3.75fps.

{
  "llm_generating" => 2,
  "tool_executing" => 1,
  "interrupting" => 1
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeBrailleSpinner

Returns a new instance of BrailleSpinner.



84
85
86
87
88
# File 'lib/tui/braille_spinner.rb', line 84

def initialize
  @state = "idle"
  @frame_index = 0
  @tick_count = 0
end

Instance Attribute Details

#stateString

Returns current session state.

Returns:

  • (String)

    current session state



82
83
84
# File 'lib/tui/braille_spinner.rb', line 82

def state
  @state
end

Instance Method Details

#active?Boolean

Whether the spinner is actively animating.

Returns:

  • (Boolean)


138
139
140
# File 'lib/tui/braille_spinner.rb', line 138

def active?
  @state != "idle"
end

#currentString?

Returns the current frame character without advancing.

Returns:

  • (String, nil)

    single braille character, or nil when idle



126
127
128
129
130
131
132
133
# File 'lib/tui/braille_spinner.rb', line 126

def current
  return nil if @state == "idle"

  frames = frames_for_state
  return nil unless frames

  (BRAILLE_BASE + frames[@frame_index]).chr(Encoding::UTF_8)
end

#tickString?

Advances the animation by one tick and returns the current braille character. Returns nil when idle (no animation).

Returns:

  • (String, nil)

    single braille character, or nil when idle



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/tui/braille_spinner.rb', line 107

def tick
  return nil if @state == "idle"

  frames = frames_for_state
  return nil unless frames

  speed = SPEED.fetch(@state, 2)
  @tick_count += 1
  if @tick_count >= speed
    @tick_count = 0
    @frame_index = (@frame_index + 1) % frames.size
  end

  (BRAILLE_BASE + frames[@frame_index]).chr(Encoding::UTF_8)
end