rooibos 0.5.0 → 0.6.1

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +9 -5
  3. data/.builds/ruby-3.3.yml +9 -5
  4. data/.builds/ruby-3.4.yml +9 -5
  5. data/.builds/ruby-4.0.0.yml +9 -5
  6. data/AGENTS.md +1 -1
  7. data/CHANGELOG.md +57 -0
  8. data/README.md +2 -2
  9. data/README.rdoc +374 -0
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +1 -1
  12. data/doc/best_practices/forms_and_validation.md +20 -0
  13. data/doc/best_practices/http_workflows.md +20 -0
  14. data/doc/best_practices/index.md +26 -0
  15. data/doc/best_practices/lists_and_tables.md +20 -0
  16. data/doc/best_practices/modal_dialogs.md +20 -0
  17. data/doc/best_practices/no_stateful_widgets.md +184 -0
  18. data/doc/best_practices/orchestration.md +20 -0
  19. data/doc/best_practices/streaming_data.md +20 -0
  20. data/doc/contributors/design/commands_and_outlets.md +1 -1
  21. data/doc/contributors/documentation_plan.md +616 -0
  22. data/doc/contributors/documentation_stub_audit.md +112 -0
  23. data/doc/contributors/documentation_style.md +275 -0
  24. data/doc/contributors/e2e_pty.md +168 -0
  25. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
  26. data/doc/contributors/specs/file_browser.md +789 -0
  27. data/doc/contributors/specs/file_browser_stories.md +774 -0
  28. data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
  29. data/doc/contributors/todo/scrollbar.md +118 -0
  30. data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
  31. data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
  32. data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
  33. data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
  34. data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
  35. data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
  36. data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
  37. data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
  38. data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
  39. data/doc/contributors/tutorial_old/12_going_further.md +20 -0
  40. data/doc/contributors/tutorial_old/index.md +20 -0
  41. data/doc/essentials/commands.md +20 -0
  42. data/doc/essentials/index.md +31 -0
  43. data/doc/essentials/messages.md +21 -0
  44. data/doc/essentials/models.md +21 -0
  45. data/doc/essentials/shortcuts.md +19 -0
  46. data/doc/essentials/the_elm_architecture.md +24 -0
  47. data/doc/essentials/the_runtime.md +21 -0
  48. data/doc/essentials/update_functions.md +20 -0
  49. data/doc/essentials/views.md +22 -0
  50. data/doc/getting_started/for_go_developers.md +16 -0
  51. data/doc/getting_started/for_python_developers.md +16 -0
  52. data/doc/getting_started/for_rails_developers.md +17 -0
  53. data/doc/getting_started/for_ratatui_ruby_developers.md +17 -0
  54. data/doc/getting_started/for_react_developers.md +17 -0
  55. data/doc/getting_started/index.md +52 -0
  56. data/doc/getting_started/install.md +20 -0
  57. data/doc/getting_started/quickstart.md +9 -45
  58. data/doc/getting_started/ruby_primer.md +19 -0
  59. data/doc/getting_started/why_rooibos.md +20 -0
  60. data/doc/index.md +79 -11
  61. data/doc/scaling_up/async_patterns.md +20 -0
  62. data/doc/scaling_up/command_composition.md +20 -0
  63. data/doc/scaling_up/custom_commands.md +21 -0
  64. data/doc/scaling_up/fractal_architecture.md +20 -0
  65. data/doc/scaling_up/index.md +30 -0
  66. data/doc/scaling_up/message_routing.md +20 -0
  67. data/doc/scaling_up/ractor_safety.md +20 -0
  68. data/doc/scaling_up/testing.md +21 -0
  69. data/doc/troubleshooting/common_errors.md +20 -0
  70. data/doc/troubleshooting/debugging.md +21 -0
  71. data/doc/troubleshooting/index.md +23 -0
  72. data/doc/troubleshooting/performance.md +20 -0
  73. data/doc/tutorial/01_project_setup.md +44 -0
  74. data/doc/tutorial/02_hello_world.md +45 -0
  75. data/doc/tutorial/03_static_file_list.md +44 -0
  76. data/doc/tutorial/04_arrow_navigation.md +47 -0
  77. data/doc/tutorial/05_real_files.md +45 -0
  78. data/doc/tutorial/06_safe_refactoring.md +21 -0
  79. data/doc/tutorial/07_red_first_tdd.md +26 -0
  80. data/doc/tutorial/08_file_metadata.md +42 -0
  81. data/doc/tutorial/09_text_preview.md +44 -0
  82. data/doc/tutorial/10_directory_tree.md +42 -0
  83. data/doc/tutorial/11_pane_focus.md +40 -0
  84. data/doc/tutorial/12_sorting.md +41 -0
  85. data/doc/tutorial/13_filtering.md +43 -0
  86. data/doc/tutorial/14_toggle_hidden.md +41 -0
  87. data/doc/tutorial/15_text_input_widget.md +43 -0
  88. data/doc/tutorial/16_rename_files.md +42 -0
  89. data/doc/tutorial/17_confirmation_dialogs.md +43 -0
  90. data/doc/tutorial/18_progress_indicators.md +43 -0
  91. data/doc/tutorial/19_atomic_operations.md +42 -0
  92. data/doc/tutorial/20_external_editor.md +42 -0
  93. data/doc/tutorial/21_modal_overlays.md +41 -0
  94. data/doc/tutorial/22_error_handling.md +43 -0
  95. data/doc/tutorial/23_terminal_capabilities.md +53 -0
  96. data/doc/tutorial/24_mouse_events.md +43 -0
  97. data/doc/tutorial/25_resize_events.md +43 -0
  98. data/doc/tutorial/26_loading_states.md +42 -0
  99. data/doc/tutorial/27_performance.md +43 -0
  100. data/doc/tutorial/28_color_schemes.md +47 -0
  101. data/doc/tutorial/29_configuration.md +124 -0
  102. data/doc/tutorial/30_going_further.md +17 -0
  103. data/doc/tutorial/index.md +17 -0
  104. data/examples/app_file_browser/app.rb +40 -0
  105. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
  106. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
  107. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
  108. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
  109. data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
  110. data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
  111. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
  112. data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
  113. data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
  114. data/examples/verify_website_first_app/app.rb +85 -0
  115. data/examples/verify_website_hello_mvu/app.rb +31 -0
  116. data/examples/widget_command_system/app.rb +15 -13
  117. data/exe/rooibos +10 -0
  118. data/generate_tutorial_stubs.rb +126 -0
  119. data/lib/rooibos/cli/commands/new.rb +373 -0
  120. data/lib/rooibos/cli/commands/run.rb +98 -0
  121. data/lib/rooibos/cli.rb +78 -0
  122. data/lib/rooibos/command/all.rb +76 -23
  123. data/lib/rooibos/command/batch.rb +61 -34
  124. data/lib/rooibos/command/custom.rb +84 -1
  125. data/lib/rooibos/command/http.rb +121 -55
  126. data/lib/rooibos/command/lifecycle.rb +5 -5
  127. data/lib/rooibos/command/open.rb +93 -0
  128. data/lib/rooibos/command/outlet.rb +105 -3
  129. data/lib/rooibos/command/wait.rb +9 -6
  130. data/lib/rooibos/command.rb +114 -89
  131. data/lib/rooibos/message/batch.rb +39 -0
  132. data/lib/rooibos/message/canceled.rb +51 -0
  133. data/lib/rooibos/message/error.rb +48 -0
  134. data/lib/rooibos/message/open.rb +30 -0
  135. data/lib/rooibos/message.rb +84 -4
  136. data/lib/rooibos/router.rb +11 -14
  137. data/lib/rooibos/runtime.rb +40 -43
  138. data/lib/rooibos/shortcuts.rb +47 -0
  139. data/lib/rooibos/test_helper.rb +71 -6
  140. data/lib/rooibos/version.rb +1 -1
  141. data/lib/rooibos/welcome.rb +237 -0
  142. data/lib/rooibos.rb +4 -3
  143. data/mise.toml +1 -1
  144. data/rbs_collection.lock.yaml +2 -2
  145. data/sig/concurrent.rbs +4 -0
  146. data/sig/gem.rbs +20 -0
  147. data/sig/rooibos/cli.rbs +42 -0
  148. data/sig/rooibos/command.rbs +59 -7
  149. data/sig/rooibos/message.rbs +66 -2
  150. data/sig/rooibos/shortcuts.rbs +14 -0
  151. data/sig/rooibos/test_helper.rbs +6 -2
  152. data/sig/rooibos/welcome.rbs +75 -0
  153. data/tasks/install.rake +29 -0
  154. data/tasks/resources/build.yml.erb +2 -0
  155. metadata +274 -38
  156. data/doc/concepts/application_architecture.md +0 -197
  157. data/doc/concepts/application_testing.md +0 -49
  158. data/doc/concepts/async_work.md +0 -164
  159. data/doc/concepts/commands.md +0 -530
  160. data/doc/concepts/message_processing.md +0 -51
  161. data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
  162. data/doc/contributors/WIP/implementation_plan.md +0 -409
  163. data/doc/contributors/WIP/init_callable_proposal.md +0 -344
  164. data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
  165. data/doc/contributors/WIP/task.md +0 -36
  166. data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
  167. data/doc/contributors/kit-no-outlet.md +0 -238
  168. data/doc/contributors/priorities.md +0 -38
  169. data/doc/images/.gitkeep +0 -0
  170. data/exe/.gitkeep +0 -0
  171. /data/doc/contributors/{WIP → design}/mvu_tea_implementations_research.md +0 -0
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ module Rooibos
9
+ # Built-in welcome screen used by scaffolded applications.
10
+ module Welcome
11
+ module UI # :nodoc:
12
+ module Styles # :nodoc:
13
+ TEXT = RatatuiRuby::Style::Style.new
14
+ FILENAME = RatatuiRuby::Style::Style.new(fg: :green)
15
+ COMMAND = RatatuiRuby::Style::Style.new(fg: :red)
16
+ URL = RatatuiRuby::Style::Style.new(fg: :blue)
17
+
18
+ COMMAND_BUTTON = RatatuiRuby::Style::Style.new(fg: :red, modifiers: [:underlined])
19
+ COMMAND_BUTTON_FOCUS = RatatuiRuby::Style::Style.new(fg: :red, modifiers: [:bold, :underlined])
20
+ COMMAND_BUTTON_HOVER = RatatuiRuby::Style::Style.new(fg: :red, modifiers: [:reversed])
21
+ COMMAND_BUTTON_BOTH = RatatuiRuby::Style::Style.new(fg: :red, modifiers: [:bold, :reversed])
22
+
23
+ URL_BUTTON = RatatuiRuby::Style::Style.new(fg: :blue, modifiers: [:underlined])
24
+ URL_BUTTON_FOCUS = RatatuiRuby::Style::Style.new(fg: :blue, modifiers: [:bold, :underlined])
25
+ URL_BUTTON_HOVER = RatatuiRuby::Style::Style.new(fg: :blue, modifiers: [:reversed])
26
+ URL_BUTTON_BOTH = RatatuiRuby::Style::Style.new(fg: :blue, modifiers: [:bold, :reversed])
27
+ end
28
+
29
+ module Widgets # :nodoc:
30
+ WELCOME_TEXT = {
31
+ "Welcome to Rooibos! You will find the Ruby code " \
32
+ "for this application in " => Styles::TEXT,
33
+ "lib/saturday.rb" => Styles::FILENAME,
34
+ ". The tests that verify it are at " => Styles::TEXT,
35
+ "test/test_saturday.rb" => Styles::FILENAME,
36
+ ". You can run the tests with " => Styles::TEXT,
37
+ "bundle exec rake test" => Styles::COMMAND,
38
+ ". Visit " => Styles::TEXT,
39
+ "www.rooibos.run" => Styles::URL,
40
+ " to learn about Rooibos and to find other " \
41
+ "Rooibos developers. You can press " => Styles::TEXT,
42
+ "Control + C" => Styles::COMMAND,
43
+ " to exit at any time." => Styles::TEXT,
44
+ }
45
+
46
+ PARAGRAPH = RatatuiRuby::Widgets::Paragraph.new(
47
+ text: RatatuiRuby::Text::Line.new(
48
+ spans: WELCOME_TEXT.map { |text, style| RatatuiRuby::Text::Span.new(content: text, style:) }
49
+ ),
50
+ wrap: true,
51
+ alignment: :left
52
+ )
53
+
54
+ def self.website_button(focused: false, hovered: false)
55
+ style = if focused && hovered
56
+ Styles::URL_BUTTON_BOTH
57
+ elsif focused
58
+ Styles::URL_BUTTON_FOCUS
59
+ elsif hovered
60
+ Styles::URL_BUTTON_HOVER
61
+ else
62
+ Styles::URL_BUTTON
63
+ end
64
+ RatatuiRuby::Text::Span.new(content: "[Visit Website]", style:)
65
+ end
66
+
67
+ def self.exit_button(focused: false, hovered: false)
68
+ style = if focused && hovered
69
+ Styles::COMMAND_BUTTON_BOTH
70
+ elsif focused
71
+ Styles::COMMAND_BUTTON_FOCUS
72
+ elsif hovered
73
+ Styles::COMMAND_BUTTON_HOVER
74
+ else
75
+ Styles::COMMAND_BUTTON
76
+ end
77
+ RatatuiRuby::Text::Span.new(content: "[Exit App]", style:)
78
+ end
79
+ end
80
+
81
+ BUTTON_SLOTS = { website: 1, exit: 3 }.freeze
82
+ FOCUS_ORDER = [:website, :exit].freeze
83
+
84
+ BUTTON_BAR_CONSTRAINTS = [
85
+ RatatuiRuby::Layout::Constraint.fill(1),
86
+ RatatuiRuby::Layout::Constraint.length(18),
87
+ RatatuiRuby::Layout::Constraint.length(2),
88
+ RatatuiRuby::Layout::Constraint.length(14),
89
+ RatatuiRuby::Layout::Constraint.fill(1),
90
+ ].freeze
91
+
92
+ CONTENT_CONSTRAINTS = [
93
+ RatatuiRuby::Layout::Constraint.fill(1),
94
+ RatatuiRuby::Layout::Constraint.length(1),
95
+ ].freeze
96
+
97
+ def self.button_bar(focused:, hovered:)
98
+ RatatuiRuby::Layout::Layout.new(
99
+ direction: :horizontal,
100
+ constraints: BUTTON_BAR_CONSTRAINTS,
101
+ children: [
102
+ nil,
103
+ Widgets.website_button(focused: focused == :website, hovered: hovered == :website),
104
+ nil,
105
+ Widgets.exit_button(focused: focused == :exit, hovered: hovered == :exit),
106
+ nil,
107
+ ]
108
+ )
109
+ end
110
+
111
+ def self.content_layout(focused:, hovered:)
112
+ RatatuiRuby::Layout::Layout.new(
113
+ direction: :vertical,
114
+ constraints: CONTENT_CONSTRAINTS,
115
+ children: [Widgets::PARAGRAPH, button_bar(focused:, hovered:)]
116
+ )
117
+ end
118
+
119
+ def self.frame(focused:, hovered:)
120
+ RatatuiRuby::Widgets::Block.new(
121
+ title: "Hello, Rooibos!",
122
+ borders: [:all],
123
+ border_style: { fg: :cyan },
124
+ padding: [2, 2, 1, 1],
125
+ children: [content_layout(focused:, hovered:)]
126
+ )
127
+ end
128
+
129
+ # Value object wrapping hit-test areas for clickable buttons.
130
+ ButtonAreas = Data.define(:website, :exit) do
131
+ def contains?(name, x, y) = public_send(name)&.contains?(x, y)
132
+
133
+ def button_at(x, y)
134
+ return :website if website&.contains?(x, y)
135
+ return :exit if exit&.contains?(x, y)
136
+
137
+ nil
138
+ end
139
+
140
+ def for_viewport(width, height)
141
+ viewport = RatatuiRuby::Layout::Rect.new(x: 0, y: 0, width:, height:)
142
+ frame = UI.frame(focused: nil, hovered: nil)
143
+ inner = frame.inner(viewport)
144
+
145
+ content_rects = RatatuiRuby::Layout::Layout.split(
146
+ inner,
147
+ direction: :vertical,
148
+ constraints: CONTENT_CONSTRAINTS
149
+ )
150
+ button_rects = RatatuiRuby::Layout::Layout.split(
151
+ content_rects[1],
152
+ direction: :horizontal,
153
+ constraints: BUTTON_BAR_CONSTRAINTS
154
+ )
155
+
156
+ ButtonAreas.new(
157
+ website: button_rects[BUTTON_SLOTS[:website]],
158
+ exit: button_rects[BUTTON_SLOTS[:exit]]
159
+ )
160
+ end
161
+ end
162
+ end
163
+
164
+ # focused: keyboard focus (persists until Tab/Shift+Tab)
165
+ # hovered: mouse hover (clears on mouse-out)
166
+ Model = Data.define(:button_areas, :focused, :hovered) do
167
+ def tree = UI.frame(focused:, hovered:)
168
+
169
+ # Returns the "active" button for activation (keyboard takes precedence)
170
+ def active_button = focused || hovered
171
+ end
172
+
173
+ View = -> (model, _tui) { model.tree }
174
+
175
+ Update = -> (message, model) {
176
+ case message
177
+ in _ if message.ctrl_c?
178
+ Rooibos::Command.exit
179
+ in _ if message.resize?
180
+ model.with(button_areas: model.button_areas.for_viewport(message.width, message.height))
181
+ in _ if message.tab?
182
+ cycle_focus(model, :forward)
183
+ in _ if message.shift_back_tab?
184
+ cycle_focus(model, :backward)
185
+ in _ if message.enter? && model.active_button
186
+ activate_button(model.active_button, model)
187
+ in _ if message.mouse? && message.down? && message.button == "left"
188
+ handle_click(message, model)
189
+ in { type: :mouse }
190
+ handle_hover(message, model)
191
+ else
192
+ model
193
+ end
194
+ }
195
+
196
+ Init = -> {
197
+ viewport = RatatuiRuby.terminal_size
198
+ areas = UI::ButtonAreas.new(website: nil, exit: nil).for_viewport(viewport.width, viewport.height)
199
+ Ractor.make_shareable Model.new(button_areas: areas, focused: nil, hovered: nil)
200
+ }
201
+
202
+ def self.handle_click(message, model)
203
+ button = model.button_areas.button_at(message.x, message.y)
204
+ activate_button(button, model)
205
+ end
206
+
207
+ def self.handle_hover(message, model)
208
+ new_hovered = model.button_areas.button_at(message.x, message.y)
209
+ return model if new_hovered == model.hovered
210
+
211
+ model.with(hovered: new_hovered)
212
+ end
213
+
214
+ def self.cycle_focus(model, direction)
215
+ order = UI::FOCUS_ORDER
216
+ return model.with(focused: order.first) unless model.focused
217
+
218
+ current_index = order.index(model.focused)
219
+ return model.with(focused: order.first) unless current_index
220
+
221
+ next_index = case direction
222
+ when :forward then (current_index + 1) % order.length
223
+ when :backward then (current_index - 1) % order.length
224
+ else 0
225
+ end
226
+ model.with(focused: order[next_index])
227
+ end
228
+
229
+ def self.activate_button(button, model)
230
+ case button
231
+ when :website then [model, Rooibos::Command.system("open 'https://www.rooibos.run'", :open_url)]
232
+ when :exit then Rooibos::Command.exit
233
+ else model
234
+ end
235
+ end
236
+ end
237
+ end
data/lib/rooibos.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "rooibos/message"
11
11
  require_relative "rooibos/command"
