ratatui_ruby 0.3.1 → 0.4.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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +54 -13
  7. data/CHANGELOG.md +186 -1
  8. data/README.md +17 -15
  9. data/doc/application_architecture.md +116 -0
  10. data/doc/application_testing.md +12 -7
  11. data/doc/contributors/better_dx.md +543 -0
  12. data/doc/contributors/design/ruby_frontend.md +1 -1
  13. data/doc/contributors/developing_examples.md +203 -0
  14. data/doc/contributors/documentation_style.md +97 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/example_analysis.md +82 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +119 -0
  19. data/doc/images/all_events.png +0 -0
  20. data/doc/images/analytics.png +0 -0
  21. data/doc/images/block_padding.png +0 -0
  22. data/doc/images/block_titles.png +0 -0
  23. data/doc/images/box_demo.png +0 -0
  24. data/doc/images/calendar_demo.png +0 -0
  25. data/doc/images/cell_demo.png +0 -0
  26. data/doc/images/chart_demo.png +0 -0
  27. data/doc/images/custom_widget.png +0 -0
  28. data/doc/images/flex_layout.png +0 -0
  29. data/doc/images/gauge_demo.png +0 -0
  30. data/doc/images/hit_test.png +0 -0
  31. data/doc/images/line_gauge_demo.png +0 -0
  32. data/doc/images/list_demo.png +0 -0
  33. data/doc/images/list_styles.png +0 -0
  34. data/doc/images/login_form.png +0 -0
  35. data/doc/images/map_demo.png +0 -0
  36. data/doc/images/mouse_events.png +0 -0
  37. data/doc/images/popup_demo.png +0 -0
  38. data/doc/images/quickstart_dsl.png +0 -0
  39. data/doc/images/quickstart_lifecycle.png +0 -0
  40. data/doc/images/ratatui_logo_demo.png +0 -0
  41. data/doc/images/readme_usage.png +0 -0
  42. data/doc/images/rich_text.png +0 -0
  43. data/doc/images/scroll_text.png +0 -0
  44. data/doc/images/scrollbar_demo.png +0 -0
  45. data/doc/images/sparkline_demo.png +0 -0
  46. data/doc/images/table_flex.png +0 -0
  47. data/doc/images/table_select.png +0 -0
  48. data/doc/images/widget_style_colors.png +0 -0
  49. data/doc/index.md +1 -0
  50. data/doc/interactive_design.md +121 -0
  51. data/doc/quickstart.md +147 -72
  52. data/examples/all_events/app.rb +169 -0
  53. data/examples/all_events/app.rbs +7 -0
  54. data/examples/all_events/test_app.rb +139 -0
  55. data/examples/analytics/app.rb +258 -0
  56. data/examples/analytics/app.rbs +7 -0
  57. data/examples/analytics/test_app.rb +132 -0
  58. data/examples/block_padding/app.rb +63 -0
  59. data/examples/block_padding/app.rbs +7 -0
  60. data/examples/block_padding/test_app.rb +31 -0
  61. data/examples/block_titles/app.rb +61 -0
  62. data/examples/block_titles/app.rbs +7 -0
  63. data/examples/block_titles/test_app.rb +34 -0
  64. data/examples/box_demo/app.rb +216 -0
  65. data/examples/box_demo/app.rbs +7 -0
  66. data/examples/box_demo/test_app.rb +88 -0
  67. data/examples/calendar_demo/app.rb +101 -0
  68. data/examples/calendar_demo/app.rbs +7 -0
  69. data/examples/calendar_demo/test_app.rb +108 -0
  70. data/examples/cell_demo/app.rb +108 -0
  71. data/examples/cell_demo/app.rbs +7 -0
  72. data/examples/cell_demo/test_app.rb +36 -0
  73. data/examples/chart_demo/app.rb +203 -0
  74. data/examples/chart_demo/app.rbs +7 -0
  75. data/examples/chart_demo/test_app.rb +102 -0
  76. data/examples/custom_widget/app.rb +51 -0
  77. data/examples/custom_widget/app.rbs +7 -0
  78. data/examples/custom_widget/test_app.rb +30 -0
  79. data/examples/flex_layout/app.rb +156 -0
  80. data/examples/flex_layout/app.rbs +7 -0
  81. data/examples/flex_layout/test_app.rb +65 -0
  82. data/examples/gauge_demo/app.rb +182 -0
  83. data/examples/gauge_demo/app.rbs +7 -0
  84. data/examples/gauge_demo/test_app.rb +120 -0
  85. data/examples/hit_test/app.rb +175 -0
  86. data/examples/hit_test/app.rbs +7 -0
  87. data/examples/hit_test/test_app.rb +102 -0
  88. data/examples/line_gauge_demo/app.rb +190 -0
  89. data/examples/line_gauge_demo/app.rbs +7 -0
  90. data/examples/line_gauge_demo/test_app.rb +129 -0
  91. data/examples/list_demo/app.rb +253 -0
  92. data/examples/list_demo/app.rbs +12 -0
  93. data/examples/list_demo/test_app.rb +237 -0
  94. data/examples/list_styles/app.rb +140 -0
  95. data/examples/list_styles/app.rbs +7 -0
  96. data/examples/list_styles/test_app.rb +157 -0
  97. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  98. data/examples/login_form/app.rbs +7 -0
  99. data/examples/login_form/test_app.rb +51 -0
  100. data/examples/map_demo/app.rb +90 -0
  101. data/examples/map_demo/app.rbs +7 -0
  102. data/examples/map_demo/test_app.rb +149 -0
  103. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  104. data/examples/mouse_events/app.rbs +7 -0
  105. data/examples/mouse_events/test_app.rb +53 -0
  106. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  107. data/examples/popup_demo/app.rbs +7 -0
  108. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  109. data/examples/quickstart_dsl/app.rb +36 -0
  110. data/examples/quickstart_dsl/app.rbs +7 -0
  111. data/examples/quickstart_dsl/test_app.rb +29 -0
  112. data/examples/quickstart_lifecycle/app.rb +39 -0
  113. data/examples/quickstart_lifecycle/app.rbs +7 -0
  114. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  115. data/examples/ratatui_logo_demo/app.rb +79 -0
  116. data/examples/ratatui_logo_demo/app.rbs +7 -0
  117. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  118. data/examples/ratatui_mascot_demo/app.rb +84 -0
  119. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  120. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  121. data/examples/readme_usage/app.rb +29 -0
  122. data/examples/readme_usage/app.rbs +7 -0
  123. data/examples/readme_usage/test_app.rb +29 -0
  124. data/examples/rich_text/app.rb +141 -0
  125. data/examples/rich_text/app.rbs +7 -0
  126. data/examples/rich_text/test_app.rb +166 -0
  127. data/examples/scroll_text/app.rb +103 -0
  128. data/examples/scroll_text/app.rbs +7 -0
  129. data/examples/scroll_text/test_app.rb +110 -0
  130. data/examples/scrollbar_demo/app.rb +143 -0
  131. data/examples/scrollbar_demo/app.rbs +7 -0
  132. data/examples/scrollbar_demo/test_app.rb +77 -0
  133. data/examples/sparkline_demo/app.rb +240 -0
  134. data/examples/sparkline_demo/app.rbs +10 -0
  135. data/examples/sparkline_demo/test_app.rb +107 -0
  136. data/examples/table_flex/app.rb +65 -0
  137. data/examples/table_flex/app.rbs +7 -0
  138. data/examples/table_flex/test_app.rb +36 -0
  139. data/examples/table_select/app.rb +198 -0
  140. data/examples/table_select/app.rbs +7 -0
  141. data/examples/table_select/test_app.rb +180 -0
  142. data/examples/widget_style_colors/app.rb +104 -0
  143. data/examples/widget_style_colors/app.rbs +14 -0
  144. data/examples/widget_style_colors/test_app.rb +48 -0
  145. data/ext/ratatui_ruby/Cargo.lock +889 -115
  146. data/ext/ratatui_ruby/Cargo.toml +4 -3
  147. data/ext/ratatui_ruby/clippy.toml +7 -0
  148. data/ext/ratatui_ruby/extconf.rb +7 -0
  149. data/ext/ratatui_ruby/src/events.rs +218 -229
  150. data/ext/ratatui_ruby/src/lib.rs +38 -10
  151. data/ext/ratatui_ruby/src/rendering.rs +90 -10
  152. data/ext/ratatui_ruby/src/style.rs +281 -98
  153. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  154. data/ext/ratatui_ruby/src/text.rs +171 -0
  155. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  156. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  157. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  158. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  159. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  160. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  161. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  162. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  163. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  164. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  165. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  166. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  167. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  168. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  169. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  170. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  171. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  172. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  173. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  174. data/lib/ratatui_ruby/cell.rb +166 -0
  175. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  176. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  177. data/lib/ratatui_ruby/event/key.rb +211 -0
  178. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  179. data/lib/ratatui_ruby/event/paste.rb +71 -0
  180. data/lib/ratatui_ruby/event/resize.rb +80 -0
  181. data/lib/ratatui_ruby/event.rb +79 -0
  182. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  184. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  185. data/lib/ratatui_ruby/schema/block.rb +186 -14
  186. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  187. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  188. data/lib/ratatui_ruby/schema/center.rb +49 -11
  189. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  190. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  191. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  192. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  193. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  194. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  195. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  196. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  197. data/lib/ratatui_ruby/schema/list.rb +93 -19
  198. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  199. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  200. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  201. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  202. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  203. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  204. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  205. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  206. data/lib/ratatui_ruby/schema/style.rb +49 -21
  207. data/lib/ratatui_ruby/schema/table.rb +119 -21
  208. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  209. data/lib/ratatui_ruby/schema/text.rb +90 -0
  210. data/lib/ratatui_ruby/session.rb +146 -0
  211. data/lib/ratatui_ruby/test_helper.rb +156 -13
  212. data/lib/ratatui_ruby/version.rb +1 -1
  213. data/lib/ratatui_ruby.rb +143 -23
  214. data/sig/ratatui_ruby/event.rbs +69 -0
  215. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  216. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  217. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  218. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  219. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  220. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  221. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  222. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  223. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  224. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  225. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  226. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  227. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  228. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  229. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  230. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  231. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  232. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  233. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  234. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  235. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  236. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  237. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  238. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  239. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  240. data/tasks/resources/build.yml.erb +13 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  242. data/tasks/terminal_preview/crash_report.rb +54 -0
  243. data/tasks/terminal_preview/example_app.rb +25 -0
  244. data/tasks/terminal_preview/launcher_script.rb +48 -0
  245. data/tasks/terminal_preview/preview_collection.rb +60 -0
  246. data/tasks/terminal_preview/preview_timing.rb +22 -0
  247. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  248. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  249. data/tasks/terminal_preview/system_appearance.rb +11 -0
  250. data/tasks/terminal_preview/terminal_window.rb +138 -0
  251. data/tasks/terminal_preview/window_id.rb +14 -0
  252. data/tasks/terminal_preview.rake +28 -0
  253. data/tasks/test.rake +1 -1
  254. metadata +174 -53
  255. data/doc/images/examples-analytics.rb.png +0 -0
  256. data/doc/images/examples-box_demo.rb.png +0 -0
  257. data/doc/images/examples-calendar_demo.rb.png +0 -0
  258. data/doc/images/examples-chart_demo.rb.png +0 -0
  259. data/doc/images/examples-custom_widget.rb.png +0 -0
  260. data/doc/images/examples-dashboard.rb.png +0 -0
  261. data/doc/images/examples-list_styles.rb.png +0 -0
  262. data/doc/images/examples-login_form.rb.png +0 -0
  263. data/doc/images/examples-map_demo.rb.png +0 -0
  264. data/doc/images/examples-mouse_events.rb.png +0 -0
  265. data/doc/images/examples-popup_demo.rb.gif +0 -0
  266. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  267. data/doc/images/examples-scroll_text.rb.png +0 -0
  268. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  269. data/doc/images/examples-stock_ticker.rb.png +0 -0
  270. data/doc/images/examples-system_monitor.rb.png +0 -0
  271. data/doc/images/examples-table_select.rb.png +0 -0
  272. data/examples/analytics.rb +0 -88
  273. data/examples/box_demo.rb +0 -71
  274. data/examples/calendar_demo.rb +0 -55
  275. data/examples/chart_demo.rb +0 -84
  276. data/examples/custom_widget.rb +0 -43
  277. data/examples/dashboard.rb +0 -72
  278. data/examples/list_styles.rb +0 -66
  279. data/examples/map_demo.rb +0 -58
  280. data/examples/quickstart_dsl.rb +0 -30
  281. data/examples/quickstart_lifecycle.rb +0 -40
  282. data/examples/readme_usage.rb +0 -21
  283. data/examples/scroll_text.rb +0 -74
  284. data/examples/scrollbar_demo.rb +0 -75
  285. data/examples/stock_ticker.rb +0 -93
  286. data/examples/system_monitor.rb +0 -94
  287. data/examples/table_select.rb +0 -70
  288. data/examples/test_analytics.rb +0 -65
  289. data/examples/test_box_demo.rb +0 -38
  290. data/examples/test_calendar_demo.rb +0 -66
  291. data/examples/test_dashboard.rb +0 -38
  292. data/examples/test_list_styles.rb +0 -61
  293. data/examples/test_login_form.rb +0 -63
  294. data/examples/test_map_demo.rb +0 -100
  295. data/examples/test_scroll_text.rb +0 -130
  296. data/examples/test_stock_ticker.rb +0 -39
  297. data/examples/test_system_monitor.rb +0 -40
  298. data/examples/test_table_select.rb +0 -37
  299. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  300. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -0,0 +1,44 @@
