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
@@ -0,0 +1,115 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Frame wrapper for exposing Ratatui's Frame to Ruby.
5
+ //!
6
+ //! This module provides `RubyFrame`, a struct that wraps `ratatui::Frame` and exposes
7
+ //! it to Ruby via Magnus. It enables explicit widget placement through `render_widget`,
8
+ //! aligning `RatatuiRuby` with native Rust Ratatui patterns.
9
+ //!
10
+ //! # Safety
11
+ //!
12
+ //! `RubyFrame` uses raw pointer casting to store a `Frame` reference with an erased
13
+ //! lifetime. This is safe because:
14
+ //! 1. `RubyFrame` is only created within `Terminal::draw()` callbacks
15
+ //! 2. `RubyFrame` is never returned from or stored beyond the callback scope
16
+ //! 3. The Ruby block receiving `RubyFrame` completes before the callback returns
17
+ //!
18
+ //! The `'static` lifetime is a lie, but a safe one within these constraints.
19
+
20
+ use crate::rendering;
21
+ use magnus::{prelude::*, Error, Value};
22
+ use ratatui::layout::Rect;
23
+ use ratatui::Frame;
24
+ use std::cell::UnsafeCell;
25
+ use std::ptr::NonNull;
26
+
27
+ /// A wrapper around Ratatui's `Frame` that can be exposed to Ruby.
28
+ ///
29
+ /// This struct uses raw pointers to hold a mutable reference to the frame,
30
+ /// which is valid only for the duration of the draw callback.
31
+ ///
32
+ /// # Safety
33
+ ///
34
+ /// We implement `Send` manually because:
35
+ /// 1. `RubyFrame` is only created and used within a single `Terminal::draw()` callback
36
+ /// 2. The Ruby VM is single-threaded (GVL), so the frame pointer is never accessed
37
+ /// from multiple threads simultaneously
38
+ /// 3. `RubyFrame` never escapes the draw callback scope
39
+ #[magnus::wrap(class = "RatatuiRuby::Frame")]
40
+ pub struct RubyFrame {
41
+ /// Pointer to the underlying frame. Valid only during the draw callback.
42
+ inner: UnsafeCell<NonNull<Frame<'static>>>,
43
+ }
44
+
45
+ // SAFETY: RubyFrame is only used within Terminal::draw() callbacks, which are
46
+ // single-threaded. The Ruby VM's GVL ensures no concurrent access.
47
+ unsafe impl Send for RubyFrame {}
48
+
49
+ impl RubyFrame {
50
+ /// Creates a new `RubyFrame` wrapping the given frame reference.
51
+ ///
52
+ /// # Safety
53
+ ///
54
+ /// The caller must ensure that:
55
+ /// 1. The `RubyFrame` does not outlive the frame reference
56
+ /// 2. No other mutable references to the frame exist while `RubyFrame` is in use
57
+ pub fn new(frame: &mut Frame<'_>) -> Self {
58
+ // SAFETY: We cast the frame pointer to 'static lifetime. This is safe because:
59
+ // - RubyFrame is only used within Terminal::draw() callbacks
60
+ // - The Ruby block completes before the callback returns
61
+ // - No reference to RubyFrame escapes the callback scope
62
+ let ptr = NonNull::from(frame);
63
+ let static_ptr: NonNull<Frame<'static>> =
64
+ // SAFETY: Lifetime erasure is safe within the draw callback scope.
65
+ // The frame pointer remains valid for the entire callback duration.
66
+ unsafe { std::mem::transmute(ptr) };
67
+
68
+ Self {
69
+ inner: UnsafeCell::new(static_ptr),
70
+ }
71
+ }
72
+
73
+ /// Returns the terminal area as a Ruby `RatatuiRuby::Rect`.
74
+ ///
75
+ /// This mirrors `frame.area()` in Rust Ratatui.
76
+ pub fn area(&self) -> Result<Value, Error> {
77
+ let ruby = magnus::Ruby::get().unwrap();
78
+
79
+ // SAFETY: The frame pointer is valid for the duration of the draw callback.
80
+ // We only read from the frame, which is safe with an immutable reference.
81
+ let area = unsafe { (*self.inner.get()).as_ref().area() };
82
+
83
+ // Create a Ruby Rect object
84
+ let module = ruby.define_module("RatatuiRuby")?;
85
+ let class = module.const_get::<_, magnus::RClass>("Rect")?;
86
+ class.funcall("new", (area.x, area.y, area.width, area.height))
87
+ }
88
+
89
+ /// Renders a widget at the specified area.
90
+ ///
91
+ /// This mirrors `frame.render_widget(widget, area)` in Rust Ratatui.
92
+ ///
93
+ /// # Arguments
94
+ ///
95
+ /// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::Paragraph`)
96
+ /// * `area` - A Ruby `Rect` or hash-like object with `x`, `y`, `width`, `height`
97
+ pub fn render_widget(&self, widget: Value, area: Value) -> Result<(), Error> {
98
+ // Parse the Ruby area into a Rust Rect
99
+ let x: u16 = area.funcall("x", ())?;
100
+ let y: u16 = area.funcall("y", ())?;
101
+ let width: u16 = area.funcall("width", ())?;
102
+ let height: u16 = area.funcall("height", ())?;
103
+ let rect = Rect::new(x, y, width, height);
104
+
105
+ // SAFETY: The frame pointer is valid for the duration of the draw callback.
106
+ // We take a mutable reference which is safe because:
107
+ // 1. RubyFrame is only used within Terminal::draw() callbacks
108
+ // 2. Ruby's GVL ensures single-threaded access
109
+ // 3. No other code holds a reference to the frame during this call
110
+ let frame = unsafe { (*self.inner.get()).as_mut() };
111
+
112
+ // Delegate to the existing render_node function
113
+ rendering::render_node(frame, rect, widget)
114
+ }
115
+ }
@@ -1,41 +1,103 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- mod buffer;
4
+ // Require SAFETY comments on all unsafe blocks
5
+ #![warn(clippy::undocumented_unsafe_blocks)]
6
+ // Enable pedantic lints for stricter code quality
7
+ #![warn(clippy::pedantic)]
8
+ // Allow certain pedantic lints that are too noisy for FFI code
9
+ #![allow(clippy::missing_errors_doc)]
10
+ #![allow(clippy::missing_panics_doc)]
11
+ #![allow(clippy::module_name_repetitions)]
12
+
5
13
  mod events;
