ratatui_ruby 0.8.0 → 0.9.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 (352) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +2 -2
  3. data/.builds/ruby-3.3.yml +2 -2
  4. data/.builds/ruby-3.4.yml +2 -2
  5. data/.builds/ruby-4.0.0.yml +2 -2
  6. data/.pre-commit-config.yaml +1 -1
  7. data/AGENTS.md +3 -3
  8. data/CHANGELOG.md +53 -1
  9. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  10. data/LICENSES/MIT-0.txt +16 -0
  11. data/README.md +33 -5
  12. data/Rakefile +1 -1
  13. data/doc/concepts/application_architecture.md +44 -3
  14. data/doc/concepts/application_testing.md +43 -1
  15. data/doc/concepts/async.md +32 -2
  16. data/doc/concepts/custom_widgets.md +247 -0
  17. data/doc/concepts/event_handling.md +32 -3
  18. data/doc/concepts/interactive_design.md +32 -2
  19. data/doc/contributors/auditing/parity.md +7 -1
  20. data/doc/contributors/design/ruby_frontend.md +85 -1
  21. data/doc/contributors/design/rust_backend.md +67 -1
  22. data/doc/contributors/developing_examples.md +56 -2
  23. data/doc/contributors/documentation_style.md +20 -3
  24. data/doc/contributors/future_work.md +169 -0
  25. data/doc/contributors/index.md +1 -1
  26. data/doc/contributors/v1.0.0_blockers.md +15 -175
  27. data/doc/getting_started/quickstart.md +22 -4
  28. data/doc/getting_started/why.md +1 -1
  29. data/doc/index.md +2 -1
  30. data/doc/troubleshooting/debugging.md +32 -2
  31. data/doc/troubleshooting/terminal_limitations.md +8 -2
  32. data/doc/troubleshooting/tui_output.md +42 -0
  33. data/examples/app_all_events/README.md +14 -2
  34. data/examples/app_all_events/app.rb +1 -1
  35. data/examples/app_all_events/model/app_model.rb +1 -1
  36. data/examples/app_all_events/model/event_color_cycle.rb +1 -1
  37. data/examples/app_all_events/model/event_entry.rb +1 -1
  38. data/examples/app_all_events/model/msg.rb +1 -1
  39. data/examples/app_all_events/model/timestamp.rb +1 -1
  40. data/examples/app_all_events/update.rb +1 -1
  41. data/examples/app_all_events/view/app_view.rb +1 -1
  42. data/examples/app_all_events/view/controls_view.rb +1 -1
  43. data/examples/app_all_events/view/counts_view.rb +1 -1
  44. data/examples/app_all_events/view/live_view.rb +1 -1
  45. data/examples/app_all_events/view/log_view.rb +1 -1
  46. data/examples/app_all_events/view.rb +1 -1
  47. data/examples/app_color_picker/README.md +20 -2
  48. data/examples/app_color_picker/app.rb +1 -1
  49. data/examples/app_color_picker/clipboard.rb +1 -1
  50. data/examples/app_color_picker/color.rb +1 -1
  51. data/examples/app_color_picker/controls.rb +1 -1
  52. data/examples/app_color_picker/copy_dialog.rb +1 -1
  53. data/examples/app_color_picker/export_pane.rb +1 -1
  54. data/examples/app_color_picker/harmony.rb +1 -1
  55. data/examples/app_color_picker/input.rb +1 -1
  56. data/examples/app_color_picker/main_container.rb +1 -1
  57. data/examples/app_color_picker/palette.rb +1 -1
  58. data/examples/app_login_form/README.md +8 -2
  59. data/examples/app_login_form/app.rb +1 -1
  60. data/examples/app_stateful_interaction/README.md +2 -2
  61. data/examples/app_stateful_interaction/app.rb +71 -17
  62. data/examples/timeout_demo.rb +1 -1
  63. data/examples/verify_quickstart_dsl/README.md +6 -0
  64. data/examples/verify_quickstart_dsl/app.rb +3 -3
  65. data/examples/verify_quickstart_layout/README.md +6 -0
  66. data/examples/verify_quickstart_layout/app.rb +3 -3
  67. data/examples/verify_quickstart_lifecycle/README.md +6 -0
  68. data/examples/verify_quickstart_lifecycle/app.rb +3 -3
  69. data/examples/verify_readme_usage/README.md +6 -0
  70. data/examples/verify_readme_usage/app.rb +3 -3
  71. data/examples/widget_barchart/README.md +6 -0
  72. data/examples/widget_barchart/app.rb +2 -2
  73. data/examples/widget_block/README.md +7 -1
  74. data/examples/widget_block/app.rb +2 -2
  75. data/examples/widget_box/README.md +6 -0
  76. data/examples/widget_box/app.rb +9 -6
  77. data/examples/widget_calendar/README.md +6 -0
  78. data/examples/widget_calendar/app.rb +2 -2
  79. data/examples/widget_canvas/README.md +4 -0
  80. data/examples/widget_canvas/app.rb +2 -2
  81. data/examples/widget_cell/README.md +6 -0
  82. data/examples/widget_cell/app.rb +2 -3
  83. data/examples/widget_center/README.md +4 -0
  84. data/examples/widget_center/app.rb +2 -2
  85. data/examples/widget_chart/README.md +6 -0
  86. data/examples/widget_chart/app.rb +2 -2
  87. data/examples/widget_gauge/README.md +6 -0
  88. data/examples/widget_gauge/app.rb +2 -2
  89. data/examples/widget_layout_split/README.md +6 -0
  90. data/examples/widget_layout_split/app.rb +3 -3
  91. data/examples/widget_line_gauge/README.md +6 -0
  92. data/examples/widget_line_gauge/app.rb +2 -2
  93. data/examples/widget_list/README.md +6 -0
  94. data/examples/widget_list/app.rb +2 -2
  95. data/examples/widget_map/README.md +8 -2
  96. data/examples/widget_map/app.rb +2 -2
  97. data/examples/widget_overlay/README.md +7 -1
  98. data/examples/widget_overlay/app.rb +2 -2
  99. data/examples/widget_popup/README.md +6 -0
  100. data/examples/widget_popup/app.rb +2 -2
  101. data/examples/widget_ratatui_logo/README.md +6 -0
  102. data/examples/widget_ratatui_logo/app.rb +2 -3
  103. data/examples/widget_ratatui_mascot/README.md +6 -0
  104. data/examples/widget_ratatui_mascot/app.rb +2 -2
  105. data/examples/widget_rect/README.md +12 -0
  106. data/examples/widget_rect/app.rb +40 -26
  107. data/examples/widget_render/README.md +6 -0
  108. data/examples/widget_render/app.rb +2 -2
  109. data/examples/widget_render/app.rbs +41 -0
  110. data/examples/widget_rich_text/README.md +6 -0
  111. data/examples/widget_rich_text/app.rb +2 -2
  112. data/examples/widget_scroll_text/README.md +6 -0
  113. data/examples/widget_scroll_text/app.rb +2 -2
  114. data/examples/widget_scrollbar/README.md +6 -0
  115. data/examples/widget_scrollbar/app.rb +2 -2
  116. data/examples/widget_sparkline/README.md +6 -0
  117. data/examples/widget_sparkline/app.rb +2 -2
  118. data/examples/widget_style_colors/README.md +6 -0
  119. data/examples/widget_style_colors/app.rb +2 -2
  120. data/examples/widget_table/README.md +8 -2
  121. data/examples/widget_table/app.rb +2 -2
  122. data/examples/widget_tabs/README.md +6 -0
  123. data/examples/widget_tabs/app.rb +2 -2
  124. data/examples/widget_text_width/README.md +6 -0
  125. data/examples/widget_text_width/app.rb +4 -4
  126. data/ext/ratatui_ruby/Cargo.lock +1 -1
  127. data/ext/ratatui_ruby/Cargo.toml +1 -1
  128. data/ext/ratatui_ruby/extconf.rb +2 -2
  129. data/ext/ratatui_ruby/src/rendering.rs +1 -1
  130. data/ext/ratatui_ruby/src/style.rs +0 -8
  131. data/ext/ratatui_ruby/src/widgets/chart.rs +0 -118
  132. data/ext/ratatui_ruby/src/widgets/list_state.rs +36 -0
  133. data/lib/ratatui_ruby/buffer/cell.rb +34 -2
  134. data/lib/ratatui_ruby/buffer.rb +2 -2
  135. data/lib/ratatui_ruby/cell.rb +34 -2
  136. data/lib/ratatui_ruby/event/focus_gained.rb +26 -2
  137. data/lib/ratatui_ruby/event/focus_lost.rb +26 -2
  138. data/lib/ratatui_ruby/event/key/character.rb +18 -2
  139. data/lib/ratatui_ruby/event/key/media.rb +2 -2
  140. data/lib/ratatui_ruby/event/key/modifier.rb +10 -2
  141. data/lib/ratatui_ruby/event/key/navigation.rb +2 -2
  142. data/lib/ratatui_ruby/event/key/system.rb +2 -2
  143. data/lib/ratatui_ruby/event/key.rb +114 -2
  144. data/lib/ratatui_ruby/event/mouse.rb +42 -2
  145. data/lib/ratatui_ruby/event/none.rb +10 -2
  146. data/lib/ratatui_ruby/event/paste.rb +34 -2
  147. data/lib/ratatui_ruby/event/resize.rb +34 -2
  148. data/lib/ratatui_ruby/event.rb +26 -2
  149. data/lib/ratatui_ruby/frame.rb +74 -2
  150. data/lib/ratatui_ruby/layout/constraint.rb +58 -2
  151. data/lib/ratatui_ruby/layout/layout.rb +47 -2
  152. data/lib/ratatui_ruby/layout/rect.rb +403 -2
  153. data/lib/ratatui_ruby/layout.rb +2 -2
  154. data/lib/ratatui_ruby/list_state.rb +113 -2
  155. data/lib/ratatui_ruby/output_guard.rb +26 -3
  156. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  157. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +2 -2
  158. data/lib/ratatui_ruby/schema/bar_chart.rb +50 -2
  159. data/lib/ratatui_ruby/schema/block.rb +21 -15
  160. data/lib/ratatui_ruby/schema/calendar.rb +2 -2
  161. data/lib/ratatui_ruby/schema/canvas.rb +10 -2
  162. data/lib/ratatui_ruby/schema/center.rb +10 -2
  163. data/lib/ratatui_ruby/schema/chart.rb +2 -28
  164. data/lib/ratatui_ruby/schema/clear.rb +10 -2
  165. data/lib/ratatui_ruby/schema/constraint.rb +58 -2
  166. data/lib/ratatui_ruby/schema/cursor.rb +10 -2
  167. data/lib/ratatui_ruby/schema/draw.rb +10 -2
  168. data/lib/ratatui_ruby/schema/gauge.rb +2 -2
  169. data/lib/ratatui_ruby/schema/layout.rb +18 -2
  170. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  171. data/lib/ratatui_ruby/schema/list.rb +10 -2
  172. data/lib/ratatui_ruby/schema/list_item.rb +10 -2
  173. data/lib/ratatui_ruby/schema/overlay.rb +10 -2
  174. data/lib/ratatui_ruby/schema/paragraph.rb +10 -2
  175. data/lib/ratatui_ruby/schema/ratatui_logo.rb +2 -2
  176. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +2 -2
  177. data/lib/ratatui_ruby/schema/rect.rb +58 -2
  178. data/lib/ratatui_ruby/schema/row.rb +10 -2
  179. data/lib/ratatui_ruby/schema/scrollbar.rb +2 -2
  180. data/lib/ratatui_ruby/schema/shape/label.rb +10 -2
  181. data/lib/ratatui_ruby/schema/sparkline.rb +10 -2
  182. data/lib/ratatui_ruby/schema/style.rb +18 -2
  183. data/lib/ratatui_ruby/schema/table.rb +2 -2
  184. data/lib/ratatui_ruby/schema/tabs.rb +2 -2
  185. data/lib/ratatui_ruby/schema/text.rb +34 -2
  186. data/lib/ratatui_ruby/scrollbar_state.rb +10 -2
  187. data/lib/ratatui_ruby/style/style.rb +18 -2
  188. data/lib/ratatui_ruby/style.rb +2 -2
  189. data/lib/ratatui_ruby/table_state.rb +10 -2
  190. data/lib/ratatui_ruby/terminal_lifecycle.rb +18 -3
  191. data/lib/ratatui_ruby/test_helper/event_injection.rb +34 -2
  192. data/lib/ratatui_ruby/test_helper/snapshot.rb +74 -9
  193. data/lib/ratatui_ruby/test_helper/style_assertions.rb +98 -2
  194. data/lib/ratatui_ruby/test_helper/terminal.rb +50 -2
  195. data/lib/ratatui_ruby/test_helper/test_doubles.rb +18 -2
  196. data/lib/ratatui_ruby/test_helper.rb +10 -2
  197. data/lib/ratatui_ruby/tui/buffer_factories.rb +2 -2
  198. data/lib/ratatui_ruby/tui/canvas_factories.rb +2 -2
  199. data/lib/ratatui_ruby/tui/core.rb +2 -2
  200. data/lib/ratatui_ruby/tui/layout_factories.rb +32 -2
  201. data/lib/ratatui_ruby/tui/state_factories.rb +2 -2
  202. data/lib/ratatui_ruby/tui/style_factories.rb +2 -2
  203. data/lib/ratatui_ruby/tui/text_factories.rb +2 -2
  204. data/lib/ratatui_ruby/tui/widget_factories.rb +2 -2
  205. data/lib/ratatui_ruby/tui.rb +11 -3
  206. data/lib/ratatui_ruby/version.rb +3 -3
  207. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -2
  208. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -2
  209. data/lib/ratatui_ruby/widgets/bar_chart.rb +58 -2
  210. data/lib/ratatui_ruby/widgets/block.rb +37 -15
  211. data/lib/ratatui_ruby/widgets/calendar.rb +2 -2
  212. data/lib/ratatui_ruby/widgets/canvas.rb +10 -2
  213. data/lib/ratatui_ruby/widgets/cell.rb +10 -2
  214. data/lib/ratatui_ruby/widgets/center.rb +10 -2
  215. data/lib/ratatui_ruby/widgets/chart.rb +2 -28
  216. data/lib/ratatui_ruby/widgets/clear.rb +10 -2
  217. data/lib/ratatui_ruby/widgets/cursor.rb +10 -2
  218. data/lib/ratatui_ruby/widgets/gauge.rb +16 -2
  219. data/lib/ratatui_ruby/widgets/line_gauge.rb +16 -2
  220. data/lib/ratatui_ruby/widgets/list.rb +41 -2
  221. data/lib/ratatui_ruby/widgets/list_item.rb +10 -2
  222. data/lib/ratatui_ruby/widgets/overlay.rb +10 -2
  223. data/lib/ratatui_ruby/widgets/paragraph.rb +10 -2
  224. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -2
  225. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -2
  226. data/lib/ratatui_ruby/widgets/row.rb +10 -2
  227. data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -2
  228. data/lib/ratatui_ruby/widgets/shape/label.rb +10 -2
  229. data/lib/ratatui_ruby/widgets/sparkline.rb +10 -2
  230. data/lib/ratatui_ruby/widgets/table.rb +62 -2
  231. data/lib/ratatui_ruby/widgets/tabs.rb +2 -2
  232. data/lib/ratatui_ruby/widgets.rb +2 -2
  233. data/lib/ratatui_ruby.rb +90 -2
  234. data/sig/examples/app_all_events/view.rbs +7 -1
  235. data/sig/examples/app_all_events/view_state.rbs +7 -1
  236. data/sig/examples/app_color_picker/app.rbs +5 -0
  237. data/sig/examples/app_stateful_interaction/app.rbs +7 -1
  238. data/sig/examples/verify_quickstart_dsl/app.rbs +7 -1
  239. data/sig/examples/verify_quickstart_lifecycle/app.rbs +7 -1
  240. data/sig/examples/verify_readme_usage/app.rbs +7 -1
  241. data/sig/examples/widget_block_demo/app.rbs +6 -0
  242. data/sig/examples/widget_box_demo/app.rbs +7 -1
  243. data/sig/examples/widget_calendar_demo/app.rbs +7 -1
  244. data/sig/examples/widget_cell_demo/app.rbs +7 -1
  245. data/sig/examples/widget_chart_demo/app.rbs +7 -1
  246. data/sig/examples/widget_gauge_demo/app.rbs +7 -1
  247. data/sig/examples/widget_layout_split/app.rbs +7 -1
  248. data/sig/examples/widget_line_gauge_demo/app.rbs +7 -1
  249. data/sig/examples/widget_list_demo/app.rbs +5 -0
  250. data/sig/examples/widget_map_demo/app.rbs +7 -1
  251. data/sig/examples/widget_popup_demo/app.rbs +7 -1
  252. data/sig/examples/widget_ratatui_logo_demo/app.rbs +7 -1
  253. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +7 -1
  254. data/sig/examples/widget_rect/app.rbs +7 -1
  255. data/sig/examples/widget_render/app.rbs +7 -1
  256. data/sig/examples/widget_rich_text/app.rbs +7 -1
  257. data/sig/examples/widget_scroll_text/app.rbs +7 -1
  258. data/sig/examples/widget_scrollbar_demo/app.rbs +7 -1
  259. data/sig/examples/widget_sparkline_demo/app.rbs +7 -1
  260. data/sig/examples/widget_style_colors/app.rbs +7 -1
  261. data/sig/examples/widget_table_demo/app.rbs +7 -1
  262. data/sig/examples/widget_text_width/app.rbs +7 -1
  263. data/sig/ratatui_ruby/event.rbs +7 -1
  264. data/sig/ratatui_ruby/frame.rbs +15 -3
  265. data/sig/ratatui_ruby/list_state.rbs +11 -1
  266. data/sig/ratatui_ruby/ratatui_ruby.rbs +8 -2
  267. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +7 -1
  268. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +6 -0
  269. data/sig/ratatui_ruby/schema/bar_chart.rbs +6 -0
  270. data/sig/ratatui_ruby/schema/block.rbs +7 -1
  271. data/sig/ratatui_ruby/schema/calendar.rbs +6 -0
  272. data/sig/ratatui_ruby/schema/canvas.rbs +6 -0
  273. data/sig/ratatui_ruby/schema/center.rbs +6 -0
  274. data/sig/ratatui_ruby/schema/chart.rbs +6 -9
  275. data/sig/ratatui_ruby/schema/constraint.rbs +6 -0
  276. data/sig/ratatui_ruby/schema/cursor.rbs +6 -0
  277. data/sig/ratatui_ruby/schema/draw.rbs +6 -0
  278. data/sig/ratatui_ruby/schema/gauge.rbs +9 -1
  279. data/sig/ratatui_ruby/schema/layout.rbs +6 -0
  280. data/sig/ratatui_ruby/schema/line_gauge.rbs +9 -1
  281. data/sig/ratatui_ruby/schema/list.rbs +9 -1
  282. data/sig/ratatui_ruby/schema/list_item.rbs +7 -1
  283. data/sig/ratatui_ruby/schema/overlay.rbs +6 -0
  284. data/sig/ratatui_ruby/schema/paragraph.rbs +6 -0
  285. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +6 -0
  286. data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +5 -0
  287. data/sig/ratatui_ruby/schema/rect.rbs +30 -0
  288. data/sig/ratatui_ruby/schema/row.rbs +7 -1
  289. data/sig/ratatui_ruby/schema/scrollbar.rbs +6 -0
  290. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -0
  291. data/sig/ratatui_ruby/schema/style.rbs +7 -1
  292. data/sig/ratatui_ruby/schema/table.rbs +11 -1
  293. data/sig/ratatui_ruby/schema/tabs.rbs +6 -0
  294. data/sig/ratatui_ruby/schema/text.rbs +7 -1
  295. data/sig/ratatui_ruby/scrollbar_state.rbs +7 -1
  296. data/sig/ratatui_ruby/session.rbs +7 -1
  297. data/sig/ratatui_ruby/table_state.rbs +7 -1
  298. data/sig/ratatui_ruby/test_helper/event_injection.rbs +7 -1
  299. data/sig/ratatui_ruby/test_helper/snapshot.rbs +7 -1
  300. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +7 -1
  301. data/sig/ratatui_ruby/test_helper/terminal.rbs +7 -1
  302. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -1
  303. data/sig/ratatui_ruby/test_helper.rbs +7 -1
  304. data/sig/ratatui_ruby/tui/buffer_factories.rbs +7 -1
  305. data/sig/ratatui_ruby/tui/canvas_factories.rbs +7 -1
  306. data/sig/ratatui_ruby/tui/core.rbs +7 -1
  307. data/sig/ratatui_ruby/tui/layout_factories.rbs +7 -1
  308. data/sig/ratatui_ruby/tui/state_factories.rbs +7 -1
  309. data/sig/ratatui_ruby/tui/style_factories.rbs +7 -1
  310. data/sig/ratatui_ruby/tui/text_factories.rbs +7 -1
  311. data/sig/ratatui_ruby/tui/widget_factories.rbs +7 -1
  312. data/sig/ratatui_ruby/tui.rbs +7 -1
  313. data/sig/ratatui_ruby/version.rbs +6 -0
  314. data/tasks/autodoc/examples.rb +1 -1
  315. data/tasks/autodoc/member.rb +1 -1
  316. data/tasks/autodoc/name.rb +1 -1
  317. data/tasks/bump/cargo_lockfile.rb +1 -1
  318. data/tasks/bump/changelog.rb +1 -1
  319. data/tasks/bump/header.rb +1 -1
  320. data/tasks/bump/history.rb +1 -1
  321. data/tasks/bump/links.rb +1 -1
  322. data/tasks/bump/manifest.rb +1 -1
  323. data/tasks/bump/ruby_gem.rb +1 -1
  324. data/tasks/bump/sem_ver.rb +1 -1
  325. data/tasks/bump/unreleased_section.rb +1 -1
  326. data/tasks/license/headers_md.rb +223 -0
  327. data/tasks/license/headers_rb.rb +210 -0
  328. data/tasks/license/license_utils.rb +130 -0
  329. data/tasks/license/snippets_md.rb +315 -0
  330. data/tasks/license/snippets_rdoc.rb +150 -0
  331. data/tasks/license.rake +91 -0
  332. data/tasks/rdoc_config.rb +1 -1
  333. data/tasks/resources/build.yml.erb +13 -7
  334. data/tasks/sourcehut.rake +3 -1
  335. data/tasks/terminal_preview/app_screenshot.rb +1 -1
  336. data/tasks/terminal_preview/crash_report.rb +1 -1
  337. data/tasks/terminal_preview/example_app.rb +1 -1
  338. data/tasks/terminal_preview/launcher_script.rb +1 -1
  339. data/tasks/terminal_preview/preview_collection.rb +1 -1
  340. data/tasks/terminal_preview/preview_timing.rb +1 -1
  341. data/tasks/terminal_preview/safety_confirmation.rb +1 -1
  342. data/tasks/terminal_preview/saved_screenshot.rb +1 -1
  343. data/tasks/terminal_preview/system_appearance.rb +1 -1
  344. data/tasks/terminal_preview/terminal_window.rb +1 -1
  345. data/tasks/terminal_preview/window_id.rb +1 -1
  346. data/tasks/website/index_page.rb +1 -1
  347. data/tasks/website/version.rb +1 -1
  348. data/tasks/website/version_menu.rb +1 -1
  349. data/tasks/website/versioned_documentation.rb +1 -1
  350. data/tasks/website/website.rb +1 -1
  351. metadata +13 -3
  352. data/doc/migration/v0_7_0.md +0 -236
