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,237 @@
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 TestListDemoApp < Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+
15
+ def setup
16
+ @app = ListDemoApp.new
17
+ end
18
+
19
+ def test_render_initial_state
20
+ with_test_terminal do
21
+ inject_key(:q)
22
+ @app.run
23
+
24
+ content = buffer_content
25
+ assert content.any? { |line| line.include?("List Widget Demo") }
26
+ assert content.any? { |line| line.include?("Large List") }
27
+ assert content.any? { |line| line.include?("Item 1") }
28
+ end
29
+ end
30
+
31
+ def test_navigate_down
32
+ with_test_terminal do
33
+ inject_keys(:down, :q)
34
+ @app.run
35
+
36
+ content = buffer_content
37
+ # Initial large list, down selects first item (Item 1)
38
+ assert content.any? { |line| line.include?(">> Item 1") }
39
+ end
40
+ end
41
+
42
+ def test_navigate_up_wraps
43
+ with_test_terminal do
44
+ inject_keys(:up, :q)
45
+ @app.run
46
+
47
+ content = buffer_content
48
+ # Should wrap to last item of Large List (Item 200)
49
+ assert content.any? { |line| line.include?(">> Item 200") }
50
+ end
51
+ end
52
+
53
+ def test_toggle_selection_on
54
+ with_test_terminal do
55
+ inject_keys(:x, :q)
56
+ @app.run
57
+
58
+ content = buffer_content
59
+ # Selects index 0 (Item 1)
60
+ assert content.any? { |line| line.include?(">> Item 1") }
61
+ assert content.any? { |line| line.include?("Selection: 0") }
62
+ end
63
+ end
64
+
65
+ def test_toggle_selection_off
66
+ with_test_terminal do
67
+ inject_keys(:x, :x, :q)
68
+ @app.run
69
+
70
+ content = buffer_content
71
+ refute content.any? { |line| line.include?(">> ") && line.include?("Item 1") }
72
+ assert content.any? { |line| line.include?("Selection: none") }
73
+ end
74
+ end
75
+
76
+ def test_cycle_item_set
77
+ with_test_terminal do
78
+ inject_keys(:i, :q)
79
+ @app.run
80
+
81
+ content = buffer_content
82
+ # Cycles to next set: Colors
83
+ assert content.any? { |line| line.include?("Colors") }
84
+ assert content.any? { |line| line.include?("Red") }
85
+ end
86
+ end
87
+
88
+ def test_cycle_item_set_multiple_times
89
+ with_test_terminal do
90
+ # 0=Large, 1=Colors, 2=Fruits, 3=Programming
91
+ inject_keys(:i, :i, :i, :q)
92
+ @app.run
93
+
94
+ content = buffer_content
95
+ assert content.any? { |line| line.include?("Programming") }
96
+ assert content.any? { |line| line.include?("Ruby") }
97
+ end
98
+ end
99
+
100
+ def test_cycle_highlight_style
101
+ with_test_terminal do
102
+ inject_keys(:h, :q)
103
+ @app.run
104
+
105
+ content = buffer_content
106
+ assert content.any? { |line| line.include?("Yellow on Black") }
107
+ end
108
+ end
109
+
110
+ def test_cycle_highlight_symbol
111
+ with_test_terminal do
112
+ inject_keys(:x, :y, :q)
113
+ @app.run
114
+
115
+ content = buffer_content
116
+ assert content.any? { |line| line.include?("▶") }
117
+ end
118
+ end
119
+
120
+ def test_cycle_direction
121
+ with_test_terminal do
122
+ inject_keys(:d, :q)
123
+ @app.run
124
+
125
+ content = buffer_content
126
+ assert content.any? { |line| line.include?("Bottom to Top") }
127
+ end
128
+ end
129
+
130
+ def test_cycle_spacing
131
+ with_test_terminal do
132
+ inject_keys(:s, :q)
133
+ @app.run
134
+
135
+ content = buffer_content
136
+ assert content.any? { |line| line.include?("List Widget Demo") }
137
+ end
138
+ end
139
+
140
+ def test_spacing_always_shows_column
141
+ with_test_terminal do
142
+ # Need to enable always spacing (:s) on Item list
143
+ inject_keys(:s, :q)
144
+ @app.run
145
+
146
+ content = buffer_content
147
+ # With :always spacing, unselected items are indented
148
+ assert content.any? { |line| line.include?(" Item 1") }
149
+ end
150
+ end
151
+
152
+ def test_cycle_base_style
153
+ with_test_terminal do
154
+ inject_keys(:b, :q)
155
+ @app.run
156
+
157
+ content = buffer_content
158
+ assert content.any? { |line| line.include?("List Widget Demo") }
159
+ end
160
+ end
161
+
162
+ def test_cycle_repeat_symbol
163
+ with_test_terminal do
164
+ inject_keys(:r, :q)
165
+ @app.run
166
+
167
+ content = buffer_content
168
+ assert content.any? { |line| line.include?("List Widget Demo") }
169
+ end
170
+ end
171
+
172
+ def test_selection_resets_on_item_set_change
173
+ with_test_terminal do
174
+ inject_keys(:x, :down, :i, :q)
175
+ @app.run
176
+
177
+ content = buffer_content
178
+ # After changing item set to Colors, selection should reset
179
+ assert content.any? { |line| line.include?("Selection: none") }
180
+ end
181
+ end
182
+
183
+ def test_quit_with_ctrl_c
184
+ with_test_terminal do
185
+ inject_keys("c", :q)
186
+ @app.run
187
+
188
+ content = buffer_content
189
+ assert content.any? { |line| line.include?("List Widget Demo") }
190
+ end
191
+ end
192
+
193
+ def test_demo_title_always_visible
194
+ with_test_terminal do
195
+ inject_keys(:i, :h, :y, :d, :s, :b, :r, :q)
196
+ @app.run
197
+
198
+ content = buffer_content
199
+ assert content.any? { |line| line.include?("List Widget Demo") }
200
+ end
201
+ end
202
+
203
+ def test_cycle_scroll_padding
204
+ with_test_terminal do
205
+ inject_keys(:p, :q)
206
+ @app.run
207
+
208
+ content = buffer_content
209
+ # 0=None, 1=1 item
210
+ assert content.any? { |line| line.include?("Scroll Padding (1 item)") }
211
+ end
212
+ end
213
+
214
+ def test_scroll_padding_changes_display
215
+ with_test_terminal do
216
+ # Switch to Colors (i), Toggle select (x), scroll down, toggle padding (p)
217
+ inject_keys(:i, :x, :down, :p, :q)
218
+ @app.run
219
+
220
+ content = buffer_content
221
+ assert content.any? { |line| line.include?("Colors") }
222
+ # Selected index 1 (Orange) of Colors
223
+ assert content.any? { |line| line.include?(">> Orange") }
224
+ end
225
+ end
226
+
227
+ def test_multiple_navigation_and_options
228
+ with_test_terminal do
229
+ # Interactive usage smoke test
230
+ inject_keys(:down, :h, :down, :y, :up, :b, :q)
231
+ @app.run
232
+
233
+ content = buffer_content
234
+ assert content.any? { |line| line.include?("List Widget Demo") }
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,140 @@
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
+ # Styled List Example
10
+ # Demonstrates advanced styling options for the List widget.
11
+ class ListStylesApp
12
+ attr_reader :selected_index, :highlight_spacing
13
+
14
+ def initialize
15
+ @items = ["Item 1", "Item 2", "Item 3"]
16
+ @selected_index = nil
17
+
18
+ @directions = [
19
+ { name: "Top to Bottom", direction: :top_to_bottom },
20
+ { name: "Bottom to Top", direction: :bottom_to_top }
21
+ ]
22
+ @direction_index = 0
23
+
24
+ @highlight_spacings = [
25
+ { name: "When Selected", spacing: :when_selected },
26
+ { name: "Always", spacing: :always },
27
+ { name: "Never", spacing: :never }
28
+ ]
29
+ @highlight_spacing_index = 0
30
+
31
+ @repeat_modes = [
32
+ { name: "Off", repeat: false },
33
+ { name: "On", repeat: true }
34
+ ]
35
+ @repeat_index = 0
36
+
37
+ @hotkey_style = RatatuiRuby::Style.new(modifiers: [:bold, :underlined])
38
+ end
39
+
40
+ def run
41
+ RatatuiRuby.run do
42
+ loop do
43
+ render
44
+ break if handle_input == :quit
45
+ sleep 0.05
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def render
53
+ selection_label = @selected_index.nil? ? "none" : @selected_index.to_s
54
+ direction_config = @directions[@direction_index]
55
+ spacing_config = @highlight_spacings[@highlight_spacing_index]
56
+ repeat_config = @repeat_modes[@repeat_index]
57
+
58
+ # Main content
59
+ main_list = RatatuiRuby::List.new(
60
+ items: @items,
61
+ selected_index: @selected_index,
62
+ style: RatatuiRuby::Style.new(fg: :white, bg: :black),
63
+ highlight_style: RatatuiRuby::Style.new(fg: :blue, bg: :white, modifiers: [:bold]),
64
+ highlight_symbol: ">> ",
65
+ repeat_highlight_symbol: repeat_config[:repeat],
66
+ highlight_spacing: spacing_config[:spacing],
67
+ direction: direction_config[:direction],
68
+ block: RatatuiRuby::Block.new(
69
+ title: "Items",
70
+ borders: [:all]
71
+ )
72
+ )
73
+
74
+ # Bottom control panel
75
+ control_panel = RatatuiRuby::Block.new(
76
+ title: "Controls",
77
+ borders: [:all],
78
+ children: [
79
+ RatatuiRuby::Paragraph.new(
80
+ text: [
81
+ RatatuiRuby::Text::Line.new(spans: [
82
+ RatatuiRuby::Text::Span.new(content: "↑/↓", style: @hotkey_style),
83
+ RatatuiRuby::Text::Span.new(content: ": Select (#{selection_label}) "),
84
+ RatatuiRuby::Text::Span.new(content: "x", style: @hotkey_style),
85
+ RatatuiRuby::Text::Span.new(content: ": Toggle Selection "),
86
+ RatatuiRuby::Text::Span.new(content: "q", style: @hotkey_style),
87
+ RatatuiRuby::Text::Span.new(content: ": Quit")
88
+ ]),
89
+ RatatuiRuby::Text::Line.new(spans: [
90
+ RatatuiRuby::Text::Span.new(content: "d", style: @hotkey_style),
91
+ RatatuiRuby::Text::Span.new(content: ": Direction (#{direction_config[:name]}) "),
92
+ RatatuiRuby::Text::Span.new(content: "s", style: @hotkey_style),
93
+ RatatuiRuby::Text::Span.new(content: ": Spacing (#{spacing_config[:name]})")
94
+ ]),
95
+ RatatuiRuby::Text::Line.new(spans: [
96
+ RatatuiRuby::Text::Span.new(content: "r", style: @hotkey_style),
97
+ RatatuiRuby::Text::Span.new(content: ": Repeat Symbol (#{repeat_config[:name]})")
98
+ ])
99
+ ]
100
+ )
101
+ ]
102
+ )
103
+
104
+ # Vertical Layout
105
+ layout = RatatuiRuby::Layout.new(
106
+ direction: :vertical,
107
+ constraints: [
108
+ RatatuiRuby::Constraint.fill(1),
109
+ RatatuiRuby::Constraint.length(5),
110
+ ],
111
+ children: [main_list, control_panel]
112
+ )
113
+
114
+ RatatuiRuby.draw(layout)
115
+ end
116
+
117
+ def handle_input
118
+ case RatatuiRuby.poll_event
119
+ in {type: :key, code: "q"} | {type: :key, code: "c", modifiers: ["ctrl"]}
120
+ :quit
121
+ in type: :key, code: "up"
122
+ @selected_index = (@selected_index || 0) - 1
123
+ @selected_index = @items.size - 1 if @selected_index.negative?
124
+ in type: :key, code: "down"
125
+ @selected_index = ((@selected_index || -1) + 1) % @items.size
126
+ in type: :key, code: "d"
127
+ @direction_index = (@direction_index + 1) % @directions.size
128
+ in type: :key, code: "s"
129
+ @highlight_spacing_index = (@highlight_spacing_index + 1) % @highlight_spacings.size
130
+ in type: :key, code: "r"
131
+ @repeat_index = (@repeat_index + 1) % @repeat_modes.size
132
+ in type: :key, code: "x"
133
+ @selected_index = @selected_index.nil? ? 0 : nil
134
+ else
135
+ nil
136
+ end
137
+ end
138
+ end
139
+
140
+ ListStylesApp.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,7 @@
1
+ class ListStylesApp
2
+ # @public
3
+ def self.new: () -> ListStylesApp
4
+
5
+ # @public
6
+ def run: () -> void
7
+ end
@@ -0,0 +1,157 @@
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 TestListStylesExample < Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+
15
+ def setup
16
+ @app = ListStylesApp.new
17
+ end
18
+
19
+ def test_render_initial_state_no_selection
20
+ with_test_terminal do
21
+ inject_key(:q)
22
+ @app.run
23
+
24
+ # Default is :when_selected with no selection, so no highlight symbol column
25
+ content = buffer_content
26
+ assert content.any? { |line| line.include?("Item 1") }
27
+ assert content.any? { |line| line.include?("Item 2") }
28
+ refute content.any? { |line| line.include?(">>") }
29
+ end
30
+ end
31
+
32
+ def test_toggle_selection_on
33
+ with_test_terminal do
34
+ # Press x to toggle on, then quit
35
+ inject_keys(:x, :q)
36
+ @app.run
37
+
38
+ content = buffer_content
39
+ assert content.any? { |line| line.include?(">> Item 1") }
40
+ end
41
+ end
42
+
43
+ def test_toggle_selection_off
44
+ with_test_terminal do
45
+ # Press x to toggle on, x again to toggle off, then quit
46
+ inject_keys(:x, :x, :q)
47
+ @app.run
48
+
49
+ content = buffer_content
50
+ refute content.any? { |line| line.include?(">>") }
51
+ end
52
+ end
53
+
54
+ def test_navigation_selects_and_moves
55
+ with_test_terminal do
56
+ inject_keys(:down, :q)
57
+ @app.run
58
+
59
+ content = buffer_content
60
+ assert content.any? { |line| line.include?(">> Item 1") }
61
+ end
62
+ end
63
+
64
+ def test_navigation_down_twice
65
+ with_test_terminal do
66
+ inject_keys(:down, :down, :q)
67
+ @app.run
68
+
69
+ content = buffer_content
70
+ assert content.any? { |line| line.include?(">> Item 2") }
71
+ end
72
+ end
73
+
74
+ def test_quit
75
+ with_test_terminal do
76
+ inject_key(:q)
77
+ @app.run
78
+ end
79
+ end
80
+
81
+ def test_toggle_highlight_spacing_to_always
82
+ with_test_terminal do
83
+ # 's' cycles: when_selected -> always -> never
84
+ inject_keys(:s, :q)
85
+ @app.run
86
+
87
+ content = buffer_content
88
+ assert content.any? { |line| line.include?("Always") }
89
+ end
90
+ end
91
+
92
+ def test_toggle_highlight_spacing_to_never
93
+ with_test_terminal do
94
+ inject_keys(:s, :s, :q)
95
+ @app.run
96
+
97
+ content = buffer_content
98
+ assert content.any? { |line| line.include?("Never") }
99
+ end
100
+ end
101
+
102
+ def test_toggle_highlight_spacing_back_to_when_selected
103
+ with_test_terminal do
104
+ inject_keys(:s, :s, :s, :q)
105
+ @app.run
106
+
107
+ content = buffer_content
108
+ assert content.any? { |line| line.include?("When Selected") }
109
+ end
110
+ end
111
+
112
+ def test_direction_cycling
113
+ with_test_terminal do
114
+ inject_keys(:d, :q)
115
+ @app.run
116
+
117
+ content = buffer_content
118
+ assert content.any? { |line| line.include?("Bottom to Top") }
119
+ end
120
+ end
121
+
122
+ def test_spacing_always_shows_column_without_selection
123
+ with_test_terminal do
124
+ # Set spacing to :always
125
+ inject_keys(:s, :q)
126
+ @app.run
127
+
128
+ content = buffer_content
129
+ # With :always, spacing column is shown even without selection
130
+ assert content.any? { |line| line.include?(" Item 1") }
131
+ end
132
+ end
133
+
134
+ def test_repeat_highlight_symbol_toggle
135
+ with_test_terminal do
136
+ # Press r to toggle repeat mode, then quit
137
+ inject_keys(:r, :q)
138
+ @app.run
139
+
140
+ content = buffer_content
141
+ assert content.any? { |line| line.include?("Repeat Symbol") }
142
+ assert content.any? { |line| line.include?("On") }
143
+ end
144
+ end
145
+
146
+ def test_repeat_highlight_symbol_cycles_back
147
+ with_test_terminal do
148
+ # Press r twice to cycle back to Off
149
+ inject_keys(:r, :r, :q)
150
+ @app.run
151
+
152
+ content = buffer_content
153
+ assert content.any? { |line| line.include?("Repeat Symbol") }
154
+ assert content.any? { |line| line.include?("Off") }
155
+ end
156
+ end
157
+ end
@@ -3,7 +3,7 @@
3
3
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
7
  require "ratatui_ruby"
