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,90 @@
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
+ module RatatuiRuby
7
+ # Namespace for rich text components (Span and Line).
8
+ # Distinct from canvas shapes and other Line usages.
9
+ module Text
10
+ # A styled string fragment.
11
+ #
12
+ # Text is rarely uniform. You need to bold a keyword, colorize an error, or dim a timestamp.
13
+ #
14
+ # This class attaches style to content. It pairs a string with visual attributes.
15
+ #
16
+ # combine spans into a {Line} to create rich text.
17
+ #
18
+ # === Examples
19
+ #
20
+ # Text::Span.new(content: "Error", style: Style.new(fg: :red, modifiers: [:bold]))
21
+ class Span < Data.define(:content, :style)
22
+ ##
23
+ # :attr_reader: content
24
+ # The text content.
25
+
26
+ ##
27
+ # :attr_reader: style
28
+ # The style to apply.
29
+
30
+ # Creates a new Span.
31
+ #
32
+ # [content] String.
33
+ # [style] Style object (optional).
34
+ def initialize(content:, style: nil)
35
+ super
36
+ end
37
+
38
+ # Concise helper for styling.
39
+ #
40
+ # Text::Span.styled("Bold", Style.new(modifiers: [:bold]))
41
+ def self.styled(content, style = nil)
42
+ new(content:, style:)
43
+ end
44
+ end
45
+
46
+ # A sequence of styled spans.
47
+ #
48
+ # Words form sentences. Spans form lines.
49
+ #
50
+ # This class composes multiple {Span} objects into a single horizontal row of text.
51
+ # It handles the layout of rich text fragments within the flow of a paragraph.
52
+ #
53
+ # Use it to build multi-colored headers, status messages, or log entries.
54
+ #
55
+ # === Examples
56
+ #
57
+ # Text::Line.new(
58
+ # spans: [
59
+ # Text::Span.styled("User: ", Style.new(modifiers: [:bold])),
60
+ # Text::Span.styled("kerrick", Style.new(fg: :blue))
61
+ # ]
62
+ # )
63
+ class Line < Data.define(:spans, :alignment)
64
+ ##
65
+ # :attr_reader: spans
66
+ # Array of Span objects.
67
+
68
+ ##
69
+ # :attr_reader: alignment
70
+ # Alignment within the container.
71
+ #
72
+ # <tt>:left</tt>, <tt>:center</tt>, or <tt>:right</tt>.
73
+
74
+ # Creates a new Line.
75
+ #
76
+ # [spans] Array of Span objects (or Strings).
77
+ # [alignment] Symbol (optional).
78
+ def initialize(spans: [], alignment: nil)
79
+ super
80
+ end
81
+
82
+ # Creates a simple line from a string.
83
+ #
84
+ # Text::Line.from_string("Hello")
85
+ def self.from_string(content, alignment: nil)
86
+ new(spans: [Span.new(content:, style: nil)], alignment:)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,146 @@
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
+ module RatatuiRuby
7
+ # Manages the terminal lifecycle and provides a concise API for the render loop.
8
+ #
9
+ # Writing a TUI loop involves repetitive boilerplate. You constantly instantiate widgets (<tt>RatatuiRuby::Paragraph.new</tt>) and call global methods (<tt>RatatuiRuby.draw</tt>). This is verbose and hard to read.
10
+ #
11
+ # The Session object simplifies this. It acts as a factory and a facade. It provides short helper methods for every widget and delegates core commands to the main module.
12
+ #
13
+ # Use it within <tt>RatatuiRuby.run</tt> to build your interface cleanly.
14
+ #
15
+ # == Available Methods
16
+ #
17
+ # The session dynamically defines factory methods for all RatatuiRuby constants.
18
+ #
19
+ # * <tt>draw(node)</tt> -> Delegates to <tt>RatatuiRuby.draw</tt>
20
+ # * <tt>poll_event</tt> -> Delegates to <tt>RatatuiRuby.poll_event</tt>
21
+ #
22
+ # === Widget Factories
23
+ #
24
+ # The session acts as a dynamic factory. It creates a helper method for **every** class defined in the `RatatuiRuby` module.
25
+ #
26
+ # **The Rule:**
27
+ # To instantiate a class like `RatatuiRuby::SomeWidget`, call `tui.some_widget(...)`.
28
+ #
29
+ # **Common Examples:**
30
+ # * <tt>paragraph(...)</tt> -> <tt>RatatuiRuby::Paragraph.new(...)</tt>
31
+ # * <tt>block(...)</tt> -> <tt>RatatuiRuby::Block.new(...)</tt>
32
+ # * <tt>layout(...)</tt> -> <tt>RatatuiRuby::Layout.new(...)</tt>
33
+ # * <tt>list(...)</tt> -> <tt>RatatuiRuby::List.new(...)</tt>
34
+ # * <tt>table(...)</tt> -> <tt>RatatuiRuby::Table.new(...)</tt>
35
+ # * <tt>style(...)</tt> -> <tt>RatatuiRuby::Style.new(...)</tt>
36
+ #
37
+ # If a new class is added to the library, it is automatically available here.
38
+ #
39
+ # === Nested Helpers
40
+ #
41
+ # * <tt>text_span(...)</tt> -> <tt>RatatuiRuby::Text::Span.new(...)</tt>
42
+ # * <tt>text_line(...)</tt> -> <tt>RatatuiRuby::Text::Line.new(...)</tt>
43
+ #
44
+ # === Examples
45
+ #
46
+ # ==== Basic Usage (Recommended)
47
+ #
48
+ # RatatuiRuby.run do |tui|
49
+ # loop do
50
+ # tui.draw \
51
+ # tui.paragraph \
52
+ # text: "Hello, Ratatui! Press 'q' to quit.",
53
+ # alignment: :center,
54
+ # block: tui.block(
55
+ # title: "My Ruby TUI App",
56
+ # borders: [:all],
57
+ # border_color: "cyan"
58
+ # )
59
+ # event = tui.poll_event
60
+ # break if event == "q" || event == :ctrl_c
61
+ # end
62
+ # end
63
+ #
64
+ # ==== Raw API (Verbose)
65
+ #
66
+ # RatatuiRuby.run do
67
+ # loop do
68
+ # RatatuiRuby.draw \
69
+ # RatatuiRuby::Paragraph.new(
70
+ # text: "Hello, Ratatui! Press 'q' to quit.",
71
+ # alignment: :center,
72
+ # block: RatatuiRuby::Block.new(
73
+ # title: "My Ruby TUI App",
74
+ # borders: [:all],
75
+ # border_color: "cyan"
76
+ # )
77
+ # )
78
+ # event = RatatuiRuby.poll_event
79
+ # break if event == "q" || event == :ctrl_c
80
+ # end
81
+ # end
82
+ #
83
+ # ==== Mixed Usage (Flexible)
84
+ #
85
+ # RatatuiRuby.run do |tui|
86
+ # loop do
87
+ # RatatuiRuby.draw \
88
+ # tui.paragraph \
89
+ # text: "Hello, Ratatui! Press 'q' to quit.",
90
+ # alignment: :center,
91
+ # block: tui.block(
92
+ # title: "My Ruby TUI App",
93
+ # borders: [:all],
94
+ # border_color: "cyan"
95
+ # )
96
+ # event = RatatuiRuby.poll_event
97
+ # break if event == "q" || event == :ctrl_c
98
+ # end
99
+ # end
100
+ class Session
101
+ # Wrap methods directly
102
+ RatatuiRuby.singleton_methods(false).each do |method_name|
103
+ define_method(method_name) do |*args, **kwargs, &block|
104
+ RatatuiRuby.public_send(method_name, *args, **kwargs, &block)
105
+ end
106
+ end
107
+
108
+ # Wrap classes as snake_case factories
109
+ RatatuiRuby.constants.each do |const_name|
110
+ next if const_name == :Buffer
111
+
112
+ klass = RatatuiRuby.const_get(const_name)
113
+ next unless klass.is_a?(Class)
114
+
115
+ method_name = const_name.to_s
116
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
117
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
118
+ .downcase
119
+
120
+ define_method(method_name) do |*args, **kwargs, &block|
121
+ klass.new(*args, **kwargs, &block)
122
+ end
123
+ end
124
+
125
+ # Wrap nested module classes with prefixed names (e.g., shape_line, text_span)
126
+ { Shape: :shape, Text: :text }.each do |mod_name, prefix|
127
+ next unless RatatuiRuby.const_defined?(mod_name)
128
+
129
+ mod = RatatuiRuby.const_get(mod_name)
130
+ mod.constants.each do |const_name|
131
+ klass = mod.const_get(const_name)
132
+ next unless klass.is_a?(Class)
133
+
134
+ class_snake = const_name.to_s
135
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
136
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
137
+ .downcase
138
+ method_name = "#{prefix}_#{class_snake}"
139
+
140
+ define_method(method_name) do |*args, **kwargs, &block|
141
+ klass.new(*args, **kwargs, &block)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
+ require "timeout"
3
+ require "minitest/mock"
4
+
5
+
2
6
 