1
+ use crate::style::parse_block;
2
+ use bumpalo::Bump;
3
+ use magnus::{prelude::*, Error, Value};
4
+ use ratatui::{
5
+ layout::Rect,
6
+ widgets::RatatuiMascot,
7
+ Frame,
8
+ };
9
+
10
+ pub fn render_ratatui_mascot(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
11
+ let block_val: Value = node.funcall("block", ())?;
12
+
13
+ let mut inner_area = area;
14
+
15
+ if !block_val.is_nil() {
16
+ let bump = Bump::new();
17
+ let block = parse_block(block_val, &bump)?;
18
+ inner_area = block.inner(area);
19
+ frame.render_widget(block, area);
20
+ }
21
+
22
+ let widget = RatatuiMascot::new();
23
+ frame.render_widget(widget, inner_area);
24
+ Ok(())
25
+ }
26
+
27
+ #[cfg(test)]
28
+ mod tests {
29
+ use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
30
+ use super::*;
31
+
32
+ #[test]
33
+ fn test_render() {
34
+ let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
35
+ let widget = RatatuiMascot::new();
36
+ widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
37
+
38
+ let content = buffer.content().iter().map(|c| c.symbol()).collect::<String>();
39
+
40
+ // The mascot uses block drawing characters
41
+ assert!(content.contains("█"), "Mascot rendering should contain block characters");
42
+ assert!(!content.trim().is_empty());
43
+ }
44
+ }
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::parse_block;
5
+ use bumpalo::Bump;
5
6
  use magnus::{prelude::*, Error, Symbol, Value};
