thaum 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -14
  3. data/examples/checkbox.rb +89 -0
  4. data/examples/counter.rb +50 -0
  5. data/examples/hello_world.rb +28 -0
  6. data/examples/layout_demo.rb +138 -0
  7. data/examples/modal.rb +76 -0
  8. data/examples/mouse.rb +60 -0
  9. data/examples/octagram_picker.rb +224 -0
  10. data/examples/picker.rb +150 -0
  11. data/examples/progress_bar.rb +90 -0
  12. data/examples/scroll_view.rb +64 -0
  13. data/examples/select.rb +64 -0
  14. data/examples/spinner.rb +66 -0
  15. data/examples/status_bar.rb +65 -0
  16. data/examples/stopwatch.rb +84 -0
  17. data/examples/table.rb +196 -0
  18. data/examples/tabs.rb +112 -0
  19. data/examples/text.rb +101 -0
  20. data/examples/theme_picker.rb +95 -0
  21. data/examples/todo.rb +242 -0
  22. data/lib/thaum/action.rb +30 -0
  23. data/lib/thaum/app.rb +87 -0
  24. data/lib/thaum/color.rb +97 -0
  25. data/lib/thaum/concerns/context_update.rb +40 -0
  26. data/lib/thaum/concerns/focus.rb +53 -0
  27. data/lib/thaum/concerns/layout.rb +349 -0
  28. data/lib/thaum/concerns/modal.rb +102 -0
  29. data/lib/thaum/concerns/tab_navigation.rb +97 -0
  30. data/lib/thaum/dispatch.rb +149 -0
  31. data/lib/thaum/escape_parser.rb +265 -0
  32. data/lib/thaum/event.rb +13 -0
  33. data/lib/thaum/events.rb +28 -0
  34. data/lib/thaum/hit_test.rb +28 -0
  35. data/lib/thaum/input_reader.rb +46 -0
  36. data/lib/thaum/key_event.rb +13 -0
  37. data/lib/thaum/keys.rb +55 -0
  38. data/lib/thaum/minitest.rb +64 -0
  39. data/lib/thaum/octagram.rb +76 -0
  40. data/lib/thaum/painter.rb +49 -0
  41. data/lib/thaum/rect.rb +5 -0
  42. data/lib/thaum/rendering/box_drawing.rb +186 -0
  43. data/lib/thaum/rendering/buffer.rb +84 -0
  44. data/lib/thaum/rendering/canvas.rb +219 -0
  45. data/lib/thaum/rendering/cell.rb +11 -0
  46. data/lib/thaum/rendering/renderer.rb +98 -0
  47. data/lib/thaum/rendering/style.rb +13 -0
  48. data/lib/thaum/run_loop.rb +182 -0
  49. data/lib/thaum/seq.rb +91 -0
  50. data/lib/thaum/sigil.rb +41 -0
  51. data/lib/thaum/sigils/button.rb +47 -0
  52. data/lib/thaum/sigils/checkbox.rb +57 -0
  53. data/lib/thaum/sigils/progress_bar.rb +65 -0
  54. data/lib/thaum/sigils/scroll_view.rb +115 -0
  55. data/lib/thaum/sigils/select.rb +56 -0
  56. data/lib/thaum/sigils/spinner.rb +39 -0
  57. data/lib/thaum/sigils/status_bar.rb +89 -0
  58. data/lib/thaum/sigils/table.rb +156 -0
  59. data/lib/thaum/sigils/tabs.rb +59 -0
  60. data/lib/thaum/sigils/text.rb +22 -0
  61. data/lib/thaum/sigils/text_input.rb +86 -0
  62. data/lib/thaum/terminal.rb +46 -0
  63. data/lib/thaum/themes.rb +267 -0
  64. data/lib/thaum/tree.rb +16 -0
  65. data/lib/thaum/version.rb +1 -1
  66. data/lib/thaum.rb +64 -1
  67. metadata +114 -4
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/octagram_picker.rb
4
+ #
5
+ # Demonstrates Thaum::Octagram by packaging a filter-as-you-type picker
6
+ # (TextInput + filtered list) as a single distributable component.
7
+ #
8
+ # The Picker Octagram:
9
+ # - declares its own partition (TextInput on top, filtered list below)
10
+ # - draws a rounded border behind its children (the Octagram render hook)
11
+ # - intercepts events bubbling up from its children:
12
+ # :up/:down → forward to the internal list (App never sees them)
13
+ # :escape → emit Picker::CancelledEvent
14
+ # SubmittedEvent → translate to Picker::SelectedEvent(value:)
15
+ #
16
+ # Compare with examples/picker.rb (same UX, but the App had to know
17
+ # about the filter list and route :up/:down itself). With Octagram, the
18
+ # App only knows about Picker::SelectedEvent and Picker::CancelledEvent.
19
+
20
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
21
+ require "thaum"
22
+
23
+ LANGUAGES = %w[
24
+ Ruby Python Elixir Erlang Haskell OCaml Rust Go Crystal Zig
25
+ JavaScript TypeScript CoffeeScript Lua Perl Raku Tcl
26
+ C C++ Java Kotlin Scala Clojure Groovy
27
+ Swift Dart Nim D Julia
28
+ Lisp Scheme Racket Prolog SmallTalk Forth
29
+ ].freeze
30
+
31
+ class FilteredList
32
+ include Thaum::Sigil
33
+
34
+ def initialize(source:, query_source:)
35
+ @source = source
36
+ @query_source = query_source
37
+ @cursor = 0
38
+ @cached_query = nil
39
+ @cached_items = source
40
+ end
41
+
42
+ def focusable? = false
43
+
44
+ def items
45
+ q = @query_source.value
46
+ return @cached_items if q == @cached_query
47
+
48
+ @cached_query = q
49
+ @cached_items = q.empty? ? @source : @source.select { |s| s.downcase.include?(q.downcase) }
50
+ @cursor = @cursor.clamp(0, [@cached_items.length - 1, 0].max)
51
+ @cached_items
52
+ end
53
+
54
+ def current = items[@cursor]
55
+ def cursor_up = (@cursor = [@cursor - 1, 0].max)
56
+
57
+ def cursor_down
58
+ last = [items.length - 1, 0].max
59
+ @cursor = (@cursor + 1).clamp(0, last)
60
+ end
61
+
62
+ def render(canvas:, theme:)
63
+ list = items
64
+ canvas.fill(bg: theme.bg)
65
+ list.each_with_index do |item, i|
66
+ break if i >= canvas.height
67
+
68
+ draw_row(canvas: canvas, item: item, i: i, theme: theme)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def draw_row(canvas:, item:, i:, theme:)
75
+ row = canvas.row(i) or return
76
+
77
+ sel = i == @cursor
78
+ bg = sel ? theme.selection : theme.bg
79
+ fg = sel ? theme.selection_fg : theme.fg
80
+ row.fill(bg: bg)
81
+ row.text(content: " #{item}", fg: fg, bg: bg)
82
+ end
83
+ end
84
+
85
+ class Picker
86
+ include Thaum::Octagram
87
+
88
+ SelectedEvent = Thaum::Event.define(:value)
89
+ CancelledEvent = Thaum::Event.define
90
+
91
+ attr_reader :input, :list
92
+
93
+ def initialize(items:)
94
+ @input = Thaum::TextInput.new
95
+ @list = FilteredList.new(source: items, query_source: @input)
96
+ end
97
+
98
+ def partition
99
+ vertical do
100
+ region(height: 1) { @input }
101
+ region(height: :fill) { @list }
102
+ end
103
+ end
104
+
105
+ # Reserve a 1-cell ring around the children so the rounded border
106
+ # the render hook draws underneath survives.
107
+ def partition_inset = { top: 1, bottom: 1, left: 1, right: 1 }
108
+
109
+ def render(canvas:, theme:)
110
+ canvas.border(fg: theme.border, style: :rounded)
111
+ end
112
+
113
+ def on_key(event)
114
+ case event.key
115
+ when :up then @list.cursor_up
116
+ when :down then @list.cursor_down
117
+ when :escape then emit CancelledEvent.new
118
+ else emit event
119
+ end
120
+ end
121
+
122
+ def on_event(event)
123
+ case event
124
+ when Thaum::TextInput::SubmittedEvent
125
+ value = @list.current
126
+ emit(value ? SelectedEvent.new(value: value) : event)
127
+ else
128
+ emit event
129
+ end
130
+ end
131
+ end
132
+
133
+ class Hint
134
+ include Thaum::Sigil
135
+
136
+ def focusable? = false
137
+
138
+ def render(canvas:, theme:)
139
+ canvas.fill(bg: theme.bar_bg)
140
+ canvas.text(content: " type to filter ↑/↓ navigate enter picks esc cancels", fg: theme.dim)
141
+ end
142
+ end
143
+
144
+ # Sits next to the Picker. Belongs to the App, not the Octagram —
145
+ # visible proof that the App composes the Picker alongside other
146
+ # sigils it owns. Updates when the user navigates inside the Picker,
147
+ # polling the Picker's exposed list state at render time.
148
+ class Notes
149
+ include Thaum::Sigil
150
+
151
+ def initialize(picker:)
152
+ @picker = picker
153
+ end
154
+
155
+ def focusable? = false
156
+
157
+ def render(canvas:, theme:)
158
+ canvas.fill(bg: theme.bg)
159
+ inner = canvas.border(fg: theme.dim, style: :single)
160
+ draw_header(canvas: inner, theme: theme)
161
+ draw_state(canvas: inner, theme: theme)
162
+ end
163
+
164
+ private
165
+
166
+ def draw_header(canvas:, theme:)
167
+ canvas.text(content: "App-owned sigil", y: 0, fg: theme.accent)
168
+ canvas.text(content: "(not inside the", y: 1, fg: theme.dim)
169
+ canvas.text(content: " Picker Octagram)", y: 2, fg: theme.dim)
170
+ end
171
+
172
+ def draw_state(canvas:, theme:)
173
+ cursor = @picker.list.current || "—"
174
+ matches = @picker.list.items.length
175
+ canvas.text(content: "Query: #{@picker.input.value.inspect}", y: 4, fg: theme.fg)
176
+ canvas.text(content: "Matches: #{matches}", y: 5, fg: theme.fg)
177
+ canvas.text(content: "Cursor: #{cursor}", y: 6, fg: theme.fg)
178
+ end
179
+ end
180
+
181
+ class PickerApp
182
+ include Thaum::App
183
+
184
+ attr_reader :result
185
+
186
+ def initialize
187
+ @picker = Picker.new(items: LANGUAGES)
188
+ @notes = Notes.new(picker: @picker)
189
+ @hint = Hint.new
190
+ @result = nil
191
+ end
192
+
193
+ def theme = Thaum::Themes::CATPPUCCIN_MOCHA
194
+
195
+ def on_mount
196
+ focus(@picker.input)
197
+ end
198
+
199
+ def on_event(event)
200
+ case event
201
+ when Picker::SelectedEvent
202
+ @result = event.value
203
+ quit
204
+ when Picker::CancelledEvent
205
+ quit
206
+ end
207
+ end
208
+
209
+ def partition
210
+ vertical do
211
+ region(height: :fill) do
212
+ horizontal do
213
+ region(width: 42) { @picker }
214
+ region(width: :fill) { @notes }
215
+ end
216
+ end
217
+ region(height: 1) { @hint }
218
+ end
219
+ end
220
+ end
221
+
222
+ app = PickerApp.new
223
+ Thaum.run(app)
224
+ puts app.result if app.result
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/picker.rb
4
+ #
5
+ # Filter-as-you-type list selector. The TextInput on top filters the
6
+ # list below as you type. Arrow keys navigate the filtered results
7
+ # (focus stays on the input — :up/:down bubble out of TextInput, so the
8
+ # App routes them to the list). Enter picks the highlighted item and
9
+ # quits, printing the selection.
10
+
11
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
12
+ require "thaum"
13
+
14
+ LANGUAGES = %w[
15
+ Ruby Python Elixir Erlang Haskell OCaml Rust Go Crystal Zig
16
+ JavaScript TypeScript CoffeeScript Lua Perl Raku Tcl
17
+ C C++ Java Kotlin Scala Clojure Groovy
18
+ Swift Dart Nim D Julia
19
+ Lisp Scheme Racket Prolog SmallTalk Forth
20
+ ].freeze
21
+
22
+ class FilteredList
23
+ include Thaum::Sigil
24
+
25
+ PickedEvent = Thaum::Event.define(:value)
26
+
27
+ def initialize(source:, query_source:)
28
+ @source = source
29
+ @query_source = query_source
30
+ @cursor = 0
31
+ @cached_query = nil
32
+ @cached_items = source
33
+ end
34
+
35
+ def focusable? = false
36
+
37
+ def current = items[@cursor]
38
+
39
+ def items
40
+ q = @query_source.value
41
+ return @cached_items if q == @cached_query
42
+
43
+ @cached_query = q
44
+ @cached_items = q.empty? ? @source : @source.select { |s| s.downcase.include?(q.downcase) }
45
+ @cursor = @cursor.clamp(0, [@cached_items.length - 1, 0].max)
46
+ @cached_items
47
+ end
48
+
49
+ def cursor_up
50
+ @cursor = [@cursor - 1, 0].max
51
+ end
52
+
53
+ def cursor_down
54
+ last = [items.length - 1, 0].max
55
+ @cursor = (@cursor + 1).clamp(0, last)
56
+ end
57
+
58
+ def pick
59
+ return if items.empty?
60
+
61
+ emit PickedEvent.new(value: current)
62
+ end
63
+
64
+ def render(canvas:, theme:)
65
+ list = items
66
+ offset = scroll_offset(height: canvas.height, count: list.length)
67
+ canvas.fill(bg: theme.bg)
68
+ visible = list[offset, canvas.height] || []
69
+ visible.each_with_index do |item, row_idx|
70
+ draw_row(canvas: canvas, item: item, item_idx: offset + row_idx, row_idx: row_idx, theme: theme)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def draw_row(canvas:, item:, item_idx:, row_idx:, theme:)
77
+ row = canvas.row(row_idx) or return
78
+
79
+ sel = item_idx == @cursor
80
+ bg = sel ? theme.selection : theme.bg
81
+ fg = sel ? theme.selection_fg : theme.fg
82
+ row.fill(bg: bg)
83
+ row.text(content: " #{item}", fg: fg, bg: bg)
84
+ end
85
+
86
+ def scroll_offset(height:, count:)
87
+ return 0 if @cursor < height || count <= height
88
+
89
+ [@cursor - height + 1, count - height].min
90
+ end
91
+ end
92
+
93
+ class HintBar
94
+ include Thaum::Sigil
95
+
96
+ def focusable? = false
97
+
98
+ def render(canvas:, theme:)
99
+ canvas.fill(bg: theme.bar_bg)
100
+ canvas.text(content: " type to filter ↑/↓ navigate enter picks esc quits", fg: theme.dim)
101
+ end
102
+ end
103
+
104
+ class PickerApp
105
+ include Thaum::App
106
+
107
+ def initialize
108
+ @input = Thaum::TextInput.new
109
+ @list = FilteredList.new(source: LANGUAGES, query_source: @input)
110
+ @hint = HintBar.new
111
+ @picked = nil
112
+ end
113
+
114
+ def theme = Thaum::Themes::CATPPUCCIN_MOCHA
115
+ def result = @picked
116
+
117
+ def on_mount
118
+ focus(@input)
119
+ end
120
+
121
+ def on_key(event)
122
+ case event.key
123
+ when :up then @list.cursor_up
124
+ when :down then @list.cursor_down
125
+ when :escape then quit
126
+ end
127
+ end
128
+
129
+ def on_event(event)
130
+ case event
131
+ when Thaum::TextInput::SubmittedEvent
132
+ @list.pick
133
+ when FilteredList::PickedEvent
134
+ @picked = event.value
135
+ quit
136
+ end
137
+ end
138
+
139
+ def partition
140
+ vertical do
141
+ region(height: 1) { @input }
142
+ region(height: :fill) { @list }
143
+ region(height: 1) { @hint }
144
+ end
145
+ end
146
+ end
147
+
148
+ app = PickerApp.new
149
+ Thaum.run(app)
150
+ puts app.result if app.result
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/progress_bar.rb
4
+ #
5
+ # Demonstrates Thaum::ProgressBar in both modes:
6
+ # - Top bar: determinate, auto-fills from 0 to 1.0 over ~5s; resets at 100%
7
+ # - Bottom bar: indeterminate, a fixed-width block walks across forever
8
+ #
9
+ # Press esc to quit, r to reset the determinate bar.
10
+
11
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
12
+ require "thaum"
13
+
14
+ class Driver
15
+ include Thaum::Sigil
16
+
17
+ def initialize(bar:, interval: 0.1, step: 0.01)
18
+ @bar = bar
19
+ @interval = interval
20
+ @step = step
21
+ @elapsed = 0.0
22
+ end
23
+
24
+ def focusable? = false
25
+
26
+ def on_tick(event)
27
+ @elapsed += event.delta
28
+ advanced = false
29
+ while @elapsed >= @interval
30
+ @elapsed -= @interval
31
+ @bar.value = (@bar.value + @step) > 1.0 ? 0.0 : @bar.value + @step
32
+ advanced = true
33
+ end
34
+ request_render if advanced
35
+ end
36
+
37
+ def render(canvas:, theme:); end
38
+ end
39
+
40
+ class Label
41
+ include Thaum::Sigil
42
+
43
+ def initialize(text:, semantic_fg: nil)
44
+ @text = text
45
+ @semantic_fg = semantic_fg
46
+ end
47
+
48
+ def focusable? = false
49
+
50
+ def render(canvas:, theme:)
51
+ canvas.fill(bg: theme.bg)
52
+ fg = @semantic_fg ? theme.send(@semantic_fg) : theme.dim
53
+ canvas.text(content: @text, fg: fg)
54
+ end
55
+ end
56
+
57
+ class ProgressApp
58
+ include Thaum::App
59
+
60
+ def initialize
61
+ @determinate = Thaum::ProgressBar.new(value: 0.0)
62
+ @indeterminate = Thaum::ProgressBar.new(indeterminate: true)
63
+ @driver = Driver.new(bar: @determinate)
64
+ @det_label = Label.new(text: " determinate (auto-fills, resets at 100%)", semantic_fg: :info_fg)
65
+ @indet_label = Label.new(text: " indeterminate (always animating)", semantic_fg: :muted_fg)
66
+ @hint = Label.new(text: " esc to quit r to reset determinate bar", semantic_fg: :dim)
67
+ end
68
+
69
+ def theme = Thaum::Themes::CATPPUCCIN_MOCHA
70
+
71
+ def on_key(event)
72
+ case event.key
73
+ when :escape then quit
74
+ when "r" then @determinate.value = 0.0
75
+ end
76
+ end
77
+
78
+ def partition
79
+ vertical do
80
+ region(height: 1) { @det_label }
81
+ region(height: 1) { @determinate }
82
+ region(height: 1) { @indet_label }
83
+ region(height: 1) { @indeterminate }
84
+ region(height: :fill) { @hint }
85
+ region(height: 0) { @driver } # off-screen ticker (height: 0 → no render)
86
+ end
87
+ end
88
+ end
89
+
90
+ Thaum.run(ProgressApp.new)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/scroll_view.rb
4
+ #
5
+ # Demonstrates Thaum::ScrollView. Renders a long, optionally wide buffer
6
+ # of pre-formatted text rows. Vertical and horizontal scrolling, ▲/▼
7
+ # indicators when off-viewport content exists.
8
+ #
9
+ # Keys: ↑/↓/pgup/pgdn/home/end for vertical, ←/→ for horizontal, esc to quit.
10
+ # Mouse wheel scrolls vertically.
11
+
12
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
13
+ require "thaum"
14
+
15
+ ROWS = (1..120).map do |n|
16
+ num = n.to_s.rjust(3)
17
+ case n % 5
18
+ when 0 then "#{num} #{'─' * 90}"
19
+ when 1 then "#{num} the quick brown fox jumps over the lazy dog #{n} times"
20
+ when 2 then "#{num} ねこは魚を食べる — wide chars stay aligned because ScrollView slices by display column"
21
+ when 3 then "#{num} far-right column reachable only by horizontal scrolling →→→→→→→→→ here: #{n * 7}"
22
+ else "#{num} short row"
23
+ end
24
+ end.freeze
25
+
26
+ class Hint
27
+ include Thaum::Sigil
28
+
29
+ def focusable? = false
30
+
31
+ def render(canvas:, theme:)
32
+ canvas.fill(bg: theme.bar_bg)
33
+ canvas.text(
34
+ content: " ↑/↓ or wheel scroll pgup/pgdn page ←/→ horizontal home/end edges esc quits",
35
+ fg: theme.dim
36
+ )
37
+ end
38
+ end
39
+
40
+ class ScrollViewApp
41
+ include Thaum::App
42
+
43
+ def initialize
44
+ @view = Thaum::ScrollView.new(rows: ROWS)
45
+ @hint = Hint.new
46
+ end
47
+
48
+ def on_mount
49
+ focus(@view)
50
+ end
51
+
52
+ def on_key(event)
53
+ quit if event.key == :escape
54
+ end
55
+
56
+ def partition
57
+ vertical do
58
+ region(height: :fill) { @view }
59
+ region(height: 1) { @hint }
60
+ end
61
+ end
62
+ end
63
+
64
+ Thaum.run(ScrollViewApp.new)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/select.rb
4
+ #
5
+ # Demonstrates Thaum::Select. Up/down to navigate, Enter to pick.
6
+ # The chosen item is printed after the app exits.
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
9
+ require "thaum"
10
+
11
+ FRUITS = %w[
12
+ apricot banana cherry date elderberry fig grape honeydew
13
+ imbe jujube kiwi lemon mango nectarine orange papaya
14
+ quince raspberry strawberry tangerine ugni vanilla watermelon
15
+ ].freeze
16
+
17
+ class Hint
18
+ include Thaum::Sigil
19
+
20
+ def focusable? = false
21
+
22
+ def render(canvas:, theme:)
23
+ canvas.fill(bg: theme.bar_bg)
24
+ canvas.text(content: " ↑/↓ navigate enter picks esc quits", fg: theme.info_fg)
25
+ end
26
+ end
27
+
28
+ class SelectApp
29
+ include Thaum::App
30
+
31
+ attr_reader :result
32
+
33
+ def initialize
34
+ @select = Thaum::Select.new(items: FRUITS)
35
+ @hint = Hint.new
36
+ @result = nil
37
+ end
38
+
39
+ def on_mount
40
+ focus(@select)
41
+ end
42
+
43
+ def on_key(event)
44
+ quit if event.key == :escape
45
+ end
46
+
47
+ def on_event(event)
48
+ return unless event.is_a?(Thaum::Select::SelectedEvent)
49
+
50
+ @result = event.item
51
+ quit
52
+ end
53
+
54
+ def partition
55
+ vertical do
56
+ region(height: :fill) { @select }
57
+ region(height: 1) { @hint }
58
+ end
59
+ end
60
+ end
61
+
62
+ app = SelectApp.new
63
+ Thaum.run(app)
64
+ puts app.result if app.result
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/spinner.rb
4
+ #
5
+ # Demonstrates Thaum::Spinner — animation frame driven by on_tick.
6
+ # Shows the default braille spinner and two custom-frame variants
7
+ # stacked side by side. Press esc to quit.
8
+
9
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
10
+ require "thaum"
11
+
12
+ class Label
13
+ include Thaum::Sigil
14
+
15
+ def initialize(text)
16
+ @text = text
17
+ end
18
+
19
+ def focusable? = false
20
+
21
+ def render(canvas:, theme:)
22
+ canvas.fill(bg: theme.bg)
23
+ canvas.text(content: @text, fg: theme.dim)
24
+ end
25
+ end
26
+
27
+ class SpinnerApp
28
+ include Thaum::App
29
+
30
+ def initialize
31
+ @default = Thaum::Spinner.new # ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
32
+ @dots = Thaum::Spinner.new(frames: %w[. .. ...], interval: 0.3)
33
+ @arrow = Thaum::Spinner.new(frames: %w[← ↖ ↑ ↗ → ↘ ↓ ↙], interval: 0.12)
34
+
35
+ @label_a = Label.new(" default braille")
36
+ @label_b = Label.new(" three dots")
37
+ @label_c = Label.new(" arrow rotation")
38
+ @hint = Label.new(" esc to quit")
39
+ end
40
+
41
+ def theme = Thaum::Themes::CATPPUCCIN_MOCHA
42
+
43
+ def on_key(event)
44
+ quit if event.key == :escape
45
+ end
46
+
47
+ def partition
48
+ vertical do
49
+ region(height: 1) { spinner_row(spinner: @default, label: @label_a) }
50
+ region(height: 1) { spinner_row(spinner: @dots, label: @label_b) }
51
+ region(height: 1) { spinner_row(spinner: @arrow, label: @label_c) }
52
+ region(height: :fill) { @hint }
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def spinner_row(spinner:, label:)
59
+ horizontal do
60
+ region(width: 4) { spinner }
61
+ region(width: :fill) { label }
62
+ end
63
+ end
64
+ end
65
+
66
+ Thaum.run(SpinnerApp.new)