ratatui_ruby 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +98 -176
  7. data/CHANGELOG.md +80 -6
  8. data/README.md +19 -7
  9. data/REUSE.toml +15 -0
  10. data/doc/application_architecture.md +179 -45
  11. data/doc/application_testing.md +80 -32
  12. data/doc/contributors/design/ruby_frontend.md +48 -8
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +191 -48
  15. data/doc/contributors/documentation_style.md +7 -0
  16. data/doc/contributors/examples_audit/p1_high.md +21 -0
  17. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  18. data/doc/contributors/examples_audit.md +41 -0
  19. data/doc/contributors/index.md +2 -0
  20. data/doc/event_handling.md +21 -7
  21. data/doc/images/app_all_events.png +0 -0
  22. data/doc/images/app_color_picker.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_stateful_interaction.png +0 -0
  25. data/doc/images/verify_quickstart_dsl.png +0 -0
  26. data/doc/images/verify_quickstart_layout.png +0 -0
  27. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  28. data/doc/images/verify_readme_usage.png +0 -0
  29. data/doc/images/widget_barchart_demo.png +0 -0
  30. data/doc/images/widget_block_demo.png +0 -0
  31. data/doc/images/widget_box_demo.png +0 -0
  32. data/doc/images/widget_calendar_demo.png +0 -0
  33. data/doc/images/widget_canvas_demo.png +0 -0
  34. data/doc/images/widget_cell_demo.png +0 -0
  35. data/doc/images/widget_center_demo.png +0 -0
  36. data/doc/images/widget_chart_demo.png +0 -0
  37. data/doc/images/widget_gauge_demo.png +0 -0
  38. data/doc/images/widget_layout_split.png +0 -0
  39. data/doc/images/widget_line_gauge_demo.png +0 -0
  40. data/doc/images/widget_list_demo.png +0 -0
  41. data/doc/images/widget_overlay_demo.png +0 -0
  42. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  43. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  44. data/doc/images/widget_render.png +0 -0
  45. data/doc/images/widget_rich_text.png +0 -0
  46. data/doc/images/widget_scroll_text.png +0 -0
  47. data/doc/images/widget_scrollbar_demo.png +0 -0
  48. data/doc/images/widget_sparkline_demo.png +0 -0
  49. data/doc/images/widget_style_colors.png +0 -0
  50. data/doc/images/widget_table_demo.png +0 -0
  51. data/doc/images/widget_table_flex.png +0 -0
  52. data/doc/images/widget_tabs_demo.png +0 -0
  53. data/doc/images/widget_text_width.png +0 -0
  54. data/doc/interactive_design.md +25 -30
  55. data/doc/quickstart.md +150 -130
  56. data/doc/terminal_limitations.md +92 -0
  57. data/examples/app_all_events/README.md +99 -0
  58. data/examples/app_all_events/app.rb +96 -0
  59. data/examples/app_all_events/model/app_model.rb +157 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +92 -0
  62. data/examples/app_all_events/model/msg.rb +37 -0
  63. data/examples/app_all_events/model/timestamp.rb +54 -0
  64. data/examples/app_all_events/update.rb +73 -0
  65. data/examples/app_all_events/view/app_view.rb +78 -0
  66. data/examples/app_all_events/view/controls_view.rb +52 -0
  67. data/examples/app_all_events/view/counts_view.rb +59 -0
  68. data/examples/app_all_events/view/live_view.rb +70 -0
  69. data/examples/app_all_events/view/log_view.rb +55 -0
  70. data/examples/app_all_events/view.rb +7 -0
  71. data/examples/app_color_picker/README.md +134 -0
  72. data/examples/app_color_picker/app.rb +74 -0
  73. data/examples/app_color_picker/clipboard.rb +84 -0
  74. data/examples/app_color_picker/color.rb +191 -0
  75. data/examples/app_color_picker/controls.rb +90 -0
  76. data/examples/app_color_picker/copy_dialog.rb +166 -0
  77. data/examples/app_color_picker/export_pane.rb +126 -0
  78. data/examples/app_color_picker/harmony.rb +56 -0
  79. data/examples/app_color_picker/input.rb +174 -0
  80. data/examples/app_color_picker/main_container.rb +178 -0
  81. data/examples/app_color_picker/palette.rb +109 -0
  82. data/examples/app_login_form/README.md +47 -0
  83. data/examples/{login_form → app_login_form}/app.rb +38 -42
  84. data/examples/app_stateful_interaction/README.md +31 -0
  85. data/examples/app_stateful_interaction/app.rb +272 -0
  86. data/examples/timeout_demo.rb +43 -0
  87. data/examples/verify_quickstart_dsl/README.md +48 -0
  88. data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
  89. data/examples/verify_quickstart_layout/README.md +71 -0
  90. data/examples/verify_quickstart_layout/app.rb +71 -0
  91. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  92. data/examples/verify_quickstart_lifecycle/app.rb +54 -0
  93. data/examples/verify_readme_usage/README.md +43 -0
  94. data/examples/verify_readme_usage/app.rb +40 -0
  95. data/examples/widget_barchart_demo/README.md +49 -0
  96. data/examples/widget_barchart_demo/app.rb +238 -0
  97. data/examples/widget_block_demo/README.md +34 -0
  98. data/examples/widget_block_demo/app.rb +256 -0
  99. data/examples/widget_box_demo/README.md +45 -0
  100. data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
  101. data/examples/widget_calendar_demo/README.md +39 -0
  102. data/examples/widget_calendar_demo/app.rb +109 -0
  103. data/examples/widget_canvas_demo/README.md +27 -0
  104. data/examples/widget_canvas_demo/app.rb +123 -0
  105. data/examples/widget_cell_demo/README.md +36 -0
  106. data/examples/widget_cell_demo/app.rb +111 -0
  107. data/examples/widget_center_demo/README.md +29 -0
  108. data/examples/widget_center_demo/app.rb +116 -0
  109. data/examples/widget_chart_demo/README.md +41 -0
  110. data/examples/widget_chart_demo/app.rb +218 -0
  111. data/examples/widget_gauge_demo/README.md +41 -0
  112. data/examples/widget_gauge_demo/app.rb +212 -0
  113. data/examples/widget_layout_split/README.md +44 -0
  114. data/examples/widget_layout_split/app.rb +246 -0
  115. data/examples/widget_line_gauge_demo/README.md +41 -0
  116. data/examples/widget_line_gauge_demo/app.rb +217 -0
  117. data/examples/widget_list_demo/README.md +49 -0
  118. data/examples/widget_list_demo/app.rb +366 -0
  119. data/examples/widget_map_demo/README.md +39 -0
  120. data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
  121. data/examples/widget_overlay_demo/app.rb +248 -0
  122. data/examples/widget_popup_demo/README.md +36 -0
  123. data/examples/widget_popup_demo/app.rb +104 -0
  124. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  125. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  126. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  127. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  128. data/examples/widget_rect/README.md +38 -0
  129. data/examples/widget_rect/app.rb +205 -0
  130. data/examples/widget_render/README.md +37 -0
  131. data/examples/widget_render/app.rb +184 -0
  132. data/examples/widget_rich_text/README.md +35 -0
  133. data/examples/widget_rich_text/app.rb +166 -0
  134. data/examples/widget_scroll_text/README.md +37 -0
  135. data/examples/widget_scroll_text/app.rb +107 -0
  136. data/examples/widget_scrollbar_demo/README.md +37 -0
  137. data/examples/widget_scrollbar_demo/app.rb +153 -0
  138. data/examples/widget_sparkline_demo/README.md +42 -0
  139. data/examples/widget_sparkline_demo/app.rb +275 -0
  140. data/examples/widget_style_colors/README.md +34 -0
  141. data/examples/widget_style_colors/app.rb +19 -21
  142. data/examples/widget_table_demo/README.md +48 -0
  143. data/examples/widget_table_demo/app.rb +239 -0
  144. data/examples/widget_tabs_demo/README.md +41 -0
  145. data/examples/widget_tabs_demo/app.rb +181 -0
  146. data/examples/widget_text_width/README.md +35 -0
  147. data/examples/widget_text_width/app.rb +106 -0
  148. data/ext/ratatui_ruby/Cargo.lock +11 -4
  149. data/ext/ratatui_ruby/Cargo.toml +2 -1
  150. data/ext/ratatui_ruby/src/events.rs +359 -62
  151. data/ext/ratatui_ruby/src/frame.rs +227 -0
  152. data/ext/ratatui_ruby/src/lib.rs +110 -27
  153. data/ext/ratatui_ruby/src/rendering.rs +8 -4
  154. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  155. data/ext/ratatui_ruby/src/style.rs +138 -57
  156. data/ext/ratatui_ruby/src/terminal.rs +42 -22
  157. data/ext/ratatui_ruby/src/text.rs +14 -7
  158. data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
  159. data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
  160. data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
  161. data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
  162. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  163. data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
  164. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  165. data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
  166. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  167. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  168. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  169. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  170. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
  171. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
  172. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
  173. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  174. data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
  175. data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
  176. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  177. data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
  178. data/lib/ratatui_ruby/cell.rb +7 -7
  179. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  180. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  181. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  182. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  183. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  184. data/lib/ratatui_ruby/event/key.rb +112 -52
  185. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  186. data/lib/ratatui_ruby/event/none.rb +43 -0
  187. data/lib/ratatui_ruby/event/paste.rb +1 -1
  188. data/lib/ratatui_ruby/event.rb +56 -4
  189. data/lib/ratatui_ruby/frame.rb +183 -0
  190. data/lib/ratatui_ruby/list_state.rb +88 -0
  191. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
  192. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
  193. data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
  194. data/lib/ratatui_ruby/schema/block.rb +163 -168
  195. data/lib/ratatui_ruby/schema/calendar.rb +66 -67
  196. data/lib/ratatui_ruby/schema/canvas.rb +63 -63
  197. data/lib/ratatui_ruby/schema/center.rb +46 -46
  198. data/lib/ratatui_ruby/schema/chart.rb +135 -143
  199. data/lib/ratatui_ruby/schema/clear.rb +42 -42
  200. data/lib/ratatui_ruby/schema/constraint.rb +76 -76
  201. data/lib/ratatui_ruby/schema/cursor.rb +30 -25
  202. data/lib/ratatui_ruby/schema/gauge.rb +54 -52
  203. data/lib/ratatui_ruby/schema/layout.rb +87 -87
  204. data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
  205. data/lib/ratatui_ruby/schema/list.rb +103 -80
  206. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  207. data/lib/ratatui_ruby/schema/overlay.rb +31 -31
  208. data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
  209. data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
  210. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
  211. data/lib/ratatui_ruby/schema/rect.rb +99 -56
  212. data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
  213. data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
  214. data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
  215. data/lib/ratatui_ruby/schema/style.rb +66 -46
  216. data/lib/ratatui_ruby/schema/table.rb +126 -115
  217. data/lib/ratatui_ruby/schema/tabs.rb +66 -67
  218. data/lib/ratatui_ruby/schema/text.rb +69 -1
  219. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  220. data/lib/ratatui_ruby/session/autodoc.rb +482 -0
  221. data/lib/ratatui_ruby/session.rb +55 -23
  222. data/lib/ratatui_ruby/table_state.rb +90 -0
  223. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  224. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  225. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  226. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  227. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  228. data/lib/ratatui_ruby/test_helper.rb +66 -193
  229. data/lib/ratatui_ruby/version.rb +1 -1
  230. data/lib/ratatui_ruby.rb +100 -51
  231. data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
  232. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  233. data/sig/examples/app_all_events/model/events.rbs +15 -0
  234. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  235. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  236. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  237. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  238. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  239. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  240. data/sig/examples/app_all_events/view.rbs +8 -0
  241. data/sig/examples/app_all_events/view_state.rbs +15 -0
  242. data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
  243. data/sig/examples/app_login_form/app.rbs +11 -0
  244. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  245. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  246. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  247. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  248. data/sig/examples/widget_block_demo/app.rbs +32 -0
  249. data/sig/examples/widget_box_demo/app.rbs +11 -0
  250. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  251. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  252. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  253. data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
  254. data/sig/examples/widget_layout_split/app.rbs +10 -0
  255. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  256. data/sig/examples/widget_list_demo/app.rbs +12 -0
  257. data/sig/examples/widget_map_demo/app.rbs +11 -0
  258. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  259. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  260. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  261. data/sig/examples/widget_rect/app.rbs +12 -0
  262. data/sig/examples/widget_render/app.rbs +10 -0
  263. data/sig/examples/widget_rich_text/app.rbs +11 -0
  264. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  265. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  266. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  267. data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
  268. data/sig/examples/widget_table_demo/app.rbs +11 -0
  269. data/sig/examples/widget_text_width/app.rbs +10 -0
  270. data/sig/ratatui_ruby/event.rbs +11 -1
  271. data/sig/ratatui_ruby/frame.rbs +11 -0
  272. data/sig/ratatui_ruby/list_state.rbs +13 -0
  273. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
  274. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  275. data/sig/ratatui_ruby/schema/draw.rbs +4 -0
  276. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  277. data/sig/ratatui_ruby/schema/layout.rbs +1 -1
  278. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  279. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  280. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  281. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  282. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  283. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  284. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  285. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  286. data/sig/ratatui_ruby/session.rbs +107 -0
  287. data/sig/ratatui_ruby/table_state.rbs +15 -0
  288. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  289. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  290. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  291. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  292. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  293. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  294. data/tasks/autodoc/examples.rb +79 -0
  295. data/tasks/autodoc/inventory.rb +63 -0
  296. data/tasks/autodoc/member.rb +56 -0
  297. data/tasks/autodoc/name.rb +19 -0
  298. data/tasks/autodoc/notice.rb +26 -0
  299. data/tasks/autodoc/rbs.rb +38 -0
  300. data/tasks/autodoc/rdoc.rb +45 -0
  301. data/tasks/autodoc.rake +53 -0
  302. data/tasks/bump/changelog.rb +3 -3
  303. data/tasks/bump/history.rb +2 -2
  304. data/tasks/bump/links.rb +67 -0
  305. data/tasks/doc.rake +600 -6
  306. data/tasks/example_viewer.html.erb +172 -0
  307. data/tasks/lint.rake +8 -4
  308. data/tasks/resources/index.html.erb +6 -0
  309. data/tasks/sourcehut.rake +70 -30
  310. data/tasks/terminal_preview/app_screenshot.rb +14 -6
  311. data/tasks/terminal_preview/crash_report.rb +7 -9
  312. data/tasks/terminal_preview/launcher_script.rb +4 -6
  313. data/tasks/terminal_preview/preview_collection.rb +4 -6
  314. data/tasks/terminal_preview/safety_confirmation.rb +3 -5
  315. data/tasks/terminal_preview/saved_screenshot.rb +10 -11
  316. data/tasks/terminal_preview/terminal_window.rb +7 -9
  317. data/tasks/test.rake +1 -1
  318. data/tasks/website/index_page.rb +3 -3
  319. data/tasks/website/version.rb +10 -10
  320. data/tasks/website/version_menu.rb +10 -12
  321. data/tasks/website/versioned_documentation.rb +49 -17
  322. data/tasks/website/website.rb +6 -8
  323. data/tasks/website.rake +4 -4
  324. metadata +232 -127
  325. data/LICENSES/BSD-2-Clause.txt +0 -9
  326. data/doc/contributors/better_dx.md +0 -543
  327. data/doc/contributors/example_analysis.md +0 -82
  328. data/doc/images/all_events.png +0 -0
  329. data/doc/images/block_padding.png +0 -0
  330. data/doc/images/block_titles.png +0 -0
  331. data/doc/images/box_demo.png +0 -0
  332. data/doc/images/calendar_demo.png +0 -0
  333. data/doc/images/cell_demo.png +0 -0
  334. data/doc/images/chart_demo.png +0 -0
  335. data/doc/images/flex_layout.png +0 -0
  336. data/doc/images/gauge_demo.png +0 -0
  337. data/doc/images/line_gauge_demo.png +0 -0
  338. data/doc/images/list_demo.png +0 -0
  339. data/doc/images/list_styles.png +0 -0
  340. data/doc/images/login_form.png +0 -0
  341. data/doc/images/quickstart_dsl.png +0 -0
  342. data/doc/images/quickstart_lifecycle.png +0 -0
  343. data/doc/images/readme_usage.png +0 -0
  344. data/doc/images/rich_text.png +0 -0
  345. data/doc/images/scroll_text.png +0 -0
  346. data/doc/images/scrollbar_demo.png +0 -0
  347. data/doc/images/sparkline_demo.png +0 -0
  348. data/doc/images/table_flex.png +0 -0
  349. data/doc/images/table_select.png +0 -0
  350. data/examples/all_events/app.rb +0 -169
  351. data/examples/all_events/app.rbs +0 -7
  352. data/examples/all_events/test_app.rb +0 -139
  353. data/examples/analytics/app.rb +0 -258
  354. data/examples/analytics/app.rbs +0 -7
  355. data/examples/analytics/test_app.rb +0 -132
  356. data/examples/block_padding/app.rb +0 -63
  357. data/examples/block_padding/app.rbs +0 -7
  358. data/examples/block_padding/test_app.rb +0 -31
  359. data/examples/block_titles/app.rb +0 -61
  360. data/examples/block_titles/app.rbs +0 -7
  361. data/examples/block_titles/test_app.rb +0 -34
  362. data/examples/box_demo/app.rbs +0 -7
  363. data/examples/box_demo/test_app.rb +0 -88
  364. data/examples/calendar_demo/app.rb +0 -101
  365. data/examples/calendar_demo/app.rbs +0 -7
  366. data/examples/calendar_demo/test_app.rb +0 -108
  367. data/examples/cell_demo/app.rb +0 -108
  368. data/examples/cell_demo/app.rbs +0 -7
  369. data/examples/cell_demo/test_app.rb +0 -36
  370. data/examples/chart_demo/app.rb +0 -203
  371. data/examples/chart_demo/app.rbs +0 -7
  372. data/examples/chart_demo/test_app.rb +0 -102
  373. data/examples/custom_widget/app.rb +0 -51
  374. data/examples/custom_widget/app.rbs +0 -7
  375. data/examples/custom_widget/test_app.rb +0 -30
  376. data/examples/flex_layout/app.rb +0 -156
  377. data/examples/flex_layout/app.rbs +0 -7
  378. data/examples/flex_layout/test_app.rb +0 -65
  379. data/examples/gauge_demo/app.rb +0 -182
  380. data/examples/gauge_demo/test_app.rb +0 -120
  381. data/examples/hit_test/app.rb +0 -175
  382. data/examples/hit_test/app.rbs +0 -7
  383. data/examples/hit_test/test_app.rb +0 -102
  384. data/examples/line_gauge_demo/app.rb +0 -190
  385. data/examples/line_gauge_demo/app.rbs +0 -7
  386. data/examples/line_gauge_demo/test_app.rb +0 -129
  387. data/examples/list_demo/app.rb +0 -253
  388. data/examples/list_demo/test_app.rb +0 -237
  389. data/examples/list_styles/app.rb +0 -140
  390. data/examples/list_styles/app.rbs +0 -7
  391. data/examples/list_styles/test_app.rb +0 -157
  392. data/examples/login_form/app.rbs +0 -7
  393. data/examples/login_form/test_app.rb +0 -51
  394. data/examples/map_demo/app.rbs +0 -7
  395. data/examples/map_demo/test_app.rb +0 -149
  396. data/examples/mouse_events/app.rb +0 -97
  397. data/examples/mouse_events/app.rbs +0 -7
  398. data/examples/mouse_events/test_app.rb +0 -53
  399. data/examples/popup_demo/app.rb +0 -103
  400. data/examples/popup_demo/app.rbs +0 -7
  401. data/examples/popup_demo/test_app.rb +0 -54
  402. data/examples/quickstart_dsl/app.rbs +0 -7
  403. data/examples/quickstart_dsl/test_app.rb +0 -29
  404. data/examples/quickstart_lifecycle/app.rb +0 -39
  405. data/examples/quickstart_lifecycle/app.rbs +0 -7
  406. data/examples/quickstart_lifecycle/test_app.rb +0 -29
  407. data/examples/ratatui_logo_demo/app.rb +0 -79
  408. data/examples/ratatui_logo_demo/app.rbs +0 -7
  409. data/examples/ratatui_logo_demo/test_app.rb +0 -51
  410. data/examples/ratatui_mascot_demo/app.rb +0 -84
  411. data/examples/ratatui_mascot_demo/app.rbs +0 -7
  412. data/examples/ratatui_mascot_demo/test_app.rb +0 -47
  413. data/examples/readme_usage/app.rb +0 -29
  414. data/examples/readme_usage/app.rbs +0 -7
  415. data/examples/readme_usage/test_app.rb +0 -29
  416. data/examples/rich_text/app.rb +0 -141
  417. data/examples/rich_text/app.rbs +0 -7
  418. data/examples/rich_text/test_app.rb +0 -166
  419. data/examples/scroll_text/app.rb +0 -103
  420. data/examples/scroll_text/app.rbs +0 -7
  421. data/examples/scroll_text/test_app.rb +0 -110
  422. data/examples/scrollbar_demo/app.rb +0 -143
  423. data/examples/scrollbar_demo/app.rbs +0 -7
  424. data/examples/scrollbar_demo/test_app.rb +0 -77
  425. data/examples/sparkline_demo/app.rb +0 -240
  426. data/examples/sparkline_demo/test_app.rb +0 -107
  427. data/examples/table_flex/app.rb +0 -65
  428. data/examples/table_flex/app.rbs +0 -7
  429. data/examples/table_flex/test_app.rb +0 -36
  430. data/examples/table_select/app.rb +0 -198
  431. data/examples/table_select/app.rbs +0 -7
  432. data/examples/table_select/test_app.rb +0 -180
  433. data/examples/widget_style_colors/test_app.rb +0 -48
  434. data/tasks/bump/comparison_links.rb +0 -41
  435. /data/doc/images/{analytics.png → app_analytics.png} +0 -0
  436. /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
  437. /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
  438. /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
  439. /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
  440. /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
  441. /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