6
7
  use ratatui::{
7
8
  layout::Rect,
@@ -13,24 +14,75 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
13
14
  let content_length: usize = node.funcall("content_length", ())?;
14
15
  let position: usize = node.funcall("position", ())?;
15
16
  let orientation_sym: Symbol = node.funcall("orientation", ())?;
16
- let thumb_symbol: String = node.funcall("thumb_symbol", ())?;
17
+
18
+ let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
19
+ let thumb_style_val: Value = node.funcall("thumb_style", ())?;
20
+ let track_symbol_val: Value = node.funcall("track_symbol", ())?;
21
+ let track_style_val: Value = node.funcall("track_style", ())?;
22
+ let begin_symbol_val: Value = node.funcall("begin_symbol", ())?;
23
+ let begin_style_val: Value = node.funcall("begin_style", ())?;
24
+ let end_symbol_val: Value = node.funcall("end_symbol", ())?;
25
+ let end_style_val: Value = node.funcall("end_style", ())?;
26
+ let style_val: Value = node.funcall("style", ())?;
27
+
17
28
  let block_val: Value = node.funcall("block", ())?;
18
29
 
19
30
  let mut state = ScrollbarState::new(content_length).position(position);
20
- let mut scrollbar = Scrollbar::default().thumb_symbol(&thumb_symbol);
31
+ let mut scrollbar = Scrollbar::default();
21
32
 
22
33
  scrollbar = match orientation_sym.to_string().as_str() {
23
- "horizontal" => scrollbar.orientation(ScrollbarOrientation::HorizontalBottom),
34
+ "vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
35
+ "horizontal_bottom" | "horizontal" => scrollbar.orientation(ScrollbarOrientation::HorizontalBottom),
36
+ "horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
24
37
  _ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
25
38
  };
26
39
 
27
- if !block_val.is_nil() {
28
- let block = parse_block(block_val)?;
40
+ // Hoisted strings to extend lifetime
41
+ let thumb_str: String;
42
+ let track_str: String;
43
+ let begin_str: String;
44
+ let end_str: String;
45
+
46
+ if !thumb_symbol_val.is_nil() {
47
+ thumb_str = thumb_symbol_val.funcall("to_s", ())?;
48
+ scrollbar = scrollbar.thumb_symbol(&thumb_str);
49
+ }
50
+ if !thumb_style_val.is_nil() {
51
+ scrollbar = scrollbar.thumb_style(crate::style::parse_style(thumb_style_val)?);
52
+ }
53
+ if !track_symbol_val.is_nil() {
54
+ track_str = track_symbol_val.funcall("to_s", ())?;
55
+ scrollbar = scrollbar.track_symbol(Some(&track_str));
56
+ }
57
+ if !track_style_val.is_nil() {
58
+ scrollbar = scrollbar.track_style(crate::style::parse_style(track_style_val)?);
59
+ }
60
+ if !begin_symbol_val.is_nil() {
61
+ begin_str = begin_symbol_val.funcall("to_s", ())?;
62
+ scrollbar = scrollbar.begin_symbol(Some(&begin_str));
63
+ }
64
+ if !begin_style_val.is_nil() {
65
+ scrollbar = scrollbar.begin_style(crate::style::parse_style(begin_style_val)?);
66
+ }
67
+ if !end_symbol_val.is_nil() {
68
+ end_str = end_symbol_val.funcall("to_s", ())?;
69
+ scrollbar = scrollbar.end_symbol(Some(&end_str));
70
+ }
71
+ if !end_style_val.is_nil() {
72
+ scrollbar = scrollbar.end_style(crate::style::parse_style(end_style_val)?);
73
+ }
74
+ if !style_val.is_nil() {
75
+ scrollbar = scrollbar.style(crate::style::parse_style(style_val)?);
76
+ }
77
+
78
+ if block_val.is_nil() {
79
+ frame.render_stateful_widget(scrollbar, area, &mut state);
80
+ } else {
81
+ let bump = Bump::new();
82
+ let block = parse_block(block_val, &bump)?;
29
83
  let inner_area = block.inner(area);
30
84
  frame.render_widget(block, area);
31
85
  frame.render_stateful_widget(scrollbar, inner_area, &mut state);
32
- } else {
33
- frame.render_stateful_widget(scrollbar, area, &mut state);
34
86
  }
35
87
  Ok(())
36
88
  }
@@ -1,20 +1,33 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- use crate::style::{parse_block, parse_style};
5
- use magnus::{prelude::*, Error, Value};
6
- use ratatui::{layout::Rect, widgets::Sparkline, Frame};
4
+ use crate::style::{parse_block, parse_style, parse_bar_set};
5
+ use bumpalo::Bump;
6
+ use magnus::{prelude::*, Error, RString, Value};
7
+ use ratatui::{layout::Rect, widgets::Sparkline, widgets::RenderDirection, Frame};
7
8
 
8
9
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
+ let bump = Bump::new();
11
+ let ruby = magnus::Ruby::get().unwrap();
9
12
  let data_val: magnus::RArray = node.funcall("data", ())?;
10
13
  let max_val: Value = node.funcall("max", ())?;
11
14
  let style_val: Value = node.funcall("style", ())?;
12
15
  let block_val: Value = node.funcall("block", ())?;
16
+ let direction_val: Value = node.funcall("direction", ())?;
17
+ let absent_value_symbol_val: Value = node.funcall("absent_value_symbol", ())?;
18
+ let absent_value_style_val: Value = node.funcall("absent_value_style", ())?;
19
+ let bar_set_val: Value = node.funcall("bar_set", ())?;
13
20
 
14
21
  let mut data_vec = Vec::new();
15
22
  for i in 0..data_val.len() {
16
- let val: u64 = data_val.entry(i as isize)?;
17
- data_vec.push(val);
23
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
24
+ let val: Value = data_val.entry(index)?;
25
+ if val.is_nil() {
26
+ data_vec.push(None);
27
+ } else {
28
+ let num: u64 = u64::try_convert(val)?;
29
+ data_vec.push(Some(num));
30
+ }
18
31
  }
19
32
 
20
33
  let mut sparkline = Sparkline::default().data(&data_vec);
@@ -29,7 +42,33 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
29
42
  }