12
12
  require_relative "rooibos/runtime"
13
13
  require_relative "rooibos/router"
14
+ require_relative "rooibos/welcome"
14
15
 
15
16
  # The Elm Architecture for Ruby.
16
17
  #
@@ -103,17 +104,17 @@ module Rooibos
103
104
  # # Verbose:
104
105
  # case message
105
106
  # in [:stats, *rest]
106
- # new_child, cmd = StatsPanel::UPDATE.call(rest, model.stats)
107
+ # new_child, cmd = StatsPanel::Update.call(rest, model.stats)
107
108
  # mapped = cmd ? Command.map(cmd) { |r| [:stats, *r] } : nil
108
109
  # [new_child, mapped]
109
110
  # end
110
111
  #
111
112
  # # Concise:
112
- # Rooibos.delegate(message, :stats, StatsPanel::UPDATE, model.stats)
113
+ # Rooibos.delegate(message, :stats, StatsPanel::Update, model.stats)
113
114
  def self.delegate(message, prefix, child_update, child_model)
114
115
  return nil unless message.is_a?(Array) && message.first == prefix
115
116
 
116
- rest = message[1..]
117
+ rest = message[1]
117
118
  new_child, command = child_update.call(rest, child_model)
118
119
  wrapped = command ? route(command, prefix) : nil
