ratatui_ruby 0.4.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 (441) 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 +98 -176
  7. data/CHANGELOG.md +80 -6
  8. data/README.md +19 -7
  9. data/REUSE.toml +15 -0
  10. data/doc/application_architecture.md +179 -45
  11. data/doc/application_testing.md +80 -32
  12. data/doc/contributors/design/ruby_frontend.md +48 -8
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +191 -48
  15. data/doc/contributors/documentation_style.md +7 -0
  16. data/doc/contributors/examples_audit/p1_high.md +21 -0
  17. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  18. data/doc/contributors/examples_audit.md +41 -0
  19. data/doc/contributors/index.md +2 -0
  20. data/doc/event_handling.md +21 -7
  21. data/doc/images/app_all_events.png +0 -0
  22. data/doc/images/app_color_picker.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_stateful_interaction.png +0 -0
  25. data/doc/images/verify_quickstart_dsl.png +0 -0
  26. data/doc/images/verify_quickstart_layout.png +0 -0
  27. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  28. data/doc/images/verify_readme_usage.png +0 -0
  29. data/doc/images/widget_barchart_demo.png +0 -0
  30. data/doc/images/widget_block_demo.png +0 -0
  31. data/doc/images/widget_box_demo.png +0 -0
  32. data/doc/images/widget_calendar_demo.png +0 -0
  33. data/doc/images/widget_canvas_demo.png +0 -0
  34. data/doc/images/widget_cell_demo.png +0 -0
  35. data/doc/images/widget_center_demo.png +0 -0
  36. data/doc/images/widget_chart_demo.png +0 -0
  37. data/doc/images/widget_gauge_demo.png +0 -0
  38. data/doc/images/widget_layout_split.png +0 -0
  39. data/doc/images/widget_line_gauge_demo.png +0 -0
  40. data/doc/images/widget_list_demo.png +0 -0
  41. data/doc/images/widget_overlay_demo.png +0 -0
  42. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  43. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  44. data/doc/images/widget_render.png +0 -0
  45. data/doc/images/widget_rich_text.png +0 -0
  46. data/doc/images/widget_scroll_text.png +0 -0
  47. data/doc/images/widget_scrollbar_demo.png +0 -0
  48. data/doc/images/widget_sparkline_demo.png +0 -0
  49. data/doc/images/widget_style_colors.png +0 -0
  50. data/doc/images/widget_table_demo.png +0 -0
  51. data/doc/images/widget_table_flex.png +0 -0
  52. data/doc/images/widget_tabs_demo.png +0 -0
  53. data/doc/images/widget_text_width.png +0 -0
  54. data/doc/interactive_design.md +25 -30
  55. data/doc/quickstart.md +150 -130
  56. data/doc/terminal_limitations.md +92 -0
  57. data/examples/app_all_events/README.md +99 -0
  58. data/examples/app_all_events/app.rb +96 -0
  59. data/examples/app_all_events/model/app_model.rb +157 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +92 -0
  62. data/examples/app_all_events/model/msg.rb +37 -0
  63. data/examples/app_all_events/model/timestamp.rb +54 -0
  64. data/examples/app_all_events/update.rb +73 -0
  65. data/examples/app_all_events/view/app_view.rb +78 -0
  66. data/examples/app_all_events/view/controls_view.rb +52 -0
  67. data/examples/app_all_events/view/counts_view.rb +59 -0
  68. data/examples/app_all_events/view/live_view.rb +70 -0
  69. data/examples/app_all_events/view/log_view.rb +55 -0
  70. data/examples/app_all_events/view.rb +7 -0
  71. data/examples/app_color_picker/README.md +134 -0
  72. data/examples/app_color_picker/app.rb +74 -0
  73. data/examples/app_color_picker/clipboard.rb +84 -0
  74. data/examples/app_color_picker/color.rb +191 -0
  75. data/examples/app_color_picker/controls.rb +90 -0
  76. data/examples/app_color_picker/copy_dialog.rb +166 -0
  77. data/examples/app_color_picker/export_pane.rb +126 -0
  78. data/examples/app_color_picker/harmony.rb +56 -0
  79. data/examples/app_color_picker/input.rb +174 -0
  80. data/examples/app_color_picker/main_container.rb +178 -0
  81. data/examples/app_color_picker/palette.rb +109 -0
  82. data/examples/app_login_form/README.md +47 -0
  83. data/examples/{login_form → app_login_form}/app.rb +38 -42
  84. data/examples/app_stateful_interaction/README.md +31 -0
  85. data/examples/app_stateful_interaction/app.rb +272 -0
  86. data/examples/timeout_demo.rb +43 -0
  87. data/examples/verify_quickstart_dsl/README.md +48 -0
  88. data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
  89. data/examples/verify_quickstart_layout/README.md +71 -0
  90. data/examples/verify_quickstart_layout/app.rb +71 -0
  91. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  92. data/examples/verify_quickstart_lifecycle/app.rb +54 -0
  93. data/examples/verify_readme_usage/README.md +43 -0
  94. data/examples/verify_readme_usage/app.rb +40 -0
  95. data/examples/widget_barchart_demo/README.md +49 -0
  96. data/examples/widget_barchart_demo/app.rb +238 -0
  97. data/examples/widget_block_demo/README.md +34 -0
  98. data/examples/widget_block_demo/app.rb +256 -0
  99. data/examples/widget_box_demo/README.md +45 -0
  100. data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
  101. data/examples/widget_calendar_demo/README.md +39 -0
  102. data/examples/widget_calendar_demo/app.rb +109 -0
  103. data/examples/widget_canvas_demo/README.md +27 -0
  104. data/examples/widget_canvas_demo/app.rb +123 -0
  105. data/examples/widget_cell_demo/README.md +36 -0
  106. data/examples/widget_cell_demo/app.rb +111 -0
  107. data/examples/widget_center_demo/README.md +29 -0
  108. data/examples/widget_center_demo/app.rb +116 -0
  109. data/examples/widget_chart_demo/README.md +41 -0
  110. data/examples/widget_chart_demo/app.rb +218 -0
  111. data/examples/widget_gauge_demo/README.md +41 -0
  112. data/examples/widget_gauge_demo/app.rb +212 -0
  113. data/examples/widget_layout_split/README.md +44 -0
  114. data/examples/widget_layout_split/app.rb +246 -0
  115. data/examples/widget_line_gauge_demo/README.md +41 -0
  116. data/examples/widget_line_gauge_demo/app.rb +217 -0
  117. data/examples/widget_list_demo/README.md +49 -0
  118. data/examples/widget_list_demo/app.rb +366 -0
  119. data/examples/widget_map_demo/README.md +39 -0
  120. data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
  121. data/examples/widget_overlay_demo/app.rb +248 -0
  122. data/examples/widget_popup_demo/README.md +36 -0
  123. data/examples/widget_popup_demo/app.rb +104 -0
  124. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  125. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  126. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  127. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  128. data/examples/widget_rect/README.md +38 -0
  129. data/examples/widget_rect/app.rb +205 -0
  130. data/examples/widget_render/README.md +37 -0
  131. data/examples/widget_render/app.rb +184 -0
  132. data/examples/widget_rich_text/README.md +35 -0
  133. data/examples/widget_rich_text/app.rb +166 -0
  134. data/examples/widget_scroll_text/README.md +37 -0
  135. data/examples/widget_scroll_text/app.rb +107 -0
  136. data/examples/widget_scrollbar_demo/README.md +37 -0
  137. data/examples/widget_scrollbar_demo/app.rb +153 -0
  138. data/examples/widget_sparkline_demo/README.md +42 -0
  139. data/examples/widget_sparkline_demo/app.rb +275 -0
  140. data/examples/widget_style_colors/README.md +34 -0
  141. data/examples/widget_style_colors/app.rb +19 -21
  142. data/examples/widget_table_demo/README.md +48 -0
  143. data/examples/widget_table_demo/app.rb +239 -0
  144. data/examples/widget_tabs_demo/README.md +41 -0
  145. data/examples/widget_tabs_demo/app.rb +181 -0
  146. data/examples/widget_text_width/README.md +35 -0
  147. data/examples/widget_text_width/app.rb +106 -0
  148. data/ext/ratatui_ruby/Cargo.lock +11 -4
  149. data/ext/ratatui_ruby/Cargo.toml +2 -1
  150. data/ext/ratatui_ruby/src/events.rs +359 -62
  151. data/ext/ratatui_ruby/src/frame.rs +227 -0
  152. data/ext/ratatui_ruby/src/lib.rs +110 -27
  153. data/ext/ratatui_ruby/src/rendering.rs +8 -4
  154. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  155. data/ext/ratatui_ruby/src/style.rs +138 -57
  156. data/ext/ratatui_ruby/src/terminal.rs +42 -22
  157. data/ext/ratatui_ruby/src/text.rs +14 -7
  158. data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
  159. data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
  160. data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
  161. data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
  162. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  163. data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
  164. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  165. data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
  166. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  167. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  168. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  169. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  170. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
  171. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
  172. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
  173. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  174. data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
  175. data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
  176. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  177. data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
  178. data/lib/ratatui_ruby/cell.rb +7 -7
  179. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  180. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  181. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  182. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  183. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  184. data/lib/ratatui_ruby/event/key.rb +112 -52
  185. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  186. data/lib/ratatui_ruby/event/none.rb +43 -0
  187. data/lib/ratatui_ruby/event/paste.rb +1 -1
  188. data/lib/ratatui_ruby/event.rb +56 -4
  189. data/lib/ratatui_ruby/frame.rb +183 -0
  190. data/lib/ratatui_ruby/list_state.rb +88 -0
  191. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
  192. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
  193. data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
  194. data/lib/ratatui_ruby/schema/block.rb +163 -168
  195. data/lib/ratatui_ruby/schema/calendar.rb +66 -67
  196. data/lib/ratatui_ruby/schema/canvas.rb +63 -63
  197. data/lib/ratatui_ruby/schema/center.rb +46 -46
  198. data/lib/ratatui_ruby/schema/chart.rb +135 -143
  199. data/lib/ratatui_ruby/schema/clear.rb +42 -42
  200. data/lib/ratatui_ruby/schema/constraint.rb +76 -76
  201. data/lib/ratatui_ruby/schema/cursor.rb +30 -25
  202. data/lib/ratatui_ruby/schema/gauge.rb +54 -52
  203. data/lib/ratatui_ruby/schema/layout.rb +87 -87
  204. data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
  205. data/lib/ratatui_ruby/schema/list.rb +103 -80
  206. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  207. data/lib/ratatui_ruby/schema/overlay.rb +31 -31
  208. data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
  209. data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
  210. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
  211. data/lib/ratatui_ruby/schema/rect.rb +99 -56
  212. data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
  213. data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
  214. data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
  215. data/lib/ratatui_ruby/schema/style.rb +66 -46
  216. data/lib/ratatui_ruby/schema/table.rb +126 -115
  217. data/lib/ratatui_ruby/schema/tabs.rb +66 -67
  218. data/lib/ratatui_ruby/schema/text.rb +69 -1
  219. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  220. data/lib/ratatui_ruby/session/autodoc.rb +482 -0
  221. data/lib/ratatui_ruby/session.rb +55 -23
  222. data/lib/ratatui_ruby/table_state.rb +90 -0
  223. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  224. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  225. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  226. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  227. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  228. data/lib/ratatui_ruby/test_helper.rb +66 -193
  229. data/lib/ratatui_ruby/version.rb +1 -1
  230. data/lib/ratatui_ruby.rb +100 -51
  231. data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
  232. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  233. data/sig/examples/app_all_events/model/events.rbs +15 -0
  234. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  235. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  236. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  237. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  238. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  239. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  240. data/sig/examples/app_all_events/view.rbs +8 -0
  241. data/sig/examples/app_all_events/view_state.rbs +15 -0
  242. data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
  243. data/sig/examples/app_login_form/app.rbs +11 -0
  244. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  245. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  246. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  247. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  248. data/sig/examples/widget_block_demo/app.rbs +32 -0
  249. data/sig/examples/widget_box_demo/app.rbs +11 -0
  250. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  251. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  252. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  253. data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
  254. data/sig/examples/widget_layout_split/app.rbs +10 -0
  255. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  256. data/sig/examples/widget_list_demo/app.rbs +12 -0
  257. data/sig/examples/widget_map_demo/app.rbs +11 -0
  258. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  259. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  260. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  261. data/sig/examples/widget_rect/app.rbs +12 -0
  262. data/sig/examples/widget_render/app.rbs +10 -0
  263. data/sig/examples/widget_rich_text/app.rbs +11 -0
  264. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  265. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  266. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  267. data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
  268. data/sig/examples/widget_table_demo/app.rbs +11 -0
  269. data/sig/examples/widget_text_width/app.rbs +10 -0
  270. data/sig/ratatui_ruby/event.rbs +11 -1
  271. data/sig/ratatui_ruby/frame.rbs +11 -0
  272. data/sig/ratatui_ruby/list_state.rbs +13 -0
  273. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
  274. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  275. data/sig/ratatui_ruby/schema/draw.rbs +4 -0
  276. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  277. data/sig/ratatui_ruby/schema/layout.rbs +1 -1
  278. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  279. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  280. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  281. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  282. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  283. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  284. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  285. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  286. data/sig/ratatui_ruby/session.rbs +107 -0
  287. data/sig/ratatui_ruby/table_state.rbs +15 -0
  288. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  289. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  290. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  291. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  292. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  293. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  294. data/tasks/autodoc/examples.rb +79 -0
  295. data/tasks/autodoc/inventory.rb +63 -0
  296. data/tasks/autodoc/member.rb +56 -0
  297. data/tasks/autodoc/name.rb +19 -0
  298. data/tasks/autodoc/notice.rb +26 -0
  299. data/tasks/autodoc/rbs.rb +38 -0
  300. data/tasks/autodoc/rdoc.rb +45 -0
  301. data/tasks/autodoc.rake +53 -0
  302. data/tasks/bump/changelog.rb +3 -3
  303. data/tasks/bump/history.rb +2 -2
  304. data/tasks/bump/links.rb +67 -0
  305. data/tasks/doc.rake +600 -6
  306. data/tasks/example_viewer.html.erb +172 -0
  307. data/tasks/lint.rake +8 -4
  308. data/tasks/resources/index.html.erb +6 -0
  309. data/tasks/sourcehut.rake +70 -30
  310. data/tasks/terminal_preview/app_screenshot.rb +14 -6
  311. data/tasks/terminal_preview/crash_report.rb +7 -9
  312. data/tasks/terminal_preview/launcher_script.rb +4 -6
  313. data/tasks/terminal_preview/preview_collection.rb +4 -6
  314. data/tasks/terminal_preview/safety_confirmation.rb +3 -5
  315. data/tasks/terminal_preview/saved_screenshot.rb +10 -11
  316. data/tasks/terminal_preview/terminal_window.rb +7 -9
  317. data/tasks/test.rake +1 -1
  318. data/tasks/website/index_page.rb +3 -3
  319. data/tasks/website/version.rb +10 -10
  320. data/tasks/website/version_menu.rb +10 -12
  321. data/tasks/website/versioned_documentation.rb +49 -17
  322. data/tasks/website/website.rb +6 -8
  323. data/tasks/website.rake +4 -4
  324. metadata +232 -127
  325. data/LICENSES/BSD-2-Clause.txt +0 -9
  326. data/doc/contributors/better_dx.md +0 -543
  327. data/doc/contributors/example_analysis.md +0 -82
  328. data/doc/images/all_events.png +0 -0
  329. data/doc/images/block_padding.png +0 -0
  330. data/doc/images/block_titles.png +0 -0
  331. data/doc/images/box_demo.png +0 -0
  332. data/doc/images/calendar_demo.png +0 -0
  333. data/doc/images/cell_demo.png +0 -0
  334. data/doc/images/chart_demo.png +0 -0
  335. data/doc/images/flex_layout.png +0 -0
  336. data/doc/images/gauge_demo.png +0 -0
  337. data/doc/images/line_gauge_demo.png +0 -0
  338. data/doc/images/list_demo.png +0 -0
  339. data/doc/images/list_styles.png +0 -0
  340. data/doc/images/login_form.png +0 -0
  341. data/doc/images/quickstart_dsl.png +0 -0
  342. data/doc/images/quickstart_lifecycle.png +0 -0
  343. data/doc/images/readme_usage.png +0 -0
  344. data/doc/images/rich_text.png +0 -0
  345. data/doc/images/scroll_text.png +0 -0
  346. data/doc/images/scrollbar_demo.png +0 -0
  347. data/doc/images/sparkline_demo.png +0 -0
  348. data/doc/images/table_flex.png +0 -0
  349. data/doc/images/table_select.png +0 -0
  350. data/examples/all_events/app.rb +0 -169
  351. data/examples/all_events/app.rbs +0 -7
  352. data/examples/all_events/test_app.rb +0 -139
  353. data/examples/analytics/app.rb +0 -258
  354. data/examples/analytics/app.rbs +0 -7
  355. data/examples/analytics/test_app.rb +0 -132
  356. data/examples/block_padding/app.rb +0 -63
  357. data/examples/block_padding/app.rbs +0 -7
  358. data/examples/block_padding/test_app.rb +0 -31
  359. data/examples/block_titles/app.rb +0 -61
  360. data/examples/block_titles/app.rbs +0 -7
  361. data/examples/block_titles/test_app.rb +0 -34
  362. data/examples/box_demo/app.rbs +0 -7
  363. data/examples/box_demo/test_app.rb +0 -88
  364. data/examples/calendar_demo/app.rb +0 -101
  365. data/examples/calendar_demo/app.rbs +0 -7
  366. data/examples/calendar_demo/test_app.rb +0 -108
  367. data/examples/cell_demo/app.rb +0 -108
  368. data/examples/cell_demo/app.rbs +0 -7
  369. data/examples/cell_demo/test_app.rb +0 -36
  370. data/examples/chart_demo/app.rb +0 -203
  371. data/examples/chart_demo/app.rbs +0 -7
  372. data/examples/chart_demo/test_app.rb +0 -102
  373. data/examples/custom_widget/app.rb +0 -51
  374. data/examples/custom_widget/app.rbs +0 -7
  375. data/examples/custom_widget/test_app.rb +0 -30
  376. data/examples/flex_layout/app.rb +0 -156
  377. data/examples/flex_layout/app.rbs +0 -7
  378. data/examples/flex_layout/test_app.rb +0 -65
  379. data/examples/gauge_demo/app.rb +0 -182
  380. data/examples/gauge_demo/test_app.rb +0 -120
  381. data/examples/hit_test/app.rb +0 -175
  382. data/examples/hit_test/app.rbs +0 -7
  383. data/examples/hit_test/test_app.rb +0 -102
  384. data/examples/line_gauge_demo/app.rb +0 -190
  385. data/examples/line_gauge_demo/app.rbs +0 -7
  386. data/examples/line_gauge_demo/test_app.rb +0 -129
  387. data/examples/list_demo/app.rb +0 -253
  388. data/examples/list_demo/test_app.rb +0 -237
  389. data/examples/list_styles/app.rb +0 -140
  390. data/examples/list_styles/app.rbs +0 -7
  391. data/examples/list_styles/test_app.rb +0 -157
  392. data/examples/login_form/app.rbs +0 -7
  393. data/examples/login_form/test_app.rb +0 -51
  394. data/examples/map_demo/app.rbs +0 -7
  395. data/examples/map_demo/test_app.rb +0 -149
  396. data/examples/mouse_events/app.rb +0 -97
  397. data/examples/mouse_events/app.rbs +0 -7
  398. data/examples/mouse_events/test_app.rb +0 -53
  399. data/examples/popup_demo/app.rb +0 -103
  400. data/examples/popup_demo/app.rbs +0 -7
  401. data/examples/popup_demo/test_app.rb +0 -54
  402. data/examples/quickstart_dsl/app.rbs +0 -7
  403. data/examples/quickstart_dsl/test_app.rb +0 -29
  404. data/examples/quickstart_lifecycle/app.rb +0 -39
  405. data/examples/quickstart_lifecycle/app.rbs +0 -7
  406. data/examples/quickstart_lifecycle/test_app.rb +0 -29
  407. data/examples/ratatui_logo_demo/app.rb +0 -79
  408. data/examples/ratatui_logo_demo/app.rbs +0 -7
  409. data/examples/ratatui_logo_demo/test_app.rb +0 -51
  410. data/examples/ratatui_mascot_demo/app.rb +0 -84
  411. data/examples/ratatui_mascot_demo/app.rbs +0 -7
  412. data/examples/ratatui_mascot_demo/test_app.rb +0 -47
  413. data/examples/readme_usage/app.rb +0 -29
  414. data/examples/readme_usage/app.rbs +0 -7
  415. data/examples/readme_usage/test_app.rb +0 -29
  416. data/examples/rich_text/app.rb +0 -141
  417. data/examples/rich_text/app.rbs +0 -7
  418. data/examples/rich_text/test_app.rb +0 -166
  419. data/examples/scroll_text/app.rb +0 -103
  420. data/examples/scroll_text/app.rbs +0 -7
  421. data/examples/scroll_text/test_app.rb +0 -110
  422. data/examples/scrollbar_demo/app.rb +0 -143
  423. data/examples/scrollbar_demo/app.rbs +0 -7
  424. data/examples/scrollbar_demo/test_app.rb +0 -77
  425. data/examples/sparkline_demo/app.rb +0 -240
  426. data/examples/sparkline_demo/test_app.rb +0 -107
  427. data/examples/table_flex/app.rb +0 -65
  428. data/examples/table_flex/app.rbs +0 -7
  429. data/examples/table_flex/test_app.rb +0 -36
  430. data/examples/table_select/app.rb +0 -198
  431. data/examples/table_select/app.rbs +0 -7
  432. data/examples/table_select/test_app.rb +0 -180
  433. data/examples/widget_style_colors/test_app.rb +0 -48
  434. data/tasks/bump/comparison_links.rb +0 -41
  435. /data/doc/images/{analytics.png → app_analytics.png} +0 -0
  436. /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
  437. /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
  438. /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
  439. /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
  440. /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
  441. /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
