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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 706f0da7ece6637bedb71da9ce2c3bd2b94c357f121fcfd305caa468a2e74637
4
- data.tar.gz: 8f0e05bc53177c92a972f7badd9db952e3eb5856413b6b16bc7e41d106ba2652
3
+ metadata.gz: c8e3aed93d74250af44d1b7169dcf7d31575b8b6a24c65d40b4067edb1cf46ea
4
+ data.tar.gz: e5f3d70e418bc9af98a1926838030aa934b13f64d617ed5c5a1809e91d17d72a
5
5
  SHA512:
6
- metadata.gz: abea59dfb0c71cff7779eee2af7766796a75d1e2499eac3cce40cf901f78ababa453caa1812c70ef44acb88c25a5bb02c9f4e8983d338b79c3b38946f624fd80
7
- data.tar.gz: eb01c6a74f469487e782372a86911ae23b7841df1bd42b0820e5e7a9577d891ce3084ae1fb31b7cf3c35d5898e89c37d5fdb8d581b7fabedf52254fc21627be8
6
+ metadata.gz: c1742d63643a1ebef6063097e3755e819da39354b631d70ba72ead21d48d8a9199a5421184698fb41a56ee0991a7411adfbcf125f6ec9e18aa980eeb6d4e03fe
7
+ data.tar.gz: 995f166470814f6ec2a0e073bc658eb52b5a5381a5c19539d4b6ef575e601eb3ac8c3b000a23b4c226b6708a5387024b71a84c43cd69a5240f538aca261a815b
data/.builds/ruby-3.2.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.5.0.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.6.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.3.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.5.0.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.6.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.4.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.5.0.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.6.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.5.0.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.6.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/AGENTS.md CHANGED
@@ -44,6 +44,12 @@ Architecture:
44
44
  - **Usage:**
45
45
  - Runs default task (compile + test + lint): `bin/agent_rake`
46
46
  - Runs specific task: `bin/agent_rake test:ruby` (for example)
47
+ - **Snapshot Testing:** When tests fail due to intentional behavior changes (not bugs), update snapshots:
48
+ - **Command:** `UPDATE_SNAPSHOTS=1 bin/agent_rake test:ruby`
49
+ - This regenerates all `snapshot/*{txt,ansi}` files to match current output
50
+ - **When to use:** After modifying widget rendering, changing default values, or updating UI behavior
51
+ - **When NOT to use:** For actual test failures that indicate bugs in your code
52
+ - After updating, verify the changes make sense by reviewing the diff of `.snapshot` files
47
53
  - **Setup:** `bin/setup` must handle both Bundler and Cargo dependencies.
48
54
  - **Git:** ALWAYS set `PAGER=cat` with `git`, `git`, etc.. **THIS IS CRITICAL!**
49
55
  - **Rake:** Our rake tasks use `git ls-files`, so errors happen when you move or delete files. In this case, ask the user to stage changes for you.
data/CHANGELOG.md CHANGED
@@ -18,6 +18,42 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
18
18
 
19
19
  ### Removed
20
20
 
