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,241 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Frame wrapper for exposing Ratatui's Frame to Ruby.
5
+ //!
6
+ //! This module provides `RubyFrame`, a struct that wraps `ratatui::Frame` and exposes
7
+ //! it to Ruby via Magnus. It enables explicit widget placement through `render_widget`,
8
+ //! aligning `RatatuiRuby` with native Rust Ratatui patterns.
9
+ //!
10
+ //! # Safety
11
+ //!
12
+ //! `RubyFrame` uses raw pointer casting to store a `Frame` reference with an erased
13
+ //! lifetime. This is safe because:
14
+ //! 1. `RubyFrame` is only created within `Terminal::draw()` callbacks
15
+ //! 2. `RubyFrame` is never returned from or stored beyond the callback scope
16
+ //! 3. The Ruby block receiving `RubyFrame` completes before the callback returns
17
+ //!
18
+ //! The `'static` lifetime is a lie, but a safe one within these constraints.
19
+
20
+ use crate::rendering;
21
+ use crate::widgets;
22
+ use magnus::{prelude::*, Error, Value};
23
+ use ratatui::layout::Rect;
24
+ use ratatui::Frame;
25
+ use std::cell::UnsafeCell;
26
+ use std::ptr::NonNull;
27
+ use std::sync::atomic::{AtomicBool, Ordering};
28
+ use std::sync::Arc;
29
+
30
+ /// A wrapper around Ratatui's `Frame` that can be exposed to Ruby.
31
+ ///
32
+ /// This struct uses raw pointers to hold a mutable reference to the frame,
33
+ /// which is valid only for the duration of the draw callback.
34
+ ///
35
+ /// # Safety
36
+ ///
37
+ /// We implement `Send` manually because:
38
+ /// 1. `RubyFrame` is only created and used within a single `Terminal::draw()` callback
39
+ /// 2. The Ruby VM is single-threaded (GVL), so the frame pointer is never accessed
40
+ /// from multiple threads simultaneously
41
+ /// 3. `RubyFrame` never escapes the draw callback scope
42
+ ///
43
+ /// The `active` flag provides runtime safety by preventing use after the draw
44
+ /// callback completes. Without this, a user could store the frame and cause
45
+ /// undefined behavior by accessing it after the underlying pointer is invalid.
46
+ #[magnus::wrap(class = "RatatuiRuby::Frame")]
47
+ pub struct RubyFrame {
48
+ /// Pointer to the underlying frame. Valid only during the draw callback.
49
+ inner: UnsafeCell<NonNull<Frame<'static>>>,
50
+ /// Shared flag to invalidate the frame when the block finishes.
51
+ /// Set to `true` during draw, `false` immediately after yield returns.
52
+ active: Arc<AtomicBool>,
53
+ }
54
+
55
+ // SAFETY: RubyFrame is only used within Terminal::draw() callbacks, which are
56
+ // single-threaded. The Ruby VM's GVL ensures no concurrent access.
57
+ unsafe impl Send for RubyFrame {}
58
+
59
+ impl RubyFrame {
60
+ /// Creates a new `RubyFrame` wrapping the given frame reference.
61
+ ///
62
+ /// # Arguments
63
+ ///
64
+ /// * `frame` - Mutable reference to the underlying Ratatui frame
65
+ /// * `active` - Shared atomic flag that controls frame validity
66
+ ///
67
+ /// # Safety
68
+ ///
69
+ /// The caller must ensure that:
70
+ /// 1. The `RubyFrame` does not outlive the frame reference
71
+ /// 2. No other mutable references to the frame exist while `RubyFrame` is in use
72
+ /// 3. The `active` flag is set to `false` after the draw callback completes
73
+ pub fn new(frame: &mut Frame<'_>, active: Arc<AtomicBool>) -> Self {
74
+ // SAFETY: We cast the frame pointer to 'static lifetime. This is safe because:
75
+ // - RubyFrame is only used within Terminal::draw() callbacks
76
+ // - The Ruby block completes before the callback returns
77
+ // - No reference to RubyFrame escapes the callback scope
78
+ let ptr = NonNull::from(frame);
79
+ let static_ptr: NonNull<Frame<'static>> =
80
+ // SAFETY: Lifetime erasure is safe within the draw callback scope.
81
+ // The frame pointer remains valid for the entire callback duration.
82
+ unsafe { std::mem::transmute(ptr) };
83
+
84
+ Self {
85
+ inner: UnsafeCell::new(static_ptr),
86
+ active,
87
+ }
88
+ }
89
+
90
+ /// Checks that the frame is still valid for use.
91
+ ///
92
+ /// Returns `Ok(())` if the frame can be used, or an error if the draw
93
+ /// callback has already completed.
94
+ fn ensure_active(&self) -> Result<(), Error> {
95
+ if self.active.load(Ordering::Relaxed) {
96
+ Ok(())
97
+ } else {
98
+ let ruby = magnus::Ruby::get().unwrap();
99
+ let module = ruby.define_module("RatatuiRuby")?;
100
+ let error_base = module.const_get::<_, magnus::RClass>("Error")?;
101
+ let error_class = error_base.const_get("Safety")?;
102
+ Err(Error::new(
103
+ error_class,
104
+ "Frame cannot be used outside of the draw block",
105
+ ))
106
+ }
107
+ }
108
+
109
+ /// Returns the terminal area as a Ruby `RatatuiRuby::Rect`.
110
+ ///
111
+ /// This mirrors `frame.area()` in Rust Ratatui.
112
+ pub fn area(&self) -> Result<Value, Error> {
113
+ self.ensure_active()?;
114
+ let ruby = magnus::Ruby::get().unwrap();
115
+
116
+ // SAFETY: The frame pointer is valid for the duration of the draw callback.
117
+ // We only read from the frame, which is safe with an immutable reference.
118
+ // The ensure_active() check above guarantees we're still in the callback.
119
+ let area = unsafe { (*self.inner.get()).as_ref().area() };
120
+
121
+ // Create a Ruby Layout::Rect object
122
+ let module = ruby.define_module("RatatuiRuby")?;
123
+ let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
124
+ let class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
125
+ class.funcall("new", (area.x, area.y, area.width, area.height))
126
+ }
127
+
128
+ /// Renders a widget at the specified area.
129
+ ///
130
+ /// This mirrors `frame.render_widget(widget, area)` in Rust Ratatui.
131
+ ///
132
+ /// # Arguments
133
+ ///
134
+ /// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::Paragraph`)
135
+ /// * `area` - A Ruby `Rect` or hash-like object with `x`, `y`, `width`, `height`
136
+ pub fn render_widget(&self, widget: Value, area: Value) -> Result<(), Error> {
137
+ self.ensure_active()?;
138
+
139
+ // Parse the Ruby area into a Rust Rect
140
+ let x: u16 = area.funcall("x", ())?;
141
+ let y: u16 = area.funcall("y", ())?;
142
+ let width: u16 = area.funcall("width", ())?;
143
+ let height: u16 = area.funcall("height", ())?;
144
+ let rect = Rect::new(x, y, width, height);
145
+
146
+ // SAFETY: The frame pointer is valid for the duration of the draw callback.
147
+ // We take a mutable reference which is safe because:
148
+ // 1. RubyFrame is only used within Terminal::draw() callbacks
149
+ // 2. Ruby's GVL ensures single-threaded access
150
+ // 3. No other code holds a reference to the frame during this call
151
+ // 4. ensure_active() above guarantees we're still in the callback
152
+ let frame = unsafe { (*self.inner.get()).as_mut() };
153
+
154
+ // Special case: Cursor widget requires Frame.set_cursor_position
155
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
156
+ let widget_class = unsafe { widget.class().name() }.into_owned();
157
+ if widget_class == "RatatuiRuby::Widgets::Cursor" {
158
+ let cursor_x: u16 = widget.funcall("x", ())?;
159
+ let cursor_y: u16 = widget.funcall("y", ())?;
160
+ frame.set_cursor_position((rect.x + cursor_x, rect.y + cursor_y));
161
+ return Ok(());
162
+ }
163
+
164
+ // Delegate to the existing render_node function
165
+ rendering::render_node(frame.buffer_mut(), rect, widget)
166
+ }
167
+
168
+ /// Renders a stateful widget at the specified area.
169
+ ///
170
+ /// This mirrors `frame.render_stateful_widget(widget, area, &mut state)` in Rust Ratatui.
171
+ /// The State object is the single source of truth for selection and offset.
172
+ /// Widget properties (`selected_index`, `selected_row`, `offset`) are ignored.
173
+ ///
174
+ /// # Arguments
175
+ ///
176
+ /// * `widget` - A Ruby widget object (e.g., `RatatuiRuby::List`)
177
+ /// * `area` - A Ruby `Rect`
178
+ /// * `state` - A Ruby state object (e.g., `RatatuiRuby::ListState`)
179
+ pub fn render_stateful_widget(
180
+ &self,
181
+ widget: Value,
182
+ area: Value,
183
+ state: Value,
184
+ ) -> Result<(), Error> {
185
+ self.ensure_active()?;
186
+ let ruby = magnus::Ruby::get().unwrap();
187
+
188
+ // Parse the Ruby area into a Rust Rect
189
+ let x: u16 = area.funcall("x", ())?;
190
+ let y: u16 = area.funcall("y", ())?;
191
+ let width: u16 = area.funcall("width", ())?;
192
+ let height: u16 = area.funcall("height", ())?;
193
+ let rect = Rect::new(x, y, width, height);
194
+
195
+ // SAFETY: The frame pointer is valid for the duration of the draw callback.
196
+ let frame = unsafe { (*self.inner.get()).as_mut() };
197
+
198
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
199
+ let widget_class = unsafe { widget.class().name() }.into_owned();
200
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
201
+ let state_class = unsafe { state.class().name() }.into_owned();
202
+
203
+ match (widget_class.as_str(), state_class.as_str()) {
204
+ ("RatatuiRuby::Widgets::List", "RatatuiRuby::ListState") => {
205
+ let buffer = frame.buffer_mut();
206
+ widgets::list::render_stateful(buffer, rect, widget, state)
207
+ }
208
+ ("RatatuiRuby::Widgets::Table", "RatatuiRuby::TableState") => {
209
+ let buffer = frame.buffer_mut();
210
+ widgets::table::render_stateful(buffer, rect, widget, state)
211
+ }
212
+ ("RatatuiRuby::Widgets::Scrollbar", "RatatuiRuby::ScrollbarState") => {
213
+ let buffer = frame.buffer_mut();
214
+ widgets::scrollbar::render_stateful(buffer, rect, widget, state)
215
+ }
216
+ _ => Err(Error::new(
217
+ ruby.exception_arg_error(),
218
+ format!("Unsupported widget/state combination: {widget_class} with {state_class}"),
219
+ )),
220
+ }
221
+ }
222
+
223
+ /// Sets the cursor position in the terminal.
224
+ ///
225
+ /// This mirrors `frame.set_cursor_position((x, y))` in Rust Ratatui.
226
+ /// Use this for text input fields to show the cursor at the correct location.
227
+ ///
228
+ /// # Arguments
229
+ ///
230
+ /// * `x` - Column position (0-indexed from left)
231
+ /// * `y` - Row position (0-indexed from top)
232
+ pub fn set_cursor_position(&self, x: u16, y: u16) -> Result<(), Error> {
233
+ self.ensure_active()?;
234
+
235
+ // SAFETY: The frame pointer is valid for the duration of the draw callback.
236
+ // ensure_active() above guarantees we're still in the callback.
237
+ let frame = unsafe { (*self.inner.get()).as_mut() };
238
+ frame.set_cursor_position((x, y));
239
+ Ok(())
240
+ }
241
+ }
@@ -0,0 +1,343 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ // Require SAFETY comments on all unsafe blocks
5
+ #![warn(clippy::undocumented_unsafe_blocks)]
6
+ // Enable pedantic lints for stricter code quality
7
+ #![warn(clippy::pedantic)]
8
+ // Allow certain pedantic lints that are too noisy for FFI code
9
+ #![allow(clippy::missing_errors_doc)]
10
+ #![allow(clippy::missing_panics_doc)]
11
+ #![allow(clippy::module_name_repetitions)]
12
+ #![allow(clippy::non_std_lazy_statics)]
13
+
14
+ mod color;
15
+ mod errors;
16
+ mod events;
17
+ mod frame;
18
+ mod rendering;
19
+ mod string_width;
20
+ mod style;
21
+ mod terminal; // New module with TerminalQuery trait
22
+ mod text;
23
+ mod widgets;
24
+
25
+ use frame::RubyFrame;
26
+ use magnus::{function, method, Error, Module, Object, Ruby, Value};
27
+ use terminal::{init_terminal, restore_terminal};
28
+
29
+ /// Draw to the terminal.
30
+ ///
31
+ /// Supports two calling conventions:
32
+ /// - Legacy: `RatatuiRuby.draw(tree)` - Renders a widget tree to the full terminal area
33
+ /// - New: `RatatuiRuby.draw { |frame| ... }` - Yields a Frame for explicit widget placement
34
+ fn draw(args: &[Value]) -> Result<(), Error> {
35
+ let ruby = Ruby::get().unwrap();
36
+
37
+ // Parse arguments: check for optional tree argument
38
+ let tree: Option<Value> = if args.is_empty() {
39
+ None
40
+ } else if args.len() == 1 {
41
+ Some(args[0])
42
+ } else {
43
+ return Err(Error::new(
44
+ ruby.exception_arg_error(),
45
+ format!(
46
+ "wrong number of arguments (given {}, expected 0..1)",
47
+ args.len()
48
+ ),
49
+ ));
50
+ };
51
+ let block_given = ruby.block_given();
52
+
53
+ // Validate: must have either tree or block, but not both
54
+ if tree.is_some() && block_given {
55
+ return Err(Error::new(
56
+ ruby.exception_arg_error(),
57
+ "Cannot provide both a tree and a block to draw",
58
+ ));
59
+ }
60
+ if tree.is_none() && !block_given {
61
+ return Err(Error::new(
62
+ ruby.exception_arg_error(),
63
+ "Must provide either a tree or a block to draw",
64
+ ));
65
+ }
66
+
67
+ // Nested draw() calls are not allowed - would deadlock
68
+ if terminal::is_in_draw_mode() {
69
+ let module = ruby.define_module("RatatuiRuby")?;
70
+ let error_base = module.const_get::<_, magnus::RClass>("Error")?;
71
+ let error_class = error_base.const_get("Invariant")?;
72
+ return Err(Error::new(
73
+ error_class,
74
+ "draw cannot be called during another draw (nested draws not allowed)",
75
+ ));
76
+ }
77
+
78
+ // Use lend_for_draw which handles snapshot capture/cleanup automatically
79
+ let result = terminal::lend_for_draw(|wrapper| {
80
+ let ruby = magnus::Ruby::get().expect("Ruby must be initialized");
81
+ let mut render_error: Option<Error> = None;
82
+
83
+ // Helper closure to execute the draw callback logic for either terminal type
84
+ let mut draw_callback = |f: &mut ratatui::Frame<'_>| {
85
+ if block_given {
86
+ // New API: yield RubyFrame to block
87
+ let active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
88
+
89
+ let ruby_frame = RubyFrame::new(f, active.clone());
90
+ if let Err(e) = ruby.yield_value::<_, Value>(ruby_frame) {
91
+ render_error = Some(e);
92
+ }
93
+
94
+ // Invalidate frame immediately after block returns
95
+ active.store(false, std::sync::atomic::Ordering::Relaxed);
96
+ } else if let Some(tree_value) = tree {
97
+ // Legacy API: render tree to full area
98
+ let area = f.area();
99
+ if let Err(e) = rendering::render_node(f.buffer_mut(), area, tree_value) {
100
+ render_error = Some(e);
101
+ }
102
+ }
103
+ };
104
+
105
+ match wrapper {
106
+ terminal::TerminalWrapper::Crossterm(term) => {
107
+ let module = ruby.define_module("RatatuiRuby").unwrap();
108
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
109
+ let error_class = error_base.const_get("Terminal").unwrap();
110
+ term.draw(&mut draw_callback)
111
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
112
+ }
113
+ terminal::TerminalWrapper::Test(term) => {
114
+ let module = ruby.define_module("RatatuiRuby").unwrap();
115
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
116
+ let error_class = error_base.const_get("Terminal").unwrap();
117
+ term.draw(&mut draw_callback)
118
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
119
+ }
120
+ }
121
+
122
+ if let Some(e) = render_error {
123
+ return Err(e);
124
+ }
125
+
126
+ Ok(())
127
+ });
128
+
129
+ result.unwrap_or_else(|| {
130
+ eprintln!("Terminal is None!");
131
+ Ok(())
132
+ })
133
+ }
134
+
135
+ // Storage for the last panic info, to be retrieved and printed after terminal restore.
136
+ thread_local! {
137
+ static LAST_PANIC: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
138
+ }
139
+
140
+ /// Enables Rust backtraces and installs a custom panic hook.
141
+ ///
142
+ /// The panic hook stores the backtrace info instead of printing immediately.
143
+ /// This allows Ruby to retrieve and print it after terminal restoration,
144
+ /// preventing output from being lost on the alternate screen.
145
+ fn enable_rust_backtrace(_ruby: &magnus::Ruby) {
146
+ std::env::set_var("RUST_BACKTRACE", "1");
147
+ std::panic::set_hook(Box::new(|info| {
148
+ let backtrace = std::backtrace::Backtrace::force_capture();
149
+ let message = format!("Rust panic: {info}\n{backtrace}");
150
+ LAST_PANIC.with(|p| {
151
+ *p.borrow_mut() = Some(message);
152
+ });
153
+ }));
154
+ }
155
+
156
+ /// Returns the last panic info (if any) and clears it.
157
+ ///
158
+ /// Call this after terminal restoration to get deferred panic output.
159
+ fn get_last_panic(_ruby: &magnus::Ruby) -> Option<String> {
160
+ LAST_PANIC.with(|p| p.borrow_mut().take())
161
+ }
162
+
163
+ /// Intentionally panics to test backtrace output.
164
+ ///
165
+ /// Only use this for debugging/testing the backtrace feature.
166
+ fn test_panic(_ruby: &magnus::Ruby) {
167
+ panic!("Test panic triggered by RatatuiRuby._test_panic");
168
+ }
169
+
170
+ /// Register the Terminal class with FFI methods.
171
+ fn register_terminal_class(ruby: &Ruby, m: magnus::RModule) -> Result<(), Error> {
172
+ let terminal_class = m.define_class("Terminal", ruby.class_object())?;
173
+ terminal_class.define_singleton_method(
174
+ "_init_test_terminal_instance",
175
+ function!(terminal::init_test_terminal_instance, 4),
176
+ )?;
177
+ terminal_class.define_singleton_method(
178
+ "_get_terminal_size_instance",
179
+ function!(terminal::get_terminal_size_instance, 1),
180
+ )?;
181
+ terminal_class.define_singleton_method(
182
+ "_available_color_count",
183
+ function!(terminal::available_color_count, 0),
184
+ )?;
185
+ terminal_class.define_singleton_method(
186
+ "_supports_keyboard_enhancement",
187
+ function!(terminal::supports_keyboard_enhancement, 0),
188
+ )?;
189
+ terminal_class.define_singleton_method(
190
+ "_terminal_window_size",
191
+ function!(terminal::terminal_window_size, 0),
192
+ )?;
193
+ terminal_class.define_singleton_method(
194
+ "_force_color_output",
195
+ function!(terminal::force_color_output, 1),
196
+ )?;
197
+ Ok(())
198
+ }
199
+
200
+ #[magnus::init]
201
+ fn init() -> Result<(), Error> {
202
+ let ruby = magnus::Ruby::get().unwrap();
203
+ let m = ruby.define_module("RatatuiRuby")?;
204
+
205
+ m.define_module_function("_init_terminal", function!(init_terminal, 4))?;
206
+ m.define_module_function("_restore_terminal", function!(restore_terminal, 0))?;
207
+ m.define_module_function("_draw", function!(draw, -1))?;
208
+ m.define_module_function(
209
+ "_enable_rust_backtrace",
210
+ function!(enable_rust_backtrace, 0),
211
+ )?;
212
+ m.define_module_function("_test_panic", function!(test_panic, 0))?;
213
+ m.define_module_function("_get_last_panic", function!(get_last_panic, 0))?;
214
+
215
+ // Register Frame class
216
+ let frame_class = m.define_class("Frame", ruby.class_object())?;
217
+ frame_class.define_method("area", method!(RubyFrame::area, 0))?;
218
+ frame_class.define_method("render_widget", method!(RubyFrame::render_widget, 2))?;
219
+ frame_class.define_method(
220
+ "render_stateful_widget",
221
+ method!(RubyFrame::render_stateful_widget, 3),
222
+ )?;
223
+ frame_class.define_method(
224
+ "set_cursor_position",
225
+ method!(RubyFrame::set_cursor_position, 2),
226
+ )?;
227
+ m.define_module_function("_poll_event", function!(events::poll_event, 1))?;
228
+ m.define_module_function("inject_test_event", function!(events::inject_test_event, 2))?;
229
+ m.define_module_function("clear_events", function!(events::clear_events, 0))?;
230
+ m.define_module_function("_all_key_codes", function!(events::all_key_codes, 0))?;
231
+
232
+ // Register State classes
233
+ widgets::list_state::register(&ruby, m)?;
234
+ widgets::table_state::register(&ruby, m)?;
235
+ widgets::scrollbar_state::register(&ruby, m)?;
236
+
237
+ // Test backend helpers
238
+ m.define_module_function(
239
+ "_init_test_terminal",
240
+ function!(terminal::init_test_terminal, 4),
241
+ )?;
242
+ m.define_module_function(
243
+ "get_buffer_content",
244
+ function!(terminal::get_buffer_content, 0),
245
+ )?;
246
+ m.define_module_function(
247
+ "get_cursor_position",
248
+ function!(terminal::get_cursor_position, 0),
249
+ )?;
250
+ m.define_module_function(
251
+ "set_cursor_position",
252
+ function!(terminal::set_cursor_position, 2),
253
+ )?;
254
+ m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
255
+ m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
256
+ m.define_module_function(
257
+ "_get_terminal_area",
258
+ function!(terminal::get_terminal_area, 0),
259
+ )?;
260
+ m.define_module_function(
261
+ "_get_terminal_size",
262
+ function!(terminal::get_terminal_size, 0),
263
+ )?;
264
+ m.define_module_function("_insert_before", function!(terminal::insert_before, 2))?;
265
+ m.define_module_function(
266
+ "_get_viewport_type",
267
+ function!(terminal::get_viewport_type, 0),
268
+ )?;
269
+ m.define_module_function("_frame_count", function!(terminal::frame_count, 0))?;
270
+
271
+ // Register Terminal class with instance-specific FFI methods
272
+ register_terminal_class(&ruby, m)?;
273
+
274
+ // Register Layout.split on the Layout::Layout class (inside the Layout module)
275
+ let layout_mod = m.const_get::<_, magnus::RModule>("Layout")?;
276
+ let layout_class = layout_mod.const_get::<_, magnus::RClass>("Layout")?;
277
+ layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
278
+ layout_class.define_singleton_method(
279
+ "_split_with_spacers",
280
+ function!(widgets::layout::split_with_spacers_layout, 4),
281
+ )?;
282
+
283
+ // Paragraph metrics
284
+ m.define_module_function(
285
+ "_paragraph_line_count",
286
+ function!(widgets::paragraph::line_count, 2),
287
+ )?;
288
+ m.define_module_function(
289
+ "_paragraph_line_width",
290
+ function!(widgets::paragraph::line_width, 1),
291
+ )?;
292
+
293
+ // Tabs metrics
294
+ m.define_module_function("_tabs_width", function!(widgets::tabs::width, 1))?;
295
+
296
+ // Text measurement
297
+ m.define_module_function("_text_width", function!(string_width::text_width, 1))?;
298
+
299
+ // Color conversion
300
+ color::register(&ruby, m)?;
301
+
302
+ Ok(())
303
+ }
304
+
305
+ #[cfg(test)]
306
+ mod tests {
307
+ use ratatui::layout::Rect;
308
+ use ratatui::style::Color;
309
+ use ratatui::widgets::Widget;
310
+ use ratatui::widgets::{Chart, Dataset, Sparkline};
311
+
312
+ #[test]
313
+ fn test_parse_color() {
314
+ // We can test this through the style module directly now
315
+ use crate::style::parse_color;
316
+ assert_eq!(parse_color("red"), Some(Color::Red));
317
+ assert_eq!(parse_color("blue"), Some(Color::Blue));
318
+ assert_eq!(parse_color("#ffffff"), Some(Color::Rgb(255, 255, 255)));
319
+ assert_eq!(parse_color("#000000"), Some(Color::Rgb(0, 0, 0)));
320
+ assert_eq!(parse_color("invalid"), None);
321
+ }
322
+
323
+ #[test]
324
+ fn test_sparkline_render() {
325
+ let mut buf = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 10, 1));
326
+ let data = vec![1, 2, 3];
327
+ let sparkline = Sparkline::default().data(&data);
328
+ sparkline.render(Rect::new(0, 0, 10, 1), &mut buf);
329
+ assert!(buf.content().iter().any(|c| c.symbol() != " "));
330
+ }
331
+
332
+ #[test]
333
+ fn test_line_chart_render() {
334
+ let mut buf = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 20, 10));
335
+ let data = vec![(0.0, 0.0), (1.0, 1.0)];
336
+ let datasets = vec![Dataset::default().data(&data)];
337
+ let chart = Chart::new(datasets)
338
+ .x_axis(ratatui::widgets::Axis::default().bounds([0.0, 1.0]))
339
+ .y_axis(ratatui::widgets::Axis::default().bounds([0.0, 1.0]));
340
+ chart.render(Rect::new(0, 0, 20, 10), &mut buf);
341
+ assert!(buf.content().iter().any(|c| c.symbol() != " "));
342
+ }
343
+ }
@@ -0,0 +1,11 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ // Require SAFETY comments on all unsafe blocks
5
+ #![warn(clippy::undocumented_unsafe_blocks)]
6
+ // Enable pedantic lints for stricter code quality
7
+ #![warn(clippy::pedantic)]
8
+ // Allow certain pedantic lints that are too noisy for FFI code
9
+ #![allow(clippy::missing_errors_doc)]
10
+ #![allow(clippy::missing_panics_doc)]
11
+ #![allow(clippy::non_std_lazy_statics)]