3
7
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
8
  # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -8,7 +12,7 @@ module RatatuiRuby
8
12
  # Helpers for testing RatatuiRuby applications.
9
13
  #
10
14
  # This module provides methods to set up a test terminal, capture buffer content,
11
- # and check cursor position, making it easier to write unit tests for your TUI apps.
15
+ # and inject events, making it easier to write unit tests for your TUI apps.
12
16
  #
13
17
  # == Usage
14
18
  #
@@ -18,24 +22,50 @@ module RatatuiRuby
18
22
  # include RatatuiRuby::TestHelper
19
23
  #
20
24
  # def test_rendering
21
- # with_test_terminal(width: 80, height: 24) do
25
+ # with_test_terminal(80, 24) do
22
26
  # # ... render your app ...
23
27
  # assert_includes buffer_content, "Hello World"
24
28
  # end
25
29
  # end
30
+ #
31
+ # def test_key_handling
32
+ # inject_event(RatatuiRuby::Event::Key.new(code: "q"))
33
+ # result = @app.handle_input
34
+ # assert_equal :quit, result
35
+ # end
26
36
  # end
27
37
  module TestHelper
28
38
  ##
29
39
  # Initializes a test terminal context with specified dimensions.
30
40
  # Restores the original terminal state after the block executes.