30
43
 
31
44
  if !block_val.is_nil() {
32
- sparkline = sparkline.block(parse_block(block_val)?);
45
+ sparkline = sparkline.block(parse_block(block_val, &bump)?);
46
+ }
47
+
48
+ if !direction_val.is_nil() {
49
+ let direction_sym: RString = direction_val.funcall("to_s", ())?;
50
+ let direction_str = direction_sym.to_string()?;
51
+ let direction = match direction_str.as_str() {
52
+ "right_to_left" => RenderDirection::RightToLeft,
53
+ _ => RenderDirection::LeftToRight,
54
+ };
55
+ sparkline = sparkline.direction(direction);
56
+ }
57
+
58
+ if !absent_value_symbol_val.is_nil() {
59
+ let symbol_str: String = String::try_convert(absent_value_symbol_val)?;
60
+ // Only use the first character if multiple are provided
61
+ if let Some(first_char) = symbol_str.chars().next() {
62
+ sparkline = sparkline.absent_value_symbol(first_char);
63
+ }
64
+ }
65
+
66
+ if !absent_value_style_val.is_nil() {
67
+ sparkline = sparkline.absent_value_style(parse_style(absent_value_style_val)?);
68
+ }
69
+
70
+ if !bar_set_val.is_nil() {
71
+ sparkline = sparkline.bar_set(parse_bar_set(bar_set_val, &bump)?);
33
72
  }
