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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +9 -5
- data/.builds/ruby-3.3.yml +9 -5
- data/.builds/ruby-3.4.yml +9 -5
- data/.builds/ruby-4.0.0.yml +9 -5
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +57 -0
- data/README.md +2 -2
- data/README.rdoc +374 -0
- data/REUSE.toml +5 -0
- data/Rakefile +1 -1
- data/doc/best_practices/forms_and_validation.md +20 -0
- data/doc/best_practices/http_workflows.md +20 -0
- data/doc/best_practices/index.md +26 -0
- data/doc/best_practices/lists_and_tables.md +20 -0
- data/doc/best_practices/modal_dialogs.md +20 -0
- data/doc/best_practices/no_stateful_widgets.md +184 -0
- data/doc/best_practices/orchestration.md +20 -0
- data/doc/best_practices/streaming_data.md +20 -0
- data/doc/contributors/design/commands_and_outlets.md +1 -1
- data/doc/contributors/documentation_plan.md +616 -0
- data/doc/contributors/documentation_stub_audit.md +112 -0
- data/doc/contributors/documentation_style.md +275 -0
- data/doc/contributors/e2e_pty.md +168 -0
- data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
- data/doc/contributors/specs/file_browser.md +789 -0
- data/doc/contributors/specs/file_browser_stories.md +774 -0
- data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
- data/doc/contributors/todo/scrollbar.md +118 -0
- data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
- data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
- data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
- data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
- data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
- data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
- data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
- data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
- data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
- data/doc/contributors/tutorial_old/12_going_further.md +20 -0
- data/doc/contributors/tutorial_old/index.md +20 -0
- data/doc/essentials/commands.md +20 -0
- data/doc/essentials/index.md +31 -0
- data/doc/essentials/messages.md +21 -0
- data/doc/essentials/models.md +21 -0
- data/doc/essentials/shortcuts.md +19 -0
- data/doc/essentials/the_elm_architecture.md +24 -0
- data/doc/essentials/the_runtime.md +21 -0
- data/doc/essentials/update_functions.md +20 -0
- data/doc/essentials/views.md +22 -0
- data/doc/getting_started/for_go_developers.md +16 -0
- data/doc/getting_started/for_python_developers.md +16 -0
- data/doc/getting_started/for_rails_developers.md +17 -0
- data/doc/getting_started/for_ratatui_ruby_developers.md +17 -0
- data/doc/getting_started/for_react_developers.md +17 -0
- data/doc/getting_started/index.md +52 -0
- data/doc/getting_started/install.md +20 -0
- data/doc/getting_started/quickstart.md +9 -45
- data/doc/getting_started/ruby_primer.md +19 -0
- data/doc/getting_started/why_rooibos.md +20 -0
- data/doc/index.md +79 -11
- data/doc/scaling_up/async_patterns.md +20 -0
- data/doc/scaling_up/command_composition.md +20 -0
- data/doc/scaling_up/custom_commands.md +21 -0
- data/doc/scaling_up/fractal_architecture.md +20 -0
- data/doc/scaling_up/index.md +30 -0
- data/doc/scaling_up/message_routing.md +20 -0
- data/doc/scaling_up/ractor_safety.md +20 -0
- data/doc/scaling_up/testing.md +21 -0
- data/doc/troubleshooting/common_errors.md +20 -0
- data/doc/troubleshooting/debugging.md +21 -0
- data/doc/troubleshooting/index.md +23 -0
- data/doc/troubleshooting/performance.md +20 -0
- data/doc/tutorial/01_project_setup.md +44 -0
- data/doc/tutorial/02_hello_world.md +45 -0
- data/doc/tutorial/03_static_file_list.md +44 -0
- data/doc/tutorial/04_arrow_navigation.md +47 -0
- data/doc/tutorial/05_real_files.md +45 -0
- data/doc/tutorial/06_safe_refactoring.md +21 -0
- data/doc/tutorial/07_red_first_tdd.md +26 -0
- data/doc/tutorial/08_file_metadata.md +42 -0
- data/doc/tutorial/09_text_preview.md +44 -0
- data/doc/tutorial/10_directory_tree.md +42 -0
- data/doc/tutorial/11_pane_focus.md +40 -0
- data/doc/tutorial/12_sorting.md +41 -0
- data/doc/tutorial/13_filtering.md +43 -0
- data/doc/tutorial/14_toggle_hidden.md +41 -0
- data/doc/tutorial/15_text_input_widget.md +43 -0
- data/doc/tutorial/16_rename_files.md +42 -0
- data/doc/tutorial/17_confirmation_dialogs.md +43 -0
- data/doc/tutorial/18_progress_indicators.md +43 -0
- data/doc/tutorial/19_atomic_operations.md +42 -0
- data/doc/tutorial/20_external_editor.md +42 -0
- data/doc/tutorial/21_modal_overlays.md +41 -0
- data/doc/tutorial/22_error_handling.md +43 -0
- data/doc/tutorial/23_terminal_capabilities.md +53 -0
- data/doc/tutorial/24_mouse_events.md +43 -0
- data/doc/tutorial/25_resize_events.md +43 -0
- data/doc/tutorial/26_loading_states.md +42 -0
- data/doc/tutorial/27_performance.md +43 -0
- data/doc/tutorial/28_color_schemes.md +47 -0
- data/doc/tutorial/29_configuration.md +124 -0
- data/doc/tutorial/30_going_further.md +17 -0
- data/doc/tutorial/index.md +17 -0
- data/examples/app_file_browser/app.rb +40 -0
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
- data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
- data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
- data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
- data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
- data/examples/verify_website_first_app/app.rb +85 -0
- data/examples/verify_website_hello_mvu/app.rb +31 -0
- data/examples/widget_command_system/app.rb +15 -13
- data/exe/rooibos +10 -0
- data/generate_tutorial_stubs.rb +126 -0
- data/lib/rooibos/cli/commands/new.rb +373 -0
- data/lib/rooibos/cli/commands/run.rb +98 -0
- data/lib/rooibos/cli.rb +78 -0
- data/lib/rooibos/command/all.rb +76 -23
- data/lib/rooibos/command/batch.rb +61 -34
- data/lib/rooibos/command/custom.rb +84 -1
- data/lib/rooibos/command/http.rb +121 -55
- data/lib/rooibos/command/lifecycle.rb +5 -5
- data/lib/rooibos/command/open.rb +93 -0
- data/lib/rooibos/command/outlet.rb +105 -3
- data/lib/rooibos/command/wait.rb +9 -6
- data/lib/rooibos/command.rb +114 -89
- data/lib/rooibos/message/batch.rb +39 -0
- data/lib/rooibos/message/canceled.rb +51 -0
- data/lib/rooibos/message/error.rb +48 -0
- data/lib/rooibos/message/open.rb +30 -0
- data/lib/rooibos/message.rb +84 -4
- data/lib/rooibos/router.rb +11 -14
- data/lib/rooibos/runtime.rb +40 -43
- data/lib/rooibos/shortcuts.rb +47 -0
- data/lib/rooibos/test_helper.rb +71 -6
- data/lib/rooibos/version.rb +1 -1
- data/lib/rooibos/welcome.rb +237 -0
- data/lib/rooibos.rb +4 -3
- data/mise.toml +1 -1
- data/rbs_collection.lock.yaml +2 -2
- data/sig/concurrent.rbs +4 -0
- data/sig/gem.rbs +20 -0
- data/sig/rooibos/cli.rbs +42 -0
- data/sig/rooibos/command.rbs +59 -7
- data/sig/rooibos/message.rbs +66 -2
- data/sig/rooibos/shortcuts.rbs +14 -0
- data/sig/rooibos/test_helper.rbs +6 -2
- data/sig/rooibos/welcome.rbs +75 -0
- data/tasks/install.rake +29 -0
- data/tasks/resources/build.yml.erb +2 -0
- metadata +274 -38
- data/doc/concepts/application_architecture.md +0 -197
- data/doc/concepts/application_testing.md +0 -49
- data/doc/concepts/async_work.md +0 -164
- data/doc/concepts/commands.md +0 -530
- data/doc/concepts/message_processing.md +0 -51
- data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
- data/doc/contributors/WIP/implementation_plan.md +0 -409
- data/doc/contributors/WIP/init_callable_proposal.md +0 -344
- data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
- data/doc/contributors/WIP/task.md +0 -36
- data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
- data/doc/contributors/kit-no-outlet.md +0 -238
- data/doc/contributors/priorities.md +0 -38
- data/doc/images/.gitkeep +0 -0
- data/exe/.gitkeep +0 -0
- /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::
|
|
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::
|
|
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
data/rbs_collection.lock.yaml
CHANGED
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
|
data/sig/rooibos/cli.rbs
ADDED
|
@@ -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
|
data/sig/rooibos/command.rbs
CHANGED
|
@@ -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
|
|
121
|
+
attr_reader envelope: Symbol
|
|
118
122
|
|
|
119
|
-
def self.new: (seconds: Float,
|
|
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: (*
|
|
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
|
|
216
|
-
attr_reader commands: Array[
|
|
249
|
+
attr_reader envelope: Symbol
|
|
250
|
+
attr_reader commands: Array[_Command]
|
|
217
251
|
attr_reader nested: bool
|
|
218
252
|
|
|
219
|
-
def self.new: (Symbol
|
|
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
|
|
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
|
data/sig/rooibos/message.rbs
CHANGED
|
@@ -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[
|
|
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[
|
|
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
|
data/sig/rooibos/shortcuts.rbs
CHANGED
|
@@ -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
|
data/sig/rooibos/test_helper.rbs
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
module Rooibos
|
|
7
7
|
module TestHelper
|
|
8
|
-
|
|
9
|
-
|
|
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
|