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,378 @@
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};
7
+ use crate::widgets::list_state::RubyListState;
8
+ use bumpalo::Bump;
9
+ use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
10
+ use ratatui::buffer::Buffer;
11
+ use ratatui::{
12
+ layout::Rect,
13
+ text::Line,
14
+ widgets::{HighlightSpacing, List, ListItem, ListState, StatefulWidget},
15
+ };
16
+
17
+ pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
18
+ let bump = Bump::new();
19
+ let ruby = magnus::Ruby::get().unwrap();
20
+ let items_val: Value = node.funcall("items", ())?;
21
+ let items_array = magnus::RArray::from_value(items_val)
22
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for items", items_val))?;
23
+ let selected_index_val: Value = node.funcall("selected_index", ())?;
24
+ let style_val: Value = node.funcall("style", ())?;
25
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
26
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
27
+ let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
28
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
29
+ let direction_val: Value = node.funcall("direction", ())?;
30
+ let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
31
+ let block_val: Value = node.funcall("block", ())?;
32
+
33
+ let mut items: Vec<ListItem> = Vec::new();
34
+ for i in 0..items_array.len() {
35
+ let index = isize::try_from(i)
36
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
37
+ let item_val: Value = items_array.entry(index)?;
38
+ let item = parse_list_item(item_val)?;
39
+ items.push(item);
40
+ }
41
+
42
+ let symbol: String = if highlight_symbol_val.is_nil() {
43
+ String::new()
44
+ } else {
45
+ String::try_convert(highlight_symbol_val)?
46
+ };
47
+
48
+ let mut state = ListState::default();
49
+ if !selected_index_val.is_nil() {
50
+ let index: usize = selected_index_val.funcall("to_int", ())?;
51
+ state.select(Some(index));
52
+ }
53
+
54
+ let offset_val: Value = node.funcall("offset", ())?;
55
+ if !offset_val.is_nil() {
56
+ let offset: usize = offset_val.funcall("to_int", ())?;
57
+ *state.offset_mut() = offset;
58
+ }
59
+
60
+ let mut list = List::new(items);
61
+
62
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
63
+ "always" => HighlightSpacing::Always,
64
+ "never" => HighlightSpacing::Never,
65
+ _ => HighlightSpacing::WhenSelected,
66
+ };
67
+ list = list.highlight_spacing(highlight_spacing);
68
+
69
+ if !highlight_symbol_val.is_nil() {
70
+ list = list.highlight_symbol(Line::from(symbol));
71
+ }
72
+
73
+ if !repeat_highlight_symbol_val.is_nil() {
74
+ let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
75
+ list = list.repeat_highlight_symbol(repeat);
76
+ }
77
+
78
+ if !direction_val.is_nil() {
79
+ let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
80
+ let direction_str = direction_sym.name().unwrap();
81
+ match direction_str.as_ref() {
82
+ "top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
83
+ "bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
84
+ _ => {
85
+ return Err(Error::new(
86
+ ruby.exception_arg_error(),
87
+ "direction must be :top_to_bottom or :bottom_to_top",
88
+ ))
89
+ }
90
+ }
91
+ }
92
+
93
+ if !scroll_padding_val.is_nil() {
94
+ let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
95
+ list = list.scroll_padding(padding);
96
+ }
97
+
98
+ if !style_val.is_nil() {
99
+ list = list.style(parse_style(style_val)?);
100
+ }
101
+
102
+ if !highlight_style_val.is_nil() {
103
+ list = list.highlight_style(parse_style(highlight_style_val)?);
104
+ }
105
+
106
+ if !block_val.is_nil() {
107
+ list = list.block(parse_block(block_val, &bump)?);
108
+ }
109
+
110
+ StatefulWidget::render(list, area, buffer, &mut state);
111
+ Ok(())
112
+ }
113
+
114
+ /// Renders a List with an external state object.
115
+ ///
116
+ /// This function ignores `selected_index` and `offset` from the widget.
117
+ /// The State object is the single source of truth for selection and scroll position.
118
+ pub fn render_stateful(
119
+ buffer: &mut Buffer,
120
+ area: Rect,
121
+ node: Value,
122
+ state_wrapper: Value,
123
+ ) -> Result<(), Error> {
124
+ let bump = Bump::new();
125
+ let ruby = magnus::Ruby::get().unwrap();
126
+
127
+ // Extract the RubyListState wrapper
128
+ let state: &RubyListState = TryConvert::try_convert(state_wrapper)?;
129
+
130
+ // Build items
131
+ let items_val: Value = node.funcall("items", ())?;
132
+ let items_array = magnus::RArray::from_value(items_val)
133
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for items", items_val))?;
134
+
135
+ let mut items: Vec<ListItem> = Vec::new();
136
+ for i in 0..items_array.len() {
137
+ let index = isize::try_from(i)
138
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
139
+ let item_val: Value = items_array.entry(index)?;
140
+ let item = parse_list_item(item_val)?;
141
+ items.push(item);
142
+ }
143
+
144
+ // Build widget (ignoring selected_index and offset — State is truth)
145
+ let style_val: Value = node.funcall("style", ())?;
146
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
147
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
148
+ let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
149
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
150
+ let direction_val: Value = node.funcall("direction", ())?;
151
+ let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
152
+ let block_val: Value = node.funcall("block", ())?;
153
+
154
+ let symbol: String = if highlight_symbol_val.is_nil() {
155
+ String::new()
156
+ } else {
157
+ String::try_convert(highlight_symbol_val)?
158
+ };
159
+
160
+ let mut list = List::new(items);
161
+
162
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
163
+ "always" => HighlightSpacing::Always,
164
+ "never" => HighlightSpacing::Never,
165
+ _ => HighlightSpacing::WhenSelected,
166
+ };
167
+ list = list.highlight_spacing(highlight_spacing);
168
+
169
+ if !highlight_symbol_val.is_nil() {
170
+ list = list.highlight_symbol(Line::from(symbol));
171
+ }
172
+
173
+ if !repeat_highlight_symbol_val.is_nil() {
174
+ let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
175
+ list = list.repeat_highlight_symbol(repeat);
176
+ }
177
+
178
+ if !direction_val.is_nil() {
179
+ let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
180
+ let direction_str = direction_sym.name().unwrap();
181
+ match direction_str.as_ref() {
182
+ "top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
183
+ "bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
184
+ _ => {
185
+ return Err(Error::new(
186
+ ruby.exception_arg_error(),
187
+ "direction must be :top_to_bottom or :bottom_to_top",
188
+ ))
189
+ }
190
+ }
191
+ }
192
+
193
+ if !scroll_padding_val.is_nil() {
194
+ let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
195
+ list = list.scroll_padding(padding);
196
+ }
197
+
198
+ if !style_val.is_nil() {
199
+ list = list.style(parse_style(style_val)?);
200
+ }
201
+
202
+ if !highlight_style_val.is_nil() {
203
+ list = list.highlight_style(parse_style(highlight_style_val)?);
204
+ }
205
+
206
+ if !block_val.is_nil() {
207
+ list = list.block(parse_block(block_val, &bump)?);
208
+ }
209
+
210
+ // Borrow the inner ListState, render, and release the borrow immediately
211
+ {
212
+ let mut inner_state = state.borrow_mut();
213
+ StatefulWidget::render(list, area, buffer, &mut inner_state);
214
+ }
215
+ // Borrow is now released
216
+
217
+ Ok(())
218
+ }
219
+
220
+ /// Parses a Ruby list item into a ratatui `ListItem`.
221
+ ///
222
+ /// Accepts:
223
+ /// - `String`: Plain text item
224
+ /// - `Text::Span`: A single styled fragment
225
+ /// - `Text::Line`: A line composed of multiple spans
226
+ /// - `RatatuiRuby::ListItem`: A `ListItem` object with content and optional style
227
+ fn parse_list_item(value: Value) -> Result<ListItem<'static>, Error> {
228
+ let ruby = magnus::Ruby::get().unwrap();
229
+
230
+ // Check if it's a RatatuiRuby::ListItem
231
+ if let Ok(class_obj) = value.funcall::<_, _, Value>("class", ()) {
232
+ if let Ok(class_name) = class_obj.funcall::<_, _, String>("name", ()) {
233
+ if class_name.contains("ListItem") {
234
+ // Extract content and style from the ListItem
235
+ let content_val: Value = value.funcall("content", ())?;
236
+ let style_val: Value = value.funcall("style", ())?;
237
+
238
+ // Parse content as a Line
239
+ let line = if let Ok(s) = String::try_convert(content_val) {
240
+ Line::from(s)
241
+ } else if let Ok(line) = parse_line(content_val) {
242
+ line
243
+ } else if let Ok(span) = parse_span(content_val) {
244
+ Line::from(vec![span])
245
+ } else {
246
+ Line::from("")
247
+ };
248
+
249
+ // Parse and apply style if present
250
+ let mut item = ListItem::new(line);
251
+ if !style_val.is_nil() {
252
+ item = item.style(parse_style(style_val)?);
253
+ }
254
+ return Ok(item);
255
+ }
256
+ }
257
+ }
258
+
259
+ // Try as String
260
+ if let Ok(s) = String::try_convert(value) {
261
+ return Ok(ListItem::new(Line::from(s)));
262
+ }
263
+
264
+ // Try as Line
265
+ if let Ok(line) = parse_line(value) {
266
+ return Ok(ListItem::new(line));
267
+ }
268
+
269
+ // Try as Span
270
+ if let Ok(span) = parse_span(value) {
271
+ return Ok(ListItem::new(Line::from(vec![span])));
272
+ }
273
+
274
+ Err(type_error_with_context(
275
+ &ruby,
276
+ "expected String, Text::Span, Text::Line, or ListItem",
277
+ value,
278
+ ))
279
+ }
280
+
281
+ #[cfg(test)]
282
+ mod tests {
283
+ use super::*;
284
+ use ratatui::buffer::Buffer;
285
+ use ratatui::widgets::List;
286
+
287
+ #[test]
288
+ fn test_list_rendering() {
289
+ let items = vec!["Item 1", "Item 2"];
290
+ let list = List::new(items)
291
+ .highlight_symbol(Line::from(">> "))
292
+ .style(ratatui::style::Style::default().fg(ratatui::style::Color::White))
293
+ .highlight_style(
294
+ ratatui::style::Style::default()
295
+ .fg(ratatui::style::Color::Yellow)
296
+ .add_modifier(ratatui::style::Modifier::BOLD),
297
+ );
298
+ let mut state = ListState::default();
299
+ state.select(Some(1));
300
+
301
+ let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
302
+ StatefulWidget::render(list, Rect::new(0, 0, 10, 2), &mut buf, &mut state);
303
+
304
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
305
+ assert!(content.contains("Item 1"));
306
+ assert!(content.contains(">> Item 2"));
307
+
308
+ // Check colors
309
+ assert_eq!(buf.cell((0, 0)).unwrap().fg, ratatui::style::Color::White);
310
+ assert_eq!(buf.cell((0, 1)).unwrap().fg, ratatui::style::Color::Yellow);
311
+ assert!(buf
312
+ .cell((0, 1))
313
+ .unwrap()
314
+ .modifier
315
+ .contains(ratatui::style::Modifier::BOLD));
316
+ }
317
+
318
+ #[test]
319
+ fn test_repeat_highlight_symbol() {
320
+ let items = vec!["Item 1", "Item 2"];
321
+ let list_without_repeat = List::new(items.clone()).highlight_symbol(Line::from(">> "));
322
+ let list_with_repeat = List::new(items)
323
+ .highlight_symbol(Line::from(">> "))
324
+ .repeat_highlight_symbol(true);
325
+
326
+ let mut state = ListState::default();
327
+ state.select(Some(0));
328
+
329
+ let mut buf1 = Buffer::empty(Rect::new(0, 0, 10, 2));
330
+ StatefulWidget::render(
331
+ list_without_repeat,
332
+ Rect::new(0, 0, 10, 2),
333
+ &mut buf1,
334
+ &mut state,
335
+ );
336
+
337
+ let mut buf2 = Buffer::empty(Rect::new(0, 0, 10, 2));
338
+ StatefulWidget::render(
339
+ list_with_repeat,
340
+ Rect::new(0, 0, 10, 2),
341
+ &mut buf2,
342
+ &mut state,
343
+ );
344
+
345
+ // Both should render, but the behavior might differ based on content width
346
+ let content1 = buf1
347
+ .content()
348
+ .iter()
349
+ .map(|c| c.symbol())
350
+ .collect::<String>();
351
+ let content2 = buf2
352
+ .content()
353
+ .iter()
354
+ .map(|c| c.symbol())
355
+ .collect::<String>();
356
+ assert!(!content1.is_empty());
357
+ assert!(!content2.is_empty());
358
+ }
359
+
360
+ #[test]
361
+ fn test_scroll_padding() {
362
+ let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];
363
+ let list = List::new(items)
364
+ .scroll_padding(1)
365
+ .highlight_symbol(Line::from(">> "));
366
+
367
+ let mut state = ListState::default();
368
+ state.select(Some(1));
369
+
370
+ let mut buf = Buffer::empty(Rect::new(0, 0, 15, 4));
371
+ StatefulWidget::render(list, Rect::new(0, 0, 15, 4), &mut buf, &mut state);
372
+
373
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
374
+ // With scroll padding, it should render but the exact behavior is handled by ratatui
375
+ assert!(!content.is_empty());
376
+ assert!(content.contains("Item"));
377
+ }
378
+ }
@@ -0,0 +1,173 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! `ListState` wrapper for exposing Ratatui's `ListState` to Ruby.
5
+ //!
6
+ //! This module provides `RubyListState`, a Magnus-wrapped struct that holds
7
+ //! a `RefCell<ListState>` 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_index`, `offset`)
13
+ //! are ignored in stateful mode.
14
+ //!
15
+ //! # Safety
16
+ //!
17
+ //! The `RefCell` is borrowed only during the `render_stateful_widget` call.
18
+ //! The borrow is released immediately after to avoid double-borrow panics
19
+ //! if a user inspects state inside a custom widget's render method.
20
+
21
+ use magnus::{function, method, prelude::*, Error, Module, Ruby};
22
+ use ratatui::widgets::ListState;
23
+ use std::cell::RefCell;
24
+
25
+ /// A wrapper around Ratatui's `ListState` exposed to Ruby.
26
+ ///
27
+ /// This struct uses `RefCell` for interior mutability, allowing the state
28
+ /// to be updated during rendering while remaining accessible from Ruby.
29
+ #[magnus::wrap(class = "RatatuiRuby::ListState")]
30
+ pub struct RubyListState {
31
+ inner: RefCell<ListState>,
32
+ }
33
+
34
+ impl RubyListState {
35
+ /// Creates a new `RubyListState` with optional initial selection.
36
+ ///
37
+ /// # Arguments
38
+ ///
39
+ /// * `selected` - Optional initial selection index
40
+ pub fn new(selected: Option<usize>) -> Self {
41
+ let mut state = ListState::default();
42
+ if let Some(idx) = selected {
43
+ state.select(Some(idx));
44
+ }
45
+ Self {
46
+ inner: RefCell::new(state),
47
+ }
48
+ }
49
+
50
+ /// Sets the selected index.
51
+ ///
52
+ /// Pass `nil` to deselect.
53
+ pub fn select(&self, index: Option<usize>) {
54
+ self.inner.borrow_mut().select(index);
55
+ }
56
+
57
+ /// Returns the currently selected index, or `nil` if nothing is selected.
58
+ pub fn selected(&self) -> Option<usize> {
59
+ self.inner.borrow().selected()
60
+ }
61
+
62
+ /// Returns the current scroll offset.
63
+ ///
64
+ /// This is the critical read-back method. After `render_stateful_widget`,
65
+ /// this returns the scroll position calculated by Ratatui to keep the
66
+ /// selection visible.
67
+ pub fn offset(&self) -> usize {
68
+ self.inner.borrow().offset()
69
+ }
70
+
71
+ /// Scrolls down by the given number of items.
72
+ pub fn scroll_down_by(&self, amount: u16) {
73
+ self.inner.borrow_mut().scroll_down_by(amount);
74
+ }
75
+
76
+ /// Scrolls up by the given number of items.
77
+ pub fn scroll_up_by(&self, amount: u16) {
78
+ self.inner.borrow_mut().scroll_up_by(amount);
79
+ }
80
+
81
+ /// Selects the next item or the first one if no item is selected.
82
+ ///
83
+ /// Note: until the list is rendered, the number of items is not known, so the index
84
+ /// is set to 0 and will be corrected when the list is rendered.
85
+ pub fn select_next(&self) {
86
+ self.inner.borrow_mut().select_next();
87
+ }
88
+
89
+ /// Selects the previous item or the last one if no item is selected.
90
+ ///
91
+ /// Note: until the list is rendered, the number of items is not known, so the index
92
+ /// is set to `usize::MAX` and will be corrected when the list is rendered.
93
+ pub fn select_previous(&self) {
94
+ self.inner.borrow_mut().select_previous();
95
+ }
96
+
97
+ /// Selects the first item.
98
+ pub fn select_first(&self) {
99
+ self.inner.borrow_mut().select_first();
100
+ }
101
+
102
+ /// Selects the last item.
103
+ ///
104
+ /// Note: until the list is rendered, the number of items is not known, so the index
105
+ /// is set to `usize::MAX` and will be corrected when the list is rendered.
106
+ pub fn select_last(&self) {
107
+ self.inner.borrow_mut().select_last();
108
+ }
109
+
110
+ /// Borrows the inner `ListState` mutably for rendering.
111
+ ///
112
+ /// # Safety
113
+ ///
114
+ /// The caller must ensure the borrow is released before returning
115
+ /// control to Ruby to avoid double-borrow panics.
116
+ pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ListState> {
117
+ self.inner.borrow_mut()
118
+ }
119
+ }
120
+
121
+ /// Registers the `ListState` class with Ruby.
122
+ pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
123
+ let class = module.define_class("ListState", ruby.class_object())?;
124
+ class.define_singleton_method("new", function!(RubyListState::new, 1))?;
125
+ class.define_method("select", method!(RubyListState::select, 1))?;
126
+ class.define_method("selected", method!(RubyListState::selected, 0))?;
127
+ class.define_method("offset", method!(RubyListState::offset, 0))?;
128
+ class.define_method("scroll_down_by", method!(RubyListState::scroll_down_by, 1))?;
129
+ class.define_method("scroll_up_by", method!(RubyListState::scroll_up_by, 1))?;
130
+ class.define_method("select_next", method!(RubyListState::select_next, 0))?;
131
+ class.define_method(
132
+ "select_previous",
133
+ method!(RubyListState::select_previous, 0),
134
+ )?;
135
+ class.define_method("select_first", method!(RubyListState::select_first, 0))?;
136
+ class.define_method("select_last", method!(RubyListState::select_last, 0))?;
137
+ Ok(())
138
+ }
139
+
140
+ #[cfg(test)]
141
+ mod tests {
142
+ use super::*;
143
+
144
+ #[test]
145
+ fn test_new_with_no_selection() {
146
+ let state = RubyListState::new(None);
147
+ assert_eq!(state.selected(), None);
148
+ assert_eq!(state.offset(), 0);
149
+ }
150
+
151
+ #[test]
152
+ fn test_new_with_selection() {
153
+ let state = RubyListState::new(Some(5));
154
+ assert_eq!(state.selected(), Some(5));
155
+ }
156
+
157
+ #[test]
158
+ fn test_select_and_deselect() {
159
+ let state = RubyListState::new(None);
160
+ state.select(Some(3));
161
+ assert_eq!(state.selected(), Some(3));
162
+ state.select(None);
163
+ assert_eq!(state.selected(), None);
164
+ }
165
+
166
+ #[test]
167
+ fn test_scroll_operations() {
168
+ let state = RubyListState::new(None);
169
+ state.scroll_down_by(5);
170
+ // Note: scroll operations affect offset, but the exact behavior
171
+ // depends on the list size which is determined during rendering
172
+ }
173
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ pub mod barchart;
5
+ pub mod block;
6
+ pub mod calendar;
7
+ pub mod canvas;
8
+ pub mod center;
9
+ pub mod chart;
10
+ pub mod clear;
11
+ pub mod cursor;
12
+ pub mod gauge;
13
+ pub mod layout;
14
+ pub mod line_gauge;
15
+ pub mod list;
16
+ pub mod list_state;
17
+ pub mod overlay;
18
+ pub mod paragraph;
19
+ pub mod ratatui_logo;
20
+ pub mod ratatui_mascot;
21
+ pub mod scrollbar;
22
+ pub mod scrollbar_state;
23
+ pub mod sparkline;
24
+ pub mod table;
25
+ pub mod table_state;
26
+ pub mod tabs;
@@ -0,0 +1,24 @@
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::rendering::render_node;
6
+ use magnus::{prelude::*, Error, Value};
7
+ use ratatui::{buffer::Buffer, layout::Rect};
8
+
9
+ pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
10
+ let ruby = magnus::Ruby::get().unwrap();
11
+ let layers_val: Value = node.funcall("layers", ())?;
12
+ let layers_array = magnus::RArray::from_value(layers_val)
13
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for layers", layers_val))?;
14
+
15
+ for i in 0..layers_array.len() {
16
+ let index = isize::try_from(i)
17
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
18
+ let layer: Value = layers_array.entry(index)?;
19
+ if let Err(e) = render_node(buffer, area, layer) {
20
+ eprintln!("Error rendering overlay layer {i}: {e:?}");
21
+ }
22
+ }
23
+ Ok(())
24
+ }
@@ -0,0 +1,87 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::style::{parse_block, parse_style};
5
+ use bumpalo::Bump;
6
+ use magnus::{prelude::*, Error, Symbol, Value};
7
+ use ratatui::buffer::Buffer;
8
+ use ratatui::{
9
+ layout::{HorizontalAlignment, Rect},
10
+ widgets::{Paragraph, Widget, Wrap},
11
+ };
12
+
13
+ use crate::text::parse_text;
14
+
15
+ pub fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
16
+ let text_val: Value = node.funcall("text", ())?;
17
+ let style_val: Value = node.funcall("style", ())?;
18
+ let block_val: Value = node.funcall("block", ())?;
19
+ let wrap: bool = node.funcall("wrap", ())?;
20
+ let alignment_opt: Option<Symbol> = node.funcall("alignment", ())?;
21
+ let scroll_val: Value = node.funcall("scroll", ())?;
22
+
23
+ let lines = parse_text(text_val)?;
24
+ let style = parse_style(style_val)?;
25
+ let mut paragraph = Paragraph::new(lines).style(style);
26
+
27
+ if !block_val.is_nil() {
28
+ paragraph = paragraph.block(parse_block(block_val, bump)?);
29
+ }
30
+
31
+ if wrap {
32
+ paragraph = paragraph.wrap(Wrap { trim: true });
33
+ }
34
+
35
+ if let Some(alignment) = alignment_opt {
36
+ match alignment.to_string().as_str() {
37
+ "center" => paragraph = paragraph.alignment(HorizontalAlignment::Center),
38
+ "right" => paragraph = paragraph.alignment(HorizontalAlignment::Right),
39
+ _ => {}
40
+ }
41
+ }
42
+
43
+ if !scroll_val.is_nil() {
44
+ let scroll_array: Vec<u16> = Vec::<u16>::try_convert(scroll_val)?;
45
+ if scroll_array.len() >= 2 {
46
+ paragraph = paragraph.scroll((scroll_array[0], scroll_array[1]));
47
+ }
48
+ }
49
+
50
+ Ok(paragraph)
51
+ }
52
+
53
+ pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
54
+ let bump = Bump::new();
55
+ let paragraph = create_paragraph(node, &bump)?;
56
+ Widget::render(paragraph, area, buffer);
57
+ Ok(())
58
+ }
59
+
60
+ pub fn line_count(node: Value, width: u16) -> Result<usize, Error> {
61
+ let bump = Bump::new();
62
+ let paragraph = create_paragraph(node, &bump)?;
63
+ Ok(paragraph.line_count(width))
64
+ }
65
+
66
+ pub fn line_width(node: Value) -> Result<usize, Error> {
67
+ let bump = Bump::new();
68
+ let paragraph = create_paragraph(node, &bump)?;
69
+ Ok(paragraph.line_width())
70
+ }
71
+
72
+ #[cfg(test)]
73
+ mod tests {
74
+ use super::*;
75
+ use ratatui::buffer::Buffer;
76
+
77
+ #[test]
78
+ fn test_paragraph_rendering() {
79
+ let p = Paragraph::new("test content").alignment(HorizontalAlignment::Center);
80
+ let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
81
+ p.render(Rect::new(0, 0, 20, 1), &mut buf);
82
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
83
+ assert!(content.contains("test content"));
84
+ // Check for centered alignment (should have leading spaces)
85
+ assert!(content.starts_with(' '));
86
+ }
87
+ }