21
+ ## [0.6.0] - 2026-01-03
22
+
23
+ ### Added
24
+
25
+ - **Rich Text Support**: `List`, `Gauge`, `LineGauge`, and `BarChart` widgets now accept rich text objects (`Text::Span`, `Text::Line`) in addition to plain strings. This enables per-character styling, multi-colored labels, and complex text formatting matching Ratatui 0.30.0 capabilities.
26
+ - **ListItem Wrapper**: New `ListItem` data class allows applying row-level styling (background color) independent of text content. `List` items can now be `String`, `Text::Span`, `Text::Line`, or `ListItem` objects.
27
+ - **Non-Blocking Event Polling**: `RatatuiRuby.poll_event` now accepts an optional `timeout:` parameter (Float seconds). Use `timeout: 0.0` for non-blocking checks, or `timeout: 0.1` for fixed timesteps. Defaults to `0.016` (16ms) to preserve existing behavior.
28
+ - **Cursor Positioning**: `Frame#set_cursor_position(x, y)` sets the terminal's hardware cursor position. Using this method is essential for input fields where the user expects visual feedback on their cursor location.
29
+ - **Text Measurement**: `RatatuiRuby::Text.width(string)` calculates the display width of a string in terminal cells, correctly handling unicode including ASCII (1 cell), CJK full-width characters (2 cells), emoji (typically 2 cells), and zero-width combining marks (0 cells). This is essential for auto-sizing widgets and responsive layouts. Delegates to the same unicode-width logic that Ratatui uses internally.
30
+ - **Scroll Offset Control**: `List` and `Table` widgets now accept an optional `offset` parameter to control the viewport's scroll position. Use this for passive scrolling (viewing without selection) or calculating click-to-item mappings. When combined with a selection, Ratatui's natural scrolling may still adjust the viewport to keep the selection visible; set selection to `nil` for fully manual scroll control.
31
+ - **Rect Geometry Helpers**: `Rect#intersects?(other)` tests whether two rectangles overlap. `Rect#intersection(other)` returns the overlapping area as a new `Rect`, or `nil` if disjoint. Essential for viewport clipping and hit testing in component architectures.
32
+ - **Stateful Rendering**: `Frame#render_stateful_widget(widget, area, state)` renders widgets with mutable state objects (`ListState`, `TableState`, `ScrollbarState`). State objects persist across frames, enabling scroll offset read-back and selection tracking. Essential for mouse click-to-row hit testing. **Precedence rule:** State object properties override widget properties (`selected_index`, `offset`).
33
+ - **Full Keyboard Support**: Key events now recognize all keys supported by crossterm: function keys (`f1`–`f24`), navigation (`home`, `end`, `page_up`, `page_down`, `insert`, `delete`), locks (`caps_lock`, `scroll_lock`, `num_lock`), system (`print_screen`, `pause`, `menu`), media controls (`play`, `play_pause`, `track_next`, etc.), and individual modifier keys (`left_shift`, `right_control`, etc.). Previously unmapped keys returned `"unknown"`; they now return proper `snake_case` strings.
34
+ - **Key Categories**: `Event::Key` now has a `kind` attribute (`:standard`, `:function`, `:media`, `:modifier`, `:system`) for logical grouping. Category predicates (`media?`, `system?`, `function?`, `modifier?`, `standard?`) enable clean event routing without string parsing. The `unmodified?` method is an alias for `standard?`.
35
+ - **Smart Predicates (DWIM)**: Key predicates now "Do What I Mean" for media keys. `pause?` returns `true` for both system `pause` and `media_pause` keys. For strict matching, use `media_pause?` or compare `event.code` directly. This reduces boilerplate when responding to conceptual actions regardless of input method.
36
+ - **Modifier Key Predicates**: New methods `super?`, `hyper?`, and `meta?` check for these modifier keys. Platform aliases are provided for `super?`: `command?`/`cmd?` (macOS), `win?` (Windows), and `tux?` (Linux). These work for both modifier flags AND individual modifier key events (e.g., `left_super`). Additionally, `control?` aliases `ctrl?` and `option?` aliases `alt?`.
37
+ - **Navigation Aliases**: Convenient predicate aliases for common keys: `return?` for Enter, `back?` for Backspace, `del?` for Delete, `ins?` for Insert, `escape?` for Esc, `pgup?`/`pageup?` for Page Up, `pgdn?`/`pagedown?` for Page Down. The special `reverse_tab?` predicate matches both the `back_tab` key and `shift+tab` combinations.
38
+ - **Indexed Color Support**: `Style` now supports `Integer` values for `fg` and `bg`, allowing use of the Xterm 256-color palette (0-255). This includes standard ANSI colors (0-15), the 6x6x6 color cube (16-231), and the grayscale ramp (232-255).
39
+ - **Rich Snapshots**: `RatatuiRuby::TestHelper#assert_rich_snapshot` validates both content and styling by comparing against stored ANSI snapshots. This allows for visual regression testing that respects colors, bold, italics, and other terminal modifiers.
40
+ - **Semantic Style Assertions**: New testing helpers `assert_color(expected, x:, y:)`, `assert_cell_style(x, y, **style)`, and `assert_area_style(area, **style)` allow precise verification of terminal cell attributes without full-screen snapshots. Punchy convenience aliases like `assert_fg`/`assert_bg`, `assert_bold`, `assert_italic`, `assert_underlined`, and color-specific assertions (e.g., `assert_red`, `assert_bg_blue`) provide a more natural API for common testing patterns.
41
+ - **Buffer Debugging**: `RatatuiRuby::TestHelper#print_buffer` outputs the current terminal state to STDOUT with full ANSI color support, making it easier to debug rendering issues during test execution.
42
+
43
+
44
+ ### Changed
45
+
46
+ - **Frozen Data Objects (Breaking)**: Events returned by `RatatuiRuby.poll_event` and `Cell` objects from `RatatuiRuby.get_cell_at` are now deeply frozen for Ractor compatibility. Code that mutates these objects (e.g., `event.modifiers << "custom"`) must copy the data before modifying. `Rect` was already frozen. Note: `Frame` and `Session` are *I/O handles* with side effects and remain intentionally non-shareable.
47
+ - **Semantic Exceptions (Breaking)**: Replaced generic `RuntimeError` with `RatatuiRuby::Error::Terminal` for backend/terminal failures and `RatatuiRuby::Error::Safety` for API contract violations (like using `Frame` outside `draw`). This allows finer-grained error handling but breaks code explicitly rescuing `RuntimeError`. `ArgumentError` works as before.
48
+ - **Media Key Codes (Breaking)**: All media key codes now use a consistent `media_` prefix: `play` → `media_play`, `stop` → `media_stop`, `play_pause` → `media_play_pause`, etc. Code comparing against literal media key strings must be updated. Use the Smart Predicates (`play?`, `stop?`) for backward-compatible behavior.
49
+ - **`Key#char` Return Value (Breaking)**: `char` now returns `nil` for non-printable keys (previously returned `""`). Code relying on `event.char.empty?` must change to `event.char.nil?` or use `event.text?` instead.
50
+
51
+ ### Fixed
52
+
53
+ - **Frame Safety**: Calling methods on a `Frame` stored outside of a `draw` block now correctly raises a `RatatuiRuby::Error::Safety` (subclass of `RatatuiRuby::Error`) instead instead of causing undefined behavior or crashes. This ensures memory safety by preventing use-after-free scenarios with the underlying Rust frame.
54
+
55
+ ### Removed
56
+
21
57
  ## [0.5.0] - 2026-01-01
