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
@@ -1,7 +1,9 @@
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_color};
4
+ use crate::style::{parse_block, parse_color, parse_style};
5
+ use crate::text::parse_text;
6
+ use bumpalo::Bump;
5
7
  use magnus::{prelude::*, Error, RArray, Symbol, Value};
6
8
  use ratatui::{
7
9
  symbols::Marker,
@@ -10,6 +12,7 @@ use ratatui::{
10
12
  };
11
13
 
12
14
  pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Result<(), Error> {
15
+ let bump = Bump::new();
13
16
  let shapes_val: RArray = node.funcall("shapes", ())?;
14
17
  let x_bounds_val: RArray = node.funcall("x_bounds", ())?;
15
18
  let y_bounds_val: RArray = node.funcall("y_bounds", ())?;
@@ -23,6 +26,10 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
23
26
  "dot" => Marker::Dot,
24
27
  "block" => Marker::Block,
25
28
  "bar" => Marker::Bar,
29
+ "half_block" => Marker::HalfBlock,
30
+ "quadrant" => Marker::Quadrant,
31
+ "sextant" => Marker::Sextant,
32
+ "octant" => Marker::Octant,
26
33
  _ => Marker::Braille,
27
34
  };
28
35
 
@@ -32,16 +39,24 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
32
39
  .marker(marker);
33
40
 
34
41
  if !block_val.is_nil() {
35
- canvas = canvas.block(parse_block(block_val)?);
42
+ canvas = canvas.block(parse_block(block_val, &bump)?);
43
+ }
44
+
45
+ let background_color_val: Value = node.funcall("background_color", ())?;
46
+ if !background_color_val.is_nil() {
47
+ let background_color =
48
+ parse_color(&background_color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
49
+ canvas = canvas.background_color(background_color);
36
50
  }
37
51
 
38
52
  let canvas = canvas.paint(|ctx| {
39
53
  for shape_val in shapes_val {
40
54
  let class = shape_val.class();
41
- let class_name = unsafe { class.name() };
55
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
56
+ let class_name = unsafe { class.name() }.into_owned();
42
57
 
43
- match class_name.as_ref() {
44
- "RatatuiRuby::Line" => {
58
+ match class_name.as_str() {
59
+ "RatatuiRuby::Shape::Line" => {
45
60
  let x1: f64 = shape_val.funcall("x1", ()).unwrap_or(0.0);
46
61
  let y1: f64 = shape_val.funcall("y1", ()).unwrap_or(0.0);
47
62
  let x2: f64 = shape_val.funcall("x2", ()).unwrap_or(0.0);
@@ -49,15 +64,9 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
49
64
  let color_val: Value = shape_val.funcall("color", ()).unwrap();
50
65
  let color =
51
66
  parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
52
- ctx.draw(&Line {
53
- x1,
54
- y1,
55
- x2,
56
- y2,
57
- color,
58
- });
67
+ ctx.draw(&Line { x1, y1, x2, y2, color });
59
68
  }
60
- "RatatuiRuby::Rectangle" => {
69
+ "RatatuiRuby::Shape::Rectangle" => {
61
70
  let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
62
71
  let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
63
72
  let width: f64 = shape_val.funcall("width", ()).unwrap_or(0.0);
@@ -65,29 +74,18 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
65
74
  let color_val: Value = shape_val.funcall("color", ()).unwrap();
66
75
  let color =
67
76
  parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
68
- ctx.draw(&Rectangle {
69
- x,
70
- y,
71
- width,
72
- height,
73
- color,
74
- });
77
+ ctx.draw(&Rectangle { x, y, width, height, color });
75
78
  }
76
- "RatatuiRuby::Circle" => {
79
+ "RatatuiRuby::Shape::Circle" => {
77
80
  let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
78
81
  let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
79
82
  let radius: f64 = shape_val.funcall("radius", ()).unwrap_or(0.0);
80
83
  let color_val: Value = shape_val.funcall("color", ()).unwrap();
81
84
  let color =
82
85
  parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
83
- ctx.draw(&Circle {
84
- x,
85
- y,
86
- radius,
87
- color,
88
- });
86
+ ctx.draw(&Circle { x, y, radius, color });
89
87
  }
90
- "RatatuiRuby::Map" => {
88
+ "RatatuiRuby::Shape::Map" => {
91
89
  let color_val: Value = shape_val.funcall("color", ()).unwrap();
92
90
  let color =
93
91
  parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
@@ -98,6 +96,26 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
98
96
  };
99
97
  ctx.draw(&Map { color, resolution });
100
98
  }
99
+ "RatatuiRuby::Shape::Label" => {
100
+ let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
101
+ let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
102
+ let text_val: Value = shape_val.funcall("text", ()).unwrap();
103
+ let style_val: Value = shape_val.funcall("style", ()).unwrap();
104
+
105
+ // Parse text - take first line if multiple
106
+ if let Ok(lines) = parse_text(text_val) {
107
+ let mut line = lines.into_iter().next().unwrap_or_default();
108
+
109
+ // Apply style if provided
110
+ if !style_val.is_nil() {
111
+ if let Ok(style) = parse_style(style_val) {
112
+ line = line.style(style);
113
+ }
114
+ }
115
+
116
+ ctx.print(x, y, line);
117
+ }
118
+ }
101
119
  _ => {}
102
120
  }
103
121
  }
@@ -132,7 +150,6 @@ mod tests {
132
150
  let mut buf = Buffer::empty(Rect::new(0, 0, 5, 5));
133
151
  canvas.render(Rect::new(0, 0, 5, 5), &mut buf);
134
152
 
135
- // Verify that some Braille characters are rendered
136
153
  let mut found_braille = false;
137
154
  for cell in buf.content() {
138
155
  if !cell.symbol().trim().is_empty() {
@@ -2,19 +2,21 @@
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
8
  layout::Rect,
8
9
  symbols,
9
10
  text::Span,
10
- widgets::{Axis, Chart, Dataset, GraphType},
11
+ widgets::{Axis, Chart, Dataset, GraphType, LegendPosition},
11
12
  Frame,
12
13
  };
13
14
 
14
15
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
16
+ let bump = Bump::new();
15
17
  let ruby = magnus::Ruby::get().unwrap();
16
- let class = node.class();
17
- let class_name = unsafe { class.name() };
18
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
19
+ let class_name = unsafe { node.class().name() }.into_owned();
18
20
 
19
21
  if class_name == "RatatuiRuby::LineChart" {
20
22
  return render_line_chart(frame, area, node);
@@ -25,18 +27,22 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
25
27
  let y_axis_val: Value = node.funcall("y_axis", ())?;
26
28
  let block_val: Value = node.funcall("block", ())?;
27
29
  let style_val: Value = node.funcall("style", ())?;
30
+ let legend_position_val: Value = node.funcall("legend_position", ())?;
31
+ let hidden_legend_constraints_val: Value = node.funcall("hidden_legend_constraints", ())?;
28
32
 
29
33
  let mut datasets = Vec::new();
30
34
  // We need to keep the data alive until the chart is rendered
31
35
  let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
32
36
 
33
37
  for i in 0..datasets_val.len() {
34
- let ds_val: Value = datasets_val.entry(i as isize)?;
38
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
39
+ let ds_val: Value = datasets_val.entry(index)?;
35
40
  let data_array: magnus::RArray = ds_val.funcall("data", ())?;
36
41
 
37
42
  let mut points = Vec::new();
38
43
  for j in 0..data_array.len() {
39
- let point_array_val: Value = data_array.entry(j as isize)?;
44
+ let index = isize::try_from(j).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
45
+ let point_array_val: Value = data_array.entry(index)?;
40
46
  let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
41
47
  Error::new(ruby.exception_type_error(), "expected array for point")
42
48
  })?;
@@ -48,13 +54,13 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
48
54
  }
49
55
 
50
56
  for (i, points) in data_storage.iter().enumerate() {
51
- let ds_val: Value = datasets_val.entry(i as isize)?;
57
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
58
+ let ds_val: Value = datasets_val.entry(index)?;
52
59
  let name: String = ds_val.funcall("name", ())?;
53
60
  let marker_sym: Symbol = ds_val.funcall("marker", ())?;
54
61
  let graph_type_sym: Symbol = ds_val.funcall("graph_type", ())?;
55
62
 
56
63
  let marker = match marker_sym.to_string().as_str() {
57
- "dot" => symbols::Marker::Dot,
58
64
  "braille" => symbols::Marker::Braille,
59
65
  "block" => symbols::Marker::Block,
60
66
  "bar" => symbols::Marker::Bar,
@@ -62,18 +68,14 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
62
68
  };
63
69
 
64
70
  let graph_type = match graph_type_sym.to_string().as_str() {
65
- "line" => GraphType::Line,
66
71
  "scatter" => GraphType::Scatter,
67
72
  _ => GraphType::Line,
68
73
  };
69
74
 
70
75
  let mut ds_style = ratatui::style::Style::default();
71
- let color_val: Value = ds_val.funcall("color", ())?;
72
- if !color_val.is_nil() {
73
- let color_str: String = color_val.funcall("to_s", ())?;
74
- if let Some(color) = crate::style::parse_color(&color_str) {
75
- ds_style = ds_style.fg(color);
76
- }
76
+ let style_val: Value = ds_val.funcall("style", ())?;
77
+ if !style_val.is_nil() {
78
+ ds_style = parse_style(style_val)?;
77
79
  }
78
80
 
79
81
  let ds = Dataset::default()
@@ -91,28 +93,53 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
91
93
  let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
92
94
 
93
95
  if !block_val.is_nil() {
94
- chart = chart.block(parse_block(block_val)?);
96
+ chart = chart.block(parse_block(block_val, &bump)?);
95
97
  }
96
98
 
97
99
  if !style_val.is_nil() {
98
100
  chart = chart.style(parse_style(style_val)?);
99
101
  }
100
102
 
103
+ if !legend_position_val.is_nil() {
104
+ let pos_sym: Symbol = legend_position_val.funcall("to_sym", ())?;
105
+ let pos = match pos_sym.to_string().as_str() {
106
+ "top_left" => LegendPosition::TopLeft,
107
+ "bottom_left" => LegendPosition::BottomLeft,
108
+ "bottom_right" => LegendPosition::BottomRight,
109
+ _ => LegendPosition::TopRight,
110
+ };
111
+ chart = chart.legend_position(Some(pos));
112
+ }
113
+
114
+ if !hidden_legend_constraints_val.is_nil() {
115
+ let constraints_array: magnus::RArray = hidden_legend_constraints_val.funcall("to_a", ())?;
116
+ if constraints_array.len() == 2 {
117
+ let width_val: Value = constraints_array.entry(0)?;
118
+ let height_val: Value = constraints_array.entry(1)?;
119
+ let width_constraint = super::layout::parse_constraint(width_val)?;
120
+ let height_constraint = super::layout::parse_constraint(height_val)?;
121
+ chart = chart.hidden_legend_constraints((width_constraint, height_constraint));
122
+ }
123
+ }
124
+
101
125
  frame.render_widget(chart, area);
102
126
  Ok(())
103
127
  }
104
128
 
105
129
  fn parse_axis(axis_val: Value) -> Result<Axis<'static>, Error> {
130
+ let ruby = magnus::Ruby::get().unwrap();
106
131
  let title: String = axis_val.funcall("title", ())?;
107
132
  let bounds_val: magnus::RArray = axis_val.funcall("bounds", ())?;
108
133
  let labels_val: magnus::RArray = axis_val.funcall("labels", ())?;
109
134
  let style_val: Value = axis_val.funcall("style", ())?;
135
+ let labels_alignment_val: Value = axis_val.funcall("labels_alignment", ())?;
110
136
 
111
137
  let bounds: [f64; 2] = [bounds_val.entry(0)?, bounds_val.entry(1)?];
112
138
 
113
139
  let mut labels = Vec::new();
114
140
  for i in 0..labels_val.len() {
115
- let label: String = labels_val.entry(i as isize)?;
141
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
142
+ let label: String = labels_val.entry(index)?;
116
143
  labels.push(Span::from(label));
117
144
  }
118
145
 
@@ -122,10 +149,21 @@ fn parse_axis(axis_val: Value) -> Result<Axis<'static>, Error> {
122
149
  axis = axis.style(parse_style(style_val)?);
123
150
  }
124
151
 
152
+ if !labels_alignment_val.is_nil() {
153
+ let alignment_sym: Symbol = labels_alignment_val.funcall("to_sym", ())?;
154
+ let alignment = match alignment_sym.to_string().as_str() {
155
+ "left" => ratatui::layout::HorizontalAlignment::Left,
156
+ "right" => ratatui::layout::HorizontalAlignment::Right,
157
+ _ => ratatui::layout::HorizontalAlignment::Center,
158
+ };
159
+ axis = axis.labels_alignment(alignment);
160
+ }
161
+
125
162
  Ok(axis)
126
163
  }
127
164
 
128
165
  fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
166
+ let bump = Bump::new();
129
167
  let ruby = magnus::Ruby::get().unwrap();
130
168
  let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
131
169
  let x_labels_val: magnus::RArray = node.funcall("x_labels", ())?;
@@ -137,12 +175,14 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
137
175
  let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
138
176
 
139
177
  for i in 0..datasets_val.len() {
140
- let ds_val: Value = datasets_val.entry(i as isize)?;
178
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
179
+ let ds_val: Value = datasets_val.entry(index)?;
141
180
  let data_array: magnus::RArray = ds_val.funcall("data", ())?;
142
181
 
143
182
  let mut points = Vec::new();
144
183
  for j in 0..data_array.len() {
145
- let point_array_val: Value = data_array.entry(j as isize)?;
184
+ let index = isize::try_from(j).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
185
+ let point_array_val: Value = data_array.entry(index)?;
146
186
  let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
147
187
  Error::new(ruby.exception_type_error(), "expected array for point")
148
188
  })?;
@@ -154,14 +194,14 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
154
194
  }
155
195
 
156
196
  for (i, points) in data_storage.iter().enumerate() {
157
- let ds_val: Value = datasets_val.entry(i as isize)?;
197
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
198
+ let ds_val: Value = datasets_val.entry(index)?;
158
199
  let name: String = ds_val.funcall("name", ())?;
159
200
 
160
201
  let mut ds_style = ratatui::style::Style::default();
161
- let color_val: Value = ds_val.funcall("color", ())?;
162
- let color_str: String = color_val.funcall("to_s", ())?;
163
- if let Some(color) = crate::style::parse_color(&color_str) {
164
- ds_style = ds_style.fg(color);
202
+ let style_val: Value = ds_val.funcall("style", ())?;
203
+ if !style_val.is_nil() {
204
+ ds_style = parse_style(style_val)?;
165
205
  }
166
206
 
167
207
  let ds = Dataset::default()
@@ -174,13 +214,15 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
174
214
 
175
215
  let mut x_labels = Vec::new();
176
216
  for i in 0..x_labels_val.len() {
177
- let label: String = x_labels_val.entry(i as isize)?;
217
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
218
+ let label: String = x_labels_val.entry(index)?;
178
219
  x_labels.push(Span::from(label));
179
220
  }
180
221
 
181
222
  let mut y_labels = Vec::new();
182
223
  for i in 0..y_labels_val.len() {
183
- let label: String = y_labels_val.entry(i as isize)?;
224
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
225
+ let label: String = y_labels_val.entry(index)?;
184
226
  y_labels.push(Span::from(label));
185
227
  }
186
228
  // Ratatui 0.29+ requires labels to be present for the axis line to render
@@ -210,7 +252,7 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
210
252
  }
211
253
  }
212
254
  }
213
- if min_x == max_x {
255
+ if (min_x - max_x).abs() < f64::EPSILON {
214
256
  max_x = min_x + 1.0;
215
257
  }
216
258
 
@@ -219,7 +261,7 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
219
261
 
220
262
  let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
221
263
  if !block_val.is_nil() {
222
- chart = chart.block(parse_block(block_val)?);
264
+ chart = chart.block(parse_block(block_val, &bump)?);
223
265
  }
224
266
 
225
267
  frame.render_widget(chart, area);
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
+ use bumpalo::Bump;
4
5
  use magnus::{prelude::*, Error, Value};
5
6
  use ratatui::{layout::Rect, widgets::Widget, Frame};
6
7
 
@@ -10,7 +11,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
11
  // If a block is provided, render it on top of the cleared area
11
12
  if let Ok(block_val) = node.funcall::<_, _, Value>("block", ()) {
12
13
  if !block_val.is_nil() {
13
- let block = crate::style::parse_block(block_val)?;
14
+ let bump = Bump::new();
15
+ let block = crate::style::parse_block(block_val, &bump)?;
14
16
  block.render(area, frame.buffer_mut());
15
17
  }
16
18
  }
@@ -2,16 +2,19 @@
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, Value};
6
7
  use ratatui::{layout::Rect, widgets::Gauge, Frame};
7
8
 
8
9
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
+ let bump = Bump::new();
9
11
  let ratio: f64 = node.funcall("ratio", ())?;
10
12
  let label_val: Value = node.funcall("label", ())?;
11
13
  let style_val: Value = node.funcall("style", ())?;
14
+ let gauge_style_val: Value = node.funcall("gauge_style", ())?;
12
15
  let block_val: Value = node.funcall("block", ())?;
13
-
14
- let mut gauge = Gauge::default().ratio(ratio);
16
+ let use_unicode: bool = node.funcall("use_unicode", ())?;
17
+ let mut gauge = Gauge::default().ratio(ratio).use_unicode(use_unicode);
15
18
 
16
19
  if !label_val.is_nil() {
17
20
  let label_str: String = label_val.funcall("to_s", ())?;
@@ -19,11 +22,15 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
19
22
  }
20
23
 
21
24
  if !style_val.is_nil() {
22
- gauge = gauge.gauge_style(parse_style(style_val)?);
25
+ gauge = gauge.style(parse_style(style_val)?);
26
+ }
27
+
28
+ if !gauge_style_val.is_nil() {
29
+ gauge = gauge.gauge_style(parse_style(gauge_style_val)?);
23
30
  }
24
31
 
25
32
  if !block_val.is_nil() {
26
- gauge = gauge.block(parse_block(block_val)?);
33
+ gauge = gauge.block(parse_block(block_val, &bump)?);
27
34
  }
28
35
 
29
36
  frame.render_widget(gauge, area);