@@ -0,0 +1,169 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! `ScrollbarState` wrapper for exposing Ratatui's `ScrollbarState` to Ruby.
5
+ //!
6
+ //! This module provides `RubyScrollbarState`, a Magnus-wrapped struct that holds
7
+ //! a `RefCell<ScrollbarState>` for interior mutability during stateful rendering.
8
+
9
+ use magnus::{function, method, prelude::*, Error, Module, Ruby};
10
+ use ratatui::widgets::ScrollbarState;
11
+ use std::cell::RefCell;
12
+
13
+ /// A wrapper around Ratatui's `ScrollbarState` exposed to Ruby.
14
+ ///
15
+ /// Ratatui's `ScrollbarState` doesn't expose getters for `position`, `content_length`,
16
+ /// or `viewport_content_length`. We track these values internally.
17
+ #[magnus::wrap(class = "RatatuiRuby::ScrollbarState")]
18
+ pub struct RubyScrollbarState {
19
+ inner: RefCell<ScrollbarState>,
20
+ /// We store these values ourselves since Ratatui's `ScrollbarState`
21
+ /// doesn't expose getters for them.
22
+ position_val: RefCell<usize>,
23
+ content_len: RefCell<usize>,
24
+ viewport_len: RefCell<usize>,
25
+ }
26
+
27
+ impl RubyScrollbarState {
28
+ /// Creates a new `RubyScrollbarState` with the given content length.
29
+ pub fn new(content_length: usize) -> Self {
30
+ Self {
31
+ inner: RefCell::new(ScrollbarState::new(content_length)),
32
+ position_val: RefCell::new(0),
33
+ content_len: RefCell::new(content_length),
34
+ viewport_len: RefCell::new(0),
35
+ }
36
+ }
37
+
38
+ /// Returns the current scroll position.
39
+ pub fn position(&self) -> usize {
40
+ *self.position_val.borrow()
41
+ }
42
+
43
+ /// Sets the current scroll position.
44
+ pub fn set_position(&self, position: usize) {
45
+ *self.position_val.borrow_mut() = position;
46
+ let mut state = self.inner.borrow_mut();
47
+ *state = state.position(position);
48
+ }
49
+
50
+ /// Returns the total content length.
51
+ pub fn content_length(&self) -> usize {
52
+ *self.content_len.borrow()
53
+ }
54
+
55
+ /// Sets the total content length.
56
+ pub fn set_content_length(&self, length: usize) {
57
+ *self.content_len.borrow_mut() = length;
58
+ let mut state = self.inner.borrow_mut();
59
+ *state = state.content_length(length);
60
+ }
61
+
62
+ /// Returns the viewport content length.
63
+ pub fn viewport_content_length(&self) -> usize {
64
+ *self.viewport_len.borrow()
65
+ }
66
+
67
+ /// Sets the viewport content length.
68
+ pub fn set_viewport_content_length(&self, length: usize) {
69
+ *self.viewport_len.borrow_mut() = length;
70
+ let mut state = self.inner.borrow_mut();
71
+ *state = state.viewport_content_length(length);
72
+ }
73
+
74
+ /// Scrolls to the first position.
75
+ pub fn first(&self) {
76
+ *self.position_val.borrow_mut() = 0;
77
+ self.inner.borrow_mut().first();
78
+ }
79
+
80
+ /// Scrolls to the last position.
81
+ pub fn last(&self) {
82
+ let content_len = *self.content_len.borrow();
83
+ let new_pos = content_len.saturating_sub(1);
84
+ *self.position_val.borrow_mut() = new_pos;
85
+ self.inner.borrow_mut().last();
86
+ }
87
+
88
+ /// Scrolls to the next position.
89
+ pub fn next(&self) {
90
+ let content_len = *self.content_len.borrow();
91
+ let current = *self.position_val.borrow();
92
+ let new_pos = (current + 1).min(content_len.saturating_sub(1));
93
+ *self.position_val.borrow_mut() = new_pos;
94
+ self.inner.borrow_mut().next();
95
+ }
96
+
97
+ /// Scrolls to the previous position.
98
+ pub fn prev(&self) {
99
+ let current = *self.position_val.borrow();
100
+ let new_pos = current.saturating_sub(1);
101
+ *self.position_val.borrow_mut() = new_pos;
102
+ self.inner.borrow_mut().prev();
103
+ }
104
+
105
+ /// Borrows the inner `ScrollbarState` mutably for rendering.
106
+ pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ScrollbarState> {
107
+ self.inner.borrow_mut()
108
+ }
109
+ }
110
+
111
+ /// Registers the `ScrollbarState` class with Ruby.
112
+ pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
113
+ let class = module.define_class("ScrollbarState", ruby.class_object())?;
114
+ class.define_singleton_method("new", function!(RubyScrollbarState::new, 1))?;
115
+ class.define_method("position", method!(RubyScrollbarState::position, 0))?;
116
+ class.define_method("position=", method!(RubyScrollbarState::set_position, 1))?;
117
+ class.define_method(
118
+ "content_length",
119
+ method!(RubyScrollbarState::content_length, 0),
120
+ )?;
121
+ class.define_method(
122
+ "content_length=",
123
+ method!(RubyScrollbarState::set_content_length, 1),
124
+ )?;
125
+ class.define_method(
126
+ "viewport_content_length",
127
+ method!(RubyScrollbarState::viewport_content_length, 0),
128
+ )?;
129
+ class.define_method(
130
+ "viewport_content_length=",
131
+ method!(RubyScrollbarState::set_viewport_content_length, 1),
132
+ )?;
133
+ class.define_method("first", method!(RubyScrollbarState::first, 0))?;
134
+ class.define_method("last", method!(RubyScrollbarState::last, 0))?;
135
+ class.define_method("next", method!(RubyScrollbarState::next, 0))?;
136
+ class.define_method("prev", method!(RubyScrollbarState::prev, 0))?;
137
+ Ok(())
138
+ }
139
+
140
+ #[cfg(test)]
141
+ mod tests {
142
+ use super::*;
143
+
144
+ #[test]
145
+ fn test_new_with_content_length() {
146
+ let state = RubyScrollbarState::new(100);
147
+ assert_eq!(state.content_length(), 100);
148
+ assert_eq!(state.position(), 0);
149
+ }
150
+
151
+ #[test]
152
+ fn test_position_navigation() {
153
+ let state = RubyScrollbarState::new(10);
154
+ state.next();
155
+ assert_eq!(state.position(), 1);
156
+ state.prev();
157
+ assert_eq!(state.position(), 0);
158
+ }
159
+
160
+ #[test]
161
+ fn test_first_and_last() {
162
+ let state = RubyScrollbarState::new(10);
163
+ state.set_position(5);
164
+ state.first();
165
+ assert_eq!(state.position(), 0);
166
+ state.last();
167
+ assert_eq!(state.position(), 9);
168
+ }
169
+ }
@@ -1,10 +1,10 @@
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::style::{parse_block, parse_style, parse_bar_set};
4
+ use crate::style::{parse_bar_set, parse_block, parse_style};
5
5
  use bumpalo::Bump;