@@ -1,58 +1,116 @@
1
- # Core Concepts
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
3
 
3
- This guide explains the core concepts and patterns available in `ratatui_ruby` for structuring your terminal applications.
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+ -->
4
6
 
5
- ## 1. Lifecycle Management
7
+ # Application Architecture
6
8
 
7
- Managing the terminal state is critical. You must enter "alternate screen" and "raw mode" on startup, and **always** restore the terminal on exit (even on errors), otherwise the user's terminal will be left in a broken state.
9
+ Architect robust TUI applications using core lifecycle patterns and API best practices.
8
10
 
9
- ### `RatatuiRuby.run` (Recommended)
11
+ ## Core Concepts
10
12
 
11
- The `run` method acts as a **Context Manager**. It handles the initialization and restoration for you, ensuring the terminal is always restored even if your code raises an exception. We recommend using `run` for all applications, as it provides a safe sandbox for your TUI.
13
+ Your app lives inside a terminal. You need to respect its rules.
14
+
15
+ ### Lifecycle Management
16
+
17
+ Terminals have state. They remember cursor positions, input modes, and screen buffers.
18
+
19
+ **The Problem:** If your app crashes or exits without cleaning up, it "breaks" the user's terminal. The cursor vanishes. Input echoes constantly. The alternate screen doesn't clear.
20
+
21
+ **The Solution:** The library's lifecycle manager handles this for you. It enters "raw mode" on startup and guarantees restoration on exit.
22
+
23
+ #### Use `RatatuiRuby.run`
24
+
25
+ This method acts as a safety net. It initializes the terminal, yields control to your block, and restores the terminal afterwards—even if your code raises an exception.
12
26
 