34
73
 
35
74
  frame.render_widget(sparkline, area);
@@ -56,4 +95,29 @@ mod tests {
56
95
  let bars = buf.content().iter().filter(|c| c.symbol() != " ").count();
57
96
  assert_eq!(bars, 4);
58
97
  }
98
+
99
+ #[test]
100
+ fn test_sparkline_absent_value_symbol() {
101
+ // Data with absent (None) and present values: [Some(5), None, Some(8), None]
102
+ let data = vec![Some(5), None, Some(8), None];
103
+ let sparkline = Sparkline::default()
104
+ .data(&data)
105
+ .absent_value_symbol("-");
106
+ let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
107
+ sparkline.render(Rect::new(0, 0, 4, 1), &mut buf);
108
+
109
+ // Collect all rendered symbols
110
+ let symbols: Vec<&str> = buf.content().iter().map(|c| c.symbol()).collect();
111
+
112
+ // Check that we have 4 cells rendered
113
+ assert_eq!(symbols.len(), 4, "Should have 4 cells rendered for 4 data points");
114
+
115
+ // Absent values (None) should render as "-"
116
+ assert_eq!(symbols[1], "-", "Second value (None) should render as dash");
117
+ assert_eq!(symbols[3], "-", "Fourth value (None) should render as dash");
118
+
119
+ // Present values should not be dashes
120
+ assert_ne!(symbols[0], "-", "First value (Some(5)) should not be dash");
121
+ assert_ne!(symbols[2], "-", "Third value (Some(8)) should not be dash");
122
+ }
59
123
  }
