rooibos 0.5.0 → 0.6.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.
- 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 +46 -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_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 +25 -20
- data/lib/rooibos/command/batch.rb +26 -25
- data/lib/rooibos/command/custom.rb +84 -1
- data/lib/rooibos/command/http.rb +59 -55
- data/lib/rooibos/command/lifecycle.rb +5 -5
- data/lib/rooibos/command/open.rb +86 -0
- data/lib/rooibos/command/outlet.rb +105 -3
- data/lib/rooibos/command/wait.rb +5 -5
- data/lib/rooibos/command.rb +57 -74
- 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 +3 -0
- data/sig/gem.rbs +20 -0
- data/sig/rooibos/cli.rbs +42 -0
- data/sig/rooibos/command.rbs +48 -0
- data/sig/rooibos/message.rbs +60 -0
- 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 +272 -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,373 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "open3"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
require "pathname"
|
|
11
|
+
require "optparse"
|
|
12
|
+
|
|
13
|
+
module Rooibos
|
|
14
|
+
module CLI
|
|
15
|
+
module Commands # :nodoc:
|
|
16
|
+
# Scaffolds a new Rooibos TUI application.
|
|
17
|
+
#
|
|
18
|
+
# Starting a TUI project from scratch is tedious. Gem structure,
|
|
19
|
+
# test setup, executable wiring, and dependency management all
|
|
20
|
+
# take time before you write your first line of application code.
|
|
21
|
+
#
|
|
22
|
+
# This command delegates to +bundle gem+ for the boilerplate, then
|
|
23
|
+
# customizes the result for Rooibos. It creates a working Model-View-Update
|
|
24
|
+
# skeleton with a passing test.
|
|
25
|
+
#
|
|
26
|
+
# Use it to bootstrap new projects.
|
|
27
|
+
#
|
|
28
|
+
# === Example
|
|
29
|
+
#
|
|
30
|
+
# rooibos new my_app
|
|
31
|
+
# rooibos new my_app --no-git
|
|
32
|
+
# rooibos new my_app --test=rspec # warns about TestHelper
|
|
33
|
+
module New
|
|
34
|
+
# Default flags for bundle gem.
|
|
35
|
+
# Note: We use --no-bundle so we can add rooibos to Gemfile first.
|
|
36
|
+
BUNDLE_GEM_DEFAULTS = %w[
|
|
37
|
+
--exe
|
|
38
|
+
--no-coc
|
|
39
|
+
--changelog
|
|
40
|
+
--no-ext
|
|
41
|
+
--git
|
|
42
|
+
--no-mit
|
|
43
|
+
--test=minitest
|
|
44
|
+
--no-ci
|
|
45
|
+
--linter=rubocop
|
|
46
|
+
--no-bundle
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
# Runs the new command.
|
|
50
|
+
#
|
|
51
|
+
# [argv] Command-line arguments (expects app name as first element).
|
|
52
|
+
def self.call(argv)
|
|
53
|
+
options = parse_options(argv)
|
|
54
|
+
|
|
55
|
+
if options[:help]
|
|
56
|
+
puts usage
|
|
57
|
+
exit(0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if argv.empty?
|
|
61
|
+
warn "Error: Missing application name"
|
|
62
|
+
warn usage
|
|
63
|
+
exit(1)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
app_name = argv.shift
|
|
67
|
+
passthrough_args = argv # Remaining args passed to bundle gem
|
|
68
|
+
|
|
69
|
+
create_app(app_name, passthrough_args, options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns command-specific usage.
|
|
73
|
+
def self.usage
|
|
74
|
+
<<~USAGE
|
|
75
|
+
Usage: rooibos new <appname> [options]
|
|
76
|
+
|
|
77
|
+
Creates a new Rooibos TUI application using `bundle gem`.
|
|
78
|
+
|
|
79
|
+
Arguments:
|
|
80
|
+
<appname> Name of the application to create
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
--help, -h Show this help
|
|
84
|
+
|
|
85
|
+
Bundle Gem Defaults (can be overridden):
|
|
86
|
+
--exe Create executable (enabled by default)
|
|
87
|
+
--no-coc No Code of Conduct (default)
|
|
88
|
+
--changelog Generate CHANGELOG.md (default)
|
|
89
|
+
--no-ext No native extension (default)
|
|
90
|
+
--git Initialize git repo (default)
|
|
91
|
+
--no-mit No MIT license (default)
|
|
92
|
+
--test=minitest Use Minitest (default, recommended)
|
|
93
|
+
--no-ci No CI config (default)
|
|
94
|
+
--linter=rubocop Use RuboCop (default)
|
|
95
|
+
--bundle Run bundle install (default)
|
|
96
|
+
|
|
97
|
+
Any bundle gem option can be passed through, for example:
|
|
98
|
+
rooibos new my_app --no-git
|
|
99
|
+
rooibos new my_app --test=rspec # warns about TestHelper
|
|
100
|
+
|
|
101
|
+
Note: Rooibos::TestHelper is only verified to work with Minitest.
|
|
102
|
+
USAGE
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.parse_options(argv)
|
|
106
|
+
options = { help: false, test_framework: "minitest", skip_bundle: false }
|
|
107
|
+
|
|
108
|
+
# Extract --help before OptionParser to avoid conflicts with passthrough
|
|
109
|
+
if argv.include?("--help") || argv.include?("-h")
|
|
110
|
+
argv.delete("--help")
|
|
111
|
+
argv.delete("-h")
|
|
112
|
+
options[:help] = true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Detect test framework from passthrough args
|
|
116
|
+
argv.each do |arg|
|
|
117
|
+
if arg.start_with?("--test=")
|
|
118
|
+
options[:test_framework] = arg.sub("--test=", "")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Detect if user wants to skip bundle install
|
|
123
|
+
if argv.include?("--no-bundle")
|
|
124
|
+
options[:skip_bundle] = true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
options
|
|
128
|
+
end
|
|
129
|
+
private_class_method :parse_options
|
|
130
|
+
|
|
131
|
+
def self.create_app(app_name, passthrough_args, options)
|
|
132
|
+
puts "Creating new Rooibos application: #{app_name}"
|
|
133
|
+
|
|
134
|
+
# Warn about non-minitest frameworks
|
|
135
|
+
test_framework = options[:test_framework].to_s
|
|
136
|
+
if %w[rspec test-unit].include?(test_framework)
|
|
137
|
+
warn "Warning: Rooibos::TestHelper has not been verified to work with #{test_framework}."
|
|
138
|
+
warn " You may need to adapt the test helpers for your framework."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Build bundle gem command
|
|
142
|
+
bundle_args = build_bundle_gem_args(passthrough_args)
|
|
143
|
+
cmd = ["bundle", "gem", app_name] + bundle_args
|
|
144
|
+
|
|
145
|
+
puts "Running: #{cmd.join(' ')}"
|
|
146
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
147
|
+
unless status.success?
|
|
148
|
+
warn "Error running bundle gem:"
|
|
149
|
+
warn stderr
|
|
150
|
+
exit(1)
|
|
151
|
+
end
|
|
152
|
+
puts stdout
|
|
153
|
+
|
|
154
|
+
# Determine the normalized gem name by looking at lib/
|
|
155
|
+
app_path = Pathname.new(app_name)
|
|
156
|
+
lib_path = app_path / "lib"
|
|
157
|
+
lib_files = Dir.glob(lib_path / "*.rb")
|
|
158
|
+
gem_name = lib_files.map { |f| File.basename(f, ".rb") }
|
|
159
|
+
.reject { |n| n.end_with?("_version") }
|
|
160
|
+
.first
|
|
161
|
+
|
|
162
|
+
unless gem_name
|
|
163
|
+
warn "Could not determine gem name from lib/ directory"
|
|
164
|
+
exit(1)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
module_name = to_module_name(gem_name)
|
|
168
|
+
|
|
169
|
+
# Overwrite lib/<gem_name>.rb with Rooibos template
|
|
170
|
+
lib_file = lib_path / "#{gem_name}.rb"
|
|
171
|
+
File.write(lib_file.to_s, app_template(gem_name, module_name))
|
|
172
|
+
puts "Updated #{lib_file}"
|
|
173
|
+
|
|
174
|
+
# Update the bundler-created executable to call Rooibos.run
|
|
175
|
+
exe_file = app_path / "exe" / gem_name
|
|
176
|
+
if exe_file.exist?
|
|
177
|
+
File.write(exe_file.to_s, exe_template(gem_name, module_name))
|
|
178
|
+
puts "Updated #{exe_file}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Add rooibos as runtime dependency to gemspec (not Gemfile) for dev/prod parity
|
|
182
|
+
gemspec_files = Dir.glob(app_path / "*.gemspec")
|
|
183
|
+
if gemspec_files.any?
|
|
184
|
+
gemspec_file = gemspec_files.first
|
|
185
|
+
content = File.read(gemspec_file)
|
|
186
|
+
|
|
187
|
+
# Comment out placeholder lines that RubyGems 3.x validates
|
|
188
|
+
content = fix_gemspec_placeholders(content)
|
|
189
|
+
|
|
190
|
+
# Check if already has rooibos dependency
|
|
191
|
+
unless content.match?(/add_(?:runtime_)?dependency.*rooibos/)
|
|
192
|
+
# Use Gem::Version to handle prerelease versions correctly
|
|
193
|
+
segments = Gem::Version.new(Rooibos::VERSION).segments
|
|
194
|
+
minor_version = segments.first(2).map(&:to_s).join(".")
|
|
195
|
+
# Insert before the final 'end' of the Gem::Specification block
|
|
196
|
+
content = content.sub(
|
|
197
|
+
/^end\s*\z/m,
|
|
198
|
+
"\n # https://www.rooibos.run\n spec.add_runtime_dependency \"rooibos\", \"~> #{minor_version}\"\nend\n"
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
File.write(gemspec_file, content)
|
|
203
|
+
puts "Updated #{gemspec_file}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Run bundle install unless user passed --no-bundle
|
|
207
|
+
unless options[:skip_bundle]
|
|
208
|
+
Dir.chdir(app_path) do
|
|
209
|
+
puts "Running bundle install..."
|
|
210
|
+
system("bundle", "install")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Update test infrastructure for Rooibos
|
|
215
|
+
if test_framework == "minitest"
|
|
216
|
+
# Add rooibos/test_helper to the project's test_helper.rb
|
|
217
|
+
test_helper = app_path / "test" / "test_helper.rb"
|
|
218
|
+
if test_helper.exist?
|
|
219
|
+
content = File.read(test_helper.to_s)
|
|
220
|
+
unless content.include?("rooibos/test_helper")
|
|
221
|
+
# Add after the gem require line
|
|
222
|
+
content = content.sub(
|
|
223
|
+
/require\s+["']#{gem_name}["']/,
|
|
224
|
+
"\\0\nrequire \"rooibos/test_helper\""
|
|
225
|
+
)
|
|
226
|
+
File.write(test_helper.to_s, content)
|
|
227
|
+
puts "Updated #{test_helper}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Replace test file with real Rooibos test
|
|
232
|
+
test_file = app_path / "test" / "test_#{gem_name}.rb"
|
|
233
|
+
if test_file.exist?
|
|
234
|
+
File.write(test_file.to_s, test_template(gem_name, module_name))
|
|
235
|
+
puts "Updated #{test_file}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Make initial git commit if git is enabled and bundle didn't
|
|
240
|
+
if git_enabled?(passthrough_args)
|
|
241
|
+
make_initial_commit(app_path)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
puts "\nDone! Your Rooibos application is ready."
|
|
245
|
+
puts " cd #{app_name}"
|
|
246
|
+
puts " rooibos run"
|
|
247
|
+
end
|
|
248
|
+
private_class_method :create_app
|
|
249
|
+
|
|
250
|
+
def self.build_bundle_gem_args(passthrough_args)
|
|
251
|
+
# Start with defaults
|
|
252
|
+
args = BUNDLE_GEM_DEFAULTS.dup
|
|
253
|
+
|
|
254
|
+
# Override with user-provided args (later args win in bundle gem)
|
|
255
|
+
args + passthrough_args
|
|
256
|
+
end
|
|
257
|
+
private_class_method :build_bundle_gem_args
|
|
258
|
+
|
|
259
|
+
def self.git_enabled?(passthrough_args)
|
|
260
|
+
# Git is enabled by default unless --no-git is passed
|
|
261
|
+
!passthrough_args.include?("--no-git")
|
|
262
|
+
end
|
|
263
|
+
private_class_method :git_enabled?
|
|
264
|
+
|
|
265
|
+
def self.make_initial_commit(app_path)
|
|
266
|
+
Dir.chdir(app_path) do
|
|
267
|
+
# Check if there are uncommitted changes
|
|
268
|
+
_, _, status = Open3.capture3("git", "status", "--porcelain")
|
|
269
|
+
return unless status.success?
|
|
270
|
+
|
|
271
|
+
uncommitted, = Open3.capture3("git", "status", "--porcelain")
|
|
272
|
+
return if uncommitted.strip.empty?
|
|
273
|
+
|
|
274
|
+
# Stage and commit
|
|
275
|
+
system("git", "add", "-A", out: File::NULL, err: File::NULL)
|
|
276
|
+
system("git", "commit", "-m", "Hello, Rooibos!\n\nhttps://www.rooibos.run",
|
|
277
|
+
out: File::NULL, err: File::NULL)
|
|
278
|
+
puts "Created initial git commit"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
private_class_method :make_initial_commit
|
|
282
|
+
|
|
283
|
+
def self.to_module_name(gem_name)
|
|
284
|
+
gem_name.split(/[-_]/).map(&:capitalize).join
|
|
285
|
+
end
|
|
286
|
+
private_class_method :to_module_name
|
|
287
|
+
|
|
288
|
+
# Fixes gemspec placeholder lines that RubyGems 3.x+ rejects.
|
|
289
|
+
# Required fields get valid placeholder values; optional metadata block is deleted.
|
|
290
|
+
def self.fix_gemspec_placeholders(content)
|
|
291
|
+
result = content
|
|
292
|
+
|
|
293
|
+
# Replace required field TODOs with valid placeholders
|
|
294
|
+
result = result.gsub(
|
|
295
|
+
/^(\s*spec\.summary\s*=\s*)"TODO:[^"]*"/,
|
|
296
|
+
'\1"A Rooibos TUI application"'
|
|
297
|
+
)
|
|
298
|
+
result = result.gsub(
|
|
299
|
+
/^(\s*spec\.description\s*=\s*)"TODO:[^"]*"/,
|
|
300
|
+
'\1"A terminal user interface application built with Rooibos"'
|
|
301
|
+
)
|
|
302
|
+
result = result.gsub(
|
|
303
|
+
/^(\s*spec\.homepage\s*=\s*)"TODO:[^"]*"/,
|
|
304
|
+
'\1"https://www.rooibos.run"'
|
|
305
|
+
)
|
|
306
|
+
result = result.gsub(
|
|
307
|
+
/^(\s*spec\.authors\s*=\s*)\["TODO:[^\]]*"\]/,
|
|
308
|
+
'\1["Author"]'
|
|
309
|
+
)
|
|
310
|
+
result = result.gsub(
|
|
311
|
+
/^(\s*spec\.email\s*=\s*)\["TODO:[^\]]*"\]/,
|
|
312
|
+
'\1["author@example.com"]'
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Delete the entire metadata block (optional and causes validation issues)
|
|
316
|
+
result.gsub(/^\s*spec\.metadata\["[^"]*"\]\s*=.*\n/, "")
|
|
317
|
+
end
|
|
318
|
+
private_class_method :fix_gemspec_placeholders
|
|
319
|
+
|
|
320
|
+
def self.exe_template(gem_name, module_name)
|
|
321
|
+
<<~RUBY
|
|
322
|
+
#!/usr/bin/env ruby
|
|
323
|
+
# frozen_string_literal: true
|
|
324
|
+
|
|
325
|
+
require "#{gem_name}"
|
|
326
|
+
Rooibos.run(#{module_name})
|
|
327
|
+
RUBY
|
|
328
|
+
end
|
|
329
|
+
private_class_method :exe_template
|
|
330
|
+
|
|
331
|
+
def self.app_template(gem_name, module_name)
|
|
332
|
+
<<~RUBY
|
|
333
|
+
# frozen_string_literal: true
|
|
334
|
+
|
|
335
|
+
require "rooibos"
|
|
336
|
+
require "rooibos/welcome"
|
|
337
|
+
|
|
338
|
+
# To get started, replace the following lines with your own code
|
|
339
|
+
# and remove the `require "rooibos/welcome"` line from above
|
|
340
|
+
module #{module_name}
|
|
341
|
+
Model = Rooibos::Welcome::Model
|
|
342
|
+
View = Rooibos::Welcome::View
|
|
343
|
+
Update = Rooibos::Welcome::Update
|
|
344
|
+
Init = Rooibos::Welcome::Init
|
|
345
|
+
end
|
|
346
|
+
RUBY
|
|
347
|
+
end
|
|
348
|
+
private_class_method :app_template
|
|
349
|
+
|
|
350
|
+
def self.test_template(gem_name, module_name)
|
|
351
|
+
<<~RUBY
|
|
352
|
+
# frozen_string_literal: true
|
|
353
|
+
|
|
354
|
+
require "test_helper"
|
|
355
|
+
|
|
356
|
+
class Test#{module_name} < Minitest::Test
|
|
357
|
+
include Rooibos::TestHelper
|
|
358
|
+
|
|
359
|
+
def test_it_exits_with_ctrl_c
|
|
360
|
+
with_test_terminal do
|
|
361
|
+
inject_key(:ctrl_c)
|
|
362
|
+
Rooibos.run(#{module_name})
|
|
363
|
+
assert true, "Should reach this point without hanging."
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
RUBY
|
|
368
|
+
end
|
|
369
|
+
private_class_method :test_template
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "pathname"
|
|
9
|
+
require "rbconfig"
|
|
10
|
+
|
|
11
|
+
module Rooibos
|
|
12
|
+
module CLI
|
|
13
|
+
module Commands
|
|
14
|
+
# Runs the Rooibos application in the current directory.
|
|
15
|
+
#
|
|
16
|
+
# Developers switch between editor and terminal constantly. Typing
|
|
17
|
+
# +bundle exec exe/my_app+ every time is tedious and error-prone.
|
|
18
|
+
#
|
|
19
|
+
# This command finds the executable in +exe/+ and runs it via bundler.
|
|
20
|
+
# It walks up the directory tree to find the project root, then
|
|
21
|
+
# executes the first executable it finds.
|
|
22
|
+
#
|
|
23
|
+
# Use it from any directory within your project.
|
|
24
|
+
#
|
|
25
|
+
# === Example
|
|
26
|
+
#
|
|
27
|
+
# rooibos run
|
|
28
|
+
module Run
|
|
29
|
+
# Runs the run command.
|
|
30
|
+
#
|
|
31
|
+
# [argv] Command-line arguments.
|
|
32
|
+
def self.call(argv)
|
|
33
|
+
if ["--help", "-h"].include?(argv.first)
|
|
34
|
+
puts usage
|
|
35
|
+
exit(0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
run_app
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns command-specific usage.
|
|
42
|
+
def self.usage # :nodoc:
|
|
43
|
+
<<~USAGE
|
|
44
|
+
Usage: rooibos run [options]
|
|
45
|
+
|
|
46
|
+
Runs the Rooibos application in the current directory.
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--help, -h Show this help
|
|
50
|
+
USAGE
|
|
51
|
+
end
|
|
52
|
+
private_class_method :usage
|
|
53
|
+
|
|
54
|
+
def self.run_app # :nodoc:
|
|
55
|
+
project_root = find_project_root
|
|
56
|
+
unless project_root
|
|
57
|
+
warn "Error: Not in a Rooibos project directory"
|
|
58
|
+
warn "Could not find a .gemspec or Gemfile"
|
|
59
|
+
exit(1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
exe_dir = project_root / "exe"
|
|
63
|
+
unless exe_dir.directory?
|
|
64
|
+
warn "Error: No exe/ directory found"
|
|
65
|
+
exit(1)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
executables = Dir.glob(exe_dir / "*").select { |f| File.executable?(f) }
|
|
69
|
+
if executables.empty?
|
|
70
|
+
warn "Error: No executable found in exe/"
|
|
71
|
+
exit(1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
executable = executables.first
|
|
75
|
+
puts "Running #{File.basename(executable)}..."
|
|
76
|
+
|
|
77
|
+
# Run with bundler/setup for gem activation, but skip bundle exec CLI
|
|
78
|
+
# to produce clean stack traces without bundler/thor frames
|
|
79
|
+
Dir.chdir(project_root)
|
|
80
|
+
exec(RbConfig.ruby, "-rbundler/setup", executable)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :run_app
|
|
83
|
+
|
|
84
|
+
def self.find_project_root # :nodoc:
|
|
85
|
+
current = Pathname.pwd
|
|
86
|
+
loop do
|
|
87
|
+
return current if (current / "Gemfile").exist?
|
|
88
|
+
return current if Dir.glob(current / "*.gemspec").any?
|
|
89
|
+
parent = current.parent
|
|
90
|
+
return nil if parent == current
|
|
91
|
+
current = parent
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
private_class_method :find_project_root
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/rooibos/cli.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require_relative "version"
|
|
9
|
+
require_relative "cli/commands/new"
|
|
10
|
+
require_relative "cli/commands/run"
|
|
11
|
+
|
|
12
|
+
module Rooibos
|
|
13
|
+
# Entry point for the Rooibos command-line interface.
|
|
14
|
+
#
|
|
15
|
+
# Rooibos provides a CLI for common development tasks. Rather than
|
|
16
|
+
# remembering incantations for each tool, use a single command.
|
|
17
|
+
#
|
|
18
|
+
# This module dispatches to subcommands. It routes <tt>new</tt> to
|
|
19
|
+
# project scaffolding and <tt>run</tt> to application execution.
|
|
20
|
+
#
|
|
21
|
+
# Use it via the +rooibos+ executable.
|
|
22
|
+
#
|
|
23
|
+
# === Example
|
|
24
|
+
#
|
|
25
|
+
# # From terminal:
|
|
26
|
+
# rooibos new my_app
|
|
27
|
+
# cd my_app
|
|
28
|
+
# rooibos run
|
|
29
|
+
#
|
|
30
|
+
# # Programmatic access:
|
|
31
|
+
# Rooibos::CLI.call(["new", "my_app"])
|
|
32
|
+
# Rooibos::CLI.call(["run"])
|
|
33
|
+
module CLI
|
|
34
|
+
# Maps command names to handler modules.
|
|
35
|
+
COMMANDS = { # :nodoc:
|
|
36
|
+
"new" => Commands::New,
|
|
37
|
+
"run" => Commands::Run,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# Entry point for the CLI.
|
|
41
|
+
#
|
|
42
|
+
# [argv] Command-line arguments array.
|
|
43
|
+
def self.call(argv)
|
|
44
|
+
command_name = argv.shift
|
|
45
|
+
|
|
46
|
+
case command_name
|
|
47
|
+
when "--version", "-v"
|
|
48
|
+
puts "Rooibos #{Rooibos::VERSION}"
|
|
49
|
+
when "--help", "-h", nil
|
|
50
|
+
puts usage
|
|
51
|
+
else
|
|
52
|
+
command = COMMANDS[command_name]
|
|
53
|
+
if command
|
|
54
|
+
command.call(argv)
|
|
55
|
+
else
|
|
56
|
+
warn "Unknown command: #{command_name}"
|
|
57
|
+
warn usage
|
|
58
|
+
exit(1)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the main usage message.
|
|
64
|
+
def self.usage
|
|
65
|
+
<<~USAGE
|
|
66
|
+
Usage: rooibos <command> [options]
|
|
67
|
+
|
|
68
|
+
Commands:
|
|
69
|
+
new <appname> Create a new Rooibos application
|
|
70
|
+
run Run the application in the current directory
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
--version, -v Show version
|
|
74
|
+
--help, -h Show this help
|
|
75
|
+
USAGE
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/rooibos/command/all.rb
CHANGED
|
@@ -11,31 +11,36 @@ module Rooibos
|
|
|
11
11
|
All = Data.define(:envelope, :commands, :nested) do
|
|
12
12
|
include Custom
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
nested
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
class << self
|
|
15
|
+
undef_method :new
|
|
16
|
+
|
|
17
|
+
def new(tag, *args)
|
|
18
|
+
# DWIM: detect nested vs splatted based on call-site arity
|
|
19
|
+
if args.size == 1 && args.first.is_a?(Array)
|
|
20
|
+
commands = args.first
|
|
21
|
+
nested = true
|
|
22
|
+
else
|
|
23
|
+
commands = args
|
|
24
|
+
nested = false
|
|
25
|
+
end
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
if RatatuiRuby::Debug.enabled?
|
|
28
|
+
commands.each do |cmd|
|
|
29
|
+
unless Ractor.shareable?(cmd)
|
|
30
|
+
raise Rooibos::Error::Invariant,
|
|
31
|
+
"Command is not Ractor-shareable: #{cmd.inspect}\n" \
|
|
32
|
+
"Use Ractor.make_shareable or a Data.define command."
|
|
33
|
+
end
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
|
-
end
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
instance = allocate
|
|
38
|
+
instance.__send__(:initialize, envelope: tag, commands: commands.freeze, nested:)
|
|
39
|
+
instance
|
|
40
|
+
end
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
# Executes the command, running all children in parallel.
|
|
39
44
|
def call(out, token)
|
|
40
45
|
# Early return for empty commands - prevents hang from zip_futures([])
|
|
41
46
|
if commands.empty?
|
|
@@ -58,7 +63,7 @@ module Rooibos
|
|
|
58
63
|
all_done = Concurrent::Promises.zip_futures(*futures)
|
|
59
64
|
Concurrent::Promises.any_event(all_done, token.origin).wait
|
|
60
65
|
|
|
61
|
-
return out.put(
|
|
66
|
+
return out.put(Message::Canceled.new(command: self)) if token.canceled?
|
|
62
67
|
|
|
63
68
|
shareable_results = Ractor.make_shareable(all_done.value!)
|
|
64
69
|
response = Message::All.new(envelope:, results: shareable_results, nested:)
|
|
@@ -15,7 +15,7 @@ module Rooibos
|
|
|
15
15
|
#
|
|
16
16
|
# This command runs children in parallel. Each child sends its own messages
|
|
17
17
|
# independently. The batch completes when all children finish or when
|
|
18
|
-
# cancellation fires. On cancellation, emits <tt>
|
|
18
|
+
# cancellation fires. On cancellation, emits <tt>Message::Canceled</tt>.
|
|
19
19
|
#
|
|
20
20
|
# Use it for parallel fetches, concurrent refreshes, or any work that
|
|
21
21
|
# does not need coordinated results.
|
|
@@ -37,39 +37,40 @@ module Rooibos
|
|
|
37
37
|
class Batch < Data.define(:commands) do
|
|
38
38
|
include Custom
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# DWIM: accept (cmd1, cmd2) or ([cmd1, cmd2])
|
|
43
|
-
commands = (args.size == 1 && args.first.is_a?(Array)) ? args.first : args
|
|
40
|
+
class << self
|
|
41
|
+
undef_method :new
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
# Initialize
|
|
44
|
+
def new(*args)
|
|
45
|
+
# DWIM: accept (cmd1, cmd2) or ([cmd1, cmd2])
|
|
46
|
+
commands = (args.size == 1 && args.first.is_a?(Array)) ? args.first : args
|
|
47
|
+
|
|
48
|
+
if RatatuiRuby::Debug.enabled?
|
|
49
|
+
commands.each do |cmd|
|
|
50
|
+
unless Ractor.shareable?(cmd)
|
|
51
|
+
raise Rooibos::Error::Invariant,
|
|
52
|
+
"Command is not Ractor-shareable: #{cmd.inspect}\n" \
|
|
53
|
+
"Use Ractor.make_shareable or a Data.define command."
|
|
54
|
+
end
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
|
-
end
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
instance = allocate
|
|
59
|
+
instance.__send__(:initialize, commands: commands.freeze)
|
|
60
|
+
instance
|
|
61
|
+
end
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
# Call it
|
|
61
65
|
def call(out, token)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
66
|
+
handles = commands.map { |cmd| out.standing(cmd, token) }
|
|
67
|
+
out.wait(*handles, token:)
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
out.put(Command.cancel(self)) if token.canceled?
|
|
69
|
+
if token.canceled?
|
|
70
|
+
out.put(Message::Canceled.new(command: self))
|
|
71
|
+
else
|
|
72
|
+
out.put(Message::Batch.new(command: self))
|
|
73
|
+
end
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|