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,194 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::errors::type_error_with_context;
5
+ use crate::style::parse_block;
6
+ use crate::text::{parse_line, parse_span};
7
+ use bumpalo::Bump;
8
+ use magnus::{prelude::*, Error, Value};
9
+ use ratatui::buffer::Buffer;
10
+ use ratatui::{layout::Rect, text::Line, widgets::Tabs, widgets::Widget};
11
+
12
+ pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
13
+ let bump = Bump::new();
14
+ let tabs = create_tabs(node, &bump)?;
15
+ tabs.render(area, buffer);
16
+ Ok(())
17
+ }
18
+
19
+ /// Parses padding value with duck-typing support:
20
+ /// - Integer: generates that many spaces
21
+ /// - Line: uses styled Line directly
22
+ /// - Span: wraps in Line
23
+ /// - String or `to_s` responder: converts to Line
24
+ fn parse_padding(val: Value) -> Result<Line<'static>, Error> {
25
+ // Handle nil or zero
26
+ if val.is_nil() {
27
+ return Ok(Line::from(""));
28
+ }
29
+
30
+ // Try as Integer first (most common case)
31
+ if let Ok(n) = usize::try_convert(val) {
32
+ if n == 0 {
33
+ return Ok(Line::from(""));
34
+ }
35
+ return Ok(Line::from(" ".repeat(n)));
36
+ }
37
+
38
+ // Try to parse as Line
39
+ if let Ok(line) = parse_line(val) {
40
+ return Ok(line);
41
+ }
42
+
43
+ // Try to parse as Span (wrap in Line)
44
+ if let Ok(span) = parse_span(val) {
45
+ return Ok(Line::from(vec![span]));
46
+ }
47
+
48
+ // Fallback: call to_s and convert to Line
49
+ let s: String = val.funcall("to_s", ())?;
50
+ Ok(Line::from(s))
51
+ }
52
+
53
+ fn create_tabs(node: Value, bump: &Bump) -> Result<Tabs<'_>, Error> {
54
+ let ruby = magnus::Ruby::get().unwrap();
55
+ let titles_val: Value = node.funcall("titles", ())?;
56
+ let selected_index: usize = node.funcall("selected_index", ())?;
57
+ let block_val: Value = node.funcall("block", ())?;
58
+ let divider_val: Value = node.funcall("divider", ())?;
59
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
60
+ let padding_left_val: Value = node.funcall("padding_left", ())?;
61
+ let padding_right_val: Value = node.funcall("padding_right", ())?;
62
+
63
+ let titles_array = magnus::RArray::from_value(titles_val)
64
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for titles", titles_val))?;
65
+
66
+ let mut titles = Vec::new();
67
+ for i in 0..titles_array.len() {
68
+ let index = isize::try_from(i)
69
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
70
+ let val: Value = titles_array.entry(index)?;
71
+ if let Ok(line) = parse_line(val) {
72
+ titles.push(line);
73
+ } else {
74
+ let s: String = String::try_convert(val)?;
75
+ titles.push(Line::from(s));
76
+ }
77
+ }
78
+
79
+ let mut tabs = Tabs::new(titles).select(selected_index);
80
+
81
+ if !divider_val.is_nil() {
82
+ if let Ok(span) = parse_span(divider_val) {
83
+ tabs = tabs.divider(span);
84
+ } else {
85
+ let divider: String = divider_val.funcall("to_s", ())?;
86
+ tabs = tabs.divider(divider);
87
+ }
88
+ }
89
+
90
+ if !highlight_style_val.is_nil() {
91
+ let style = crate::style::parse_style(highlight_style_val)?;
92
+ tabs = tabs.highlight_style(style);
93
+ }
94
+
95
+ let style_val: Value = node.funcall("style", ())?;
96
+ if !style_val.is_nil() {
97
+ tabs = tabs.style(crate::style::parse_style(style_val)?);
98
+ }
99
+
100
+ if !block_val.is_nil() {
101
+ tabs = tabs.block(parse_block(block_val, bump)?);
102
+ }
103
+
104
+ // Handle duck-typed padding: Integer (spaces), String, Line, or anything with to_s
105
+ let left_padding = parse_padding(padding_left_val)?;
106
+ let right_padding = parse_padding(padding_right_val)?;
107
+ if !left_padding.spans.is_empty() || !right_padding.spans.is_empty() {
108
+ tabs = tabs.padding(left_padding, right_padding);
109
+ }
110
+
111
+ Ok(tabs)
112
+ }
113
+
114
+ pub fn width(node: Value) -> Result<usize, Error> {
115
+ let ruby = magnus::Ruby::get().unwrap();
116
+ let titles_val: Value = node.funcall("titles", ())?;
117
+ let divider_val: Value = node.funcall("divider", ())?;
118
+ let padding_left: usize = node.funcall("padding_left", ())?;
119
+ let padding_right: usize = node.funcall("padding_right", ())?;
120
+
121
+ let titles_array = magnus::RArray::from_value(titles_val)
122
+ .ok_or_else(|| type_error_with_context(&ruby, "expected array for titles", titles_val))?;
123
+
124
+ let mut total_width = padding_left + padding_right;
125
+
126
+ let mut titles_count = 0;
127
+ for i in 0..titles_array.len() {
128
+ let index = isize::try_from(i)
129
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
130
+ let val: Value = titles_array.entry(index)?;
131
+ let line_width = if let Ok(line) = parse_line(val) {
132
+ line.width()
133
+ } else {
134
+ let s: String = String::try_convert(val)?;
135
+ ratatui::text::Span::raw(s).width()
136
+ };
137
+ total_width += line_width;
138
+ titles_count += 1;
139
+ }
140
+
141
+ if titles_count > 1 {
142
+ let divider_width = if divider_val.is_nil() {
143
+ 1 // Default divider is "|"
144
+ } else if let Ok(span) = parse_span(divider_val) {
145
+ span.width()
146
+ } else {
147
+ let d: String = divider_val.funcall("to_s", ())?;
148
+ ratatui::text::Span::raw(d).width()
149
+ };
150
+ total_width += (titles_count - 1) * divider_width;
151
+ }
152
+
153
+ Ok(total_width)
154
+ }
155
+
156
+ #[cfg(test)]
157
+ mod tests {
158
+ use super::*;
159
+ use ratatui::buffer::Buffer;
160
+ use ratatui::style::{Color, Modifier, Style};
161
+ use ratatui::text::Line;
162
+ use ratatui::widgets::{Tabs, Widget};
163
+
164
+ #[test]
165
+ fn test_tabs_rendering() {
166
+ let titles = vec![Line::from("Tab1"), Line::from("Tab2")];
167
+ let tabs = Tabs::new(titles).select(1).divider("|");
168
+ let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
169
+ tabs.render(Rect::new(0, 0, 15, 1), &mut buf);
170
+ // Should contain tab titles
171
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
172
+ assert!(content.contains("Tab1"));
173
+ assert!(content.contains("Tab2"));
174
+ assert!(content.contains('|'));
175
+ }
176
+
177
+ #[test]
178
+ fn test_tabs_highlight_style() {
179
+ let titles = vec![Line::from("Tab1"), Line::from("Tab2")];
180
+ let highlight_style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
181
+ let tabs = Tabs::new(titles).select(0).highlight_style(highlight_style);
182
+
183
+ let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
184
+ tabs.render(Rect::new(0, 0, 15, 1), &mut buf);
185
+
186
+ // Check the first cell of the first tab (which is selected)
187
+ // " Tab1 "
188
+ // Index 1 should be 'T' with Red+Bold
189
+ let cell = &buf.content()[1];
190
+ assert_eq!(cell.symbol(), "T");
191
+ assert_eq!(cell.fg, Color::Red);
192
+ assert!(cell.modifier.contains(Modifier::BOLD));
193
+ }
194
+ }
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ module Backend
10
+ # Terminal window dimensions in characters and pixels.
11
+ #
12
+ # Some operations need both character grid size and pixel dimensions.
13
+ # Sixel graphics, image rendering, and precise layout calculations all
14
+ # benefit from knowing both measurements at once.
15
+ #
16
+ # This struct bundles both sizes together. It matches upstream Ratatui's
17
+ # <tt>backend::WindowSize</tt> struct exactly.
18
+ #
19
+ # Both fields are <tt>Layout::Size</tt> instances. This reuses the same
20
+ # type for character and pixel dimensions, matching upstream design.
21
+ #
22
+ # Note: Pixel dimensions may be zero on some systems. Unix marks these
23
+ # fields "unused" in TIOCGWINSZ. Windows does not implement them.
24
+ #
25
+ # === Example
26
+ #
27
+ #--
28
+ # SPDX-SnippetBegin
29
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
30
+ # SPDX-License-Identifier: MIT-0
31
+ #++
32
+ # ws = RatatuiRuby::Terminal.window_size
33
+ # if ws
34
+ # puts "#{ws.columns_rows.width}x#{ws.columns_rows.height} chars"
35
+ # puts "#{ws.pixels.width}x#{ws.pixels.height} pixels"
36
+ # end
37
+ #--
38
+ # SPDX-SnippetEnd
39
+ #++
40
+ class WindowSize < Data.define(:columns_rows, :pixels)
41
+ ##
42
+ # :attr_reader: columns_rows
43
+ # Size of the window in characters (columns/rows) as <tt>Layout::Size</tt>.
44
+
45
+ ##
46
+ # :attr_reader: pixels
47
+ # Size of the window in pixels as <tt>Layout::Size</tt>.
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ # Backend abstractions for terminal rendering.
10
+ #
11
+ # This module contains types related to terminal backend operations.
12
+ # It mirrors upstream Ratatui's <tt>backend</tt> module structure.
13
+ module Backend
14
+ class << self
15
+ # Queries terminal window size in characters and pixels.
16
+ #
17
+ # Some operations need both the character grid and pixel dimensions.
18
+ # Querying them separately wastes syscalls. Most backends fetch both
19
+ # at once anyway.
20
+ #
21
+ # This method queries crossterm for window dimensions. It returns a
22
+ # <tt>Backend::WindowSize</tt> with <tt>columns_rows</tt> and
23
+ # <tt>pixels</tt> fields, each as <tt>Layout::Size</tt> instances.
24
+ # Returns <tt>nil</tt> if the query fails.
25
+ #
26
+ # Note: Pixel dimensions may be zero on some systems. Unix marks
27
+ # these fields "unused" in TIOCGWINSZ. Windows does not implement them.
28
+ #
29
+ # === Example
30
+ #
31
+ #--
32
+ # SPDX-SnippetBegin
33
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
34
+ # SPDX-License-Identifier: MIT-0
35
+ #++
36
+ # ws = RatatuiRuby::Backend.window_size
37
+ # if ws
38
+ # puts "#{ws.columns_rows.width}x#{ws.columns_rows.height} chars"
39
+ # puts "#{ws.pixels.width}x#{ws.pixels.height} pixels"
40
+ # end
41
+ #--
42
+ # SPDX-SnippetEnd
43
+ #++
44
+ def window_size
45
+ window_size = Terminal._terminal_window_size
46
+ return nil unless window_size
47
+ columns, rows, px_width, px_height = window_size
48
+ WindowSize.new(
49
+ columns_rows: Layout::Size.new(width: columns, height: rows),
50
+ pixels: Layout::Size.new(width: px_width, height: px_height)
51
+ )
52
+ rescue
53
+ nil
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ require_relative "backend/window_size"
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ module Buffer
10
+ # Represents a single cell in the terminal buffer.
11
+ #
12
+ # A terminal grid is made of cells. Each cell contains a character (symbol) and styling (colors, modifiers).
13
+ # When testing, you often need to verify that a specific cell renders correctly.
14
+ #
15
+ # This object encapsulates that state. It provides predicate methods for modifiers, making assertions readable.
16
+ #
17
+ # Use it to inspect the visual state of your application in tests.
18
+ #
19
+ # === Examples
20
+ #
21
+ #--
22
+ # SPDX-SnippetBegin
23
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
24
+ # SPDX-License-Identifier: MIT-0
25
+ #++
26
+ # cell = RatatuiRuby.get_cell_at(0, 0)
27
+ # cell.char # => "H"
28
+ # cell.fg # => :red
29
+ # cell.bold? # => true
30
+ #
31
+ #--
32
+ # SPDX-SnippetEnd
33
+ #++
34
+ class Cell
35
+ # The character displayed in the cell.
36
+ #
37
+ # Named to match Ratatui's Cell::symbol() method.
38
+ attr_reader :symbol
39
+
40
+ # Alias for Rubyists who prefer a shorter name.
41
+ alias char symbol
42
+
43
+ # The foreground color of the cell (e.g., :red, :blue, "#ff0000").
44
+ attr_reader :fg
45
+
46
+ # The background color of the cell (e.g., :black, nil).
47
+ attr_reader :bg
48
+
49
+ # The underline color of the cell.
50
+ #
51
+ # Distinct from foreground color. Some terminals support colored underlines.
52
+ attr_reader :underline_color
53
+
54
+ # The list of active modifiers (e.g., ["bold", "italic"]).
55
+ attr_reader :modifiers
56
+
57
+ # Returns an empty cell (space character, no styles).
58
+ #
59
+ # === Example
60
+ #
61
+ #--
62
+ # SPDX-SnippetBegin
63
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
64
+ # SPDX-License-Identifier: MIT-0
65
+ #++
66
+ # Buffer::Cell.empty # => #<RatatuiRuby::Buffer::Cell char=" ">
67
+ #
68
+ #--
69
+ # SPDX-SnippetEnd
70
+ #++
71
+ def self.empty
72
+ new(symbol: " ", fg: nil, bg: nil, underline_color: nil, modifiers: [])
73
+ end
74
+
75
+ # Returns a default cell (alias for empty).
76
+ #
77
+ # === Example
78
+ #
79
+ #--
80
+ # SPDX-SnippetBegin
81
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
82
+ # SPDX-License-Identifier: MIT-0
83
+ #++
84
+ # Buffer::Cell.default # => #<RatatuiRuby::Buffer::Cell char=" ">
85
+ #
86
+ #--
87
+ # SPDX-SnippetEnd
88
+ #++
89
+ def self.default
90
+ empty
91
+ end
92
+
93
+ # Returns a cell with a specific character and no styles.
94
+ #
95
+ # [symbol] String (single character).
96
+ #
97
+ # === Example
98
+ #
99
+ #--
100
+ # SPDX-SnippetBegin
101
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
102
+ # SPDX-License-Identifier: MIT-0
103
+ #++
104
+ # Buffer::Cell.symbol("X") # => #<RatatuiRuby::Buffer::Cell symbol="X">
105
+ #
106
+ #--
107
+ # SPDX-SnippetEnd
108
+ #++
109
+ def self.symbol(symbol)
110
+ new(symbol:, fg: nil, bg: nil, underline_color: nil, modifiers: [])
111
+ end
112
+
113
+ # Alias for Rubyists who prefer a shorter name.
114
+ def self.char(char)
115
+ symbol(char)
116
+ end
117
+
118
+ # Creates a new Cell.
119
+ #
120
+ # [symbol] String (single character). Aliased as <tt>char:</tt>.
121
+ # [fg] Symbol or String (nullable).
122
+ # [bg] Symbol or String (nullable).
123
+ # [underline_color] Symbol or String (nullable).
124
+ # [modifiers] Array of Strings, Symbols, or any object responding to to_sym or to_s.
125
+ # Normalized to Symbols for consistent output.
126
+ def initialize(symbol: nil, char: nil, fg: nil, bg: nil, underline_color: nil, modifiers: [])
127
+ @symbol = (symbol || char || " ").freeze
128
+ @fg = fg&.freeze
129
+ @bg = bg&.freeze
130
+ @underline_color = underline_color&.freeze
131
+ @modifiers = modifiers.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m.to_s.to_sym }.freeze
132
+ freeze
133
+ end
134
+
135
+ # Returns true if the cell has the bold modifier.
136
+ def bold?
137
+ modifiers.include?(:bold)
138
+ end
139
+
140
+ # Returns true if the cell has the dim modifier.
141
+ def dim?
142
+ modifiers.include?(:dim)
143
+ end
144
+
145
+ # Returns true if the cell has the italic modifier.
146
+ def italic?
147
+ modifiers.include?(:italic)
148
+ end
149
+
150
+ # Returns true if the cell has the underlined modifier.
151
+ def underlined?
152
+ modifiers.include?(:underlined)
153
+ end
154
+
155
+ # Returns true if the cell has the slow_blink modifier.
156
+ def slow_blink?
157
+ modifiers.include?(:slow_blink)
158
+ end
159
+
160
+ # Returns true if the cell has the rapid_blink modifier.
161
+ def rapid_blink?
162
+ modifiers.include?(:rapid_blink)
163
+ end
164
+
165
+ # Returns true if the cell has the reversed modifier.
166
+ def reversed?
167
+ modifiers.include?(:reversed)
168
+ end
169
+
170
+ # Returns true if the cell has the hidden modifier.
171
+ def hidden?
172
+ modifiers.include?(:hidden)
173
+ end
174
+
175
+ # Returns true if the cell has the crossed_out modifier.
176
+ def crossed_out?
177
+ modifiers.include?(:crossed_out)
178
+ end
179
+
180
+ # Checks equality with another Cell.
181
+ def ==(other)
182
+ other.is_a?(Cell) &&
183
+ char == other.char &&
184
+ fg == other.fg &&
185
+ bg == other.bg &&
186
+ underline_color == other.underline_color &&
187
+ modifiers == other.modifiers
188
+ end
189
+
190
+ # Returns a string representation of the cell.
191
+ def inspect
192
+ parts = ["symbol=#{symbol.inspect}"]
193
+ parts << "fg=#{fg.inspect}" if fg
194
+ parts << "bg=#{bg.inspect}" if bg
195
+ parts << "underline_color=#{underline_color.inspect}" if underline_color
196
+ parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
197
+ "#<#{self.class} #{parts.join(' ')}>"
198
+ end
199
+
200
+ # Returns the cell's character.
201
+ def to_s
202
+ symbol
203
+ end
204
+
205
+ # Support for pattern matching.
206
+ # Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
207
+ def deconstruct_keys(keys)
208
+ { symbol:, char: symbol, fg:, bg:, underline_color:, modifiers: }
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ # Buffer primitives for terminal cell inspection.
10
+ #
11
+ # Widgets render to an intermediate buffer, not directly to the terminal.
12
+ # Testing and debugging require access to buffer state.
13
+ #
14
+ # This module mirrors +ratatui::buffer+ and provides query methods
15
+ # for inspecting buffer contents, converting between coordinates and indices,
16
+ # and retrieving individual cells.
17
+ #
18
+ # Use it in tests to verify rendered output or in debugging to inspect state.
19
+ module Buffer
20
+ class << self
21
+ # Converts a position to a linear buffer index.
22
+ #
23
+ # Buffers store cells in a flat array, row by row. Widget code
24
+ # works with (x, y) coordinates. Bridging these representations
25
+ # requires index translation.
26
+ #
27
+ # The index is calculated as <tt>y * width + x</tt>.
28
+ #
29
+ # === Example
30
+ #
31
+ #--
32
+ # SPDX-SnippetBegin
33
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
34
+ # SPDX-License-Identifier: MIT-0
35
+ #++
36
+ # # In a 10-wide buffer, position (3, 2) maps to index 23
37
+ # Buffer.index_of(3, 2) # => 23
38
+ #--
39
+ # SPDX-SnippetEnd
40
+ #++
41
+ #
42
+ # [x] Column (0-indexed from left).
43
+ # [y] Row (0-indexed from top).
44
+ #
45
+ # Returns the linear index (Integer).
46
+ def index_of(x, y)
47
+ area = RatatuiRuby._get_terminal_area
48
+ (y * area["width"]) + x
49
+ end
50
+
51
+ # Converts a linear buffer index to position coordinates.
52
+ #
53
+ # Inverse of +index_of+. When iterating over buffer content
54
+ # by index, use this to recover the original coordinates.
55
+ #
56
+ # === Example
57
+ #
58
+ #--
59
+ # SPDX-SnippetBegin
60
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
61
+ # SPDX-License-Identifier: MIT-0
62
+ #++
63
+ # # In a 10-wide buffer, index 23 maps to position (3, 2)
64
+ # Buffer.pos_of(23) # => [3, 2]
65
+ #--
66
+ # SPDX-SnippetEnd
67
+ #++
68
+ #
69
+ # [index] Linear buffer index (Integer).
70
+ #
71
+ # Returns <tt>[x, y]</tt> coordinates.
72
+ def pos_of(index)
73
+ area = RatatuiRuby._get_terminal_area
74
+ width = area["width"]
75
+ x = index % width
76
+ y = index / width
77
+ [x, y]
78
+ end
79
+
80
+ # Returns the Cell at the specified position.
81
+ #
82
+ # Tests assert on cell contents. This method provides direct
83
+ # access without iterating the entire buffer.
84
+ #
85
+ # Delegates to +RatatuiRuby.get_cell_at+.
86
+ #
87
+ # === Example
88
+ #
89
+ #--
90
+ # SPDX-SnippetBegin
91
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
92
+ # SPDX-License-Identifier: MIT-0
93
+ #++
94
+ # cell = Buffer.get(0, 0)
95
+ # assert_equal "H", cell.char
96
+ #--
97
+ # SPDX-SnippetEnd
98
+ #++
99
+ #
100
+ # [x] Column (0-indexed from left).
101
+ # [y] Row (0-indexed from top).
102
+ #
103
+ # Returns a Buffer::Cell containing the character and style at that position.
104
+ def get(x, y)
105
+ RatatuiRuby.get_cell_at(x, y)
106
+ end
107
+
108
+ # Returns all cells in the buffer as a flat array.
109
+ #
110
+ # Snapshot testing compares entire buffer states. Manually
111
+ # iterating coordinates is verbose and error-prone.
112
+ #
113
+ # This method returns every cell, ordered row by row
114
+ # (top to bottom, left to right).
115
+ #
116
+ # === Example
117
+ #
118
+ #--
119
+ # SPDX-SnippetBegin
120
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
121
+ # SPDX-License-Identifier: MIT-0
122
+ #++
123
+ # cells = Buffer.content
124
+ # cells.each { |cell| puts cell.char }
125
+ #--
126
+ # SPDX-SnippetEnd
127
+ #++
128
+ #
129
+ # Returns an Array of Buffer::Cell objects.
130
+ def content
131
+ area = RatatuiRuby._get_terminal_area
132
+ width = area["width"]
133
+ height = area["height"]
134
+ cells = [] #: Array[Buffer::Cell]
135
+ (0...height).each do |y|
136
+ (0...width).each do |x|
137
+ cells << RatatuiRuby.get_cell_at(x, y)
138
+ end
139
+ end
140
+ cells
141
+ end
142
+
143
+ # Ruby-idiomatic alias (TIMTOWTDI)
144
+ alias [] get
145
+ end
146
+ end
147
+ end
148
+
149
+ require_relative "buffer/cell"