ratatui_ruby 0.3.1 → 0.4.0

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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +54 -13
  7. data/CHANGELOG.md +186 -1
  8. data/README.md +17 -15
  9. data/doc/application_architecture.md +116 -0
  10. data/doc/application_testing.md +12 -7
  11. data/doc/contributors/better_dx.md +543 -0
  12. data/doc/contributors/design/ruby_frontend.md +1 -1
  13. data/doc/contributors/developing_examples.md +203 -0
  14. data/doc/contributors/documentation_style.md +97 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/example_analysis.md +82 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +119 -0
  19. data/doc/images/all_events.png +0 -0
  20. data/doc/images/analytics.png +0 -0
  21. data/doc/images/block_padding.png +0 -0
  22. data/doc/images/block_titles.png +0 -0
  23. data/doc/images/box_demo.png +0 -0
  24. data/doc/images/calendar_demo.png +0 -0
  25. data/doc/images/cell_demo.png +0 -0
  26. data/doc/images/chart_demo.png +0 -0
  27. data/doc/images/custom_widget.png +0 -0
  28. data/doc/images/flex_layout.png +0 -0
  29. data/doc/images/gauge_demo.png +0 -0
  30. data/doc/images/hit_test.png +0 -0
  31. data/doc/images/line_gauge_demo.png +0 -0
  32. data/doc/images/list_demo.png +0 -0
  33. data/doc/images/list_styles.png +0 -0
  34. data/doc/images/login_form.png +0 -0
  35. data/doc/images/map_demo.png +0 -0
  36. data/doc/images/mouse_events.png +0 -0
  37. data/doc/images/popup_demo.png +0 -0
  38. data/doc/images/quickstart_dsl.png +0 -0
  39. data/doc/images/quickstart_lifecycle.png +0 -0
  40. data/doc/images/ratatui_logo_demo.png +0 -0
  41. data/doc/images/readme_usage.png +0 -0
  42. data/doc/images/rich_text.png +0 -0
  43. data/doc/images/scroll_text.png +0 -0
  44. data/doc/images/scrollbar_demo.png +0 -0
  45. data/doc/images/sparkline_demo.png +0 -0
  46. data/doc/images/table_flex.png +0 -0
  47. data/doc/images/table_select.png +0 -0
  48. data/doc/images/widget_style_colors.png +0 -0
  49. data/doc/index.md +1 -0
  50. data/doc/interactive_design.md +121 -0
  51. data/doc/quickstart.md +147 -72
  52. data/examples/all_events/app.rb +169 -0
  53. data/examples/all_events/app.rbs +7 -0
  54. data/examples/all_events/test_app.rb +139 -0
  55. data/examples/analytics/app.rb +258 -0
  56. data/examples/analytics/app.rbs +7 -0
  57. data/examples/analytics/test_app.rb +132 -0
  58. data/examples/block_padding/app.rb +63 -0
  59. data/examples/block_padding/app.rbs +7 -0
  60. data/examples/block_padding/test_app.rb +31 -0
  61. data/examples/block_titles/app.rb +61 -0
  62. data/examples/block_titles/app.rbs +7 -0
  63. data/examples/block_titles/test_app.rb +34 -0
  64. data/examples/box_demo/app.rb +216 -0
  65. data/examples/box_demo/app.rbs +7 -0
  66. data/examples/box_demo/test_app.rb +88 -0
  67. data/examples/calendar_demo/app.rb +101 -0
  68. data/examples/calendar_demo/app.rbs +7 -0
  69. data/examples/calendar_demo/test_app.rb +108 -0
  70. data/examples/cell_demo/app.rb +108 -0
  71. data/examples/cell_demo/app.rbs +7 -0
  72. data/examples/cell_demo/test_app.rb +36 -0
  73. data/examples/chart_demo/app.rb +203 -0
  74. data/examples/chart_demo/app.rbs +7 -0
  75. data/examples/chart_demo/test_app.rb +102 -0
  76. data/examples/custom_widget/app.rb +51 -0
  77. data/examples/custom_widget/app.rbs +7 -0
  78. data/examples/custom_widget/test_app.rb +30 -0
  79. data/examples/flex_layout/app.rb +156 -0
  80. data/examples/flex_layout/app.rbs +7 -0
  81. data/examples/flex_layout/test_app.rb +65 -0
  82. data/examples/gauge_demo/app.rb +182 -0
  83. data/examples/gauge_demo/app.rbs +7 -0
  84. data/examples/gauge_demo/test_app.rb +120 -0
  85. data/examples/hit_test/app.rb +175 -0
  86. data/examples/hit_test/app.rbs +7 -0
  87. data/examples/hit_test/test_app.rb +102 -0
  88. data/examples/line_gauge_demo/app.rb +190 -0
  89. data/examples/line_gauge_demo/app.rbs +7 -0
  90. data/examples/line_gauge_demo/test_app.rb +129 -0
  91. data/examples/list_demo/app.rb +253 -0
  92. data/examples/list_demo/app.rbs +12 -0
  93. data/examples/list_demo/test_app.rb +237 -0
  94. data/examples/list_styles/app.rb +140 -0
  95. data/examples/list_styles/app.rbs +7 -0
  96. data/examples/list_styles/test_app.rb +157 -0
  97. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  98. data/examples/login_form/app.rbs +7 -0
  99. data/examples/login_form/test_app.rb +51 -0
  100. data/examples/map_demo/app.rb +90 -0
  101. data/examples/map_demo/app.rbs +7 -0
  102. data/examples/map_demo/test_app.rb +149 -0
  103. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  104. data/examples/mouse_events/app.rbs +7 -0
  105. data/examples/mouse_events/test_app.rb +53 -0
  106. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  107. data/examples/popup_demo/app.rbs +7 -0
  108. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  109. data/examples/quickstart_dsl/app.rb +36 -0
  110. data/examples/quickstart_dsl/app.rbs +7 -0
  111. data/examples/quickstart_dsl/test_app.rb +29 -0
  112. data/examples/quickstart_lifecycle/app.rb +39 -0
  113. data/examples/quickstart_lifecycle/app.rbs +7 -0
  114. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  115. data/examples/ratatui_logo_demo/app.rb +79 -0
  116. data/examples/ratatui_logo_demo/app.rbs +7 -0
  117. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  118. data/examples/ratatui_mascot_demo/app.rb +84 -0
  119. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  120. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  121. data/examples/readme_usage/app.rb +29 -0
  122. data/examples/readme_usage/app.rbs +7 -0
  123. data/examples/readme_usage/test_app.rb +29 -0
  124. data/examples/rich_text/app.rb +141 -0
  125. data/examples/rich_text/app.rbs +7 -0
  126. data/examples/rich_text/test_app.rb +166 -0
  127. data/examples/scroll_text/app.rb +103 -0
  128. data/examples/scroll_text/app.rbs +7 -0
  129. data/examples/scroll_text/test_app.rb +110 -0
  130. data/examples/scrollbar_demo/app.rb +143 -0
  131. data/examples/scrollbar_demo/app.rbs +7 -0
  132. data/examples/scrollbar_demo/test_app.rb +77 -0
  133. data/examples/sparkline_demo/app.rb +240 -0
  134. data/examples/sparkline_demo/app.rbs +10 -0
  135. data/examples/sparkline_demo/test_app.rb +107 -0
  136. data/examples/table_flex/app.rb +65 -0
  137. data/examples/table_flex/app.rbs +7 -0
  138. data/examples/table_flex/test_app.rb +36 -0
  139. data/examples/table_select/app.rb +198 -0
  140. data/examples/table_select/app.rbs +7 -0
  141. data/examples/table_select/test_app.rb +180 -0
  142. data/examples/widget_style_colors/app.rb +104 -0
  143. data/examples/widget_style_colors/app.rbs +14 -0
  144. data/examples/widget_style_colors/test_app.rb +48 -0
  145. data/ext/ratatui_ruby/Cargo.lock +889 -115
  146. data/ext/ratatui_ruby/Cargo.toml +4 -3
  147. data/ext/ratatui_ruby/clippy.toml +7 -0
  148. data/ext/ratatui_ruby/extconf.rb +7 -0
  149. data/ext/ratatui_ruby/src/events.rs +218 -229
  150. data/ext/ratatui_ruby/src/lib.rs +38 -10
  151. data/ext/ratatui_ruby/src/rendering.rs +90 -10
  152. data/ext/ratatui_ruby/src/style.rs +281 -98
  153. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  154. data/ext/ratatui_ruby/src/text.rs +171 -0
  155. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  156. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  157. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  158. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  159. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  160. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  161. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  162. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  163. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  164. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  165. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  166. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  167. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  168. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  169. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  170. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  171. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  172. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  173. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  174. data/lib/ratatui_ruby/cell.rb +166 -0
  175. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  176. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  177. data/lib/ratatui_ruby/event/key.rb +211 -0
  178. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  179. data/lib/ratatui_ruby/event/paste.rb +71 -0
  180. data/lib/ratatui_ruby/event/resize.rb +80 -0
  181. data/lib/ratatui_ruby/event.rb +79 -0
  182. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  184. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  185. data/lib/ratatui_ruby/schema/block.rb +186 -14
  186. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  187. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  188. data/lib/ratatui_ruby/schema/center.rb +49 -11
  189. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  190. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  191. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  192. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  193. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  194. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  195. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  196. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  197. data/lib/ratatui_ruby/schema/list.rb +93 -19
  198. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  199. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  200. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  201. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  202. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  203. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  204. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  205. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  206. data/lib/ratatui_ruby/schema/style.rb +49 -21
  207. data/lib/ratatui_ruby/schema/table.rb +119 -21
  208. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  209. data/lib/ratatui_ruby/schema/text.rb +90 -0
  210. data/lib/ratatui_ruby/session.rb +146 -0
  211. data/lib/ratatui_ruby/test_helper.rb +156 -13
  212. data/lib/ratatui_ruby/version.rb +1 -1
  213. data/lib/ratatui_ruby.rb +143 -23
  214. data/sig/ratatui_ruby/event.rbs +69 -0
  215. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  216. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  217. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  218. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  219. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  220. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  221. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  222. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  223. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  224. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  225. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  226. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  227. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  228. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  229. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  230. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  231. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  232. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  233. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  234. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  235. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  236. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  237. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  238. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  239. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  240. data/tasks/resources/build.yml.erb +13 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  242. data/tasks/terminal_preview/crash_report.rb +54 -0
  243. data/tasks/terminal_preview/example_app.rb +25 -0
  244. data/tasks/terminal_preview/launcher_script.rb +48 -0
  245. data/tasks/terminal_preview/preview_collection.rb +60 -0
  246. data/tasks/terminal_preview/preview_timing.rb +22 -0
  247. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  248. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  249. data/tasks/terminal_preview/system_appearance.rb +11 -0
  250. data/tasks/terminal_preview/terminal_window.rb +138 -0
  251. data/tasks/terminal_preview/window_id.rb +14 -0
  252. data/tasks/terminal_preview.rake +28 -0
  253. data/tasks/test.rake +1 -1
  254. metadata +174 -53
  255. data/doc/images/examples-analytics.rb.png +0 -0
  256. data/doc/images/examples-box_demo.rb.png +0 -0
  257. data/doc/images/examples-calendar_demo.rb.png +0 -0
  258. data/doc/images/examples-chart_demo.rb.png +0 -0
  259. data/doc/images/examples-custom_widget.rb.png +0 -0
  260. data/doc/images/examples-dashboard.rb.png +0 -0
  261. data/doc/images/examples-list_styles.rb.png +0 -0
  262. data/doc/images/examples-login_form.rb.png +0 -0
  263. data/doc/images/examples-map_demo.rb.png +0 -0
  264. data/doc/images/examples-mouse_events.rb.png +0 -0
  265. data/doc/images/examples-popup_demo.rb.gif +0 -0
  266. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  267. data/doc/images/examples-scroll_text.rb.png +0 -0
  268. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  269. data/doc/images/examples-stock_ticker.rb.png +0 -0
  270. data/doc/images/examples-system_monitor.rb.png +0 -0
  271. data/doc/images/examples-table_select.rb.png +0 -0
  272. data/examples/analytics.rb +0 -88
  273. data/examples/box_demo.rb +0 -71
  274. data/examples/calendar_demo.rb +0 -55
  275. data/examples/chart_demo.rb +0 -84
  276. data/examples/custom_widget.rb +0 -43
  277. data/examples/dashboard.rb +0 -72
  278. data/examples/list_styles.rb +0 -66
  279. data/examples/map_demo.rb +0 -58
  280. data/examples/quickstart_dsl.rb +0 -30
  281. data/examples/quickstart_lifecycle.rb +0 -40
  282. data/examples/readme_usage.rb +0 -21
  283. data/examples/scroll_text.rb +0 -74
  284. data/examples/scrollbar_demo.rb +0 -75
  285. data/examples/stock_ticker.rb +0 -93
  286. data/examples/system_monitor.rb +0 -94
  287. data/examples/table_select.rb +0 -70
  288. data/examples/test_analytics.rb +0 -65
  289. data/examples/test_box_demo.rb +0 -38
  290. data/examples/test_calendar_demo.rb +0 -66
  291. data/examples/test_dashboard.rb +0 -38
  292. data/examples/test_list_styles.rb +0 -61
  293. data/examples/test_login_form.rb +0 -63
  294. data/examples/test_map_demo.rb +0 -100
  295. data/examples/test_scroll_text.rb +0 -130
  296. data/examples/test_stock_ticker.rb +0 -39
  297. data/examples/test_system_monitor.rb +0 -40
  298. data/examples/test_table_select.rb +0 -37
  299. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  300. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+
