ratatui_ruby 0.5.0 → 0.7.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 (311) 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 +10 -4
  7. data/CHANGELOG.md +79 -7
  8. data/README.md +37 -5
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +96 -22
  11. data/doc/application_testing.md +76 -30
  12. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  13. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  14. data/doc/contributors/architectural_overhaul/task.md +37 -0
  15. data/doc/contributors/design/ruby_frontend.md +288 -56
  16. data/doc/contributors/design/rust_backend.md +349 -54
  17. data/doc/contributors/developing_examples.md +134 -49
  18. data/doc/contributors/index.md +7 -5
  19. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  20. data/doc/event_handling.md +11 -3
  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_canvas_demo.png +0 -0
  32. data/doc/images/widget_cell_demo.png +0 -0
  33. data/doc/images/widget_center_demo.png +0 -0
  34. data/doc/images/widget_chart_demo.png +0 -0
  35. data/doc/images/widget_list_demo.png +0 -0
  36. data/doc/images/widget_overlay_demo.png +0 -0
  37. data/doc/images/widget_render.png +0 -0
  38. data/doc/images/widget_rich_text.png +0 -0
  39. data/doc/images/widget_scroll_text.png +0 -0
  40. data/doc/images/widget_sparkline_demo.png +0 -0
  41. data/doc/images/widget_table_demo.png +0 -0
  42. data/doc/images/widget_tabs_demo.png +0 -0
  43. data/doc/images/widget_text_width.png +0 -0
  44. data/doc/index.md +11 -6
  45. data/doc/interactive_design.md +2 -2
  46. data/doc/quickstart.md +127 -165
  47. data/doc/terminal_limitations.md +92 -0
  48. data/doc/v0.7.0_migration.md +236 -0
  49. data/doc/why.md +93 -0
  50. data/examples/app_all_events/README.md +47 -27
  51. data/examples/app_all_events/app.rb +38 -35
  52. data/examples/app_all_events/model/app_model.rb +157 -0
  53. data/examples/app_all_events/model/event_entry.rb +17 -0
  54. data/examples/app_all_events/model/msg.rb +37 -0
  55. data/examples/app_all_events/update.rb +73 -0
  56. data/examples/app_all_events/view/app_view.rb +9 -9
  57. data/examples/app_all_events/view/controls_view.rb +9 -7
  58. data/examples/app_all_events/view/counts_view.rb +13 -9
  59. data/examples/app_all_events/view/live_view.rb +9 -8
  60. data/examples/app_all_events/view/log_view.rb +11 -16
  61. data/examples/app_color_picker/README.md +84 -42
  62. data/examples/app_color_picker/app.rb +24 -62
  63. data/examples/app_color_picker/controls.rb +90 -0
  64. data/examples/app_color_picker/copy_dialog.rb +45 -49
  65. data/examples/app_color_picker/export_pane.rb +126 -0
  66. data/examples/app_color_picker/input.rb +99 -67
  67. data/examples/app_color_picker/main_container.rb +178 -0
  68. data/examples/app_color_picker/palette.rb +55 -26
  69. data/examples/app_login_form/README.md +49 -0
  70. data/examples/app_login_form/app.rb +2 -3
  71. data/examples/app_stateful_interaction/README.md +33 -0
  72. data/examples/app_stateful_interaction/app.rb +272 -0
  73. data/examples/timeout_demo.rb +43 -0
  74. data/examples/verify_quickstart_dsl/README.md +49 -0
  75. data/examples/verify_quickstart_dsl/app.rb +2 -0
  76. data/examples/verify_quickstart_layout/README.md +71 -0
  77. data/examples/verify_quickstart_layout/app.rb +2 -0
  78. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  79. data/examples/verify_quickstart_lifecycle/app.rb +10 -4
  80. data/examples/verify_readme_usage/README.md +43 -0
  81. data/examples/verify_readme_usage/app.rb +8 -2
  82. data/examples/widget_barchart_demo/README.md +50 -0
  83. data/examples/widget_barchart_demo/app.rb +5 -5
  84. data/examples/widget_block_demo/README.md +36 -0
  85. data/examples/widget_block_demo/app.rb +256 -0
  86. data/examples/widget_box_demo/README.md +45 -0
  87. data/examples/widget_calendar_demo/README.md +39 -0
  88. data/examples/widget_calendar_demo/app.rb +5 -1
  89. data/examples/widget_canvas_demo/README.md +27 -0
  90. data/examples/widget_canvas_demo/app.rb +123 -0
  91. data/examples/widget_cell_demo/README.md +36 -0
  92. data/examples/widget_cell_demo/app.rb +31 -24
  93. data/examples/widget_center_demo/README.md +29 -0
  94. data/examples/widget_center_demo/app.rb +116 -0
  95. data/examples/widget_chart_demo/README.md +41 -0
  96. data/examples/widget_chart_demo/app.rb +7 -2
  97. data/examples/widget_gauge_demo/README.md +41 -0
  98. data/examples/widget_layout_split/README.md +44 -0
  99. data/examples/widget_line_gauge_demo/README.md +41 -0
  100. data/examples/widget_list_demo/README.md +49 -0
  101. data/examples/widget_list_demo/app.rb +91 -107
  102. data/examples/widget_map_demo/README.md +39 -0
  103. data/examples/{app_map_demo → widget_map_demo}/app.rb +4 -4
  104. data/examples/widget_overlay_demo/README.md +36 -0
  105. data/examples/widget_overlay_demo/app.rb +248 -0
  106. data/examples/widget_popup_demo/README.md +36 -0
  107. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  108. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  109. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  110. data/examples/widget_rect/README.md +38 -0
  111. data/examples/widget_render/README.md +37 -0
  112. data/examples/widget_render/app.rb +3 -3
  113. data/examples/widget_rich_text/README.md +35 -0
  114. data/examples/widget_rich_text/app.rb +62 -33
  115. data/examples/widget_scroll_text/README.md +37 -0
  116. data/examples/widget_scroll_text/app.rb +0 -1
  117. data/examples/widget_scrollbar_demo/README.md +37 -0
  118. data/examples/widget_sparkline_demo/README.md +42 -0
  119. data/examples/widget_sparkline_demo/app.rb +4 -3
  120. data/examples/widget_style_colors/README.md +34 -0
  121. data/examples/widget_table_demo/README.md +48 -0
  122. data/examples/{app_table_select → widget_table_demo}/app.rb +65 -12
  123. data/examples/widget_tabs_demo/README.md +41 -0
  124. data/examples/widget_tabs_demo/app.rb +15 -1
  125. data/examples/widget_text_width/README.md +35 -0
  126. data/examples/widget_text_width/app.rb +113 -0
  127. data/exe/.gitkeep +0 -0
  128. data/ext/ratatui_ruby/Cargo.lock +11 -4
  129. data/ext/ratatui_ruby/Cargo.toml +2 -1
  130. data/ext/ratatui_ruby/src/events.rs +238 -26
  131. data/ext/ratatui_ruby/src/frame.rs +116 -3
  132. data/ext/ratatui_ruby/src/lib.rs +37 -6
  133. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  134. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  135. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  136. data/ext/ratatui_ruby/src/text.rs +13 -4
  137. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  138. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  139. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  140. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  141. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  142. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  143. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  144. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  145. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  146. data/ext/ratatui_ruby/src/widgets/table.rs +191 -34
  147. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  148. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  149. data/lib/ratatui_ruby/buffer.rb +15 -0
  150. data/lib/ratatui_ruby/cell.rb +4 -4
  151. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  152. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  153. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  154. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  155. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  156. data/lib/ratatui_ruby/event/key.rb +111 -51
  157. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  158. data/lib/ratatui_ruby/event/paste.rb +1 -1
  159. data/lib/ratatui_ruby/frame.rb +100 -4
  160. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  161. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  162. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  163. data/lib/ratatui_ruby/layout.rb +19 -0
  164. data/lib/ratatui_ruby/list_state.rb +88 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  166. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  167. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  168. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  169. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  170. data/lib/ratatui_ruby/schema/list.rb +25 -4
  171. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  172. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  173. data/lib/ratatui_ruby/schema/row.rb +66 -0
  174. data/lib/ratatui_ruby/schema/style.rb +24 -4
  175. data/lib/ratatui_ruby/schema/table.rb +29 -11
  176. data/lib/ratatui_ruby/schema/text.rb +96 -3
  177. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  178. data/lib/ratatui_ruby/style/style.rb +81 -0
  179. data/lib/ratatui_ruby/style.rb +15 -0
  180. data/lib/ratatui_ruby/table_state.rb +90 -0
  181. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  182. data/lib/ratatui_ruby/test_helper/snapshot.rb +414 -0
  183. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  184. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  185. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  186. data/lib/ratatui_ruby/test_helper.rb +65 -358
  187. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  188. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  189. data/lib/ratatui_ruby/tui/core.rb +38 -0
  190. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  191. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  192. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  193. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  194. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  195. data/lib/ratatui_ruby/tui.rb +75 -0
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  198. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  199. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  200. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  201. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  202. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  203. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  204. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  205. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  206. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  207. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  208. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  209. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  210. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  211. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  212. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  213. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  214. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  215. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  216. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  217. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  218. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  219. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  220. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  221. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  222. data/lib/ratatui_ruby/widgets.rb +40 -0
  223. data/lib/ratatui_ruby.rb +64 -57
  224. data/sig/examples/app_all_events/view.rbs +1 -1
  225. data/sig/examples/app_all_events/view_state.rbs +1 -1
  226. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  227. data/sig/examples/widget_block_demo/app.rbs +32 -0
  228. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  229. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  230. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  231. data/sig/ratatui_ruby/event.rbs +11 -1
  232. data/sig/ratatui_ruby/frame.rbs +2 -0
  233. data/sig/ratatui_ruby/list_state.rbs +13 -0
  234. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  235. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  236. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  237. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  238. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  239. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  240. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  241. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  242. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  243. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  244. data/sig/ratatui_ruby/schema/text.rbs +9 -6
  245. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  246. data/sig/ratatui_ruby/session.rbs +41 -48
  247. data/sig/ratatui_ruby/table_state.rbs +15 -0
  248. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  249. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  250. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  251. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  252. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  253. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  254. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  255. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  256. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  257. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  258. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  259. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  260. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  261. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  262. data/sig/ratatui_ruby/tui.rbs +19 -0
  263. data/tasks/autodoc/examples.rb +79 -0
  264. data/tasks/autodoc.rake +7 -35
  265. data/tasks/bump/changelog.rb +3 -3
  266. data/tasks/bump/links.rb +67 -0
  267. data/tasks/sourcehut.rake +64 -21
  268. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  269. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  270. metadata +169 -48
  271. data/doc/contributors/dwim_dx.md +0 -366
  272. data/doc/images/app_analytics.png +0 -0
  273. data/doc/images/app_custom_widget.png +0 -0
  274. data/doc/images/app_mouse_events.png +0 -0
  275. data/doc/images/app_table_select.png +0 -0
  276. data/doc/images/widget_block_padding.png +0 -0
  277. data/doc/images/widget_block_titles.png +0 -0
  278. data/doc/images/widget_list_styles.png +0 -0
  279. data/doc/images/widget_table_flex.png +0 -0
  280. data/examples/app_all_events/model/events.rb +0 -180
  281. data/examples/app_all_events/model/highlight.rb +0 -57
  282. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  283. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  284. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  285. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  286. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  287. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  288. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  289. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  290. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  291. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  292. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  293. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  294. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  295. data/examples/app_all_events/view_state.rb +0 -42
  296. data/examples/app_color_picker/scene.rb +0 -201
  297. data/examples/widget_block_padding/app.rb +0 -67
  298. data/examples/widget_block_titles/app.rb +0 -69
  299. data/examples/widget_list_styles/app.rb +0 -141
  300. data/examples/widget_table_flex/app.rb +0 -95
  301. data/lib/ratatui_ruby/session/autodoc.rb +0 -417
  302. data/lib/ratatui_ruby/session.rb +0 -163
  303. data/sig/examples/widget_block_padding/app.rbs +0 -11
  304. data/sig/examples/widget_block_titles/app.rbs +0 -11
  305. data/sig/examples/widget_list_styles/app.rbs +0 -11
  306. data/tasks/autodoc/inventory.rb +0 -61
  307. data/tasks/autodoc/notice.rb +0 -26
  308. data/tasks/autodoc/rbs.rb +0 -38
  309. data/tasks/autodoc/rdoc.rb +0 -45
  310. data/tasks/bump/comparison_links.rb +0 -41
  311. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