14
+ mod frame;
6
15
  mod rendering;
7
16
  mod style;
8
17
  mod terminal;
18
+ mod text;
9
19
  mod widgets;
10
20
 
11
- use magnus::{function, Class, Error, Module, Value};
21
+ use frame::RubyFrame;
22
+ use magnus::{function, method, Error, Module, Object, Ruby, Value};
12
23
  use terminal::{init_terminal, restore_terminal, TERMINAL};
13
24
 
14
- fn draw(tree: Value) -> Result<(), Error> {
15
- let ruby = magnus::Ruby::get().unwrap();
25
+ /// Draw to the terminal.
26
+ ///
27
+ /// Supports two calling conventions:
28
+ /// - Legacy: `RatatuiRuby.draw(tree)` - Renders a widget tree to the full terminal area
29
+ /// - New: `RatatuiRuby.draw { |frame| ... }` - Yields a Frame for explicit widget placement
30
+ fn draw(args: &[Value]) -> Result<(), Error> {
31
+ let ruby = Ruby::get().unwrap();
32
+
33
+ // Parse arguments: check for optional tree argument
34
+ let tree: Option<Value> = if args.is_empty() {
35
+ None
36
+ } else if args.len() == 1 {
37
+ Some(args[0])
38
+ } else {
39
+ return Err(Error::new(
40
+ ruby.exception_arg_error(),
41
+ format!(
42
+ "wrong number of arguments (given {}, expected 0..1)",
43
+ args.len()
44
+ ),
45
+ ));
46
+ };
47
+ let block_given = ruby.block_given();
48
+
49
+ // Validate: must have either tree or block, but not both
50
+ if tree.is_some() && block_given {
51
+ return Err(Error::new(
52
+ ruby.exception_arg_error(),
53
+ "Cannot provide both a tree and a block to draw",
54
+ ));
55
+ }
56
+ if tree.is_none() && !block_given {
57
+ return Err(Error::new(
58
+ ruby.exception_arg_error(),
59
+ "Must provide either a tree or a block to draw",
60
+ ));
61
+ }
62
+
16
63
  let mut term_lock = TERMINAL.lock().unwrap();
64
+ let mut render_error: Option<Error> = None;
65
+
66
+ // Helper closure to execute the draw callback logic for either terminal type
67
+ let mut draw_callback = |f: &mut ratatui::Frame<'_>| {
68
+ if block_given {
69
+ // New API: yield RubyFrame to block
70
+ let ruby_frame = RubyFrame::new(f);
71
+ if let Err(e) = ruby.yield_value::<_, Value>(ruby_frame) {
72
+ render_error = Some(e);
73
+ }
74
+ } else if let Some(tree_value) = tree {
75
+ // Legacy API: render tree to full area
76
+ if let Err(e) = rendering::render_node(f, f.area(), tree_value) {
77
+ render_error = Some(e);
78
+ }
79
+ }
80
+ };
81
+
17
82
  if let Some(wrapper) = term_lock.as_mut() {
18
83
  match wrapper {
19
84
  terminal::TerminalWrapper::Crossterm(term) => {
20
- term.draw(|f| {
21
- if let Err(e) = rendering::render_node(f, f.area(), tree) {
22
- eprintln!("Render error: {:?}", e);
23
- }
24
- })
25
- .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
85
+ term.draw(&mut draw_callback)
86
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
26
87
  }
27
88
  terminal::TerminalWrapper::Test(term) => {
28
- term.draw(|f| {
29
- if let Err(e) = rendering::render_node(f, f.area(), tree) {
30
- eprintln!("Render error: {:?}", e);
31
- }
32
- })
33
- .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
89
+ term.draw(&mut draw_callback)
90
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
34
91
  }
35
92
  }
36
93
  } else {
37
94
  eprintln!("Terminal is None!");
38
95
  }
96
+
97
+ if let Some(e) = render_error {
98
+ return Err(e);
99
+ }
100
+
39
101
  Ok(())
40
102
  }