@@ -1,7 +1,6 @@
1
1
  <!--
2
- SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
-
4
- SPDX-License-Identifier: AGPL-3.0-or-later
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
4
  -->
6
5
 
7
6
  # Application Architecture
@@ -24,6 +23,11 @@ Terminals have state. They remember cursor positions, input modes, and screen bu
24
23
 
25
24
  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.
26
25
 
26
+ <!-- SPDX-SnippetBegin -->
27
+ <!--
28
+ SPDX-FileCopyrightText: 2026 Kerrick Long
29
+ SPDX-License-Identifier: MIT-0
30
+ -->
27
31
  ```ruby
28
32
  RatatuiRuby.run do |tui|
29
33
  loop do
@@ -35,11 +39,17 @@ RatatuiRuby.run do |tui|
35
39
  end
36
40
  # Terminal is restored here
37
41
  ```
42
+ <!-- SPDX-SnippetEnd -->
38
43
 
39
44
  #### Manual Management
40
45
 
41
46
  Need granular control? You can initialize and restore the terminal yourself. Use `ensure` blocks to guarantee cleanup.
42
47
 
48
+ <!-- SPDX-SnippetBegin -->
49
+ <!--
50
+ SPDX-FileCopyrightText: 2026 Kerrick Long
51
+ SPDX-License-Identifier: MIT-0
52
+ -->
43
53
  ```ruby
44
54
  RatatuiRuby.init_terminal
45
55
  begin
@@ -51,6 +61,7 @@ ensure
51
61
  # Terminal is restored here
52
62
  end
53
63
  ```