@@ -2,8 +2,10 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use crate::text::{parse_line, parse_span};
6
+ use crate::widgets::table_state::RubyTableState;
5
7
  use bumpalo::Bump;
6
- use magnus::{prelude::*, Error, Symbol, Value};
8
+ use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
7
9
  use ratatui::{
8
10
  layout::{Constraint, Flex, Rect},
9
11
  widgets::{Cell, HighlightSpacing, Row, Table, TableState},
@@ -21,7 +23,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
21
23
  let widths_val: Value = node.funcall("widths", ())?;
22
24
  let widths_array = magnus::RArray::from_value(widths_val)
23
25
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
24
- let highlight_style_val: Value = node.funcall("highlight_style", ())?;
26
+ let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
25
27
  let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
26
28
  let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
27
29
  let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
@@ -72,8 +74,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
72
74
  table = table.block(parse_block(block_val, &bump)?);
73
75
  }
74
76
 
75
- if !highlight_style_val.is_nil() {
76
- table = table.row_highlight_style(parse_style(highlight_style_val)?);
77
+ if !row_highlight_style_val.is_nil() {
78
+ table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
77
79
  }
78
80
 
79
81
  if !column_highlight_style_val.is_nil() {
@@ -110,12 +112,170 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
110
112
  state.select_column(Some(index));
111
113
  }
112
114
 
115
+ let offset_val: Value = node.funcall("offset", ())?;
116
+ if !offset_val.is_nil() {
117
+ let offset: usize = offset_val.funcall("to_int", ())?;
118
+ *state.offset_mut() = offset;
119
+ }
120
+
113
121
  frame.render_stateful_widget(table, area, &mut state);
114
122
  Ok(())
115
123
  }
