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,568 @@
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
+
10
+ module RatatuiRuby
11
+ module TestHelper
12
+ ##
13
+ # Snapshot testing assertions for terminal UIs.
14
+ #
15
+ # Verifying every character of a TUI screen by hand is tedious. Snapshots let you
16
+ # capture the screen once and compare against it in future runs.
17
+ #
18
+ # This mixin provides <tt>assert_plain_snapshot</tt> for plain text,
19
+ # <tt>assert_rich_snapshot</tt> for styled ANSI output, and
20
+ # <tt>assert_snapshots</tt> (plural) for both. All auto-create snapshot
21
+ # files on first run.
22
+ #
23
+ # Use it to verify complex layouts, styles, and interactions without manual assertions.
24
+ #
25
+ # === Snapshot Files
26
+ #
27
+ # Snapshots live in a <tt>snapshots/</tt> subdirectory next to your test file:
28
+ #
29
+ #--
30
+ # SPDX-SnippetBegin
31
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
32
+ # SPDX-License-Identifier: MIT-0
33
+ #++
34
+ # test/examples/my_app/test_app.rb
35
+ # test/examples/my_app/snapshots/initial_render.txt
36
+ # test/examples/my_app/snapshots/initial_render.ansi
37
+ #
38
+ #--
39
+ # SPDX-SnippetEnd
40
+ #++
41
+ # === Creating and Updating Snapshots
42
+ #
43
+ # Run tests with <tt>UPDATE_SNAPSHOTS=1</tt> to create or refresh snapshots:
44
+ #
45
+ #--
46
+ # SPDX-SnippetBegin
47
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
48
+ # SPDX-License-Identifier: MIT-0
49
+ #++
50
+ # UPDATE_SNAPSHOTS=1 bundle exec rake test
51
+ #
52
+ #--
53
+ # SPDX-SnippetEnd
54
+ #++
55
+ # === Seeding Random Data
56
+ #
57
+ # Random data (scatter plots, generated content) breaks snapshot stability.
58
+ # Use a seeded <tt>Random</tt> instance instead of <tt>Kernel.rand</tt>:
59
+ #
60
+ #--
61
+ # SPDX-SnippetBegin
62
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
63
+ # SPDX-License-Identifier: MIT-0
64
+ #++
65
+ # class MyApp
66
+ # def initialize(seed: nil)
67
+ # @rng = seed ? Random.new(seed) : Random.new
68
+ # end
69
+ #
70
+ # def generate_data
71
+ # (0..20).map { @rng.rand(0.0..10.0) }
72
+ # end
73
+ # end
74
+ #
75
+ # # In your test
76
+ # def setup
77
+ # @app = MyApp.new(seed: 42)
78
+ # end
79
+ #
80
+ #--
81
+ # SPDX-SnippetEnd
82
+ #++
83
+ # For libraries like Faker, see their docs on deterministic random:
84
+ # https://github.com/faker-ruby/faker#deterministic-random
85
+ #
86
+ # === Normalization Blocks
87
+ #
88
+ # Mask dynamic content (timestamps, IDs) with a normalization block:
89
+ #
90
+ #--
91
+ # SPDX-SnippetBegin
92
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
93
+ # SPDX-License-Identifier: MIT-0
94
+ #++
95
+ # assert_snapshots("dashboard") do |lines|
96
+ # lines.map { |l| l.gsub(/\d{4}-\d{2}-\d{2}/, "YYYY-MM-DD") }
97
+ # end
98
+ #
99
+ #--
100
+ # SPDX-SnippetEnd
101
+ #++
102
+ #
103
+ # === Class-Wide Normalization
104
+ #
105
+ # When every test in a class faces the same dynamic content, override
106
+ # <tt>normalize_snapshots</tt> instead of repeating the block. Return a callable
107
+ # (or Array of callables) that transforms lines. The hook runs before any per-call
108
+ # block, so the two compose naturally. See <tt>normalize_snapshots</tt> for details.
109
+ module Snapshot
110
+ # Override this method to normalize all snapshots in a test class.
111
+ #
112
+ # Snapshot assertions compare screen content against stored files. Dynamic content
113
+ # (timestamps, temp paths, random IDs) breaks those comparisons. Passing a normalization
114
+ # block to each assertion fixes one test but creates duplication when every test in a
115
+ # class faces the same dynamic content.
116
+ #
117
+ # Override <tt>normalize_snapshots</tt> in your test class to define class-wide
118
+ # normalization. Accept an Array of Strings (lines) and return the transformed Array.
119
+ #
120
+ # The hook runs before any per-call normalization block, so the two compose naturally:
121
+ # the hook handles class-wide concerns and the block handles one-off masking.
122
+ #
123
+ # Returns <tt>lines</tt> unchanged by default (no normalization).
124
+ #
125
+ # === Example
126
+ #
127
+ #--
128
+ # SPDX-SnippetBegin
129
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
130
+ # SPDX-License-Identifier: MIT-0
131
+ #++
132
+ # class TestFileExplorer < Minitest::Test
133
+ # include RatatuiRuby::TestHelper
134
+ #
135
+ # private def normalize_snapshots(lines)
136
+ # lines.map { |l| l.gsub(Dir.pwd, "STABLE_PATH") }
137
+ # end
138
+ #
139
+ # def test_initial_render
140
+ # # normalize_snapshots runs automatically — no block needed
141
+ # assert_snapshots("initial_render")
142
+ # end
143
+ #
144
+ # def test_after_scroll
145
+ # # Per-call block composes with the hook (hook runs first)
146
+ # assert_snapshots("after_scroll") do |lines|
147
+ # lines.map { |l| l.gsub(/\d{2}:\d{2}/, "XX:XX") }
148
+ # end
149
+ # end
150
+ # end
151
+ #
152
+ #--
153
+ # SPDX-SnippetEnd
154
+ #++
155
+ private def normalize_snapshots(lines)
156
+ lines
157
+ end
158
+ ##
159
+ # Asserts that the current screen content matches a stored plain text snapshot.
160
+ #
161
+ # Plain text snapshots capture layout but miss styling bugs: wrong colors, missing bold,
162
+ # invisible text on a matching background. *Prefer <tt>assert_snapshots</tt>* (plural) to catch
163
+ # styling regressions.
164
+ #
165
+ # Plain text snapshots are human-readable when viewed in any editor or diff tool. They
166
+ # pair well with rich snapshots for documentation. Use <tt>assert_snapshots</tt> to generate both.
167
+ #
168
+ #--
169
+ # SPDX-SnippetBegin
170
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
171
+ # SPDX-License-Identifier: MIT-0
172
+ #++
173
+ # assert_plain_snapshot("login_screen")
174
+ # # Compares against: test/snapshots/login_screen.txt
175
+ #
176
+ # # With normalization block
177
+ # assert_plain_snapshot("clock") do |actual|
178
+ # actual.map { |l| l.gsub(/\d{2}:\d{2}/, "XX:XX") }
179
+ # end
180
+ #
181
+ #--
182
+ # SPDX-SnippetEnd
183
+ #++
184
+ # [name] String name of the snapshot (without extension).
185
+ # [msg] String optional failure message.
186
+ def assert_plain_snapshot(name, msg = nil, snapshot_dir: nil, &)
187
+ # Get the path of the test file calling this method
188
+ snapshot_dir ||= File.join(File.dirname(caller_locations(1, 1).first.path), "snapshots")
189
+ snapshot_path = File.join(snapshot_dir, "#{name}.txt")
190
+
191
+ assert_screen_matches(snapshot_path, msg, &)
192
+ end
193
+
194
+ ##
195
+ # Asserts that the current screen content matches the expected content.
196
+ #
197
+ # Users need to verify that the entire TUI screen looks exactly as expected.
198
+ # Manually checking every cell or line is tedious and error-prone.
199
+ #
200
+ # This helper compares the current buffer content against an expected string (file path)
201
+ # or array of strings. It supports automatic snapshot creation and updating via
202
+ # the +UPDATE_SNAPSHOTS+ environment variable.
203
+ #
204
+ # Use it to verify complex UI states, layouts, and renderings.
205
+ #
206
+ # == Usage
207
+ #
208
+ #--
209
+ # SPDX-SnippetBegin
210
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
211
+ # SPDX-License-Identifier: MIT-0
212
+ #++
213
+ # # Direct comparison
214
+ # assert_screen_matches(["Line 1", "Line 2"])
215
+ #
216
+ # # File comparison
217
+ # assert_screen_matches("test/snapshots/login.txt")
218
+ #
219
+ # # With normalization (e.g., masking dynamic data)
220
+ # assert_screen_matches("test/snapshots/dashboard.txt") do |lines|
221
+ # lines.map { |l| l.gsub(/User ID: \d+/, "User ID: XXX") }
222
+ # end
223
+ #
224
+ #--
225
+ # SPDX-SnippetEnd
226
+ #++
227
+ # [expected] String (file path) or Array<String> (content).
228
+ # [msg] String optional failure message.
229
+ #
230
+ # == Non-Determinism
231
+ #
232
+ # To prevent flaky tests, this assertion performs a "Flakiness Check" when creating or updating
233
+ # snapshots. It captures the screen content, immediately re-renders the buffer, and compares
234
+ # the two results.
235
+ #
236
+ # Ensure your render logic is deterministic by seeding random number generators and stubbing
237
+ # time where necessary.
238
+ def assert_screen_matches(expected, msg = nil)
239
+ actual_lines = normalize_snapshots(buffer_content)
240
+
241
+ if block_given?
242
+ actual_lines = yield(actual_lines)
243
+ end
244
+
245
+ if expected.is_a?(String)
246
+ # Snapshot file mode
247
+ snapshot_path = expected
248
+ update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true"
249
+
250
+ if !File.exist?(snapshot_path) || update_snapshots
251
+ FileUtils.mkdir_p(File.dirname(snapshot_path))
252
+
253
+ content_to_write = "#{actual_lines.join("\n")}\n"
254
+
255
+ begin
256
+ # Delete old file first to avoid git index stale-read issues
257
+ FileUtils.rm_f(snapshot_path)
258
+
259
+ # Write with explicit mode to ensure clean write
260
+ File.write(snapshot_path, content_to_write, mode: "w")
261
+ rescue => e
262
+ warn "Failed to write snapshot #{snapshot_path}: #{e.message}"
263
+ raise
264
+ end
265
+
266
+ if update_snapshots
267
+ puts "Updated snapshot: #{snapshot_path}"
268
+ else
269
+ puts "Created snapshot: #{snapshot_path}"
270
+ end
271
+
272
+ end
273
+ expected_lines = File.readlines(snapshot_path, chomp: true)
274
+ else
275
+ # Direct comparison mode
276
+ expected_lines = expected
277
+ end
278
+
279
+ msg ||= "Screen content mismatch"
280
+
281
+ assert_equal expected_lines.size, actual_lines.size, "#{msg}: Line count mismatch"
282
+
283
+ expected_lines.each_with_index do |expected_line, i|
284
+ actual_line = actual_lines[i]
285
+ assert_equal expected_line, actual_line,
286
+ "#{msg}: Line #{i + 1} mismatch.\nExpected: #{expected_line.inspect}\nActual: #{actual_line.inspect}"
287
+ end
288
+ end
289
+
290
+ ##
291
+ # Asserts that the current screen content (including colors and styles) matches a stored ANSI snapshot.
292
+ #
293
+ # TUIs communicate meaning through colors and styles. Rich snapshots capture everything:
294
+ # wrong colors, missing bold, invisible text on a matching background. *Prefer <tt>assert_snapshots</tt>*
295
+ # (plural) to also generate human-readable plain text files for documentation.
296
+ #
297
+ # The <tt>.ansi</tt> snapshot files contain ANSI escape codes. You can <tt>cat</tt> them in a terminal
298
+ # to see exactly what the screen looked like.
299
+ #
300
+ #--
301
+ # SPDX-SnippetBegin
302
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
303
+ # SPDX-License-Identifier: MIT-0
304
+ #++
305
+ # assert_rich_snapshot("login_screen")
306
+ # # Compares against: test/snapshots/login_screen.ansi
307
+ #
308
+ # # With normalization
309
+ # assert_rich_snapshot("log_view") do |lines|
310
+ # lines.map { |l| l.gsub(/\d{2}:\d{2}:\d{2}/, "HH:MM:SS") }
311
+ # end
312
+ #
313
+ #--
314
+ # SPDX-SnippetEnd
315
+ #++
316
+ # [name] String snapshot name.
317
+ # [msg] String optional failure message.
318
+ def assert_rich_snapshot(name, msg = nil, snapshot_dir: nil)
319
+ snapshot_dir ||= File.join(File.dirname(caller_locations(1, 1).first.path), "snapshots")
320
+ snapshot_path = File.join(snapshot_dir, "#{name}.ansi")
321
+
322
+ actual_content = _render_buffer_with_ansi
323
+
324
+ lines = normalize_snapshots(actual_content.split("\n"))
325
+ if block_given?
326
+ lines = yield(lines)
327
+ end
328
+ actual_content = "#{lines.join("\n")}\n"
329
+
330
+ update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true"
331
+
332
+ if !File.exist?(snapshot_path) || update_snapshots
333
+ FileUtils.mkdir_p(File.dirname(snapshot_path))
334
+
335
+ begin
336
+ # Delete old file first to avoid git index stale-read issues
337
+ FileUtils.rm_f(snapshot_path)
338
+
339
+ # Write with explicit mode to ensure clean write
340
+ File.write(snapshot_path, actual_content, mode: "w")
341
+ rescue => e
342
+ warn "Failed to write rich snapshot #{snapshot_path}: #{e.message}"
343
+ raise
344
+ end
345
+
346
+ puts (update_snapshots ? "Updated" : "Created") + " rich snapshot: #{snapshot_path}"
347
+
348
+ end
349
+
350
+ expected_content = File.read(snapshot_path)
351
+
352
+ # Compare byte-for-byte first
353
+ if expected_content != actual_content
354
+ # Fallback to line-by-line diff for better error messages
355
+ expected_lines = expected_content.split("\n")
356
+ actual_lines = actual_content.split("\n")
357
+
358
+ assert_equal expected_lines.size, actual_lines.size, "#{msg}: Line count mismatch"
359
+
360
+ expected_lines.each_with_index do |exp, i|
361
+ act = actual_lines[i]
362
+ assert_equal exp, act, "#{msg}: Rich content mismatch at line #{i + 1}"
363
+ end
364
+ end
365
+ end
366
+
367
+ ##
368
+ # Asserts both plain text and rich (ANSI-styled) snapshots match.
369
+ #
370
+ # This is the recommended snapshot assertion. It calls both <tt>assert_plain_snapshot</tt> and
371
+ # <tt>assert_rich_snapshot</tt> with the same name, generating <tt>.txt</tt> and <tt>.ansi</tt> files.
372
+ #
373
+ # Rich snapshots catch styling bugs that plain text misses. Plain text snapshots are
374
+ # human-readable in any editor or diff tool, making them valuable for documentation and
375
+ # code review. Together, they provide comprehensive coverage and discoverability.
376
+ #
377
+ #--
378
+ # SPDX-SnippetBegin
379
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
380
+ # SPDX-License-Identifier: MIT-0
381
+ #++
382
+ # assert_snapshots("login_screen")
383
+ # # Creates/compares: snapshots/login_screen.txt AND snapshots/login_screen.ansi
384
+ #
385
+ # # With normalization (masks dynamic content like timestamps)
386
+ # assert_snapshots("dashboard") do |lines|
387
+ # lines.map { |l| l.gsub(/\d{2}:\d{2}:\d{2}/, "HH:MM:SS") }
388
+ # end
389
+ #
390
+ #--
391
+ # SPDX-SnippetEnd
392
+ #++
393
+ # [name] String snapshot name (without extension).
394
+ # [msg] String optional failure message.
395
+ def assert_snapshots(name, msg = nil, &)
396
+ snapshot_dir = File.join(File.dirname(caller_locations(1, 1).first.path), "snapshots")
397
+ assert_plain_snapshot(name, msg, snapshot_dir:, &)
398
+ assert_rich_snapshot(name, msg, snapshot_dir:, &)
399
+ end
400
+
401
+ ##
402
+ # Returns the current buffer content as an ANSI-encoded string.
403
+ #
404
+ # The rich snapshot assertion captures styled output. Sometimes you need the raw ANSI
405
+ # string for debugging, custom assertions, or programmatic inspection.
406
+ #
407
+ # This method renders the buffer with escape codes for colors and modifiers.
408
+ # You can `cat` the output to see exactly what the terminal would display.
409
+ #
410
+ # === Example
411
+ #
412
+ #--
413
+ # SPDX-SnippetBegin
414
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
415
+ # SPDX-License-Identifier: MIT-0
416
+ #++
417
+ # with_test_terminal(80, 25) do
418
+ # RatatuiRuby.run do |tui|
419
+ # tui.draw tui.paragraph(text: "Hello", block: tui.block(title: "Test"))
420
+ # break
421
+ # end
422
+ # ansi_output = render_rich_buffer
423
+ # puts ansi_output # Shows styled output with escape codes
424
+ # end
425
+ #
426
+ #--
427
+ # SPDX-SnippetEnd
428
+ #++
429
+ def render_rich_buffer
430
+ _render_buffer_with_ansi
431
+ end
432
+
433
+ private def _render_buffer_with_ansi
434
+ RatatuiRuby.get_buffer_content # Ensure buffer is fresh if needed
435
+
436
+ lines = buffer_content
437
+ height = lines.size
438
+ width = lines.first&.length || 0
439
+
440
+ output = String.new
441
+
442
+ (0...height).each do |y|
443
+ current_fg = nil
444
+ current_bg = nil
445
+ current_modifiers = []
446
+
447
+ # Reset at start of line
448
+ output << "\e[0m"
449
+
450
+ (0...width).each do |x|
451
+ cell = RatatuiRuby.get_cell_at(x, y)
452
+ char = cell.char || " "
453
+
454
+ # Check for changes
455
+ fg_changed = cell.fg != current_fg
456
+ bg_changed = cell.bg != current_bg
457
+ mod_changed = cell.modifiers != current_modifiers
458
+
459
+ if fg_changed || bg_changed || mod_changed
460
+ # If modifiers change, easiest is to reset and re-apply everything
461
+ # because removing a modifier (e.g. bold) requires reset usually.
462
+ if mod_changed
463
+ output << "\e[0m"
464
+ output << _ansi_for_modifiers(cell.modifiers)
465
+ # Force re-apply colors after reset
466
+ output << _ansi_for_color(cell.fg, :fg)
467
+ output << _ansi_for_color(cell.bg, :bg)
468
+ else
469
+ # Modifiers same, just update colors if needed
470
+ output << _ansi_for_color(cell.fg, :fg) if fg_changed
471
+ output << _ansi_for_color(cell.bg, :bg) if bg_changed
472
+ end
473
+
474
+ current_fg = cell.fg
475
+ current_bg = cell.bg
476
+ current_modifiers = cell.modifiers
477
+ end
478
+
479
+ output << char
480
+ rescue
481
+ output << " "
482
+ end
483
+ output << "\e[0m\n" # Reset at end of line
484
+ end
485
+ output
486
+ end
487
+
488
+ private def _ansi_for_color(color, layer)
489
+ return "" if color.nil?
490
+
491
+ base = (layer == :fg) ? 38 : 48
492
+
493
+ case color
494
+ when Symbol
495
+ if color.to_s.start_with?("indexed_")
496
+ # Extracted indexed color :indexed_5 -> 5
497
+ idx = color.to_s.split("_").last.to_i
498
+ "\e[#{base};5;#{idx}m"
499
+ else
500
+ # Named colors
501
+ _ansi_named_color(color, layer == :fg)
502
+ end
503
+ when String
504
+ if color.start_with?("#")
505
+ # Hex color: #RRGGBB -> r;g;b
506
+ r = color[1..2].to_i(16)
507
+ g = color[3..4].to_i(16)
508
+ b = color[5..6].to_i(16)
509
+ "\e[#{base};2;#{r};#{g};#{b}m"
510
+ else
511
+ ""
512
+ end
513
+ else
514
+ ""
515
+ end
516
+ end
517
+
518
+ private def _ansi_named_color(name, is_fg)
519
+ # Map symbol to standard ANSI code offset
520
+ # FG: 30-37 (dim), 90-97 (bright)
521
+ # BG: 40-47 (dim), 100-107 (bright)
522
+
523
+ offset = is_fg ? 30 : 40
524
+
525
+ case name
526
+ when :black then "\e[#{offset}m"
527
+ when :red then "\e[#{offset + 1}m"
528
+ when :green then "\e[#{offset + 2}m"
529
+ when :yellow then "\e[#{offset + 3}m"
530
+ when :blue then "\e[#{offset + 4}m"
531
+ when :magenta then "\e[#{offset + 5}m"
532
+ when :cyan then "\e[#{offset + 6}m"
533
+ when :gray then is_fg ? "\e[90m" : "\e[100m" # Dark gray usually
534
+ when :dark_gray then is_fg ? "\e[90m" : "\e[100m"
535
+ when :light_red then "\e[#{offset + 60 + 1}m"
536
+ when :light_green then "\e[#{offset + 60 + 2}m"
537
+ when :light_yellow then "\e[#{offset + 60 + 3}m"
538
+ when :light_blue then "\e[#{offset + 60 + 4}m"
539
+ when :light_magenta then "\e[#{offset + 60 + 5}m"
540
+ when :light_cyan then "\e[#{offset + 60 + 6}m"
541
+ when :white then "\e[#{offset + 60 + 7}m"
542
+ else ""
543
+ end
544
+ end
545
+
546
+ private def _ansi_for_modifiers(modifiers)
547
+ return "" if modifiers.nil? || modifiers.empty?
548
+
549
+ seq = []
550
+ seq << "1" if modifiers.include?(:bold)
551
+ seq << "2" if modifiers.include?(:dim)
552
+ seq << "3" if modifiers.include?(:italic)
553
+ seq << "4" if modifiers.include?(:underlined)
554
+ seq << "5" if modifiers.include?(:slow_blink)
555
+ seq << "6" if modifiers.include?(:rapid_blink)
556
+ seq << "7" if modifiers.include?(:reversed)
557
+ seq << "8" if modifiers.include?(:hidden)
558
+ seq << "9" if modifiers.include?(:crossed_out)
559
+
560
+ if seq.any?
561
+ "\e[#{seq.join(';')}m"
562
+ else
563
+ ""
564
+ end
565
+ end
566
+ end
567
+ end
568
+ end
@@ -0,0 +1,24 @@
1
+ ┌Aligned Chart─────────────────────────────────────────────────────────────────┐
2
+ │10│Value ┌──────┐│
3
+ │ │ │TestDS││
4
+ │ │ ••└──────┘│
5
+ │ │ •••• │
6
+ │ │ •••• │
7
+ │ │ •••• │
8
+ │ │ •••• │
9
+ │ │ •••• │
10
+ │ │ •••• │
11
+ │ │ ••• │
12
+ │ 5│ •••• │
13
+ │ │ •••• │
14
+ │ │ •••• │
15
+ │ │ •••• │
16
+ │ │ •••• │
17
+ │ │ •••• │
18
+ │ │ •••• │
19
+ │ │ •••• │
20
+ │ │ •••• │
21
+ │ 0│•• Time│
22
+ │ └───────────────────────────────────────────────────────────────────────────│
23
+ │ 0 5 10│
24
+ └──────────────────────────────────────────────────────────────────────────────┘
@@ -0,0 +1,24 @@
1
+ ┌Aligned Chart─────────────────────────────────────────────────────────────────┐
2
+ │10│Value ┌──────┐│
3
+ │ │ │TestDS││
4
+ │ │ ••└──────┘│
5
+ │ │ •••• │
6
+ │ │ •••• │
7
+ │ │ •••• │
8
+ │ │ •••• │
9
+ │ │ •••• │
10
+ │ │ •••• │
11
+ │ │ ••• │
12
+ │ 5│ •••• │
13
+ │ │ •••• │
14
+ │ │ •••• │
15
+ │ │ •••• │
16
+ │ │ •••• │
17
+ │ │ •••• │
18
+ │ │ •••• │
19
+ │ │ •••• │
20
+ │ │ •••• │
21
+ │ 0│•• Time│
22
+ │ └───────────────────────────────────────────────────────────────────────────│
23
+ │ 0 5 10│
24
+ └──────────────────────────────────────────────────────────────────────────────┘
@@ -0,0 +1,5 @@
1
+ █████ 
2
+ █████ 
3
+ █████ 
4
+ ██5██ 
5
+ Group 
@@ -0,0 +1,5 @@
1
+ █████
2
+ █████
3
+ █████
4
+ ██5██
5
+ Group
@@ -0,0 +1,24 @@
1
+ ┌Test Chart────────────────────────────────────────────────────────────────────┐
2
+ │10│Value ┌──────┐│
3
+ │ │ │TestDS││
4
+ │ │ ••└──────┘│
5
+ │ │ •••• │
6
+ │ │ •••• │
7
+ │ │ •••• │
8
+ │ │ •••• │
9
+ │ │ •••• │
10
+ │ │ •••• │
11
+ │ │ ••• │
12
+ │ │ •••• │
13
+ │ │ •••• │
14
+ │ │ •••• │
15
+ │ │ •••• │
16
+ │ │ •••• │
17
+ │ │ •••• │
18
+ │ │ •••• │
19
+ │ │ •••• │
20
+ │ │ •••• │
21
+ │0 │•• Time│
22
+ │ └───────────────────────────────────────────────────────────────────────────│
23
+ │ 0 10│
24
+ └──────────────────────────────────────────────────────────────────────────────┘
@@ -0,0 +1,24 @@
1
+ ┌Test Chart────────────────────────────────────────────────────────────────────┐
2
+ │10│Value ┌──────┐│
3
+ │ │ │TestDS││
4
+ │ │ ••└──────┘│
5
+ │ │ •••• │
6
+ │ │ •••• │
7
+ │ │ •••• │
8
+ │ │ •••• │
9
+ │ │ •••• │
10
+ │ │ •••• │
11
+ │ │ ••• │
12
+ │ │ •••• │
13
+ │ │ •••• │
14
+ │ │ •••• │
15
+ │ │ •••• │
16
+ │ │ •••• │
17
+ │ │ •••• │
18
+ │ │ •••• │
19
+ │ │ •••• │
20
+ │ │ •••• │
21
+ │0 │•• Time│
22
+ │ └───────────────────────────────────────────────────────────────────────────│
23
+ │ 0 10│
24
+ └──────────────────────────────────────────────────────────────────────────────┘