8
8
 
9
9
  class LoginFormApp
@@ -16,17 +16,16 @@ class LoginFormApp
16
16
  end
17
17
 
18
18
  def run
19
- RatatuiRuby.init_terminal
20
- begin
19
+ RatatuiRuby.run do
21
20
  loop do
22
21
  render
23
22
  break if handle_input == :quit
24
23
  end
25
- ensure
26
- RatatuiRuby.restore_terminal
27
24
  end
28
25
  end
29
26
 
27
+ private
28
+
30
29
  def render
31
30
  # 1. Base Layer Construction
32
31
  # We want a cursor relative to the paragraph.
@@ -44,7 +43,7 @@ class LoginFormApp
44
43
  RatatuiRuby::Paragraph.new(
45
44
  text: "#{PREFIX}#{@username}#{SUFFIX}",
46
45
  block: RatatuiRuby::Block.new(borders: :all, title: "Login Form"),
47
- align: :left
46
+ alignment: :left
48
47
  ),
49
48
  RatatuiRuby::Cursor.new(x: cursor_x, y: cursor_y),
50
49
  ])
@@ -63,7 +62,7 @@ class LoginFormApp
63
62
  text: "Login Successful!\nPress 'q' to quit.",
64
63
  style: RatatuiRuby::Style.new(fg: :green, bg: :black),