31
41
  #
32
- # +width+:: width of the test terminal (default: 20)
33
- # +height+:: height of the test terminal (default: 10)
42
+ # +width+:: width of the test terminal (default: 80)
43
+ # +height+:: height of the test terminal (default: 24)
44
+ #
45
+ # +timeout+:: maximum execution time in seconds (default: 2). Pass nil to disable.
34
46
  #
35
47
  # If a block is given, it is executed within the test terminal context.
36
- def with_test_terminal(width = 20, height = 10)
48
+ def with_test_terminal(width = 80, height = 24, timeout: 2)
37
49
  RatatuiRuby.init_test_terminal(width, height)
38
- yield
50
+ # Flush any lingering events from previous tests
51
+ while RatatuiRuby.poll_event; end
52
+
53
+ RatatuiRuby.stub :init_terminal, nil do
54
+ RatatuiRuby.stub :restore_terminal, nil do
55
+ begin
56
+ @_ratatui_test_terminal_active = true
57
+ if timeout
58
+ Timeout.timeout(timeout) do
59
+ yield
60
+ end
61
+ else
62
+ yield
63
+ end
64
+ ensure
65
+ @_ratatui_test_terminal_active = false
66
+ end
67
+ end
68
+ end
39
69
  ensure
40
70
  RatatuiRuby.restore_terminal
41
71
  end
@@ -61,15 +91,128 @@ module RatatuiRuby
61
91
  end
