ratatui_ruby 0.3.1 → 0.5.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 (350) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +89 -132
  7. data/CHANGELOG.md +223 -1
  8. data/README.md +23 -16
  9. data/REUSE.toml +20 -0
  10. data/doc/application_architecture.md +176 -0
  11. data/doc/application_testing.md +17 -10
  12. data/doc/contributors/design/ruby_frontend.md +11 -7
  13. data/doc/contributors/developing_examples.md +261 -0
  14. data/doc/contributors/documentation_style.md +104 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/index.md +2 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +125 -0
  19. data/doc/images/app_all_events.png +0 -0
  20. data/doc/images/app_analytics.png +0 -0
  21. data/doc/images/app_color_picker.png +0 -0
  22. data/doc/images/app_custom_widget.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_map_demo.png +0 -0
  25. data/doc/images/app_mouse_events.png +0 -0
  26. data/doc/images/app_table_select.png +0 -0
  27. data/doc/images/verify_quickstart_dsl.png +0 -0
  28. data/doc/images/verify_quickstart_layout.png +0 -0
  29. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  30. data/doc/images/verify_readme_usage.png +0 -0
  31. data/doc/images/widget_barchart_demo.png +0 -0
  32. data/doc/images/widget_block_padding.png +0 -0
  33. data/doc/images/widget_block_titles.png +0 -0
  34. data/doc/images/widget_box_demo.png +0 -0
  35. data/doc/images/widget_calendar_demo.png +0 -0
  36. data/doc/images/widget_cell_demo.png +0 -0
  37. data/doc/images/widget_chart_demo.png +0 -0
  38. data/doc/images/widget_gauge_demo.png +0 -0
  39. data/doc/images/widget_layout_split.png +0 -0
  40. data/doc/images/widget_line_gauge_demo.png +0 -0
  41. data/doc/images/widget_list_demo.png +0 -0
  42. data/doc/images/widget_list_styles.png +0 -0
  43. data/doc/images/widget_popup_demo.png +0 -0
  44. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  45. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  46. data/doc/images/widget_rect.png +0 -0
  47. data/doc/images/widget_render.png +0 -0
  48. data/doc/images/widget_rich_text.png +0 -0
  49. data/doc/images/widget_scroll_text.png +0 -0
  50. data/doc/images/widget_scrollbar_demo.png +0 -0
  51. data/doc/images/widget_sparkline_demo.png +0 -0
  52. data/doc/images/widget_style_colors.png +0 -0
  53. data/doc/images/widget_table_flex.png +0 -0
  54. data/doc/images/widget_tabs_demo.png +0 -0
  55. data/doc/index.md +1 -0
  56. data/doc/interactive_design.md +116 -0
  57. data/doc/quickstart.md +186 -84
  58. data/examples/app_all_events/README.md +81 -0
  59. data/examples/app_all_events/app.rb +93 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +75 -0
  62. data/examples/app_all_events/model/events.rb +180 -0
  63. data/examples/app_all_events/model/highlight.rb +57 -0
  64. data/examples/app_all_events/model/timestamp.rb +54 -0
  65. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +24 -0
  66. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +24 -0
  67. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +24 -0
  68. data/examples/app_all_events/test/snapshots/after_key_a.txt +24 -0
  69. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +24 -0
  70. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +24 -0
  71. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +24 -0
  72. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +24 -0
  73. data/examples/app_all_events/test/snapshots/after_paste.txt +24 -0
  74. data/examples/app_all_events/test/snapshots/after_resize.txt +24 -0
  75. data/examples/app_all_events/test/snapshots/after_right_click.txt +24 -0
  76. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +24 -0
  77. data/examples/app_all_events/test/snapshots/initial_state.txt +24 -0
  78. data/examples/app_all_events/view/app_view.rb +78 -0
  79. data/examples/app_all_events/view/controls_view.rb +50 -0
  80. data/examples/app_all_events/view/counts_view.rb +55 -0
  81. data/examples/app_all_events/view/live_view.rb +69 -0
  82. data/examples/app_all_events/view/log_view.rb +60 -0
  83. data/{lib/ratatui_ruby/output.rb → examples/app_all_events/view.rb} +1 -1
  84. data/examples/app_all_events/view_state.rb +42 -0
  85. data/examples/app_color_picker/README.md +94 -0
  86. data/examples/app_color_picker/app.rb +112 -0
  87. data/examples/app_color_picker/clipboard.rb +84 -0
  88. data/examples/app_color_picker/color.rb +191 -0
  89. data/examples/app_color_picker/copy_dialog.rb +170 -0
  90. data/examples/app_color_picker/harmony.rb +56 -0
  91. data/examples/app_color_picker/input.rb +142 -0
  92. data/examples/app_color_picker/palette.rb +80 -0
  93. data/examples/app_color_picker/scene.rb +201 -0
  94. data/examples/app_login_form/app.rb +108 -0
  95. data/examples/app_map_demo/app.rb +93 -0
  96. data/examples/app_table_select/app.rb +201 -0
  97. data/examples/verify_quickstart_dsl/app.rb +45 -0
  98. data/examples/verify_quickstart_layout/app.rb +69 -0
  99. data/examples/verify_quickstart_lifecycle/app.rb +48 -0
  100. data/examples/verify_readme_usage/app.rb +34 -0
  101. data/examples/widget_barchart_demo/app.rb +238 -0
  102. data/examples/widget_block_padding/app.rb +67 -0
  103. data/examples/widget_block_titles/app.rb +69 -0
  104. data/examples/widget_box_demo/app.rb +250 -0
  105. data/examples/widget_calendar_demo/app.rb +109 -0
  106. data/examples/widget_cell_demo/app.rb +104 -0
  107. data/examples/widget_chart_demo/app.rb +213 -0
  108. data/examples/widget_gauge_demo/app.rb +212 -0
  109. data/examples/widget_layout_split/app.rb +246 -0
  110. data/examples/widget_line_gauge_demo/app.rb +217 -0
  111. data/examples/widget_list_demo/app.rb +382 -0
  112. data/examples/widget_list_styles/app.rb +141 -0
  113. data/examples/widget_popup_demo/app.rb +104 -0
  114. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  115. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  116. data/examples/widget_rect/app.rb +205 -0
  117. data/examples/widget_render/app.rb +184 -0
  118. data/examples/widget_rich_text/app.rb +137 -0
  119. data/examples/widget_scroll_text/app.rb +108 -0
  120. data/examples/widget_scrollbar_demo/app.rb +153 -0
  121. data/examples/widget_sparkline_demo/app.rb +274 -0
  122. data/examples/widget_style_colors/app.rb +102 -0
  123. data/examples/widget_table_flex/app.rb +95 -0
  124. data/examples/widget_tabs_demo/app.rb +167 -0
  125. data/ext/ratatui_ruby/Cargo.lock +889 -115
  126. data/ext/ratatui_ruby/Cargo.toml +4 -3
  127. data/ext/ratatui_ruby/clippy.toml +7 -0
  128. data/ext/ratatui_ruby/extconf.rb +7 -0
  129. data/ext/ratatui_ruby/src/events.rs +293 -219
  130. data/ext/ratatui_ruby/src/frame.rs +115 -0
  131. data/ext/ratatui_ruby/src/lib.rs +105 -24
  132. data/ext/ratatui_ruby/src/rendering.rs +94 -10
  133. data/ext/ratatui_ruby/src/style.rs +357 -93
  134. data/ext/ratatui_ruby/src/terminal.rs +121 -31
  135. data/ext/ratatui_ruby/src/text.rs +178 -0
  136. data/ext/ratatui_ruby/src/widgets/barchart.rs +99 -24
  137. data/ext/ratatui_ruby/src/widgets/block.rs +32 -3
  138. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  139. data/ext/ratatui_ruby/src/widgets/canvas.rs +44 -9
  140. data/ext/ratatui_ruby/src/widgets/chart.rs +79 -27
  141. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  142. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  143. data/ext/ratatui_ruby/src/widgets/layout.rs +223 -15
  144. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  145. data/ext/ratatui_ruby/src/widgets/list.rs +114 -11
  146. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  147. data/ext/ratatui_ruby/src/widgets/overlay.rs +4 -2
  148. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  149. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  150. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +51 -0
  151. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +61 -7
  152. data/ext/ratatui_ruby/src/widgets/sparkline.rs +73 -6
  153. data/ext/ratatui_ruby/src/widgets/table.rs +177 -64
  154. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  155. data/lib/ratatui_ruby/cell.rb +166 -0
  156. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  157. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  158. data/lib/ratatui_ruby/event/key.rb +211 -0
  159. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  160. data/lib/ratatui_ruby/event/none.rb +43 -0
  161. data/lib/ratatui_ruby/event/paste.rb +71 -0
  162. data/lib/ratatui_ruby/event/resize.rb +80 -0
  163. data/lib/ratatui_ruby/event.rb +131 -0
  164. data/lib/ratatui_ruby/frame.rb +87 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  166. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +23 -0
  167. data/lib/ratatui_ruby/schema/bar_chart.rb +226 -17
  168. data/lib/ratatui_ruby/schema/block.rb +178 -11
  169. data/lib/ratatui_ruby/schema/calendar.rb +70 -14
  170. data/lib/ratatui_ruby/schema/canvas.rb +213 -46
  171. data/lib/ratatui_ruby/schema/center.rb +46 -8
  172. data/lib/ratatui_ruby/schema/chart.rb +134 -32
  173. data/lib/ratatui_ruby/schema/clear.rb +22 -53
  174. data/lib/ratatui_ruby/schema/constraint.rb +72 -12
  175. data/lib/ratatui_ruby/schema/cursor.rb +23 -5
  176. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  177. data/lib/ratatui_ruby/schema/gauge.rb +56 -12
  178. data/lib/ratatui_ruby/schema/layout.rb +91 -9
  179. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  180. data/lib/ratatui_ruby/schema/list.rb +92 -16
  181. data/lib/ratatui_ruby/schema/overlay.rb +29 -3
  182. data/lib/ratatui_ruby/schema/paragraph.rb +82 -25
  183. data/lib/ratatui_ruby/schema/ratatui_logo.rb +29 -0
  184. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +34 -0
  185. data/lib/ratatui_ruby/schema/rect.rb +59 -10
  186. data/lib/ratatui_ruby/schema/scrollbar.rb +127 -19
  187. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  188. data/lib/ratatui_ruby/schema/sparkline.rb +120 -12
  189. data/lib/ratatui_ruby/schema/style.rb +39 -11
  190. data/lib/ratatui_ruby/schema/table.rb +109 -18
  191. data/lib/ratatui_ruby/schema/tabs.rb +71 -10
  192. data/lib/ratatui_ruby/schema/text.rb +90 -0
  193. data/lib/ratatui_ruby/session/autodoc.rb +417 -0
  194. data/lib/ratatui_ruby/session.rb +163 -0
  195. data/lib/ratatui_ruby/test_helper.rb +322 -13
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby.rb +184 -38
  198. data/sig/examples/app_all_events/app.rbs +11 -0
  199. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  200. data/sig/examples/app_all_events/model/events.rbs +15 -0
  201. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  202. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  203. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  204. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  205. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  206. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  207. data/sig/examples/app_all_events/view.rbs +8 -0
  208. data/sig/examples/app_all_events/view_state.rbs +15 -0
  209. data/sig/examples/app_color_picker/app.rbs +12 -0
  210. data/sig/examples/app_login_form/app.rbs +11 -0
  211. data/sig/examples/app_map_demo/app.rbs +11 -0
  212. data/sig/examples/app_table_select/app.rbs +11 -0
  213. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  214. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  215. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  216. data/sig/examples/widget_block_padding/app.rbs +11 -0
  217. data/sig/examples/widget_block_titles/app.rbs +11 -0
  218. data/sig/examples/widget_box_demo/app.rbs +11 -0
  219. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  220. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  221. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  222. data/sig/examples/widget_gauge_demo/app.rbs +11 -0
  223. data/sig/examples/widget_layout_split/app.rbs +10 -0
  224. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  225. data/sig/examples/widget_list_demo/app.rbs +12 -0
  226. data/sig/examples/widget_list_styles/app.rbs +11 -0
  227. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  228. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  229. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  230. data/sig/examples/widget_rect/app.rbs +12 -0
  231. data/sig/examples/widget_render/app.rbs +10 -0
  232. data/sig/examples/widget_rich_text/app.rbs +11 -0
  233. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  234. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  235. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  236. data/sig/examples/widget_style_colors/app.rbs +14 -0
  237. data/sig/examples/widget_table_flex/app.rbs +11 -0
  238. data/sig/ratatui_ruby/event.rbs +69 -0
  239. data/sig/ratatui_ruby/frame.rbs +9 -0
  240. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -3
  241. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  242. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  243. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  244. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  245. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  246. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  247. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  248. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  249. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  250. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  251. data/sig/ratatui_ruby/schema/draw.rbs +27 -0
  252. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  253. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  254. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  255. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  256. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  257. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +8 -0
  258. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  259. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  260. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  261. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  262. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  263. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  264. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  265. data/sig/ratatui_ruby/session.rbs +94 -0
  266. data/tasks/autodoc/inventory.rb +61 -0
  267. data/tasks/autodoc/member.rb +56 -0
  268. data/tasks/autodoc/name.rb +19 -0
  269. data/tasks/autodoc/notice.rb +26 -0
  270. data/tasks/autodoc/rbs.rb +38 -0
  271. data/tasks/autodoc/rdoc.rb +45 -0
  272. data/tasks/autodoc.rake +47 -0
  273. data/tasks/bump/history.rb +2 -2
  274. data/tasks/doc.rake +600 -6
  275. data/tasks/example_viewer.html.erb +172 -0
  276. data/tasks/lint.rake +8 -4
  277. data/tasks/resources/build.yml.erb +13 -11
  278. data/tasks/resources/index.html.erb +6 -0
  279. data/tasks/sourcehut.rake +4 -4
  280. data/tasks/terminal_preview/app_screenshot.rb +33 -0
  281. data/tasks/terminal_preview/crash_report.rb +52 -0
  282. data/tasks/terminal_preview/example_app.rb +25 -0
  283. data/tasks/terminal_preview/launcher_script.rb +46 -0
  284. data/tasks/terminal_preview/preview_collection.rb +58 -0
  285. data/tasks/terminal_preview/preview_timing.rb +22 -0
  286. data/tasks/terminal_preview/safety_confirmation.rb +56 -0
  287. data/tasks/terminal_preview/saved_screenshot.rb +53 -0
  288. data/tasks/terminal_preview/system_appearance.rb +11 -0
  289. data/tasks/terminal_preview/terminal_window.rb +136 -0
  290. data/tasks/terminal_preview/window_id.rb +14 -0
  291. data/tasks/terminal_preview.rake +28 -0
  292. data/tasks/test.rake +2 -2
  293. data/tasks/website/index_page.rb +3 -3
  294. data/tasks/website/version.rb +10 -10
  295. data/tasks/website/version_menu.rb +10 -12
  296. data/tasks/website/versioned_documentation.rb +49 -17
  297. data/tasks/website/website.rb +6 -8
  298. data/tasks/website.rake +4 -4
  299. metadata +206 -54
  300. data/LICENSES/BSD-2-Clause.txt +0 -9
  301. data/doc/images/examples-analytics.rb.png +0 -0
  302. data/doc/images/examples-box_demo.rb.png +0 -0
  303. data/doc/images/examples-calendar_demo.rb.png +0 -0
  304. data/doc/images/examples-chart_demo.rb.png +0 -0
  305. data/doc/images/examples-custom_widget.rb.png +0 -0
  306. data/doc/images/examples-dashboard.rb.png +0 -0
  307. data/doc/images/examples-list_styles.rb.png +0 -0
  308. data/doc/images/examples-login_form.rb.png +0 -0
  309. data/doc/images/examples-map_demo.rb.png +0 -0
  310. data/doc/images/examples-mouse_events.rb.png +0 -0
  311. data/doc/images/examples-popup_demo.rb.gif +0 -0
  312. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  313. data/doc/images/examples-scroll_text.rb.png +0 -0
  314. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  315. data/doc/images/examples-stock_ticker.rb.png +0 -0
  316. data/doc/images/examples-system_monitor.rb.png +0 -0
  317. data/doc/images/examples-table_select.rb.png +0 -0
  318. data/examples/analytics.rb +0 -88
  319. data/examples/box_demo.rb +0 -71
  320. data/examples/calendar_demo.rb +0 -55
  321. data/examples/chart_demo.rb +0 -84
  322. data/examples/custom_widget.rb +0 -43
  323. data/examples/dashboard.rb +0 -72
  324. data/examples/list_styles.rb +0 -66
  325. data/examples/login_form.rb +0 -115
  326. data/examples/map_demo.rb +0 -58
  327. data/examples/mouse_events.rb +0 -95
  328. data/examples/popup_demo.rb +0 -105
  329. data/examples/quickstart_dsl.rb +0 -30
  330. data/examples/quickstart_lifecycle.rb +0 -40
  331. data/examples/readme_usage.rb +0 -21
  332. data/examples/scroll_text.rb +0 -74
  333. data/examples/scrollbar_demo.rb +0 -75
  334. data/examples/stock_ticker.rb +0 -93
  335. data/examples/system_monitor.rb +0 -94
  336. data/examples/table_select.rb +0 -70
  337. data/examples/test_analytics.rb +0 -65
  338. data/examples/test_box_demo.rb +0 -38
  339. data/examples/test_calendar_demo.rb +0 -66
  340. data/examples/test_dashboard.rb +0 -38
  341. data/examples/test_list_styles.rb +0 -61
  342. data/examples/test_login_form.rb +0 -63
  343. data/examples/test_map_demo.rb +0 -100
  344. data/examples/test_popup_demo.rb +0 -62
  345. data/examples/test_scroll_text.rb +0 -130
  346. data/examples/test_stock_ticker.rb +0 -39
  347. data/examples/test_system_monitor.rb +0 -40
  348. data/examples/test_table_select.rb +0 -37
  349. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  350. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+ require "minitest/mock"