116
124
 
125
+ /// Renders a Table with an external state object.
126
+ ///
127
+ /// This function ignores `selected_row`, `selected_column`, and `offset` from the widget.
128
+ /// The State object is the single source of truth for selection and scroll position.
129
+ pub fn render_stateful(
130
+ frame: &mut Frame,
131
+ area: Rect,
132
+ node: Value,
133
+ state_wrapper: Value,
134
+ ) -> Result<(), Error> {
135
+ let bump = Bump::new();
136
+ let ruby = magnus::Ruby::get().unwrap();
137
+
138
+ // Extract the RubyTableState wrapper
139
+ let state: &RubyTableState = TryConvert::try_convert(state_wrapper)?;
140
+
141
+ // Parse rows
142
+ let rows_value: Value = node.funcall("rows", ())?;
143
+ let rows_array = magnus::RArray::from_value(rows_value)
144
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
145
+ let widths_val: Value = node.funcall("widths", ())?;
146
+ let widths_array = magnus::RArray::from_value(widths_val)
147
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
148
+
149
+ let mut rows = Vec::new();
150
+ for i in 0..rows_array.len() {
151
+ let index = isize::try_from(i)
152
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
153
+ let row_val: Value = rows_array.entry(index)?;
154
+ rows.push(parse_row(row_val)?);
155
+ }
156
+
157
+ let constraints = parse_constraints(widths_array)?;
158
+
159
+ // Build table (ignoring selected_row, selected_column, offset — State is truth)
160
+ let header_val: Value = node.funcall("header", ())?;
161
+ let footer_val: Value = node.funcall("footer", ())?;
162
+ let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
163
+ let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
164
+ let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
165
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
166
+ let block_val: Value = node.funcall("block", ())?;
167
+ let flex_sym: Symbol = node.funcall("flex", ())?;
168
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
169
+ let style_val: Value = node.funcall("style", ())?;
170
+ let column_spacing_val: Value = node.funcall("column_spacing", ())?;
171
+
172
+ let flex = match flex_sym.to_string().as_str() {
173
+ "start" => Flex::Start,
174
+ "center" => Flex::Center,
175
+ "end" => Flex::End,
176
+ "space_between" => Flex::SpaceBetween,
177
+ "space_around" => Flex::SpaceAround,
178
+ "space_evenly" => Flex::SpaceEvenly,
179
+ _ => Flex::Legacy,
180
+ };
181
+
182
+ let mut table = Table::new(rows, constraints).flex(flex);
183
+
184
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
185
+ "always" => HighlightSpacing::Always,
186
+ "never" => HighlightSpacing::Never,
187
+ _ => HighlightSpacing::WhenSelected,
188
+ };
189
+ table = table.highlight_spacing(highlight_spacing);
190
+
191
+ if !header_val.is_nil() {
192
+ table = table.header(parse_row(header_val)?);
193
+ }
194
+ if !footer_val.is_nil() {
195
+ table = table.footer(parse_row(footer_val)?);
196
+ }
197
+ if !block_val.is_nil() {
198
+ table = table.block(parse_block(block_val, &bump)?);
199
+ }
200
+ if !row_highlight_style_val.is_nil() {
201
+ table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
202
+ }
203
+ if !column_highlight_style_val.is_nil() {
204
+ table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
205
+ }
206
+ if !cell_highlight_style_val.is_nil() {
207
+ table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
208
+ }
209
+ if !highlight_symbol_val.is_nil() {
210
+ let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
211
+ table = table.highlight_symbol(symbol);
212
+ }
213
+ if !style_val.is_nil() {
214
+ table = table.style(parse_style(style_val)?);
215
+ }
216
+ if !column_spacing_val.is_nil() {
217
+ let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
218
+ table = table.column_spacing(spacing);
219
+ }
220
+
221
+ // Borrow the inner TableState, render, and release the borrow immediately
222
+ {
223
+ let mut inner_state = state.borrow_mut();
224
+ frame.render_stateful_widget(table, area, &mut inner_state);
225
+ }
226
+
227
+ Ok(())
228
+ }
229
+
117
230
  fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