13
27
  ```ruby
14
28
  RatatuiRuby.run do |tui|
15
29
  loop do
16
- # Your code here
17
- tui.draw(...)
30
+ tui.draw do |frame|
31
+ frame.render_widget(tui.paragraph(text: "Hello"), frame.area)
32
+ end
33
+ break if tui.poll_event == "q"
18
34
  end
19
35
  end
20
36
  # Terminal is restored here
21
37
  ```
22
38
 
23
- ### Manual Management (Advanced)
39
+ #### Manual Management
24
40
 
25
- You can manage this manually if you need granular control, but use `ensure` blocks!
41
+ Need granular control? You can initialize and restore the terminal yourself. Use `ensure` blocks to guarantee cleanup.
26
42
 
27
43
  ```ruby
28
44
  RatatuiRuby.init_terminal
29
45
  begin
30
- # Your code here
31
- RatatuiRuby.draw(...)
46
+ RatatuiRuby.draw do |frame|
47
+ frame.render_widget(RatatuiRuby::Paragraph.new(text: "Hello"), frame.area)
48
+ end
32
49
  ensure
33
50
  RatatuiRuby.restore_terminal
51
+ # Terminal is restored here
52
+ end
53
+ ```
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
34
87
  end
35
88
  ```
36
89
 
37
- ## 2. API Convenience
90
+ ### API Convenience
38
91
 
39
- ### Session API (Recommended)
92
+ Writing UI trees involves nesting many widgets.
40
93
 
41
- The block yielded by `run` is a `RatatuiRuby::Session` instance (`tui`).
42
- It provides factory methods for every widget class (converting snake_case to CamelCase) and aliases for module functions.
94
+ **The Problem:** Explicitly namespacing `RatatuiRuby::` for every widget (e.g., `RatatuiRuby::Paragraph.new`) is tedious. It creates visual noise that hides your layout structure.
43
95
 
44
- **Why use it?** It significantly reduces verbosity and repeated `RatatuiRuby::` namespacing, making the UI tree structure easier to read.
96
+ **The Solution:** The Session API (`tui`) provides shorthand factories for every widget. It yields a session object to your block.
45
97
 
46
98
  ```ruby