5
+ require "fileutils"
6
+
3
7
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
8
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
9
 
@@ -8,7 +12,7 @@ module RatatuiRuby
8
12
  # Helpers for testing RatatuiRuby applications.
9
13
  #
10
14
  # This module provides methods to set up a test terminal, capture buffer content,
11
- # and check cursor position, making it easier to write unit tests for your TUI apps.
15
+ # and inject events, making it easier to write unit tests for your TUI apps.
12
16
  #
13
17
  # == Usage
14
18
  #
@@ -18,24 +22,49 @@ module RatatuiRuby
18
22
  # include RatatuiRuby::TestHelper
19
23
  #
20
24
  # def test_rendering
21
- # with_test_terminal(width: 80, height: 24) do
25
+ # with_test_terminal(80, 24) do
22
26
  # # ... render your app ...
23
27
  # assert_includes buffer_content, "Hello World"
24
28
  # end
25
29
  # end
30
+ #
31
+ # def test_key_handling
32
+ # inject_event(RatatuiRuby::Event::Key.new(code: "q"))
33
+ # result = @app.handle_input
34
+ # assert_equal :quit, result
35
+ # end
26
36
  # end
27
37
  module TestHelper
28
38
  ##
29
39
  # Initializes a test terminal context with specified dimensions.
