ratatui_ruby 0.5.0 → 0.6.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 (234) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +6 -0
  7. data/CHANGELOG.md +44 -7
  8. data/README.md +11 -4
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +84 -10
  11. data/doc/application_testing.md +75 -29
  12. data/doc/contributors/design/ruby_frontend.md +39 -3
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +129 -44
  15. data/doc/contributors/examples_audit/p1_high.md +21 -0
  16. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  17. data/doc/contributors/examples_audit.md +41 -0
  18. data/doc/event_handling.md +11 -3
  19. data/doc/images/app_all_events.png +0 -0
  20. data/doc/images/app_color_picker.png +0 -0
  21. data/doc/images/app_login_form.png +0 -0
  22. data/doc/images/app_stateful_interaction.png +0 -0
  23. data/doc/images/verify_quickstart_dsl.png +0 -0
  24. data/doc/images/verify_quickstart_layout.png +0 -0
  25. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  26. data/doc/images/verify_readme_usage.png +0 -0
  27. data/doc/images/widget_barchart_demo.png +0 -0
  28. data/doc/images/widget_block_demo.png +0 -0
  29. data/doc/images/widget_canvas_demo.png +0 -0
  30. data/doc/images/widget_cell_demo.png +0 -0
  31. data/doc/images/widget_center_demo.png +0 -0
  32. data/doc/images/widget_chart_demo.png +0 -0
  33. data/doc/images/widget_list_demo.png +0 -0
  34. data/doc/images/widget_overlay_demo.png +0 -0
  35. data/doc/images/widget_render.png +0 -0
  36. data/doc/images/widget_rich_text.png +0 -0
  37. data/doc/images/widget_scroll_text.png +0 -0
  38. data/doc/images/widget_sparkline_demo.png +0 -0
  39. data/doc/images/widget_table_demo.png +0 -0
  40. data/doc/images/widget_tabs_demo.png +0 -0
  41. data/doc/images/widget_text_width.png +0 -0
  42. data/doc/quickstart.md +69 -76
  43. data/doc/terminal_limitations.md +92 -0
  44. data/examples/app_all_events/README.md +45 -27
  45. data/examples/app_all_events/app.rb +38 -35
  46. data/examples/app_all_events/model/app_model.rb +157 -0
  47. data/examples/app_all_events/model/event_entry.rb +17 -0
  48. data/examples/app_all_events/model/msg.rb +37 -0
  49. data/examples/app_all_events/update.rb +73 -0
  50. data/examples/app_all_events/view/app_view.rb +8 -8
  51. data/examples/app_all_events/view/controls_view.rb +8 -6
  52. data/examples/app_all_events/view/counts_view.rb +12 -8
  53. data/examples/app_all_events/view/live_view.rb +8 -7
  54. data/examples/app_all_events/view/log_view.rb +10 -15
  55. data/examples/app_color_picker/README.md +84 -44
  56. data/examples/app_color_picker/app.rb +24 -62
  57. data/examples/app_color_picker/controls.rb +90 -0
  58. data/examples/app_color_picker/copy_dialog.rb +45 -49
  59. data/examples/app_color_picker/export_pane.rb +126 -0
  60. data/examples/app_color_picker/input.rb +99 -67
  61. data/examples/app_color_picker/main_container.rb +178 -0
  62. data/examples/app_color_picker/palette.rb +55 -26
  63. data/examples/app_login_form/README.md +47 -0
  64. data/examples/app_login_form/app.rb +2 -3
  65. data/examples/app_stateful_interaction/README.md +31 -0
  66. data/examples/app_stateful_interaction/app.rb +272 -0
  67. data/examples/timeout_demo.rb +43 -0
  68. data/examples/verify_quickstart_dsl/README.md +48 -0
  69. data/examples/verify_quickstart_dsl/app.rb +2 -0
  70. data/examples/verify_quickstart_layout/README.md +71 -0
  71. data/examples/verify_quickstart_layout/app.rb +2 -0
  72. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  73. data/examples/verify_quickstart_lifecycle/app.rb +8 -2
  74. data/examples/verify_readme_usage/README.md +43 -0
  75. data/examples/verify_readme_usage/app.rb +8 -2
  76. data/examples/widget_barchart_demo/README.md +49 -0
  77. data/examples/widget_barchart_demo/app.rb +5 -5
  78. data/examples/widget_block_demo/README.md +34 -0
  79. data/examples/widget_block_demo/app.rb +256 -0
  80. data/examples/widget_box_demo/README.md +45 -0
  81. data/examples/widget_calendar_demo/README.md +39 -0
  82. data/examples/widget_canvas_demo/README.md +27 -0
  83. data/examples/widget_canvas_demo/app.rb +123 -0
  84. data/examples/widget_cell_demo/README.md +36 -0
  85. data/examples/widget_cell_demo/app.rb +31 -24
  86. data/examples/widget_center_demo/README.md +29 -0
  87. data/examples/widget_center_demo/app.rb +116 -0
  88. data/examples/widget_chart_demo/README.md +41 -0
  89. data/examples/widget_chart_demo/app.rb +7 -2
  90. data/examples/widget_gauge_demo/README.md +41 -0
  91. data/examples/widget_layout_split/README.md +44 -0
  92. data/examples/widget_line_gauge_demo/README.md +41 -0
  93. data/examples/widget_list_demo/README.md +49 -0
  94. data/examples/widget_list_demo/app.rb +91 -107
  95. data/examples/widget_map_demo/README.md +39 -0
  96. data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
  97. data/examples/widget_overlay_demo/app.rb +248 -0
  98. data/examples/widget_popup_demo/README.md +36 -0
  99. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  100. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  101. data/examples/widget_rect/README.md +38 -0
  102. data/examples/widget_render/README.md +37 -0
  103. data/examples/widget_rich_text/README.md +35 -0
  104. data/examples/widget_rich_text/app.rb +62 -33
  105. data/examples/widget_scroll_text/README.md +37 -0
  106. data/examples/widget_scroll_text/app.rb +0 -1
  107. data/examples/widget_scrollbar_demo/README.md +37 -0
  108. data/examples/widget_sparkline_demo/README.md +42 -0
  109. data/examples/widget_sparkline_demo/app.rb +4 -3
  110. data/examples/widget_style_colors/README.md +34 -0
  111. data/examples/widget_table_demo/README.md +48 -0
  112. data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
  113. data/examples/widget_tabs_demo/README.md +41 -0
  114. data/examples/widget_tabs_demo/app.rb +15 -1
  115. data/examples/widget_text_width/README.md +35 -0
  116. data/examples/widget_text_width/app.rb +106 -0
  117. data/exe/.gitkeep +0 -0
  118. data/ext/ratatui_ruby/Cargo.lock +11 -4
  119. data/ext/ratatui_ruby/Cargo.toml +2 -1
  120. data/ext/ratatui_ruby/src/events.rs +238 -26
  121. data/ext/ratatui_ruby/src/frame.rs +113 -1
  122. data/ext/ratatui_ruby/src/lib.rs +34 -4
  123. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  124. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  125. data/ext/ratatui_ruby/src/text.rs +1 -1
  126. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  127. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  128. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  129. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  130. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  131. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  132. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  133. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  134. data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
  135. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  136. data/lib/ratatui_ruby/cell.rb +4 -4
  137. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  138. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  139. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  140. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  141. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  142. data/lib/ratatui_ruby/event/key.rb +111 -51
  143. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  144. data/lib/ratatui_ruby/event/paste.rb +1 -1
  145. data/lib/ratatui_ruby/frame.rb +96 -0
  146. data/lib/ratatui_ruby/list_state.rb +88 -0
  147. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  148. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  149. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  150. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  151. data/lib/ratatui_ruby/schema/list.rb +25 -4
  152. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  153. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  154. data/lib/ratatui_ruby/schema/style.rb +24 -4
  155. data/lib/ratatui_ruby/schema/table.rb +21 -3
  156. data/lib/ratatui_ruby/schema/text.rb +69 -1
  157. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  158. data/lib/ratatui_ruby/session/autodoc.rb +65 -0
  159. data/lib/ratatui_ruby/session.rb +22 -7
  160. data/lib/ratatui_ruby/table_state.rb +90 -0
  161. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  162. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  163. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  164. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  165. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  166. data/lib/ratatui_ruby/test_helper.rb +65 -358
  167. data/lib/ratatui_ruby/version.rb +1 -1
  168. data/lib/ratatui_ruby.rb +42 -19
  169. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  170. data/sig/examples/widget_block_demo/app.rbs +32 -0
  171. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  172. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  173. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  174. data/sig/ratatui_ruby/event.rbs +11 -1
  175. data/sig/ratatui_ruby/frame.rbs +2 -0
  176. data/sig/ratatui_ruby/list_state.rbs +13 -0
  177. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  178. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  179. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  180. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  181. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  182. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  183. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  184. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  185. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  186. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  187. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  188. data/sig/ratatui_ruby/session.rbs +13 -0
  189. data/sig/ratatui_ruby/table_state.rbs +15 -0
  190. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  191. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  192. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  193. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  194. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  195. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  196. data/tasks/autodoc/examples.rb +79 -0
  197. data/tasks/autodoc/inventory.rb +9 -7
  198. data/tasks/autodoc.rake +11 -5
  199. data/tasks/bump/changelog.rb +3 -3
  200. data/tasks/bump/links.rb +67 -0
  201. data/tasks/sourcehut.rake +61 -21
  202. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  203. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  204. metadata +111 -37
  205. data/doc/images/app_table_select.png +0 -0
  206. data/doc/images/widget_block_padding.png +0 -0
  207. data/doc/images/widget_block_titles.png +0 -0
  208. data/doc/images/widget_list_styles.png +0 -0
  209. data/examples/app_all_events/model/events.rb +0 -180
  210. data/examples/app_all_events/model/highlight.rb +0 -57
  211. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  212. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  213. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  214. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  215. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  216. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  217. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  218. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  219. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  220. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  221. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  222. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  223. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  224. data/examples/app_all_events/view_state.rb +0 -42
  225. data/examples/app_color_picker/scene.rb +0 -201
  226. data/examples/widget_block_padding/app.rb +0 -67
  227. data/examples/widget_block_titles/app.rb +0 -69
  228. data/examples/widget_list_styles/app.rb +0 -141
  229. data/examples/widget_table_flex/app.rb +0 -95
  230. data/sig/examples/widget_block_padding/app.rbs +0 -11
  231. data/sig/examples/widget_block_titles/app.rbs +0 -11
  232. data/sig/examples/widget_list_styles/app.rbs +0 -11
  233. data/tasks/bump/comparison_links.rb +0 -41
  234. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+ require "faker"