47
99
  RatatuiRuby.run do |tui|
48
100
  loop do
49
- layout = tui.layout(
50
- direction: :horizontal,
51
- constraints: [
52
- RatatuiRuby::Constraint.length(20),
53
- RatatuiRuby::Constraint.min(0)
54
- ],
55
- children: [
101
+ tui.draw do |frame|
102
+ # Split layout using Session helpes
103
+ sidebar_area, content_area = tui.layout_split(
104
+ frame.area,
105
+ direction: :horizontal,
106
+ constraints: [
107
+ tui.constraint_length(20),
108
+ tui.constraint_min(0)
109
+ ]
110
+ )
111
+
112
+ # Render sidebar
113
+ frame.render_widget(
56
114
  tui.paragraph(
57
115
  text: tui.text_line(spans: [
58
116
  tui.text_span(content: "Side", style: tui.style(fg: :blue)),
@@ -60,15 +118,19 @@ RatatuiRuby.run do |tui|
60
118
  ]),
61
119
  block: tui.block(borders: [:all], title: "Nav")
62
120
  ),
121
+ sidebar_area
122
+ )
123
+
124
+ # Render main content
125
+ frame.render_widget(
63
126
  tui.paragraph(
64
127
  text: "Main Content",
65
128
  style: tui.style(fg: :green),
66
129
  block: tui.block(borders: [:all], title: "Content")
67
- )
68
- ]
69
- )
70
-
71
- tui.draw(layout)
130
+ ),
131
+ content_area
132
+ )
133
+ end
72
134
 