64
+ <!-- SPDX-SnippetEnd -->
54
65
 
55
66
  #### Signal Handling
56
67
 
@@ -69,6 +80,11 @@ External processes send signals. Your TUI must handle them gracefully.
69
80
  > [!IMPORTANT]
70
81
  > **Ctrl+C in Raw Mode:** When your app is in raw mode, pressing Ctrl+C does *not* send SIGINT. It's captured as a `:ctrl_c` key event. Handle this in your event loop—don't use `trap("INT")`.
71
82
 
83
+ <!-- SPDX-SnippetBegin -->
84
+ <!--
85
+ SPDX-FileCopyrightText: 2026 Kerrick Long
86
+ SPDX-License-Identifier: MIT-0
87
+ -->
72
88
  ```ruby
73
89
  RatatuiRuby.run do |tui|
74
90
  loop do
@@ -78,6 +94,7 @@ RatatuiRuby.run do |tui|
78
94
  end
79
95
  end
80
96
  ```
97
+ <!-- SPDX-SnippetEnd -->
81
98
 
82
99
  **Recovery:** If a TUI app leaves your terminal broken, run `reset` in the shell to restore normal behavior.
83
100
 
@@ -97,6 +114,11 @@ Most widgets are stateless configuration. You create them, render them, and they
97
114
 
