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,124 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Configuration
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
By the end of this guide, you will:
|
|
10
|
+
|
|
11
|
+
- TODO: Write learning objectives
|
|
12
|
+
|
|
13
|
+
> ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
|
|
14
|
+
|
|
15
|
+
## User Stories
|
|
16
|
+
|
|
17
|
+
## Story 27: Configuration Management
|
|
18
|
+
|
|
19
|
+
**As a** power user
|
|
20
|
+
**I want to** save my preferences
|
|
21
|
+
**So that** my settings persist between sessions
|
|
22
|
+
|
|
23
|
+
### Acceptance Criteria
|
|
24
|
+
- Config file in ~/.config/file_browser/config.yml
|
|
25
|
+
- Configurable preferences:
|
|
26
|
+
- Default sort order
|
|
27
|
+
- Hidden files visibility default
|
|
28
|
+
- Color scheme/theme
|
|
29
|
+
- Default editor (overrides $EDITOR)
|
|
30
|
+
- Pane width ratios
|
|
31
|
+
- Mouse support enabled/disabled
|
|
32
|
+
- Config file created on first run with defaults
|
|
33
|
+
- Invalid config handled gracefully with warnings
|
|
34
|
+
- Command-line flags override config file
|
|
35
|
+
- `--config-path` flag to use alternate config location
|
|
36
|
+
|
|
37
|
+
### Notes
|
|
38
|
+
- Addresses specification section 5.3 (Configuration)
|
|
39
|
+
- Introduces YAML parsing
|
|
40
|
+
- May be deferred to 1.1 release
|
|
41
|
+
- Should have schema validation
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Future Enhancements (Deferred)
|
|
46
|
+
|
|
47
|
+
**As a** product owner
|
|
48
|
+
**I want to** track future enhancement ideas
|
|
49
|
+
**So that** we have a roadmap for post-1.0 releases
|
|
50
|
+
|
|
51
|
+
### Deferred Features
|
|
52
|
+
|
|
53
|
+
The following features are explicitly deferred to future versions:
|
|
54
|
+
|
|
55
|
+
- **Bookmarks/Favorites:** Quick access to frequently used directories
|
|
56
|
+
- **Multi-file Selection:** Select multiple files with Space, operate on batch
|
|
57
|
+
- **File Content Search:** Integrate grep for searching within files
|
|
58
|
+
- **Git Integration:** Show file status (modified, untracked, etc.)
|
|
59
|
+
- **Trash/Recycle Bin:** Soft delete instead of permanent deletion
|
|
60
|
+
- **Dual-Pane Mode:** Side-by-side panes for easier copying
|
|
61
|
+
- **Archive Preview:** View contents of zip, tar, etc.
|
|
62
|
+
- **Image Preview:** ASCII art representation of images
|
|
63
|
+
- **Custom File Type Icons:** User-defined icons for extensions
|
|
64
|
+
- **External Tool Integration:** Diff, merge, etc.
|
|
65
|
+
- **Plugins/Extensions:** Allow third-party extensions
|
|
66
|
+
- **Remote Filesystem Support:** SFTP, S3, etc.
|
|
67
|
+
|
|
68
|
+
### Notes
|
|
69
|
+
- These align with specification section 9 (Future Enhancements)
|
|
70
|
+
- Not stories yet - just ideas for future planning
|
|
71
|
+
- May become stories in 1.1, 1.2, etc.
|
|
72
|
+
- Community feedback will help prioritize
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Implementation Notes
|
|
77
|
+
|
|
78
|
+
### Story Sequencing
|
|
79
|
+
|
|
80
|
+
Stories are ordered to:
|
|
81
|
+
1. **Start with tutorial foundation** (Stories -4 to 0) - teaches Rooibos concepts incrementally
|
|
82
|
+
2. **Build walking skeleton** (Story 1) - proves end-to-end integration with real files
|
|
83
|
+
3. **Build core navigation** (Stories 2-3) - essential functionality
|
|
84
|
+
4. **Add UI structure** (Stories 4-6) - professional appearance
|
|
85
|
+
5. **Enable exploration** (Stories 7-8) - power user features
|
|
86
|
+
6. **Add organization** (Stories 9-11) - finding and filtering
|
|
87
|
+
7. **Enable modification** (Stories 12-17) - file operations
|
|
88
|
+
8. **Improve usability** (Stories 18-19) - help and errors
|
|
89
|
+
9. **Add enhancements** (Stories 20-21) - mouse and resize
|
|
90
|
+
10. **Optimize and polish** (Stories 22-24) - performance and aesthetics
|
|
91
|
+
11. **Ensure quality** (Story 25) - testing and documentation
|
|
92
|
+
12. **Advanced features** (Stories 26-27) - color schemes and configuration
|
|
93
|
+
13. **Future planning** - deferred enhancements
|
|
94
|
+
|
|
95
|
+
### Story Sizing
|
|
96
|
+
|
|
97
|
+
- Stories -4 to -1: Extra Small (tutorial foundation)
|
|
98
|
+
- Story 0: Small (critical transition)
|
|
99
|
+
- Stories 1-3: Small
|
|
100
|
+
- Stories 4-8: Medium
|
|
101
|
+
- Stories 9-17: Small-Medium
|
|
102
|
+
- Stories 18-21: Medium
|
|
103
|
+
- Stories 22-24: Large
|
|
104
|
+
- Story 25: Ongoing (parallel with all stories)
|
|
105
|
+
- Story 26: Medium
|
|
106
|
+
- Story 27: Small-Medium
|
|
107
|
+
|
|
108
|
+
### Dependencies
|
|
109
|
+
|
|
110
|
+
- Story -3 depends on Story -4 (need project setup)
|
|
111
|
+
- Story -2 depends on Story -3 (need runnable app)
|
|
112
|
+
- Story -1 depends on Story -2 (need model and state)
|
|
113
|
+
- Story 0 depends on Story -1 (need navigation working with fake data)
|
|
114
|
+
- Story 1 depends on Story 0 (Story 0 completes Story 1)
|
|
115
|
+
- Story 4 depends on Stories 1-3 (need basic functionality before layout)
|
|
116
|
+
- Story 6 depends on Story 4 (need preview pane)
|
|
117
|
+
- Story 8 depends on Story 4 (need multiple panes)
|
|
118
|
+
- Stories 12-17 can be done in any order after Story 11
|
|
119
|
+
- Story 21 depends on Story 4 (need layout to resize)
|
|
120
|
+
- Story 24 should be done after most features complete
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
[**Previous:** Color Schemes](./28_color_schemes.md) | [**Next:** Going Further](./30_going_further.md)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Going Further
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
By the end of this guide, you will:
|
|
10
|
+
|
|
11
|
+
- TODO: Write learning objectives
|
|
12
|
+
|
|
13
|
+
> ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
[**Previous:** Configuration](./29_configuration.md) |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Tutorial: Build a File Browser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
By the end of this guide, you will:
|
|
10
|
+
|
|
11
|
+
- TODO: Write learning objectives
|
|
12
|
+
|
|
13
|
+
> ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
| [**Next:** Project Setup](./01_project_setup.md)
|
|
@@ -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
|
|