73
135
  event = tui.poll_event
74
136
  break if event == "q" || event == :ctrl_c
@@ -76,22 +138,25 @@ RatatuiRuby.run do |tui|
76
138
  end
77
139
  ```
78
140
 
79
- ### Raw API
141
+ #### Raw API
80
142
 
81
- You can always use the raw module methods and classes directly. This is useful if you are building your own abstractions or prefer explicit class instantiation.
82
-
83
- **Comparison:** Notice how much more verbose the same UI definition is.
143
+ Building your own abstractions? You might prefer explicit class instantiation. The raw constants are always available.
84
144
 
85
145
  ```ruby
86
146
  RatatuiRuby.run do
87
147
  loop do
88
- layout = RatatuiRuby::Layout.new(
89
- direction: :horizontal,
90
- constraints: [
91
- RatatuiRuby::Constraint.length(20),
92
- RatatuiRuby::Constraint.min(0)
93
- ],
94
- children: [
148
+ RatatuiRuby.draw do |frame|
149
+ # Manual split
150
+ rects = RatatuiRuby::Layout.split(
151
+ frame.area,
152
+ direction: :horizontal,
153
+ constraints: [
154
+ RatatuiRuby::Constraint.length(20),
155
+ RatatuiRuby::Constraint.min(0)
156
+ ]
157
+ )
158
+
159
+ frame.render_widget(
95
160
  RatatuiRuby::Paragraph.new(
96
161
  text: RatatuiRuby::Text::Line.new(spans: [
97
162
  RatatuiRuby::Text::Span.new(content: "Side", style: RatatuiRuby::Style.new(fg: :blue)),
@@ -99,18 +164,87 @@ RatatuiRuby.run do
99
164
  ]),
100
165
  block: RatatuiRuby::Block.new(borders: [:all], title: "Nav")
101
166
  ),
167
+ rects[0]
168
+ )
169
+
170
+ frame.render_widget(
102
171
  RatatuiRuby::Paragraph.new(
103
172
  text: "Main Content",
104
173
  style: RatatuiRuby::Style.new(fg: :green),
105
174
  block: RatatuiRuby::Block.new(borders: [:all], title: "Content")
106
- )
107
- ]
108
- )
109
-
110
- RatatuiRuby.draw(layout)
175
+ ),
176
+ rects[1]
177
+ )
178
+ end
111
179
 