119
120
  [new_child, wrapped]
data/mise.toml CHANGED
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: LGPL-3.0-or-later
3
3
 
4
4
  [tools]
5
- ruby = "4.0.0"
5
+ ruby = "4.0.1"
6
6
  rust = "1.91.1"
7
7
  python = "3.12"
8
8
  pre-commit = "latest"
@@ -66,11 +66,11 @@ gems:
66
66
  source:
67
67
  type: stdlib
68
68
  - name: prism
69
- version: 1.7.0
69
+ version: 1.8.0
70
70
  source:
71
71
  type: rubygems
72
72
  - name: ratatui_ruby
73
- version: 0.10.2
73
+ version: 1.1.0
74
74
  source:
75
75
  type: rubygems
76
76
  - name: ratatui_ruby-devtools
data/sig/concurrent.rbs CHANGED
@@ -31,6 +31,7 @@ module Concurrent
31
31
  def rejected?: () -> bool
32
32
  def wait: (?Numeric? timeout) -> self
33
33
  def value: () -> T?
34
+ def value!: () -> T
34
35
  def reason: () -> Exception?
35
36
  end
36
37
 
@@ -39,6 +40,9 @@ module Concurrent
39
40
 
40
41
  # Races multiple futures/events, returning when any resolves.
41
42
  def self.any_event: (*Future[untyped] | ResolvableEvent) -> ResolvableEvent
