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.
- checksums.yaml +4 -4
- data/README.md +106 -14
- data/examples/checkbox.rb +89 -0
- data/examples/counter.rb +50 -0
- data/examples/hello_world.rb +28 -0
- data/examples/layout_demo.rb +138 -0
- data/examples/modal.rb +76 -0
- data/examples/mouse.rb +60 -0
- data/examples/octagram_picker.rb +224 -0
- data/examples/picker.rb +150 -0
- data/examples/progress_bar.rb +90 -0
- data/examples/scroll_view.rb +64 -0
- data/examples/select.rb +64 -0
- data/examples/spinner.rb +66 -0
- data/examples/status_bar.rb +65 -0
- data/examples/stopwatch.rb +84 -0
- data/examples/table.rb +196 -0
- data/examples/tabs.rb +112 -0
- data/examples/text.rb +101 -0
- data/examples/theme_picker.rb +95 -0
- data/examples/todo.rb +242 -0
- data/lib/thaum/action.rb +30 -0
- data/lib/thaum/app.rb +87 -0
- data/lib/thaum/color.rb +97 -0
- data/lib/thaum/concerns/context_update.rb +40 -0
- data/lib/thaum/concerns/focus.rb +53 -0
- data/lib/thaum/concerns/layout.rb +349 -0
- data/lib/thaum/concerns/modal.rb +102 -0
- data/lib/thaum/concerns/tab_navigation.rb +97 -0
- data/lib/thaum/dispatch.rb +149 -0
- data/lib/thaum/escape_parser.rb +265 -0
- data/lib/thaum/event.rb +13 -0
- data/lib/thaum/events.rb +28 -0
- data/lib/thaum/hit_test.rb +28 -0
- data/lib/thaum/input_reader.rb +46 -0
- data/lib/thaum/key_event.rb +13 -0
- data/lib/thaum/keys.rb +55 -0
- data/lib/thaum/minitest.rb +64 -0
- data/lib/thaum/octagram.rb +76 -0
- data/lib/thaum/painter.rb +49 -0
- data/lib/thaum/rect.rb +5 -0
- data/lib/thaum/rendering/box_drawing.rb +186 -0
- data/lib/thaum/rendering/buffer.rb +84 -0
- data/lib/thaum/rendering/canvas.rb +219 -0
- data/lib/thaum/rendering/cell.rb +11 -0
- data/lib/thaum/rendering/renderer.rb +98 -0
- data/lib/thaum/rendering/style.rb +13 -0
- data/lib/thaum/run_loop.rb +182 -0
- data/lib/thaum/seq.rb +91 -0
- data/lib/thaum/sigil.rb +41 -0
- data/lib/thaum/sigils/button.rb +47 -0
- data/lib/thaum/sigils/checkbox.rb +57 -0
- data/lib/thaum/sigils/progress_bar.rb +65 -0
- data/lib/thaum/sigils/scroll_view.rb +115 -0
- data/lib/thaum/sigils/select.rb +56 -0
- data/lib/thaum/sigils/spinner.rb +39 -0
- data/lib/thaum/sigils/status_bar.rb +89 -0
- data/lib/thaum/sigils/table.rb +156 -0
- data/lib/thaum/sigils/tabs.rb +59 -0
- data/lib/thaum/sigils/text.rb +22 -0
- data/lib/thaum/sigils/text_input.rb +86 -0
- data/lib/thaum/terminal.rb +46 -0
- data/lib/thaum/themes.rb +267 -0
- data/lib/thaum/tree.rb +16 -0
- data/lib/thaum/version.rb +1 -1
- data/lib/thaum.rb +64 -1
- 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
|
data/examples/picker.rb
ADDED
|
@@ -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)
|
data/examples/select.rb
ADDED
|
@@ -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
|
data/examples/spinner.rb
ADDED
|
@@ -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)
|