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,166 @@
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 "test_helper"
9
+ require_relative "app"
10
+
11
+ class TestRichTextApp < Minitest::Test
12
+ include RatatuiRuby::TestHelper
13
+
14
+ include RatatuiRuby::TestHelper
15
+
16
+ def test_simple_span
17
+ span = RatatuiRuby::Text::Span.new(content: "hello", style: nil)
18
+ assert_equal "hello", span.content
19
+ assert_nil span.style
20
+ end
21
+
22
+ def test_styled_span
23
+ style = RatatuiRuby::Style.new(fg: :red, modifiers: [:bold])
24
+ span = RatatuiRuby::Text::Span.new(content: "error", style:)
25
+ assert_equal "error", span.content
26
+ assert_equal style, span.style
27
+ end
28
+
29
+ def test_span_styled_helper
30
+ style = RatatuiRuby::Style.new(modifiers: [:italic])
31
+ span = RatatuiRuby::Text::Span.styled("italic text", style)
32
+ assert_equal "italic text", span.content
33
+ assert_equal style, span.style
34
+ end
35
+
36
+ def test_span_styled_helper_without_style
37
+ span = RatatuiRuby::Text::Span.styled("plain")
38
+ assert_equal "plain", span.content
39
+ assert_nil span.style
40
+ end
41
+
42
+ def test_simple_line
43
+ spans = [
44
+ RatatuiRuby::Text::Span.new(content: "hello", style: nil),
45
+ RatatuiRuby::Text::Span.new(content: " world", style: nil)
46
+ ]
47
+ line = RatatuiRuby::Text::Line.new(spans:)
48
+ assert_equal spans, line.spans
49
+ assert_nil line.alignment
50
+ end
51
+
52
+ def test_line_with_alignment
53
+ spans = [RatatuiRuby::Text::Span.new(content: "centered", style: nil)]
54
+ line = RatatuiRuby::Text::Line.new(spans:, alignment: :center)
55
+ assert_equal :center, line.alignment
56
+ end
57
+
58
+ def test_line_from_string
59
+ line = RatatuiRuby::Text::Line.from_string("test content")
60
+ assert_equal 1, line.spans.length
61
+ assert_equal "test content", line.spans[0].content
62
+ assert_nil line.spans[0].style
63
+ end
64
+
65
+ def test_line_from_string_with_alignment
66
+ line = RatatuiRuby::Text::Line.from_string("right aligned", alignment: :right)
67
+ assert_equal :right, line.alignment
68
+ end
69
+
70
+ def test_paragraph_with_string
71
+ para = RatatuiRuby::Paragraph.new(text: "simple text")
72
+ assert_equal "simple text", para.text
73
+ end
74
+
75
+ def test_paragraph_with_span
76
+ span = RatatuiRuby::Text::Span.new(content: "bold", style: RatatuiRuby::Style.new(modifiers: [:bold]))
77
+ para = RatatuiRuby::Paragraph.new(text: span)
78
+ assert_equal span, para.text
79
+ end
80
+
81
+ def test_paragraph_with_line
82
+ line = RatatuiRuby::Text::Line.new(
83
+ spans: [
84
+ RatatuiRuby::Text::Span.new(content: "hello ", style: nil),
85
+ RatatuiRuby::Text::Span.new(content: "world", style: RatatuiRuby::Style.new(fg: :green))
86
+ ]
87
+ )
88
+ para = RatatuiRuby::Paragraph.new(text: line)
89
+ assert_equal line, para.text
90
+ end
91
+
92
+ def test_paragraph_with_multiple_lines
93
+ lines = [
94
+ RatatuiRuby::Text::Line.new(
95
+ spans: [RatatuiRuby::Text::Span.new(content: "line 1", style: nil)]
96
+ ),
97
+ RatatuiRuby::Text::Line.new(
98
+ spans: [RatatuiRuby::Text::Span.new(content: "line 2", style: RatatuiRuby::Style.new(fg: :red))]
99
+ )
100
+ ]
101
+ para = RatatuiRuby::Paragraph.new(text: lines)
102
+ assert_equal lines, para.text
103
+ end
104
+
105
+ def test_paragraph_renders_with_rich_text
106
+ with_test_terminal do
107
+ # Test that a paragraph with rich text can be rendered without error
108
+ line = RatatuiRuby::Text::Line.new(
109
+ spans: [
110
+ RatatuiRuby::Text::Span.new(content: "normal ", style: nil),
111
+ RatatuiRuby::Text::Span.new(content: "bold", style: RatatuiRuby::Style.new(modifiers: [:bold])),
112
+ RatatuiRuby::Text::Span.new(content: " text", style: nil)
113
+ ]
114
+ )
115
+ para = RatatuiRuby::Paragraph.new(
116
+ text: line,
117
+ block: RatatuiRuby::Block.new(title: "Test", borders: [:all])
118
+ )
119
+ # Should not raise an error when rendering
120
+ RatatuiRuby.draw(para)
121
+ end
122
+ end
123
+
124
+ def test_paragraph_renders_multiple_rich_lines
125
+ with_test_terminal do
126
+ lines = [
127
+ RatatuiRuby::Text::Line.new(
128
+ spans: [
129
+ RatatuiRuby::Text::Span.new(content: "✓ ", style: RatatuiRuby::Style.new(fg: :green, modifiers: [:bold])),
130
+ RatatuiRuby::Text::Span.new(content: "Complete", style: nil)
131
+ ]
132
+ ),
133
+ RatatuiRuby::Text::Line.new(
134
+ spans: [
135
+ RatatuiRuby::Text::Span.new(content: "✗ ", style: RatatuiRuby::Style.new(fg: :red, modifiers: [:bold])),
136
+ RatatuiRuby::Text::Span.new(content: "Failed", style: nil)
137
+ ]
138
+ )
139
+ ]
140
+ para = RatatuiRuby::Paragraph.new(text: lines)
141
+ # Should not raise an error when rendering
142
+ RatatuiRuby.draw(para)
143
+ end
144
+ end
145
+ end
146
+
147
+ require_relative "app"
148
+
149
+ class TestRichTextApp < Minitest::Test
150
+ include RatatuiRuby::TestHelper
151
+
152
+ include RatatuiRuby::TestHelper
153
+
154
+ def test_app_runs
155
+ with_test_terminal do
156
+ inject_key(:q)
157
+ RichTextApp.new.run
158
+
159
+ assert buffer_content.any? { |line| line.include?("Simple Rich Text") }
160
+ assert buffer_content.any? { |line| line.include?("Status Report") }
161
+ end
162
+ end
163
+ end
164
+
165
+
166
+
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
8
+ require "ratatui_ruby"
9
+
10
+ # Demo: Scrollable Paragraph
11
+ # Shows how to scroll through long text content using arrow keys
12
+ class ScrollTextApp
13
+ def initialize
14
+ @scroll_x = 0
15
+ @scroll_y = 0
16
+
17
+ @lines = (1..100).map { |i| "Line #{i}: This is a long line of text that can be scrolled horizontally" }
18
+ @hotkey_style = RatatuiRuby::Style.new(modifiers: [:bold, :underlined])
19
+ end
20
+
21
+ def run
22
+ RatatuiRuby.run do
23
+ loop do
24
+ draw
25
+ break if handle_input == :quit
26
+ end
27
+ end
28
+ end
29
+
30
+ def render
31
+ draw
32
+ end
33
+
34
+ def handle_input
35
+ case RatatuiRuby.poll_event
36
+ in {type: :key, code: "q"} | {type: :key, code: "c", modifiers: ["ctrl"]}
37
+ :quit
38
+ in type: :key, code: "up"
39
+ @scroll_y = [@scroll_y - 1, 0].max
40
+ in type: :key, code: "down"
41
+ @scroll_y = [@scroll_y + 1, @lines.length].min
42
+ in type: :key, code: "left"
43
+ @scroll_x = [@scroll_x - 1, 0].max
44
+ in type: :key, code: "right"
45
+ @scroll_x = [@scroll_x + 1, 100].min
46
+ else
47
+ nil
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def draw
54
+ text = @lines.join("\n")
55
+
56
+ # Main content
57
+ main_paragraph = RatatuiRuby::Paragraph.new(
58
+ text: text,
59
+ scroll: [@scroll_y, @scroll_x],
60
+ block: RatatuiRuby::Block.new(
61
+ title: "Scrollable Text (#{text.lines.count} lines)",
62
+ borders: [:all]
63
+ )
64
+ )
65
+
66
+ # Bottom control panel
67
+ control_panel = RatatuiRuby::Block.new(
68
+ title: "Controls",
69
+ borders: [:all],
70
+ children: [
71
+ RatatuiRuby::Paragraph.new(
72
+ text: [
73
+ RatatuiRuby::Text::Line.new(spans: [
74
+ RatatuiRuby::Text::Span.new(content: "NAVIGATION (Size: #{main_paragraph.line_count(65535)}x#{main_paragraph.line_width})", style: RatatuiRuby::Style.new(modifiers: [:bold]))
75
+ ]),
76
+ RatatuiRuby::Text::Line.new(spans: [
77
+ RatatuiRuby::Text::Span.new(content: "↑/↓", style: @hotkey_style),
78
+ RatatuiRuby::Text::Span.new(content: ": Vert Scroll (#{@scroll_y}/#{main_paragraph.line_count(65535)}) "),
79
+ RatatuiRuby::Text::Span.new(content: "←/→", style: @hotkey_style),
80
+ RatatuiRuby::Text::Span.new(content: ": Horz Scroll (#{@scroll_x}/#{main_paragraph.line_width}) "),
81
+ RatatuiRuby::Text::Span.new(content: "q", style: @hotkey_style),
82
+ RatatuiRuby::Text::Span.new(content: ": Quit")
83
+ ])
84
+ ]
85
+ )
86
+ ]
87
+ )
88
+
89
+ # Vertical Layout
90
+ layout = RatatuiRuby::Layout.new(
91
+ direction: :vertical,
92
+ constraints: [
93
+ RatatuiRuby::Constraint.fill(1),
94
+ RatatuiRuby::Constraint.length(5),
95
+ ],
96
+ children: [main_paragraph, control_panel]
97
+ )
98
+
99
+ RatatuiRuby.draw(layout)
100
+ end
101
+ end
102
+
103
+ ScrollTextApp.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,7 @@
1
+ class ScrollTextApp
2
+ # @public
3
+ def self.new: () -> ScrollTextApp
4
+
5
+ # @public
6
+ def run: () -> void
7
+ end
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
8
+ require "minitest/autorun"
9
+ require "ratatui_ruby/test_helper"
10
+
11
+ require_relative "app"
12
+
13
+ class TestScrollTextApp < Minitest::Test
14
+ include RatatuiRuby::TestHelper
15
+
16
+ def setup
17
+ @app = ScrollTextApp.new
18
+ end
19
+
20
+ def test_demo_initialization
21
+ assert_instance_of ScrollTextApp, @app
22
+ end
23
+
24
+ def test_initial_rendering
25
+ with_test_terminal do
26
+ # Queue quit
27
+ inject_key(:q)
28
+
29
+ @app.run
30
+
31
+ content = buffer_content
32
+
33
+ # Should show Line 1 somewhere in the output
34
+ assert content.any? { |line| line.include?("Line 1") }
35
+ # Should show controls
36
+ assert content.any? { |line| line.include?("Controls") }
37
+ assert content.any? { |line| line.include?("Vert Scroll (0/102)") }
38
+ end
39
+ end
40
+
41
+ def test_scroll_down
42
+ with_test_terminal do
43
+ # Scroll down then quit
44
+ inject_keys(:down, :q)
45
+
46
+ @app.run
47
+
48
+ content = buffer_content
49
+
50
+ # Should show controls with updated Y position
51
+ assert content.any? { |line| line.include?("Vert Scroll (1/102)") }
52
+ end
53
+ end
54
+
55
+ def test_scroll_right
56
+ with_test_terminal do
57
+ # Scroll right then quit
58
+ inject_keys(:right, :q)
59
+
60
+ @app.run
61
+
62
+ content = buffer_content
63
+
64
+ # Should render without error
65
+ assert content.any? { |line| line.include?("Controls") }
66
+ end
67
+ end
68
+
69
+ def test_scroll_left_at_edge
70
+ with_test_terminal do
71
+ # Scroll left then quit (boundary test)
72
+ inject_keys(:left, :q)
73
+
74
+ @app.run
75
+
76
+ content = buffer_content
77
+
78
+ # Should still show the controls
79
+ assert content.any? { |line| line.include?("Vert Scroll") }
80
+ end
81
+ end
82
+
83
+ def test_scroll_up_at_top
84
+ with_test_terminal do
85
+ # Scroll up then quit (boundary test)
86
+ inject_keys(:up, :q)
87
+
88
+ @app.run
89
+
90
+ content = buffer_content
91
+
92
+ # Should still show the controls
93
+ assert content.any? { |line| line.include?("Controls") }
94
+ end
95
+ end
96
+
97
+ def test_multiple_scrolls
98
+ with_test_terminal do
99
+ # Scroll down and right multiple times
100
+ inject_keys(:down, :down, :right, :right, :right, :q)
101
+
102
+ @app.run
103
+
104
+ content = buffer_content
105
+
106
+ # Should render without error
107
+ assert content.any? { |line| line.include?("Scrollable Text") }
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,143 @@
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
+ # Simple Scrollbar Demo
10
+ class ScrollbarDemoApp
11
+ def initialize
12
+ @scroll_position = 0
13
+ @content_length = 50
14
+ @lines = (1..@content_length).map { |i| "Line #{i}" }
15
+ @orientation_index = 0
16
+ @orientations = [
17
+ :vertical,
18
+ :vertical_right,
19
+ :vertical_left,
20
+ :horizontal,
21
+ :horizontal_bottom,
22
+ :horizontal_top
23
+ ]
24
+ @theme_index = 0
25
+ @themes = [
26
+ {
27
+ name: "Standard",
28
+ track_symbol: nil,
29
+ thumb_symbol: "█",
30
+ track_style: nil,
31
+ thumb_style: nil,
32
+ begin_symbol: nil,
33
+ end_symbol: nil
34
+ },
35
+ {
36
+ name: "Rounded",
37
+ track_symbol: "│",
38
+ thumb_symbol: "┃",
39
+ track_style: { fg: "dark_gray" },
40
+ thumb_style: { fg: "cyan" },
41
+ begin_symbol: "▲",
42
+ end_symbol: "▼"
43
+ },
44
+ {
45
+ name: "ASCII",
46
+ track_symbol: "|",
47
+ thumb_symbol: "#",
48
+ track_style: { fg: "white" },
49
+ thumb_style: { fg: "red" },
50
+ begin_symbol: "^",
51
+ end_symbol: "v"
52
+ },
53
+ {
54
+ name: "Minimal",
55
+ track_symbol: " ",
56
+ thumb_symbol: "▐",
57
+ track_style: nil,
58
+ thumb_style: { fg: "yellow" },
59
+ begin_symbol: nil,
60
+ end_symbol: nil
61
+ }
62
+ ]
63
+ end
64
+
65
+ def run
66
+ RatatuiRuby.run do
67
+ loop do
68
+ draw
69
+ event = RatatuiRuby.poll_event
70
+ break if event == "q" || event == :ctrl_c
71
+
72
+ handle_event(event)
73
+ end
74
+ end
75
+ end
76
+
77
+ private def handle_event(event)
78
+ return unless event
79
+
80
+ if event.mouse?
81
+ case event.kind
82
+ when "scroll_up"
83
+ @scroll_position = [@scroll_position - 1, 0].max
84
+ when "scroll_down"
85
+ @scroll_position = [@scroll_position + 1, @content_length].min
86
+ end
87
+ end
88
+
89
+ if event.key? && event.to_s == "s"
90
+ @theme_index = (@theme_index + 1) % @themes.length
91
+ end
92
+
93
+ if event.key? && event.to_s == "o"
94
+ @orientation_index = (@orientation_index + 1) % @orientations.length
95
+ end
96
+ end
97
+
98
+ private def draw
99
+ # Calculate visible lines based on scroll position
100
+ # In a real app, you'd want to know the height of the available area.
101
+ # For this demo, we'll just show all lines but offset the text.
102
+ visible_lines = @lines[@scroll_position..-1] || []
103
+
104
+ # Paragraph with content
105
+ theme = @themes[@theme_index]
106
+ orientation = @orientations[@orientation_index]
107
+
108
+ p = RatatuiRuby::Paragraph.new(
109
+ text: visible_lines.join("\n"),
110
+ block: RatatuiRuby::Block.new(
111
+ titles: [
112
+ { content: "Scroll with Mouse Wheel | Theme: #{theme[:name]} | Orientation: #{orientation}" },
113
+ { content: "Press 's' to cycle theme, 'o' to cycle orientation", position: :bottom, alignment: :center }
114
+ ],
115
+ borders: [:all]
116
+ )
117
+ )
118
+
119
+ # Scrollbar
120
+ s = RatatuiRuby::Scrollbar.new(
121
+ content_length: @content_length,
122
+ position: @scroll_position,
123
+ orientation: orientation,
124
+ track_symbol: theme[:track_symbol],
125
+ thumb_symbol: theme[:thumb_symbol],
126
+ track_style: theme[:track_style],
127
+ thumb_style: theme[:thumb_style],
128
+ begin_symbol: theme[:begin_symbol],
129
+ end_symbol: theme[:end_symbol]
130
+ )
131
+
132
+ # Use Overlay to stack Scrollbar on top of Paragraph.
133
+ # The Scrollbar will position itself on the correct edge (top/bottom/left/right)
134
+ # based on its orientation, demonstrating that the property works independently of layout.
135
+ overlay = RatatuiRuby::Overlay.new(
136
+ layers: [p, s]
137
+ )
138
+
139
+ RatatuiRuby.draw(overlay)
140
+ end
141
+ end
142
+
143
+ ScrollbarDemoApp.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,7 @@
1
+ class ScrollbarDemoApp
2
+ # @public
3
+ def self.new: () -> ScrollbarDemoApp
4
+
5
+ # @public
6
+ def run: () -> void
7
+ end
@@ -0,0 +1,77 @@
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
+ require "test_helper"
7
+ require_relative "app"
8
+
9
+ class TestScrollbarDemoApp < Minitest::Test
10
+ include RatatuiRuby::TestHelper
11
+
12
+ def setup
13
+ @app = ScrollbarDemoApp.new
14
+ end
15
+
16
+ def test_initial_render
17
+ with_test_terminal do
18
+ # Queue quit event to exit loop immediately
19
+ inject_key(:q)
20
+
21
+ # Stub init_terminal/restore_terminal to use test terminal
22
+ @app.run
23
+
24
+ content = buffer_content
25
+
26
+ # Top border with title (truncated due to small width)
27
+ assert_includes content[0], "Scroll"
28
+ # First line of content
29
+ assert_includes content[1], "Line 1"
30
+ # Scrollbar area (far right)
31
+ assert_equal "█", content[1][-1]
32
+ end
33
+ end
34
+
35
+ def test_scroll_down
36
+ with_test_terminal do
37
+ # Queue scroll down + quit
38
+ inject_event(RatatuiRuby::Event::Mouse.new(kind: "scroll_down", x: 0, y: 0, button: "none"))
39
+ inject_key(:q)
40
+
41
+ @app.run
42
+
43
+ content = buffer_content
44
+
45
+ # Now it should start from Line 2
46
+ assert_includes content[1], "Line 2"
47
+ refute_includes content[1], "Line 1"
48
+ end
49
+ end
50
+
51
+ def test_scroll_up
52
+ with_test_terminal do
53
+ # Queue scroll down + scroll up + quit
54
+ inject_event(RatatuiRuby::Event::Mouse.new(kind: "scroll_down", x: 0, y: 0, button: "none"))
55
+ inject_event(RatatuiRuby::Event::Mouse.new(kind: "scroll_up", x: 0, y: 0, button: "none"))
56
+ inject_key(:q)
57
+
58
+ @app.run
59
+
60
+ content = buffer_content
61
+
62
+ assert_includes content[1], "Line 1"
63
+ end
64
+ end
65
+
66
+ def test_theme_cycling
67
+ with_test_terminal do
68
+ # Queue 's' + quit
69
+ inject_keys(:s, :q)
70
+
71
+ @app.run
72
+
73
+ content = buffer_content
74
+ assert_match(/Theme: Rounded/, content[0])
75
+ end
76
+ end
77
+ end