62
92
 
63
93
  ##
64
- # Injects a mock event into the event queue for testing purposes.
94
+ # Injects an event into the event queue for testing.
95
+ #
96
+ # Pass any RatatuiRuby::Event object. The event will be returned by
97
+ # the next call to RatatuiRuby.poll_event.
98
+ #
99
+ # Raises a +RuntimeError+ if called outside of a +with_test_terminal+ block.
65
100
  #
66
- # +event_type+:: "key" or "mouse"
67
- # +data+:: a Hash containing event data
101
+ # == Examples
68
102
  #
69
- # inject_event("key", { code: "a" })
70
- # inject_event("mouse", { kind: "down", x: 0, y: 0 })
71
- def inject_event(event_type, data)
72
- RatatuiRuby.inject_test_event(event_type, data)
103
+ # with_test_terminal do
104
+ # # Key events
105
+ # inject_event(RatatuiRuby::Event::Key.new(code: "q"))
106
+ # inject_event(RatatuiRuby::Event::Key.new(code: "s", modifiers: ["ctrl"]))
107
+ #
108
+ # # Mouse events
109
+ # inject_event(RatatuiRuby::Event::Mouse.new(kind: "down", button: "left", x: 10, y: 5))
110
+ #
111
+ # # Resize events
112
+ # inject_event(RatatuiRuby::Event::Resize.new(width: 120, height: 40))
113
+ #
114
+ # # Paste events
115
+ # inject_event(RatatuiRuby::Event::Paste.new(content: "Hello"))
116
+ #
117
+ # # Focus events
118
+ # inject_event(RatatuiRuby::Event::FocusGained.new)
119
+ # inject_event(RatatuiRuby::Event::FocusLost.new)
120
+ # end
121
+ def inject_event(event)
122
+ unless @_ratatui_test_terminal_active
123
+ raise "Events must be injected inside a `with_test_terminal` block. " \
124
+ "Calling this method outside the block causes a race condition where the event " \
125
+ "is flushed before the application starts."
126
+ end
127
+
128
+ case event
129
+ when RatatuiRuby::Event::Key
130
+ RatatuiRuby.inject_test_event("key", { code: event.code, modifiers: event.modifiers })
131
+ when RatatuiRuby::Event::Mouse
132
+ RatatuiRuby.inject_test_event("mouse", {
133
+ kind: event.kind,
134
+ button: event.button,
135
+ x: event.x,
136
+ y: event.y,
137
+ modifiers: event.modifiers
138
+ })
139
+ when RatatuiRuby::Event::Resize
140
+ RatatuiRuby.inject_test_event("resize", { width: event.width, height: event.height })
141
+ when RatatuiRuby::Event::Paste
142
+ RatatuiRuby.inject_test_event("paste", { content: event.content })
143
+ when RatatuiRuby::Event::FocusGained
144
+ RatatuiRuby.inject_test_event("focus_gained", {})
145
+ when RatatuiRuby::Event::FocusLost
146
+ RatatuiRuby.inject_test_event("focus_lost", {})
147
+ else
148
+ raise ArgumentError, "Unknown event type: #{event.class}"
149
+ end
150
+ end
151
+
152
+ ##
153
+ # Injects multiple Key events into the queue.
154
+ #
155
+ # Supports multiple formats for convenience:
156
+ #
157
+ # * String: Converted to a Key event with that code.
158
+ # * Symbol: Parsed as modifier_code (e.g., <tt>:ctrl_c</tt>, <tt>:enter</tt>).
159
+ # * Hash: Passed to Key.new constructor.
160
+ # * Key: Passed directly.
161
+ #
162
+ # == Examples
163
+ #
164
+ # with_test_terminal do
165
+ # inject_keys("a", "b", "c")
166
+ # inject_keys(:enter, :esc)
167
+ # inject_keys(:ctrl_c, :alt_shift_left)
168
+ # inject_keys("j", { code: "k", modifiers: ["ctrl"] })
169
+ # end
170
+ def inject_keys(*args)
171
+ args.each do |arg|
172
+ event = case arg
173
+ when String
174
+ RatatuiRuby::Event::Key.new(code: arg)
175
+ when Symbol
176
+ parts = arg.to_s.split("_")
177
+ code = parts.pop
178
+ modifiers = parts
179
+ RatatuiRuby::Event::Key.new(code: code, modifiers: modifiers)
180
+ when Hash
181
+ RatatuiRuby::Event::Key.new(**arg)
182
+ when RatatuiRuby::Event::Key
183
+ arg
184
+ else
185
+ raise ArgumentError, "Invalid key argument: #{arg.inspect}. Expected String, Symbol, Hash, or Key event."
186
+ end
187
+ inject_event(event)
188
+ end
189
+ end
190
+ alias inject_key inject_keys
191
+
192
+ ##
193
+ # Returns the cell attributes at the given coordinates.
194
+ #
195
+ # get_cell(0, 0)
196
+ # # => { "symbol" => "H", "fg" => :red, "bg" => nil }
197
+ def get_cell(x, y)
198
+ RatatuiRuby.get_cell_at(x, y)
199
+ end
200
+
201
+ ##
202
+ # Asserts that the cell at the given coordinates has the expected attributes.
203
+ #
204
+ # assert_cell_style(0, 0, char: "H", fg: :red)
205
+ def assert_cell_style(x, y, **expected_attributes)
206
+ cell = get_cell(x, y)
207
+ expected_attributes.each do |key, value|
208
+ actual_value = cell.public_send(key)
209
+ if value.nil?
210
+ assert_nil actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=nil, but got #{actual_value.inspect}"
211
+ else
212
+ assert_equal value, actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=#{value.inspect}, but got #{actual_value.inspect}"
213
+ end
214
+ end
73
215
  end