98
115
  **Use Case:** When you need to read back the scroll offset (e.g., for mouse hit testing) or persist selection without managing indexes manually.
99
116
 
117
+ <!-- SPDX-SnippetBegin -->
118
+ <!--
119
+ SPDX-FileCopyrightText: 2026 Kerrick Long
120
+ SPDX-License-Identifier: MIT-0
121
+ -->
100
122
  ```ruby
101
123
  # Initialize state once
102
124
  @list_state = RatatuiRuby::ListState.new
@@ -116,6 +138,7 @@ RatatuiRuby.run do |tui|
116
138
  end
117
139
  end
118
140
  ```
141
+ <!-- SPDX-SnippetEnd -->
119
142
 
120
143
  ### API Convenience
121
144
 
@@ -125,6 +148,11 @@ Writing UI trees involves nesting many widgets.
125
148
 
126
149
  **The Solution:** The TUI API (`tui`) provides shorthand factories for every widget. It yields a TUI object to your block.
127
150
 
151
+ <!-- SPDX-SnippetBegin -->
152
+ <!--
153
+ SPDX-FileCopyrightText: 2026 Kerrick Long
154
+ SPDX-License-Identifier: MIT-0
155
+ -->
128
156
  ```ruby
129
157
  RatatuiRuby.run do |tui|
130
158
  loop do
@@ -167,11 +195,17 @@ RatatuiRuby.run do |tui|
167
195
  end
168
196
  end
169
197
  ```