30
40
  # Restores the original terminal state after the block executes.
31
41
  #
32
- # +width+:: width of the test terminal (default: 20)
33
- # +height+:: height of the test terminal (default: 10)
42
+ # +width+:: width of the test terminal (default: 80)
43
+ # +height+:: height of the test terminal (default: 24)
44
+ #
45
+ # +timeout+:: maximum execution time in seconds (default: 2). Pass nil to disable.
34
46
  #
35
47
  # If a block is given, it is executed within the test terminal context.
36
- def with_test_terminal(width = 20, height = 10)
48
+ def with_test_terminal(width = 80, height = 24, **opts)
37
49
  RatatuiRuby.init_test_terminal(width, height)
38
- yield
50
+ # Flush any lingering events from previous tests
51
+ while (event = RatatuiRuby.poll_event) && !event.none?; end
52
+
53
+ RatatuiRuby.stub :init_terminal, nil do
54
+ RatatuiRuby.stub :restore_terminal, nil do
55
+ @_ratatui_test_terminal_active = true
56
+ timeout = opts.fetch(:timeout, 2)
57
+ if timeout
58
+ Timeout.timeout(timeout) do
59
+ yield
60
+ end
61
+ else
62
+ yield
63
+ end
64
+ ensure
65
+ @_ratatui_test_terminal_active = false
66
+ end
67
+ end
39
68
  ensure