9
+
10
+ # A "Master Class" example demonstrating Stateful Widget Rendering and Interaction.
11
+ #
12
+ # This example shows how to:
13
+ # 1. Use mutable State objects (ListState, TableState) for selection and scrolling
14
+ # 2. Read back the calculated scroll offset from the backend (state.offset)
15
+ # 3. Implement precise mouse-click-to-row interaction using that offset
16
+ class AppStatefulInteraction
17
+ def initialize
18
+ # Data Models
19
+ # Tables are the categories on the left
20
+ @tables = ["Users", "Orders", "Products", "Invoices", "Audit Logs"]
21
+ @headers = {
22
+ "Users" => ["Name", "Email", "Role"],
23
+ "Orders" => ["Order ID", "Status", "Amount"],
24
+ "Products" => ["Product", "SKU", "Status"],
25
+ "Invoices" => ["Invoice #", "Status", "Amount"],
26
+ "Audit Logs" => ["Event", "Action", "IP Address"],
27
+ }
28
+
29
+ # Generate dummy data for each table
30
+ # Use fixed seed for deterministic behavior in CI/Tests
31
+ if ENV["CI"] == "true" || ENV["RATA_SEED"]
32
+ seed = (ENV["RATA_SEED"] || 12345).to_i
33
+ Faker::Config.random = Random.new(seed)
34
+ # Also seed Kernel.rand/Array#sample just in case
35
+ srand(seed)
36
+ end
37
+ rand_price = -> { "$#{Faker::Commerce.price(range: 10..500.0)}" }
38
+
39
+ @data = {
40
+ "Users" => Array.new(50) { [Faker::Name.name, Faker::Internet.email, %w[Admin Editor Viewer].sample] },
41
+ "Orders" => Array.new(50) { [Faker::Commerce.promotion_code(digits: 4), ["Completed", "Pending", "Failed"].sample, rand_price.call] },
42
+ "Products" => Array.new(50) { [Faker::Commerce.product_name, "SKU-#{Faker::Number.number(digits: 4)}", ["In Stock", "Low Stock"].sample] },
43
+ "Invoices" => Array.new(50) { ["INV-#{Faker::Number.number(digits: 6)}", ["Paid", "Unpaid"].sample, rand_price.call] },
44
+ "Audit Logs" => Array.new(50) { ["Log #{Faker::Number.unique.number(digits: 3)}", ["Login Success", "Login Failed", "Logout"].sample, Faker::Internet.ip_v4_address] },
45
+ }
46
+
47
+ # State Objects - These are mutable and persist across frames!
48
+ @list_state = RatatuiRuby::ListState.new(nil)
49
+ @table_state = RatatuiRuby::TableState.new(nil)
50
+
51
+ # Initialize selection
52
+ @list_state.select(0)
53
+ @table_state.select(0)
54
+
55
+ # Active Pane Focus (:list or :table)
56
+ @active_pane = :list
57
+ end
58
+
59
+ def run
60
+ RatatuiRuby.run do |tui|
61
+ @tui = tui
62
+
63
+ # Styles can only be created once TUI is initialized
64
+ @style_active = @tui.style(fg: :yellow, modifiers: [:bold])
65
+ @style_inactive = @tui.style(fg: :dark_gray)
66
+ @style_highlight = @tui.style(bg: :blue, fg: :white, modifiers: [:bold])
67
+
68
+ loop do
69
+ render
70
+ break if handle_input == :quit
71
+ end
72
+ end
73
+ end
74
+
75
+ private def render
76
+ @tui.draw do |frame|
77
+ # 1. Layout
78
+ main_area, help_area = @tui.layout_split(
79
+ frame.area,
80
+ direction: :vertical,
81
+ constraints: [
82
+ @tui.constraint_fill(1),
83
+ @tui.constraint_length(1),
84
+ ]
85
+ )
86
+
87
+ list_area, table_area = @tui.layout_split(
88
+ main_area,
89
+ direction: :horizontal,
90
+ constraints: [
91
+ @tui.constraint_percentage(30),
92
+ @tui.constraint_percentage(70),
93
+ ]
94
+ )
95
+
96
+ # Save areas for hit testing
97
+ @list_area = list_area
98
+ @table_area = table_area
99
+
100
+ # 2. Render List (Left Pane)
101
+ render_list(frame, list_area)
102
+
103
+ # 3. Render Table (Right Pane)
104
+ render_table(frame, table_area)
105
+
106
+ # 4. Render Help
107
+ help_text = "q: Quit | Tab/Arrows: Nav | Mouse: Click rows (Try it!)"
108
+ frame.render_widget(@tui.paragraph(text: help_text), help_area)
109
+ end
110
+ end
111
+
112
+ private def render_list(frame, area)
113
+ is_active = @active_pane == :list
114
+
115
+ # Render main list
116
+ list = @tui.list(
117
+ items: @tables,
118
+ block: @tui.block(
119
+ title: " Tables ",
120
+ borders: [:all],
121
+ border_style: is_active ? @style_active : @style_inactive
122
+ ),
123
+ highlight_style: @style_highlight
124
+ )
125
+ # KEY STEP: Pass the state object!
126
+ frame.render_stateful_widget(list, area, @list_state)
127
+
128
+ # Render Scrollbar
129
+ scrollbar = @tui.scrollbar(
130
+ content_length: 0,
131
+ position: 0,
132
+ orientation: :vertical_right,
133
+ track_symbol: nil,
134
+ thumb_symbol: "▐"
135
+ )
136
+ scrollbar_state = RatatuiRuby::ScrollbarState.new(@tables.size)
137
+ scrollbar_state.position = @list_state.offset
138
+ scrollbar_state.viewport_content_length = area.height - 2
139
+
140
+ frame.render_stateful_widget(scrollbar, area, scrollbar_state)
141
+ end
142
+
143
+ private def render_table(frame, area)
144
+ is_active = @active_pane == :table
145
+
146
+ # Get current data based on list selection
147
+ current_table = @tables[@list_state.selected || 0]
148
+ rows = @data[current_table]
149
+
150
+ # Render table
151
+ table = @tui.table(
152
+ rows:,
153
+ header: @headers[current_table],
154
+ widths: [
155
+ @tui.constraint_percentage(30),
156
+ @tui.constraint_percentage(40),
157
+ @tui.constraint_percentage(30),
158
+ ],
159
+ block: @tui.block(
160
+ title: " #{current_table} Data ",
161
+ borders: [:all],
162
+ border_style: is_active ? @style_active : @style_inactive
163
+ ),
164
+ highlight_style: @style_highlight
165
+ )
166
+
167
+ frame.render_stateful_widget(table, area, @table_state)
168
+
169
+ # Render Scrollbar
170
+ scrollbar = @tui.scrollbar(
171
+ content_length: 0,
172
+ position: 0,
173
+ orientation: :vertical_right,
174
+ track_symbol: nil,
175
+ thumb_symbol: "▐"
176
+ )
177
+ scrollbar_state = RatatuiRuby::ScrollbarState.new(rows.size)
178
+ scrollbar_state.position = @table_state.offset
179
+ scrollbar_state.viewport_content_length = area.height - 4 # borders + header + margin
180
+
181
+ frame.render_stateful_widget(scrollbar, area, scrollbar_state)
182
+ end
183
+
184
+ private def handle_input
185
+ case @tui.poll_event
186
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
187
+ :quit
188
+
189
+ # Navigation
190
+ in { type: :key, code: "tab" } | { type: :key, code: "right" } | { type: :key, code: "left" }
191
+ @active_pane = (@active_pane == :list) ? :table : :list
192
+
193
+ in { type: :key, code: "down" }
194
+ scroll_active(1)
195
+
196
+ in { type: :key, code: "up" }
197
+ scroll_active(-1)
198
+
199
+ # Mouse Interaction
200
+ in { type: :mouse, kind: "down", x:, y: }
201
+ handle_click(x, y)
202
+
203
+ else
204
+ # no-op
205
+ end
206
+ end
207
+
208
+ private def scroll_active(delta)
209
+ if @active_pane == :list
210
+ i = @list_state.selected || 0
211
+ new_i = (i + delta).clamp(0, @tables.size - 1)
212
+ @list_state.select(new_i)
213
+ # Reset table selection when switching categories
214
+ if i != new_i
215
+ @table_state.select(0)
216
+ @table_state.select_column(nil) # Ensure clean slate
217
+ end
218
+ else
219
+ current_rows = @data[@tables[@list_state.selected || 0]].size
220
+ i = @table_state.selected || 0
221
+ new_i = (i + delta).clamp(0, current_rows - 1)
222
+ @table_state.select(new_i)
223
+ end
224
+ end
225
+
226
+ private def handle_click(x, y)
227
+ if @list_area.contains?(x, y)
228
+ handle_list_click(y)
229
+ elsif @table_area.contains?(x, y)
230
+ handle_table_click(y)
231
+ end
232
+ end
233
+
234
+ private def handle_list_click(mouse_y)
235
+ @active_pane = :list
236
+
237
+ # CRITICAL: Read back the offset!
238
+ # Formula: clicked_index = (mouse_y - list_top - border_width) + offset
239
+ offset = @list_state.offset
240
+ list_top = @list_area.y
241
+ border_width = 1 # Top border
242
+
243
+ clicked_row = (mouse_y - list_top - border_width) + offset
244
+
245
+ if clicked_row >= 0 && clicked_row < @tables.size
246
+ @list_state.select(clicked_row)
247
+ @table_state.select(0) # Reset table when category changes
248
+ end
249
+ end
250
+
251
+ private def handle_table_click(mouse_y)
252
+ @active_pane = :table
253
+
254
+ # CRITICAL: Read back the offset!
255
+ # Formula: clicked_index = (mouse_y - table_top - border - header_height - margin) + offset
256
+ offset = @table_state.offset
257
+ table_top = @table_area.y
258
+ border_width = 1
259
+ header_height = 1
260
+ # No header_margin without Row margin
261
+ effective_top = table_top + border_width + header_height
262
+
263
+ clicked_row = (mouse_y - effective_top) + offset
264
+
265
+ current_table_data = @data[@tables[@list_state.selected || 0]]
266
+ if clicked_row >= 0 && clicked_row < current_table_data.size
267
+ @table_state.select(clicked_row)
268
+ end
269
+ end
270
+ end
271
+
272
+ AppStatefulInteraction.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # Timeout Demo: Non-Blocking Event Polling
7
+ #
8
+ # This demo shows how to use poll_event with a timeout for game loops
9
+ # and animation systems that need to update at a fixed frame rate
10
+ # regardless of user input.
11
+ #
12
+ # Run: bundle exec ruby examples/timeout_demo.rb
13
+ #
14
+ # Expected behavior:
15
+ # - "Tick..." prints every 100ms continuously
16
+ # - Pressing a key prints "Key Pressed: [key]" immediately
17
+ # - Press 'q' to quit
18
+
19
+ require "bundler/setup"
20
+ require "ratatui_ruby"
21
+
22
+ puts "Timeout Demo - Press 'q' to quit"
23
+ puts "Watch: continuous ticks with responsive key handling"
24
+ puts
25
+
26
+ tick_count = 0
27
+ running = true
28
+
29
+ while running
30
+ # Poll with 100ms timeout (~10 FPS tick rate)
31
+ event = RatatuiRuby.poll_event(timeout: 0.1)
32
+
33
+ if event.none?
34
+ # No input, just tick
35
+ tick_count += 1
36
+ puts "Tick #{tick_count}..."
37
+ elsif event.key?
38
+ puts "Key Pressed: #{event.code}"
39
+ running = false if event.code == "q"
40
+ end
41
+ end
42
+
43
+ puts "\nGoodbye!"
@@ -0,0 +1,48 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Quickstart DSL Verification
7
+
8
+ Verifies the "Idiomatic Session" tutorial in the [Quickstart](../../doc/quickstart.md#idiomatic-session).
9
+
10
+ This example exists as a documentation regression test. It ensures the recommended DSL and session-based workflow remains functional.
11
+
12
+ ## Usage
13
+
14
+ <!-- SYNC:START:app.rb:main -->
15
+ ```ruby
16
+ RatatuiRuby.run do |tui|
17
+ loop do
18
+ # 2. Create your UI with methods instead of classes.
19
+ view = tui.paragraph(
20
+ text: "Hello, Ratatui! Press 'q' to quit.",
21
+ alignment: :center,
22
+ block: tui.block(
23
+ title: "My Ruby TUI App",
24
+ title_alignment: :center,
25
+ borders: [:all],
26
+ border_color: "cyan",
27
+ style: { fg: "white" }
28
+ )
29
+ )
30
+
31
+ # 3. Use RatatuiRuby methods, too.
32
+ tui.draw do |frame|
33
+ frame.render_widget(view, frame.area)
34
+ end
35
+
36
+ # 4. Poll for events with pattern matching
37
+ case tui.poll_event
38
+ in { type: :key, code: "q" }
39
+ break
40
+ else
41
+ # Ignore other events
42
+ end
43
+ end
44
+ end
45
+ ```
46
+ <!-- SYNC:END -->
47
+
48
+ ![verify_quickstart_dsl](../../doc/images/verify_quickstart_dsl.png)
@@ -10,6 +10,7 @@ require "ratatui_ruby"
10
10
  class VerifyQuickstartDsl
11
11
  def run
12
12
  # 1. Initialize the terminal, start the run loop, and ensure the terminal is restored.
13
+ # [SYNC:START:main]
13
14
  RatatuiRuby.run do |tui|
14
15
  loop do
15
16
  # 2. Create your UI with methods instead of classes.
@@ -39,6 +40,7 @@ class VerifyQuickstartDsl
39
40
  end
40
41
  end
41
42
  end
43
+ # [SYNC:END:main]
42
44
  end
43
45
  end
44
46
 
@@ -0,0 +1,71 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Quickstart Layout Verification
7
+
8
+ Verifies the "Adding Layouts" tutorial in the [Quickstart](../../doc/quickstart.md#adding-layouts).
9
+
10
+ This example exists as a documentation regression test. It ensures the layout and constraints examples remain functional.
11
+
12
+ ## Usage
13
+
14
+ <!-- SYNC:START:app.rb:main -->
15
+ ```ruby
16
+ loop do
17
+ tui.draw do |frame|
18
+ # 1. Split the screen
19
+ top, bottom = tui.layout_split(
20
+ frame.area,
21
+ direction: :vertical,
22
+ constraints: [
23
+ tui.constraint_percentage(75),
24
+ tui.constraint_percentage(25),
25
+ ]
26
+ )
27
+
28
+ # 2. Render Top Widget
29
+ frame.render_widget(
30
+ tui.paragraph(
31
+ text: "Hello, Ratatui!",
32
+ alignment: :center,
33
+ block: tui.block(title: "Content", borders: [:all], border_color: "cyan")
34
+ ),
35
+ top
36
+ )
37
+
38
+ # 3. Render Bottom Widget with Styled Text
39
+ # We use a Line of Spans to style specific characters
40
+ text_line = tui.text_line(
41
+ spans: [
42
+ tui.text_span(content: "Press '"),
43
+ tui.text_span(
44
+ content: "q",
45
+ style: tui.style(modifiers: [:bold, :underlined])
46
+ ),
47
+ tui.text_span(content: "' to quit."),
48
+ ],
49
+ alignment: :center
50
+ )
51
+
52
+ frame.render_widget(
53
+ tui.paragraph(
54
+ text: text_line,
55
+ block: tui.block(title: "Controls", borders: [:all])
56
+ ),
57
+ bottom
58
+ )
59
+ end
60
+
61
+ case tui.poll_event
62
+ in { type: :key, code: "q" }
63
+ break
64
+ else
65
+ # Ignore other events
66
+ end
67
+ end
68
+ ```
69
+ <!-- SYNC:END -->
70
+
71
+ ![verify_quickstart_layout](../../doc/images/verify_quickstart_layout.png)
@@ -10,6 +10,7 @@ require "ratatui_ruby"
10
10
  class VerifyQuickstartLayout
11
11
  def run
12
12
  RatatuiRuby.run do |tui|
13
+ # [SYNC:START:main]
13
14
  loop do
14
15
  tui.draw do |frame|
15
16
  # 1. Split the screen
@@ -62,6 +63,7 @@ class VerifyQuickstartLayout
62
63
  # Ignore other events
63
64
  end
64
65
  end
66
+ # [SYNC:END:main]
65
67
  end
66
68
  end
67
69
  end
@@ -0,0 +1,56 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Quickstart Lifecycle Verification
7
+
8
+ Verifies the "Basic Application" tutorial in the [Quickstart](../../doc/quickstart.md#basic-application).
9
+
10
+ This example exists as a documentation regression test. It ensures the core lifecycle example presented to new users remains functional.
11
+
12
+ ## Usage
13
+
14
+ <!-- SYNC:START:app.rb:main -->
15
+ ```ruby
16
+ # 1. Initialize the terminal
17
+ RatatuiRuby.init_terminal
18
+
19
+ begin
20
+ # The Main Loop
21
+ loop do
22
+ # 2. Create your UI (Immediate Mode)
23
+ # We define a Paragraph widget inside a Block with a title and borders.
24
+ view = RatatuiRuby::Paragraph.new(
25
+ text: "Hello, Ratatui! Press 'q' to quit.",
26
+ alignment: :center,
27
+ block: RatatuiRuby::Block.new(
28
+ title: "My Ruby TUI App",
29
+ title_alignment: :center,
30
+ borders: [:all],
31
+ border_color: "cyan",
32
+ style: { fg: "white" }
33
+ )
34
+ )
35
+
36
+ # 3. Draw the UI
37
+ RatatuiRuby.draw do |frame|
38
+ frame.render_widget(view, frame.area)
39
+ end
40
+
41
+ # 4. Poll for events
42
+ case RatatuiRuby.poll_event
43
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
44
+ break
45
+ else
46
+ nil
47
+ end
48
+ end
49
+ ensure
50
+ # 5. Restore the terminal to its original state
51
+ RatatuiRuby.restore_terminal
52
+ end
53
+ ```
54
+ <!-- SYNC:END -->
55
+
56
+ ![verify_quickstart_lifecycle](../../doc/images/verify_quickstart_lifecycle.png)
@@ -9,6 +9,7 @@ require "ratatui_ruby"
9
9
 
10
10
  class VerifyQuickstartLifecycle
11
11
  def run
12
+ # [SYNC:START:main]
12
13
  # 1. Initialize the terminal
13
14
  RatatuiRuby.init_terminal
14
15
 
@@ -35,13 +36,18 @@ class VerifyQuickstartLifecycle
35
36
  end
36
37
 
37
38
  # 4. Poll for events
38
- event = RatatuiRuby.poll_event
39
- break if event.key? && event.code == "q"
39
+ case RatatuiRuby.poll_event
40
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
41
+ break
42
+ else
43
+ nil
44
+ end
40
45
  end
41
46
  ensure
42
47
  # 5. Restore the terminal to its original state
43
48
  RatatuiRuby.restore_terminal
44
49
  end
50
+ # [SYNC:END:main]
45
51
  end
46
52
  end
47
53
 
@@ -0,0 +1,43 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # README Usage Verification
7
+
8
+ Verifies the primary usage example in the project [README](../../README.md#usage).
9
+
10
+ This example exists as a documentation regression test. It ensures that the very first code snippet a user sees actually works.
11
+
12
+ ## Usage
13
+
14
+ <!-- SYNC:START:app.rb:main -->
15
+ ```ruby
16
+ RatatuiRuby.run do |tui|
17
+ loop do
18
+ tui.draw do |frame|
19
+ frame.render_widget(
20
+ tui.paragraph(
21
+ text: "Hello, Ratatui! Press 'q' to quit.",
22
+ alignment: :center,
23
+ block: tui.block(
24
+ title: "My Ruby TUI App",
25
+ borders: [:all],
26
+ border_color: "cyan"
27
+ )
28
+ ),
29
+ frame.area
30
+ )
31
+ end
32
+ case tui.poll_event
33
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
34
+ break
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ ```
41
+ <!-- SYNC:END -->
42
+
43
+ ![verify_readme_usage](../../doc/images/verify_readme_usage.png)
@@ -8,6 +8,7 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
8
8
  require "ratatui_ruby"
9
9
  class VerifyReadmeUsage
10
10
  def run
11
+ # [SYNC:START:main]
11
12
  RatatuiRuby.run do |tui|
12
13
  loop do
13
14
  tui.draw do |frame|
@@ -24,10 +25,15 @@ class VerifyReadmeUsage
24
25
  frame.area
25
26
  )
26
27
  end
27
- event = tui.poll_event
28
- break if event == "q" || event == :ctrl_c
28
+ case tui.poll_event
29
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
30
+ break
31
+ else
32
+ nil
33
+ end
29
34
  end
30
35
  end
36
+ # [SYNC:END:main]
31
37
  end
32
38
  end
33
39
 
@@ -0,0 +1,49 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # BarChart Widget Example
7
+
8
+ Visualizes categorical data with interactive attribute cycling.
9
+
10
+ Comparing magnitudes in raw tables requires mental arithmetic. Bar charts make these comparisons instant and intuitive.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **Data Formats**: Supports simple Hashes, Arrays with individual styles, and Groups (stacked/grouped bars).
15
+ - **Orientation**: Switch between Vertical and Horizontal layouts.
16
+ - **Customization**:
17
+ - Adjustable bar widths and gaps.
18
+ - Custom characters for bars (ASCII art support).
19
+ - Detailed styling for labels and values.
20
+ - **Mini Mode**: Compact rendering for dashboard widgets.
21
+
22
+ ## Hotkeys
23
+
24
+ - **d**: Cycle Data Source (`data`)
25
+ - **v**: Toggle Direction (`direction`)
26
+ - **w**: Adjust Bar Width (`bar_width`)
27
+ - **a**: Adjust Bar Gap (`bar_gap`)
28
+ - **g**: Adjust Group Gap (`group_gap`)
29
+ - **b**: Cycle Bar Character Set (`bar_set`)
30
+ - **s**: Cycle Chart Style (`style`)
31
+ - **x**: Cycle Label Style (`label_style`)
32
+ - **z**: Cycle Value Style (`value_style`)
33
+ - **m**: Toggle Mini Mode (Compact View)
34
+ - **q**: Quit
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ ruby examples/widget_barchart_demo/app.rb
40
+ ```
41
+
42
+ ## Learning Outcomes
43
+
44
+ Use this example if you need to...
45
+ - Visualize categorical data (e.g., sales by quarter, CPU usage by core).
46
+ - Create "stats" dashboards with compact visualizations.
47
+ - Understand how `RatatuiRuby::BarChart` handles different data structures.
48
+
49
+ ![Demo](/doc/images/widget_barchart_demo.png)
@@ -23,11 +23,11 @@ require "ratatui_ruby"
23
23
  # rdoc-image:/doc/images/widget_barchart_demo.png
24
24
  class WidgetBarchartDemo
25
25
  def initialize
26
- @data_index = 0
26
+ @data_index = 2
27
27
  @styles = nil # Initialized in run
28
- @style_index = 0
29
- @label_style_index = 0
30
- @value_style_index = 0
28
+ @style_index = 3
29
+ @label_style_index = 3
30
+ @value_style_index = 3
31
31
  @bar_sets = [
32
32
  { name: "Default", set: nil },
33
33
  { name: "Numbers (Short)", set: { 8 => "8", 7 => "7", 6 => "6", 5 => "5", 4 => "4", 3 => "3", 2 => "2", 1 => "1", 0 => "0" } },
@@ -38,7 +38,7 @@ class WidgetBarchartDemo
38
38
  @direction = :vertical
39
39
  @bar_width = 8
40
40
  @bar_gap = 1
41
- @group_gap = 0
41
+ @group_gap = 2
42
42
  @height_mode = :full
43
43
  @hotkey_style = nil
44
44
  end