65
64
  block: RatatuiRuby::Block.new(borders: :all),
66
- align: :center,
65
+ alignment: :center,
67
66
  wrap: true
68
67
  ),
69
68
  width_percent: 30,
@@ -85,16 +84,13 @@ class LoginFormApp
85
84
  event = RatatuiRuby.poll_event
86
85
  return unless event
87
86
 
88
- if event[:type] == :key
87
+ if event.key?
89
88
  if @show_popup
90
- # If popup is shown, any key (or specifically q) quits?
91
- # "The Center widget contains a Green Box saying 'Login Successful! Press q.'"
92
- if event[:code] == "q"
93
- :quit
94
- end
89
+ return :quit if event == "q" || event == :ctrl_c
95
90
  else
96
91
  # Login Form Input
97
- case event[:code]
92
+ return :quit if event == :ctrl_c
93
+ case event.code
98
94
  when "enter"
99
95
  @show_popup = true
100
96
  when "backspace"
@@ -103,8 +99,8 @@ class LoginFormApp
103
99
  :quit
104
100
  else
105
101
  # Simple text input
106
- if event[:code].length == 1
107
- @username += event[:code]
102
+ if event.text? && !event.ctrl? && !event.alt?
103
+ @username += event.code
108
104
  end
109
105
  end