40
69
  RatatuiRuby.restore_terminal
41
70
  end
@@ -61,15 +90,295 @@ module RatatuiRuby
61
90
  end
62
91
 
63
92
  ##
64
- # Injects a mock event into the event queue for testing purposes.
93
+ # Injects an event into the event queue for testing.
94
+ #
95
+ # Pass any RatatuiRuby::Event object. The event will be returned by
96
+ # the next call to RatatuiRuby.poll_event.
97
+ #
98
+ # Raises a +RuntimeError+ if called outside of a +with_test_terminal+ block.
99
+ #
100
+ # == Examples
101
+ #
102
+ # with_test_terminal do
103
+ # # Key events
104
+ # inject_event(RatatuiRuby::Event::Key.new(code: "q"))
105
+ # inject_event(RatatuiRuby::Event::Key.new(code: "s", modifiers: ["ctrl"]))
106
+ #
107
+ # # Mouse events
108
+ # inject_event(RatatuiRuby::Event::Mouse.new(kind: "down", button: "left", x: 10, y: 5))
109
+ #
110
+ # # Resize events
111
+ # inject_event(RatatuiRuby::Event::Resize.new(width: 120, height: 40))
112
+ #
113
+ # # Paste events
114
+ # inject_event(RatatuiRuby::Event::Paste.new(content: "Hello"))
115
+ #
116
+ # # Focus events
117
+ # inject_event(RatatuiRuby::Event::FocusGained.new)
118
+ # inject_event(RatatuiRuby::Event::FocusLost.new)
119
+ # end
120
+ def inject_event(event)
121
+ unless @_ratatui_test_terminal_active
122
+ raise "Events must be injected inside a `with_test_terminal` block. " \
123
+ "Calling this method outside the block causes a race condition where the event " \
124
+ "is flushed before the application starts."
125
+ end
126
+
127
+ case event
128
+ when RatatuiRuby::Event::Key
129
+ RatatuiRuby.inject_test_event("key", { code: event.code, modifiers: event.modifiers })
130
+ when RatatuiRuby::Event::Mouse
131
+ RatatuiRuby.inject_test_event("mouse", {
132
+ kind: event.kind,
133
+ button: event.button,
134
+ x: event.x,
135
+ y: event.y,
136
+ modifiers: event.modifiers,
137
+ })
138
+ when RatatuiRuby::Event::Resize
139
+ RatatuiRuby.inject_test_event("resize", { width: event.width, height: event.height })
140
+ when RatatuiRuby::Event::Paste
141
+ RatatuiRuby.inject_test_event("paste", { content: event.content })
142
+ when RatatuiRuby::Event::FocusGained
143
+ RatatuiRuby.inject_test_event("focus_gained", {})
144
+ when RatatuiRuby::Event::FocusLost
145
+ RatatuiRuby.inject_test_event("focus_lost", {})
146
+ else
147
+ raise ArgumentError, "Unknown event type: #{event.class}"
148
+ end
149
+ end
150
+
151
+ ##
152
+ # Injects a mouse event.
153
+ #
154
+ # inject_mouse(x: 10, y: 5, kind: :down, button: :left)
155
+ def inject_mouse(x:, y:, kind: :down, modifiers: [], button: :left)
156
+ event = RatatuiRuby::Event::Mouse.new(
157
+ kind: kind.to_s,
158
+ x:,
159
+ y:,
160
+ button: button.to_s,
161
+ modifiers:
162
+ )
163
+ inject_event(event)
164
+ end
165
+
166
+ ##
167
+ # Injects a mouse left click (down) event.
168
+ #
169
+ # inject_click(x: 10, y: 5)
170
+ def inject_click(x:, y:, modifiers: [])
171
+ inject_mouse(x:, y:, kind: :down, modifiers:, button: :left)
172
+ end
173
+
174
+ ##
175
+ # Injects a mouse right click (down) event.
176
+ #
177
+ # inject_right_click(x: 10, y: 5)
178
+ def inject_right_click(x:, y:, modifiers: [])
179
+ inject_mouse(x:, y:, kind: :down, modifiers:, button: :right)
180
+ end
181
+
182
+ ##
183
+ # Injects a mouse drag event.
184
+ #
185
+ # inject_drag(x: 10, y: 5)
186
+ def inject_drag(x:, y:, modifiers: [], button: :left)
187
+ inject_mouse(x:, y:, kind: :drag, modifiers:, button:)
188
+ end
189
+
190
+ ##
191
+ # Injects multiple Key events into the queue.
192
+ #
193
+ # Supports multiple formats for convenience:
194
+ #
195
+ # * String: Converted to a Key event with that code.
196
+ # * Symbol: Parsed as modifier_code (e.g., <tt>:ctrl_c</tt>, <tt>:enter</tt>).
197
+ # * Hash: Passed to Key.new constructor.
198
+ # * Key: Passed directly.
199
+ #
200
+ # == Examples
201
+ #
202
+ # with_test_terminal do
203
+ # inject_keys("a", "b", "c")
204
+ # inject_keys(:enter, :esc)
205
+ # inject_keys(:ctrl_c, :alt_shift_left)
206
+ # inject_keys("j", { code: "k", modifiers: ["ctrl"] })
207
+ # end
208
+ def inject_keys(*args)
209
+ args.each do |arg|
210
+ event = case arg
211
+ when String
212
+ RatatuiRuby::Event::Key.new(code: arg)
213
+ when Symbol
214
+ parts = arg.to_s.split("_")
215
+ code = parts.pop
216
+ modifiers = parts
217
+ RatatuiRuby::Event::Key.new(code:, modifiers:)
218
+ when Hash
219
+ RatatuiRuby::Event::Key.new(**arg)
220
+ when RatatuiRuby::Event::Key
221
+ arg
222
+ else
223
+ raise ArgumentError, "Invalid key argument: #{arg.inspect}. Expected String, Symbol, Hash, or Key event."
224
+ end
225
+ inject_event(event)
226
+ end
227
+ end
228
+ alias inject_key inject_keys
229
+
230
+ ##
231
+ # Returns the cell attributes at the given coordinates.
232
+ #
233
+ # get_cell(0, 0)
234
+ # # => { "symbol" => "H", "fg" => :red, "bg" => nil }
235
+ def get_cell(x, y)
236
+ RatatuiRuby.get_cell_at(x, y)
237
+ end
238
+
239
+ ##
240
+ # Asserts that the cell at the given coordinates has the expected attributes.
241
+ #
242
+ # assert_cell_style(0, 0, char: "H", fg: :red)
243
+ def assert_cell_style(x, y, **expected_attributes)
244
+ cell = get_cell(x, y)
245
+ expected_attributes.each do |key, value|
246
+ actual_value = cell.public_send(key)
247
+ if value.nil?
248
+ assert_nil actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=nil, but got #{actual_value.inspect}"
249
+ else
250
+ assert_equal value, actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=#{value.inspect}, but got #{actual_value.inspect}"
251
+ end
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Mock frame for unit testing views.
257
+ #
258
+ # Captures widgets passed to +render_widget+ for inspection.
259
+ # Does not render anything—purely captures the output.
260
+ #
261
+ # == Examples
262
+ #
263
+ # frame = MockFrame.new
264
+ # View::Log.new.call(state, tui, frame, area)
265
+ # widget = frame.rendered_widgets.first[:widget]
266
+ # assert_equal "Event Log", widget.block.title
267
+ MockFrame = Data.define(:rendered_widgets) do
268
+ def initialize(rendered_widgets: [])
269
+ super
270
+ end
271
+
272
+ def render_widget(widget, area)
273
+ rendered_widgets << { widget:, area: }
274
+ end
275
+ end
276
+
277
+ ##
278
+ # Stub area for unit testing views.
279
+ #
280
+ # Provides the minimal interface views expect (+width+, +height+).
281
+ #
282
+ # == Examples
283
+ #
284
+ # area = StubRect.new(width: 60, height: 20)
285
+ StubRect = Data.define(:x, :y, :width, :height) do
286
+ def initialize(x: 0, y: 0, width: 80, height: 24)
287
+ super
288
+ end
289
+ end
290
+
291
+ ##
292
+ # Asserts that the current screen content matches a stored snapshot.
293
+ #
294
+ # This method simplifies snapshot testing by automatically resolving the snapshot path
295
+ # relative to the test file calling this method. It assumes a "snapshots" directory
296
+ # exists in the same directory as the test file.
65
297
  #