112
180
  event = RatatuiRuby.poll_event
113
181
  break if event == "q" || event == :ctrl_c
114
182
  end
115
183
  end
116
184
  ```
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
+
221
+ ## Reference Architectures
222
+
223
+ Simple scripts work well with valid linear code. Complex apps need structure.
224
+
225
+ We provide these reference architectures to inspire you:
226
+
227
+ ### Proto-TEA (Model-View-Update)
228
+
229
+ **Source:** [examples/app_all_events](../examples/app_all_events/README.md)
230
+
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.
236
+
237
+ Use this when you want predictable state management and easy-to-test logic.
238
+
239
+ ### Proto-Kit (Component-Based)
240
+
241
+ **Source:** [examples/app_color_picker](../examples/app_color_picker/README.md)
242
+
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.
248
+
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,66 +32,118 @@ 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
47
45
  # Uses default 80x24 terminal
48
46
  with_test_terminal do
49
- # 1. Instantiate your app/component
47
+ # 1. Instantiate your widget
50
48
  widget = RatatuiRuby::Paragraph.new(text: "Hello World")
51
49
 
52
- # 2. Render it
53
- RatatuiRuby.draw(widget)
50
+ # 2. Render it using the Frame API
51
+ RatatuiRuby.draw do |frame|
52
+ frame.render_widget(widget, frame.area)
53
+ end
54
54
 
55
55
  # 3. Assert on the output
56
- assert_includes buffer_content[0], "Hello World"
56
+ assert_includes buffer_content.first, "Hello World"
57
57
  end
58
58
  end
59
59
  ```