198
+ <!-- SPDX-SnippetEnd -->
170
199
 
171
200
  #### Raw API
172
201
 
173
202
  Building your own abstractions? You might prefer explicit class instantiation. The raw constants are always available.
174
203
 
204
+ <!-- SPDX-SnippetBegin -->
205
+ <!--
206
+ SPDX-FileCopyrightText: 2026 Kerrick Long
207
+ SPDX-License-Identifier: MIT-0
208
+ -->
175
209
  ```ruby
176
210
  RatatuiRuby.run do
177
211
  loop do
@@ -212,6 +246,7 @@ RatatuiRuby.run do
212
246
  end
213
247
  end
214
248
  ```
249
+ <!-- SPDX-SnippetEnd -->
215
250
 
216
251
  ## Thread and Ractor Safety
217
252
 
@@ -236,6 +271,11 @@ These have side effects and are intentionally not shareable:
236
271
  | `TUI` | Cache in `@tui` during run loop. Don't include in Models. |
237
272
  | `Frame` | Pass to helpers during draw block. Invalid after block returns. |
238
273
 
274
+ <!-- SPDX-SnippetBegin -->
275
+ <!--
276
+ SPDX-FileCopyrightText: 2026 Kerrick Long
277
+ SPDX-License-Identifier: MIT-0
278
+ -->
239
279
  ```ruby
240
280
  # Good: Cache session in instance variable
241
281
  RatatuiRuby.run do |tui|
@@ -246,6 +286,7 @@ end
246
286
  # Bad: Include in immutable Model (won't work with Ractors)
247
287
  Model = Data.define(:tui, :count) # Don't do this
248
288
  ```
289
+ <!-- SPDX-SnippetEnd -->
249
290
 
250
291
 
251
292
  ## Reference Architectures
@@ -1,5 +1,5 @@
1
1
  <!--
2
- SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
3
  SPDX-License-Identifier: CC-BY-SA-4.0
4
4
  -->
5
5
  # Application Testing Guide
@@ -18,19 +18,31 @@ Use it to write fast, deterministic tests for your TUI applications.
18
18
 
19
19
  First, require the test helper in your test file or `test_helper.rb`:
20
20
 
21
+ <!-- SPDX-SnippetBegin -->
22
+ <!--
23
+ SPDX-FileCopyrightText: 2025 Kerrick Long
24
+ SPDX-License-Identifier: MIT-0
25
+ -->
21
26
  ```ruby
22
27
  require "ratatui_ruby/test_helper"
23
28
  require "minitest/autorun" # or your preferred test framework
24
29
  ```
30
+ <!-- SPDX-SnippetEnd -->
25
31
 
26
32
  Then, include the module in your test class:
27
33
 
34
+ <!-- SPDX-SnippetBegin -->
35
+ <!--
36
+ SPDX-FileCopyrightText: 2025 Kerrick Long
37
+ SPDX-License-Identifier: MIT-0
38
+ -->
28
39
  ```ruby
29
40
  class MyApplicationTest < Minitest::Test
30
41
  include RatatuiRuby::TestHelper
31
42
  # ...
32
43
  end
33
44
  ```
45
+ <!-- SPDX-SnippetEnd -->
34
46
 
35
47
  ## Writing a View Test
36
48
 