66
- # +event_type+:: "key" or "mouse"
67
- # +data+:: a Hash containing event data
298
+ # # In test/test_login.rb
299
+ # assert_snapshot("login_screen")
300
+ # # Look for: test/snapshots/login_screen.txt
68
301
  #
69
- # inject_event("key", { code: "a" })
70
- # inject_event("mouse", { kind: "down", x: 0, y: 0 })
71
- def inject_event(event_type, data)
72
- RatatuiRuby.inject_test_event(event_type, data)
302
+ # # With normalization block
303
+ # assert_snapshot("clock") do |actual|
304
+ # actual.map { |l| l.gsub(/\d{2}:\d{2}/, "XX:XX") }
305
+ # end
306
+ #
307
+ # [name] String name of the snapshot (without extension).
308
+ # [msg] String optional failure message.
309
+ def assert_snapshot(name, msg = nil, &)
310
+ # Get the path of the test file calling this method
311
+ caller_path = caller_locations(1, 1).first.path
312
+ snapshot_dir = File.join(File.dirname(caller_path), "snapshots")
313
+ snapshot_path = File.join(snapshot_dir, "#{name}.txt")
314
+
315
+ assert_screen_matches(snapshot_path, msg, &)
316
+ end
317
+
318
+ ##
319
+ # Asserts that the current screen content matches the expected content.
320
+ #
321
+ # Users need to verify that the entire TUI screen looks exactly as expected.
322
+ # Manually checking every cell or line is tedious and error-prone.
323
+ #
324
+ # This helper compares the current buffer content against an expected string (file path)
325
+ # or array of strings. It supports automatic snapshot creation and updating via
326
+ # the +UPDATE_SNAPSHOTS+ environment variable.
327
+ #
328
+ # Use it to verify complex UI states, layouts, and renderings.
329
+ #
330
+ # == Usage
331
+ #
332
+ # # Direct comparison
333
+ # assert_screen_matches(["Line 1", "Line 2"])
334
+ #
335
+ # # File comparison
336
+ # assert_screen_matches("test/snapshots/login.txt")
337
+ #
338
+ # # With normalization (e.g., masking dynamic data)
339
+ # assert_screen_matches("test/snapshots/dashboard.txt") do |lines|
340
+ # lines.map { |l| l.gsub(/User ID: \d+/, "User ID: XXX") }
341
+ # end
342
+ #
343
+ # [expected] String (file path) or Array<String> (content).
344
+ # [msg] String optional failure message.
345
+ def assert_screen_matches(expected, msg = nil)
346
+ actual_lines = buffer_content
347
+
348
+ if block_given?
349
+ actual_lines = yield(actual_lines)
350
+ end
351
+
352
+ if expected.is_a?(String)
353
+ # Snapshot file mode
354
+ snapshot_path = expected
355
+ update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true"
356
+
357
+ if !File.exist?(snapshot_path) || update_snapshots
358
+ FileUtils.mkdir_p(File.dirname(snapshot_path))
359
+ File.write(snapshot_path, "#{actual_lines.join("\n")}\n")
360
+ if update_snapshots
361
+ puts "Updated snapshot: #{snapshot_path}"
362
+ else
363
+ puts "Created snapshot: #{snapshot_path}"
364
+ end
365
+ end
366
+
367
+ expected_lines = File.readlines(snapshot_path, chomp: true)
368
+ else
369
+ # Direct comparison mode
370
+ expected_lines = expected
371
+ end
372
+
373
+ msg ||= "Screen content mismatch"
374
+
375
+ assert_equal expected_lines.size, actual_lines.size, "#{msg}: Line count mismatch"
376
+
377
+ expected_lines.each_with_index do |expected_line, i|
378
+ actual_line = actual_lines[i]
379
+ assert_equal expected_line, actual_line,
380
+ "#{msg}: Line #{i + 1} mismatch.\nExpected: #{expected_line.inspect}\nActual: #{actual_line.inspect}"
381
+ end
73
382
  end