60
60
 
61
- ### `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).
62
62
 
63
- Returns the current state of the terminal as an Array of Strings. Useful for verifying that specific text appears where you expect it.
64
-
65
- ```ruby
66
- rows = buffer_content
67
- assert_equal "Title", rows[0].strip
68
- assert_match /Results: \d+/, rows[2]
69
- ```
63
+ ## Verifying Styles
70
64
 
71
- ### `cursor_position`
65
+ You often need to check colors and modifiers (bold, italic) to ensure your highlighting logic works.
72
66
 
73
- 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`.
74
68
 
75
69
  ```ruby
76
- pos = cursor_position
77
- assert_equal 5, pos[:x]
78
- 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)
79
76
  ```
80
77
 
81
- ### `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.
82
83
 
83
- 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.
84
85
 
85
86
  > [!IMPORTANT]
86
- > 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.
87
88
 
88
89
  ```ruby
89
90
  with_test_terminal do
90
91
  # Simulate 'q' key press
91
92
  inject_event("key", { code: "q" })
92
93
 
93
- # Now poll_event will return the 'q' key event
94
+ # The application receives the 'q' event
94
95
  event = RatatuiRuby.poll_event
95
96
  assert_equal "q", event.code
96
97
  end
97
98
  ```
98
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
+
99
147
  ## Example
