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,187 @@
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
+ require "timeout"
9
+ require "minitest/mock"
10
+
11
+ module RatatuiRuby
12
+ module TestHelper
13
+ ##
14
+ # Terminal setup and buffer inspection for TUI tests.
15
+ #
16
+ # Testing TUIs against a real terminal is slow, flaky, and hard to automate.
17
+ # Initializing, cleaning up, and inspecting terminal state by hand is tedious.
18
+ #
19
+ # This mixin wraps a headless test terminal. It handles setup, teardown,
20
+ # and provides methods to query buffer content, cursor position, and cell styles.
21
+ #
22
+ # Use it to write fast, deterministic tests for your TUI applications.
23
+ #
24
+ # === Example
25
+ #
26
+ #--
27
+ # SPDX-SnippetBegin
28
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
29
+ # SPDX-License-Identifier: MIT-0
30
+ #++
31
+ # class MyTest < Minitest::Test
32
+ # include RatatuiRuby::TestHelper
33
+ #
34
+ # def test_rendering
35
+ # with_test_terminal(80, 24) do
36
+ # MyApp.new.run_once
37
+ # assert_includes buffer_content.join, "Hello"
38
+ # end
39
+ # end
40
+ # end
41
+ #--
42
+ # SPDX-SnippetEnd
43
+ #++
44
+ module Terminal
45
+ ##
46
+ # Initializes a test terminal context with specified dimensions.
47
+ # Restores the original terminal state after the block executes.
48
+ #
49
+ # [width] Integer width of the test terminal (default: 80).
50
+ # [height] Integer height of the test terminal (default: 24).
51
+ # [timeout] Integer maximum execution time in seconds (default: 2). Pass <tt>nil</tt> to disable.
52
+ #
53
+ # === Example
54
+ #
55
+ #--
56
+ # SPDX-SnippetBegin
57
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
58
+ # SPDX-License-Identifier: MIT-0
59
+ #++
60
+ # with_test_terminal(120, 40) do
61
+ # # render and test your app
62
+ # end
63
+ #--
64
+ # SPDX-SnippetEnd
65
+ #++
66
+ def with_test_terminal(width = 80, height = 24, **opts)
67
+ # Defensive cleanup: reset any stale session state from previous test failures
68
+ RatatuiRuby.instance_variable_set(:@tui_session_active, false)
69
+
70
+ # Extract and resolve viewport
71
+ viewport_param = opts[:viewport]
72
+ viewport = case viewport_param
73
+ when nil then RatatuiRuby::Terminal::Viewport.fullscreen
74
+ when RatatuiRuby::Terminal::Viewport then viewport_param
75
+ end
76
+
77
+ RatatuiRuby.init_test_terminal(width, height, viewport.type.to_s, viewport.height)
78
+ # Flush any lingering events from previous tests
79
+ while (event = RatatuiRuby.poll_event) && !event.none?; end
80
+
81
+ RatatuiRuby.stub :init_terminal, nil do
82
+ RatatuiRuby.stub :restore_terminal, nil do
83
+ @_ratatui_test_terminal_active = true
84
+ timeout = opts.fetch(:timeout, 2)
85
+ if timeout
86
+ Timeout.timeout(timeout) do
87
+ yield
88
+ end
89
+ else
90
+ yield
91
+ end
92
+ ensure
93
+ @_ratatui_test_terminal_active = false
94
+ end
95
+ end
96
+ ensure
97
+ RatatuiRuby.restore_terminal
98
+ end
99
+
100
+ ##
101
+ # Current content of the terminal buffer as an array of strings.
102
+ # Each string represents one row.
103
+ #
104
+ # === Example
105
+ #
106
+ #--
107
+ # SPDX-SnippetBegin
108
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
109
+ # SPDX-License-Identifier: MIT-0
110
+ #++
111
+ # buffer_content
112
+ # # => ["Row 1 text", "Row 2 text", ...]
113
+ #--
114
+ # SPDX-SnippetEnd
115
+ #++
116
+ def buffer_content
117
+ RatatuiRuby.get_buffer_content.split("\n")
118
+ end
119
+
120
+ ##
121
+ # Current cursor position as a hash with <tt>:x</tt> and <tt>:y</tt> keys.
122
+ #
123
+ # === Example
124
+ #
125
+ #--
126
+ # SPDX-SnippetBegin
127
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
128
+ # SPDX-License-Identifier: MIT-0
129
+ #++
130
+ # cursor_position
131
+ # # => { x: 0, y: 0 }
132
+ #--
133
+ # SPDX-SnippetEnd
134
+ #++
135
+ def cursor_position
136
+ x, y = RatatuiRuby.get_cursor_position
137
+ { x:, y: }
138
+ end
139
+
140
+ ##
141
+ # Cell attributes at the given coordinates.
142
+ #
143
+ # Returns a hash with <tt>"symbol"</tt>, <tt>"fg"</tt>, and <tt>"bg"</tt> keys.
144
+ #
145
+ # [x] Integer column position (0-indexed).
146
+ # [y] Integer row position (0-indexed).
147
+ #
148
+ # === Example
149
+ #
150
+ #--
151
+ # SPDX-SnippetBegin
152
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
153
+ # SPDX-License-Identifier: MIT-0
154
+ #++
155
+ # get_cell(0, 0)
156
+ # # => { "symbol" => "H", "fg" => :red, "bg" => nil }
157
+ #--
158
+ # SPDX-SnippetEnd
159
+ #++
160
+ def get_cell(x, y)
161
+ RatatuiRuby.get_cell_at(x, y)
162
+ end
163
+
164
+ ##
165
+ # Prints the current buffer to STDOUT with full ANSI colors.
166
+ # Useful for debugging test failures.
167
+ #
168
+ # === Example
169
+ #
170
+ #--
171
+ # SPDX-SnippetBegin
172
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
173
+ # SPDX-License-Identifier: MIT-0
174
+ #++
175
+ # with_test_terminal do
176
+ # MyApp.new.render
177
+ # print_buffer # see exactly what would display
178
+ # end
179
+ #--
180
+ # SPDX-SnippetEnd
181
+ #++
182
+ def print_buffer
183
+ puts _render_buffer_with_ansi
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,86 @@
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 TestHelper
10
+ ##
11
+ # Test doubles for view testing.
12
+ #
13
+ # View tests verify widget rendering without a real terminal. Real frames draw
14
+ # to the screen. Real rects come from terminal dimensions. Mocking both by hand
15
+ # is tedious.
16
+ #
17
+ # This mixin provides <tt>MockFrame</tt> to capture rendered widgets and
18
+ # <tt>StubRect</tt> to supply fixed dimensions.
19
+ #
20
+ # Use them to test view logic in isolation.
21
+ #
22
+ # === Example
23
+ #
24
+ #--
25
+ # SPDX-SnippetBegin
26
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
27
+ # SPDX-License-Identifier: MIT-0
28
+ #++
29
+ # frame = MockFrame.new
30
+ # area = StubRect.new(width: 60, height: 20)
31
+ # MyView.new.call(state, tui, frame, area)
32
+ #
33
+ # widget = frame.rendered_widgets.first[:widget]
34
+ # assert_equal "Dashboard", widget.block.title
35
+ #--
36
+ # SPDX-SnippetEnd
37
+ #++
38
+ module TestDoubles
39
+ ##
40
+ # Mock frame for view tests.
41
+ #
42
+ # Captures widgets passed to <tt>render_widget</tt> for later inspection.
43
+ #
44
+ # === Example
45
+ #
46
+ #--
47
+ # SPDX-SnippetBegin
48
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
49
+ # SPDX-License-Identifier: MIT-0
50
+ #++
51
+ # frame = MockFrame.new
52
+ # View::Log.new.call(state, tui, frame, area)
53
+ # widget = frame.rendered_widgets.first[:widget]
54
+ # assert_equal "Event Log", widget.block.title
55
+ #--
56
+ # SPDX-SnippetEnd
57
+ #++
58
+ MockFrame = Data.define(:rendered_widgets) do
59
+ def initialize(rendered_widgets: [])
60
+ super
61
+ end
62
+
63
+ def render_widget(widget, area)
64
+ rendered_widgets << { widget:, area: }
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Stub rect with fixed dimensions.
70
+ #
71
+ # [x] Integer left edge (default: 0).
72
+ # [y] Integer top edge (default: 0).
73
+ # [width] Integer width in cells (default: 80).
74
+ # [height] Integer height in cells (default: 24).
75
+ #
76
+ # === Example
77
+ #
78
+ # area = StubRect.new(width: 60, height: 20)
79
+ StubRect = Data.define(:x, :y, :width, :height) do
80
+ def initialize(x: 0, y: 0, width: 80, height: 24)
81
+ super
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,115 @@
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
+ require "fileutils"
9
+ require_relative "test_helper/terminal"
10
+ require_relative "test_helper/snapshot"
11
+ require_relative "test_helper/event_injection"
12
+ require_relative "test_helper/style_assertions"
13
+ require_relative "test_helper/test_doubles"
14
+ require_relative "test_helper/global_state"
15
+ require_relative "test_helper/subprocess_timeout"
16
+
17
+ module RatatuiRuby
18
+ ##
19
+ # Helpers for testing RatatuiRuby applications.
20
+ #
21
+ # Writing TUI tests by hand is tedious. You need a headless terminal, event
22
+ # injection, snapshot comparisons, and style assertions. Wiring all that up
23
+ # yourself is error-prone.
24
+ #
25
+ # This module bundles everything you need. Include it in your test class and
26
+ # start writing tests immediately.
27
+ #
28
+ # == Included Mixins
29
+ #
30
+ # [Terminal] Sets up a headless terminal and queries its buffer.
31
+ # [Snapshot] Compares the screen against stored reference files.
32
+ # [EventInjection] Simulates keypresses, mouse clicks, and resize events.
33
+ # [StyleAssertions] Checks foreground color, background color, and text modifiers.
34
+ # [TestDoubles] Provides mocks and stubs for testing views in isolation.
35
+ # [GlobalState] Provides with_argv and with_env helpers for testing global state access.
36
+ #
37
+ # == Example
38
+ #
39
+ #--
40
+ # SPDX-SnippetBegin
41
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
42
+ # SPDX-License-Identifier: MIT-0
43
+ #++
44
+ # require "ratatui_ruby/test_helper"
45
+ #
46
+ # class TestMyApp < Minitest::Test
47
+ # include RatatuiRuby::TestHelper
48
+ #
49
+ # def test_initial_render
50
+ # with_test_terminal(80, 24) do
51
+ # MyApp.new.run_once
52
+ # assert_snapshots("initial")
53
+ # end
54
+ # end
55
+ #
56
+ # def test_themes
57
+ # with_test_terminal do
58
+ # app = ThemeDemo.new
59
+ # app.run_once
60
+ # assert_rich_snapshot("default_theme")
61
+ #
62
+ # inject_key("t", modifiers: [:ctrl])
63
+ # app.run_once
64
+ # assert_rich_snapshot("dark_theme")
65
+ #
66
+ # inject_key("t", modifiers: [:ctrl])
67
+ # app.run_once
68
+ # assert_rich_snapshot("high_contrast_theme")
69
+ # end
70
+ # end
71
+ #
72
+ # def test_highlighter_applies_selection_style
73
+ # with_test_terminal(40, 5) do
74
+ # RatatuiRuby.draw do |frame|
75
+ # highlighter = MyApp::UI::Highlighter.new(:yellow)
76
+ # highlighter.render_at(frame, 0, 2, "Selected Item")
77
+ # end
78
+ #
79
+ # assert_fg_color(:yellow, 0, 2)
80
+ # assert_bold(0, 2)
81
+ # end
82
+ # end
83
+ #
84
+ # def test_view_in_isolation
85
+ # frame = MockFrame.new
86
+ # area = StubRect.new(width: 60, height: 20)
87
+ #
88
+ # MyView.new.call(state, tui, frame, area)
89
+ #
90
+ # widget = frame.rendered_widgets.first[:widget]
91
+ # assert_equal "Dashboard", widget.block.title
92
+ # end
93
+ # end
94
+ #--
95
+ # SPDX-SnippetEnd
96
+ #++
97
+ module TestHelper
98
+ ##
99
+ # Auto-enables debug mode when TestHelper is included.
100
+ #
101
+ # This ensures Rust backtraces are available in tests.
102
+ # Skips remote debugging since tests don't need it.
103
+ def self.included(base)
104
+ RatatuiRuby::Debug.enable!(source: :test)
105
+ end
106
+
107
+ include Terminal
108
+ include Snapshot
109
+ include EventInjection
110
+ include StyleAssertions
111
+ include TestDoubles
112
+ include GlobalState
113
+ include SubprocessTimeout
114
+ end
115
+ end
@@ -0,0 +1,245 @@
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 Text
10
+ # A sequence of styled spans.
11
+ #
12
+ # Words form sentences. Spans form lines.
13
+ #
14
+ # This class composes multiple {Span} objects into a single horizontal row of text.
15
+ # It handles the layout of rich text fragments within the flow of a paragraph.
16
+ #
17
+ # Use it to build multi-colored headers, status messages, or log entries.
18
+ #
19
+ # === Examples
20
+ #
21
+ #--
22
+ # SPDX-SnippetBegin
23
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
24
+ # SPDX-License-Identifier: MIT-0
25
+ #++
26
+ # Text::Line.new(
27
+ # spans: [
28
+ # Text::Span.styled("User: ", Style.new(modifiers: [:bold])),
29
+ # Text::Span.styled("kerrick", Style.new(fg: :blue))
30
+ # ]
31
+ # )
32
+ #--
33
+ # SPDX-SnippetEnd
34
+ #++
35
+ class Line < Data.define(:spans, :alignment, :style)
36
+ ##
37
+ # :attr_reader: spans
38
+ # Array of Span objects.
39
+
40
+ ##
41
+ # :attr_reader: alignment
42
+ # Alignment within the container.
43
+ #
44
+ # <tt>:left</tt>, <tt>:center</tt>, or <tt>:right</tt>.
45
+
46
+ ##
47
+ # :attr_reader: style
48
+ # Line-level style applied to all spans.
49
+ #
50
+ # A Style object that sets colors/modifiers for the entire line.
51
+
52
+ # Creates a new Line.
53
+ #
54
+ # [spans] Array of Span objects (or Strings).
55
+ # [alignment] Symbol (optional).
56
+ # [style] Style object (optional).
57
+ def initialize(spans: [], alignment: nil, style: nil)
58
+ super
59
+ end
60
+
61
+ # Creates a simple line from a string.
62
+ #
63
+ # Text::Line.from_string("Hello")
64
+ def self.from_string(content, alignment: nil)
65
+ new(spans: [Span.new(content:, style: nil)], alignment:)
66
+ end
67
+
68
+ # Calculates the display width of this line in terminal cells.
69
+ #
70
+ # Sums the widths of all span contents using the same unicode-aware
71
+ # algorithm as Text.width. Useful for layout calculations.
72
+ #
73
+ # === Examples
74
+ #
75
+ #--
76
+ # SPDX-SnippetBegin
77
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
78
+ # SPDX-License-Identifier: MIT-0
79
+ #++
80
+ # line = Text::Line.new(spans: [
81
+ # Text::Span.new(content: "Hello "),
82
+ # Text::Span.new(content: "世界")
83
+ # ])
84
+ # line.width # => 10 (6 ASCII + 4 CJK)
85
+ #
86
+ #--
87
+ # SPDX-SnippetEnd
88
+ #++
89
+ # Returns: Integer (number of terminal cells).
90
+ def width
91
+ RatatuiRuby::Text.width(spans.map { |s| s.content.to_s }.join)
92
+ end
93
+
94
+ # Left-aligns this line of text.
95
+ #
96
+ # Convenience shortcut for <tt>alignment: :left</tt>. Setting the alignment of a Line
97
+ # overrides the alignment of its parent Text or Widget.
98
+ #
99
+ # === Example
100
+ #
101
+ #--
102
+ # SPDX-SnippetBegin
103
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
104
+ # SPDX-License-Identifier: MIT-0
105
+ #++
106
+ # line = Text::Line.new(spans: [Text::Span.new(content: "Hello")])
107
+ # aligned = line.left_aligned
108
+ # aligned.alignment # => :left
109
+ #
110
+ #--
111
+ # SPDX-SnippetEnd
112
+ #++
113
+ # Returns: Line.
114
+ def left_aligned
115
+ with(alignment: :left)
116
+ end
117
+
118
+ # Center-aligns this line of text.
119
+ #
120
+ # Convenience shortcut for <tt>alignment: :center</tt>. Setting the alignment of a Line
121
+ # overrides the alignment of its parent Text or Widget.
122
+ #
123
+ # === Example
124
+ #
125
+ #--
126
+ # SPDX-SnippetBegin
127
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
128
+ # SPDX-License-Identifier: MIT-0
129
+ #++
130
+ # line = Text::Line.new(spans: [Text::Span.new(content: "Hello")])
131
+ # centered = line.centered
132
+ # centered.alignment # => :center
133
+ #
134
+ #--
135
+ # SPDX-SnippetEnd
136
+ #++
137
+ # Returns: Line.
138
+ def centered
139
+ with(alignment: :center)
140
+ end
141
+
142
+ # Right-aligns this line of text.
143
+ #
144
+ # Convenience shortcut for <tt>alignment: :right</tt>. Setting the alignment of a Line
145
+ # overrides the alignment of its parent Text or Widget.
146
+ #
147
+ # === Example
148
+ #
149
+ #--
150
+ # SPDX-SnippetBegin
151
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
152
+ # SPDX-License-Identifier: MIT-0
153
+ #++
154
+ # line = Text::Line.new(spans: [Text::Span.new(content: "Hello")])
155
+ # aligned = line.right_aligned
156
+ # aligned.alignment # => :right
157
+ #
158
+ #--
159
+ # SPDX-SnippetEnd
160
+ #++
161
+ # Returns: Line.
162
+ def right_aligned
163
+ with(alignment: :right)
164
+ end
165
+
166
+ # Adds a span to the line.
167
+ #
168
+ # Since Line is immutable (a Data subclass), this returns a new Line with the span appended.
169
+ # The original line remains unchanged.
170
+ #
171
+ # === Example
172
+ #
173
+ #--
174
+ # SPDX-SnippetBegin
175
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
176
+ # SPDX-License-Identifier: MIT-0
177
+ #++
178
+ # line = Text::Line.new(spans: [Text::Span.new(content: "Hello, ")])
179
+ # extended = line.push_span(Text::Span.new(content: "world!"))
180
+ # extended.spans.size # => 2
181
+ # line.spans.size # => 1 (original unchanged)
182
+ #
183
+ #--
184
+ # SPDX-SnippetEnd
185
+ #++
186
+ # [span] Span to append.
187
+ #
188
+ # Returns: Line.
189
+ def push_span(span)
190
+ with(spans: spans + [span])
191
+ end
192
+
193
+ # Patches the style of this line, adding modifiers from the given style.
194
+ #
195
+ # Applies <tt>patch_style</tt> to each span in the line. Use this when you want to layer
196
+ # styles on all spans without replacing their existing styles.
197
+ #
198
+ # === Example
199
+ #
200
+ #--
201
+ # SPDX-SnippetBegin
202
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
203
+ # SPDX-License-Identifier: MIT-0
204
+ #++
205
+ # line = Text::Line.new(spans: [Text::Span.new(content: "Hello")])
206
+ # styled = line.patch_style(Style::Style.new(fg: :red))
207
+ # styled.spans.first.style.fg # => :red
208
+ #
209
+ #--
210
+ # SPDX-SnippetEnd
211
+ #++
212
+ # [patch] Style::Style to merge onto each span.
213
+ #
214
+ # Returns: Line.
215
+ def patch_style(patch)
216
+ with(spans: spans.map { |s| s.patch_style(patch) })
217
+ end
218
+
219
+ # Resets the style of this line.
220
+ #
221
+ # Applies <tt>reset_style</tt> to each span in the line, clearing all styling.
222
+ #
223
+ # === Example
224
+ #
225
+ #--
226
+ # SPDX-SnippetBegin
227
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
228
+ # SPDX-License-Identifier: MIT-0
229
+ #++
230
+ # line = Text::Line.new(spans: [
231
+ # Text::Span.new(content: "styled", style: Style::Style.new(fg: :red))
232
+ # ])
233
+ # reset = line.reset_style
234
+ # reset.spans.first.style # => nil
235
+ #
236
+ #--
237
+ # SPDX-SnippetEnd
238
+ #++
239
+ # Returns: Line.
240
+ def reset_style
241
+ with(spans: spans.map(&:reset_style))
242
+ end
243
+ end
244
+ end
245
+ end