@@ -40,6 +52,11 @@ To test a view or widget, wrap your assertions in `with_test_terminal`. This set
40
52
  2. **Render your code:** Instantiate your widget and draw it to a frame.
41
53
  3. **Assert output:** Check the `buffer_content` against your expectations.
42
54
 
55
+ <!-- SPDX-SnippetBegin -->
56
+ <!--
57
+ SPDX-FileCopyrightText: 2026 Kerrick Long
58
+ SPDX-License-Identifier: MIT-0
59
+ -->
43
60
  ```ruby
44
61
  def test_rendering
45
62
  # Uses default 80x24 terminal
@@ -57,6 +74,7 @@ def test_rendering
57
74
  end
58
75
  end
59
76
  ```
77
+ <!-- SPDX-SnippetEnd -->
60
78
 
61
79
  For the full API list, including `buffer_content` and `cursor_position`, see [RatatuiRuby::TestHelper::Terminal](../lib/ratatui_ruby/test_helper/terminal.rb).
62
80
 
@@ -66,6 +84,11 @@ You often need to check colors and modifiers (bold, italic) to ensure your highl
66
84
 
67
85
  Use `assert_fg_color`, `assert_bg_color`, and modifier helpers like `assert_bold`.
68
86
 
87
+ <!-- SPDX-SnippetBegin -->
88
+ <!--
89
+ SPDX-FileCopyrightText: 2026 Kerrick Long
90
+ SPDX-License-Identifier: MIT-0
91
+ -->
69
92
  ```ruby
70
93
  # Assert specific cell style
71
94
  assert_fg_color(:red, 0, 0)
@@ -74,6 +97,7 @@ assert_bold(0, 0)
74
97
  # Or check a whole area
75
98
  assert_area_style({ x: 0, y: 0, w: 10, h: 1 }, bg: :blue)
76
99
  ```
100
+ <!-- SPDX-SnippetEnd -->
77
101
 
78
102
  See [RatatuiRuby::TestHelper::StyleAssertions](../lib/ratatui_ruby/test_helper/style_assertions.rb) for the comprehensive list of style helpers.
79
103
 
@@ -86,6 +110,11 @@ Use `inject_event` to push mock events into the queue. This ensures safe, determ
86
110
  > [!IMPORTANT]
87
111
  > Call `inject_event` inside a `with_test_terminal` block to avoid race conditions.
88
112
 
113
+ <!-- SPDX-SnippetBegin -->
114
+ <!--
115
+ SPDX-FileCopyrightText: 2026 Kerrick Long
116
+ SPDX-License-Identifier: MIT-0
117
+ -->
89
118
  ```ruby
90
119
  with_test_terminal do
91
120
  # Simulate 'q' key press
@@ -96,6 +125,7 @@ with_test_terminal do
96
125
  assert_equal "q", event.code
97
126
  end
98
127
  ```
128
+ <!-- SPDX-SnippetEnd -->
99
129
 
100
130
  See [RatatuiRuby::TestHelper::EventInjection](../lib/ratatui_ruby/test_helper/event_injection.rb) for helper methods like `inject_keys` and `inject_click`.
101
131
 
@@ -105,12 +135,18 @@ Snapshots let you verify complex layouts without manually asserting every line.
105
135
 
106
136
  Use `assert_snapshots` to compare the current screen against stored reference files.
107
137
 
138
+ <!-- SPDX-SnippetBegin -->
139
+ <!--
140
+ SPDX-FileCopyrightText: 2026 Kerrick Long
141
+ SPDX-License-Identifier: MIT-0
142
+ -->
108
143
  ```ruby
109
144
  with_test_terminal do
110
145
  MyApp.new.run
111
146
  assert_snapshots("dashboard_view")
112
147
  end
113
148
  ```
149
+ <!-- SPDX-SnippetEnd -->
114
150
 
115
151
  This generates both `.txt` (plain text) and `.ansi` (styled) snapshot files. The `.ansi` files contain ANSI escape codes—`cat` them in a terminal to see exactly what the screen looked like. For a visual tour of your test suite, try `cat **/*.ansi` in any shell that supports globbing.
116
152
 
@@ -130,6 +166,11 @@ Sometimes you want to test a single view component without spinning up the full
130
166
 
131
167
  Use `MockFrame` and `StubRect` to test render logic in isolation.
132
168
 
169
+ <!-- SPDX-SnippetBegin -->
170
+ <!--
171
+ SPDX-FileCopyrightText: 2026 Kerrick Long
172
+ SPDX-License-Identifier: MIT-0
173
+ -->
133
174
  ```ruby
134
175
  def test_logs_view
135
176
  frame = RatatuiRuby::TestHelper::TestDoubles::MockFrame.new
@@ -143,6 +184,7 @@ def test_logs_view
143
184
  assert_equal "Logs", rendered[:widget].block.title
144
185
  end
145
186
  ```
187
+ <!-- SPDX-SnippetEnd -->
146
188
 
147
189
  See [RatatuiRuby::TestHelper::TestDoubles](../lib/ratatui_ruby/test_helper/test_doubles.rb).
148
190
 
@@ -1,6 +1,6 @@
1
1
  <!--
2
- SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: AGPL-3.0-or-later
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
4
  -->
5
5
 
6
6
  # Async Operations in TUI Applications
@@ -21,12 +21,18 @@ This guide explains async patterns that work with raw terminal mode.
21
21
 
22
22
  ### What Breaks
23
23
 
24
+ <!-- SPDX-SnippetBegin -->
25
+ <!--
26
+ SPDX-FileCopyrightText: 2026 Kerrick Long
27
+ SPDX-License-Identifier: MIT-0
28
+ -->
24
29
  ```ruby
25
30
  # These fail inside a Thread during raw mode:
26
31
  `git ls-remote --tags origin` # Returns empty or hangs
27
32
  IO.popen(["git", "ls-remote", ...]) # Same
28
33
  Open3.capture2("git", "ls-remote", ...) # Same
29
34
  ```
35
+ <!-- SPDX-SnippetEnd -->
30
36
 
31
37
  The commands succeed synchronously. They fail asynchronously. The difference: thread context inherits the parent's raw terminal state.
32
38
 
@@ -46,11 +52,17 @@ Ruby's GIL releases during I/O. But:
46
52
 
47
53
  Run slow operations before entering the TUI:
48
54
 