118
231
  let ruby = magnus::Ruby::get().unwrap();
232
+
233
+ // Check if this is a RatatuiRuby::Row object with cells + style + height + margins
234
+ let class = row_val.class();
235
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
236
+ let class_name = unsafe { class.name() }.into_owned();
237
+
238
+ if class_name == "RatatuiRuby::Widgets::Row" {
239
+ let cells_val: Value = row_val.funcall("cells", ())?;
240
+ let style_val: Value = row_val.funcall("style", ())?;
241
+ let height_val: Value = row_val.funcall("height", ())?;
242
+ let top_margin_val: Value = row_val.funcall("top_margin", ())?;
243
+ let bottom_margin_val: Value = row_val.funcall("bottom_margin", ())?;
244
+
245
+ let cells_array = magnus::RArray::from_value(cells_val).ok_or_else(|| {
246
+ Error::new(ruby.exception_type_error(), "expected array for Row.cells")
247
+ })?;
248
+
249
+ let mut cells = Vec::new();
250
+ for i in 0..cells_array.len() {
251
+ let index = isize::try_from(i)
252
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
253
+ let entry_val: Value = cells_array.entry(index)?;
254
+ cells.push(parse_cell(entry_val)?);
255
+ }
256
+
257
+ let mut row = Row::new(cells);
258
+
259
+ if !style_val.is_nil() {
260
+ row = row.style(parse_style(style_val)?);
261
+ }
262
+ if !height_val.is_nil() {
263
+ let h: u16 = height_val.funcall("to_int", ())?;
264
+ row = row.height(h);
265
+ }
266
+ if !top_margin_val.is_nil() {
267
+ let m: u16 = top_margin_val.funcall("to_int", ())?;
268
+ row = row.top_margin(m);
269
+ }
270
+ if !bottom_margin_val.is_nil() {
271
+ let m: u16 = bottom_margin_val.funcall("to_int", ())?;
272
+ row = row.bottom_margin(m);
273
+ }
274
+
275
+ return Ok(row);
276
+ }
277
+
278
+ // Fallback: plain array of cells
119
279
  let row_array = magnus::RArray::from_value(row_val)
