ratatui_ruby 1.4.0-x86_64-linux

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 (292) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +15 -0
  3. data/LICENSES/AGPL-3.0-or-later.txt +661 -0
  4. data/LICENSES/CC-BY-SA-4.0.txt +427 -0
  5. data/LICENSES/CC0-1.0.txt +121 -0
  6. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  7. data/LICENSES/MIT-0.txt +16 -0
  8. data/LICENSES/MIT.txt +21 -0
  9. data/REUSE.toml +42 -0
  10. data/exe/.gitkeep +0 -0
  11. data/ext/ratatui_ruby/.cargo/config.toml +13 -0
  12. data/ext/ratatui_ruby/.gitignore +4 -0
  13. data/ext/ratatui_ruby/Cargo.lock +1737 -0
  14. data/ext/ratatui_ruby/Cargo.toml +24 -0
  15. data/ext/ratatui_ruby/clippy.toml +7 -0
  16. data/ext/ratatui_ruby/extconf.rb +21 -0
  17. data/ext/ratatui_ruby/src/color.rs +82 -0
  18. data/ext/ratatui_ruby/src/errors.rs +28 -0
  19. data/ext/ratatui_ruby/src/events.rs +700 -0
  20. data/ext/ratatui_ruby/src/frame.rs +241 -0
  21. data/ext/ratatui_ruby/src/lib.rs +343 -0
  22. data/ext/ratatui_ruby/src/lib_header.rs +11 -0
  23. data/ext/ratatui_ruby/src/rendering.rs +158 -0
  24. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  25. data/ext/ratatui_ruby/src/style.rs +469 -0
  26. data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
  27. data/ext/ratatui_ruby/src/terminal/init.rs +233 -0
  28. data/ext/ratatui_ruby/src/terminal/mod.rs +42 -0
  29. data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
  30. data/ext/ratatui_ruby/src/terminal/queries.rs +231 -0
  31. data/ext/ratatui_ruby/src/terminal/query.rs +400 -0
  32. data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
  33. data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
  34. data/ext/ratatui_ruby/src/text.rs +225 -0
  35. data/ext/ratatui_ruby/src/widgets/barchart.rs +169 -0
  36. data/ext/ratatui_ruby/src/widgets/block.rs +41 -0
  37. data/ext/ratatui_ruby/src/widgets/calendar.rs +84 -0
  38. data/ext/ratatui_ruby/src/widgets/canvas.rs +183 -0
  39. data/ext/ratatui_ruby/src/widgets/center.rs +79 -0
  40. data/ext/ratatui_ruby/src/widgets/chart.rs +222 -0
  41. data/ext/ratatui_ruby/src/widgets/clear.rs +39 -0
  42. data/ext/ratatui_ruby/src/widgets/cursor.rs +32 -0
  43. data/ext/ratatui_ruby/src/widgets/gauge.rs +65 -0
  44. data/ext/ratatui_ruby/src/widgets/layout.rs +379 -0
  45. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +100 -0
  46. data/ext/ratatui_ruby/src/widgets/list.rs +378 -0
  47. data/ext/ratatui_ruby/src/widgets/list_state.rs +173 -0
  48. data/ext/ratatui_ruby/src/widgets/mod.rs +26 -0
  49. data/ext/ratatui_ruby/src/widgets/overlay.rs +24 -0
  50. data/ext/ratatui_ruby/src/widgets/paragraph.rs +87 -0
  51. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  52. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +55 -0
  53. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +214 -0
  54. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  55. data/ext/ratatui_ruby/src/widgets/sparkline.rs +127 -0
  56. data/ext/ratatui_ruby/src/widgets/table.rs +415 -0
  57. data/ext/ratatui_ruby/src/widgets/table_state.rs +203 -0
  58. data/ext/ratatui_ruby/src/widgets/tabs.rs +194 -0
  59. data/lib/ratatui_ruby/backend/window_size.rb +50 -0
  60. data/lib/ratatui_ruby/backend.rb +59 -0
  61. data/lib/ratatui_ruby/buffer/cell.rb +212 -0
  62. data/lib/ratatui_ruby/buffer.rb +149 -0
  63. data/lib/ratatui_ruby/cell.rb +208 -0
  64. data/lib/ratatui_ruby/debug.rb +215 -0
  65. data/lib/ratatui_ruby/draw.rb +63 -0
  66. data/lib/ratatui_ruby/event/focus_gained.rb +125 -0
  67. data/lib/ratatui_ruby/event/focus_lost.rb +127 -0
  68. data/lib/ratatui_ruby/event/key/character.rb +53 -0
  69. data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
  70. data/lib/ratatui_ruby/event/key/media.rb +46 -0
  71. data/lib/ratatui_ruby/event/key/modifier.rb +107 -0
  72. data/lib/ratatui_ruby/event/key/navigation.rb +72 -0
  73. data/lib/ratatui_ruby/event/key/system.rb +47 -0
  74. data/lib/ratatui_ruby/event/key.rb +479 -0
  75. data/lib/ratatui_ruby/event/mouse.rb +291 -0
  76. data/lib/ratatui_ruby/event/none.rb +53 -0
  77. data/lib/ratatui_ruby/event/paste.rb +130 -0
  78. data/lib/ratatui_ruby/event/resize.rb +221 -0
  79. data/lib/ratatui_ruby/event/sync.rb +52 -0
  80. data/lib/ratatui_ruby/event.rb +163 -0
  81. data/lib/ratatui_ruby/frame.rb +257 -0
  82. data/lib/ratatui_ruby/labs/a11y.rb +182 -0
  83. data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
  84. data/lib/ratatui_ruby/labs.rb +47 -0
  85. data/lib/ratatui_ruby/layout/alignment.rb +91 -0
  86. data/lib/ratatui_ruby/layout/constraint.rb +337 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +258 -0
  88. data/lib/ratatui_ruby/layout/position.rb +81 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +733 -0
  90. data/lib/ratatui_ruby/layout/size.rb +62 -0
  91. data/lib/ratatui_ruby/layout.rb +29 -0
  92. data/lib/ratatui_ruby/list_state.rb +201 -0
  93. data/lib/ratatui_ruby/output_guard.rb +171 -0
  94. data/lib/ratatui_ruby/ratatui_ruby.so +0 -0
  95. data/lib/ratatui_ruby/scrollbar_state.rb +122 -0
  96. data/lib/ratatui_ruby/style/color.rb +149 -0
  97. data/lib/ratatui_ruby/style/style.rb +147 -0
  98. data/lib/ratatui_ruby/style.rb +19 -0
  99. data/lib/ratatui_ruby/symbols.rb +435 -0
  100. data/lib/ratatui_ruby/synthetic_events.rb +106 -0
  101. data/lib/ratatui_ruby/table_state.rb +251 -0
  102. data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
  103. data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
  104. data/lib/ratatui_ruby/terminal.rb +66 -0
  105. data/lib/ratatui_ruby/terminal_lifecycle.rb +303 -0
  106. data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
  107. data/lib/ratatui_ruby/test_helper/event_injection.rb +241 -0
  108. data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
  109. data/lib/ratatui_ruby/test_helper/snapshot.rb +568 -0
  110. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
  111. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
  112. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
  113. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
  114. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
  115. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
  116. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
  117. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
  118. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
  119. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
  120. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
  121. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
  122. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
  123. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
  124. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
  125. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
  126. data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
  127. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
  128. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
  129. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
  130. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
  131. data/lib/ratatui_ruby/test_helper/style_assertions.rb +449 -0
  132. data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
  133. data/lib/ratatui_ruby/test_helper/terminal.rb +187 -0
  134. data/lib/ratatui_ruby/test_helper/test_doubles.rb +86 -0
  135. data/lib/ratatui_ruby/test_helper.rb +115 -0
  136. data/lib/ratatui_ruby/text/line.rb +245 -0
  137. data/lib/ratatui_ruby/text/span.rb +158 -0
  138. data/lib/ratatui_ruby/text.rb +99 -0
  139. data/lib/ratatui_ruby/tui/buffer_factories.rb +22 -0
  140. data/lib/ratatui_ruby/tui/canvas_factories.rb +149 -0
  141. data/lib/ratatui_ruby/tui/core.rb +67 -0
  142. data/lib/ratatui_ruby/tui/layout_factories.rb +153 -0
  143. data/lib/ratatui_ruby/tui/state_factories.rb +77 -0
  144. data/lib/ratatui_ruby/tui/style_factories.rb +22 -0
  145. data/lib/ratatui_ruby/tui/text_factories.rb +86 -0
  146. data/lib/ratatui_ruby/tui/widget_factories.rb +272 -0
  147. data/lib/ratatui_ruby/tui.rb +106 -0
  148. data/lib/ratatui_ruby/version.rb +12 -0
  149. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +51 -0
  150. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +29 -0
  151. data/lib/ratatui_ruby/widgets/bar_chart.rb +308 -0
  152. data/lib/ratatui_ruby/widgets/block.rb +266 -0
  153. data/lib/ratatui_ruby/widgets/calendar.rb +88 -0
  154. data/lib/ratatui_ruby/widgets/canvas.rb +297 -0
  155. data/lib/ratatui_ruby/widgets/cell.rb +59 -0
  156. data/lib/ratatui_ruby/widgets/center.rb +71 -0
  157. data/lib/ratatui_ruby/widgets/chart.rb +172 -0
  158. data/lib/ratatui_ruby/widgets/clear.rb +66 -0
  159. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  160. data/lib/ratatui_ruby/widgets/cursor.rb +54 -0
  161. data/lib/ratatui_ruby/widgets/gauge.rb +146 -0
  162. data/lib/ratatui_ruby/widgets/line_gauge.rb +158 -0
  163. data/lib/ratatui_ruby/widgets/list.rb +252 -0
  164. data/lib/ratatui_ruby/widgets/list_item.rb +55 -0
  165. data/lib/ratatui_ruby/widgets/overlay.rb +55 -0
  166. data/lib/ratatui_ruby/widgets/paragraph.rb +113 -0
  167. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +35 -0
  168. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +40 -0
  169. data/lib/ratatui_ruby/widgets/row.rb +123 -0
  170. data/lib/ratatui_ruby/widgets/scrollbar.rb +147 -0
  171. data/lib/ratatui_ruby/widgets/shape/label.rb +80 -0
  172. data/lib/ratatui_ruby/widgets/sparkline.rb +153 -0
  173. data/lib/ratatui_ruby/widgets/table.rb +213 -0
  174. data/lib/ratatui_ruby/widgets/tabs.rb +91 -0
  175. data/lib/ratatui_ruby/widgets.rb +43 -0
  176. data/lib/ratatui_ruby.rb +555 -0
  177. data/sig/examples/app_all_events/app.rbs +11 -0
  178. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  179. data/sig/examples/app_all_events/model/event_entry.rbs +23 -0
  180. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  181. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  182. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  183. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  184. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  185. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  186. data/sig/examples/app_all_events/view.rbs +14 -0
  187. data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
  188. data/sig/examples/app_color_picker/app.rbs +17 -0
  189. data/sig/examples/app_external_editor/app.rbs +12 -0
  190. data/sig/examples/app_login_form/app.rbs +11 -0
  191. data/sig/examples/app_stateful_interaction/app.rbs +39 -0
  192. data/sig/examples/verify_quickstart_dsl/app.rbs +17 -0
  193. data/sig/examples/verify_quickstart_lifecycle/app.rbs +17 -0
  194. data/sig/examples/verify_readme_usage/app.rbs +17 -0
  195. data/sig/examples/widget_block_demo/app.rbs +38 -0
  196. data/sig/examples/widget_box_demo/app.rbs +17 -0
  197. data/sig/examples/widget_calendar_demo/app.rbs +17 -0
  198. data/sig/examples/widget_cell_demo/app.rbs +17 -0
  199. data/sig/examples/widget_chart_demo/app.rbs +17 -0
  200. data/sig/examples/widget_gauge_demo/app.rbs +17 -0
  201. data/sig/examples/widget_layout_split/app.rbs +16 -0
  202. data/sig/examples/widget_line_gauge_demo/app.rbs +17 -0
  203. data/sig/examples/widget_list_demo/app.rbs +17 -0
  204. data/sig/examples/widget_map_demo/app.rbs +17 -0
  205. data/sig/examples/widget_popup_demo/app.rbs +17 -0
  206. data/sig/examples/widget_ratatui_logo_demo/app.rbs +17 -0
  207. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +17 -0
  208. data/sig/examples/widget_rect/app.rbs +18 -0
  209. data/sig/examples/widget_render/app.rbs +16 -0
  210. data/sig/examples/widget_rich_text/app.rbs +17 -0
  211. data/sig/examples/widget_scroll_text/app.rbs +17 -0
  212. data/sig/examples/widget_scrollbar_demo/app.rbs +17 -0
  213. data/sig/examples/widget_sparkline_demo/app.rbs +16 -0
  214. data/sig/examples/widget_style_colors/app.rbs +20 -0
  215. data/sig/examples/widget_table_demo/app.rbs +17 -0
  216. data/sig/examples/widget_text_width/app.rbs +16 -0
  217. data/sig/generated/event_key_predicates.rbs +1348 -0
  218. data/sig/manifest.yaml +5 -0
  219. data/sig/patches/data.rbs +26 -0
  220. data/sig/patches/debugger__.rbs +8 -0
  221. data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
  222. data/sig/ratatui_ruby/backend.rbs +12 -0
  223. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  224. data/sig/ratatui_ruby/buffer.rbs +18 -0
  225. data/sig/ratatui_ruby/cell.rbs +44 -0
  226. data/sig/ratatui_ruby/clear.rbs +18 -0
  227. data/sig/ratatui_ruby/constraint.rbs +26 -0
  228. data/sig/ratatui_ruby/debug.rbs +45 -0
  229. data/sig/ratatui_ruby/draw.rbs +30 -0
  230. data/sig/ratatui_ruby/event.rbs +249 -0
  231. data/sig/ratatui_ruby/frame.rbs +23 -0
  232. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  233. data/sig/ratatui_ruby/labs.rbs +90 -0
  234. data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
  235. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  236. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  237. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  238. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  239. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  240. data/sig/ratatui_ruby/list_state.rbs +23 -0
  241. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  242. data/sig/ratatui_ruby/ratatui_ruby.rbs +113 -0
  243. data/sig/ratatui_ruby/rect.rbs +17 -0
  244. data/sig/ratatui_ruby/scrollbar_state.rbs +24 -0
  245. data/sig/ratatui_ruby/session.rbs +93 -0
  246. data/sig/ratatui_ruby/style/color.rbs +22 -0
  247. data/sig/ratatui_ruby/style/style.rbs +29 -0
  248. data/sig/ratatui_ruby/symbols.rbs +141 -0
  249. data/sig/ratatui_ruby/synthetic_events.rbs +24 -0
  250. data/sig/ratatui_ruby/table_state.rbs +27 -0
  251. data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
  252. data/sig/ratatui_ruby/terminal/viewport.rbs +33 -0
  253. data/sig/ratatui_ruby/terminal_lifecycle.rbs +39 -0
  254. data/sig/ratatui_ruby/test_helper/event_injection.rbs +22 -0
  255. data/sig/ratatui_ruby/test_helper/snapshot.rbs +37 -0
  256. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +77 -0
  257. data/sig/ratatui_ruby/test_helper/terminal.rbs +20 -0
  258. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +32 -0
  259. data/sig/ratatui_ruby/test_helper.rbs +18 -0
  260. data/sig/ratatui_ruby/text/line.rbs +27 -0
  261. data/sig/ratatui_ruby/text/span.rbs +23 -0
  262. data/sig/ratatui_ruby/text.rbs +12 -0
  263. data/sig/ratatui_ruby/tui/buffer_factories.rbs +16 -0
  264. data/sig/ratatui_ruby/tui/canvas_factories.rbs +38 -0
  265. data/sig/ratatui_ruby/tui/core.rbs +23 -0
  266. data/sig/ratatui_ruby/tui/layout_factories.rbs +39 -0
  267. data/sig/ratatui_ruby/tui/state_factories.rbs +23 -0
  268. data/sig/ratatui_ruby/tui/style_factories.rbs +18 -0
  269. data/sig/ratatui_ruby/tui/text_factories.rbs +23 -0
  270. data/sig/ratatui_ruby/tui/widget_factories.rbs +138 -0
  271. data/sig/ratatui_ruby/tui.rbs +25 -0
  272. data/sig/ratatui_ruby/version.rbs +12 -0
  273. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  274. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  275. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  276. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  277. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  278. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  279. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  280. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  281. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  282. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  283. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  284. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  285. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  286. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  287. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  288. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  289. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  290. data/sig/ratatui_ruby/widgets.rbs +16 -0
  291. data/vendor/goodcop/base.yml +1047 -0
  292. metadata +729 -0