43
+
44
+ # Joins multiple futures, resolving when all complete.
45
+ def self.zip_futures: (*Future[untyped]) -> Future[Array[untyped]]
42
46
  end
43
47
 
44
48
  # Single-element blocking container for synchronization.
data/sig/gem.rbs ADDED
@@ -0,0 +1,20 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ # Local shim for Gem::Version#segments not in stdlib RBS.
7
+ # TODO: Remove when upstream RBS adds this method.
8
+
9
+ module Gem
10
+ class Version
11
+ # Returns version segments as an array of integers and strings.
12
+ def segments: () -> Array[Integer | String]
13
+ end
14
+ end
15
+
16
+ # Local shim for RatatuiRuby.terminal_size alias not in upstream RBS.
17
+ # The Ruby gem has: alias terminal_size get_terminal_size
18
+ module RatatuiRuby
19
+ def self.terminal_size: () -> RatatuiRuby::Layout::Rect
20
+ end
@@ -0,0 +1,42 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module Rooibos
7
+ module CLI
8
+ COMMANDS: Hash[String, singleton(Commands::New) | singleton(Commands::Run)]
9
+
10
+ def self.call: (Array[String]) -> void
11
+ def self.usage: () -> String
12
+
13
+ module Commands
14
+ module New
15
+ # Options hash from parse_options.
16
+ type options = Hash[Symbol, bool | String]
17
+
18
+ BUNDLE_GEM_DEFAULTS: Array[String]
19
+
20
+ def self.call: (Array[String]) -> void
21
+ def self.usage: () -> String
22
+ def self.parse_options: (Array[String]) -> options
23
+ def self.create_app: (String, Array[String], options) -> void
24
+ def self.build_bundle_gem_args: (Array[String]) -> Array[String]
25
+ def self.git_enabled?: (Array[String]) -> bool
26
+ def self.make_initial_commit: (Pathname) -> void
27
+ def self.to_module_name: (String) -> String
28
+ def self.fix_gemspec_placeholders: (String) -> String
29
+ def self.exe_template: (String, String) -> String
30
+ def self.app_template: (String, String) -> String
31
+ def self.test_template: (String, String) -> String
32
+ end
33
+
34
+ module Run
35
+ def self.call: (Array[String]) -> void
36
+ def self.usage: () -> String
37
+ def self.run_app: () -> void
38
+ def self.find_project_root: () -> Pathname?
39
+ end
40
+ end
41
+ end
42
+ end
@@ -18,6 +18,10 @@ module Rooibos
18
18
  # Grace period for cooperative cancellation (seconds).