74
216
  end
75
217
  end
218
+
@@ -6,5 +6,5 @@
6
6
  module RatatuiRuby
7
7
  # The version of the ratatui_ruby gem.
8
8
  # See https://semver.org/spec/v2.0.0.html
9
- VERSION = "0.3.1"
9
+ VERSION = "0.4.0"
10
10
  end
data/lib/ratatui_ruby.rb CHANGED
@@ -12,9 +12,12 @@ require_relative "ratatui_ruby/schema/constraint"
12
12
  require_relative "ratatui_ruby/schema/list"
13
13
  require_relative "ratatui_ruby/schema/style"
14
14
  require_relative "ratatui_ruby/schema/gauge"
15
+ require_relative "ratatui_ruby/schema/line_gauge"
15
16
  require_relative "ratatui_ruby/schema/table"
16
17
  require_relative "ratatui_ruby/schema/tabs"
17
18
  require_relative "ratatui_ruby/schema/bar_chart"
19
+ require_relative "ratatui_ruby/schema/bar_chart/bar"
20
+ require_relative "ratatui_ruby/schema/bar_chart/bar_group"
18
21
  require_relative "ratatui_ruby/schema/sparkline"
19
22
  require_relative "ratatui_ruby/schema/chart"
20
23
  require_relative "ratatui_ruby/schema/clear"
@@ -23,7 +26,14 @@ require_relative "ratatui_ruby/schema/overlay"
23
26
  require_relative "ratatui_ruby/schema/center"
24
27
  require_relative "ratatui_ruby/schema/scrollbar"
25
28
  require_relative "ratatui_ruby/schema/canvas"
29
+ require_relative "ratatui_ruby/schema/shape/label"
26
30
  require_relative "ratatui_ruby/schema/calendar"
31
+ require_relative "ratatui_ruby/schema/ratatui_logo"
32
+ require_relative "ratatui_ruby/schema/ratatui_mascot"
33
+ require_relative "ratatui_ruby/schema/text"
34
+ require_relative "ratatui_ruby/schema/draw"
35
+ require_relative "ratatui_ruby/event"
36
+ require_relative "ratatui_ruby/cell"
27
37
 