120
280
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
121
281
 
@@ -134,42 +294,39 @@ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
134
294
  // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
135
295
  let class_name = unsafe { class.name() }.into_owned();
136
296
 
137
- if class_name == "RatatuiRuby::Paragraph" {
297
+ // Try Text::Line first (contains multiple spans)
298
+ if class_name.contains("Line") {
299
+ if let Ok(line) = parse_line(cell_val) {
300
+ return Ok(Cell::from(line));
301
+ }
302
+ }
303
+
304
+ // Try Text::Span
305
+ if class_name.contains("Span") {
306
+ if let Ok(span) = parse_span(cell_val) {
307
+ return Ok(Cell::from(ratatui::text::Line::from(vec![span])));
308
+ }
309
+ }
310
+
311
+ if class_name == "RatatuiRuby::Widgets::Paragraph" {
138
312
  let text: String = cell_val.funcall("text", ())?;
139
313
  let style_val: Value = cell_val.funcall("style", ())?;
140
314
  let cell_style = parse_style(style_val)?;
141
315
  Ok(Cell::from(text).style(cell_style))
142
- } else if class_name == "RatatuiRuby::Style" {
316
+ } else if class_name == "RatatuiRuby::Style::Style" {
143
317
  Ok(Cell::from("").style(parse_style(cell_val)?))
144
- } else if class_name == "RatatuiRuby::Cell" {
145
- let symbol: String = cell_val.funcall("char", ())?;
146
- let fg_val: Value = cell_val.funcall("fg", ())?;
147
- let bg_val: Value = cell_val.funcall("bg", ())?;
148
- let modifiers_val: Value = cell_val.funcall("modifiers", ())?;
149
-
150
- let mut style = ratatui::style::Style::default();
151
- if !fg_val.is_nil() {
152
- if let Some(color) = crate::style::parse_color_value(fg_val)? {
153
- style = style.fg(color);
154
- }
155
- }
156
- if !bg_val.is_nil() {
157
- if let Some(color) = crate::style::parse_color_value(bg_val)? {
158
- style = style.bg(color);
159
- }
160
- }
161
- if let Some(mods_array) = magnus::RArray::from_value(modifiers_val) {
162
- let ruby = magnus::Ruby::get().unwrap();
163
- for i in 0..mods_array.len() {
164
- let index = isize::try_from(i)
165
- .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
166
- let mod_str: String = mods_array.entry::<String>(index)?;
167
- if let Some(modifier) = crate::style::parse_modifier_str(&mod_str) {
168
- style = style.add_modifier(modifier);
169
- }
170
- }
318
+ } else if class_name == "RatatuiRuby::Widgets::Cell" {
319
+ // Widgets::Cell has content (String/Span/Line) and optional style
320
+ let content_val: Value = cell_val.funcall("content", ())?;
321
+ let style_val: Value = cell_val.funcall("style", ())?;
322
+
323
+ // Recursively parse the content (could be String, Span, or Line)
324
+ let mut cell = parse_cell(content_val)?;
325
+
326
+ if !style_val.is_nil() {
327
+ cell = cell.style(parse_style(style_val)?);
171
328
  }
172
- Ok(Cell::from(symbol).style(style))
329
+ Ok(cell)
173
330
  } else {
174
331
  let cell_str: String = cell_val.funcall("to_s", ())?;
175
332
  Ok(Cell::from(cell_str))
@@ -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
+ }
@@ -0,0 +1,168 @@
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
+ module Buffer
8
+ # Represents a single cell in the terminal buffer.
9
+ #
10
+ # A terminal grid is made of cells. Each cell contains a character (symbol) and styling (colors, modifiers).
11
+ # When testing, you often need to verify that a specific cell renders correctly.
12
+ #
13
+ # This object encapsulates that state. It provides predicate methods for modifiers, making assertions readable.
14
+ #
15
+ # Use it to inspect the visual state of your application in tests.
16
+ #
17
+ # === Examples
18
+ #
19
+ # cell = RatatuiRuby.get_cell_at(0, 0)
20
+ # cell.char # => "H"
21
+ # cell.fg # => :red
22
+ # cell.bold? # => true
23
+ #
24
+ class Cell
25
+ # The character displayed in the cell.
26
+ #
27
+ # Named to match Ratatui's Cell::symbol() method.
28
+ attr_reader :symbol
29
+
30
+ # Alias for Rubyists who prefer a shorter name.
31
+ alias char symbol
32
+
33
+ # The foreground color of the cell (e.g., :red, :blue, "#ff0000").
34
+ attr_reader :fg
35
+
36
+ # The background color of the cell (e.g., :black, nil).
37
+ attr_reader :bg
38
+
39
+ # The list of active modifiers (e.g., ["bold", "italic"]).
40
+ attr_reader :modifiers
41
+
42
+ # Returns an empty cell (space character, no styles).
43
+ #
44
+ # === Example
45
+ #
46
+ # Buffer::Cell.empty # => #<RatatuiRuby::Buffer::Cell char=" ">
47
+ #
48
+ def self.empty
49
+ new(symbol: " ", fg: nil, bg: nil, modifiers: [])
50
+ end
51
+
52
+ # Returns a default cell (alias for empty).
53
+ #
54
+ # === Example
55
+ #
56
+ # Buffer::Cell.default # => #<RatatuiRuby::Buffer::Cell char=" ">
57
+ #
58
+ def self.default
59
+ empty
60
+ end
61
+
62
+ # Returns a cell with a specific character and no styles.
63
+ #
64
+ # [symbol] String (single character).
65
+ #
66
+ # === Example
67
+ #
68
+ # Buffer::Cell.symbol("X") # => #<RatatuiRuby::Buffer::Cell symbol="X">
69
+ #
70
+ def self.symbol(symbol)
71
+ new(symbol:, fg: nil, bg: nil, modifiers: [])
72
+ end
73
+
74
+ # Alias for Rubyists who prefer a shorter name.
75
+ def self.char(char)
76
+ symbol(char)
77
+ end
78
+
79
+ # Creates a new Cell.
80
+ #
81
+ # [symbol] String (single character). Aliased as <tt>char:</tt>.
82
+ # [fg] Symbol or String (nullable).
83
+ # [bg] Symbol or String (nullable).
84
+ # [modifiers] Array of Strings.
85
+ def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
86
+ @symbol = (symbol || char || " ").freeze
87
+ @fg = fg&.freeze
88
+ @bg = bg&.freeze
89
+ @modifiers = modifiers.map(&:freeze).freeze
90
+ freeze
91
+ end
92
+
93
+ # Returns true if the cell has the bold modifier.
94
+ def bold?
95
+ modifiers.include?("bold")
96
+ end
97
+
98
+ # Returns true if the cell has the dim modifier.
99
+ def dim?
100
+ modifiers.include?("dim")
101
+ end
102
+
103
+ # Returns true if the cell has the italic modifier.
104
+ def italic?
105
+ modifiers.include?("italic")
106
+ end
107
+
108
+ # Returns true if the cell has the underlined modifier.
109
+ def underlined?
110
+ modifiers.include?("underlined")
111
+ end
112
+
113
+ # Returns true if the cell has the slow_blink modifier.
114
+ def slow_blink?
115
+ modifiers.include?("slow_blink")
116
+ end
117
+
118
+ # Returns true if the cell has the rapid_blink modifier.
119
+ def rapid_blink?
120
+ modifiers.include?("rapid_blink")
121
+ end
122
+
123
+ # Returns true if the cell has the reversed modifier.
124
+ def reversed?
125
+ modifiers.include?("reversed")
126
+ end
127
+
128
+ # Returns true if the cell has the hidden modifier.
129
+ def hidden?
130
+ modifiers.include?("hidden")
131
+ end
132
+
133
+ # Returns true if the cell has the crossed_out modifier.
134
+ def crossed_out?
135
+ modifiers.include?("crossed_out")
136
+ end
137
+
138
+ # Checks equality with another Cell.
139
+ def ==(other)
140
+ other.is_a?(Cell) &&
141
+ char == other.char &&
142
+ fg == other.fg &&
143
+ bg == other.bg &&
144
+ modifiers == other.modifiers
145
+ end
146
+
147
+ # Returns a string representation of the cell.
148
+ def inspect
149
+ parts = ["symbol=#{symbol.inspect}"]
150
+ parts << "fg=#{fg.inspect}" if fg
151
+ parts << "bg=#{bg.inspect}" if bg
152
+ parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
153
+ "#<#{self.class} #{parts.join(' ')}>"
154
+ end
155
+
156
+ # Returns the cell's character.
157
+ def to_s
158
+ symbol
159
+ end
160
+
161
+ # Support for pattern matching.
162
+ # Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
163
+ def deconstruct_keys(keys)
164
+ { symbol:, char: symbol, fg:, bg:, modifiers: }
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,15 @@
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
+ # Buffer primitives for terminal cell inspection.
8
+ #
9
+ # This module mirrors +ratatui::buffer+ and contains:
10
+ # - {Cell} — Single terminal cell (for inspection)
11
+ module Buffer
12
+ end
13
+ end
14
+
15
+ require_relative "buffer/cell"
@@ -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
 
@@ -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
@@ -0,0 +1,44 @@
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 and logic for media keys.
10
+ module Media
11
+ # Returns true if this is a media key.
12
+ #
13
+ # Media keys include: play, pause, stop, track controls, volume controls.
14
+ # These are only available in terminals supporting the Kitty keyboard protocol.
15
+ #
16
+ # event.media? # => true for media_play, media_pause, etc.
17
+ def media?
18
+ @kind == :media
19
+ end
20
+
21
+ # Handles media-specific DWIM logic for method_missing.
22
+ private def match_media_dwim?(key_name)
23
+ return false unless @kind == :media
24
+
25
+ # Allow unprefixed predicate
26
+ # e.g., pause? returns true for media_pause
27
+ if @code.start_with?("media_")
28
+ base_code = @code.delete_prefix("media_")
29
+ return true if key_name == base_code
30
+ end
31
+
32
+ # Bidirectional media overlaps
33
+ # e.g., play? and pause? both match media_play_pause
34
+ return true if @code == "media_play_pause" && (key_name == "play" || key_name == "pause")
35
+
36
+ # e.g., play_pause? matches media_play or media_pause
37
+ return true if key_name == "play_pause" && (@code == "media_play" || @code == "media_pause")
38
+
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end