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,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Usage: bundle exec ruby examples/status_bar.rb
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates Thaum::StatusBar. Three segments along the bottom:
|
|
6
|
+
# - "Ready" (plain)
|
|
7
|
+
# - clock (plain, updates each tick)
|
|
8
|
+
# - "[ Quit ]" (clickable — exits the app)
|
|
9
|
+
#
|
|
10
|
+
# Click the Quit segment with the mouse, or press esc.
|
|
11
|
+
|
|
12
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
13
|
+
require "thaum"
|
|
14
|
+
|
|
15
|
+
class Body
|
|
16
|
+
include Thaum::Sigil
|
|
17
|
+
|
|
18
|
+
def focusable? = false
|
|
19
|
+
|
|
20
|
+
def render(canvas:, theme:)
|
|
21
|
+
canvas.fill(bg: theme.bg)
|
|
22
|
+
inner = canvas.border(fg: theme.border, style: :rounded)
|
|
23
|
+
inner.text(content: " StatusBar demo", y: 1, fg: theme.fg)
|
|
24
|
+
inner.text(content: " Click [ Quit ] in the bar, or press esc.", y: 3, fg: theme.info_fg)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class StatusBarApp
|
|
29
|
+
include Thaum::App
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@body = Body.new
|
|
33
|
+
@bar = Thaum::StatusBar.new(segments: segments)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def theme = Thaum::Themes::CATPPUCCIN_MOCHA
|
|
37
|
+
|
|
38
|
+
def on_key(event)
|
|
39
|
+
quit if event.key == :escape
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_tick(_event)
|
|
43
|
+
new_segments = segments
|
|
44
|
+
@bar.segments = new_segments
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def partition
|
|
48
|
+
vertical do
|
|
49
|
+
region(height: :fill) { @body }
|
|
50
|
+
region(height: 1) { @bar }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def segments
|
|
57
|
+
[
|
|
58
|
+
"Ready",
|
|
59
|
+
Time.now.strftime("%H:%M:%S"),
|
|
60
|
+
{ label: "[ Quit ]", on_click: -> { quit } }
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Thaum.run(StatusBarApp.new)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Usage: bundle exec ruby examples/stopwatch.rb
|
|
4
|
+
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
6
|
+
require "thaum"
|
|
7
|
+
|
|
8
|
+
class StopwatchSigil
|
|
9
|
+
include Thaum::Sigil
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@elapsed = 0.0
|
|
13
|
+
@running = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def on_key(event)
|
|
17
|
+
case event.key
|
|
18
|
+
when " " then @running = !@running
|
|
19
|
+
when "r" then @elapsed = 0.0
|
|
20
|
+
else emit(event)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def on_tick(event)
|
|
25
|
+
return unless @running
|
|
26
|
+
|
|
27
|
+
@elapsed += event.delta
|
|
28
|
+
request_render
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(canvas:, theme:)
|
|
32
|
+
mid = canvas.height / 2
|
|
33
|
+
canvas.fill(bg: theme.bg)
|
|
34
|
+
canvas.text(content: format_time, fg: @running ? theme.accent : theme.fg, align: :center, y: mid - 1)
|
|
35
|
+
hint = @running ? "space pause r reset t theme esc quit" : "space start r reset t theme esc quit"
|
|
36
|
+
canvas.text(content: hint, fg: theme.dim, align: :center, y: mid + 1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def format_time
|
|
42
|
+
minutes = (@elapsed / 60).to_i
|
|
43
|
+
seconds = (@elapsed % 60).to_i
|
|
44
|
+
centis = ((@elapsed * 100) % 100).to_i
|
|
45
|
+
format("%<m>02d:%<s>02d.%<c>02d", m: minutes, s: seconds, c: centis)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class StopwatchApp
|
|
50
|
+
include Thaum::App
|
|
51
|
+
|
|
52
|
+
THEME_CYCLE = %i[catppuccin_mocha gruvbox_dark nord dracula solarized_dark catppuccin_latte].freeze
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@stopwatch = StopwatchSigil.new
|
|
56
|
+
@theme_index = 0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Thaum.run reads this fresh every frame; swapping the index and requesting
|
|
60
|
+
# a render is all that's needed to retheme the whole app.
|
|
61
|
+
def theme = Thaum::Themes[THEME_CYCLE[@theme_index]]
|
|
62
|
+
|
|
63
|
+
def on_key(event)
|
|
64
|
+
case event.key
|
|
65
|
+
when :escape then quit
|
|
66
|
+
when "t" then cycle_theme
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def partition
|
|
71
|
+
vertical do
|
|
72
|
+
region(height: :fill) { @stopwatch }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def cycle_theme
|
|
79
|
+
@theme_index = (@theme_index + 1) % THEME_CYCLE.length
|
|
80
|
+
request_render
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Thaum.run(StopwatchApp.new, tick: 0.05)
|
data/examples/table.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Usage: bundle exec ruby examples/table.rb
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates Thaum::Table. Auto-computed column widths, vertical
|
|
6
|
+
# scrolling, row selection. Enter prints the selected row after exit.
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
9
|
+
require "thaum"
|
|
10
|
+
|
|
11
|
+
HEADERS = %w[Name Size Type Modified].freeze
|
|
12
|
+
|
|
13
|
+
ROWS = [
|
|
14
|
+
["CLAUDE.md", "1.2K", "doc", "2026-05-27"],
|
|
15
|
+
["DECISIONS.md", "8.4K", "doc", "2026-06-10"],
|
|
16
|
+
["Gemfile", "237B", "ruby", "2026-05-27"],
|
|
17
|
+
["LICENSE.txt", "1.1K", "text", "2026-05-27"],
|
|
18
|
+
["README.md", "412B", "doc", "2026-05-27"],
|
|
19
|
+
["Rakefile", "188B", "ruby", "2026-05-27"],
|
|
20
|
+
["bin/console", "187B", "ruby", "2026-05-27"],
|
|
21
|
+
["bin/setup", "133B", "ruby", "2026-05-27"],
|
|
22
|
+
["examples/counter.rb", "1.1K", "ruby", "2026-05-31"],
|
|
23
|
+
["examples/layout_demo.rb", "2.3K", "ruby", "2026-06-04"],
|
|
24
|
+
["examples/picker.rb", "3.6K", "ruby", "2026-06-15"],
|
|
25
|
+
["examples/stopwatch.rb", "1.9K", "ruby", "2026-06-02"],
|
|
26
|
+
["examples/theme_picker.rb", "2.0K", "ruby", "2026-06-10"],
|
|
27
|
+
["examples/todo.rb", "5.4K", "ruby", "2026-06-09"],
|
|
28
|
+
["lib/thaum.rb", "4.7K", "ruby", "2026-06-15"],
|
|
29
|
+
["lib/thaum/action.rb", "1.4K", "ruby", "2026-06-04"],
|
|
30
|
+
["lib/thaum/app.rb", "2.9K", "ruby", "2026-06-10"],
|
|
31
|
+
["lib/thaum/buffer.rb", "1.6K", "ruby", "2026-06-02"],
|
|
32
|
+
["lib/thaum/canvas.rb", "5.1K", "ruby", "2026-06-10"],
|
|
33
|
+
["lib/thaum/color.rb", "3.3K", "ruby", "2026-06-08"],
|
|
34
|
+
["lib/thaum/escape_parser.rb", "4.2K", "ruby", "2026-06-02"],
|
|
35
|
+
["lib/thaum/events.rb", "418B", "ruby", "2026-06-10"],
|
|
36
|
+
["lib/thaum/layout.rb", "3.7K", "ruby", "2026-06-04"],
|
|
37
|
+
["lib/thaum/renderer.rb", "4.5K", "ruby", "2026-06-08"],
|
|
38
|
+
["lib/thaum/sigil.rb", "1.1K", "ruby", "2026-06-04"],
|
|
39
|
+
["lib/thaum/sigils/button.rb", "744B", "ruby", "2026-06-05"],
|
|
40
|
+
["lib/thaum/sigils/scroll_view.rb", "2.6K", "ruby", "2026-06-15"],
|
|
41
|
+
["lib/thaum/sigils/select.rb", "1.1K", "ruby", "2026-06-05"],
|
|
42
|
+
["lib/thaum/sigils/table.rb", "3.9K", "ruby", "2026-06-15"],
|
|
43
|
+
["lib/thaum/sigils/text.rb", "316B", "ruby", "2026-06-04"],
|
|
44
|
+
["lib/thaum/sigils/text_input.rb", "1.7K", "ruby", "2026-06-15"],
|
|
45
|
+
["lib/thaum/themes.rb", "2.8K", "ruby", "2026-06-10"]
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
class Hint
|
|
49
|
+
include Thaum::Sigil
|
|
50
|
+
|
|
51
|
+
def focusable? = false
|
|
52
|
+
|
|
53
|
+
def render(canvas:, theme:)
|
|
54
|
+
canvas.fill(bg: theme.bar_bg)
|
|
55
|
+
canvas.text(content: " ↑/↓ navigate pgup/pgdn jump home/end edges enter picks esc quits",
|
|
56
|
+
fg: theme.muted_fg)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class FileTable
|
|
61
|
+
include Thaum::Sigil
|
|
62
|
+
|
|
63
|
+
PAGE_STEP = 10
|
|
64
|
+
|
|
65
|
+
attr_reader :rows, :cursor, :offset
|
|
66
|
+
|
|
67
|
+
def initialize(headers:, rows:)
|
|
68
|
+
@headers = headers
|
|
69
|
+
@rows = rows
|
|
70
|
+
@cursor = 0
|
|
71
|
+
@offset = 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
SelectedEvent = Thaum::Event.define(:index, :row)
|
|
75
|
+
|
|
76
|
+
def on_key(event)
|
|
77
|
+
case event.key
|
|
78
|
+
when :up then @cursor = [@cursor - 1, 0].max
|
|
79
|
+
when :down then @cursor = [@cursor + 1, rows.length - 1].min if rows.any?
|
|
80
|
+
when :home then @cursor = 0
|
|
81
|
+
when :end then @cursor = rows.length - 1 if rows.any?
|
|
82
|
+
when :page_up then @cursor = [@cursor - PAGE_STEP, 0].max
|
|
83
|
+
when :page_down then @cursor = [@cursor + PAGE_STEP, rows.length - 1].min if rows.any?
|
|
84
|
+
when :enter then emit_selected
|
|
85
|
+
else emit event
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render(canvas:, theme:)
|
|
90
|
+
canvas.fill(bg: theme.bg)
|
|
91
|
+
visible_offset(canvas)
|
|
92
|
+
|
|
93
|
+
render_header(canvas: canvas, theme: theme)
|
|
94
|
+
render_separator(canvas: canvas, theme: theme)
|
|
95
|
+
render_data_rows(canvas: canvas, theme: theme)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def emit_selected
|
|
101
|
+
return if rows.empty?
|
|
102
|
+
|
|
103
|
+
emit SelectedEvent.new(index: @cursor, row: rows[@cursor])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_header(canvas:, theme:)
|
|
107
|
+
row = canvas.row(0) or return
|
|
108
|
+
|
|
109
|
+
row.fill(bg: theme.bar_bg)
|
|
110
|
+
row.text(content: @headers.join(" | "), fg: theme.accent, bg: theme.bar_bg)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render_separator(canvas:, theme:)
|
|
114
|
+
row = canvas.row(1) or return
|
|
115
|
+
|
|
116
|
+
row.fill(bg: theme.bg)
|
|
117
|
+
row.text(content: "─" * canvas.width, fg: theme.border, bg: theme.bg)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_data_rows(canvas:, theme:, widths: nil)
|
|
121
|
+
visible_rows = canvas.height - 2
|
|
122
|
+
return if visible_rows <= 0
|
|
123
|
+
|
|
124
|
+
visible_rows.times do |i|
|
|
125
|
+
file_idx = @offset + i
|
|
126
|
+
row_data = rows[file_idx] or break
|
|
127
|
+
row = canvas.row(i + 2) or break
|
|
128
|
+
|
|
129
|
+
selected = file_idx == @cursor
|
|
130
|
+
bg = selected ? theme.selection : theme.bg
|
|
131
|
+
fg = color_for_type(row_data[2], selected: selected, theme: theme)
|
|
132
|
+
row.fill(bg: bg)
|
|
133
|
+
row.text(content: row_data.join(" | "), fg: fg, bg: bg)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def visible_offset(canvas)
|
|
138
|
+
visible_rows = canvas.height - 2
|
|
139
|
+
return if visible_rows <= 0
|
|
140
|
+
|
|
141
|
+
if @cursor < @offset
|
|
142
|
+
@offset = @cursor
|
|
143
|
+
elsif @cursor >= @offset + visible_rows
|
|
144
|
+
@offset = @cursor - visible_rows + 1
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def color_for_type(file_type, selected:, theme:)
|
|
149
|
+
return theme.selection_fg if selected
|
|
150
|
+
|
|
151
|
+
case file_type
|
|
152
|
+
when "ruby" then theme.success_fg
|
|
153
|
+
when "doc" then theme.info_fg
|
|
154
|
+
when "text" then theme.muted_fg
|
|
155
|
+
else theme.fg
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class TableApp
|
|
161
|
+
include Thaum::App
|
|
162
|
+
|
|
163
|
+
attr_reader :result
|
|
164
|
+
|
|
165
|
+
def initialize
|
|
166
|
+
@table = FileTable.new(headers: HEADERS, rows: ROWS)
|
|
167
|
+
@hint = Hint.new
|
|
168
|
+
@result = nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def on_mount
|
|
172
|
+
focus(@table)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def on_key(event)
|
|
176
|
+
quit if event.key == :escape
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def on_event(event)
|
|
180
|
+
return unless event.is_a?(FileTable::SelectedEvent)
|
|
181
|
+
|
|
182
|
+
@result = event.row
|
|
183
|
+
quit
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def partition
|
|
187
|
+
vertical do
|
|
188
|
+
region(height: :fill) { @table }
|
|
189
|
+
region(height: 1) { @hint }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
app = TableApp.new
|
|
195
|
+
Thaum.run(app)
|
|
196
|
+
puts app.result.join(" | ") if app.result
|
data/examples/tabs.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Usage: bundle exec ruby examples/tabs.rb
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates Thaum::Tabs. Four tabs along the top; ←/→ navigate, the
|
|
6
|
+
# content pane below switches via Tabs::ActivatedEvent. Press esc to quit.
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
9
|
+
require "thaum"
|
|
10
|
+
|
|
11
|
+
PAGES = {
|
|
12
|
+
"Overview" => [
|
|
13
|
+
"Thaum is a Ruby TUI framework.",
|
|
14
|
+
"",
|
|
15
|
+
"Sigils render and handle events.",
|
|
16
|
+
"Layouts partition space.",
|
|
17
|
+
"Octagrams package both as composites."
|
|
18
|
+
],
|
|
19
|
+
"Sigils" => [
|
|
20
|
+
"Built-in sigils:",
|
|
21
|
+
"",
|
|
22
|
+
" Text, TextInput, Select, Button",
|
|
23
|
+
" Table, ScrollView",
|
|
24
|
+
" Spinner, ProgressBar, Checkbox, Tabs"
|
|
25
|
+
],
|
|
26
|
+
"Themes" => [
|
|
27
|
+
"Eight themes ship with the framework:",
|
|
28
|
+
"",
|
|
29
|
+
" Catppuccin Mocha / Latte",
|
|
30
|
+
" Gruvbox Dark, Nord, Dracula",
|
|
31
|
+
" Solarized Dark / Light, Material"
|
|
32
|
+
],
|
|
33
|
+
"Keys" => [
|
|
34
|
+
"Universal: Ctrl-C exits.",
|
|
35
|
+
"",
|
|
36
|
+
"Tab / Shift-Tab cycle focus.",
|
|
37
|
+
"Bracketed paste becomes PasteEvent.",
|
|
38
|
+
"Mouse support arrives in a later phase."
|
|
39
|
+
]
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
class Page
|
|
43
|
+
include Thaum::Sigil
|
|
44
|
+
|
|
45
|
+
def initialize(tabs)
|
|
46
|
+
@tabs = tabs
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def focusable? = false
|
|
50
|
+
|
|
51
|
+
def render(canvas:, theme:)
|
|
52
|
+
canvas.fill(bg: theme.bg)
|
|
53
|
+
inner = canvas.border(fg: theme.border, style: :rounded)
|
|
54
|
+
lines = PAGES.fetch(@tabs.current)
|
|
55
|
+
lines.each_with_index do |line, i|
|
|
56
|
+
break if i >= inner.height
|
|
57
|
+
|
|
58
|
+
inner.text(content: " #{line}", y: i, fg: theme.fg)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Hint
|
|
64
|
+
include Thaum::Sigil
|
|
65
|
+
|
|
66
|
+
def focusable? = false
|
|
67
|
+
|
|
68
|
+
def render(canvas:, theme:)
|
|
69
|
+
canvas.fill(bg: theme.bar_bg)
|
|
70
|
+
canvas.text(content: " ←/→ switch tabs esc quits", fg: theme.dim)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class TabsApp
|
|
75
|
+
include Thaum::App
|
|
76
|
+
|
|
77
|
+
def initialize
|
|
78
|
+
@tabs = Thaum::Tabs.new(labels: PAGES.keys)
|
|
79
|
+
@page = Page.new(@tabs)
|
|
80
|
+
@hint = Hint.new
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def theme = Thaum::Themes::CATPPUCCIN_MOCHA
|
|
84
|
+
|
|
85
|
+
def on_mount
|
|
86
|
+
focus(@tabs)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def on_key(event)
|
|
90
|
+
quit if event.key == :escape
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Tabs::ActivatedEvent bubbles up every time the active tab changes. The
|
|
94
|
+
# Page sigil reads @tabs.current on each render, so we don't need to
|
|
95
|
+
# do anything with it — but acknowledging it silences the framework's
|
|
96
|
+
# default "unhandled event" warning to stderr.
|
|
97
|
+
def on_event(event)
|
|
98
|
+
return if event.is_a?(Thaum::Tabs::ActivatedEvent)
|
|
99
|
+
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def partition
|
|
104
|
+
vertical do
|
|
105
|
+
region(height: 1) { @tabs }
|
|
106
|
+
region(height: :fill) { @page }
|
|
107
|
+
region(height: 1) { @hint }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Thaum.run(TabsApp.new)
|
data/examples/text.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Usage: bundle exec ruby examples/text.rb
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates Thaum::Text — the configurable drop-in Sigil for static
|
|
6
|
+
# content. Each row shows a different (align, wrap) combination.
|
|
7
|
+
# Press esc to quit.
|
|
8
|
+
|
|
9
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
10
|
+
require "thaum"
|
|
11
|
+
|
|
12
|
+
LONG = "Thaum is a Ruby TUI framework. Sigils render and handle events; " \
|
|
13
|
+
"Layouts partition space. This sentence is long enough to demonstrate " \
|
|
14
|
+
"word-wrap inside a bounded region."
|
|
15
|
+
|
|
16
|
+
class Bar
|
|
17
|
+
include Thaum::Sigil
|
|
18
|
+
|
|
19
|
+
def initialize(label:, semantic_fg: nil)
|
|
20
|
+
@label = label
|
|
21
|
+
@semantic_fg = semantic_fg
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def focusable? = false
|
|
25
|
+
|
|
26
|
+
def render(canvas:, theme:)
|
|
27
|
+
canvas.fill(bg: theme.bar_bg)
|
|
28
|
+
fg = @semantic_fg ? theme.send(@semantic_fg) : theme.muted_fg
|
|
29
|
+
canvas.text(content: " #{@label}", fg: fg)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class SemanticText
|
|
34
|
+
include Thaum::Sigil
|
|
35
|
+
|
|
36
|
+
def initialize(label:, semantic_color:)
|
|
37
|
+
@label = label
|
|
38
|
+
@semantic_color = semantic_color
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def focusable? = false
|
|
42
|
+
|
|
43
|
+
def render(canvas:, theme:)
|
|
44
|
+
fg = theme.send(@semantic_color)
|
|
45
|
+
content = "#{@label}: #{@semantic_color}".ljust(60)
|
|
46
|
+
|
|
47
|
+
canvas.fill(bg: theme.bg)
|
|
48
|
+
canvas.text(content:, fg:)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class TextApp
|
|
53
|
+
include Thaum::App
|
|
54
|
+
|
|
55
|
+
def initialize
|
|
56
|
+
@bar_align = Bar.new(label: "alignment — left / center / right")
|
|
57
|
+
@left = Thaum::Text.new(content: "left-aligned", align: :left)
|
|
58
|
+
@center = Thaum::Text.new(content: "center-aligned", align: :center)
|
|
59
|
+
@right = Thaum::Text.new(content: "right-aligned", align: :right)
|
|
60
|
+
|
|
61
|
+
@bar_wrap = Bar.new(label: "wrap — :word (4-line box) vs :none (truncated)")
|
|
62
|
+
@wrapped = Thaum::Text.new(content: LONG, align: :left, wrap: :word)
|
|
63
|
+
@no_wrap = Thaum::Text.new(content: LONG, align: :left, wrap: :none)
|
|
64
|
+
|
|
65
|
+
@bar_semantic = Bar.new(label: "semantic color tokens", semantic_fg: :accent)
|
|
66
|
+
@success = SemanticText.new(label: "✓ Success", semantic_color: :success_fg)
|
|
67
|
+
@warning = SemanticText.new(label: "⚠ Warning", semantic_color: :warning_fg)
|
|
68
|
+
@error = SemanticText.new(label: "✗ Error", semantic_color: :error_fg)
|
|
69
|
+
@info = SemanticText.new(label: "ℹ Info", semantic_color: :info_fg)
|
|
70
|
+
@muted = SemanticText.new(label: "◻ Muted", semantic_color: :muted_fg)
|
|
71
|
+
@disabled = SemanticText.new(label: "⊘ Disabled", semantic_color: :disabled_fg)
|
|
72
|
+
|
|
73
|
+
@hint = Bar.new(label: "esc to quit")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def on_key(event)
|
|
77
|
+
quit if event.key == :escape
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def partition
|
|
81
|
+
vertical do
|
|
82
|
+
region(height: 1) { @bar_align }
|
|
83
|
+
region(height: 1) { @left }
|
|
84
|
+
region(height: 1) { @center }
|
|
85
|
+
region(height: 1) { @right }
|
|
86
|
+
region(height: 1) { @bar_wrap }
|
|
87
|
+
region(height: 4) { @wrapped }
|
|
88
|
+
region(height: 1) { @no_wrap }
|
|
89
|
+
region(height: 1) { @bar_semantic }
|
|
90
|
+
region(height: 1) { @success }
|
|
91
|
+
region(height: 1) { @warning }
|
|
92
|
+
region(height: 1) { @error }
|
|
93
|
+
region(height: 1) { @info }
|
|
94
|
+
region(height: 1) { @muted }
|
|
95
|
+
region(height: 1) { @disabled }
|
|
96
|
+
region(height: :fill) { @hint }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
Thaum.run(TextApp.new)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Usage: bundle exec ruby examples/theme_picker.rb
|
|
4
|
+
#
|
|
5
|
+
# The App's theme is whatever the list cursor is on. Thaum resolves
|
|
6
|
+
# app.theme fresh each frame, so a cursor move reflects instantly in
|
|
7
|
+
# the preview pane — no events, no context, no on_update.
|
|
8
|
+
|
|
9
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
10
|
+
require "thaum"
|
|
11
|
+
|
|
12
|
+
SLOTS = %i[
|
|
13
|
+
bg fg accent border
|
|
14
|
+
success_fg warning_fg error_fg info_fg
|
|
15
|
+
dim muted_fg disabled_fg
|
|
16
|
+
selection selection_fg pressed
|
|
17
|
+
input_bg bar_bg
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
class ThemeList
|
|
21
|
+
include Thaum::Sigil
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@themes = Thaum::Themes.names
|
|
25
|
+
@cursor = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def current = @themes.fetch(@cursor)
|
|
29
|
+
|
|
30
|
+
def on_key(event)
|
|
31
|
+
case event.key
|
|
32
|
+
when :up then move(-1)
|
|
33
|
+
when :down then move(1)
|
|
34
|
+
else emit(event)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def render(canvas:, theme:)
|
|
39
|
+
canvas.fill(bg: theme.bar_bg)
|
|
40
|
+
@themes.each_with_index do |item, i|
|
|
41
|
+
row = canvas.row(i) or break
|
|
42
|
+
|
|
43
|
+
selected = i == @cursor
|
|
44
|
+
bg = selected ? theme.selection : theme.bar_bg
|
|
45
|
+
fg = selected ? theme.accent : theme.fg
|
|
46
|
+
row.fill(bg: bg)
|
|
47
|
+
row.text(content: " #{item}", fg: fg, bg: bg)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def move(offset)
|
|
52
|
+
@cursor = (@cursor + offset) % @themes.size
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class PreviewSigil
|
|
57
|
+
include Thaum::Sigil
|
|
58
|
+
|
|
59
|
+
def focusable? = false
|
|
60
|
+
|
|
61
|
+
def render(canvas:, theme:)
|
|
62
|
+
canvas.fill(bg: theme.bg)
|
|
63
|
+
SLOTS.each_with_index do |slot, i|
|
|
64
|
+
row = canvas.row(i + 1) or break
|
|
65
|
+
|
|
66
|
+
hex = theme.send(slot)
|
|
67
|
+
row.fill(bg: hex, x: 2, width: 6)
|
|
68
|
+
row.text(content: "#{slot.to_s.ljust(14)} #{hex}", fg: theme.fg, x: 10)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class ThemePickerApp
|
|
74
|
+
include Thaum::App
|
|
75
|
+
|
|
76
|
+
def initialize
|
|
77
|
+
@list = ThemeList.new
|
|
78
|
+
@preview = PreviewSigil.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def theme = Thaum::Themes[@list.current]
|
|
82
|
+
|
|
83
|
+
def on_key(event)
|
|
84
|
+
quit if event.key == :escape
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def partition
|
|
88
|
+
horizontal do
|
|
89
|
+
region(width: 22) { @list }
|
|
90
|
+
region(width: :fill) { @preview }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Thaum.run(ThemePickerApp.new)
|