6
6
  use magnus::{prelude::*, Error, RString, Value};
7
- use ratatui::{layout::Rect, widgets::Sparkline, widgets::RenderDirection, Frame};
7
+ use ratatui::{layout::Rect, widgets::RenderDirection, widgets::Sparkline, Frame};
8
8
 
9
9
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
10
  let bump = Bump::new();
@@ -20,7 +20,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
20
20
 
21
21
  let mut data_vec = Vec::new();
22
22
  for i in 0..data_val.len() {
23
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
23
+ let index = isize::try_from(i)
24
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
24
25
  let val: Value = data_val.entry(index)?;
25
26
  if val.is_nil() {
26
27
  data_vec.push(None);
@@ -100,22 +101,24 @@ mod tests {
100
101
  fn test_sparkline_absent_value_symbol() {
101
102
  // Data with absent (None) and present values: [Some(5), None, Some(8), None]
102
103
  let data = vec![Some(5), None, Some(8), None];
103
- let sparkline = Sparkline::default()
104
- .data(&data)
105
- .absent_value_symbol("-");
104
+ let sparkline = Sparkline::default().data(&data).absent_value_symbol("-");
106
105
  let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
107
106
  sparkline.render(Rect::new(0, 0, 4, 1), &mut buf);
108
-
107
+
109
108
  // Collect all rendered symbols
110
109
  let symbols: Vec<&str> = buf.content().iter().map(|c| c.symbol()).collect();
111
-
110
+
112
111
  // Check that we have 4 cells rendered
113
- assert_eq!(symbols.len(), 4, "Should have 4 cells rendered for 4 data points");
114
-
112
+ assert_eq!(
113
+ symbols.len(),
114
+ 4,
115
+ "Should have 4 cells rendered for 4 data points"
116
+ );
117
+
115
118
  // Absent values (None) should render as "-"
116
119
  assert_eq!(symbols[1], "-", "Second value (None) should render as dash");
117
120
  assert_eq!(symbols[3], "-", "Fourth value (None) should render as dash");
118
-
121
+
119
122
  // Present values should not be dashes
120
123
  assert_ne!(symbols[0], "-", "First value (Some(5)) should not be dash");
121
124
  assert_ne!(symbols[2], "-", "Third value (Some(8)) should not be dash");
@@ -2,8 +2,9 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use crate::widgets::table_state::RubyTableState;
5
6
  use bumpalo::Bump;
6
- use magnus::{prelude::*, Error, Symbol, Value};
7
+ use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
7
8
  use ratatui::{
8
9
  layout::{Constraint, Flex, Rect},
9
10
  widgets::{Cell, HighlightSpacing, Row, Table, TableState},
@@ -33,7 +34,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
33
34
 
34
35
  let mut rows = Vec::new();
35
36
  for i in 0..rows_array.len() {
36
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
37
+ let index = isize::try_from(i)
38
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
37
39
  let row_val: Value = rows_array.entry(index)?;
38
40
  rows.push(parse_row(row_val)?);
39
41
  }
@@ -109,10 +111,121 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
109
111
  state.select_column(Some(index));
110
112
  }
111
113
 
114
+ let offset_val: Value = node.funcall("offset", ())?;
115
+ if !offset_val.is_nil() {
116
+ let offset: usize = offset_val.funcall("to_int", ())?;
117
+ *state.offset_mut() = offset;
118
+ }
119
+
112
120
  frame.render_stateful_widget(table, area, &mut state);
113
121
  Ok(())
114
122
  }
115
123
 
124
+ /// Renders a Table with an external state object.
125
+ ///
126
+ /// This function ignores `selected_row`, `selected_column`, and `offset` from the widget.
127
+ /// The State object is the single source of truth for selection and scroll position.
128
+ pub fn render_stateful(
129
+ frame: &mut Frame,
130
+ area: Rect,
131
+ node: Value,
132
+ state_wrapper: Value,
133
+ ) -> Result<(), Error> {
134
+ let bump = Bump::new();
135
+ let ruby = magnus::Ruby::get().unwrap();
136
+
137
+ // Extract the RubyTableState wrapper
138
+ let state: &RubyTableState = TryConvert::try_convert(state_wrapper)?;
139
+
140
+ // Parse rows
141
+ let rows_value: Value = node.funcall("rows", ())?;
142
+ let rows_array = magnus::RArray::from_value(rows_value)
143
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
144
+ let widths_val: Value = node.funcall("widths", ())?;
145
+ let widths_array = magnus::RArray::from_value(widths_val)
146
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
147
+
148
+ let mut rows = Vec::new();
149
+ for i in 0..rows_array.len() {
150
+ let index = isize::try_from(i)
151
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
152
+ let row_val: Value = rows_array.entry(index)?;
153
+ rows.push(parse_row(row_val)?);
154
+ }
155
+
156
+ let constraints = parse_constraints(widths_array)?;
157
+
158
+ // Build table (ignoring selected_row, selected_column, offset — State is truth)
159
+ let header_val: Value = node.funcall("header", ())?;
160
+ let footer_val: Value = node.funcall("footer", ())?;
161
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
162
+ let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
163
+ let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
164
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
165
+ let block_val: Value = node.funcall("block", ())?;
166
+ let flex_sym: Symbol = node.funcall("flex", ())?;
167
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
168
+ let style_val: Value = node.funcall("style", ())?;
169
+ let column_spacing_val: Value = node.funcall("column_spacing", ())?;
170
+
171
+ let flex = match flex_sym.to_string().as_str() {
172
+ "start" => Flex::Start,
173
+ "center" => Flex::Center,
174
+ "end" => Flex::End,
175
+ "space_between" => Flex::SpaceBetween,
176
+ "space_around" => Flex::SpaceAround,
177
+ "space_evenly" => Flex::SpaceEvenly,
178
+ _ => Flex::Legacy,
179
+ };
180
+
181
+ let mut table = Table::new(rows, constraints).flex(flex);
182
+
183
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
184
+ "always" => HighlightSpacing::Always,
185
+ "never" => HighlightSpacing::Never,
186
+ _ => HighlightSpacing::WhenSelected,
187
+ };
188
+ table = table.highlight_spacing(highlight_spacing);
189
+
190
+ if !header_val.is_nil() {
191
+ table = table.header(parse_row(header_val)?);
192
+ }
193
+ if !footer_val.is_nil() {
194
+ table = table.footer(parse_row(footer_val)?);
195
+ }
196
+ if !block_val.is_nil() {
197
+ table = table.block(parse_block(block_val, &bump)?);
198
+ }
199
+ if !highlight_style_val.is_nil() {
200
+ table = table.row_highlight_style(parse_style(highlight_style_val)?);
201
+ }
202
+ if !column_highlight_style_val.is_nil() {
203
+ table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
204
+ }
205
+ if !cell_highlight_style_val.is_nil() {
206
+ table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
207
+ }
208
+ if !highlight_symbol_val.is_nil() {
209
+ let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
210
+ table = table.highlight_symbol(symbol);
211
+ }
212
+ if !style_val.is_nil() {
213
+ table = table.style(parse_style(style_val)?);
214
+ }
215
+ if !column_spacing_val.is_nil() {
216
+ let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
217
+ table = table.column_spacing(spacing);
218
+ }
219
+
220
+ // Borrow the inner TableState, render, and release the borrow immediately
221
+ {
222
+ let mut inner_state = state.borrow_mut();
223
+ frame.render_stateful_widget(table, area, &mut inner_state);
224
+ }
225
+
226
+ Ok(())
227
+ }
228
+
116
229
  fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
117
230
  let ruby = magnus::Ruby::get().unwrap();
118
231
  let row_array = magnus::RArray::from_value(row_val)
@@ -120,7 +233,8 @@ fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
120
233
 
121
234
  let mut cells = Vec::new();
122
235
  for i in 0..row_array.len() {
123
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
236
+ let index = isize::try_from(i)
237
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
124
238
  let cell_val: Value = row_array.entry(index)?;
125
239
  cells.push(parse_cell(cell_val)?);
126
240
  }
@@ -159,7 +273,8 @@ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
159
273
  if let Some(mods_array) = magnus::RArray::from_value(modifiers_val) {
160
274
  let ruby = magnus::Ruby::get().unwrap();
161
275
  for i in 0..mods_array.len() {
162
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
276
+ let index = isize::try_from(i)
277
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
163
278
  let mod_str: String = mods_array.entry::<String>(index)?;
164
279
  if let Some(modifier) = crate::style::parse_modifier_str(&mod_str) {
165
280
  style = style.add_modifier(modifier);
@@ -177,7 +292,8 @@ fn parse_constraints(widths_array: magnus::RArray) -> Result<Vec<Constraint>, Er
177
292
  let ruby = magnus::Ruby::get().unwrap();
178
293
  let mut constraints = Vec::new();
179
294
  for i in 0..widths_array.len() {
180
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
295
+ let index = isize::try_from(i)
296
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
181
297
  let constraint_obj: Value = widths_array.entry(index)?;
182
298
  let type_sym: Symbol = constraint_obj.funcall("type", ())?;
183
299
  let value_obj: Value = constraint_obj.funcall("value", ())?;
@@ -0,0 +1,121 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! `TableState` wrapper for exposing Ratatui's `TableState` to Ruby.
5
+ //!
6
+ //! This module provides `RubyTableState`, a Magnus-wrapped struct that holds
7
+ //! a `RefCell<TableState>` for interior mutability during stateful rendering.
8
+ //!
9
+ //! # Design
10
+ //!
11
+ //! When using `render_stateful_widget`, the State object is the single source
12
+ //! of truth for selection and offset. Widget properties (`selected_row`,
13
+ //! `selected_column`, `offset`) are ignored in stateful mode.
14
+
15
+ use magnus::{function, method, prelude::*, Error, Module, Ruby};
16
+ use ratatui::widgets::TableState;
17
+ use std::cell::RefCell;
18
+
19
+ /// A wrapper around Ratatui's `TableState` exposed to Ruby.
20
+ #[magnus::wrap(class = "RatatuiRuby::TableState")]
21
+ pub struct RubyTableState {
22
+ inner: RefCell<TableState>,
23
+ }
24
+
25
+ impl RubyTableState {
26
+ /// Creates a new `RubyTableState` with optional initial selection.
27
+ pub fn new(selected: Option<usize>) -> Self {
28
+ let mut state = TableState::default();
29
+ if let Some(idx) = selected {
30
+ state.select(Some(idx));
31
+ }
32
+ Self {
33
+ inner: RefCell::new(state),
34
+ }
35
+ }
36
+
37
+ /// Sets the selected row index.
38
+ pub fn select(&self, index: Option<usize>) {
39
+ self.inner.borrow_mut().select(index);
40
+ }
41
+
42
+ /// Returns the currently selected row index.
43
+ pub fn selected(&self) -> Option<usize> {
44
+ self.inner.borrow().selected()
45
+ }
46
+
47
+ /// Sets the selected column index.
48
+ pub fn select_column(&self, index: Option<usize>) {
49
+ self.inner.borrow_mut().select_column(index);
50
+ }
51
+
52
+ /// Returns the currently selected column index.
53
+ pub fn selected_column(&self) -> Option<usize> {
54
+ self.inner.borrow().selected_column()
55
+ }
56
+
57
+ /// Returns the current scroll offset.
58
+ pub fn offset(&self) -> usize {
59
+ self.inner.borrow().offset()
60
+ }
61
+
62
+ /// Scrolls down by the given number of rows.
63
+ pub fn scroll_down_by(&self, amount: u16) {
64
+ self.inner.borrow_mut().scroll_down_by(amount);
65
+ }
66
+
67
+ /// Scrolls up by the given number of rows.
68
+ pub fn scroll_up_by(&self, amount: u16) {
69
+ self.inner.borrow_mut().scroll_up_by(amount);
70
+ }
71
+
72
+ /// Borrows the inner `TableState` mutably for rendering.
73
+ pub fn borrow_mut(&self) -> std::cell::RefMut<'_, TableState> {
74
+ self.inner.borrow_mut()
75
+ }
76
+ }
77
+
78
+ /// Registers the `TableState` class with Ruby.
79
+ pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
80
+ let class = module.define_class("TableState", ruby.class_object())?;
81
+ class.define_singleton_method("new", function!(RubyTableState::new, 1))?;
82
+ class.define_method("select", method!(RubyTableState::select, 1))?;
83
+ class.define_method("selected", method!(RubyTableState::selected, 0))?;
84
+ class.define_method("select_column", method!(RubyTableState::select_column, 1))?;
85
+ class.define_method(
86
+ "selected_column",
87
+ method!(RubyTableState::selected_column, 0),
88
+ )?;
89
+ class.define_method("offset", method!(RubyTableState::offset, 0))?;
90
+ class.define_method("scroll_down_by", method!(RubyTableState::scroll_down_by, 1))?;
91
+ class.define_method("scroll_up_by", method!(RubyTableState::scroll_up_by, 1))?;
92
+ Ok(())
93
+ }
94
+
95
+ #[cfg(test)]
96
+ mod tests {
97
+ use super::*;
98
+
99
+ #[test]
100
+ fn test_new_with_no_selection() {
101
+ let state = RubyTableState::new(None);
102
+ assert_eq!(state.selected(), None);
103
+ assert_eq!(state.selected_column(), None);
104
+ assert_eq!(state.offset(), 0);
105
+ }
106
+
107
+ #[test]
108
+ fn test_new_with_selection() {
109
+ let state = RubyTableState::new(Some(3));
110
+ assert_eq!(state.selected(), Some(3));
111
+ }
112
+
113
+ #[test]
114
+ fn test_column_selection() {
115
+ let state = RubyTableState::new(None);
116
+ state.select_column(Some(2));
117
+ assert_eq!(state.selected_column(), Some(2));
118
+ state.select_column(None);
119
+ assert_eq!(state.selected_column(), None);
120
+ }
121
+ }
@@ -15,7 +15,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
15
15
 
16
16
  use crate::text::parse_line;
17
17
 
18
- fn create_tabs<'a>(node: Value, bump: &'a Bump) -> Result<Tabs<'a>, Error> {
18
+ fn create_tabs(node: Value, bump: &Bump) -> Result<Tabs<'_>, Error> {
19
19
  let ruby = magnus::Ruby::get().unwrap();
20
20
  let titles_val: Value = node.funcall("titles", ())?;
21
21
  let selected_index: usize = node.funcall("selected_index", ())?;
@@ -30,7 +30,8 @@ fn create_tabs<'a>(node: Value, bump: &'a Bump) -> Result<Tabs<'a>, Error> {
30
30
 
31
31
  let mut titles = Vec::new();
32
32
  for i in 0..titles_array.len() {
33
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
33
+ let index = isize::try_from(i)
34
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
34
35
  let val: Value = titles_array.entry(index)?;
35
36
  if let Ok(line) = parse_line(val) {
36
37
  titles.push(line);
@@ -81,10 +82,11 @@ pub fn width(node: Value) -> Result<usize, Error> {
81
82
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for titles"))?;
82
83
 
83
84
  let mut total_width = padding_left + padding_right;
84
-
85
+
85
86
  let mut titles_count = 0;
86
87
  for i in 0..titles_array.len() {
87
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
88
+ let index = isize::try_from(i)
89
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
88
90
  let val: Value = titles_array.entry(index)?;
89
91
  let line_width = if let Ok(line) = parse_line(val) {
90
92
  line.width()
@@ -97,11 +99,11 @@ pub fn width(node: Value) -> Result<usize, Error> {
97
99
  }
98
100
 
99
101
  if titles_count > 1 {
100
- let divider_width = if !divider_val.is_nil() {
102
+ let divider_width = if divider_val.is_nil() {
103
+ 1 // Default divider is "|"
104
+ } else {
101
105
  let d: String = divider_val.funcall("to_s", ())?;
102
106
  ratatui::text::Span::raw(d).width()
103
- } else {
104
- 1 // Default divider is "|"
105
107
  };
106
108
  total_width += (titles_count - 1) * divider_width;
107
109
  }
@@ -134,10 +136,8 @@ mod tests {
134
136
  fn test_tabs_highlight_style() {
135
137
  let titles = vec![Line::from("Tab1"), Line::from("Tab2")];
136
138
  let highlight_style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
137
- let tabs = Tabs::new(titles)
138
- .select(0)
139
- .highlight_style(highlight_style);
140
-
139
+ let tabs = Tabs::new(titles).select(0).highlight_style(highlight_style);
140
+
141
141
  let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
142
142
  tabs.render(Rect::new(0, 0, 15, 1), &mut buf);
143
143
 
@@ -67,7 +67,7 @@ module RatatuiRuby
67
67
  # Cell.symbol("X") # => #<RatatuiRuby::Cell symbol="X">
68
68
  #
69
69
  def self.symbol(symbol)
70
- new(symbol: symbol, fg: nil, bg: nil, modifiers: [])
70
+ new(symbol:, fg: nil, bg: nil, modifiers: [])
71
71
  end
72
72
 
73
73
  # Alias for Rubyists who prefer a shorter name.
@@ -82,10 +82,10 @@ module RatatuiRuby
82
82
  # [bg] Symbol or String (nullable).
83
83
  # [modifiers] Array of Strings.
84
84
  def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
85
- @symbol = symbol || char || " "
86
- @fg = fg
87
- @bg = bg
88
- @modifiers = modifiers.freeze
85
+ @symbol = (symbol || char || " ").freeze
86
+ @fg = fg&.freeze
87
+ @bg = bg&.freeze
88
+ @modifiers = modifiers.map(&:freeze).freeze
89
89
  freeze
90
90
  end
91
91
 
@@ -149,7 +149,7 @@ module RatatuiRuby
149
149
  parts << "fg=#{fg.inspect}" if fg
150
150
  parts << "bg=#{bg.inspect}" if bg
151
151
  parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
152
- "#<#{self.class} #{parts.join(" ")}>"
152
+ "#<#{self.class} #{parts.join(' ')}>"
153
153
  end
154
154
 
155
155
  # Returns the cell's character.
@@ -160,7 +160,7 @@ module RatatuiRuby
160
160
  # Support for pattern matching.
161
161
  # Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
162
162
  def deconstruct_keys(keys)
163
- { symbol: symbol, char: symbol, fg: fg, bg: bg, modifiers: modifiers }
163
+ { symbol:, char: symbol, fg:, bg:, modifiers: }
164
164
  end
165
165
  end
166
166
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ module RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods for handling printable characters.
10
+ module Character
11
+ # Returns true if the key represents a single printable character.
12
+ #
13
+ # RatatuiRuby::Event::Key.new(code: "a").text? # => true
14
+ # RatatuiRuby::Event::Key.new(code: "enter").text? # => false
15
+ # RatatuiRuby::Event::Key.new(code: "space").text? # => false ("space" is not 1 char, " " is)
16
+ def text?
17
+ @code.length == 1
18
+ end
19
+
20
+ # Returns the key as a printable character (if applicable).
21
+ #
22
+ # [Printable Characters]
23
+ # Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
24
+ # [Special Keys]
25
+ # Returns <tt>nil</tt> (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt>).
26
+ #
27
+ # RatatuiRuby::Event::Key.new(code: "a").char # => "a"
28
+ # RatatuiRuby::Event::Key.new(code: "enter").char # => nil
29
+ def char
30
+ text? ? @code : nil
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end