22
58
 
23
59
  ### Added
@@ -285,10 +321,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
285
321
  - **Input Handling**: Robust handling for both Keyboard and Mouse events.
286
322
  - **Testing Support**: Included `RatatuiRuby::TestHelper` and RSpec integration to make testing your TUI applications possible.
287
323
 
288
- [Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby/compare/v0.5.0...HEAD
289
- [0.5.0]: https://git.sr.ht/~kerrick/ratatui_ruby/compare/v0.4.0...v0.5.0
290
- [0.4.0]: https://git.sr.ht/~kerrick/ratatui_ruby/compare/v0.3.1...v0.4.0
291
- [0.3.1]: https://git.sr.ht/~kerrick/ratatui_ruby/compare/v0.3.0...v0.3.1
292
- [0.3.0]: https://git.sr.ht/~kerrick/ratatui_ruby/compare/v0.2.0...v0.3.0
293
- [0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby/compare/v0.1.0...v0.2.0
294
- [0.1.0]: https://git.sr.ht/~kerrick/ratatui_ruby/tree/v0.1.0
324
+ [Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/HEAD
325
+ [0.6.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.6.0
326
+ [0.5.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.5.0
327
+ [0.4.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.4.0
328
+ [0.3.1]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.3.1
329
+ [0.3.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.3.0
330
+ [0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.2.0
331
+ [0.1.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.1.0
data/README.md CHANGED
@@ -23,7 +23,7 @@ Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcem
23
23
  > [!WARNING]
24
24
  > **ratatui_ruby** is currently in **BETA**. The API may change between minor versions.
25
25
 
26
- Please join the **announce** mailing list at https://lists.sr.ht/~kerrick/ratatui_ruby-announce to stay up-to-date on new releases and announcements.
26
+ Please join the **announce** mailing list at https://lists.sr.ht/~kerrick/ratatui_ruby-announce to stay up-to-date on new releases and announcements. See the [`trunk` branch](https://git.sr.ht/~kerrick/ratatui_ruby/tree/trunk) for pre-release updates.
27
27
 
28
28
 
29
29
  ## Compatibility
@@ -63,8 +63,8 @@ gem install ratatui_ruby
63
63
 
64
64
  **ratatui_ruby** uses an immediate-mode API. You describe your UI using Ruby objects and call `draw` in a loop.
65
65
 
66
+ <!-- SYNC:START:examples/verify_readme_usage/app.rb:main -->
66
67
  ```ruby
67
- require "ratatui_ruby"
68
68
  RatatuiRuby.run do |tui|
69
69
  loop do
70
70
  tui.draw do |frame|
@@ -81,11 +81,16 @@ RatatuiRuby.run do |tui|
81
81
  frame.area
82
82
  )
83
83
  end
84
- event = tui.poll_event
85
- break if event == "q" || event == :ctrl_c
84
+ case tui.poll_event
85
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
86
+ break
87
+ else
88
+ nil
89
+ end
86
90
  end
87
91
  end
88
92
  ```
93
+ <!-- SYNC:END -->
89
94
 
90
95
  For a full tutorial, see [the Quickstart](./doc/quickstart.md). For an explanation of the application architecture, see [Application Architecture](./doc/application_architecture.md).
91
96
 
@@ -103,6 +108,8 @@ Issues for the underlying Rust library should be filed at [ratatui/ratatui](http
103
108
 
104
109
  Want to help develop **ratatui_ruby**? Check out the [contribution guide on the wiki](https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md).
105
110
 
111
+ **Note**: Active development happens on the `trunk` branch. Use `trunk` if you are a contributor or want the latest cutting-edge features. `stable` is for stable releases only.
112
+
106
113
 
107
114
  ## Copyright & License
108
115
 
data/REUSE.toml CHANGED
@@ -22,16 +22,11 @@ SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
22
22
  SPDX-License-Identifier = "AGPL-3.0-or-later"
23
23
 
24
24
  [[annotations]]
25
- path = 'test/snapshots/*.txt'
25
+ path = '**/snapshots/*.txt'
26
26
  SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
27
27
  SPDX-License-Identifier = "AGPL-3.0-or-later"
28
28
 
29
29
  [[annotations]]
30
- path = 'examples/app_all_events/test/snapshots/*.txt'
31
- SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
32
- SPDX-License-Identifier = "AGPL-3.0-or-later"
33
-
34
- [[annotations]]
35
- path = 'test/examples/app_all_events/snapshots/*.txt'
30
+ path = '**/snapshots/*.ansi'
36
31
  SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
37
32
  SPDX-License-Identifier = "AGPL-3.0-or-later"
@@ -52,6 +52,41 @@ ensure
52
52
  end
53
53
  ```
54
54
 
55
+ ### Stateful Widgets
56
+
57
+ Most widgets are stateless configuration. You create them, render them, and they are gone. However, the **runtime status** of some widgets (like Lists and Tables) must persist across frames (e.g., scroll offsets or selection).
58
+
59
+ **The Problem:** If you re-create a List configuration every frame, you lose the context of where it was scrolled or what was selected. If Ratatui auto-scrolls to a selection, you can't read that new offset back from an immutable input widget.
60
+
61
+ **The Solution:** Use "Stateful Rendering". You create a mutable State object (Output/Status) once and pass it to `render_stateful_widget`. **The Widget configuration (Input) is still mandatory**, but the State object (passed separately) captures the runtime changes.
62
+
63
+ > [!IMPORTANT]
64
+ > **Precedence Rule:** When using `render_stateful_widget`, the **State object is the single source of truth** for selection and offset. Widget properties (`selected_index`, `selected_row`, `offset`) are **ignored**.
65
+ >
66
+ > For example: `list(selected_index: 0)` with `state.select(5)` → Item 5 is highlighted, not Item 0.
67
+
68
+ **Use Case:** When you need to read back the scroll offset (e.g., for mouse hit testing) or persist selection without managing indexes manually.
69
+
70
+ ```ruby
71
+ # Initialize state once
72
+ @list_state = RatatuiRuby::ListState.new
73
+
74
+ RatatuiRuby.run do |tui|
75
+ loop do
76
+ tui.draw do |frame|
77
+ # Create immutable widget (selected_index is ignored in stateful mode)
78
+ list = tui.list(items: ["A", "B", "C"])
79
+
80
+ # Render with state — state takes precedence
81
+ frame.render_stateful_widget(list, frame.area, @list_state)
82
+ end
83
+
84
+ # Read back offset calculated by Ratatui
85
+ puts "Current Scroll Offset: #{@list_state.offset}"
86
+ end
87
+ end
88
+ ```
89
+
55
90
  ### API Convenience
56
91
 
57
92
  Writing UI trees involves nesting many widgets.
@@ -148,29 +183,68 @@ RatatuiRuby.run do
148
183
  end
149
184
  ```
150
185
 
186
+ ## Thread and Ractor Safety
187
+
188
+ Building for Ruby 4.0's parallel future? Know which objects can travel between Ractors.
189
+
190
+ ### Data Objects (Shareable)
191
+
192
+ These are deeply frozen and `Ractor.shareable?`. Include them in TEA Models/Messages freely:
193
+
194
+ | Object | Source |
195
+ |--------|--------|
196
+ | `Event::*` | `poll_event` |
197
+ | `Cell` | `get_cell_at` |
198
+ | `Rect` | `Layout.split`, `Frame#area` |
199
+
200
+ ### I/O Handles (Not Shareable)
201
+
202
+ These have side effects and are intentionally not shareable:
203
+
204
+ | Object | Valid Usage |
205
+ |--------|-------------|
206
+ | `Session` | Cache in `@tui` during run loop. Don't include in Models. |
207
+ | `Frame` | Pass to helpers during draw block. Invalid after block returns. |
208
+
209
+ ```ruby
210
+ # Good: Cache session in instance variable
211
+ RatatuiRuby.run do |tui|
212
+ @tui = tui
213
+ loop { render; handle_input }
214
+ end
215
+
216
+ # Bad: Include in immutable Model (won't work with Ractors)
217
+ Model = Data.define(:tui, :count) # Don't do this
218
+ ```
219
+
220
+
151
221
  ## Reference Architectures
152
222
 
153
223
  Simple scripts work well with valid linear code. Complex apps need structure.
154
224
 
155
225
  We provide these reference architectures to inspire you:
156
226
 
157
- ### MVVM (Model-View-ViewModel)
227
+ ### Proto-TEA (Model-View-Update)
158
228
 
159
229
  **Source:** [examples/app_all_events](../examples/app_all_events/README.md)
160
230
 
161
- This pattern strictly separates concerns:
162
- * **Model:** Pure business logic. It handles data and events.
163
- * **View State (ViewModel):** An immutable data structure built fresh every frame. It calculates logic-dependent properties (like `border_color`) so the View doesn't have to.
164
- * **View:** Pure rendering logic. It takes the View State and draws it.
231
+ This pattern implements unidirectional data flow inspired by The Elm Architecture:
232
+ * **Model:** A single immutable `Data.define` object holding all application state.
233
+ * **Msg:** Semantic value objects that decouple raw events from business logic.
234
+ * **Update:** A pure function that computes the next state: `Update.call(msg, model) -> Model`.
235
+ * **View:** Pure rendering logic that accepts the immutable Model.
165
236
 
166
- Use this when your app has complex state rules that clutter your rendering code.
237
+ Use this when you want predictable state management and easy-to-test logic.
167
238
 
168
- ### Scene-Orchestrated MVC
239
+ ### Proto-Kit (Component-Based)
169
240
 
170
241
  **Source:** [examples/app_color_picker](../examples/app_color_picker/README.md)
171
242
 
172
- This pattern addresses the difficulty of mouse interaction and layout management:
173
- * **Scene:** A specialized View that owns the layout *and* hit testing. It caches the screen coordinates of widgets during the draw phase.
174
- * **App (Controller):** Handles events by querying the Scene (e.g., `scene.rect_at(x, y)`).
243
+ This pattern addresses the difficulty of mouse interaction and complex UI orchestration:
244
+ * **Component Contract:** Every UI element implements `render(tui, frame, area)` and `handle_event(event)`.
245
+ * **Encapsulated Hit Testing:** Components cache their render area and check `contains?` internally.
246
+ * **Symbolic Signals:** `handle_event` returns semantic symbols (`:consumed`, `:submitted`) instead of just booleans.
247
+ * **Container (Mediator):** A parent container routes events via Chain of Responsibility and coordinates cross-component effects.
175
248
 
176
249
  Use this when you need rich interactivity (mouse clicks, drag-and-drop) or complex dynamic layouts.
250
+
@@ -8,15 +8,11 @@ This guide explains how to test your RatatuiRuby applications using the provided
8
8
 
9
9
  ## Overview
10
10
 
11
- RatatuiRuby includes a `TestHelper` module designed to simplify unit testing of TUI applications. It allows you to:
11
+ You need to verify that your application looks and behaves correctly. Manually checking every character on a terminal screen is tedious. Dealing with race conditions and complex state management in tests creates friction.
12
12
 
13
- - Initialize a virtual "test terminal" with specific dimensions.
13
+ The `TestHelper` module solves this. It provides a headless "test terminal" to capture output and a suite of robust assertions to verify state.
14
14
 
15
- - Capture the rendered output (the "buffer") to assert against expected text.
16
-
17
- - Inspect the cursor position.
18
-
19
- - Simulate user input (using `inject_event`).
15
+ Use it to write fast, deterministic tests for your TUI applications.
20
16
 
21
17
  ## Setup
22
18
 
@@ -36,11 +32,13 @@ class MyApplicationTest < Minitest::Test
36
32
  end
37
33
  ```
38
34
 
39
- ## Basic Usage
35
+ ## Writing a View Test
40
36
 
41
- ### `with_test_terminal`
37
+ To test a view or widget, wrap your assertions in `with_test_terminal`. This sets up a temporary, in-memory backend for Ratatui to draw to.
42
38
 
43
- Wrap your test assertions in `with_test_terminal`. This sets up a temporary, in-memory backend for Ratatui to draw to, instead of the real terminal. It automatically cleans up afterwards.
39
+ 1. **Initialize the terminal:** Call `with_test_terminal`.
40
+ 2. **Render your code:** Instantiate your widget and draw it to a frame.
41
+ 3. **Assert output:** Check the `buffer_content` against your expectations.
44
42
 
45
43
  ```ruby
46
44
  def test_rendering
@@ -55,49 +53,97 @@ def test_rendering
55
53
  end
56
54
 
57
55
  # 3. Assert on the output
58
- assert_includes buffer_content[0], "Hello World"
56
+ assert_includes buffer_content.first, "Hello World"
59
57
  end
60
58
  end
61
59
  ```
62
60
 
63
- ### `buffer_content`
61
+ For the full API list, including `buffer_content` and `cursor_position`, see [RatatuiRuby::TestHelper::Terminal](../lib/ratatui_ruby/test_helper/terminal.rb).
64
62
 
65
- Returns the current state of the terminal as an Array of Strings. Useful for verifying that specific text appears where you expect it.
66
-
67
- ```ruby
68
- rows = buffer_content
69
- assert_equal "Title", rows[0].strip
70
- assert_match /Results: \d+/, rows[2]
71
- ```
63
+ ## Verifying Styles
72
64
 
73
- ### `cursor_position`
65
+ You often need to check colors and modifiers (bold, italic) to ensure your highlighting logic works.
74
66
 
75
- Returns the current cursor coordinates as `{ x: Integer, y: Integer }`. Useful for forms or ensuring focus is correct.
67
+ Use `assert_fg_color`, `assert_bg_color`, and modifier helpers like `assert_bold`.
76
68
 
77
69
  ```ruby
78
- pos = cursor_position
79
- assert_equal 5, pos[:x]
80
- assert_equal 2, pos[:y]
70
+ # Assert specific cell style
71
+ assert_fg_color(:red, 0, 0)
72
+ assert_bold(0, 0)
73
+
74
+ # Or check a whole area
75
+ assert_area_style({ x: 0, y: 0, w: 10, h: 1 }, bg: :blue)
81
76
  ```
82
77
 
83
- ### `inject_event`
78
+ See [RatatuiRuby::TestHelper::StyleAssertions](../lib/ratatui_ruby/test_helper/style_assertions.rb) for the comprehensive list of style helpers.
79
+
80
+ ## Simulating Input
81
+
82
+ You need to test user interactions like typing or clicking. Stubbing `poll_event` directly is brittle.
84
83
 
85
- Injects a mock event into the event queue. This is the preferred way to simulate user input instead of stubbing `poll_event`.
84
+ Use `inject_event` to push mock events into the queue. This ensures safe, deterministic handling of input.
86
85
 
87
86
  > [!IMPORTANT]
88
- > You must call `inject_event` inside a `with_test_terminal` block. Calling it outside leads to race conditions where events are flushed before the application starts.
87
+ > Call `inject_event` inside a `with_test_terminal` block to avoid race conditions.
89
88
 
90
89
  ```ruby
91
90
  with_test_terminal do
92
91
  # Simulate 'q' key press
93
92
  inject_event("key", { code: "q" })
94
93
 
95
- # Now poll_event will return the 'q' key event
94
+ # The application receives the 'q' event
96
95
  event = RatatuiRuby.poll_event
97
96
  assert_equal "q", event.code
98
97
  end
99
98
  ```
100
99
 
100
+ See [RatatuiRuby::TestHelper::EventInjection](../lib/ratatui_ruby/test_helper/event_injection.rb) for helper methods like `inject_keys` and `inject_click`.
101
+
102
+ ## Snapshot Testing
103
+
104
+ Snapshots let you verify complex layouts without manually asserting every line.
105
+
106
+ Use `assert_snapshot` to compare the current screen against a stored reference file.
107
+
108
+ ```ruby
109
+ with_test_terminal do
110
+ MyApp.new.run
111
+ assert_snapshot("dashboard_view")
112
+ end
113
+ ```
114
+
115
+ ### Handling Non-Determinism
116
+
117
+ Snapshots must be deterministic. Random data or current timestamps will cause test failures ("flakes").
118
+
119
+ To prevent this:
120
+ 1. **Seed Randomness:** Use a fixed seed for any RNG.
121
+ 2. **Stub Time:** Force the application to use a static time.
122
+
123
+ For detailed strategies and code examples, see [RatatuiRuby::TestHelper::Snapshot](../lib/ratatui_ruby/test_helper/snapshot.rb).
124
+
125
+ ## Isolated View Testing
126
+
127
+ Sometimes you want to test a single view component without spinning up the full `TestTerminal` engine.
128
+
129
+ Use `MockFrame` and `StubRect` to test render logic in isolation.
130
+
131
+ ```ruby
132
+ def test_logs_view
133
+ frame = RatatuiRuby::TestHelper::TestDoubles::MockFrame.new
134
+ area = RatatuiRuby::TestHelper::TestDoubles::StubRect.new(width: 40, height: 10)
135
+
136
+ # Call your view directly
137
+ MyView.new.render(frame, area)
138
+
139
+ # Inspect what was rendered
140
+ rendered = frame.rendered_widgets.first
141
+ assert_equal "Logs", rendered[:widget].block.title
142
+ end
143
+ ```
144
+
145
+ See [RatatuiRuby::TestHelper::TestDoubles](../lib/ratatui_ruby/test_helper/test_doubles.rb).
146
+
101
147
  ## Example
102
148
 
103
- Be sure to check out the [examples directory](../examples/) in the repository, which contains several fully tested example applications showcasing these patterns.
149
+ Check out the [examples directory](../examples/) for fully tested applications showcasing these patterns.
@@ -13,13 +13,16 @@ This document describes the design philosophy and structure of the Ruby layer in
13
13
 
14
14
  The Ruby frontend is designed as a **thin, declarative layer** over the Rust backend. It uses an **Immediate Mode** paradigm where the user constructs a tree of pure data objects every frame to represent the desired UI state.
15
15
 
16
- ### 1. View Tree as Data
16
+ ### 1. Separation of Configuration and Status
17
17
 
18
- Unlike traditional OO GUI toolkits (like Qt or Swing) where widgets are retained objects with internal state, `ratatui_ruby` widgets are immutable value objects.
18
+ `ratatui_ruby` strictly separates **what** a widget is (Configuration) from **where** it is (Status).
19
+
20
+ #### Configuration (Input)
21
+ Widgets (e.g., `RatatuiRuby::List`) are immutable value objects defining the *desired appearance* for the current frame. They are pure inputs to the renderer.
19
22
 
20
23
  * Implemented using Ruby 3.2+ `Data` classes.
21
24
  * Located in `lib/ratatui_ruby/schema/`.
22
- * These objects act as a Schema or Interface Definition Language (IDL) between Ruby and Rust.
25
+ * Act as a Schema/IDL between Ruby and Rust.
23
26
 
24
27
  **Example:**
25
28
  ```ruby
@@ -31,6 +34,39 @@ paragraph = RatatuiRuby::Paragraph.new(
31
34
  )
32
35
  ```
33
36
 
37
+ #### Status (Output)
38
+ **Optional.** Only specific widgets (like `List` and `Table`) rely on runtime status. State objects (e.g., `RatatuiRuby::ListState`) track metrics calculated by the backend, such as scroll offsets.
39
+
40
+ * passed as a *secondary argument* to `render_stateful_widget`.
41
+ * **The Widget Configuration is still required.** You cannot render a State without its corresponding Widget.
42
+ * Updated in-place by the Rust backend to reflect the actual rendered state.
43
+
44
+ **Example:**
45
+ ```ruby
46
+ # 1. Initialize State once (Input/Output)
47
+ list_state = RatatuiRuby::ListState.new
48
+ list_state.select(3)
49
+
50
+ RatatuiRuby.run do |tui|
51
+ loop do
52
+ tui.draw do |frame|
53
+ # 2. Define Configuration (Input)
54
+ # (Note: In a real app, you'd probably use `tui.list(...)` helper)
55
+ list = RatatuiRuby::List.new(items: ["A", "B", "C", "D"])
56
+
57
+ # 3. Render with both (Side Effect: updates list_state)
58
+ frame.render_stateful_widget(list, frame.area, list_state)
59
+ end
60
+
61
+ # 4. Read back Status (Output)
62
+ # If the backend auto-scrolled to keep index 3 visible:
63
+ puts "Scroll Offset: #{list_state.offset}"
64
+
65
+ break if tui.poll_event == "q"
66
+ end
67
+ end
68
+ ```
69
+
34
70
  ### 2. Immediate Mode Rendering
35
71
 
36
72
  The application loop typically looks like this:
@@ -16,6 +16,7 @@ The project follows a **Structured Design** approach, separating concerns into m
16
16
  1. **Single Generic Renderer**: The backend implements a single generic renderer that accepts a Ruby `Value` representing the root of the view tree.
17
17
  2. **No Custom Rust Structs for UI**: Do not define custom Rust structs that mirror Ruby UI components. Instead, extract data directly from Ruby objects using `funcall`.
18
18
  3. **Dynamic Dispatch**: Use `value.class().name()` (e.g., `"RatatuiRuby::Paragraph"`) to dynamically dispatch rendering logic to the appropriate widget module.
19
+ * *Exception:* `render_stateful_widget` bypasses generic dispatch for specific Widget/State pairs (e.g., List + ListState) to allow mutating the State object.
19
20
  4. **Immediate Mode**: The renderer traverses the Ruby object tree every frame and rebuilds the Ratatui widget tree on the fly.
20
21
 
21
22
  ### Module Structure