74
383
  end
75
384
  end
@@ -6,5 +6,5 @@
6
6
  module RatatuiRuby
7
7
  # The version of the ratatui_ruby gem.
8
8
  # See https://semver.org/spec/v2.0.0.html
9
- VERSION = "0.3.1"
9
+ VERSION = "0.5.0"
10
10
  end
data/lib/ratatui_ruby.rb CHANGED
@@ -12,9 +12,12 @@ require_relative "ratatui_ruby/schema/constraint"
12
12
  require_relative "ratatui_ruby/schema/list"
13
13
  require_relative "ratatui_ruby/schema/style"
14
14
  require_relative "ratatui_ruby/schema/gauge"
15
+ require_relative "ratatui_ruby/schema/line_gauge"
15
16
  require_relative "ratatui_ruby/schema/table"
16
17
  require_relative "ratatui_ruby/schema/tabs"
17
18
  require_relative "ratatui_ruby/schema/bar_chart"
19
+ require_relative "ratatui_ruby/schema/bar_chart/bar"
20
+ require_relative "ratatui_ruby/schema/bar_chart/bar_group"
18
21
  require_relative "ratatui_ruby/schema/sparkline"
19
22
  require_relative "ratatui_ruby/schema/chart"
20
23
  require_relative "ratatui_ruby/schema/clear"
@@ -23,7 +26,15 @@ require_relative "ratatui_ruby/schema/overlay"
23
26
  require_relative "ratatui_ruby/schema/center"
24
27
  require_relative "ratatui_ruby/schema/scrollbar"
25
28
  require_relative "ratatui_ruby/schema/canvas"
29
+ require_relative "ratatui_ruby/schema/shape/label"
26
30
  require_relative "ratatui_ruby/schema/calendar"