19
19
  # Runtime waits this long before force-killing the thread.
20
20
  def rooibos_cancellation_grace_period: () -> Float
21
+
22
+ # Runtime type checking (from Kernel).
23
+ # Required for DWIM nested detection in All.new.
24
+ def is_a?: (Module | Class module_or_class) -> bool
21
25
  end
22
26
 
23
27
  # Sentinel value for application termination.
@@ -114,9 +118,9 @@ module Rooibos
114
118
  include Custom
115
119
 
116
120
  attr_reader seconds: Float
117
- attr_reader tag: Symbol
121
+ attr_reader envelope: Symbol
118
122
 
119
- def self.new: (seconds: Float, tag: Symbol) -> instance
123
+ def self.new: (seconds: Float, envelope: Symbol) -> instance
120
124
 
121
125
  # Execute the timer with cooperative cancellation.
122
126
  def call: (Outlet out, Concurrent::Cancellation token) -> void
@@ -135,12 +139,25 @@ module Rooibos
135
139
 
136
140
  # Mixin for user-defined custom commands.
137
141
  module Custom
142
+ # Interface for classes with a members method (Data.define, Struct).
143
+ interface _HasMembers
144
+ def members: () -> Array[Symbol]
145
+ end
146
+
147
+ # Methods excluded from deconstruct_keys introspection.
148
+ # Computed from Object, Data.define, Struct, and PP prototypes.
149
+ INFRASTRUCTURE_METHODS: Array[Symbol]
150
+
138
151
  # Brand predicate for command identification.