28
38
  begin
29
39
  require "ratatui_ruby/ratatui_ruby"
@@ -32,20 +42,53 @@ rescue LoadError
32
42
  require_relative "ratatui_ruby/ratatui_ruby"
33
43
  end
34
44
 
35
- # The RatatuiRuby module acts as a namespace for the entire gem.
36
- # It provides the main entry points for initializing the terminal and drawing the UI.
45
+ # Main entry point for the library.
46
+ #
47
+ # Terminal UIs require low-level control using C/Rust and high-level abstraction in Ruby.
48
+ #
49
+ # This module bridges the gap. It provides the native methods to initialize the terminal, handle raw mode, and render the widget tree.
50
+ #
51
+ # Use `RatatuiRuby.run` to start your application.
37
52
  module RatatuiRuby
38
53
  # Generic error class for RatatuiRuby.
39
54
  class Error < StandardError; end
40
55
 
41
56
  ##
42
- # :method: init_terminal
43
- # :call-seq: init_terminal() -> nil
44
- #
45
57
  # Initializes the terminal for TUI mode.
46
58
  # Enters alternate screen and enables raw mode.
47
59
  #
48
- # (Native method implemented in Rust)
60
+ # [focus_events] whether to enable focus gain/loss events (default: true).
61
+ # [bracketed_paste] whether to enable bracketed paste mode (default: true).
62
+ def self.init_terminal(focus_events: true, bracketed_paste: true)
63
+ _init_terminal(focus_events, bracketed_paste)
64
+ end
65
+
66
+ @experimental_warnings = true
67
+ class << self
68
+ ##
69
+ # :attr_accessor: experimental_warnings
70
+ # Whether to show warnings when using experimental features (default: true).
71
+ attr_accessor :experimental_warnings
72
+ end
73
+
74
+ ##
75
+ # Warns about usage of an experimental feature unless warnings are suppressed.
76
+ #
77
+ # [feature_name] String name of the feature (e.g., "Paragraph#line_count")
78
+ #
79
+ # This warns only once per feature name per session.
80
+ def self.warn_experimental_feature(feature_name)
81
+ return unless experimental_warnings
82
+
83
+ @warned_features ||= {}
84
+ return if @warned_features[feature_name]
85
+
86
+ warn "WARNING: #{feature_name} is an experimental feature and may change in future versions. Disable this warning with RatatuiRuby.experimental_warnings = false."
87
+ @warned_features[feature_name] = true
88
+ end
89
+
90
+ # (Native method _init_terminal implemented in Rust)
91
+ private_class_method :_init_terminal
49
92
 
50
93
  ##
51
94
  # :method: restore_terminal
@@ -67,14 +110,49 @@ module RatatuiRuby
67
110
 
68
111
  ##
69
112
  # :method: poll_event
70
- # :call-seq: poll_event() -> Hash, nil
113
+ # :call-seq: poll_event() -> Event, nil
71
114
  #
72
- # Polls for a keyboard event.
115
+ # Checks for user input.
73
116
  #
74
- # poll_event
75
- # # => { type: :key, code: "a", modifiers: ["ctrl"] }
117
+ # Returns a discrete event (Key, Mouse, Resize) if one is available in the queue.
118
+ # Returns nil immediately if the queue is empty (non-blocking).
76
119
  #
