ratatui_ruby 1.4.0-x86_64-linux

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 (292) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +15 -0
  3. data/LICENSES/AGPL-3.0-or-later.txt +661 -0
  4. data/LICENSES/CC-BY-SA-4.0.txt +427 -0
  5. data/LICENSES/CC0-1.0.txt +121 -0
  6. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  7. data/LICENSES/MIT-0.txt +16 -0
  8. data/LICENSES/MIT.txt +21 -0
  9. data/REUSE.toml +42 -0
  10. data/exe/.gitkeep +0 -0
  11. data/ext/ratatui_ruby/.cargo/config.toml +13 -0
  12. data/ext/ratatui_ruby/.gitignore +4 -0
  13. data/ext/ratatui_ruby/Cargo.lock +1737 -0
  14. data/ext/ratatui_ruby/Cargo.toml +24 -0
  15. data/ext/ratatui_ruby/clippy.toml +7 -0
  16. data/ext/ratatui_ruby/extconf.rb +21 -0
  17. data/ext/ratatui_ruby/src/color.rs +82 -0
  18. data/ext/ratatui_ruby/src/errors.rs +28 -0
  19. data/ext/ratatui_ruby/src/events.rs +700 -0
  20. data/ext/ratatui_ruby/src/frame.rs +241 -0
  21. data/ext/ratatui_ruby/src/lib.rs +343 -0
  22. data/ext/ratatui_ruby/src/lib_header.rs +11 -0
  23. data/ext/ratatui_ruby/src/rendering.rs +158 -0
  24. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  25. data/ext/ratatui_ruby/src/style.rs +469 -0
  26. data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
  27. data/ext/ratatui_ruby/src/terminal/init.rs +233 -0
  28. data/ext/ratatui_ruby/src/terminal/mod.rs +42 -0
  29. data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
  30. data/ext/ratatui_ruby/src/terminal/queries.rs +231 -0
  31. data/ext/ratatui_ruby/src/terminal/query.rs +400 -0
  32. data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
  33. data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
  34. data/ext/ratatui_ruby/src/text.rs +225 -0
  35. data/ext/ratatui_ruby/src/widgets/barchart.rs +169 -0
  36. data/ext/ratatui_ruby/src/widgets/block.rs +41 -0
  37. data/ext/ratatui_ruby/src/widgets/calendar.rs +84 -0
  38. data/ext/ratatui_ruby/src/widgets/canvas.rs +183 -0
  39. data/ext/ratatui_ruby/src/widgets/center.rs +79 -0
  40. data/ext/ratatui_ruby/src/widgets/chart.rs +222 -0
  41. data/ext/ratatui_ruby/src/widgets/clear.rs +39 -0
  42. data/ext/ratatui_ruby/src/widgets/cursor.rs +32 -0
  43. data/ext/ratatui_ruby/src/widgets/gauge.rs +65 -0
  44. data/ext/ratatui_ruby/src/widgets/layout.rs +379 -0
  45. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +100 -0
  46. data/ext/ratatui_ruby/src/widgets/list.rs +378 -0
  47. data/ext/ratatui_ruby/src/widgets/list_state.rs +173 -0
  48. data/ext/ratatui_ruby/src/widgets/mod.rs +26 -0
  49. data/ext/ratatui_ruby/src/widgets/overlay.rs +24 -0
  50. data/ext/ratatui_ruby/src/widgets/paragraph.rs +87 -0
  51. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  52. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +55 -0
  53. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +214 -0
  54. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  55. data/ext/ratatui_ruby/src/widgets/sparkline.rs +127 -0
  56. data/ext/ratatui_ruby/src/widgets/table.rs +415 -0
  57. data/ext/ratatui_ruby/src/widgets/table_state.rs +203 -0
  58. data/ext/ratatui_ruby/src/widgets/tabs.rs +194 -0
  59. data/lib/ratatui_ruby/backend/window_size.rb +50 -0
  60. data/lib/ratatui_ruby/backend.rb +59 -0
  61. data/lib/ratatui_ruby/buffer/cell.rb +212 -0
  62. data/lib/ratatui_ruby/buffer.rb +149 -0
  63. data/lib/ratatui_ruby/cell.rb +208 -0
  64. data/lib/ratatui_ruby/debug.rb +215 -0
  65. data/lib/ratatui_ruby/draw.rb +63 -0
  66. data/lib/ratatui_ruby/event/focus_gained.rb +125 -0
  67. data/lib/ratatui_ruby/event/focus_lost.rb +127 -0
  68. data/lib/ratatui_ruby/event/key/character.rb +53 -0
  69. data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
  70. data/lib/ratatui_ruby/event/key/media.rb +46 -0
  71. data/lib/ratatui_ruby/event/key/modifier.rb +107 -0
  72. data/lib/ratatui_ruby/event/key/navigation.rb +72 -0
  73. data/lib/ratatui_ruby/event/key/system.rb +47 -0
  74. data/lib/ratatui_ruby/event/key.rb +479 -0
  75. data/lib/ratatui_ruby/event/mouse.rb +291 -0
  76. data/lib/ratatui_ruby/event/none.rb +53 -0
  77. data/lib/ratatui_ruby/event/paste.rb +130 -0
  78. data/lib/ratatui_ruby/event/resize.rb +221 -0
  79. data/lib/ratatui_ruby/event/sync.rb +52 -0
  80. data/lib/ratatui_ruby/event.rb +163 -0
  81. data/lib/ratatui_ruby/frame.rb +257 -0
  82. data/lib/ratatui_ruby/labs/a11y.rb +182 -0
  83. data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
  84. data/lib/ratatui_ruby/labs.rb +47 -0
  85. data/lib/ratatui_ruby/layout/alignment.rb +91 -0
  86. data/lib/ratatui_ruby/layout/constraint.rb +337 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +258 -0
  88. data/lib/ratatui_ruby/layout/position.rb +81 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +733 -0
  90. data/lib/ratatui_ruby/layout/size.rb +62 -0
  91. data/lib/ratatui_ruby/layout.rb +29 -0
  92. data/lib/ratatui_ruby/list_state.rb +201 -0
  93. data/lib/ratatui_ruby/output_guard.rb +171 -0
  94. data/lib/ratatui_ruby/ratatui_ruby.so +0 -0
  95. data/lib/ratatui_ruby/scrollbar_state.rb +122 -0
  96. data/lib/ratatui_ruby/style/color.rb +149 -0
  97. data/lib/ratatui_ruby/style/style.rb +147 -0
  98. data/lib/ratatui_ruby/style.rb +19 -0
  99. data/lib/ratatui_ruby/symbols.rb +435 -0
  100. data/lib/ratatui_ruby/synthetic_events.rb +106 -0
  101. data/lib/ratatui_ruby/table_state.rb +251 -0
  102. data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
  103. data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
  104. data/lib/ratatui_ruby/terminal.rb +66 -0
  105. data/lib/ratatui_ruby/terminal_lifecycle.rb +303 -0
  106. data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
  107. data/lib/ratatui_ruby/test_helper/event_injection.rb +241 -0
  108. data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
  109. data/lib/ratatui_ruby/test_helper/snapshot.rb +568 -0
  110. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
  111. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
  112. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
  113. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
  114. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
  115. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
  116. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
  117. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
  118. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
  119. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
  120. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
  121. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
  122. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
  123. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
  124. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
  125. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
  126. data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
  127. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
  128. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
  129. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
  130. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
  131. data/lib/ratatui_ruby/test_helper/style_assertions.rb +449 -0
  132. data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
  133. data/lib/ratatui_ruby/test_helper/terminal.rb +187 -0
  134. data/lib/ratatui_ruby/test_helper/test_doubles.rb +86 -0
  135. data/lib/ratatui_ruby/test_helper.rb +115 -0
  136. data/lib/ratatui_ruby/text/line.rb +245 -0
  137. data/lib/ratatui_ruby/text/span.rb +158 -0
  138. data/lib/ratatui_ruby/text.rb +99 -0
  139. data/lib/ratatui_ruby/tui/buffer_factories.rb +22 -0
  140. data/lib/ratatui_ruby/tui/canvas_factories.rb +149 -0
  141. data/lib/ratatui_ruby/tui/core.rb +67 -0
  142. data/lib/ratatui_ruby/tui/layout_factories.rb +153 -0
  143. data/lib/ratatui_ruby/tui/state_factories.rb +77 -0
  144. data/lib/ratatui_ruby/tui/style_factories.rb +22 -0
  145. data/lib/ratatui_ruby/tui/text_factories.rb +86 -0
  146. data/lib/ratatui_ruby/tui/widget_factories.rb +272 -0
  147. data/lib/ratatui_ruby/tui.rb +106 -0
  148. data/lib/ratatui_ruby/version.rb +12 -0
  149. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +51 -0
  150. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +29 -0
  151. data/lib/ratatui_ruby/widgets/bar_chart.rb +308 -0
  152. data/lib/ratatui_ruby/widgets/block.rb +266 -0
  153. data/lib/ratatui_ruby/widgets/calendar.rb +88 -0
  154. data/lib/ratatui_ruby/widgets/canvas.rb +297 -0
  155. data/lib/ratatui_ruby/widgets/cell.rb +59 -0
  156. data/lib/ratatui_ruby/widgets/center.rb +71 -0
  157. data/lib/ratatui_ruby/widgets/chart.rb +172 -0
  158. data/lib/ratatui_ruby/widgets/clear.rb +66 -0
  159. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  160. data/lib/ratatui_ruby/widgets/cursor.rb +54 -0
  161. data/lib/ratatui_ruby/widgets/gauge.rb +146 -0
  162. data/lib/ratatui_ruby/widgets/line_gauge.rb +158 -0
  163. data/lib/ratatui_ruby/widgets/list.rb +252 -0
  164. data/lib/ratatui_ruby/widgets/list_item.rb +55 -0
  165. data/lib/ratatui_ruby/widgets/overlay.rb +55 -0
  166. data/lib/ratatui_ruby/widgets/paragraph.rb +113 -0
  167. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +35 -0
  168. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +40 -0
  169. data/lib/ratatui_ruby/widgets/row.rb +123 -0
  170. data/lib/ratatui_ruby/widgets/scrollbar.rb +147 -0
  171. data/lib/ratatui_ruby/widgets/shape/label.rb +80 -0
  172. data/lib/ratatui_ruby/widgets/sparkline.rb +153 -0
  173. data/lib/ratatui_ruby/widgets/table.rb +213 -0
  174. data/lib/ratatui_ruby/widgets/tabs.rb +91 -0
  175. data/lib/ratatui_ruby/widgets.rb +43 -0
  176. data/lib/ratatui_ruby.rb +555 -0
  177. data/sig/examples/app_all_events/app.rbs +11 -0
  178. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  179. data/sig/examples/app_all_events/model/event_entry.rbs +23 -0
  180. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  181. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  182. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  183. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  184. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  185. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  186. data/sig/examples/app_all_events/view.rbs +14 -0
  187. data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
  188. data/sig/examples/app_color_picker/app.rbs +17 -0
  189. data/sig/examples/app_external_editor/app.rbs +12 -0
  190. data/sig/examples/app_login_form/app.rbs +11 -0
  191. data/sig/examples/app_stateful_interaction/app.rbs +39 -0
  192. data/sig/examples/verify_quickstart_dsl/app.rbs +17 -0
  193. data/sig/examples/verify_quickstart_lifecycle/app.rbs +17 -0
  194. data/sig/examples/verify_readme_usage/app.rbs +17 -0
  195. data/sig/examples/widget_block_demo/app.rbs +38 -0
  196. data/sig/examples/widget_box_demo/app.rbs +17 -0
  197. data/sig/examples/widget_calendar_demo/app.rbs +17 -0
  198. data/sig/examples/widget_cell_demo/app.rbs +17 -0
  199. data/sig/examples/widget_chart_demo/app.rbs +17 -0
  200. data/sig/examples/widget_gauge_demo/app.rbs +17 -0
  201. data/sig/examples/widget_layout_split/app.rbs +16 -0
  202. data/sig/examples/widget_line_gauge_demo/app.rbs +17 -0
  203. data/sig/examples/widget_list_demo/app.rbs +17 -0
  204. data/sig/examples/widget_map_demo/app.rbs +17 -0
  205. data/sig/examples/widget_popup_demo/app.rbs +17 -0
  206. data/sig/examples/widget_ratatui_logo_demo/app.rbs +17 -0
  207. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +17 -0
  208. data/sig/examples/widget_rect/app.rbs +18 -0
  209. data/sig/examples/widget_render/app.rbs +16 -0
  210. data/sig/examples/widget_rich_text/app.rbs +17 -0
  211. data/sig/examples/widget_scroll_text/app.rbs +17 -0
  212. data/sig/examples/widget_scrollbar_demo/app.rbs +17 -0
  213. data/sig/examples/widget_sparkline_demo/app.rbs +16 -0
  214. data/sig/examples/widget_style_colors/app.rbs +20 -0
  215. data/sig/examples/widget_table_demo/app.rbs +17 -0
  216. data/sig/examples/widget_text_width/app.rbs +16 -0
  217. data/sig/generated/event_key_predicates.rbs +1348 -0
  218. data/sig/manifest.yaml +5 -0
  219. data/sig/patches/data.rbs +26 -0
  220. data/sig/patches/debugger__.rbs +8 -0
  221. data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
  222. data/sig/ratatui_ruby/backend.rbs +12 -0
  223. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  224. data/sig/ratatui_ruby/buffer.rbs +18 -0
  225. data/sig/ratatui_ruby/cell.rbs +44 -0
  226. data/sig/ratatui_ruby/clear.rbs +18 -0
  227. data/sig/ratatui_ruby/constraint.rbs +26 -0
  228. data/sig/ratatui_ruby/debug.rbs +45 -0
  229. data/sig/ratatui_ruby/draw.rbs +30 -0
  230. data/sig/ratatui_ruby/event.rbs +249 -0
  231. data/sig/ratatui_ruby/frame.rbs +23 -0
  232. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  233. data/sig/ratatui_ruby/labs.rbs +90 -0
  234. data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
  235. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  236. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  237. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  238. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  239. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  240. data/sig/ratatui_ruby/list_state.rbs +23 -0
  241. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  242. data/sig/ratatui_ruby/ratatui_ruby.rbs +113 -0
  243. data/sig/ratatui_ruby/rect.rbs +17 -0
  244. data/sig/ratatui_ruby/scrollbar_state.rbs +24 -0
  245. data/sig/ratatui_ruby/session.rbs +93 -0
  246. data/sig/ratatui_ruby/style/color.rbs +22 -0
  247. data/sig/ratatui_ruby/style/style.rbs +29 -0
  248. data/sig/ratatui_ruby/symbols.rbs +141 -0
  249. data/sig/ratatui_ruby/synthetic_events.rbs +24 -0
  250. data/sig/ratatui_ruby/table_state.rbs +27 -0
  251. data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
  252. data/sig/ratatui_ruby/terminal/viewport.rbs +33 -0
  253. data/sig/ratatui_ruby/terminal_lifecycle.rbs +39 -0
  254. data/sig/ratatui_ruby/test_helper/event_injection.rbs +22 -0
  255. data/sig/ratatui_ruby/test_helper/snapshot.rbs +37 -0
  256. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +77 -0
  257. data/sig/ratatui_ruby/test_helper/terminal.rbs +20 -0
  258. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +32 -0
  259. data/sig/ratatui_ruby/test_helper.rbs +18 -0
  260. data/sig/ratatui_ruby/text/line.rbs +27 -0
  261. data/sig/ratatui_ruby/text/span.rbs +23 -0
  262. data/sig/ratatui_ruby/text.rbs +12 -0
  263. data/sig/ratatui_ruby/tui/buffer_factories.rbs +16 -0
  264. data/sig/ratatui_ruby/tui/canvas_factories.rbs +38 -0
  265. data/sig/ratatui_ruby/tui/core.rbs +23 -0
  266. data/sig/ratatui_ruby/tui/layout_factories.rbs +39 -0
  267. data/sig/ratatui_ruby/tui/state_factories.rbs +23 -0
  268. data/sig/ratatui_ruby/tui/style_factories.rbs +18 -0
  269. data/sig/ratatui_ruby/tui/text_factories.rbs +23 -0
  270. data/sig/ratatui_ruby/tui/widget_factories.rbs +138 -0
  271. data/sig/ratatui_ruby/tui.rbs +25 -0
  272. data/sig/ratatui_ruby/version.rbs +12 -0
  273. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  274. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  275. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  276. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  277. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  278. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  279. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  280. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  281. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  282. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  283. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  284. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  285. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  286. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  287. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  288. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  289. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  290. data/sig/ratatui_ruby/widgets.rbs +16 -0
  291. data/vendor/goodcop/base.yml +1047 -0
  292. metadata +729 -0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ class Event