110
106
  end
@@ -0,0 +1,7 @@
1
+ class LoginFormApp
2
+ # @public
3
+ def self.new: () -> LoginFormApp
4
+
5
+ # @public
6
+ def run: () -> void
7
+ end
@@ -0,0 +1,51 @@
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 TestLoginForm < Minitest::Test
13
+ include RatatuiRuby::TestHelper
14
+
15
+ def setup
16
+ @app = LoginFormApp.new
17
+ end
18
+
19
+ def test_render_initial_state
20
+ with_test_terminal do
21
+ # Queue quit
22
+ inject_keys(:esc)
23
+
24
+ @app.run
25
+
26
+ assert buffer_content.any? { |line| line.include?("Enter Username:") }
27
+ end
28
+ end
29
+
30
+ def test_input_handling
31
+ with_test_terminal do
32
+ # Type 'a' then quit
33
+ inject_keys("a", :esc)
34
+
35
+ @app.run
36
+
37
+ assert buffer_content.any? { |line| line.include?("Enter Username: [ a ]") }
38
+ end
39
+ end
40
+
41
+ def test_popup_flow
42
+ with_test_terminal do
43
+ # Enter username 'user', press Enter, then quit
44
+ inject_keys("u", "s", "e", "r", :enter, :q)
45
+
46
+ @app.run
47
+
48
+ assert buffer_content.any? { |line| line.include?("Successful!") }
49
+ end
50
+ end
51
+ end