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,233 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal initialization and restoration functions.
5
+
6
+ use magnus::value::ReprValue;
7
+ use magnus::{Error, Module};
8
+ use ratatui::{
9
+ backend::{CrosstermBackend, TestBackend},
10
+ Terminal, TerminalOptions, Viewport,
11
+ };
12
+ use std::collections::HashMap;
13
+ use std::io;
14
+ use std::sync::atomic::{AtomicU64, Ordering};
15
+
16
+ use super::TerminalWrapper;
17
+
18
+ // Track whether we're using fullscreen viewport (for restore_terminal)
19
+ thread_local! {
20
+ static IS_FULLSCREEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
21
+ }
22
+
23
+ // Instance-based terminal tracking (Proposal 1 from terminal.md)
24
+ thread_local! {
25
+ static TERMINAL_INSTANCES: std::cell::RefCell<HashMap<u64, TerminalWrapper>> =
26
+ std::cell::RefCell::new(HashMap::new());
27
+ }
28
+ static NEXT_TERMINAL_ID: AtomicU64 = AtomicU64::new(1);
29
+
30
+ #[allow(clippy::needless_pass_by_value)] // Magnus FFI requires owned String, not &str
31
+ pub fn init_terminal(
32
+ focus_events: bool,
33
+ bracketed_paste: bool,
34
+ viewport_type: String,
35
+ viewport_height: Option<u16>,
36
+ ) -> Result<(), Error> {
37
+ let ruby = magnus::Ruby::get().unwrap();
38
+
39
+ // Check if already initialized
40
+ if super::is_initialized() {
41
+ return Ok(());
42
+ }
43
+
44
+ let module = ruby.define_module("RatatuiRuby")?;
45
+ let error_base = module.const_get::<_, magnus::RClass>("Error")?;
46
+ let error_class = error_base.const_get("Terminal")?;
47
+
48
+ // Parse viewport type
49
+ let viewport = match viewport_type.as_ref() {
50
+ "inline" => {
51
+ let height = viewport_height.unwrap_or(8);
52
+ Viewport::Inline(height)
53
+ }
54
+ _ => Viewport::Fullscreen,
55
+ };
56
+
57
+ ratatui::crossterm::terminal::enable_raw_mode()
58
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
59
+ let mut stdout = io::stdout();
60
+
61
+ // Only enter alternate screen for fullscreen viewports
62
+ if matches!(viewport, Viewport::Fullscreen) {
63
+ ratatui::crossterm::execute!(stdout, ratatui::crossterm::terminal::EnterAlternateScreen)
64
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
65
+ }
66
+
67
+ ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableMouseCapture)
68
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
69
+
70
+ if focus_events {
71
+ ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableFocusChange)
72
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
73
+ }
74
+ if bracketed_paste {
75
+ ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableBracketedPaste)
76
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
77
+ }
78
+
79
+ let backend = CrosstermBackend::new(stdout);
80
+
81
+ // Store whether we're using fullscreen for restore_terminal (before moving viewport)
82
+ let is_fullscreen = matches!(viewport, Viewport::Fullscreen);
83
+ IS_FULLSCREEN.with(|f| f.set(is_fullscreen));
84
+
85
+ let options = TerminalOptions { viewport };
86
+ let terminal = Terminal::with_options(backend, options)
87
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
88
+
89
+ super::set_terminal(TerminalWrapper::Crossterm(terminal));
90
+ Ok(())
91
+ }
92
+
93
+ #[allow(clippy::needless_pass_by_value)] // Magnus FFI requires owned String, not &str
94
+ pub fn init_test_terminal(
95
+ width: u16,
96
+ height: u16,
97
+ viewport_type: String,
98
+ viewport_height: Option<u16>,
99
+ ) -> Result<(), Error> {
100
+ let ruby = magnus::Ruby::get().unwrap();
101
+ let backend = TestBackend::new(width, height);
102
+ let module = ruby.define_module("RatatuiRuby")?;
103
+ let error_base = module.const_get::<_, magnus::RClass>("Error")?;
104
+ let error_class = error_base.const_get("Terminal")?;
105
+
106
+ // Parse viewport type (same as init_terminal)
107
+ let viewport = match viewport_type.as_ref() {
108
+ "inline" => {
109
+ let vp_height = viewport_height.unwrap_or(height);
110
+ Viewport::Inline(vp_height)
111
+ }
112
+ _ => Viewport::Fullscreen,
113
+ };
114
+
115
+ let options = TerminalOptions { viewport };
116
+ let terminal = Terminal::with_options(backend, options)
117
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
118
+
119
+ super::set_terminal(TerminalWrapper::Test(terminal));
120
+ Ok(())
121
+ }
122
+
123
+ // Instance-based terminal initialization (Proposal 1 from terminal.md)
124
+ // Returns terminal ID for Ruby to store
125
+ #[allow(clippy::needless_pass_by_value)]
126
+ pub fn init_test_terminal_instance(
127
+ width: u16,
128
+ height: u16,
129
+ viewport_type: String,
130
+ viewport_height: Option<u16>,
131
+ ) -> Result<u64, Error> {
132
+ let ruby = magnus::Ruby::get().unwrap();
133
+ let backend = TestBackend::new(width, height);
134
+ let module = ruby.define_module("RatatuiRuby")?;
135
+ let error_base = module.const_get::<_, magnus::RClass>("Error")?;
136
+ let error_class = error_base.const_get("Terminal")?;
137
+
138
+ // Parse viewport type
139
+ let viewport = match viewport_type.as_ref() {
140
+ "inline" => {
141
+ let vp_height = viewport_height.unwrap_or(height);
142
+ Viewport::Inline(vp_height)
143
+ }
144
+ _ => Viewport::Fullscreen,
145
+ };
146
+
147
+ let options = TerminalOptions { viewport };
148
+ let terminal = Terminal::with_options(backend, options)
149
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
150
+
151
+ // Generate unique ID and store instance
152
+ let id = NEXT_TERMINAL_ID.fetch_add(1, Ordering::SeqCst);
153
+ TERMINAL_INSTANCES.with(|instances| {
154
+ instances
155
+ .borrow_mut()
156
+ .insert(id, TerminalWrapper::Test(terminal));
157
+ });
158
+
159
+ Ok(id)
160
+ }
161
+
162
+ pub fn restore_terminal() {
163
+ if let Some(wrapper) = super::take_terminal() {
164
+ match wrapper {
165
+ TerminalWrapper::Crossterm(mut t) => {
166
+ let _ = ratatui::crossterm::terminal::disable_raw_mode();
167
+
168
+ // Only leave alternate screen if we were in fullscreen mode
169
+ let is_fullscreen = IS_FULLSCREEN.with(std::cell::Cell::get);
170
+ if is_fullscreen {
171
+ let _ = ratatui::crossterm::execute!(
172
+ t.backend_mut(),
173
+ ratatui::crossterm::terminal::LeaveAlternateScreen,
174
+ ratatui::crossterm::event::DisableMouseCapture,
175
+ ratatui::crossterm::event::DisableFocusChange,
176
+ ratatui::crossterm::event::DisableBracketedPaste
177
+ );
178
+ } else {
179
+ let _ = ratatui::crossterm::execute!(
180
+ t.backend_mut(),
181
+ ratatui::crossterm::event::DisableMouseCapture,
182
+ ratatui::crossterm::event::DisableFocusChange,
183
+ ratatui::crossterm::event::DisableBracketedPaste
184
+ );
185
+ }
186
+ }
187
+ TerminalWrapper::Test(_) => {}
188
+ }
189
+ }
190
+ }
191
+
192
+ /// Get terminal size for a specific instance.
193
+ /// Returns `Layout::Rect` object (not hash!)
194
+ pub fn get_terminal_size_instance(terminal_id: u64) -> Result<magnus::Value, Error> {
195
+ let ruby = magnus::Ruby::get().unwrap();
196
+
197
+ TERMINAL_INSTANCES.with(|instances| {
198
+ let instances = instances.borrow();
199
+ if let Some(wrapper) = instances.get(&terminal_id) {
200
+ let size = match wrapper {
201
+ TerminalWrapper::Crossterm(term) => term.size().map_err(|e| {
202
+ let module = ruby.define_module("RatatuiRuby").unwrap();
203
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
204
+ let error_class = error_base.const_get("Terminal").unwrap();
205
+ Error::new(error_class, e.to_string())
206
+ })?,
207
+ TerminalWrapper::Test(term) => term.size().unwrap_or_default(),
208
+ };
209
+
210
+ // Construct Layout::Rect object in Rust (NOT a hash!)
211
+ let module = ruby.define_module("RatatuiRuby")?;
212
+ let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
213
+ let rect_class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
214
+
215
+ // Create hash with keyword args for Rect.new
216
+ let args = ruby.hash_new();
217
+ args.aset(ruby.to_symbol("x"), 0)?;
218
+ args.aset(ruby.to_symbol("y"), 0)?;
219
+ args.aset(ruby.to_symbol("width"), size.width)?;
220
+ args.aset(ruby.to_symbol("height"), size.height)?;
221
+
222
+ rect_class.funcall_public("new", (0, 0, size.width, size.height))
223
+ } else {
224
+ let module = ruby.define_module("RatatuiRuby")?;
225
+ let error_base = module.const_get::<_, magnus::RClass>("Error")?;
226
+ let error_class = error_base.const_get("Terminal")?;
227
+ Err(Error::new(
228
+ error_class,
229
+ format!("Terminal instance {terminal_id} not found"),
230
+ ))
231
+ }
232
+ })
233
+ }
@@ -0,0 +1,42 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal management module.
5
+ //!
6
+ //! Provides thread-local terminal storage with safe accessors. Queries
7
+ //! during `draw()` callbacks return data from a pre-captured snapshot,
8
+ //! avoiding reentrancy issues with the terminal lock.
9
+
10
+ mod capabilities;
11
+ mod init;
12
+ mod mutations;
13
+ mod queries;
14
+ mod query;
15
+ mod storage;
16
+ mod wrapper;
17
+
18
+ pub use storage::{
19
+ is_in_draw_mode, is_initialized, lend_for_draw, set_terminal, take_terminal, with_query,
20
+ with_terminal_mut,
21
+ };
22
+ pub use wrapper::TerminalWrapper;
23
+
24
+ // Init/restore functions
25
+ pub use init::{
26
+ get_terminal_size_instance, init_terminal, init_test_terminal, init_test_terminal_instance,
27
+ restore_terminal,
28
+ };
29
+
30
+ // Query functions
31
+ pub use queries::{
32
+ frame_count, get_buffer_content, get_cell_at, get_cursor_position, get_terminal_area,
33
+ get_terminal_size, get_viewport_type,
34
+ };
35
+
36
+ // Mutation functions
37
+ pub use mutations::{insert_before, resize_terminal, set_cursor_position};
38
+
39
+ // Capability detection
40
+ pub use capabilities::{
41
+ available_color_count, force_color_output, supports_keyboard_enhancement, terminal_window_size,
42
+ };
@@ -0,0 +1,158 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal mutation functions (write operations).
5
+
6
+ use magnus::{Error, Module};
7
+
8
+ use super::TerminalWrapper;
9
+
10
+ pub fn insert_before(height: u16, widget: magnus::Value) -> Result<(), Error> {
11
+ let ruby = magnus::Ruby::get().unwrap();
12
+
13
+ // with_terminal_mut returns None during draw mode, so we handle that case
14
+ crate::terminal::with_terminal_mut(|wrapper| {
15
+ let module = ruby.define_module("RatatuiRuby").unwrap();
16
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
17
+ let error_class = error_base.const_get("Terminal").unwrap();
18
+
19
+ match wrapper {
20
+ TerminalWrapper::Crossterm(term) => {
21
+ // Capture rendering error since closure can't return Result
22
+ let mut render_error: Option<String> = None;
23
+
24
+ let result = term.insert_before(height, |buf| {
25
+ let area = buf.area();
26
+ let area_copy = *area; // Copy rect before closure capture
27
+
28
+ // Render widget to buffer using centralized dispatch
29
+ let render_result =
30
+ crate::rendering::render_widget_to_buffer(buf, area_copy, widget);
31
+
32
+ if let Err(e) = render_result {
33
+ render_error = Some(e.to_string());
34
+ }
35
+ });
36
+
37
+ // Handle insert_before error
38
+ result.map_err(|e| Error::new(error_class, e.to_string()))?;
39
+
40
+ // Handle rendering error
41
+ if let Some(err_msg) = render_error {
42
+ return Err(Error::new(error_class, err_msg));
43
+ }
44
+ }
45
+ TerminalWrapper::Test(term) => {
46
+ // Capture rendering error since closure can't return Result
47
+ let mut render_error: Option<String> = None;
48
+
49
+ let result = term.insert_before(height, |buf| {
50
+ let area = buf.area();
51
+ let area_copy = *area; // Copy rect before closure capture
52
+
53
+ // Render widget to buffer using centralized dispatch
54
+ let render_result =
55
+ crate::rendering::render_widget_to_buffer(buf, area_copy, widget);
56
+
57
+ if let Err(e) = render_result {
58
+ render_error = Some(e.to_string());
59
+ }
60
+ });
61
+
62
+ // Handle insert_before error
63
+ result.map_err(|e| Error::new(error_class, e.to_string()))?;
64
+
65
+ // Handle rendering error
66
+ if let Some(err_msg) = render_error {
67
+ return Err(Error::new(error_class, err_msg));
68
+ }
69
+ }
70
+ }
71
+ Ok(())
72
+ })
73
+ .unwrap_or_else(|| {
74
+ // During draw or terminal not initialized
75
+ let module = ruby.define_module("RatatuiRuby").unwrap();
76
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
77
+ if crate::terminal::is_in_draw_mode() {
78
+ let error_class = error_base.const_get("Invariant").unwrap();
79
+ Err(Error::new(
80
+ error_class,
81
+ "insert_before cannot be called during draw",
82
+ ))
83
+ } else {
84
+ let error_class = error_base.const_get("Terminal").unwrap();
85
+ Err(Error::new(error_class, "Terminal not initialized"))
86
+ }
87
+ })
88
+ }
89
+
90
+ pub fn set_cursor_position(x: u16, y: u16) -> Result<(), Error> {
91
+ let ruby = magnus::Ruby::get().unwrap();
92
+
93
+ crate::terminal::with_terminal_mut(|wrapper| {
94
+ let module = ruby.define_module("RatatuiRuby").unwrap();
95
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
96
+ let error_class = error_base.const_get("Terminal").unwrap();
97
+
98
+ match wrapper {
99
+ TerminalWrapper::Crossterm(term) => {
100
+ term.set_cursor_position((x, y))
101
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
102
+ }
103
+ TerminalWrapper::Test(term) => {
104
+ term.set_cursor_position((x, y))
105
+ .map_err(|e| Error::new(error_class, e.to_string()))?;
106
+ }
107
+ }
108
+ Ok(())
109
+ })
110
+ .unwrap_or_else(|| {
111
+ let module = ruby.define_module("RatatuiRuby").unwrap();
112
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
113
+ if crate::terminal::is_in_draw_mode() {
114
+ let error_class = error_base.const_get("Invariant").unwrap();
115
+ Err(Error::new(
116
+ error_class,
117
+ "set_cursor_position cannot be called during draw",
118
+ ))
119
+ } else {
120
+ let error_class = error_base.const_get("Terminal").unwrap();
121
+ Err(Error::new(error_class, "Terminal is not initialized"))
122
+ }
123
+ })
124
+ }
125
+
126
+ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
127
+ let ruby = magnus::Ruby::get().unwrap();
128
+
129
+ crate::terminal::with_terminal_mut(|wrapper| {
130
+ match wrapper {
131
+ TerminalWrapper::Crossterm(_) => {}
132
+ TerminalWrapper::Test(terminal) => {
133
+ terminal.backend_mut().resize(width, height);
134
+ if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
135
+ let module = ruby.define_module("RatatuiRuby").unwrap();
136
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
137
+ let error_class = error_base.const_get("Terminal").unwrap();
138
+ return Err(Error::new(error_class, e.to_string()));
139
+ }
140
+ }
141
+ }
142
+ Ok(())
143
+ })
144
+ .unwrap_or_else(|| {
145
+ let module = ruby.define_module("RatatuiRuby").unwrap();
146
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
147
+ if crate::terminal::is_in_draw_mode() {
148
+ let error_class = error_base.const_get("Invariant").unwrap();
149
+ Err(Error::new(
150
+ error_class,
151
+ "resize_terminal cannot be called during draw",
152
+ ))
153
+ } else {
154
+ // No terminal initialized is OK for resize - just no-op
155
+ Ok(())
156
+ }
157
+ })
158
+ }
@@ -0,0 +1,231 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal query functions (read-only access to terminal state).
5
+
6
+ use magnus::value::ReprValue;
7
+ use magnus::{Error, Module, Value};
8
+
9
+ use super::TerminalWrapper;
10
+
11
+ pub fn get_buffer_content() -> Result<String, Error> {
12
+ let ruby = magnus::Ruby::get().unwrap();
13
+
14
+ // This function builds a string from all cells - we need direct terminal access
15
+ crate::terminal::with_terminal_mut(|wrapper| {
16
+ if let TerminalWrapper::Test(terminal) = wrapper {
17
+ let buffer = terminal.backend().buffer();
18
+ let area = buffer.area;
19
+ let mut result = String::new();
20
+ for y in 0..area.height {
21
+ for x in 0..area.width {
22
+ let cell = buffer.cell((x, y)).unwrap();
23
+ result.push_str(cell.symbol());
24
+ }
25
+ result.push('\n');
26
+ }
27
+ Ok(result)
28
+ } else {
29
+ let module = ruby.define_module("RatatuiRuby").unwrap();
30
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
31
+ let error_class = error_base.const_get("Terminal").unwrap();
32
+ Err(Error::new(
33
+ error_class,
34
+ "Terminal is not initialized as TestBackend",
35
+ ))
36
+ }
37
+ })
38
+ .unwrap_or_else(|| {
39
+ let module = ruby.define_module("RatatuiRuby").unwrap();
40
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
41
+ let error_class = error_base.const_get("Terminal").unwrap();
42
+ Err(Error::new(error_class, "Terminal is not initialized"))
43
+ })
44
+ }
45
+
46
+ pub fn get_terminal_area() -> Result<magnus::RHash, Error> {
47
+ let ruby = magnus::Ruby::get().unwrap();
48
+
49
+ crate::terminal::with_query(|q| {
50
+ let area = q.viewport_area();
51
+ let hash = ruby.hash_new();
52
+ hash.aset("x", area.x).unwrap();
53
+ hash.aset("y", area.y).unwrap();
54
+ hash.aset("width", area.width).unwrap();
55
+ hash.aset("height", area.height).unwrap();
56
+ Ok(hash)
57
+ })
58
+ .unwrap_or_else(|| {
59
+ let module = ruby.define_module("RatatuiRuby").unwrap();
60
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
61
+ let error_class = error_base.const_get("Terminal").unwrap();
62
+ Err(Error::new(error_class, "Terminal is not initialized"))
63
+ })
64
+ }
65
+
66
+ /// Returns the full terminal backend size (not the viewport)
67
+ pub fn get_terminal_size() -> Result<magnus::RHash, Error> {
68
+ let ruby = magnus::Ruby::get().unwrap();
69
+
70
+ crate::terminal::with_query(|q| {
71
+ let size = q.size();
72
+ let hash = ruby.hash_new();
73
+ hash.aset("x", 0).unwrap();
74
+ hash.aset("y", 0).unwrap();
75
+ hash.aset("width", size.width).unwrap();
76
+ hash.aset("height", size.height).unwrap();
77
+ Ok(hash)
78
+ })
79
+ .unwrap_or_else(|| {
80
+ let module = ruby.define_module("RatatuiRuby").unwrap();
81
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
82
+ let error_class = error_base.const_get("Terminal").unwrap();
83
+ Err(Error::new(error_class, "Terminal is not initialized"))
84
+ })
85
+ }
86
+
87
+ pub fn get_viewport_type() -> Result<String, Error> {
88
+ let ruby = magnus::Ruby::get().unwrap();
89
+
90
+ crate::terminal::with_query(|q| {
91
+ let viewport = q.viewport_area();
92
+ let size = q.size();
93
+ if viewport.height < size.height {
94
+ Ok("inline".to_string())
95
+ } else {
96
+ Ok("fullscreen".to_string())
97
+ }
98
+ })
99
+ .unwrap_or_else(|| {
100
+ let module = ruby.define_module("RatatuiRuby").unwrap();
101
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
102
+ let error_class = error_base.const_get("Terminal").unwrap();
103
+ Err(Error::new(error_class, "Terminal not initialized"))
104
+ })
105
+ }
106
+
107
+ pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
108
+ let ruby = magnus::Ruby::get().unwrap();
109
+
110
+ crate::terminal::with_query(|q| Ok(q.cursor_position())).unwrap_or_else(|| {
111
+ let module = ruby.define_module("RatatuiRuby").unwrap();
112
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
113
+ let error_class = error_base.const_get("Terminal").unwrap();
114
+ Err(Error::new(
115
+ error_class,
116
+ "Terminal is not initialized as TestBackend",
117
+ ))
118
+ })
119
+ }
120
+
121
+ pub fn get_cell_at(x: u16, y: u16) -> Result<magnus::RHash, Error> {
122
+ let ruby = magnus::Ruby::get().unwrap();
123
+
124
+ crate::terminal::with_query(|q| {
125
+ if let Some(cell) = q.cell_at(x, y) {
126
+ let hash = ruby.hash_new();
127
+ hash.aset("char", cell.symbol()).unwrap();
128
+ hash.aset("fg", color_to_value(cell.fg)).unwrap();
129
+ hash.aset("bg", color_to_value(cell.bg)).unwrap();
130
+ hash.aset("underline_color", color_to_value(cell.underline_color))
131
+ .unwrap();
132
+ hash.aset("modifiers", modifiers_to_value(cell.modifier))
133
+ .unwrap();
134
+ Ok(hash)
135
+ } else {
136
+ let module = ruby.define_module("RatatuiRuby").unwrap();
137
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
138
+ let error_class = error_base.const_get("Terminal").unwrap();
139
+ Err(Error::new(
140
+ error_class,
141
+ format!("Coordinates ({x}, {y}) out of bounds"),
142
+ ))
143
+ }
144
+ })
145
+ .unwrap_or_else(|| {
146
+ let module = ruby.define_module("RatatuiRuby").unwrap();
147
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
148
+ let error_class = error_base.const_get("Terminal").unwrap();
149
+ Err(Error::new(
150
+ error_class,
151
+ "Terminal is not initialized as TestBackend",
152
+ ))
153
+ })
154
+ }
155
+
156
+ fn color_to_value(color: ratatui::style::Color) -> Value {
157
+ let ruby = magnus::Ruby::get().unwrap();
158
+ match color {
159
+ ratatui::style::Color::Reset => ruby.qnil().as_value(),
160
+ ratatui::style::Color::Black => ruby.to_symbol("black").as_value(),
161
+ ratatui::style::Color::Red => ruby.to_symbol("red").as_value(),
162
+ ratatui::style::Color::Green => ruby.to_symbol("green").as_value(),
163
+ ratatui::style::Color::Yellow => ruby.to_symbol("yellow").as_value(),
164
+ ratatui::style::Color::Blue => ruby.to_symbol("blue").as_value(),
165
+ ratatui::style::Color::Magenta => ruby.to_symbol("magenta").as_value(),
166
+ ratatui::style::Color::Cyan => ruby.to_symbol("cyan").as_value(),
167
+ ratatui::style::Color::Gray => ruby.to_symbol("gray").as_value(),
168
+ ratatui::style::Color::DarkGray => ruby.to_symbol("dark_gray").as_value(),
169
+ ratatui::style::Color::LightRed => ruby.to_symbol("light_red").as_value(),
170
+ ratatui::style::Color::LightGreen => ruby.to_symbol("light_green").as_value(),
171
+ ratatui::style::Color::LightYellow => ruby.to_symbol("light_yellow").as_value(),
172
+ ratatui::style::Color::LightBlue => ruby.to_symbol("light_blue").as_value(),
173
+ ratatui::style::Color::LightMagenta => ruby.to_symbol("light_magenta").as_value(),
174
+ ratatui::style::Color::LightCyan => ruby.to_symbol("light_cyan").as_value(),
175
+ ratatui::style::Color::White => ruby.to_symbol("white").as_value(),
176
+ ratatui::style::Color::Rgb(r, g, b) => ruby
177
+ .str_new(&(format!("#{r:02x}{g:02x}{b:02x}")))
178
+ .as_value(),
179
+ ratatui::style::Color::Indexed(i) => ruby.to_symbol(format!("indexed_{i}")).as_value(),
180
+ }
181
+ }
182
+
183
+ fn modifiers_to_value(modifier: ratatui::style::Modifier) -> Value {
184
+ let ruby = magnus::Ruby::get().unwrap();
185
+ let ary = ruby.ary_new();
186
+
187
+ if modifier.contains(ratatui::style::Modifier::BOLD) {
188
+ let _ = ary.push(ruby.to_symbol("bold"));
189
+ }
190
+ if modifier.contains(ratatui::style::Modifier::ITALIC) {
191
+ let _ = ary.push(ruby.to_symbol("italic"));
192
+ }
193
+ if modifier.contains(ratatui::style::Modifier::DIM) {
194
+ let _ = ary.push(ruby.to_symbol("dim"));
195
+ }
196
+ if modifier.contains(ratatui::style::Modifier::UNDERLINED) {
197
+ let _ = ary.push(ruby.to_symbol("underlined"));
198
+ }
199
+ if modifier.contains(ratatui::style::Modifier::REVERSED) {
200
+ let _ = ary.push(ruby.to_symbol("reversed"));
201
+ }
202
+ if modifier.contains(ratatui::style::Modifier::HIDDEN) {
203
+ let _ = ary.push(ruby.to_symbol("hidden"));
204
+ }
205
+ if modifier.contains(ratatui::style::Modifier::CROSSED_OUT) {
206
+ let _ = ary.push(ruby.to_symbol("crossed_out"));
207
+ }
208
+ if modifier.contains(ratatui::style::Modifier::SLOW_BLINK) {
209
+ let _ = ary.push(ruby.to_symbol("slow_blink"));
210
+ }
211
+ if modifier.contains(ratatui::style::Modifier::RAPID_BLINK) {
212
+ let _ = ary.push(ruby.to_symbol("rapid_blink"));
213
+ }
214
+
215
+ ary.as_value()
216
+ }
217
+
218
+ /// Returns the number of frames that have been drawn
219
+ pub fn frame_count() -> Result<usize, Error> {
220
+ let ruby = magnus::Ruby::get().unwrap();
221
+
222
+ crate::terminal::with_query(|q| Ok(q.frame_count())).unwrap_or_else(|| {
223
+ let module = ruby.define_module("RatatuiRuby").unwrap();
224
+ let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
225
+ let error_class = error_base.const_get("Invariant").unwrap();
226
+ Err(Error::new(
227
+ error_class,
228
+ "Cannot query frame_count: terminal not initialized. Use RatatuiRuby.run or call init_terminal first.",
229
+ ))
230
+ })
231
+ }