77
- # (Native method implemented in Rust)
120
+ # === Example
121
+ #
122
+ # event = RatatuiRuby.poll_event
123
+ # puts "Key pressed" if event.is_a?(RatatuiRuby::Event::Key)
124
+ #
125
+ def self.poll_event
126
+ raw = _poll_event
127
+ return nil if raw.nil?
128
+
129
+ case raw[:type]
130
+ when :key
131
+ Event::Key.new(code: raw[:code], modifiers: raw[:modifiers] || [])
132
+ when :mouse
133
+ Event::Mouse.new(
134
+ kind: raw[:kind].to_s,
135
+ x: raw[:x],
136
+ y: raw[:y],
137
+ button: raw[:button].to_s,
138
+ modifiers: raw[:modifiers] || []
139
+ )
140
+ when :resize
141
+ Event::Resize.new(width: raw[:width], height: raw[:height])
142
+ when :paste
143
+ Event::Paste.new(content: raw[:content])
144
+ when :focus_gained
145
+ Event::FocusGained.new
146
+ when :focus_lost
147
+ Event::FocusLost.new
148
+ else
149
+ # Fallback for unknown events, though ideally we cover them all
150
+ nil
151
+ end
152
+ end
153
+
154
+ # (Native method _poll_event implemented in Rust)
155
+ private_class_method :_poll_event
78
156
 
79
157
  ##
80
158
  # :method: inject_test_event
@@ -89,21 +167,63 @@ module RatatuiRuby
89
167
  # (Native method implemented in Rust)
90
168
 
91
169
  ##
92
- # Provides a convenience wrapper for the main TUI loop.
93
- # Initializes the terminal, runs the loop, and ensures the terminal is restored.
170
+ # :method: run
171
+ # :call-seq: run { |session| ... } -> Object
172
+ #
173
+ # Starts the TUI application lifecycle.
174
+ #
175
+ # Managing generic setup/teardown (raw mode, alternate screen) manualy is error-prone. If your app crashes, the terminal might be left in a broken state.
176
+ #
177
+ # This method handles the safety net. It initializes the terminal, yields a {Session}, and ensures the terminal state is restored even if exceptions occur.
178
+ #
179
+ # === Example
94
180
  #
95
- # RatatuiRuby.main_loop do
96
- # draw RatatuiRuby::Paragraph.new(text: "Hello")
97
- # event = RatatuiRuby::poll_event
98
- # break if event && event[:type] == :key && event[:code] == "q"
181
+ # RatatuiRuby.run(focus_events: false) do |tui|
182
+ # tui.draw(tui.paragraph(text: "Hi"))
183
+ # sleep 1
99
184
  # end
100
- def self.main_loop
101
- require_relative "ratatui_ruby/dsl"
102
- init_terminal
103
- loop do
104
- yield DSL.new
105
- end
185
+ def self.run(focus_events: true, bracketed_paste: true)
186
+ require_relative "ratatui_ruby/session"
187
+ init_terminal(focus_events: focus_events, bracketed_paste: bracketed_paste)
188
+ yield Session.new
106
189
  ensure
107
190
  restore_terminal
108
191
  end
192
+
193
+ ##
194
+ # :method: get_cell_at
195
+ # :call-seq: get_cell_at(x, y) -> Cell
196
+ #
197
+ # Inspects the terminal buffer at specific coordinates.
198
+ #
199
+ # When writing tests, you need to verify that your widget drew the correct characters and styles.
200
+ # This method provides deep inspection of the cell's state (symbol, colors, modifiers).
201
+ #
202
+ # Returns a {Cell} object.
203
+ #
204
+ # Values depend on what the backend has rendered. If nothing has been rendered to a cell, it may contain defaults (empty symbol, nil colors).
205
+ #
206
+ # === Example
207
+ #
208
+ # cell = RatatuiRuby.get_cell_at(10, 5)
209
+ # expect(cell.symbol).to eq("X")
210
+ # expect(cell.fg).to eq(:red)
211
+ # expect(cell).to be_bold
212
+ #
213
+ def self.get_cell_at(x, y)
214
+ raw = _get_cell_at(x, y)
215
+ Cell.new(
216
+ char: raw["char"],
217
+ fg: raw["fg"],
218
+ bg: raw["bg"],
219
+ modifiers: raw["modifiers"] || []
220
+ )
221
+ end
222
+
223
+ # (Native method _get_cell_at implemented in Rust)
224
+ private_class_method :_get_cell_at
225
+
226
+ # Hide native Layout._split helper
227
+ Layout.singleton_class.send(:private, :_split)
228
+
109
229
  end