139
152
  def rooibos_command?: () -> true
140
153
 
141
154
  # Cleanup time after cancellation is requested. In seconds.
142
155
  # Default: 0.1 seconds (100 milliseconds).
143
156
  def rooibos_cancellation_grace_period: () -> Float
157
+
158
+ # Deconstructs for hash-based pattern matching.
159
+ # Introspects public query methods and returns a hash with :type discriminator.
160
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
144
161
  end
145
162
 
146
163
  # Minimal interface for callables accepted by Lifecycle.run_sync.
@@ -179,8 +196,17 @@ module Rooibos
179
196
 
180
197
  # Messaging gateway for custom commands.
181
198
  class Outlet
199
+ # Internal handle for async streaming commands.
200
+ class AsyncHandle < Data
201
+ attr_reader future: Concurrent::Promises::Future[void]
202
+
203
+ def self.new: (future: Concurrent::Promises::Future[void]) -> instance
204
+ end
205
+
182
206
  @channel: Concurrent::Promises::Channel
183
207
  @live: Lifecycle
208
+ @pending_async: Array[AsyncHandle]
209
+
184
210
 
185
211
  def initialize: (Concurrent::Promises::Channel channel, lifecycle: Lifecycle) -> void
186
212
 
@@ -188,10 +214,18 @@ module Rooibos
188
214
  attr_reader live: Lifecycle