55
+ <!-- SPDX-SnippetBegin -->
56
+ <!--
57
+ SPDX-FileCopyrightText: 2026 Kerrick Long
58
+ SPDX-License-Identifier: MIT-0
59
+ -->
49
60
  ```ruby
50
61
  def initialize
51
62
  @data = fetch_data # Runs before RatatuiRuby.run
52
63
  end
53
64
  ```
65
+ <!-- SPDX-SnippetEnd -->
54
66
 
55
67
  **Trade-off**: Delays startup.
56
68
 
@@ -58,6 +70,11 @@ end
58
70
 
59
71
  Spawn a separate process before entering raw mode. Write results to a temp file. Poll for completion:
60
72
 
73
+ <!-- SPDX-SnippetBegin -->
74
+ <!--
75
+ SPDX-FileCopyrightText: 2026 Kerrick Long
76
+ SPDX-License-Identifier: MIT-0
77
+ -->
61
78
  ```ruby
62
79
  class AsyncChecker
63
80
  CACHE_FILE = File.join(Dir.tmpdir, "my_check_result.txt")
@@ -81,6 +98,7 @@ class AsyncChecker
81
98
  end
82
99
  end
83
100
  ```
101
+ <!-- SPDX-SnippetEnd -->
84
102
 
85
103
  **Key points**:
86
104
 
@@ -93,9 +111,15 @@ end
93
111
 
94
112
  Ruby threads work for pure computation:
95
113
 
114
+ <!-- SPDX-SnippetBegin -->
115
+ <!--
116
+ SPDX-FileCopyrightText: 2026 Kerrick Long
117
+ SPDX-License-Identifier: MIT-0
118
+ -->
96
119
  ```ruby
97
120
  Thread.new { @result = expensive_calculation }
98
121
  ```
122
+ <!-- SPDX-SnippetEnd -->
99
123
 
100
124
  Avoid threads for shell commands.
101
125
 
@@ -122,6 +146,11 @@ For TUI async, `Process.spawn` solves the problem cleanly.
122
146
 
123
147
  Check if a tag exists on the remote:
124
148
 
149
+ <!-- SPDX-SnippetBegin -->
150
+ <!--
151
+ SPDX-FileCopyrightText: 2026 Kerrick Long
152
+ SPDX-License-Identifier: MIT-0
153
+ -->
125
154
  ```ruby
126
155
  class GitRepo
127
156
  CACHE_FILE = File.join(Dir.tmpdir, "git_tag_pushed.txt")
@@ -156,5 +185,6 @@ class GitRepo
156
185
  end
157
186
  end
158
187
  ```
188
+ <!-- SPDX-SnippetEnd -->
159
189
 
160
190
  The TUI starts instantly. The tag check runs in the background. The checklist updates when the result arrives.