31
+ require_relative "ratatui_ruby/schema/ratatui_logo"
32
+ require_relative "ratatui_ruby/schema/ratatui_mascot"
33
+ require_relative "ratatui_ruby/schema/text"
34
+ require_relative "ratatui_ruby/schema/draw"
35
+ require_relative "ratatui_ruby/event"
36
+ require_relative "ratatui_ruby/cell"
37
+ require_relative "ratatui_ruby/frame"
27
38
 
28
39
  begin
29
40
  require "ratatui_ruby/ratatui_ruby"
@@ -32,78 +43,213 @@ rescue LoadError
32
43
  require_relative "ratatui_ruby/ratatui_ruby"
33
44
  end
34
45
 
35
- # The RatatuiRuby module acts as a namespace for the entire gem.
36
- # It provides the main entry points for initializing the terminal and drawing the UI.
46
+ # Main entry point for the library.
47
+ #
48
+ # Terminal UIs require low-level control using C/Rust and high-level abstraction in Ruby.
49
+ #
50
+ # This module bridges the gap. It provides the native methods to initialize the terminal, handle raw mode, and render the widget tree.
51
+ #
52
+ # Use `RatatuiRuby.run` to start your application.
37
53
  module RatatuiRuby
38
54
  # Generic error class for RatatuiRuby.
39
55
  class Error < StandardError; end
40
56
 
41
57
  ##
42
- # :method: init_terminal
43
- # :call-seq: init_terminal() -> nil
44
- #
45
58
  # Initializes the terminal for TUI mode.
46
59
  # Enters alternate screen and enables raw mode.
47
60
  #
48
- # (Native method implemented in Rust)
61
+ # [focus_events] whether to enable focus gain/loss events (default: true).
62
+ # [bracketed_paste] whether to enable bracketed paste mode (default: true).
63
+ def self.init_terminal(focus_events: true, bracketed_paste: true)
64
+ _init_terminal(focus_events, bracketed_paste)
65
+ end
66
+
67
+ @experimental_warnings = true
68
+ class << self
69
+ ##
70
+ # :attr_accessor: experimental_warnings
71
+ # Whether to show warnings when using experimental features (default: true).
72
+ attr_accessor :experimental_warnings
73
+ end
49
74
 
50
75
  ##
51
- # :method: restore_terminal
52
- # :call-seq: restore_terminal() -> nil
53
- #
76
+ # :singleton-method: restore_terminal
54
77
  # Restores the terminal to its original state.
55
78
  # Leaves alternate screen and disables raw mode.
56
79
  #
57
80
  # (Native method implemented in Rust)
58
81
 
59
82
  ##
60
- # :method: draw
61
- # :call-seq: draw(node) -> nil
83
+ # :singleton-method: inject_test_event
84
+ # Injects a mock event into the event queue for testing purposes.
85
+ # [event_type] "key" or "mouse"
86
+ # [data] a Hash containing event data
62
87
  #
63
- # Draws the given UI node tree to the terminal.
64
- # [node] the root node of the UI tree (Paragraph, Layout).
88
+ # inject_test_event("key", { code: "a" })
65
89
  #
66
90
  # (Native method implemented in Rust)
67
91
 
68
92
  ##
69
- # :method: poll_event
70
- # :call-seq: poll_event() -> Hash, nil
71
- #
72
- # Polls for a keyboard event.
93
+ # Warns about usage of an experimental feature unless warnings are suppressed.
73
94
  #
74
- # poll_event
75
- # # => { type: :key, code: "a", modifiers: ["ctrl"] }
95
+ # [feature_name] String name of the feature (e.g., "Paragraph#line_count")
76
96
  #
77
- # (Native method implemented in Rust)
97
+ # This warns only once per feature name per session.
98
+ def self.warn_experimental_feature(feature_name)
99
+ return unless experimental_warnings
100
+
101
+ @warned_features ||= {}
102
+ return if @warned_features[feature_name]
103
+
104
+ warn "WARNING: #{feature_name} is an experimental feature and may change in future versions. Disable this warning with RatatuiRuby.experimental_warnings = false."
105
+ @warned_features[feature_name] = true
106
+ end
107
+
108
+ # (Native method _init_terminal implemented in Rust)
109
+ private_class_method :_init_terminal
78
110
 
79
111
  ##
80
- # :method: inject_test_event
81
- # :call-seq: inject_test_event(event_type, data) -> nil
112
+ # Draws the given UI node tree to the terminal.
82
113
  #
83
- # Injects a mock event into the event queue for testing purposes.
84
- # [event_type] "key" or "mouse"
85
- # [data] a Hash containing event data
114
+ # TUI applications need to render widgets to the screen. Rendering could
115
+ # happen all at once with a pre-built tree, or incrementally with direct
116
+ # frame access.
86
117
  #
87
- # inject_test_event("key", { code: "a" })
118
+ # This method handles both. Pass a tree for declarative rendering, or
119
+ # pass a block to manipulate the frame directly. The block receives a
120
+ # {Frame} object for imperative drawing.
88
121
  #
89
- # (Native method implemented in Rust)
122
+ # [tree] A widget tree (Paragraph, Layout, etc.) to render. Optional if
123
+ # a block is given.
124
+ #
125
+ # === Examples
126
+ #
127
+ # Legacy declarative style (tree-based):
128
+ #
129
+ # RatatuiRuby.draw(Paragraph.new(text: "Hello"))
130
+ #
131
+ # New imperative style (block-based):
132
+ #
133
+ # RatatuiRuby.draw do |frame|
134
+ # frame.render_widget(Paragraph.new(text: "Hello"), frame.area)
135
+ # end
136
+ #
137
+ def self.draw(tree = nil, &block)
138
+ if tree && block
139
+ raise ArgumentError, "Cannot provide both a tree and a block to draw"
140
+ end
141
+ unless tree || block
142
+ raise ArgumentError, "Must provide either a tree or a block to draw"
143
+ end
144
+
145
+ if tree
146
+ _draw(tree)
147
+ else
148
+ _draw(&block)
149
+ end
150
+ end
151
+
152
+ # (Native method _draw implemented in Rust)
153
+ private_class_method :_draw
90
154
 