10
+ # Synthetic event for synchronizing async operations in tests.
11
+ #
12
+ # Testing async behavior is tricky. You inject an event, but results arrive
13
+ # later. By the time you assert, the async work may not have completed.
14
+ #
15
+ # When a runtime (Tea, Kit) encounters this event, it should wait for all
16
+ # pending async operations to complete before processing the next event.
17
+ # This enables deterministic testing without changing production code paths.
18
+ #
19
+ # Inject this event between user actions and assertions to ensure async
20
+ # results have been processed:
21
+ #
22
+ # === Example
23
+ #
24
+ #--
25
+ # SPDX-SnippetBegin
26
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
27
+ # SPDX-License-Identifier: MIT-0
28
+ #++
29
+ # inject_key("s") # Triggers async command
30
+ # inject_sync # Wait for command to complete
31
+ # inject_key(:q) # Quit after seeing results
32
+ # Tea.run(...)
33
+ # assert_snapshots("after_s_with_results")
34
+ #
35
+ #--
36
+ # SPDX-SnippetEnd
37
+ #++
38
+ # This is not "test mode"—it's a real event that runtimes handle.
39
+ # Production apps could use it too (e.g., "ensure saves complete before quit").
40
+ class Sync < Event
41
+ # Returns true for Sync events.
42
+ def sync?
43
+ true
44
+ end
45
+
46
+ # Deconstructs the event for pattern matching.
47
+ def deconstruct_keys(keys)
48
+ { type: :sync }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ # Base class for all RatatuiRuby events.
10
+ #
11
+ # Events represent terminal input: keyboard, mouse, resize, paste, focus changes.
12
+ # Returned by RatatuiRuby.poll_event. All events support Ruby 3.0+ pattern matching.
13
+ #
14
+ # == Event Types
15
+ #
16
+ # * <tt>Key</tt> — keyboard input
17
+ # * <tt>Mouse</tt> — mouse clicks, movement, wheel
18
+ # * <tt>Resize</tt> — terminal resized
19
+ # * <tt>Paste</tt> — clipboard paste
20
+ # * <tt>FocusGained</tt> — terminal gained focus
21
+ # * <tt>FocusLost</tt> — terminal lost focus
22
+ # * <tt>None</tt> — no event available (Null Object)
23
+ #
24
+ # == Pattern Matching (Exhaustive)
25
+ #
26
+ # Use <tt>case...in</tt> to dispatch on every possible event type. This ensures
27
+ # you handle every case without needing an +else+ clause:
28
+ #
29
+ #--
30
+ # SPDX-SnippetBegin
31
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
32
+ # SPDX-License-Identifier: MIT-0
33
+ #++
34
+ # case RatatuiRuby.poll_event
35
+ # in { type: :key, code: "q" }
36
+ # break
37
+ # in { type: :key, code: code, modifiers: }
38
+ # handle_key(code, modifiers)
39
+ # in { type: :mouse, kind: "down", x:, y: }
40
+ # handle_click(x, y)
41
+ # in { type: :mouse, kind:, x:, y: }
42
+ # # handle other mouse activities
43
+ # in { type: :resize, width:, height: }
44
+ # handle_resize(width, height)
45
+ # in { type: :paste, content: }
46
+ # handle_paste(content)
47
+ # in { type: :focus_gained }
48
+ # handle_focus_gain
49
+ # in { type: :focus_lost }
50
+ # handle_focus_loss
51
+ # in { type: :none }
52
+ # # Idle
53
+ # end
54
+ #
55
+ #--
56
+ # SPDX-SnippetEnd
57
+ #++
58
+ # == Predicates
59
+ #
60
+ # Check event types with predicates without pattern matching:
61
+ #
62
+ #--
63
+ # SPDX-SnippetBegin
64
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
65
+ # SPDX-License-Identifier: MIT-0
66
+ #++
67
+ # event = RatatuiRuby.poll_event
68
+ # if event.key?
69
+ # puts "Key pressed"
70
+ # elsif event.none?
71
+ # # Idle
72
+ # elsif event.mouse?
73
+ # puts "Mouse event"
74
+ # end
75
+ #--
76
+ # SPDX-SnippetEnd
77
+ #++
78
+ class Event
79
+ # Returns true if this is a None event.
80
+ def none?
81
+ false
82
+ end
83
+
84
+ # Returns true if this is a Key event.
85
+ def key?
86
+ false
87
+ end
88
+
89
+ # Returns true if this is a Mouse event.
90
+ def mouse?
91
+ false
92
+ end
93
+
94
+ # Returns true if this is a Resize event.
95
+ def resize?
96
+ false
97
+ end
98
+
99
+ # Returns true if this is a Paste event.
100
+ def paste?
101
+ false
102
+ end
103
+
104
+ # Returns true if this is a FocusGained event.
105
+ def focus_gained?
106
+ false
107
+ end
108
+
109
+ # Returns true if this is a FocusLost event.
110
+ def focus_lost?
111
+ false
112
+ end
113
+
114
+ # Returns true if this is a Sync event.
115
+ def sync?
116
+ false
117
+ end
118
+
119
+ # Responds to dynamic predicate methods for key checks.
120
+ # All non-Key events return false for any key predicate.
121
+ def method_missing(name, *args, **kwargs, &block)
122
+ if name.to_s.end_with?("?")
123
+ false
124
+ else
125
+ super
126
+ end
127
+ end
128
+
129
+ # Declares that this class responds to dynamic predicate methods.
130
+ def respond_to_missing?(name, *args)
131
+ name.to_s.end_with?("?") || super
132
+ end
133
+
134
+ # Deconstructs the event for pattern matching.
135
+ #
136
+ # Keys argument is unused but required by the protocol.
137
+ #
138
+ #--
139
+ # SPDX-SnippetBegin
140
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
141
+ # SPDX-License-Identifier: MIT-0
142
+ #++
143
+ # case event
144
+ # in type: :key, code:
145
+ # puts "Key: #{code}"
146
+ # end
147
+ #--
148
+ # SPDX-SnippetEnd
149
+ #++
150
+ def deconstruct_keys(keys)
151
+ {}
152
+ end
153
+ end
154
+ end
155
+
156
+ require_relative "event/none"
157
+ require_relative "event/key"
158
+ require_relative "event/mouse"
159
+ require_relative "event/resize"
160
+ require_relative "event/paste"
161
+ require_relative "event/focus_gained"
162
+ require_relative "event/focus_lost"
163
+ require_relative "event/sync"
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ # Provides access to the terminal buffer for rendering widgets.
10
+ #
11
+ # Rendering in immediate-mode TUIs requires knowing the terminal dimensions and
12
+ # placing widgets at specific positions. Without explicit control, layout
13
+ # calculations become duplicated between rendering and hit testing.
14
+ #
15
+ # This class exposes the terminal frame during a draw call. It provides the
16
+ # current area and methods to render widgets at precise locations.
17
+ #
18
+ # Use it inside a <tt>RatatuiRuby.draw</tt> block to render widgets with full
19
+ # control over placement.
20
+ #
21
+ # == Thread/Ractor Safety
22
+ #
23
+ # Frame is an *I/O handle*, not a data object. It has side effects
24
+ # (render_widget, set_cursor_position) and is intentionally *not*
25
+ # Ractor-shareable. Passing it to helper methods during the draw block is
26
+ # fine. However, do not include it in immutable Models/Messages or pass
27
+ # it to other Ractors. Frame is only valid during the draw block's execution.
28
+ #
29
+ # === Examples
30
+ #
31
+ # Basic usage with a single widget:
32
+ #
33
+ #--
34
+ # SPDX-SnippetBegin
35
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
36
+ # SPDX-License-Identifier: MIT-0
37
+ #++
38
+ # RatatuiRuby.draw do |frame|
39
+ # paragraph = RatatuiRuby::Widgets::Paragraph.new(text: "Hello, world!")
40
+ # frame.render_widget(paragraph, frame.area)
41
+ # end
42
+ #
43
+ #--
44
+ # SPDX-SnippetEnd
45
+ #++
46
+ # Using Layout.split for multi-region layouts:
47
+ #
48
+ #--
49
+ # SPDX-SnippetBegin
50
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
51
+ # SPDX-License-Identifier: MIT-0
52
+ #++
53
+ # RatatuiRuby.draw do |frame|
54
+ # sidebar, main = RatatuiRuby::Layout.split(
55
+ # frame.area,
56
+ # direction: :horizontal,
57
+ # constraints: [
58
+ # RatatuiRuby::Layout::Constraint.length(20),
59
+ # RatatuiRuby::Layout::Constraint.fill(1)
60
+ # ]
61
+ # )
62
+ #
63
+ # frame.render_widget(sidebar_widget, sidebar)
64
+ # frame.render_widget(main_widget, main)
65
+ #
66
+ # # Store rects for hit testing — no duplication!
67
+ # @regions = { sidebar: sidebar, main: main }
68
+ # end
69
+ #--
70
+ # SPDX-SnippetEnd
71
+ #++
72
+ class Frame
73
+ ##
74
+ # :method: area
75
+ # :call-seq: area() -> Rect
76
+ #
77
+ # Returns the full terminal area as a Rect.
78
+ #
79
+ # The returned Rect represents the entire drawable area of the terminal.
80
+ # Use it as the starting point for layout calculations.
81
+ #
82
+ # === Example
83
+ #
84
+ #--
85
+ # SPDX-SnippetBegin
86
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
87
+ # SPDX-License-Identifier: MIT-0
88
+ #++
89
+ # RatatuiRuby.draw do |frame|
90
+ # puts "Terminal size: #{frame.area.width}x#{frame.area.height}"
91
+ # end
92
+ #
93
+ #--
94
+ # SPDX-SnippetEnd
95
+ #++
96
+ # (Native method implemented in Rust)
97
+
98
+ ##
99
+ # :method: render_widget
100
+ # :call-seq: render_widget(widget, area) -> nil
101
+ #
102
+ # Renders a widget at the specified area.
103
+ #
104
+ # Widgets in RatatuiRuby are immutable Data objects. This method takes a
105
+ # widget and a Rect, rendering the widget's content within that region.
106
+ #
107
+ # [widget]
108
+ # The widget to render (Paragraph, Layout, List, Table, etc.).
109
+ # [area]
110
+ #--
111
+ # SPDX-SnippetBegin
112
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
113
+ # SPDX-License-Identifier: MIT-0
114
+ #++
115
+ # A Rect specifying where to render the widget.
116
+ #
117
+ #--
118
+ # SPDX-SnippetEnd
119
+ #++
120
+ # === Example
121
+ #
122
+ #--
123
+ # SPDX-SnippetBegin
124
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
125
+ # SPDX-License-Identifier: MIT-0
126
+ #++
127
+ # RatatuiRuby.draw do |frame|
128
+ # para = RatatuiRuby::Widgets::Paragraph.new(text: "Content")
129
+ # frame.render_widget(para, frame.area)
130
+ # end
131
+ #
132
+ #--
133
+ # SPDX-SnippetEnd
134
+ #++
135
+ # (Native method implemented in Rust)
136
+
137
+ ##
138
+ # :method: render_stateful_widget
139
+ # :call-seq: render_stateful_widget(widget, area, state) -> nil
140
+ #
141
+ # Renders a widget with persistent state.
142
+ #
143
+ # Some UI components (like List or Table) have **runtime status** (Status) that
144
+ # changes during rendering, such as the current scroll offset.
145
+ #
146
+ # Since Widget definitions (Configuration Definition) are immutable inputs,
147
+ # you must pass a separate mutable State object (Output Status) to capture
148
+ # these changes.
149
+ #
150
+ # Note: The Widget configuration is *always* required. The State object is
151
+ # only used for specific widgets that need to persist runtime status.
152
+ #
153
+ #
154
+ # [widget]
155
+ # The immutable widget configuration (Input) (e.g., RatatuiRuby::List).
156
+ # [area]
157
+ # The Rect area to render into.
158
+ # [state]
159
+ #--
160
+ # SPDX-SnippetBegin
161
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
162
+ # SPDX-License-Identifier: MIT-0
163
+ #++
164
+ # The mutable state object (Output) (e.g., RatatuiRuby::ListState).
165
+ #
166
+ #--
167
+ # SPDX-SnippetEnd
168
+ #++
169
+ # === Example
170
+ #
171
+ #--
172
+ # SPDX-SnippetBegin
173
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
174
+ # SPDX-License-Identifier: MIT-0
175
+ #++
176
+ # # Initialize state once (outside the loop)
177
+ # @list_state = RatatuiRuby::ListState.new
178
+ #
179
+ # RatatuiRuby.draw do |frame|
180
+ # list = RatatuiRuby::Widgets::List.new(items: ["A", "B"])
181
+ # frame.render_stateful_widget(list, frame.area, @list_state)
182
+ # end
183
+ #
184
+ # # Read back the offset calculated by Ratatui
185
+ # puts @list_state.offset
186
+ #
187
+ #--
188
+ # SPDX-SnippetEnd
189
+ #++
190
+ # (Native method implemented in Rust)
191
+
192
+ ##
193
+ # :method: set_cursor_position
194
+ # :call-seq: set_cursor_position(x, y) -> nil
195
+ #
196
+ # Positions the blinking cursor at the given coordinates.
197
+ #
198
+ # Text input fields show users where typed characters will appear. Without
199
+ # a visible cursor, users cannot tell if the input is focused or where text
200
+ # will insert.
201
+ #
202
+ # This method moves the terminal cursor to a specific cell. Coordinates are
203
+ # 0-indexed from the terminal's top-left corner.
204
+ #
205
+ # Use it when building login forms, search bars, or command palettes.
206
+ #
207
+ # [x]
208
+ # Column position (<tt>0</tt> = leftmost column).
209
+ # [y]
210
+ #--
211
+ # SPDX-SnippetBegin
212
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
213
+ # SPDX-License-Identifier: MIT-0
214
+ #++
215
+ # Row position (<tt>0</tt> = topmost row).
216
+ #
217
+ #--
218
+ # SPDX-SnippetEnd
219
+ #++
220
+ # === Example
221
+ #
222
+ # Position the cursor at the end of typed text in a login form:
223
+ #
224
+ #--
225
+ # SPDX-SnippetBegin
226
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
227
+ # SPDX-License-Identifier: MIT-0
228
+ #++
229
+ # PREFIX = "Username: [ "
230
+ # username = "alice"
231
+ #
232
+ # RatatuiRuby.draw do |frame|
233
+ # # Render the input field
234
+ # prompt = RatatuiRuby::Widgets::Paragraph.new(
235
+ # text: "#{PREFIX}#{username} ]",
236
+ # block: RatatuiRuby::Widgets::Block.new(borders: :all)
237
+ # )
238
+ # frame.render_widget(prompt, frame.area)
239
+ #
240
+ # # Position cursor after the typed text
241
+ # # Account for border (1) + prefix length + username length
242
+ # cursor_x = 1 + PREFIX.length + username.length
243
+ # cursor_y = 1 # First line inside border
244
+ # frame.set_cursor_position(cursor_x, cursor_y)
245
+ # end
246
+ #
247
+ #--
248
+ # SPDX-SnippetEnd
249
+ #++
250
+ # See also:
251
+ # - {Component-based implementation using Frame API}[link:/examples/app_color_picker/app_rb.html]
252
+ # - {Declarative implementation using Tree API}[link:/examples/app_login_form/app_rb.html]
253
+ # - RatatuiRuby::Cursor (Tree API alternative)
254
+ #
255
+ # (Native method implemented in Rust)
256
+ end
257
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ require "tmpdir"
9
+
10
+ module RatatuiRuby
11
+ module Labs
12
+ # A11Y lab: exports widget tree as XML.
13
+ #
14
+ # Writes an XML representation of the widget tree to a temporary file
15
+ # every frame when enabled.
16
+ module A11y
17
+ # Path to the XML output file in the system temp directory.
18
+ OUTPUT_PATH = File.join(Dir.tmpdir, "ratatui_ruby_a11y.xml").freeze
19
+
20
+ class << self
21
+ # Dumps the widget tree to XML (single widget for tree mode).
22
+ def dump_widget_tree(widget, _area = nil)
23
+ ensure_rexml_loaded
24
+ doc = REXML::Document.new
25
+ doc.add(REXML::XMLDecl.new("1.0", "UTF-8"))
26
+ doc.add(build_element(widget))
27
+ write_document(doc)
28
+ end
29
+
30
+ # Returns startup message for users to see before TUI launches.
31
+ #
32
+ # Since stdout is captured during TUI rendering, users need to know
33
+ # where the XML file will be written before the app starts.
34
+ def startup_message
35
+ <<~MSG
36
+ A11Y Lab enabled! Widget tree will be written to:
37
+ #{OUTPUT_PATH}
38
+
39
+ Press Enter to launch the TUI...
40
+ MSG
41
+ end
42
+
43
+ # Dumps multiple widgets captured from Frame API mode.
44
+ def dump_widgets(widgets_with_areas)
45
+ ensure_rexml_loaded
46
+ Labs.warn_once!("Labs::A11y (RR_LABS=A11Y)")
47
+
48
+ # Reset counter each frame for stable IDs
49
+ @widget_id_counter = 0
50
+
51
+ doc = REXML::Document.new
52
+ doc.add(REXML::XMLDecl.new("1.0", "UTF-8"))
53
+
54
+ frame = REXML::Element.new("RatatuiFrame")
55
+ widgets_with_areas.each do |widget, area|
56
+ frame.add(build_element_with_area(widget, area))
57
+ end
58
+ doc.add(frame)
59
+
60
+ write_document(doc)
61
+ end
62
+
63
+ private def write_document(doc)
64
+ output = +""
65
+ formatter = REXML::Formatters::Pretty.new(2)
66
+ formatter.compact = true
67
+ formatter.write(doc, output)
68
+ File.write(OUTPUT_PATH, output)
69
+ end
70
+
71
+ private def build_element_with_area(widget, area)
72
+ class_name = widget.class.name&.split("::")&.last || "Unknown"
73
+ element = REXML::Element.new(class_name)
74
+
75
+ # Generate unique id for this widget
76
+ @widget_id_counter ||= 0
77
+ @widget_id_counter += 1
78
+ widget_id = "w#{@widget_id_counter}"
79
+ element.add_attribute("id", widget_id)
80
+
81
+ # Add area attributes
82
+ element.add_attribute("x", area.x.to_s)
83
+ element.add_attribute("y", area.y.to_s)
84
+ element.add_attribute("width", area.width.to_s)
85
+ element.add_attribute("height", area.height.to_s)
86
+
87
+ add_members(element, widget, parent_id: widget_id)
88
+ element
89
+ end
90
+
91
+ private def build_element(node)
92
+ class_name = node.class.name&.split("::")&.last || "Unknown"
93
+ element = REXML::Element.new(class_name)
94
+
95
+ if node.respond_to?(:to_h) && node.respond_to?(:members)
96
+ add_members(element, node)
97
+ else
98
+ element.text = node.to_s
99
+ end
100
+
101
+ element
102
+ end
103
+
104
+ private def add_members(element, node, parent_id: nil)
105
+ return unless node.respond_to?(:to_h) && node.respond_to?(:members)
106
+
107
+ node.to_h.each do |key, value|
108
+ # Skip nil and empty values entirely (no noise in output)
109
+ next if value.nil?
110
+ next if value.respond_to?(:empty?) && value.empty?
111
+
112
+ # Skip objects where all members are nil/empty (like default Style)
113
+ if value.respond_to?(:to_h) && value.respond_to?(:members)
114
+ attrs = value.to_h.compact
115
+ next if attrs.empty? || attrs.values.all? { |v| v.respond_to?(:empty?) && v.empty? }
116
+ end
117
+
118
+ # Scalar values → XML attributes
119
+ # Exception: 'text' and 'content' accept Text (multi-line capable)
120
+ # Complex values → XML child elements
121
+ multiline_keys = %w[text content]
122
+ if scalar?(value) && !multiline_keys.include?(key.to_s)
123
+ element.add_attribute(key.to_s, value.to_s)
124
+ else
125
+ # Special handling for 'block' wrapper - add ARIA role and id/for
126
+ is_block_wrapper = key.to_s == "block" && parent_id
127
+ child = build_child_element(key, value, is_wrapper: is_block_wrapper, parent_id:)
128
+ element.add(child) if child
129
+ end
130
+ end
131
+ end
132
+
133
+ private def scalar?(value)
134
+ case value
135
+ when String, Symbol, Numeric, TrueClass, FalseClass
136
+ true
137
+ else
138
+ false
139
+ end
140
+ end
141
+
142
+ private def build_child_element(key, value, is_wrapper: false, parent_id: nil)
143
+ element = REXML::Element.new(key.to_s)
144
+
145
+ # Add ARIA role and id/for association for block wrappers
146
+ if is_wrapper && parent_id
147
+ element.add_attribute("role", "group")
148
+ element.add_attribute("for", parent_id)
149
+ end
150
+
151
+ case value
152
+ when Array
153
+ value.each { |item| element.add(build_element(item)) }
154
+ when Hash
155
+ # Plain Hash: serialize keys as attributes
156
+ value.each do |k, v|
157
+ next if v.nil?
158
+ next if v.respond_to?(:empty?) && v.empty?
159
+ element.add_attribute(k.to_s, v.to_s)
160
+ end
161
+ else
162
+ if value.respond_to?(:to_h) && value.respond_to?(:members)
163
+ add_members(element, value)
164
+ else
165
+ element.text = value.to_s
166
+ end
167
+ end
168
+
169
+ element
170
+ end
171
+
172
+ # Lazily loads REXML when first needed.
173
+ private def ensure_rexml_loaded
174
+ return if defined?(@rexml_loaded) && @rexml_loaded
175
+
176
+ require "rexml/document"
177
+ @rexml_loaded = true
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ class Frame
10
+ # A11Y Lab Integration
11
+ #
12
+ # When the A11Y lab is enabled, we capture widgets as they are rendered
13
+ # and write the tree to XML when flush_a11y_capture is called.
14
+ module A11yCapture
15
+ # Intercepts render_widget to capture widgets for A11Y export.
16
+ # @param widget [widget] The widget being rendered
17
+ # @param area [Layout::Rect] The area to render into
18
+ def render_widget(widget, area)
19
+ if Labs.enabled?(:a11y)
20
+ widgets = (@a11y_widgets ||= []) #: Array[[(_CustomWidget | widget), Layout::Rect]]
21
+ widgets << [widget, area]
22
+ end
23
+ super
24
+ end
25
+
26
+ # Intercepts render_stateful_widget to capture widgets for A11Y export.
27
+ # @param widget [widget] The widget being rendered
28
+ # @param area [Layout::Rect] The area to render into
29
+ # @param state [Object] The widget state
30
+ def render_stateful_widget(widget, area, state)
31
+ if Labs.enabled?(:a11y)
32
+ widgets = (@a11y_widgets ||= []) #: Array[[(_CustomWidget | widget), Layout::Rect]]
33
+ widgets << [widget, area]
34
+ end
35
+ super
36
+ end
37
+
38
+ # Called at end of draw block to flush captured widgets
39
+ def flush_a11y_capture
40
+ widgets = @a11y_widgets
41
+ return unless Labs.enabled?(:a11y) && widgets&.any?
42
+
43
+ Labs::A11y.dump_widgets(widgets)
44
+ @a11y_widgets = nil
45
+ end
46
+ end
47
+
48
+ prepend A11yCapture
49
+ end
50
+ end