189
215
 
190
216
  # Sends a message to the runtime.
191
- def put: (*Object args) -> void
217
+ def put: (*message args) -> void
192
218
 
193
219
  # Runs a child command synchronously, returning its result.
194
220
  def source: (_Callable command, Concurrent::Cancellation token, ?timeout: Float) -> Object?
221
+
222
+ # Spawns an async streaming command.
223
+ # Returns a handle for use with wait.
224
+ def standing: (_Callable command, Concurrent::Cancellation token) -> AsyncHandle
225
+
226
+ # Blocks until async commands complete.
227
+ # If no handles given, waits for all pending async commands.
228
+ def wait: (*AsyncHandle handles, ?token: Concurrent::Cancellation?) -> void
195
229
  end
196
230
 
197
231
  # A fire-and-forget parallel command.
@@ -212,17 +246,17 @@ module Rooibos
212
246
  class All
213
247
  include Custom
214
248
 
215
- attr_reader tag: Symbol
216
- attr_reader commands: Array[Custom]
249
+ attr_reader envelope: Symbol
250
+ attr_reader commands: Array[_Command]
217
251
  attr_reader nested: bool
218
252
 
219
- def self.new: (Symbol tag, *Custom | Array[Custom]) -> instance
253
+ def self.new: (Symbol envelope, *_Command | Array[_Command]) -> instance
220
254
 
221
255
  def call: (Outlet outlet, Concurrent::Cancellation token) -> void
222
256
  end
223
257
 
224
258
  # Creates an aggregating parallel command.
225
- def self.all: (Symbol tag, *Custom | Array[Custom]) -> All
259
+ def self.all: (Symbol envelope, *_Command | Array[_Command]) -> All
226
260
 
227
261
  # HTTP response shapes for pattern matching
228
262
  type http_success_shape = { type: :http, envelope: Symbol, status: Integer, body: String, headers: Hash[String, String] }
@@ -261,5 +295,23 @@ module Rooibos
261
295
  # DWIM: accepts positional args, method keywords (get:, post:, etc.), or explicit keywords.
262
296
  # See Http class and documentation for all supported patterns.
263
297
  def self.http: (*(Symbol | String | nil), **(Symbol | String | Integer | Float | Hash[String, String] | ^(String, Hash[String, String]?, Integer?) -> Object | nil)) -> Http
298
+
299
+ # Opens a file or URL with the system's default application.
300
+ class Open < Data
301
+ include Custom
302
+
303
+ attr_reader path: String
304
+ attr_reader envelope: String
305
+
306
+ def self.new: (path: String, envelope: String) -> instance
307
+
308
+ # Builds the platform-specific open command.
309
+ def self.system_command: (String path, ?String platform) -> String
310
+
311
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
312
+ end
313
+
314
+ # Creates an open command for launching files with system opener.
315
+ def self.open: (String path, ?String envelope) -> Open
264
316
  end