@@ -2,116 +2,222 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use bumpalo::Bump;
5
6
  use magnus::{prelude::*, Error, Symbol, Value};
6
7
  use ratatui::{
7
- layout::{Constraint, Rect},
8
- widgets::{Cell, Row, Table, TableState},
8
+ layout::{Constraint, Flex, Rect},
9
+ widgets::{Cell, HighlightSpacing, Row, Table, TableState},
9
10
  Frame,
10
11
  };
11
12
 
12
13
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
14
+ let bump = Bump::new();
13
15
  let ruby = magnus::Ruby::get().unwrap();
14
16
  let header_val: Value = node.funcall("header", ())?;
15
- let rows_val: Value = node.funcall("rows", ())?;
16
- let rows_array = magnus::RArray::from_value(rows_val)
17
+ let footer_val: Value = node.funcall("footer", ())?;
18
+ let rows_value: Value = node.funcall("rows", ())?;
19
+ let rows_array = magnus::RArray::from_value(rows_value)
17
20
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
18
21
  let widths_val: Value = node.funcall("widths", ())?;
19
22
  let widths_array = magnus::RArray::from_value(widths_val)
20
23
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
21
24
  let highlight_style_val: Value = node.funcall("highlight_style", ())?;
25
+ let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
26
+ let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
22
27
  let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
23
28
  let selected_row_val: Value = node.funcall("selected_row", ())?;
29
+ let selected_column_val: Value = node.funcall("selected_column", ())?;
24
30
  let block_val: Value = node.funcall("block", ())?;
31
+ let flex_sym: Symbol = node.funcall("flex", ())?;
32
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
25
33
 
26
34
  let mut rows = Vec::new();
27
35
  for i in 0..rows_array.len() {
28
- let row_val: Value = rows_array.entry(i as isize)?;
29
- let row_array = magnus::RArray::from_value(row_val)
30
- .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
31
-
32
- let mut cells = Vec::new();
33
- for j in 0..row_array.len() {
34
- let cell_val: Value = row_array.entry(j as isize)?;
35
- let class = cell_val.class();
36
- let class_name = unsafe { class.name() };
37
-
38
- if class_name.as_ref() == "RatatuiRuby::Paragraph" {
39
- let text: String = cell_val.funcall("text", ())?;
40
- let style_val: Value = cell_val.funcall("style", ())?;
41
- let cell_style = parse_style(style_val)?;
42
- cells.push(Cell::from(text).style(cell_style));
43
- } else if class_name.as_ref() == "RatatuiRuby::Style" {
44
- cells.push(Cell::from("").style(parse_style(cell_val)?));
45
- } else {
46
- let cell_str: String = cell_val.funcall("to_s", ())?;
47
- cells.push(Cell::from(cell_str));
48
- }
49
- }
50
- rows.push(Row::new(cells));
36
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
37
+ let row_val: Value = rows_array.entry(index)?;
38
+ rows.push(parse_row(row_val)?);
51
39
  }
52
40
 
53
- let mut constraints = Vec::new();
54
- for i in 0..widths_array.len() {
55
- let constraint_obj: Value = widths_array.entry(i as isize)?;
56
- let type_sym: Symbol = constraint_obj.funcall("type", ())?;
57
- let value: u16 = constraint_obj.funcall("value", ())?;
41
+ let constraints = parse_constraints(widths_array)?;
58
42
 
59
- match type_sym.to_string().as_str() {
60
- "length" => constraints.push(Constraint::Length(value)),
61
- "percentage" => constraints.push(Constraint::Percentage(value)),
62
- "min" => constraints.push(Constraint::Min(value)),
63
- _ => {}
64
- }
65
- }
43
+ let flex = match flex_sym.to_string().as_str() {
44
+ "start" => Flex::Start,
45
+ "center" => Flex::Center,
46
+ "end" => Flex::End,
47
+ "space_between" => Flex::SpaceBetween,
48
+ "space_around" => Flex::SpaceAround,
49
+ "space_evenly" => Flex::SpaceEvenly,
50
+ _ => Flex::Legacy,
51
+ };
66
52
 