@@ -0,0 +1,247 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Custom Widgets
7
+
8
+ Build anything. Escape the widget library.
9
+
10
+ ## What Terminals Offer
11
+
12
+ Terminals do not have pixels. They have character cells arranged in a grid. Each cell holds one character with foreground color, background color, and text modifiers (bold, italic, underline).
13
+
14
+ This constraint shapes what you can draw:
15
+
16
+ - **Characters**: Any Unicode character fits in a cell
17
+ - **Box-drawing**: Lines, corners, and boxes (`│`, `┌`, `─`, `└`)
18
+ - **Block elements**: Partial fills (`▀`, `▄`, `█`, `░`, `▒`, `▓`)
19
+ - **Braille patterns**: 2×4 "pixel" grids per cell for pseudo-graphics
20
+ - **Nerd Fonts**: Icons and glyphs if the user's font supports them
21
+
22
+ The built-in Canvas widget uses Braille patterns for line graphs and shapes. Custom widgets give you direct control over every cell.
23
+
24
+ ## The Problem
25
+
26
+ Standard widgets handle common needs. Paragraphs display text. Lists show selections. Tables organize data.
27
+
28
+ But terminals can do more. You want a game board, a network graph, or a custom visualization. The built-in widgets cannot help you here.
29
+
30
+ ## The Solution
31
+
32
+ Any Ruby object that implements `render(area)` works as a widget. You are not limited to what the library ships. Define a class. Implement one method. Pass it to `frame.render_widget`.
33
+
34
+ The Engine calls your `render` method with the area where your widget should draw. You return an array of Draw commands. The Engine executes them.
35
+
36
+ ## The Contract
37
+
38
+ Your custom widget implements [the `_CustomWidget` interface](../../sig/ratatui_ruby/frame.rbs). The `area` parameter is a `Rect` with `x`, `y`, `width`, and `height`. It tells you where to draw and how much space you have.
39
+
40
+ ## Draw Commands
41
+
42
+ Two commands describe what to draw:
43
+
44
+ | Command | Purpose |
45
+ |---------|---------|
46
+ | `Draw.string(x, y, text, style)` | Draw a styled string at absolute coordinates |
47
+ | `Draw.cell(x, y, cell)` | Draw a single cell (character + style) |
48
+
49
+ <!-- SPDX-SnippetBegin -->
50
+ <!--
51
+ SPDX-FileCopyrightText: 2026 Kerrick Long
52
+ SPDX-License-Identifier: MIT-0
53
+ -->
54
+ ```ruby
55
+ class HelloWidget
56
+ def render(area)
57
+ [
58
+ RatatuiRuby::Draw.string(
59
+ area.x,
60
+ area.y,
61
+ "Hello, World!",
62
+ RatatuiRuby::Style::Style.new(fg: :green, modifiers: [:bold])
63
+ )
64
+ ]
65
+ end
66
+ end
67
+ ```
68
+ <!-- SPDX-SnippetEnd -->
69
+
70
+ ## Coordinate Offsets
71
+
72
+ The `area.x` and `area.y` values are not always zero. When your widget renders inside a `Block` with borders, or within a nested layout, the area's origin shifts.
73
+
74
+ Always add `area.x` and `area.y` to your drawing coordinates. This pattern ensures your widget works regardless of where it appears on screen.
75
+
76
+ <!-- SPDX-SnippetBegin -->
77
+ <!--
78
+ SPDX-FileCopyrightText: 2026 Kerrick Long
79
+ SPDX-License-Identifier: MIT-0
80
+ -->
81
+ ```ruby
82
+ class DiagonalWidget
83
+ def render(area)
84
+ (0...area.height).filter_map do |i|
85
+ next if i >= area.width # Stay within bounds
86
+
87
+ RatatuiRuby::Draw.string(
88
+ area.x + i, # Offset from area origin
89
+ area.y + i,
90
+ "\\",
91
+ RatatuiRuby::Style::Style.new(fg: :red)
92
+ )
93
+ end
94
+ end
95
+ end
96
+ ```
97
+ <!-- SPDX-SnippetEnd -->
98
+
99
+ ## Composability
100
+
101
+ Custom widgets compose with standard widgets. Wrap them in Blocks. Place them in layouts. Mix them with Paragraphs and Lists.
102
+
103
+ <!-- SPDX-SnippetBegin -->
104
+ <!--
105
+ SPDX-FileCopyrightText: 2026 Kerrick Long
106
+ SPDX-License-Identifier: MIT-0
107
+ -->
108
+ ```ruby
109
+ RatatuiRuby.run do |tui|
110
+ tui.draw do |frame|
111
+ areas = tui.layout_split(
112
+ frame.area,
113
+ direction: :horizontal,
114
+ constraints: [tui.constraint_percentage(50), tui.constraint_percentage(50)]
115
+ )
116
+
117
+ # Standard widget on the left
118
+ frame.render_widget(tui.paragraph(text: "Standard"), areas[0])
119
+
120
+ # Custom widget on the right
121
+ frame.render_widget(DiagonalWidget.new, areas[1])
122
+ end
123
+ end
124
+ ```
125
+ <!-- SPDX-SnippetEnd -->
126
+
127
+ To render inside a bordered Block, calculate the inner area first:
128
+
129
+ <!-- SPDX-SnippetBegin -->
130
+ <!--
131
+ SPDX-FileCopyrightText: 2026 Kerrick Long
132
+ SPDX-License-Identifier: MIT-0
133
+ -->
134
+ ```ruby
135
+ tui.draw do |frame|
136
+ # Render the block frame
137
+ block = tui.block(title: "Custom", borders: [:all])
138
+ frame.render_widget(block, frame.area)
139
+
140
+ # Calculate inner area (1-cell border on all sides)
141
+ inner = tui.rect(
142
+ x: frame.area.x + 1,
143
+ y: frame.area.y + 1,
144
+ width: [frame.area.width - 2, 0].max,
145
+ height: [frame.area.height - 2, 0].max
146
+ )
147
+
148
+ # Render custom widget inside
149
+ frame.render_widget(MyWidget.new, inner)
150
+ end
151
+ ```
152
+ <!-- SPDX-SnippetEnd -->
153
+
154
+ ## Using Custom Widgets in Layouts
155
+
156
+ Custom widgets work as children in Layout trees. The layout system passes the calculated area to your `render` method.
157
+
158
+ <!-- SPDX-SnippetBegin -->
159
+ <!--
160
+ SPDX-FileCopyrightText: 2026 Kerrick Long
161
+ SPDX-License-Identifier: MIT-0
162
+ -->
163
+ ```ruby
164
+ layout = RatatuiRuby::Layout::Layout.new(
165
+ direction: :vertical,
166
+ constraints: [
167
+ RatatuiRuby::Layout::Constraint.length(1),
168
+ RatatuiRuby::Layout::Constraint.fill(1),
169
+ ],
170
+ children: [
171
+ RatatuiRuby::Widgets::Paragraph.new(text: "Header"),
172
+ MyCustomWidget.new, # Your widget here
173
+ ]
174
+ )
175
+
176
+ RatatuiRuby.draw(layout)
177
+ ```
178
+ <!-- SPDX-SnippetEnd -->
179
+
180
+ ## Testing Custom Widgets
181
+
182
+ Custom widgets return arrays. Test them by calling `render` directly and asserting on the result.
183
+
184
+ <!-- SPDX-SnippetBegin -->
185
+ <!--
186
+ SPDX-FileCopyrightText: 2026 Kerrick Long
187
+ SPDX-License-Identifier: MIT-0
188
+ -->
189
+ ```ruby
190
+ def test_hello_widget_output
191
+ area = RatatuiRuby::Rect.new(x: 0, y: 0, width: 20, height: 5)
192
+ widget = HelloWidget.new
193
+ commands = widget.render(area)
194
+
195
+ assert_equal 1, commands.length
196
+ assert_equal 0, commands[0].x
197
+ assert_equal 0, commands[0].y
198
+ assert_equal "Hello, World!", commands[0].string
199
+ end
200
+ ```
201
+ <!-- SPDX-SnippetEnd -->
202
+
203
+ For visual testing, use the test helper to render to a buffer and assert on content:
204
+
205
+ <!-- SPDX-SnippetBegin -->
206
+ <!--
207
+ SPDX-FileCopyrightText: 2026 Kerrick Long
208
+ SPDX-License-Identifier: MIT-0
209
+ -->
210
+ ```ruby
211
+ class TestMyWidget < Minitest::Test
212
+ include RatatuiRuby::TestHelper
213
+
214
+ def test_renders_in_terminal
215
+ with_test_terminal(10, 5) do
216
+ RatatuiRuby.draw(MyWidget.new)
217
+ assert_equal "Expected ", buffer_content[0]
218
+ end
219
+ end
220
+ end
221
+ ```
222
+ <!-- SPDX-SnippetEnd -->
223
+
224
+ ## Typing Your Widgets (RBS)
225
+
226
+ Type your custom widgets by implementing the `_CustomWidget` interface:
227
+
228
+ <!-- SPDX-SnippetBegin -->
229
+ <!--
230
+ SPDX-FileCopyrightText: 2026 Kerrick Long
231
+ SPDX-License-Identifier: MIT-0
232
+ -->
233
+ ```rbs
234
+ # my_widget.rbs
235
+ class MyWidget
236
+ def render: (RatatuiRuby::Rect area) -> Array[RatatuiRuby::Draw::StringCmd | RatatuiRuby::Draw::CellCmd]
237
+ end
238
+ ```
239
+ <!-- SPDX-SnippetEnd -->
240
+
241
+ The interface uses structural typing. Any class with a matching `render` signature satisfies it.
242
+
243
+ ## Related Resources
244
+
245
+ - [Custom Render Example](../examples/widget_render/README.md) — Full working example
246
+ - [Cell Example](../examples/widget_cell/README.md) — Low-level cell drawing
247
+ - [Application Testing](./application_testing.md) — Test helper reference