91
155
  ##
92
- # Provides a convenience wrapper for the main TUI loop.
93
- # Initializes the terminal, runs the loop, and ensures the terminal is restored.
156
+ # Checks for user input.
157
+ #
158
+ # Returns a discrete event (Key, Mouse, Resize) if one is available in the queue.
159
+ # Returns RatatuiRuby::Event::None if the queue is empty (non-blocking).
160
+ #
161
+ # === Example
94
162
  #
95
- # RatatuiRuby.main_loop do
96
- # draw RatatuiRuby::Paragraph.new(text: "Hello")
97
- # event = RatatuiRuby::poll_event
98
- # break if event && event[:type] == :key && event[:code] == "q"
163
+ # event = RatatuiRuby.poll_event
164
+ # if event.none?
165
+ # puts "No input available"
166
+ # elsif event.key?
167
+ # puts "Key pressed"
99
168
  # end
100
- def self.main_loop
101
- require_relative "ratatui_ruby/dsl"
102
- init_terminal
103
- loop do
104
- yield DSL.new
169
+ #
170
+ def self.poll_event
171
+ raw = _poll_event
172
+ return Event::None.new if raw.nil?
173
+
174
+ case raw[:type]
175
+ when :key
176
+ Event::Key.new(code: raw[:code], modifiers: raw[:modifiers] || [])
177
+ when :mouse
178
+ Event::Mouse.new(
179
+ kind: raw[:kind].to_s,
180
+ x: raw[:x],
181
+ y: raw[:y],
182
+ button: raw[:button].to_s,
183
+ modifiers: raw[:modifiers] || []
184
+ )
185
+ when :resize
186
+ Event::Resize.new(width: raw[:width], height: raw[:height])
187
+ when :paste
188
+ Event::Paste.new(content: raw[:content])
189
+ when :focus_gained
190
+ Event::FocusGained.new
191
+ when :focus_lost
192
+ Event::FocusLost.new
193
+ else
194
+ # Fallback for unknown events, though ideally we cover them all
195
+ nil
105
196
  end
197
+ end
198
+
199
+ # (Native method _poll_event implemented in Rust)
200
+ private_class_method :_poll_event
201
+
202
+ ##
203
+ # Starts the TUI application lifecycle.
204
+ #
205
+ # Managing generic setup/teardown (raw mode, alternate screen) manualy is error-prone. If your app crashes, the terminal might be left in a broken state.
206
+ #
207
+ # This method handles the safety net. It initializes the terminal, yields a {Session}, and ensures the terminal state is restored even if exceptions occur.
208
+ #
209
+ # === Example
210
+ #
211
+ # RatatuiRuby.run(focus_events: false) do |tui|
212
+ # tui.draw(tui.paragraph(text: "Hi"))
213
+ # sleep 1
214
+ # end
215
+ def self.run(focus_events: true, bracketed_paste: true)
216
+ require_relative "ratatui_ruby/session"
217
+ init_terminal(focus_events:, bracketed_paste:)
218
+ yield Session.new
106
219
  ensure
107
220
  restore_terminal
108
221
  end
222
+
223
+ ##
224
+ # Inspects the terminal buffer at specific coordinates.
225
+ #
226
+ # When writing tests, you need to verify that your widget drew the correct characters and styles.
227
+ # This method provides deep inspection of the cell's state (symbol, colors, modifiers).
228
+ #
229
+ # Returns a {Cell} object.
230
+ #
231
+ # Values depend on what the backend has rendered. If nothing has been rendered to a cell, it may contain defaults (empty symbol, nil colors).
232
+ #
233
+ # === Example
234
+ #
235
+ # cell = RatatuiRuby.get_cell_at(10, 5)
236
+ # expect(cell.symbol).to eq("X")
237
+ # expect(cell.fg).to eq(:red)
238
+ # expect(cell).to be_bold
239
+ #
240
+ def self.get_cell_at(x, y)
241
+ raw = _get_cell_at(x, y)
242
+ Cell.new(
243
+ char: raw["char"],
244
+ fg: raw["fg"],
245
+ bg: raw["bg"],
246
+ modifiers: raw["modifiers"] || []
247
+ )
248
+ end
249
+
250
+ # (Native method _get_cell_at implemented in Rust)
251
+ private_class_method :_get_cell_at
252
+
253
+ # Hide native Layout._split helper
254
+ Layout.singleton_class.__send__(:private, :_split)
109
255
  end
@@ -0,0 +1,11 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ class AppAllEvents
6
+ # @public
7
+ def self.new: () -> AppAllEvents
8
+
9
+ # @public
10
+ def run: () -> void
11
+ end
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ class EventEntry < Data
5
+ attr_reader type: Symbol
6
+ attr_reader sub_key: Symbol | String | nil
7
+ attr_reader color: Symbol
8
+ attr_reader timestamp: Timestamp
9
+ attr_reader data: Hash[Symbol, untyped]
10
+
11
+ def self.from_event(type: Symbol, sub_key: Symbol | String | nil, color: Symbol, timestamp: Timestamp, data: Hash[Symbol, untyped]) -> instance
12
+ def matches_type?: (Symbol check_type) -> bool
13
+ def matches_sub_type?: (Symbol check_type, Symbol | String check_sub_key) -> bool
14
+ def matches_kind?: (String kind) -> bool
15
+ def self.new: (?type: Symbol, ?sub_key: Symbol | String | nil, ?color: Symbol, ?timestamp: Timestamp, ?data: Hash[Symbol, untyped]) -> instance
16
+ end