67
- let mut table = Table::new(rows, constraints);
53
+ let mut table = Table::new(rows, constraints).flex(flex);
54
+
55
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
56
+ "always" => HighlightSpacing::Always,
57
+ "never" => HighlightSpacing::Never,
58
+ _ => HighlightSpacing::WhenSelected,
59
+ };
60
+ table = table.highlight_spacing(highlight_spacing);
68
61
 
69
62
  if !header_val.is_nil() {
70
- let header_array = magnus::RArray::from_value(header_val).ok_or_else(|| {
71
- Error::new(ruby.exception_type_error(), "expected array for header")
72
- })?;
73
- let mut header_cells = Vec::new();
74
- for i in 0..header_array.len() {
75
- let cell_val: Value = header_array.entry(i as isize)?;
76
- let class = cell_val.class();
77
- let class_name = unsafe { class.name() };
78
-
79
- if class_name.as_ref() == "RatatuiRuby::Paragraph" {
80
- let text: String = cell_val.funcall("text", ())?;
81
- let style_val: Value = cell_val.funcall("style", ())?;
82
- let cell_style = parse_style(style_val)?;
83
- header_cells.push(Cell::from(text).style(cell_style));
84
- } else {
85
- let cell_str: String = cell_val.funcall("to_s", ())?;
86
- header_cells.push(Cell::from(cell_str));
87
- }
88
- }
89
- table = table.header(Row::new(header_cells));
63
+ table = table.header(parse_row(header_val)?);
64
+ }
65
+
66
+ if !footer_val.is_nil() {
67
+ table = table.footer(parse_row(footer_val)?);
90
68
  }
91
69
 
92
70
  if !block_val.is_nil() {
93
- table = table.block(parse_block(block_val)?);
71
+ table = table.block(parse_block(block_val, &bump)?);
94
72
  }
95
73
 
96
74
  if !highlight_style_val.is_nil() {
97
75
  table = table.row_highlight_style(parse_style(highlight_style_val)?);
98
76
  }
99
77
 
78
+ if !column_highlight_style_val.is_nil() {
79
+ table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
80
+ }
81
+
82
+ if !cell_highlight_style_val.is_nil() {
83
+ table = table.cell_highlight_style(parse_style(cell_highlight_style_val)?);
84
+ }
85
+
100
86
  if !highlight_symbol_val.is_nil() {
101
87
  let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
102
88
  table = table.highlight_symbol(symbol);
103
89
  }
104
90
 
91
+ let style_val: Value = node.funcall("style", ())?;
92
+ if !style_val.is_nil() {
93
+ table = table.style(parse_style(style_val)?);
94
+ }
95
+
96
+ let column_spacing_val: Value = node.funcall("column_spacing", ())?;
97
+ if !column_spacing_val.is_nil() {
98
+ let spacing: u16 = column_spacing_val.funcall("to_int", ())?;
99
+ table = table.column_spacing(spacing);
100
+ }
101
+
105
102
  let mut state = TableState::default();
106
103
  if !selected_row_val.is_nil() {
107
104
  let index: usize = selected_row_val.funcall("to_int", ())?;
108
105
  state.select(Some(index));
109
106
  }
107
+ if !selected_column_val.is_nil() {
108
+ let index: usize = selected_column_val.funcall("to_int", ())?;
109
+ state.select_column(Some(index));
110
+ }
110
111
 
111
112
  frame.render_stateful_widget(table, area, &mut state);
112
113
  Ok(())
113
114
  }
114
115
 
