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,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
require "rooibos"
|
|
10
|
+
|
|
11
|
+
module Tutorial01
|
|
12
|
+
module FileBrowser
|
|
13
|
+
Model = Data.define(:current_directory, :file_names)
|
|
14
|
+
|
|
15
|
+
View = -> (model, tui) {
|
|
16
|
+
tui.layout(children: [
|
|
17
|
+
tui.paragraph(text: model.current_directory),
|
|
18
|
+
tui.list(items: model.file_names),
|
|
19
|
+
])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Update = -> (message, model) {
|
|
23
|
+
if message.ctrl_c? or message.q?
|
|
24
|
+
Rooibos::Command.exit
|
|
25
|
+
else
|
|
26
|
+
model
|
|
27
|
+
end
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Init = -> {
|
|
31
|
+
current_directory = Dir.pwd
|
|
32
|
+
file_names = Dir.children(current_directory)
|
|
33
|
+
Ractor.make_shareable Model.new(current_directory, file_names)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if __FILE__ == $0
|
|
39
|
+
Rooibos.run(Tutorial01::FileBrowser)
|
|
40
|
+
end
|
|
@@ -28,17 +28,17 @@ module DashboardManual
|
|
|
28
28
|
# modal is active. Only user input (keys/mouse) should be blocked.
|
|
29
29
|
case message
|
|
30
30
|
# Route command results to panels
|
|
31
|
-
in [:stats,
|
|
31
|
+
in [:stats, rest]
|
|
32
32
|
new_panel, command = StatsPanel::Update.call(rest, model.stats)
|
|
33
33
|
mapped_command = command ? Command.map(command) { |child_result| [:stats, *child_result] } : nil
|
|
34
34
|
return [model.with(stats: new_panel), mapped_command]
|
|
35
35
|
|
|
36
|
-
in [:network,
|
|
36
|
+
in [:network, rest]
|
|
37
37
|
new_panel, command = NetworkPanel::Update.call(rest, model.network)
|
|
38
38
|
mapped_command = command ? Command.map(command) { |child_result| [:network, *child_result] } : nil
|
|
39
39
|
return [model.with(network: new_panel), mapped_command]
|
|
40
40
|
|
|
41
|
-
in [:shell_output,
|
|
41
|
+
in [:shell_output, rest]
|
|
42
42
|
# Route streaming command output to modal
|
|
43
43
|
new_modal, command = CustomShellModal::Update.call(message, model.shell_modal)
|
|
44
44
|
return [model.with(shell_modal: new_modal), command]
|
|
@@ -61,22 +61,22 @@ module DashboardManual
|
|
|
61
61
|
[model.with(shell_modal: CustomShellModal.open), nil]
|
|
62
62
|
|
|
63
63
|
in _ if message.s?
|
|
64
|
-
command = Command.map(SystemInfo.fetch_command) { |batch| [:stats, batch
|
|
64
|
+
command = Command.map(SystemInfo.fetch_command) { |batch| [:stats, batch] }
|
|
65
65
|
new_stats = model.stats.with(system_info: model.stats.system_info.with(loading: true))
|
|
66
66
|
[model.with(stats: new_stats), command]
|
|
67
67
|
|
|
68
68
|
in _ if message.d?
|
|
69
|
-
command = Command.map(DiskUsage.fetch_command) { |batch| [:stats, batch
|
|
69
|
+
command = Command.map(DiskUsage.fetch_command) { |batch| [:stats, batch] }
|
|
70
70
|
new_stats = model.stats.with(disk_usage: model.stats.disk_usage.with(loading: true))
|
|
71
71
|
[model.with(stats: new_stats), command]
|
|
72
72
|
|
|
73
73
|
in _ if message.p?
|
|
74
|
-
command = Command.map(Ping.fetch_command) { |batch| [:network, batch
|
|
74
|
+
command = Command.map(Ping.fetch_command) { |batch| [:network, batch] }
|
|
75
75
|
new_network = model.network.with(ping: model.network.ping.with(loading: true))
|
|
76
76
|
[model.with(network: new_network), command]
|
|
77
77
|
|
|
78
78
|
in _ if message.u?
|
|
79
|
-
command = Command.map(Uptime.fetch_command) { |batch| [:network, batch
|
|
79
|
+
command = Command.map(Uptime.fetch_command) { |batch| [:network, batch] }
|
|
80
80
|
new_network = model.network.with(uptime: model.network.uptime.with(loading: true))
|
|
81
81
|
[model.with(network: new_network), command]
|
|
82
82
|
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
require "rooibos"
|
|
9
9
|
# Text input fragment for custom shell command modal.
|
|
10
10
|
#
|
|
11
|
-
# Handles text entry. Sets
|
|
11
|
+
# Handles text entry. Sets canceled: or submitted: in model for parent to detect.
|
|
12
12
|
module CustomShellInput
|
|
13
|
-
Model = Data.define(:text, :
|
|
13
|
+
Model = Data.define(:text, :canceled, :submitted)
|
|
14
14
|
|
|
15
15
|
Init = -> do
|
|
16
|
-
Ractor.make_shareable(Model.new(text: "",
|
|
16
|
+
Ractor.make_shareable(Model.new(text: "", canceled: false, submitted: false))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
View = -> (model, tui) do
|
|
@@ -57,10 +57,10 @@ module CustomShellInput
|
|
|
57
57
|
Update = -> (message, model) do
|
|
58
58
|
case message
|
|
59
59
|
in _ if message.respond_to?(:esc?) && message.esc?
|
|
60
|
-
[model.with(
|
|
60
|
+
[model.with(canceled: true), nil]
|
|
61
61
|
|
|
62
62
|
in _ if message.respond_to?(:enter?) && message.enter?
|
|
63
|
-
return [model.with(
|
|
63
|
+
return [model.with(canceled: true), nil] if model.text.strip.empty?
|
|
64
64
|
[model.with(submitted: true), nil]
|
|
65
65
|
|
|
66
66
|
in _ if message.respond_to?(:backspace?) && message.backspace?
|
|
@@ -37,7 +37,7 @@ module CustomShellModal
|
|
|
37
37
|
# Delegate first, then check if user wants to close
|
|
38
38
|
new_input, _cmd = CustomShellInput::Update.call(message, model.input)
|
|
39
39
|
|
|
40
|
-
if new_input.
|
|
40
|
+
if new_input.canceled
|
|
41
41
|
[Init.(), nil]
|
|
42
42
|
elsif new_input.submitted
|
|
43
43
|
shell_cmd = new_input.text
|
|
@@ -32,9 +32,9 @@ module DiskUsage
|
|
|
32
32
|
|
|
33
33
|
Update = -> (message, model) do
|
|
34
34
|
case message
|
|
35
|
-
in
|
|
35
|
+
in { type: :system, envelope: :disk_usage, status: 0, stdout: }
|
|
36
36
|
[model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
|
|
37
|
-
in
|
|
37
|
+
in { type: :system, envelope: :disk_usage, stderr: }
|
|
38
38
|
[model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
|
|
39
39
|
else
|
|
40
40
|
[model, nil]
|
|
@@ -32,11 +32,11 @@ module NetworkPanel
|
|
|
32
32
|
|
|
33
33
|
Update = -> (message, model) do
|
|
34
34
|
case message
|
|
35
|
-
in
|
|
36
|
-
new_child, command = Ping::Update.call(
|
|
35
|
+
in { envelope: :ping, ** }
|
|
36
|
+
new_child, command = Ping::Update.call(message, model.ping)
|
|
37
37
|
[model.with(ping: new_child), command]
|
|
38
|
-
in
|
|
39
|
-
new_child, command = Uptime::Update.call(
|
|
38
|
+
in { envelope: :uptime, ** }
|
|
39
|
+
new_child, command = Uptime::Update.call(message, model.uptime)
|
|
40
40
|
[model.with(uptime: new_child), command]
|
|
41
41
|
else
|
|
42
42
|
[model, nil]
|
|
@@ -32,9 +32,9 @@ module Ping
|
|
|
32
32
|
|
|
33
33
|
Update = -> (message, model) do
|
|
34
34
|
case message
|
|
35
|
-
in
|
|
35
|
+
in { type: :system, envelope: :ping, status: 0, stdout: }
|
|
36
36
|
[model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
|
|
37
|
-
in
|
|
37
|
+
in { type: :system, envelope: :ping, stderr: }
|
|
38
38
|
[model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
|
|
39
39
|
else
|
|
40
40
|
[model, nil]
|
|
@@ -32,11 +32,11 @@ module StatsPanel
|
|
|
32
32
|
|
|
33
33
|
Update = -> (message, model) do
|
|
34
34
|
case message
|
|
35
|
-
in
|
|
36
|
-
new_child, command = SystemInfo::Update.call(
|
|
35
|
+
in { envelope: :system_info, ** }
|
|
36
|
+
new_child, command = SystemInfo::Update.call(message, model.system_info)
|
|
37
37
|
[model.with(system_info: new_child), command]
|
|
38
|
-
in
|
|
39
|
-
new_child, command = DiskUsage::Update.call(
|
|
38
|
+
in { envelope: :disk_usage, ** }
|
|
39
|
+
new_child, command = DiskUsage::Update.call(message, model.disk_usage)
|
|
40
40
|
[model.with(disk_usage: new_child), command]
|
|
41
41
|
else
|
|
42
42
|
[model, nil]
|
|
@@ -32,9 +32,9 @@ module SystemInfo
|
|
|
32
32
|
|
|
33
33
|
Update = -> (message, model) do
|
|
34
34
|
case message
|
|
35
|
-
in
|
|
35
|
+
in { type: :system, envelope: :system_info, status: 0, stdout: }
|
|
36
36
|
[model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
|
|
37
|
-
in
|
|
37
|
+
in { type: :system, envelope: :system_info, stderr: }
|
|
38
38
|
[model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
|
|
39
39
|
else
|
|
40
40
|
[model, nil]
|
|
@@ -32,9 +32,9 @@ module Uptime
|
|
|
32
32
|
|
|
33
33
|
Update = -> (message, model) do
|
|
34
34
|
case message
|
|
35
|
-
in
|
|
35
|
+
in { type: :system, envelope: :uptime, status: 0, stdout: }
|
|
36
36
|
[model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
|
|
37
|
-
in
|
|
37
|
+
in { type: :system, envelope: :uptime, stderr: }
|
|
38
38
|
[model.with(output: Ractor.make_shareable("Error: #{stderr.strip}"), loading: false), nil]
|
|
39
39
|
else
|
|
40
40
|
[model, nil]
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
require "rooibos"
|
|
9
|
+
|
|
10
|
+
module FileBrowser
|
|
11
|
+
# Model: What state does your app need?
|
|
12
|
+
Model = Data.define(:path, :entries, :selected, :error)
|
|
13
|
+
|
|
14
|
+
Init = -> {
|
|
15
|
+
path = Dir.pwd
|
|
16
|
+
entries = Entries[path]
|
|
17
|
+
Ractor.make_shareable( # Ensures thread safety
|
|
18
|
+
Model.new(path:, entries:, selected: entries.first, error: nil))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
View = -> (model, tui) {
|
|
22
|
+
tui.block(
|
|
23
|
+
titles: [
|
|
24
|
+
model.error || model.path,
|
|
25
|
+
{ content: KEYS, position: :bottom, alignment: :right },
|
|
26
|
+
],
|
|
27
|
+
borders: [:all],
|
|
28
|
+
border_style: if model.error then tui.style(fg: :red) else nil end,
|
|
29
|
+
children: [
|
|
30
|
+
tui.list(items: model.entries.map(&ListItem[model, tui]),
|
|
31
|
+
selected_index: model.entries.index(model.selected),
|
|
32
|
+
highlight_symbol: "",
|
|
33
|
+
highlight_style: tui.style(modifiers: [:reversed])),
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Update = -> (message, model) {
|
|
39
|
+
return model.with(error: ERROR) if message.error?
|
|
40
|
+
model = model.with(error: nil) if model.error && message.key?
|
|
41
|
+
|
|
42
|
+
if message.ctrl_c? || message.q? then Rooibos::Command.exit
|
|
43
|
+
elsif message.home? || message.g? then model.with(selected: model.entries.first)
|
|
44
|
+
elsif message.end? || message.G? then model.with(selected: model.entries.last)
|
|
45
|
+
elsif message.up_arrow? || message.k? then Select[:-, model]
|
|
46
|
+
elsif message.down_arrow? || message.j? then Select[:+, model]
|
|
47
|
+
elsif message.enter? then Open[model]
|
|
48
|
+
elsif message.escape? then Navigate[File.dirname(model.path), model]
|
|
49
|
+
end
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
KEYS = "↑/↓/Home/End: Select | Enter: Open | Esc: Navigate Up | q: Quit"
|
|
53
|
+
ERROR = "Sorry, opening the selected file failed."
|
|
54
|
+
|
|
55
|
+
ListItem = -> (model, tui) {
|
|
56
|
+
-> (name) {
|
|
57
|
+
modifiers = name.start_with?(".") ? [:dim] : []
|
|
58
|
+
fg = :blue if name.end_with?("/")
|
|
59
|
+
tui.list_item(content: name, style: tui.style(fg:, modifiers:))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Select = -> (operator, model) {
|
|
64
|
+
new_index = model.entries.index(model.selected).public_send(operator, 1)
|
|
65
|
+
model.with(selected: model.entries[new_index.clamp(0, model.entries.length - 1)])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Open = -> (model) {
|
|
69
|
+
full = File.join(model.path, model.selected.delete_suffix("/"))
|
|
70
|
+
model.selected.end_with?("/") ? Navigate[full, model] : Rooibos::Command.open(full)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Navigate = -> (path, model) {
|
|
74
|
+
entries = Entries[path]
|
|
75
|
+
model.with(path:, entries:, selected: entries.first, error: nil)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Entries = -> (path) {
|
|
79
|
+
Dir.children(path).map { |name|
|
|
80
|
+
File.directory?(File.join(path, name)) ? "#{name}/" : name
|
|
81
|
+
}.sort_by { |name| [name.end_with?("/") ? 0 : 1, name.downcase] }
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
Rooibos.run(FileBrowser) if __FILE__ == $0
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
require "rooibos"
|
|
9
|
+
|
|
10
|
+
module Counter
|
|
11
|
+
# Init: How do you create the initial model?
|
|
12
|
+
Init = -> { 0 }
|
|
13
|
+
|
|
14
|
+
# View: What does the user see?
|
|
15
|
+
View = -> (model, tui) { tui.paragraph(text: <<~END) }
|
|
16
|
+
Current count: #{model}.
|
|
17
|
+
Press any key to increment.
|
|
18
|
+
Press Ctrl+C to quit.
|
|
19
|
+
END
|
|
20
|
+
|
|
21
|
+
# Update: What happens when things change?
|
|
22
|
+
Update = -> (message, model) {
|
|
23
|
+
if message.ctrl_c?
|
|
24
|
+
Rooibos::Command.exit
|
|
25
|
+
elsif message.key?
|
|
26
|
+
model + 1
|
|
27
|
+
end
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Rooibos.run(Counter) if __FILE__ == $0
|
|
@@ -25,13 +25,15 @@ require "rooibos"
|
|
|
25
25
|
# rdoc-image:/doc/images/widget_cmd_exec.png
|
|
26
26
|
class WidgetCommandSystem
|
|
27
27
|
Model = Data.define(:result, :loading, :last_command)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
Init = -> {
|
|
29
|
+
Model.new(
|
|
30
|
+
result: "Press a key to run a command...",
|
|
31
|
+
loading: false,
|
|
32
|
+
last_command: nil
|
|
33
|
+
)
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
View = -> (model, tui) do
|
|
35
37
|
hotkey_style = tui.style(modifiers: [:bold, :underlined])
|
|
36
38
|
dim_style = tui.style(fg: :dark_gray)
|
|
37
39
|
|
|
@@ -97,12 +99,12 @@ class WidgetCommandSystem
|
|
|
97
99
|
)
|
|
98
100
|
end
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
Update = -> (message, model) do
|
|
101
103
|
case message
|
|
102
|
-
# Handle command results
|
|
103
|
-
in
|
|
104
|
+
# Handle command results (hash-based pattern matching)
|
|
105
|
+
in { type: :system, envelope: :got_output, stdout:, status: 0 }
|
|
104
106
|
[model.with(result: stdout.strip.freeze, loading: false), nil]
|
|
105
|
-
in
|
|
107
|
+
in { type: :system, envelope: :got_output, stderr:, status: }
|
|
106
108
|
[model.with(result: "Error (exit #{status}): #{stderr.strip}".freeze, loading: false), nil]
|
|
107
109
|
|
|
108
110
|
# Handle key presses
|
|
@@ -113,11 +115,11 @@ class WidgetCommandSystem
|
|
|
113
115
|
in _ if message.u?
|
|
114
116
|
[model.with(loading: true, last_command: "uname -a"), Rooibos::Command.system("uname -a", :got_output)]
|
|
115
117
|
in _ if message.s?
|
|
116
|
-
|
|
118
|
+
cmd = "sleep 3 && echo 'Slept for 3s'"
|
|
117
119
|
[model.with(loading: true, last_command: cmd.freeze), Rooibos::Command.system(cmd, :got_output)]
|
|
118
120
|
in _ if message.f?
|
|
119
121
|
# Intentional failure to demonstrate error handling
|
|
120
|
-
|
|
122
|
+
cmd = "ls /nonexistent_path_12345"
|
|
121
123
|
[model.with(loading: true, last_command: cmd.freeze), Rooibos::Command.system(cmd, :got_output)]
|
|
122
124
|
else
|
|
123
125
|
model
|
|
@@ -125,7 +127,7 @@ class WidgetCommandSystem
|
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
def run
|
|
128
|
-
Rooibos.run(
|
|
130
|
+
Rooibos.run(WidgetCommandSystem)
|
|
129
131
|
end
|
|
130
132
|
end
|
|
131
133
|
|
data/exe/rooibos
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
# Parse tutorial structure from documentation_plan.md
|
|
7
|
+
TUTORIAL_FILES = {
|
|
8
|
+
"index.md" => { title: "Tutorial: Build a File Browser", story: nil },
|
|
9
|
+
"01_project_setup.md" => { title: "Project Setup", story: "-4" },
|
|
10
|
+
"02_hello_world.md" => { title: "Hello World", story: "-3" },
|
|
11
|
+
"03_static_file_list.md" => { title: "Static File List", story: "-2" },
|
|
12
|
+
"04_arrow_navigation.md" => { title: "Arrow Navigation", story: "-1" },
|
|
13
|
+
"05_real_files.md" => { title: "Real Files", story: "0" },
|
|
14
|
+
"06_safe_refactoring.md" => { title: "Safe Refactoring", story: "4a" },
|
|
15
|
+
"07_red_first_tdd.md" => { title: "Red-First TDD", story: "4b" },
|
|
16
|
+
"08_file_metadata.md" => { title: "File Metadata", story: "5" },
|
|
17
|
+
"09_text_preview.md" => { title: "Text Preview", story: "6" },
|
|
18
|
+
"10_directory_tree.md" => { title: "Directory Tree", story: "7" },
|
|
19
|
+
"11_pane_focus.md" => { title: "Pane Focus", story: "8" },
|
|
20
|
+
"12_sorting.md" => { title: "Sorting", story: "9" },
|
|
21
|
+
"13_filtering.md" => { title: "Filtering", story: "10" },
|
|
22
|
+
"14_toggle_hidden.md" => { title: "Toggle Hidden Files", story: "11" },
|
|
23
|
+
"15_text_input_widget.md" => { title: "Text Input Widget", story: "12" },
|
|
24
|
+
"16_rename_files.md" => { title: "Rename Files", story: "13" },
|
|
25
|
+
"17_confirmation_dialogs.md" => { title: "Confirmation Dialogs", story: "14" },
|
|
26
|
+
"18_progress_indicators.md" => { title: "Progress Indicators", story: "15" },
|
|
27
|
+
"19_atomic_operations.md" => { title: "Atomic Operations", story: "16" },
|
|
28
|
+
"20_external_editor.md" => { title: "External Editor", story: "17" },
|
|
29
|
+
"21_modal_overlays.md" => { title: "Modal Overlays", story: "18" },
|
|
30
|
+
"22_error_handling.md" => { title: "Error Handling", story: "19" },
|
|
31
|
+
"23_terminal_capabilities.md" => { title: "Terminal Capabilities", story: "23" },
|
|
32
|
+
"24_mouse_events.md" => { title: "Mouse Events", story: "20" },
|
|
33
|
+
"25_resize_events.md" => { title: "Resize Events", story: "21" },
|
|
34
|
+
"26_loading_states.md" => { title: "Loading States", story: "22" },
|
|
35
|
+
"27_performance.md" => { title: "Performance", story: "24" },
|
|
36
|
+
"28_color_schemes.md" => { title: "Color Schemes", story: "26" },
|
|
37
|
+
"29_configuration.md" => { title: "Configuration", story: "27" },
|
|
38
|
+
"30_going_further.md" => { title: "Going Further", story: nil },
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Parse stories from file_browser_stories.md
|
|
42
|
+
stories_content = File.read("doc/contributors/specs/file_browser_stories.md")
|
|
43
|
+
|
|
44
|
+
# Extract each story
|
|
45
|
+
stories = {}
|
|
46
|
+
current_story = nil
|
|
47
|
+
current_content = []
|
|
48
|
+
|
|
49
|
+
stories_content.each_line do |line|
|
|
50
|
+
if line =~ /^## Story (-?\d+[ab]?): (.+)$/
|
|
51
|
+
# Save previous story
|
|
52
|
+
if current_story
|
|
53
|
+
stories[current_story] = current_content.join
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
current_story = $1
|
|
57
|
+
current_content = [line]
|
|
58
|
+
elsif current_story
|
|
59
|
+
current_content << line
|
|
60
|
+
|
|
61
|
+
# Stop at the next story or end of stories section
|
|
62
|
+
if line =~ /^---$/ && current_content.size > 5
|
|
63
|
+
# Check if next section is another story or implementation notes
|
|
64
|
+
peek_ahead = stories_content.lines[stories_content.lines.index(line) + 1]
|
|
65
|
+
if peek_ahead && !peek_ahead.start_with?("## Story")
|
|
66
|
+
stories[current_story] = current_content.join
|
|
67
|
+
break if peek_ahead.start_with?("## Implementation Notes")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Save last story
|
|
74
|
+
stories[current_story] = current_content.join if current_story
|
|
75
|
+
|
|
76
|
+
# Create tutorial directory
|
|
77
|
+
FileUtils.mkdir_p("doc/tutorial")
|
|
78
|
+
|
|
79
|
+
# Generate stub files
|
|
80
|
+
TUTORIAL_FILES.each do |filename, meta|
|
|
81
|
+
filepath = "doc/tutorial/#{filename}"
|
|
82
|
+
|
|
83
|
+
# Determine previous/next files
|
|
84
|
+
files_list = TUTORIAL_FILES.keys
|
|
85
|
+
current_index = files_list.index(filename)
|
|
86
|
+
prev_file = (current_index > 0) ? files_list[current_index - 1] : nil
|
|
87
|
+
next_file = (current_index < files_list.size - 1) ? files_list[current_index + 1] : nil
|
|
88
|
+
|
|
89
|
+
prev_title = prev_file ? TUTORIAL_FILES[prev_file][:title] : nil
|
|
90
|
+
next_title = next_file ? TUTORIAL_FILES[next_file][:title] : nil
|
|
91
|
+
|
|
92
|
+
# Get story content if applicable
|
|
93
|
+
story_section = ""
|
|
94
|
+
if meta[:story]
|
|
95
|
+
story_key = meta[:story]
|
|
96
|
+
if stories[story_key]
|
|
97
|
+
story_section = "\n## User Stories\n\n#{stories[story_key]}\n"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Generate stub content
|
|
102
|
+
content = <<~MARKDOWN
|
|
103
|
+
<!--
|
|
104
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
105
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
106
|
+
-->
|
|
107
|
+
|
|
108
|
+
# #{meta[:title]}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
By the end of this guide, you will:
|
|
112
|
+
|
|
113
|
+
- TODO: Write learning objectives
|
|
114
|
+
|
|
115
|
+
> ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
|
|
116
|
+
#{story_section}
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
#{prev_file ? "[**Previous:** #{prev_title}](./#{prev_file})" : ''} | #{next_file ? "[**Next:** #{next_title}](./#{next_file})" : ''}
|
|
120
|
+
MARKDOWN
|
|
121
|
+
|
|
122
|
+
File.write(filepath, content)
|
|
123
|
+
puts "Created #{filepath}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
puts "\nDone! Created #{TUTORIAL_FILES.size} tutorial stub files."
|