100
148
 
101
- 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.
@@ -9,15 +9,20 @@ This document describes the design philosophy and structure of the Ruby layer in
9
9
 
10
10
  ## Core Philosophy: Data-Driven UI
11
11
 
12
+
13
+
12
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.
13
15
 
14
- ### 1. View Tree as Data
16
+ ### 1. Separation of Configuration and Status
15
17
 
16
- 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.
17
22
 
18
23
  * Implemented using Ruby 3.2+ `Data` classes.
19
24
  * Located in `lib/ratatui_ruby/schema/`.
20
- * 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.
21
26
 
22
27
  **Example:**
23
28
  ```ruby
@@ -29,6 +34,39 @@ paragraph = RatatuiRuby::Paragraph.new(
29
34
  )
30
35
  ```
31
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
+
32
70
  ### 2. Immediate Mode Rendering
33
71
 
34
72
  The application loop typically looks like this:
@@ -43,11 +81,13 @@ loop do
43
81
  event = RatatuiRuby.poll_event
44
82
  break if event == :esc
45
83
 
46
- # 3. Construct View Tree
47
- ui = RatatuiRuby::Paragraph.new(text: "Time: #{Time.now}")
48
-
49
- # 4. Draw
50
- RatatuiRuby.draw(ui)
84
+ # 3. Construct View Tree & Draw
85
+ RatatuiRuby.draw do |frame|
86
+ frame.render_widget(
87
+ RatatuiRuby::Paragraph.new(text: "Time: #{Time.now}"),
88
+ frame.area
89
+ )
90
+ end
51
91
  end
52
92
  ```
53
93
 
@@ -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