116
+ fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
117
+ let ruby = magnus::Ruby::get().unwrap();
118
+ let row_array = magnus::RArray::from_value(row_val)
119
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
120
+
121
+ let mut cells = Vec::new();
122
+ for i in 0..row_array.len() {
123
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
124
+ let cell_val: Value = row_array.entry(index)?;
125
+ cells.push(parse_cell(cell_val)?);
126
+ }
127
+ Ok(Row::new(cells))
128
+ }
129
+
130
+ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
131
+ let class = cell_val.class();
132
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
133
+ let class_name = unsafe { class.name() }.into_owned();
134
+
135
+ if class_name == "RatatuiRuby::Paragraph" {
136
+ let text: String = cell_val.funcall("text", ())?;
137
+ let style_val: Value = cell_val.funcall("style", ())?;
138
+ let cell_style = parse_style(style_val)?;
139
+ Ok(Cell::from(text).style(cell_style))
140
+ } else if class_name == "RatatuiRuby::Style" {
141
+ Ok(Cell::from("").style(parse_style(cell_val)?))
142
+ } else if class_name == "RatatuiRuby::Cell" {
143
+ let symbol: String = cell_val.funcall("char", ())?;
144
+ let fg_val: Value = cell_val.funcall("fg", ())?;
145
+ let bg_val: Value = cell_val.funcall("bg", ())?;
146
+ let modifiers_val: Value = cell_val.funcall("modifiers", ())?;
147
+
148
+ let mut style = ratatui::style::Style::default();
149
+ if !fg_val.is_nil() {
150
+ if let Some(color) = crate::style::parse_color_value(fg_val)? {
151
+ style = style.fg(color);
152
+ }
153
+ }
154
+ if !bg_val.is_nil() {
155
+ if let Some(color) = crate::style::parse_color_value(bg_val)? {
156
+ style = style.bg(color);
157
+ }
158
+ }
159
+ if let Some(mods_array) = magnus::RArray::from_value(modifiers_val) {
160
+ let ruby = magnus::Ruby::get().unwrap();
161
+ for i in 0..mods_array.len() {
162
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
163
+ let mod_str: String = mods_array.entry::<String>(index)?;
164
+ if let Some(modifier) = crate::style::parse_modifier_str(&mod_str) {
165
+ style = style.add_modifier(modifier);
166
+ }
167
+ }
168
+ }
169
+ Ok(Cell::from(symbol).style(style))
170
+ } else {
171
+ let cell_str: String = cell_val.funcall("to_s", ())?;
172
+ Ok(Cell::from(cell_str))
173
+ }
174
+ }
175
+
176
+ fn parse_constraints(widths_array: magnus::RArray) -> Result<Vec<Constraint>, Error> {
177
+ let ruby = magnus::Ruby::get().unwrap();
178
+ let mut constraints = Vec::new();
179
+ for i in 0..widths_array.len() {
180
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
181
+ let constraint_obj: Value = widths_array.entry(index)?;
182
+ let type_sym: Symbol = constraint_obj.funcall("type", ())?;
183
+ let value_obj: Value = constraint_obj.funcall("value", ())?;
184
+
185
+ match type_sym.to_string().as_str() {
186
+ "length" => {
187
+ let val = u16::try_convert(value_obj)?;
188
+ constraints.push(Constraint::Length(val));
189
+ }
190
+ "percentage" => {
191
+ let val = u16::try_convert(value_obj)?;
192
+ constraints.push(Constraint::Percentage(val));
193
+ }
194
+ "min" => {
195
+ let val = u16::try_convert(value_obj)?;
196
+ constraints.push(Constraint::Min(val));
197
+ }
198
+ "max" => {
199
+ let val = u16::try_convert(value_obj)?;
200
+ constraints.push(Constraint::Max(val));
201
+ }
202
+ "fill" => {
203
+ let val = u16::try_convert(value_obj)?;
204
+ constraints.push(Constraint::Fill(val));
205
+ }
206
+ "ratio" => {
207
+ if let Some(arr) = magnus::RArray::from_value(value_obj) {
208
+ if arr.len() == 2 {
209
+ let n = u32::try_convert(arr.entry(0)?)?;
210
+ let d = u32::try_convert(arr.entry(1)?)?;
211
+ constraints.push(Constraint::Ratio(n, d));
212
+ }
213
+ }
214
+ }
215
+ _ => {}
216
+ }
217
+ }
218
+ Ok(constraints)
219
+ }
220
+
115
221
  #[cfg(test)]
116
222
  mod tests {
117
223
  use super::*;
@@ -122,14 +228,17 @@ mod tests {
122
228
  fn test_table_rendering() {
123
229
  let rows = vec![Row::new(vec!["C1", "C2"])];
124
230
  let table = Table::new(rows, [Constraint::Length(3), Constraint::Length(3)])
125
- .header(Row::new(vec!["H1", "H2"]));
126
- let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
127
- Widget::render(table, Rect::new(0, 0, 10, 2), &mut buf);
231
+ .header(Row::new(vec!["H1", "H2"]))
232
+ .footer(Row::new(vec!["F1", "F2"]));
233
+ let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
234
+ Widget::render(table, Rect::new(0, 0, 10, 3), &mut buf);
128
235
 
129
236
  let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
130
237
  // Check for presence of header and row content
131
238
  assert!(content.contains("H1"));
132
239
  assert!(content.contains("H2"));
240
+ assert!(content.contains("F1"));
241
+ assert!(content.contains("F2"));
133
242
  assert!(content.contains("C1"));
134
243
  assert!(content.contains("C2"));
135
244
  }