265
317
  end
@@ -4,13 +4,26 @@
4
4
  #++
5
5
 
6
6
  module Rooibos
7
+ # Any value passed through Outlet#put to the runtime.
8
+ # Commands may produce any type; Predicates mixin is optional.
9
+ type message = Object
10
+
7
11
  # Messages sent from commands to update functions.
8
12
  module Message
9
13
  # Fallback predicate mixin.
10
14
  module Predicates
15
+ # Converts the message to a Symbol representation.
16
+ def to_sym: () -> Symbol
17
+
18
+ # Compares the message with another object.
19
+ def ==: (top other) -> bool
20
+
11
21
  # Returns false for unknown predicate methods.
12
22
  def method_missing: (Symbol name, *untyped args, **untyped kwargs) ?{ () -> untyped } -> bool
13
23
 
24
+ # Default deconstruct_keys for classes that don't define their own.
25
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
26
+
14
27
  # Responds to all predicate methods.
15
28
  def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
16
29
  end
@@ -100,22 +113,73 @@ module Rooibos
100
113
  def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
101
114
  end
102
115
 
116
+ # Completion sentinel from Command.batch.
117
+ # Signals when all children of a batch command have finished.
118
+ class Batch < Data
119
+ include Predicates
120
+
121
+ attr_reader command: Command::Batch
122
+
123
+ def self.new: (command: Command::Batch) -> instance
124
+
125
+ def batch?: () -> bool
126
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
127
+ end
128
+
103
129
  # Response from Command.all aggregating parallel execution.
104
130
  class All
105
131
  include Predicates
106
132
 
107
133
  attr_reader envelope: Symbol
108
- attr_reader results: Array[untyped]
134
+ attr_reader results: Array[message]
109
135
  attr_reader nested: bool
110
136
 
111
137
  def initialize: (
112
138
  envelope: Symbol,
113
- results: Array[untyped],
139
+ results: Array[message],
114
140
  nested: bool
115
141
  ) -> void
116
142
 
117
143
  def all?: () -> bool
118
144
  def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
119
145
  end
146
+
147
+ # Error message from a failed command.
148
+ class Error < Data
149
+ include Predicates
150
+
151
+ attr_reader command: Command::Custom
152
+ attr_reader exception: Exception
153
+
154
+ def self.new: (command: Command::Custom, exception: Exception) -> instance
155
+
156
+ def error?: () -> bool
157
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
158
+ end
159
+
160
+ # Cancellation notification from a canceled command.
161
+ class Canceled < Data
162
+ include Predicates
163
+
164
+ attr_reader command: untyped
165
+
166
+ def self.new: (command: untyped) -> instance
167
+
168
+ def canceled?: () -> bool
169
+ alias cancelled? canceled?
170
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
171
+ end
172
+
173
+ # Success notification from Command.open.
174
+ class Open < Data
175
+ include Predicates
176
+
177
+ attr_reader envelope: String
178
+
179
+ def self.new: (envelope: String) -> instance
180
+
181
+ def open?: () -> bool
182
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
183
+ end
120
184
  end
121
185
  end
@@ -12,5 +12,19 @@ module Rooibos
12
12
  def self.sh: (String command, Symbol | Class tag) -> Command::System
13
13
  def self.map: (Command::execution inner_command) { (Array[untyped]) -> Array[untyped] } -> Command::Mapped
14
14
  end
15
+
16
+ # Short aliases for Message types.
17
+ module Msg
18
+ Timer: singleton(Message::Timer)
19
+ Http: singleton(Message::HttpResponse)
20
+
21
+ module Sh
22
+ Batch: singleton(Message::System::Batch)
23
+ Stream: singleton(Message::System::Stream)
24
+ end
25
+
26
+ All: singleton(Message::All)
27
+ Batch: singleton(Message::Batch)
28
+ end
15
29
  end
16
30
  end
@@ -5,6 +5,10 @@
5
5
 
6
6
  module Rooibos
7
7
  module TestHelper
8
- def validate_rooibos_command!: (Runtime::_MaybeCommand) -> nil
9
- end
8
+ def validate_rooibos_command!: (Runtime::_MaybeCommand) -> nil
9
+
10
+ # Fails if any Command::Error is present in the messages array.
11
+ # Relies on Minitest's flunk method being available in the including class.
12
+ def assert_no_errors: (Array[untyped] messages, ?String? msg) -> void
13
+ end
10
14
  end