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,400 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal query trait and implementations.
5
+ //!
6
+ //! This module provides a compiler-enforced interface for querying terminal state.
7
+ //! All queries go through `TerminalQuery` trait which has two implementations:
8
+ //! - `LiveTerminal`: Used outside draw, queries actual terminal
9
+ //! - `DrawSnapshot`: Used during draw, queries pre-captured snapshot
10
+
11
+ use ratatui::buffer::Cell;
12
+ use ratatui::layout::Rect;
13
+
14
+ /// All terminal queries MUST go through this trait.
15
+ /// Compiler enforces both implementations when adding new queries.
16
+ pub trait TerminalQuery {
17
+ fn size(&self) -> Rect;
18
+ fn viewport_area(&self) -> Rect;
19
+ fn is_test_mode(&self) -> bool;
20
+ fn cursor_position(&self) -> Option<(u16, u16)>;
21
+ fn cell_at(&self, x: u16, y: u16) -> Option<Cell>;
22
+ fn frame_count(&self) -> usize;
23
+ }
24
+
25
+ /// Snapshot of terminal state captured before draw.
26
+ /// Answers queries during draw without accessing terminal.
27
+ #[derive(Clone)]
28
+ pub struct DrawSnapshot {
29
+ pub size: Rect,
30
+ pub viewport_area: Rect,
31
+ pub is_test_mode: bool,
32
+ pub cursor_position: Option<(u16, u16)>,
33
+ pub buffer: Option<ratatui::buffer::Buffer>,
34
+ pub frame_count: usize,
35
+ }
36
+
37
+ impl DrawSnapshot {
38
+ /// Capture snapshot from a `TerminalWrapper`.
39
+ pub fn capture(wrapper: &mut super::wrapper::TerminalWrapper) -> Self {
40
+ match wrapper {
41
+ super::wrapper::TerminalWrapper::Crossterm(t) => {
42
+ let size = t.size().unwrap_or_default();
43
+ let frame = t.get_frame();
44
+ let viewport = frame.area();
45
+ let count = frame.count();
46
+ Self {
47
+ size: Rect::new(0, 0, size.width, size.height),
48
+ viewport_area: viewport,
49
+ is_test_mode: false,
50
+ cursor_position: None,
51
+ buffer: None,
52
+ frame_count: count,
53
+ }
54
+ }
55
+ super::wrapper::TerminalWrapper::Test(t) => {
56
+ let size = t.size().unwrap_or_default();
57
+ let frame = t.get_frame();
58
+ let viewport = frame.area();
59
+ let count = frame.count();
60
+ let cursor = t.get_cursor_position().ok().map(Into::into);
61
+ let buffer = t.backend().buffer().clone();
62
+ Self {
63
+ size: Rect::new(0, 0, size.width, size.height),
64
+ viewport_area: viewport,
65
+ is_test_mode: true,
66
+ cursor_position: cursor,
67
+ buffer: Some(buffer),
68
+ frame_count: count,
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ impl TerminalQuery for DrawSnapshot {
76
+ fn size(&self) -> Rect {
77
+ self.size
78
+ }
79
+ fn viewport_area(&self) -> Rect {
80
+ self.viewport_area
81
+ }
82
+ fn is_test_mode(&self) -> bool {
83
+ self.is_test_mode
84
+ }
85
+ fn cursor_position(&self) -> Option<(u16, u16)> {
86
+ self.cursor_position
87
+ }
88
+ fn cell_at(&self, x: u16, y: u16) -> Option<Cell> {
89
+ self.buffer.as_ref()?.cell((x, y)).cloned()
90
+ }
91
+ fn frame_count(&self) -> usize {
92
+ self.frame_count
93
+ }
94
+ }
95
+
96
+ /// Live queries against actual terminal (outside draw).
97
+ /// Uses interior mutability since ratatui's API requires &mut for some queries.
98
+ pub struct LiveTerminal<'a>(std::cell::RefCell<&'a mut super::wrapper::TerminalWrapper>);
99
+
100
+ impl<'a> LiveTerminal<'a> {
101
+ pub fn new(wrapper: &'a mut super::wrapper::TerminalWrapper) -> Self {
102
+ Self(std::cell::RefCell::new(wrapper))
103
+ }
104
+ }
105
+
106
+ impl TerminalQuery for LiveTerminal<'_> {
107
+ fn size(&self) -> Rect {
108
+ match *self.0.borrow() {
109
+ super::wrapper::TerminalWrapper::Crossterm(ref t) => {
110
+ let s = t.size().unwrap_or_default();
111
+ Rect::new(0, 0, s.width, s.height)
112
+ }
113
+ super::wrapper::TerminalWrapper::Test(ref t) => {
114
+ let s = t.size().unwrap_or_default();
115
+ Rect::new(0, 0, s.width, s.height)
116
+ }
117
+ }
118
+ }
119
+
120
+ fn viewport_area(&self) -> Rect {
121
+ match *self.0.borrow_mut() {
122
+ super::wrapper::TerminalWrapper::Crossterm(ref mut t) => t.get_frame().area(),
123
+ super::wrapper::TerminalWrapper::Test(ref mut t) => t.get_frame().area(),
124
+ }
125
+ }
126
+ fn is_test_mode(&self) -> bool {
127
+ matches!(*self.0.borrow(), super::wrapper::TerminalWrapper::Test(_))
128
+ }
129
+ fn cursor_position(&self) -> Option<(u16, u16)> {
130
+ match *self.0.borrow_mut() {
131
+ super::wrapper::TerminalWrapper::Test(ref mut t) => {
132
+ t.get_cursor_position().ok().map(Into::into)
133
+ }
134
+ super::wrapper::TerminalWrapper::Crossterm(_) => None,
135
+ }
136
+ }
137
+ fn cell_at(&self, x: u16, y: u16) -> Option<Cell> {
138
+ match &*self.0.borrow() {
139
+ super::wrapper::TerminalWrapper::Test(t) => t.backend().buffer().cell((x, y)).cloned(),
140
+ super::wrapper::TerminalWrapper::Crossterm(_) => None,
141
+ }
142
+ }
143
+ fn frame_count(&self) -> usize {
144
+ match *self.0.borrow_mut() {
145
+ super::wrapper::TerminalWrapper::Crossterm(ref mut t) => t.get_frame().count(),
146
+ super::wrapper::TerminalWrapper::Test(ref mut t) => t.get_frame().count(),
147
+ }
148
+ }
149
+ }
150
+
151
+ #[cfg(test)]
152
+ mod tests {
153
+ use super::*;
154
+
155
+ // Test: TerminalQuery trait can be implemented by a simple struct
156
+ struct MockQuery {
157
+ size: Rect,
158
+ }
159
+
160
+ impl TerminalQuery for MockQuery {
161
+ fn size(&self) -> Rect {
162
+ self.size
163
+ }
164
+ fn viewport_area(&self) -> Rect {
165
+ Rect::default()
166
+ }
167
+ fn is_test_mode(&self) -> bool {
168
+ false
169
+ }
170
+ fn cursor_position(&self) -> Option<(u16, u16)> {
171
+ None
172
+ }
173
+ fn cell_at(&self, _x: u16, _y: u16) -> Option<Cell> {
174
+ None
175
+ }
176
+ fn frame_count(&self) -> usize {
177
+ 0
178
+ }
179
+ }
180
+
181
+ #[test]
182
+ fn test_trait_can_be_implemented() {
183
+ let query = MockQuery {
184
+ size: Rect::new(0, 0, 80, 24),
185
+ };
186
+ assert_eq!(query.size(), Rect::new(0, 0, 80, 24));
187
+ }
188
+
189
+ #[test]
190
+ fn test_draw_snapshot_returns_size() {
191
+ let snapshot = super::DrawSnapshot {
192
+ size: Rect::new(0, 0, 120, 40),
193
+ viewport_area: Rect::new(0, 0, 120, 40),
194
+ is_test_mode: false,
195
+ cursor_position: None,
196
+ buffer: None,
197
+ frame_count: 0,
198
+ };
199
+ assert_eq!(snapshot.size(), Rect::new(0, 0, 120, 40));
200
+ }
201
+
202
+ #[test]
203
+ fn test_draw_snapshot_returns_viewport_area() {
204
+ let snapshot = super::DrawSnapshot {
205
+ size: Rect::new(0, 0, 120, 40),
206
+ viewport_area: Rect::new(5, 10, 80, 20), // Different from size!
207
+ is_test_mode: false,
208
+ cursor_position: None,
209
+ buffer: None,
210
+ frame_count: 0,
211
+ };
212
+ // This should return the stored viewport_area, not Rect::default()
213
+ assert_eq!(snapshot.viewport_area(), Rect::new(5, 10, 80, 20));
214
+ }
215
+
216
+ #[test]
217
+ fn test_draw_snapshot_returns_is_test_mode() {
218
+ let snapshot = super::DrawSnapshot {
219
+ size: Rect::default(),
220
+ viewport_area: Rect::default(),
221
+ is_test_mode: true, // TRUE!
222
+ cursor_position: None,
223
+ buffer: None,
224
+ frame_count: 0,
225
+ };
226
+ assert!(snapshot.is_test_mode());
227
+ }
228
+
229
+ #[test]
230
+ fn test_draw_snapshot_returns_cursor_position() {
231
+ let snapshot = super::DrawSnapshot {
232
+ size: Rect::default(),
233
+ viewport_area: Rect::default(),
234
+ is_test_mode: false,
235
+ cursor_position: Some((15, 20)), // Not None!
236
+ buffer: None,
237
+ frame_count: 0,
238
+ };
239
+ assert_eq!(snapshot.cursor_position(), Some((15, 20)));
240
+ }
241
+
242
+ #[test]
243
+ fn test_draw_snapshot_returns_cell_at() {
244
+ use ratatui::buffer::Buffer;
245
+
246
+ // Create a buffer with a known cell
247
+ let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
248
+ buffer[(5, 5)].set_char('X');
249
+
250
+ let snapshot = super::DrawSnapshot {
251
+ size: Rect::new(0, 0, 10, 10),
252
+ viewport_area: Rect::new(0, 0, 10, 10),
253
+ is_test_mode: true,
254
+ cursor_position: None,
255
+ buffer: Some(buffer),
256
+ frame_count: 0,
257
+ };
258
+
259
+ let cell = snapshot.cell_at(5, 5);
260
+ assert!(cell.is_some());
261
+ assert_eq!(cell.unwrap().symbol(), "X");
262
+ }
263
+
264
+ #[test]
265
+ fn test_capture_from_terminal_wrapper() {
266
+ use crate::terminal::TerminalWrapper;
267
+ use ratatui::{backend::TestBackend, Terminal};
268
+
269
+ let backend = TestBackend::new(80, 24);
270
+ let terminal = Terminal::new(backend).unwrap();
271
+ let mut wrapper = TerminalWrapper::Test(terminal);
272
+
273
+ let snapshot = super::DrawSnapshot::capture(&mut wrapper);
274
+
275
+ assert_eq!(snapshot.size(), Rect::new(0, 0, 80, 24));
276
+ assert!(snapshot.is_test_mode());
277
+ }
278
+
279
+ #[test]
280
+ fn test_live_terminal_exists() {
281
+ use crate::terminal::TerminalWrapper;
282
+ use ratatui::{backend::TestBackend, Terminal};
283
+
284
+ let backend = TestBackend::new(80, 24);
285
+ let terminal = Terminal::new(backend).unwrap();
286
+ let mut wrapper = TerminalWrapper::Test(terminal);
287
+
288
+ // LiveTerminal should exist and wrap a TerminalWrapper reference
289
+ let _live = super::LiveTerminal::new(&mut wrapper);
290
+ }
291
+
292
+ #[test]
293
+ fn test_live_terminal_returns_size() {
294
+ use crate::terminal::TerminalWrapper;
295
+ use ratatui::{backend::TestBackend, Terminal};
296
+
297
+ let backend = TestBackend::new(100, 50);
298
+ let terminal = Terminal::new(backend).unwrap();
299
+ let mut wrapper = TerminalWrapper::Test(terminal);
300
+
301
+ let live = super::LiveTerminal::new(&mut wrapper);
302
+ // LiveTerminal must implement TerminalQuery
303
+ let size = live.size();
304
+ assert_eq!(size.width, 100);
305
+ assert_eq!(size.height, 50);
306
+ }
307
+
308
+ #[test]
309
+ fn test_live_terminal_returns_is_test_mode() {
310
+ use crate::terminal::TerminalWrapper;
311
+ use ratatui::{backend::TestBackend, Terminal};
312
+
313
+ let backend = TestBackend::new(80, 24);
314
+ let terminal = Terminal::new(backend).unwrap();
315
+ let mut wrapper = TerminalWrapper::Test(terminal);
316
+
317
+ let live = super::LiveTerminal::new(&mut wrapper);
318
+ // TestBackend should return true for is_test_mode
319
+ assert!(live.is_test_mode());
320
+ }
321
+
322
+ #[test]
323
+ fn test_live_terminal_returns_viewport_area() {
324
+ use crate::terminal::TerminalWrapper;
325
+ use ratatui::{backend::TestBackend, Terminal};
326
+
327
+ let backend = TestBackend::new(120, 40);
328
+ let terminal = Terminal::new(backend).unwrap();
329
+ let mut wrapper = TerminalWrapper::Test(terminal);
330
+
331
+ let live = super::LiveTerminal::new(&mut wrapper);
332
+ let viewport = live.viewport_area();
333
+ // Viewport should match the terminal size for fullscreen mode
334
+ assert_eq!(viewport.width, 120);
335
+ assert_eq!(viewport.height, 40);
336
+ }
337
+
338
+ #[test]
339
+ fn test_live_terminal_viewport_area_differs_from_size_for_inline() {
340
+ use crate::terminal::TerminalWrapper;
341
+ use ratatui::{backend::TestBackend, Terminal, Viewport};
342
+
343
+ // Create terminal with inline viewport of height 5 in a 40-line terminal
344
+ let backend = TestBackend::new(80, 40);
345
+ let options = ratatui::TerminalOptions {
346
+ viewport: Viewport::Inline(5),
347
+ };
348
+ let terminal = Terminal::with_options(backend, options).unwrap();
349
+ let mut wrapper = TerminalWrapper::Test(terminal);
350
+
351
+ let live = super::LiveTerminal::new(&mut wrapper);
352
+ let size = live.size();
353
+ let viewport = live.viewport_area();
354
+
355
+ // Terminal size is 80x40
356
+ assert_eq!(size.width, 80);
357
+ assert_eq!(size.height, 40);
358
+
359
+ // But viewport is only 5 lines high (inline mode)
360
+ assert_eq!(viewport.width, 80);
361
+ assert_eq!(viewport.height, 5);
362
+ }
363
+
364
+ #[test]
365
+ fn test_live_terminal_frame_count_increases_after_draw() {
366
+ use crate::terminal::TerminalWrapper;
367
+ use ratatui::{backend::TestBackend, Terminal};
368
+
369
+ let backend = TestBackend::new(80, 24);
370
+ let mut terminal = Terminal::new(backend).unwrap();
371
+
372
+ // Draw once to increment the counter
373
+ terminal.draw(|_frame| {}).unwrap();
374
+
375
+ let mut wrapper = TerminalWrapper::Test(terminal);
376
+ let live = super::LiveTerminal::new(&mut wrapper);
377
+
378
+ // Frame count should be 1 after one draw, NOT 0!
379
+ assert_eq!(live.frame_count(), 1);
380
+ }
381
+
382
+ #[test]
383
+ fn test_live_terminal_frame_count_increases_with_multiple_draws() {
384
+ use crate::terminal::TerminalWrapper;
385
+ use ratatui::{backend::TestBackend, Terminal};
386
+
387
+ let backend = TestBackend::new(80, 24);
388
+ let mut terminal = Terminal::new(backend).unwrap();
389
+
390
+ // Draw twice
391
+ terminal.draw(|_frame| {}).unwrap();
392
+ terminal.draw(|_frame| {}).unwrap();
393
+
394
+ let mut wrapper = TerminalWrapper::Test(terminal);
395
+ let live = super::LiveTerminal::new(&mut wrapper);
396
+
397
+ // Frame count should be 2 after two draws!
398
+ assert_eq!(live.frame_count(), 2);
399
+ }
400
+ }
@@ -0,0 +1,109 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Private terminal storage with safe accessor functions.
5
+ //!
6
+ //! This module provides thread-local storage for the terminal and draw snapshot,
7
+ //! with functions that route queries correctly:
8
+ //! - During draw: queries go to snapshot (no lock needed)
9
+ //! - Outside draw: queries go to live terminal
10
+
11
+ use super::query::{DrawSnapshot, LiveTerminal, TerminalQuery};
12
+ use super::wrapper::TerminalWrapper;
13
+ use std::cell::RefCell;
14
+
15
+ thread_local! {
16
+ static TERMINAL: RefCell<Option<TerminalWrapper>> = const { RefCell::new(None) };
17
+ static DRAW_SNAPSHOT: RefCell<Option<DrawSnapshot>> = const { RefCell::new(None) };
18
+ }
19
+
20
+ /// Check if we're currently inside a draw operation.
21
+ pub fn is_in_draw_mode() -> bool {
22
+ DRAW_SNAPSHOT.with(|cell| cell.borrow().is_some())
23
+ }
24
+
25
+ /// Execute a query. Routes to snapshot during draw, live terminal outside.
26
+ pub fn with_query<R, F>(f: F) -> Option<R>
27
+ where
28
+ F: Fn(&dyn TerminalQuery) -> R,
29
+ {
30
+ // During draw: use snapshot
31
+ DRAW_SNAPSHOT
32
+ .with(|cell| cell.borrow().as_ref().map(|snapshot| f(snapshot)))
33
+ .or_else(|| {
34
+ // Outside draw: use live terminal from thread-local storage
35
+ TERMINAL.with(|cell| {
36
+ cell.borrow_mut().as_mut().map(|t| {
37
+ let live = LiveTerminal::new(t);
38
+ f(&live)
39
+ })
40
+ })
41
+ })
42
+ }
43
+
44
+ /// Mutable access to terminal. Only works OUTSIDE draw.
45
+ pub fn with_terminal_mut<R, F>(f: F) -> Option<R>
46
+ where
47
+ F: FnOnce(&mut TerminalWrapper) -> R,
48
+ {
49
+ // During draw: reject
50
+ if is_in_draw_mode() {
51
+ return None;
52
+ }
53
+ TERMINAL.with(|cell| cell.borrow_mut().as_mut().map(f))
54
+ }
55
+
56
+ /// Execute draw with snapshot for queries.
57
+ pub fn lend_for_draw<R, F>(f: F) -> Option<R>
58
+ where
59
+ F: FnOnce(&mut TerminalWrapper) -> R,
60
+ {
61
+ // Take terminal out of storage
62
+ let mut terminal = TERMINAL.with(|cell| cell.borrow_mut().take())?;
63
+
64
+ // Capture snapshot BEFORE draw (while we have &mut)
65
+ let snapshot = DrawSnapshot::capture(&mut terminal);
66
+ DRAW_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(snapshot));
67
+
68
+ let result = f(&mut terminal);
69
+
70
+ // Clear snapshot, restore terminal
71
+ DRAW_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
72
+ TERMINAL.with(|cell| *cell.borrow_mut() = Some(terminal));
73
+
74
+ Some(result)
75
+ }
76
+
77
+ /// Set the terminal (used during initialization).
78
+ pub fn set_terminal(wrapper: TerminalWrapper) {
79
+ TERMINAL.with(|cell| {
80
+ *cell.borrow_mut() = Some(wrapper);
81
+ });
82
+ }
83
+
84
+ /// Take the terminal out of storage (used during cleanup).
85
+ pub fn take_terminal() -> Option<TerminalWrapper> {
86
+ TERMINAL.with(|cell| cell.borrow_mut().take())
87
+ }
88
+
89
+ /// Check if terminal is initialized.
90
+ pub fn is_initialized() -> bool {
91
+ TERMINAL.with(|cell| cell.borrow().is_some())
92
+ }
93
+
94
+ #[cfg(test)]
95
+ mod tests {
96
+ use crate::terminal::query::DrawSnapshot;
97
+ use ratatui::layout::Rect;
98
+
99
+ #[test]
100
+ fn test_is_in_draw_mode_false_by_default() {
101
+ assert!(!super::is_in_draw_mode());
102
+ }
103
+
104
+ #[test]
105
+ fn test_with_query_returns_none_when_not_initialized() {
106
+ let result = super::with_query(|q| q.size());
107
+ assert!(result.is_none());
108
+ }
109
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal wrapper enum for different backend types.
5
+
6
+ use ratatui::{
7
+ backend::{CrosstermBackend, TestBackend},
8
+ Terminal,
9
+ };
10
+ use std::io;
11
+
12
+ /// Unified terminal type supporting different backends.
13
+ pub enum TerminalWrapper {
14
+ Crossterm(Terminal<CrosstermBackend<io::Stdout>>),
15
+ Test(Terminal<TestBackend>),
16
+ }