9
+ # Demonstrates Gauge widget with interactive attribute cycling.
10
+ class GaugeDemoApp
11
+ def initialize
12
+ @ratio = 0.65
13
+ @ratios = [0.0, 0.25, 0.5, 0.65, 0.8, 0.95, 1.0]
14
+ @ratio_index = 3
15
+
16
+ @gauge_colors = [
17
+ { name: "Green", color: :green },
18
+ { name: "Yellow", color: :yellow },
19
+ { name: "Red", color: :red },
20
+ { name: "Cyan", color: :cyan },
21
+ { name: "Blue", color: :blue }
22
+ ]
23
+ @gauge_color_index = 0
24
+
25
+ @bg_styles = [
26
+ { name: "None", style: nil },
27
+ { name: "Dark Gray BG", style: RatatuiRuby::Style.new(fg: :dark_gray) },
28
+ { name: "White on Black", style: RatatuiRuby::Style.new(fg: :white, bg: :black) },
29
+ { name: "Bold White", style: RatatuiRuby::Style.new(fg: :white, modifiers: [:bold]) }
30
+ ]
31
+ @bg_style_index = 1
32
+
33
+ @use_unicode_options = [true, false]
34
+ @use_unicode_index = 0
35
+
36
+ @label_modes = [
37
+ { name: "Percentage", template: ->(ratio) { "#{(ratio * 100).to_i}%" } },
38
+ { name: "Ratio (decimal)", template: ->(ratio) { format("%.2f", ratio) } },
39
+ { name: "Progress", template: ->(ratio) { "Progress: #{(ratio * 100).to_i}%" } },
40
+ { name: "None", template: ->(ratio) { nil } }
41
+ ]
42
+ @label_mode_index = 0
43
+ end
44
+
45
+ def run
46
+ RatatuiRuby.run do
47
+ loop do
48
+ render
49
+ break if handle_input == :quit
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def render
57
+ @ratio = @ratios[@ratio_index]
58
+ gauge_color = @gauge_colors[@gauge_color_index][:color]
59
+ bg_style = @bg_styles[@bg_style_index][:style]
60
+ use_unicode = @use_unicode_options[@use_unicode_index]
61
+ label_template = @label_modes[@label_mode_index][:template]
62
+
63
+ gauge_style = RatatuiRuby::Style.new(fg: gauge_color)
64
+ label = label_template.call(@ratio)
65
+
66
+ layout = RatatuiRuby::Layout.new(
67
+ direction: :vertical,
68
+ constraints: [
69
+ RatatuiRuby::Constraint.fill(1),
70
+ RatatuiRuby::Constraint.length(6)
71
+ ],
72
+ children: [
73
+ # Main content area with multiple gauge examples
74
+ RatatuiRuby::Layout.new(
75
+ direction: :vertical,
76
+ constraints: [
77
+ RatatuiRuby::Constraint.length(1),
78
+ RatatuiRuby::Constraint.fill(1),
79
+ RatatuiRuby::Constraint.fill(1),
80
+ RatatuiRuby::Constraint.fill(1),
81
+ RatatuiRuby::Constraint.length(1)
82
+ ],
83
+ children: [
84
+ RatatuiRuby::Paragraph.new(
85
+ text: "Gauge Widget Demo",
86
+ style: RatatuiRuby::Style.new(modifiers: [:bold])
87
+ ),
88
+ # Gauge 1: Main interactive gauge
89
+ RatatuiRuby::Gauge.new(
90
+ ratio: @ratio,
91
+ label:,
92
+ style: bg_style,
93
+ gauge_style:,
94
+ use_unicode:,
95
+ block: RatatuiRuby::Block.new(title: "Interactive Gauge")
96
+ ),
97
+ # Gauge 2: Inverse ratio for comparison
98
+ RatatuiRuby::Gauge.new(
99
+ ratio: 1.0 - @ratio,
100
+ label: label_template.call(1.0 - @ratio),
101
+ style: bg_style,
102
+ gauge_style:,
103
+ use_unicode:,
104
+ block: RatatuiRuby::Block.new(title: "Inverse (1.0 - ratio)")
105
+ ),
106
+ # Gauge 3: Fixed at different stages
107
+ RatatuiRuby::Gauge.new(
108
+ ratio: [@ratio, 0.5].max,
109
+ label: "Min 50%",
110
+ style: RatatuiRuby::Style.new(fg: :dark_gray),
111
+ gauge_style: RatatuiRuby::Style.new(fg: :magenta),
112
+ use_unicode:,
113
+ block: RatatuiRuby::Block.new(title: "Min Threshold (Magenta)")
114
+ ),
115
+ RatatuiRuby::Paragraph.new(text: "")
116
+ ]
117
+ ),
118
+ # Bottom controls panel
119
+ RatatuiRuby::Block.new(
120
+ title: "Controls",
121
+ borders: [:all],
122
+ children: [
123
+ RatatuiRuby::Paragraph.new(
124
+ text: [
125
+ # Navigation & General
126
+ RatatuiRuby::Text::Line.new(spans: [
127
+ RatatuiRuby::Text::Span.new(content: "←/→", style: RatatuiRuby::Style.new(modifiers: [:bold, :underlined])),
128
+ RatatuiRuby::Text::Span.new(content: ": Adjust Ratio (#{format('%.2f', @ratio)}) "),
129
+ RatatuiRuby::Text::Span.new(content: "q", style: RatatuiRuby::Style.new(modifiers: [:bold, :underlined])),
130
+ RatatuiRuby::Text::Span.new(content: ": Quit")
131
+ ]),
132
+ # Styling
133
+ RatatuiRuby::Text::Line.new(spans: [
134
+ RatatuiRuby::Text::Span.new(content: "g", style: RatatuiRuby::Style.new(modifiers: [:bold, :underlined])),
135
+ RatatuiRuby::Text::Span.new(content: ": Color (#{@gauge_colors[@gauge_color_index][:name]}) "),
136
+ RatatuiRuby::Text::Span.new(content: "b", style: RatatuiRuby::Style.new(modifiers: [:bold, :underlined])),
137
+ RatatuiRuby::Text::Span.new(content: ": Background (#{@bg_styles[@bg_style_index][:name]})")
138
+ ]),
139
+ # Options
140
+ RatatuiRuby::Text::Line.new(spans: [
141
+ RatatuiRuby::Text::Span.new(content: "u", style: RatatuiRuby::Style.new(modifiers: [:bold, :underlined])),
142
+ RatatuiRuby::Text::Span.new(content: ": Unicode (#{use_unicode ? 'On' : 'Off'}) "),
143
+ RatatuiRuby::Text::Span.new(content: "l", style: RatatuiRuby::Style.new(modifiers: [:bold, :underlined])),
144
+ RatatuiRuby::Text::Span.new(content: ": Label (#{@label_modes[@label_mode_index][:name]})")
145
+ ])
146
+ ]
147
+ )
148
+ ]
149
+ )
150
+ ]
151
+ )
152
+
153
+ RatatuiRuby.draw(layout)
154
+ end
155
+
156
+ def handle_input
157
+ event = RatatuiRuby.poll_event
158
+ return unless event
159
+
160
+ case event
161
+ in {type: :key, code: "q"} | {type: :key, code: "c", modifiers: ["ctrl"]}
162
+ :quit
163
+ in type: :key, code: "right"
164
+ @ratio_index = (@ratio_index + 1) % @ratios.length
165
+ in type: :key, code: "left"
166
+ @ratio_index = (@ratio_index - 1) % @ratios.length
167
+ in type: :key, code: "g"
168
+ @gauge_color_index = (@gauge_color_index + 1) % @gauge_colors.length
169
+ in type: :key, code: "b"
170
+ @bg_style_index = (@bg_style_index + 1) % @bg_styles.length
171
+ in type: :key, code: "u"
172
+ @use_unicode_index = (@use_unicode_index + 1) % @use_unicode_options.length
173
+ in type: :key, code: "l"
174
+ @label_mode_index = (@label_mode_index + 1) % @label_modes.length
175
+ else
176
+ # Ignore other events
177
+ nil
178
+ end
179
+ end
180
+ end
181
+
182
+ GaugeDemoApp.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,7 @@
1
+ class SystemMonitorApp
2
+ # @public
3
+ def self.new: () -> SystemMonitorApp
4
+
5
+ # @public
6
+ def run: () -> void
7
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+ require "ratatui_ruby/test_helper"
9
+ require "minitest/autorun"
10
+ require_relative "app"
11
+
12
+ class TestGaugeDemo < Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+
15
+ def setup
16
+ @app = GaugeDemoApp.new
17
+ end
18
+
19
+ def test_initial_render
20
+ with_test_terminal do
21
+ inject_key(:q)
22
+ @app.run
23
+
24
+ content = buffer_content.join("\n")
25
+ assert_includes content, "Interactive Gauge"
26
+ assert_includes content, "Inverse"
27
+ assert_includes content, "Min Threshold"
28
+ end
29
+ end
30
+
31
+ def test_ratio_increment
32
+ with_test_terminal do
33
+ inject_key(:right)
34
+ inject_key(:q)
35
+ @app.run
36
+
37
+ content = buffer_content.join("\n")
38
+ # After pressing right from index 3 (0.65), should be at index 4 (0.80)
39
+ assert_includes content, "0.80"
40
+ end
41
+ end
42
+
43
+ def test_ratio_decrement
44
+ with_test_terminal do
45
+ inject_key(:left)
46
+ inject_key(:q)
47
+ @app.run
48
+
49
+ content = buffer_content.join("\n")
50
+ # After pressing left from index 3 (0.65), should be at index 2 (0.50)
51
+ assert_includes content, "0.50"
52
+ end
53
+ end
54
+
55
+ def test_gauge_color_cycling
56
+ with_test_terminal do
57
+ inject_key(:g)
58
+ inject_key(:q)
59
+ @app.run
60
+
61
+ content = buffer_content.join("\n")
62
+ # After pressing g from index 0 (Green), should be at index 1 (Yellow)
63
+ assert_includes content, "Yellow"
64
+ refute_includes content, "Green"
65
+ end
66
+ end
67
+
68
+ def test_background_style_cycling
69
+ with_test_terminal do
70
+ inject_key(:b)
71
+ inject_key(:q)
72
+ @app.run
73
+
74
+ content = buffer_content.join("\n")
75
+ # After pressing b from index 1 (Dark Gray BG), should be at index 2 (White on Black)
76
+ assert_includes content, "White on Black"
77
+ refute_includes content, "Dark Gray BG"
78
+ end
79
+ end
80
+
81
+ def test_unicode_toggle
82
+ with_test_terminal do
83
+ inject_key(:u)
84
+ inject_key(:q)
85
+ @app.run
86
+
87
+ content = buffer_content.join("\n")
88
+ # After toggling unicode, should still show gauge controls
89
+ assert_includes content, "Off"
90
+ end
91
+ end
92
+
93
+ def test_label_mode_cycling
94
+ with_test_terminal do
95
+ inject_key(:l)
96
+ inject_key(:q)
97
+ @app.run
98
+
99
+ content = buffer_content.join("\n")
100
+ # After pressing l from index 0 (Percentage), should be at index 1 (Ratio decimal)
101
+ # The gauge should show decimal format instead of percentage
102
+ assert_includes content, "0.65"
103
+ end
104
+ end
105
+
106
+ def test_multiple_interactions
107
+ with_test_terminal do
108
+ inject_keys(:right, :g, :b, :u, :l, :q)
109
+ @app.run
110
+
111
+ content = buffer_content.join("\n")
112
+ # Verify all controls are visible
113
+ assert_includes content, "Adjust Ratio"
114
+ assert_includes content, "Color"
115
+ assert_includes content, "Background"
116
+ assert_includes content, "Unicode"
117
+ assert_includes content, "Label"
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+
9
+ # Demonstrates hit testing using Layout.split with the Cached Layout Pattern.
10
+ #
11
+ # This example shows how to calculate layout regions *once per frame* and reuse
12
+ # those regions for both rendering and hit testing. This is essential for
13
+ # immediate-mode UI development where the same layout is used by multiple
14
+ # subsystems (render, event handling, etc.).
15
+ #
16
+ # Controls:
17
+ # - Left/Right arrows: Adjust split ratio
18
+ # - Click: Detect which panel was clicked
19
+ # - q: Quit
20
+ class HitTestApp
21
+ def initialize
22
+ @left_ratio = 50
23
+ @message = "Click a panel or adjust ratio"
24
+ @last_click = nil
25
+ @hotkey_style = RatatuiRuby::Style.new(modifiers: [:bold, :underlined])
26
+ end
27
+
28
+ def run
29
+ RatatuiRuby.run do
30
+ loop do
31
+ calculate_layout # Phase 1: Layout calculation (once per frame)
32
+ render # Phase 2: Draw to terminal
33
+ break if handle_input == :quit # Phase 3: Consume input using cached rects
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def calculate_layout
41
+ # Single source of truth for layout geometry.
42
+ # Calculated once per frame, then reused by render() and handle_input().
43
+ full_area = RatatuiRuby::Rect.new(x: 0, y: 0, width: 80, height: 24)
44
+
45
+ # First split: main content vs bottom controls.
46
+ @main_area, @control_area = RatatuiRuby::Layout.split(
47
+ full_area,
48
+ direction: :vertical,
49
+ constraints: [
50
+ RatatuiRuby::Constraint.fill(1),
51
+ RatatuiRuby::Constraint.length(7),
52
+ ]
53
+ )
54
+
55
+ # Second split: within main content, left vs right panels.
56
+ @left_rect, @right_rect = RatatuiRuby::Layout.split(
57
+ @main_area,
58
+ direction: :horizontal,
59
+ constraints: [
60
+ RatatuiRuby::Constraint.percentage(@left_ratio),
61
+ RatatuiRuby::Constraint.percentage(100 - @left_ratio)
62
+ ]
63
+ )
64
+ end
65
+
66
+ def render
67
+ # Build UI with the pre-calculated regions
68
+ left_panel = build_panel("Left Panel", @left_rect, @last_click == :left)
69
+ right_panel = build_panel("Right Panel", @right_rect, @last_click == :right)
70
+
71
+ layout = RatatuiRuby::Layout.new(
72
+ direction: :horizontal,
73
+ constraints: [
74
+ RatatuiRuby::Constraint.percentage(@left_ratio),
75
+ RatatuiRuby::Constraint.percentage(100 - @left_ratio)
76
+ ],
77
+ children: [left_panel, right_panel]
78
+ )
79
+
80
+ # Bottom control panel
81
+ control_panel = RatatuiRuby::Block.new(
82
+ title: "Controls",
83
+ borders: [:all],
84
+ children: [
85
+ RatatuiRuby::Paragraph.new(
86
+ text: [
87
+ RatatuiRuby::Text::Line.new(spans: [
88
+ RatatuiRuby::Text::Span.new(content: "RATIO", style: RatatuiRuby::Style.new(modifiers: [:bold]))
89
+ ]),
90
+ RatatuiRuby::Text::Line.new(spans: [
91
+ RatatuiRuby::Text::Span.new(content: "←", style: @hotkey_style),
92
+ RatatuiRuby::Text::Span.new(content: ": Decrease (#{@left_ratio}%) "),
93
+ RatatuiRuby::Text::Span.new(content: "→", style: @hotkey_style),
94
+ RatatuiRuby::Text::Span.new(content: ": Increase (#{@left_ratio}%) "),
95
+ RatatuiRuby::Text::Span.new(content: "q", style: @hotkey_style),
96
+ RatatuiRuby::Text::Span.new(content: ": Quit")
97
+ ]),
98
+ RatatuiRuby::Text::Line.new(spans: [
99
+ RatatuiRuby::Text::Span.new(content: "HIT TESTING", style: RatatuiRuby::Style.new(modifiers: [:bold]))
100
+ ]),
101
+ "Click panels above to detect hits.",
102
+ "Last Click: #{@last_click || 'None'} - #{@message}"
103
+ ]
104
+ )
105
+ ]
106
+ )
107
+
108
+ # Full layout with bottom controls
109
+ full_layout = RatatuiRuby::Layout.new(
110
+ direction: :vertical,
111
+ constraints: [
112
+ RatatuiRuby::Constraint.fill(1),
113
+ RatatuiRuby::Constraint.length(7),
114
+ ],
115
+ children: [layout, control_panel]
116
+ )
117
+
118
+ RatatuiRuby.draw(full_layout)
119
+ end
120
+
121
+ def build_panel(title, rect, active)
122
+ content = "#{title}\n\n" \
123
+ "Width: #{rect.width}, Height: #{rect.height}\n" \
124
+ "Position: (#{rect.x}, #{rect.y})"
125
+
126
+ RatatuiRuby::Paragraph.new(
127
+ text: content,
128
+ alignment: :center,
129
+ block: RatatuiRuby::Block.new(
130
+ title: "#{title} (#{active ? 'CLICKED' : 'idle'})",
131
+ borders: [:all],
132
+ border_color: active ? "green" : "white"
133
+ )
134
+ )
135
+ end
136
+
137
+ def handle_input
138
+ event = RatatuiRuby.poll_event
139
+ return unless event
140
+
141
+ case event
142
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
143
+ return :quit
144
+ in type: :key, code: "left"
145
+ @left_ratio = [@left_ratio - 10, 10].max
146
+ @message = "Ratio: #{@left_ratio}% / #{100 - @left_ratio}%"
147
+ @last_click = nil
148
+ in type: :key, code: "right"
149
+ @left_ratio = [@left_ratio + 10, 90].min
150
+ @message = "Ratio: #{@left_ratio}% / #{100 - @left_ratio}%"
151
+ @last_click = nil
152
+ in type: :mouse, kind: "down", x: click_x, y: click_y
153
+ handle_click(click_x, click_y)
154
+ else
155
+ @message = "Unhandled: #{event.class} #{event.inspect}"
156
+ nil
157
+ end
158
+ nil
159
+ end
160
+
161
+ def handle_click(x, y)
162
+ if @left_rect.contains?(x, y)
163
+ @last_click = :left
164
+ @message = "Left Panel clicked at (#{x}, #{y})"
165
+ elsif @right_rect.contains?(x, y)
166
+ @last_click = :right
167
+ @message = "Right Panel clicked at (#{x}, #{y})"
168
+ else
169
+ @last_click = nil
170
+ @message = "Clicked outside panels at (#{x}, #{y})"
171
+ end
172
+ end
173
+ end
174
+
175
+ HitTestApp.new.run if __FILE__ == $0
@@ -0,0 +1,7 @@
1
+ class HitTestApp
2
+ # @public
3
+ def self.new: () -> HitTestApp
4
+
5
+ # @public
6
+ def run: () -> void
7
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+ require "ratatui_ruby/test_helper"
9
+ require "minitest/autorun"
10
+ require_relative "app"
11
+
12
+ class TestHitTestExample < Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+
15
+ def setup
16
+ @app = HitTestApp.new
17
+ end
18
+
19
+ def test_initial_render_shows_both_panels
20
+ with_test_terminal do
21
+ inject_key(:q)
22
+ @app.run
23
+
24
+ content = buffer_content
25
+ assert content.any? { |line| line.include?("Left Panel") }
26
+ assert content.any? { |line| line.include?("Right Panel") }
27
+ end
28
+ end
29
+
30
+ def test_left_panel_click
31
+ with_test_terminal do
32
+ # Click in left half at x=10, then quit
33
+ inject_event(RatatuiRuby::Event::Mouse.new(kind: "down", button: "left", x: 10, y: 12))
34
+ inject_key(:q)
35
+
36
+ @app.run
37
+
38
+ content = buffer_content
39
+ # Check both status line and debug message
40
+ assert content.any? { |line| line.include?("Last Click: left") }
41
+ assert content.any? { |line| line.include?("Left Panel clicked") }
42
+ end
43
+ end
44
+
45
+ def test_right_panel_click
46
+ with_test_terminal do
47
+ # Click in right half at x=50, then quit
48
+ inject_event(RatatuiRuby::Event::Mouse.new(kind: "down", button: "left", x: 50, y: 12))
49
+ inject_key(:q)
50
+
51
+ @app.run
52
+
53
+ content = buffer_content
54
+ assert content.any? { |line| line.include?("Last Click: right") }
55
+ assert content.any? { |line| line.include?("Right Panel clicked") }
56
+ end
57
+ end
58
+
59
+ def test_ratio_change_decreases_left
60
+ with_test_terminal do
61
+ # Shrink left panel using left arrow
62
+ inject_keys("left", :q)
63
+ @app.run
64
+
65
+ content = buffer_content
66
+ assert content.any? { |line| line.include?("40%") }
67
+ end
68
+ end
69
+
70
+ def test_ratio_change_increases_left
71
+ with_test_terminal do
72
+ # Expand left panel using right arrow
73
+ inject_keys("right", :q)
74
+ @app.run
75
+
76
+ content = buffer_content
77
+ assert content.any? { |line| line.include?("60%") }
78
+ end
79
+ end
80
+
81
+ def test_ratio_minimum_boundary
82
+ with_test_terminal do
83
+ # Try to go below 10%
84
+ inject_keys("left", "left", "left", "left", "left", :q)
85
+ @app.run
86
+
87
+ content = buffer_content
88
+ assert content.any? { |line| line.include?("10%") }
89
+ end
90
+ end
91
+
92
+ def test_ratio_maximum_boundary
93
+ with_test_terminal do
94
+ # Try to go above 90%
95
+ inject_keys("right", "right", "right", "right", "right", "right", "right", "right", "right", :q)
96
+ @app.run
97
+
98
+ content = buffer_content
99
+ assert content.any? { |line| line.include?("90%") }
100
+ end
101
+ end
102
+ end