@@ -0,0 +1,415 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::errors::type_error_with_context;
5
+ use crate::style::{parse_block, parse_style};
6
+ use crate::text::{parse_line, parse_span, parse_text};
7
+ use crate::widgets::table_state::RubyTableState;
8
+ use bumpalo::Bump;
9
+ use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
10
+ use ratatui::{
11
+ buffer::Buffer,
12
+ layout::{Constraint, Flex, Rect},
13
+ widgets::{Cell, HighlightSpacing, Row, StatefulWidget, Table, TableState},
14
+ };
15
+
16
+ pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
17
+ let bump = Bump::new();
18
+ let ruby = magnus::Ruby::get().unwrap();
19
+ let header_val: Value = node.funcall("header", ())?;
20
+ let footer_val: Value = node.funcall("footer", ())?;
21
+ let rows_value: Value = node.funcall("rows", ())?;
22
+ let rows_array = magnus::RArray::from_value(rows_value)
23
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for rows", rows_value))?;
24
+ let widths_val: Value = node.funcall("widths", ())?;
25
+ let widths_array = magnus::RArray::from_value(widths_val)
26
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for widths", widths_val))?;
27
+ let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
28
+ let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
29
+ let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
30
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
31
+ let selected_row_val: Value = node.funcall("selected_row", ())?;
32
+ let selected_column_val: Value = node.funcall("selected_column", ())?;
33
+ let block_val: Value = node.funcall("block", ())?;
34
+ let flex_sym: Symbol = node.funcall("flex", ())?;
35
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
36
+
37
+ let mut rows = Vec::new();
38
+ for i in 0..rows_array.len() {
39
+ let index = isize::try_from(i)
40
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
41
+ let row_val: Value = rows_array.entry(index)?;
42
+ rows.push(parse_row(row_val)?);
43
+ }
44
+
45
+ let constraints = parse_constraints(widths_array)?;
46
+
47
+ let flex = match flex_sym.to_string().as_str() {
48
+ "start" => Flex::Start,
49
+ "center" => Flex::Center,
50
+ "end" => Flex::End,
51
+ "space_between" => Flex::SpaceBetween,
52
+ "space_around" => Flex::SpaceAround,
53
+ "space_evenly" => Flex::SpaceEvenly,
54
+ _ => Flex::Legacy,
55
+ };
56
+
57
+ let mut table = Table::new(rows, constraints).flex(flex);
58
+
59
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
60
+ "always" => HighlightSpacing::Always,
61
+ "never" => HighlightSpacing::Never,
62
+ _ => HighlightSpacing::WhenSelected,
63
+ };
64
+ table = table.highlight_spacing(highlight_spacing);
65
+
66
+ if !header_val.is_nil() {
67
+ table = table.header(parse_row(header_val)?);
68
+ }
69
+
70
+ if !footer_val.is_nil() {
71
+ table = table.footer(parse_row(footer_val)?);
72
+ }
73
+
74
+ if !block_val.is_nil() {
75
+ table = table.block(parse_block(block_val, &bump)?);
76
+ }
77
+
78
+ if !row_highlight_style_val.is_nil() {
79
+ table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
80
+ }
81
+
82
+ if !column_highlight_style_val.is_nil() {
83
+ table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
84
+ }
85
+
86
+ if !cell_highlight_style_val.is_nil() {
87
+ table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
88
+ }
89
+
90
+ if !highlight_symbol_val.is_nil() {
91
+ if let Ok(text) = parse_text(highlight_symbol_val) {
92
+ table = table.highlight_symbol(ratatui::text::Text::from(text));
93
+ } else {
94
+ let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
95
+ table = table.highlight_symbol(symbol);
96
+ }
97
+ }
98
+
99
+ let style_val: Value = node.funcall("style", ())?;
100
+ if !style_val.is_nil() {
101
+ table = table.style(parse_style(style_val)?);
102
+ }
103
+
104
+ let column_spacing_val: Value = node.funcall("column_spacing", ())?;
105
+ if !column_spacing_val.is_nil() {
106
+ let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
107
+ table = table.column_spacing(spacing);
108
+ }
109
+
110
+ let mut state = TableState::default();
111
+ if !selected_row_val.is_nil() {
112
+ let index: usize = selected_row_val.funcall("to_int", ())?;
113
+ state.select(Some(index));
114
+ }
115
+ if !selected_column_val.is_nil() {
116
+ let index: usize = selected_column_val.funcall("to_int", ())?;
117
+ state.select_column(Some(index));
118
+ }
119
+
120
+ let offset_val: Value = node.funcall("offset", ())?;
121
+ if !offset_val.is_nil() {
122
+ let offset: usize = offset_val.funcall("to_int", ())?;
123
+ *state.offset_mut() = offset;
124
+ }
125
+
126
+ StatefulWidget::render(table, area, buffer, &mut state);
127
+ Ok(())
128
+ }
129
+
130
+ /// Renders a Table with an external state object.
131
+ ///
132
+ /// This function ignores `selected_row`, `selected_column`, and `offset` from the widget.
133
+ /// The State object is the single source of truth for selection and scroll position.
134
+ pub fn render_stateful(
135
+ buffer: &mut Buffer,
136
+ area: Rect,
137
+ node: Value,
138
+ state_wrapper: Value,
139
+ ) -> Result<(), Error> {
140
+ let bump = Bump::new();
141
+ let ruby = magnus::Ruby::get().unwrap();
142
+
143
+ // Extract the RubyTableState wrapper
144
+ let state: &RubyTableState = TryConvert::try_convert(state_wrapper)?;
145
+
146
+ // Parse rows
147
+ let rows_value: Value = node.funcall("rows", ())?;
148
+ let rows_array = magnus::RArray::from_value(rows_value)
149
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for rows", rows_value))?;
150
+ let widths_val: Value = node.funcall("widths", ())?;
151
+ let widths_array = magnus::RArray::from_value(widths_val)
152
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for widths", widths_val))?;
153
+
154
+ let mut rows = Vec::new();
155
+ for i in 0..rows_array.len() {
156
+ let index = isize::try_from(i)
157
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
158
+ let row_val: Value = rows_array.entry(index)?;
159
+ rows.push(parse_row(row_val)?);
160
+ }
161
+
162
+ let constraints = parse_constraints(widths_array)?;
163
+
164
+ // Build table (ignoring selected_row, selected_column, offset — State is truth)
165
+ let header_val: Value = node.funcall("header", ())?;
166
+ let footer_val: Value = node.funcall("footer", ())?;
167
+ let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
168
+ let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
169
+ let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
170
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
171
+ let block_val: Value = node.funcall("block", ())?;
172
+ let flex_sym: Symbol = node.funcall("flex", ())?;
173
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
174
+ let style_val: Value = node.funcall("style", ())?;
175
+ let column_spacing_val: Value = node.funcall("column_spacing", ())?;
176
+
177
+ let flex = match flex_sym.to_string().as_str() {
178
+ "start" => Flex::Start,
179
+ "center" => Flex::Center,
180
+ "end" => Flex::End,
181
+ "space_between" => Flex::SpaceBetween,
182
+ "space_around" => Flex::SpaceAround,
183
+ "space_evenly" => Flex::SpaceEvenly,
184
+ _ => Flex::Legacy,
185
+ };
186
+
187
+ let mut table = Table::new(rows, constraints).flex(flex);
188
+
189
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
190
+ "always" => HighlightSpacing::Always,
191
+ "never" => HighlightSpacing::Never,
192
+ _ => HighlightSpacing::WhenSelected,
193
+ };
194
+ table = table.highlight_spacing(highlight_spacing);
195
+
196
+ if !header_val.is_nil() {
197
+ table = table.header(parse_row(header_val)?);
198
+ }
199
+ if !footer_val.is_nil() {
200
+ table = table.footer(parse_row(footer_val)?);
201
+ }
202
+ if !block_val.is_nil() {
203
+ table = table.block(parse_block(block_val, &bump)?);
204
+ }
205
+ if !row_highlight_style_val.is_nil() {
206
+ table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
207
+ }
208
+ if !column_highlight_style_val.is_nil() {
209
+ table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
210
+ }
211
+ if !cell_highlight_style_val.is_nil() {
212
+ table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
213
+ }
214
+ if !highlight_symbol_val.is_nil() {
215
+ if let Ok(text) = parse_text(highlight_symbol_val) {
216
+ table = table.highlight_symbol(ratatui::text::Text::from(text));
217
+ } else {
218
+ let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
219
+ table = table.highlight_symbol(symbol);
220
+ }
221
+ }
222
+ if !style_val.is_nil() {
223
+ table = table.style(parse_style(style_val)?);
224
+ }
225
+ if !column_spacing_val.is_nil() {
226
+ let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
227
+ table = table.column_spacing(spacing);
228
+ }
229
+
230
+ // Borrow the inner TableState, render, and release the borrow immediately
231
+ {
232
+ let mut inner_state = state.borrow_mut();
233
+ StatefulWidget::render(table, area, buffer, &mut inner_state);
234
+ }
235
+
236
+ Ok(())
237
+ }
238
+
239
+ fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
240
+ let ruby = magnus::Ruby::get().unwrap();
241
+
242
+ // Check if this is a RatatuiRuby::Row object with cells + style + height + margins
243
+ let class = row_val.class();
244
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
245
+ let class_name = unsafe { class.name() }.into_owned();
246
+
247
+ if class_name == "RatatuiRuby::Widgets::Row" {
248
+ let cells_val: Value = row_val.funcall("cells", ())?;
249
+ let style_val: Value = row_val.funcall("style", ())?;
250
+ let height_val: Value = row_val.funcall("height", ())?;
251
+ let top_margin_val: Value = row_val.funcall("top_margin", ())?;
252
+ let bottom_margin_val: Value = row_val.funcall("bottom_margin", ())?;
253
+
254
+ let cells_array = magnus::RArray::from_value(cells_val).ok_or_else(|| {
255
+ type_error_with_context(&ruby, "expected array for Row.cells", cells_val)
256
+ })?;
257
+
258
+ let mut cells = Vec::new();
259
+ for i in 0..cells_array.len() {
260
+ let index = isize::try_from(i)
261
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
262
+ let entry_val: Value = cells_array.entry(index)?;
263
+ cells.push(parse_cell(entry_val)?);
264
+ }
265
+
266
+ let mut row = Row::new(cells);
267
+
268
+ if !style_val.is_nil() {
269
+ row = row.style(parse_style(style_val)?);
270
+ }
271
+ if !height_val.is_nil() {
272
+ let h: u16 = height_val.funcall("to_int", ())?;
273
+ row = row.height(h);
274
+ }
275
+ if !top_margin_val.is_nil() {
276
+ let m: u16 = top_margin_val.funcall("to_int", ())?;
277
+ row = row.top_margin(m);
278
+ }
279
+ if !bottom_margin_val.is_nil() {
280
+ let m: u16 = bottom_margin_val.funcall("to_int", ())?;
281
+ row = row.bottom_margin(m);
282
+ }
283
+
284
+ return Ok(row);
285
+ }
286
+
287
+ // Fallback: plain array of cells
288
+ let row_array = magnus::RArray::from_value(row_val)
289
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for row", row_val))?;
290
+
291
+ let mut cells = Vec::new();
292
+ for i in 0..row_array.len() {
293
+ let index = isize::try_from(i)
294
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
295
+ let cell_val: Value = row_array.entry(index)?;
296
+ cells.push(parse_cell(cell_val)?);
297
+ }
298
+ Ok(Row::new(cells))
299
+ }
300
+
301
+ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
302
+ let class = cell_val.class();
303
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
304
+ let class_name = unsafe { class.name() }.into_owned();
305
+
306
+ // Try Text::Line first (contains multiple spans)
307
+ if class_name.contains("Line") {
308
+ if let Ok(line) = parse_line(cell_val) {
309
+ return Ok(Cell::from(line));
310
+ }
311
+ }
312
+
313
+ // Try Text::Span
314
+ if class_name.contains("Span") {
315
+ if let Ok(span) = parse_span(cell_val) {
316
+ return Ok(Cell::from(ratatui::text::Line::from(vec![span])));
317
+ }
318
+ }
319
+
320
+ if class_name == "RatatuiRuby::Widgets::Paragraph" {
321
+ let text: String = cell_val.funcall("text", ())?;
322
+ let style_val: Value = cell_val.funcall("style", ())?;
323
+ let cell_style = parse_style(style_val)?;
324
+ Ok(Cell::from(text).style(cell_style))
325
+ } else if class_name == "RatatuiRuby::Style::Style" {
326
+ Ok(Cell::from("").style(parse_style(cell_val)?))
327
+ } else if class_name == "RatatuiRuby::Widgets::Cell" {
328
+ // Widgets::Cell has content (String/Span/Line) and optional style
329
+ let content_val: Value = cell_val.funcall("content", ())?;
330
+ let style_val: Value = cell_val.funcall("style", ())?;
331
+
332
+ // Recursively parse the content (could be String, Span, or Line)
333
+ let mut cell = parse_cell(content_val)?;
334
+
335
+ if !style_val.is_nil() {
336
+ cell = cell.style(parse_style(style_val)?);
337
+ }
338
+ Ok(cell)
339
+ } else {
340
+ let cell_str: String = cell_val.funcall("to_s", ())?;
341
+ Ok(Cell::from(cell_str))
342
+ }
343
+ }
344
+
345
+ fn parse_constraints(widths_array: magnus::RArray) -> Result<Vec<Constraint>, Error> {
346
+ let ruby = magnus::Ruby::get().unwrap();
347
+ let mut constraints = Vec::new();
348
+ for i in 0..widths_array.len() {
349
+ let index = isize::try_from(i)
350
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
351
+ let constraint_obj: Value = widths_array.entry(index)?;
352
+ let type_sym: Symbol = constraint_obj.funcall("type", ())?;
353
+ let value_obj: Value = constraint_obj.funcall("value", ())?;
354
+
355
+ match type_sym.to_string().as_str() {
356
+ "length" => {
357
+ let val = u16::try_convert(value_obj)?;
358
+ constraints.push(Constraint::Length(val));
359
+ }
360
+ "percentage" => {
361
+ let val = u16::try_convert(value_obj)?;
362
+ constraints.push(Constraint::Percentage(val));
363
+ }
364
+ "min" => {
365
+ let val = u16::try_convert(value_obj)?;
366
+ constraints.push(Constraint::Min(val));
367
+ }
368
+ "max" => {
369
+ let val = u16::try_convert(value_obj)?;
370
+ constraints.push(Constraint::Max(val));
371
+ }
372
+ "fill" => {
373
+ let val = u16::try_convert(value_obj)?;
374
+ constraints.push(Constraint::Fill(val));
375
+ }
376
+ "ratio" => {
377
+ if let Some(arr) = magnus::RArray::from_value(value_obj) {
378
+ if arr.len() == 2 {
379
+ let n = u32::try_convert(arr.entry(0)?)?;
380
+ let d = u32::try_convert(arr.entry(1)?)?;
381
+ constraints.push(Constraint::Ratio(n, d));
382
+ }
383
+ }
384
+ }
385
+ _ => {}
386
+ }
387
+ }
388
+ Ok(constraints)
389
+ }
390
+
391
+ #[cfg(test)]
392
+ mod tests {
393
+ use super::*;
394
+ use ratatui::buffer::Buffer;
395
+ use ratatui::widgets::{Row, Table, Widget};
396
+
397
+ #[test]
398
+ fn test_table_rendering() {
399
+ let rows = vec![Row::new(vec!["C1", "C2"])];
400
+ let table = Table::new(rows, [Constraint::Length(3), Constraint::Length(3)])
401
+ .header(Row::new(vec!["H1", "H2"]))
402
+ .footer(Row::new(vec!["F1", "F2"]));
403
+ let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
404
+ Widget::render(table, Rect::new(0, 0, 10, 3), &mut buf);
405
+
406
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
407
+ // Check for presence of header and row content
408
+ assert!(content.contains("H1"));
409
+ assert!(content.contains("H2"));
410
+ assert!(content.contains("F1"));
411
+ assert!(content.contains("F2"));
412
+ assert!(content.contains("C1"));
413
+ assert!(content.contains("C2"));
414
+ }
415
+ }
@@ -0,0 +1,203 @@
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
+ /// Returns the currently selected cell as (row, column) tuple.
73
+ /// Returns None if either row or column is not selected.
74
+ pub fn selected_cell(&self) -> Option<(usize, usize)> {
75
+ self.inner.borrow().selected_cell()
76
+ }
77
+
78
+ /// Selects the next column or the first one if no column is selected.
79
+ pub fn select_next_column(&self) {
80
+ self.inner.borrow_mut().select_next_column();
81
+ }
82
+
83
+ /// Selects the previous column or the last one if no column is selected.
84
+ pub fn select_previous_column(&self) {
85
+ self.inner.borrow_mut().select_previous_column();
86
+ }
87
+
88
+ /// Selects the first column.
89
+ pub fn select_first_column(&self) {
90
+ self.inner.borrow_mut().select_first_column();
91
+ }
92
+
93
+ /// Selects the last column.
94
+ pub fn select_last_column(&self) {
95
+ self.inner.borrow_mut().select_last_column();
96
+ }
97
+
98
+ /// Selects the next row or the first one if no row is selected.
99
+ pub fn select_next(&self) {
100
+ self.inner.borrow_mut().select_next();
101
+ }
102
+
103
+ /// Selects the previous row or the last one if no row is selected.
104
+ pub fn select_previous(&self) {
105
+ self.inner.borrow_mut().select_previous();
106
+ }
107
+
108
+ /// Selects the first row.
109
+ pub fn select_first(&self) {
110
+ self.inner.borrow_mut().select_first();
111
+ }
112
+
113
+ /// Selects the last row.
114
+ pub fn select_last(&self) {
115
+ self.inner.borrow_mut().select_last();
116
+ }
117
+
118
+ /// Creates a new `RubyTableState` with a selected cell (row, column).
119
+ pub fn with_selected_cell(cell: Option<(usize, usize)>) -> Self {
120
+ let state = TableState::default().with_selected_cell(cell);
121
+ Self {
122
+ inner: RefCell::new(state),
123
+ }
124
+ }
125
+
126
+ /// Borrows the inner `TableState` mutably for rendering.
127
+ pub fn borrow_mut(&self) -> std::cell::RefMut<'_, TableState> {
128
+ self.inner.borrow_mut()
129
+ }
130
+ }
131
+
132
+ /// Registers the `TableState` class with Ruby.
133
+ pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
134
+ let class = module.define_class("TableState", ruby.class_object())?;
135
+ class.define_singleton_method("new", function!(RubyTableState::new, 1))?;
136
+ class.define_singleton_method(
137
+ "with_selected_cell",
138
+ function!(RubyTableState::with_selected_cell, 1),
139
+ )?;
140
+ class.define_method("select", method!(RubyTableState::select, 1))?;
141
+ class.define_method("selected", method!(RubyTableState::selected, 0))?;
142
+ class.define_method("select_column", method!(RubyTableState::select_column, 1))?;
143
+ class.define_method(
144
+ "selected_column",
145
+ method!(RubyTableState::selected_column, 0),
146
+ )?;
147
+ class.define_method("selected_cell", method!(RubyTableState::selected_cell, 0))?;
148
+ class.define_method(
149
+ "select_next_column",
150
+ method!(RubyTableState::select_next_column, 0),
151
+ )?;
152
+ class.define_method(
153
+ "select_previous_column",
154
+ method!(RubyTableState::select_previous_column, 0),
155
+ )?;
156
+ class.define_method(
157
+ "select_first_column",
158
+ method!(RubyTableState::select_first_column, 0),
159
+ )?;
160
+ class.define_method(
161
+ "select_last_column",
162
+ method!(RubyTableState::select_last_column, 0),
163
+ )?;
164
+ class.define_method("offset", method!(RubyTableState::offset, 0))?;
165
+ class.define_method("scroll_down_by", method!(RubyTableState::scroll_down_by, 1))?;
166
+ class.define_method("scroll_up_by", method!(RubyTableState::scroll_up_by, 1))?;
167
+ class.define_method("select_next", method!(RubyTableState::select_next, 0))?;
168
+ class.define_method(
169
+ "select_previous",
170
+ method!(RubyTableState::select_previous, 0),
171
+ )?;
172
+ class.define_method("select_first", method!(RubyTableState::select_first, 0))?;
173
+ class.define_method("select_last", method!(RubyTableState::select_last, 0))?;
174
+ Ok(())
175
+ }
176
+
177
+ #[cfg(test)]
178
+ mod tests {
179
+ use super::*;
180
+
181
+ #[test]
182
+ fn test_new_with_no_selection() {
183
+ let state = RubyTableState::new(None);
184
+ assert_eq!(state.selected(), None);
185
+ assert_eq!(state.selected_column(), None);
186
+ assert_eq!(state.offset(), 0);
187
+ }
188
+
189
+ #[test]
190
+ fn test_new_with_selection() {
191
+ let state = RubyTableState::new(Some(3));
192
+ assert_eq!(state.selected(), Some(3));
193
+ }
194
+
195
+ #[test]
196
+ fn test_column_selection() {
197
+ let state = RubyTableState::new(None);
198
+ state.select_column(Some(2));
199
+ assert_eq!(state.selected_column(), Some(2));
200
+ state.select_column(None);
201
+ assert_eq!(state.selected_column(), None);
202
+ }
203
+ }