41
103
 
@@ -44,16 +106,17 @@ fn init() -> Result<(), Error> {
44
106
  let ruby = magnus::Ruby::get().unwrap();
45
107
  let m = ruby.define_module("RatatuiRuby")?;
46
108
 
47
- let buffer_class = m.define_class("Buffer", ruby.class_object())?;
48
- buffer_class.undef_default_alloc_func();
49
- buffer_class.define_method("set_string", magnus::method!(buffer::BufferWrapper::set_string, 4))?;
50
- buffer_class.define_method("area", magnus::method!(buffer::BufferWrapper::area, 0))?;
51
-
52
- m.define_module_function("init_terminal", function!(init_terminal, 0))?;
109
+ m.define_module_function("_init_terminal", function!(init_terminal, 2))?;
53
110
  m.define_module_function("restore_terminal", function!(restore_terminal, 0))?;
54
- m.define_module_function("draw", function!(draw, 1))?;
55
- m.define_module_function("poll_event", function!(events::poll_event, 0))?;
111
+ m.define_module_function("_draw", function!(draw, -1))?;
112
+
113
+ // Register Frame class
114
+ let frame_class = m.define_class("Frame", ruby.class_object())?;
115
+ frame_class.define_method("area", method!(RubyFrame::area, 0))?;
116
+ frame_class.define_method("render_widget", method!(RubyFrame::render_widget, 2))?;
117
+ m.define_module_function("_poll_event", function!(events::poll_event, 0))?;
56
118
  m.define_module_function("inject_test_event", function!(events::inject_test_event, 2))?;
119
+ m.define_module_function("clear_events", function!(events::clear_events, 0))?;
57
120
 
58
121
  // Test backend helpers
59
122
  m.define_module_function(
@@ -68,8 +131,26 @@ fn init() -> Result<(), Error> {
68
131
  "get_cursor_position",
69
132
  function!(terminal::get_cursor_position, 0),
70
133
  )?;
134
+ m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
71
135
  m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
72
136
 
137
+ // Register Layout.split on the Layout class
138
+ let layout_class = m.const_get::<_, magnus::RClass>("Layout")?;
139
+ layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
140
+
141
+ // Paragraph metrics
142
+ m.define_module_function(
143
+ "_paragraph_line_count",
144
+ function!(widgets::paragraph::line_count, 2),
145
+ )?;
146
+ m.define_module_function(
147
+ "_paragraph_line_width",
148
+ function!(widgets::paragraph::line_width, 1),
149
+ )?;
150
+
151
+ // Tabs metrics
152
+ m.define_module_function("_tabs_width", function!(widgets::tabs::width, 1))?;
153
+
73
154
  Ok(())
74
155
  }
75
156
 
@@ -1,29 +1,40 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- use crate::buffer::BufferWrapper;
4
+ use crate::style::{parse_color_value, parse_modifier_str, parse_style};
5
5
  use crate::widgets;
6
- use magnus::{prelude::*, Error, Value};
7
- use ratatui::{layout::Rect, Frame};
6
+ use magnus::{prelude::*, Error, RArray, Value};
7
+ use ratatui::{buffer::Buffer, layout::Rect, style::Style, Frame};
8
8
 
9
9
  pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
10
  if node.respond_to("render", true)? {
11
- let wrapper = BufferWrapper::new(frame.buffer_mut());
12
11
  let ruby = magnus::Ruby::get().unwrap();
13
12
  let ruby_area = {
14
13
  let module = ruby.define_module("RatatuiRuby")?;
15
14
  let class = module.const_get::<_, magnus::RClass>("Rect")?;
16
15
  class.funcall::<_, _, Value>("new", (area.x, area.y, area.width, area.height))?
17
16
  };
18
- let wrapper_obj = ruby.obj_wrap(wrapper);
19
- node.funcall::<_, _, Value>("render", (ruby_area, wrapper_obj))?;
17
+
18
+ // Call render with just the area (no buffer!)
19
+ let commands: Value = node.funcall("render", (ruby_area,))?;
20
+
21
+ // Process returned draw commands
22
+ if let Some(arr) = RArray::from_value(commands) {
23
+ for i in 0..arr.len() {
24
+ let ruby = magnus::Ruby::get().unwrap();
25
+ let index = isize::try_from(i)
26
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
27
+ let cmd: Value = arr.entry(index)?;
28
+ process_draw_command(frame.buffer_mut(), cmd)?;
29
+ }
30
+ }
20
31
  return Ok(());
21
32
  }
22
33
 
23
- let class = node.class();
24
- let class_name = unsafe { class.name() };
34
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
35
+ let class_name = unsafe { node.class().name() }.into_owned();
25
36
 
26
- match class_name.as_ref() {
37
+ match class_name.as_str() {
27
38
  "RatatuiRuby::Paragraph" => widgets::paragraph::render(frame, area, node)?,
28
39
  "RatatuiRuby::Clear" => widgets::clear::render(frame, area, node)?,
29
40
  "RatatuiRuby::Cursor" => widgets::cursor::render(frame, area, node)?,
@@ -32,6 +43,7 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
32
43
  "RatatuiRuby::Layout" => widgets::layout::render(frame, area, node)?,
33
44
  "RatatuiRuby::List" => widgets::list::render(frame, area, node)?,
34
45
  "RatatuiRuby::Gauge" => widgets::gauge::render(frame, area, node)?,
46
+ "RatatuiRuby::LineGauge" => widgets::line_gauge::render(frame, area, node)?,
35
47
  "RatatuiRuby::Table" => widgets::table::render(frame, area, node)?,
36
48
  "RatatuiRuby::Block" => widgets::block::render(frame, area, node)?,
37
49
  "RatatuiRuby::Tabs" => widgets::tabs::render(frame, area, node)?,
@@ -41,9 +53,81 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
41
53
  "RatatuiRuby::Calendar" => widgets::calendar::render(frame, area, node)?,
42
54
  "RatatuiRuby::Sparkline" => widgets::sparkline::render(frame, area, node)?,
43
55
  "RatatuiRuby::Chart" | "RatatuiRuby::LineChart" => {
44
- widgets::chart::render(frame, area, node)?
56
+ widgets::chart::render(frame, area, node)?;
57
+ }
58
+ "RatatuiRuby::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node),
59
+ "RatatuiRuby::RatatuiMascot" => {
60
+ widgets::ratatui_mascot::render_ratatui_mascot(frame, area, node)?;
45
61
  }
46
62
  _ => {}
47
63
  }
48
64
  Ok(())
49
65
  }
66
+
67
+ fn process_draw_command(buffer: &mut Buffer, cmd: Value) -> Result<(), Error> {
68
+ let ruby = magnus::Ruby::get().unwrap();
69
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
70
+ let class_name = unsafe { cmd.class().name() }.into_owned();
71
+
72
+ match class_name.as_str() {
73
+ "RatatuiRuby::Draw::StringCmd" => {
74
+ let x: u16 = cmd.funcall("x", ())?;
75
+ let y: u16 = cmd.funcall("y", ())?;
76
+ let string: String = cmd.funcall("string", ())?;
77
+ let style_val: Value = cmd.funcall("style", ())?;
78
+ let style = parse_style(style_val)?;
79
+ buffer.set_string(x, y, string, style);
80
+ }
81
+ "RatatuiRuby::Draw::CellCmd" => {
82
+ let x: u16 = cmd.funcall("x", ())?;
83
+ let y: u16 = cmd.funcall("y", ())?;
84
+ let cell_val: Value = cmd.funcall("cell", ())?;
85
+
86
+ let area = buffer.area;
87
+ if x >= area.x + area.width || y >= area.y + area.height {
88
+ return Ok(());
89
+ }
90
+
91
+ let symbol: String = cell_val.funcall("char", ())?;
92
+ let fg_val: Value = cell_val.funcall("fg", ())?;
93
+ let bg_val: Value = cell_val.funcall("bg", ())?;
94
+ let modifiers_val: Value = cell_val.funcall("modifiers", ())?;
95
+
96
+ let mut style = Style::default();
97
+
98
+ if !fg_val.is_nil() {
99
+ if let Some(color) = parse_color_value(fg_val)? {
100
+ style = style.fg(color);
101
+ }
102
+ }
103
+ if !bg_val.is_nil() {
104
+ if let Some(color) = parse_color_value(bg_val)? {
105
+ style = style.bg(color);
106
+ }
107
+ }
108
+
109
+ if let Some(mods_array) = RArray::from_value(modifiers_val) {
110
+ for i in 0..mods_array.len() {
111
+ let index = isize::try_from(i)
112
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
113
+ let mod_str: String = mods_array.entry::<String>(index)?;
114
+ if let Some(modifier) = parse_modifier_str(&mod_str) {
115
+ style = style.add_modifier(modifier);
116
+ }
117
+ }
118
+ }
119
+
120
+ if let Some(cell) = buffer.cell_mut((x, y)) {
121
+ cell.set_symbol(&symbol).set_style(style);
122
+ }
123
+ }
124
+ _ => {
125
+ return Err(Error::new(
126
+ ruby.exception_type_error(),
127
+ format!("Unknown draw command: {class_name}"),
128
+ ));
129
+ }
130
+ }
131
+
132
+ Ok(())
133
+ }