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,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)