rooibos 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +9 -5
  3. data/.builds/ruby-3.3.yml +9 -5
  4. data/.builds/ruby-3.4.yml +9 -5
  5. data/.builds/ruby-4.0.0.yml +9 -5
  6. data/AGENTS.md +1 -1
  7. data/CHANGELOG.md +57 -0
  8. data/README.md +2 -2
  9. data/README.rdoc +374 -0
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +1 -1
  12. data/doc/best_practices/forms_and_validation.md +20 -0
  13. data/doc/best_practices/http_workflows.md +20 -0
  14. data/doc/best_practices/index.md +26 -0
  15. data/doc/best_practices/lists_and_tables.md +20 -0
  16. data/doc/best_practices/modal_dialogs.md +20 -0
  17. data/doc/best_practices/no_stateful_widgets.md +184 -0
  18. data/doc/best_practices/orchestration.md +20 -0
  19. data/doc/best_practices/streaming_data.md +20 -0
  20. data/doc/contributors/design/commands_and_outlets.md +1 -1
  21. data/doc/contributors/documentation_plan.md +616 -0
  22. data/doc/contributors/documentation_stub_audit.md +112 -0
  23. data/doc/contributors/documentation_style.md +275 -0
  24. data/doc/contributors/e2e_pty.md +168 -0
  25. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
  26. data/doc/contributors/specs/file_browser.md +789 -0
  27. data/doc/contributors/specs/file_browser_stories.md +774 -0
  28. data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
  29. data/doc/contributors/todo/scrollbar.md +118 -0
  30. data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
  31. data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
  32. data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
  33. data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
  34. data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
  35. data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
  36. data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
  37. data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
  38. data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
  39. data/doc/contributors/tutorial_old/12_going_further.md +20 -0
  40. data/doc/contributors/tutorial_old/index.md +20 -0
  41. data/doc/essentials/commands.md +20 -0
  42. data/doc/essentials/index.md +31 -0
  43. data/doc/essentials/messages.md +21 -0
  44. data/doc/essentials/models.md +21 -0
  45. data/doc/essentials/shortcuts.md +19 -0
  46. data/doc/essentials/the_elm_architecture.md +24 -0
  47. data/doc/essentials/the_runtime.md +21 -0
  48. data/doc/essentials/update_functions.md +20 -0
  49. data/doc/essentials/views.md +22 -0
  50. data/doc/getting_started/for_go_developers.md +16 -0
  51. data/doc/getting_started/for_python_developers.md +16 -0
  52. data/doc/getting_started/for_rails_developers.md +17 -0
  53. data/doc/getting_started/for_ratatui_ruby_developers.md +17 -0
  54. data/doc/getting_started/for_react_developers.md +17 -0
  55. data/doc/getting_started/index.md +52 -0
  56. data/doc/getting_started/install.md +20 -0
  57. data/doc/getting_started/quickstart.md +9 -45
  58. data/doc/getting_started/ruby_primer.md +19 -0
  59. data/doc/getting_started/why_rooibos.md +20 -0
  60. data/doc/index.md +79 -11
  61. data/doc/scaling_up/async_patterns.md +20 -0
  62. data/doc/scaling_up/command_composition.md +20 -0
  63. data/doc/scaling_up/custom_commands.md +21 -0
  64. data/doc/scaling_up/fractal_architecture.md +20 -0
  65. data/doc/scaling_up/index.md +30 -0
  66. data/doc/scaling_up/message_routing.md +20 -0
  67. data/doc/scaling_up/ractor_safety.md +20 -0
  68. data/doc/scaling_up/testing.md +21 -0
  69. data/doc/troubleshooting/common_errors.md +20 -0
  70. data/doc/troubleshooting/debugging.md +21 -0
  71. data/doc/troubleshooting/index.md +23 -0
  72. data/doc/troubleshooting/performance.md +20 -0
  73. data/doc/tutorial/01_project_setup.md +44 -0
  74. data/doc/tutorial/02_hello_world.md +45 -0
  75. data/doc/tutorial/03_static_file_list.md +44 -0
  76. data/doc/tutorial/04_arrow_navigation.md +47 -0
  77. data/doc/tutorial/05_real_files.md +45 -0
  78. data/doc/tutorial/06_safe_refactoring.md +21 -0
  79. data/doc/tutorial/07_red_first_tdd.md +26 -0
  80. data/doc/tutorial/08_file_metadata.md +42 -0
  81. data/doc/tutorial/09_text_preview.md +44 -0
  82. data/doc/tutorial/10_directory_tree.md +42 -0
  83. data/doc/tutorial/11_pane_focus.md +40 -0
  84. data/doc/tutorial/12_sorting.md +41 -0
  85. data/doc/tutorial/13_filtering.md +43 -0
  86. data/doc/tutorial/14_toggle_hidden.md +41 -0
  87. data/doc/tutorial/15_text_input_widget.md +43 -0
  88. data/doc/tutorial/16_rename_files.md +42 -0
  89. data/doc/tutorial/17_confirmation_dialogs.md +43 -0
  90. data/doc/tutorial/18_progress_indicators.md +43 -0
  91. data/doc/tutorial/19_atomic_operations.md +42 -0
  92. data/doc/tutorial/20_external_editor.md +42 -0
  93. data/doc/tutorial/21_modal_overlays.md +41 -0
  94. data/doc/tutorial/22_error_handling.md +43 -0
  95. data/doc/tutorial/23_terminal_capabilities.md +53 -0
  96. data/doc/tutorial/24_mouse_events.md +43 -0
  97. data/doc/tutorial/25_resize_events.md +43 -0
  98. data/doc/tutorial/26_loading_states.md +42 -0
  99. data/doc/tutorial/27_performance.md +43 -0
  100. data/doc/tutorial/28_color_schemes.md +47 -0
  101. data/doc/tutorial/29_configuration.md +124 -0
  102. data/doc/tutorial/30_going_further.md +17 -0
  103. data/doc/tutorial/index.md +17 -0
  104. data/examples/app_file_browser/app.rb +40 -0
  105. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
  106. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
  107. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
  108. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
  109. data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
  110. data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
  111. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
  112. data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
  113. data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
  114. data/examples/verify_website_first_app/app.rb +85 -0
  115. data/examples/verify_website_hello_mvu/app.rb +31 -0
  116. data/examples/widget_command_system/app.rb +15 -13
  117. data/exe/rooibos +10 -0
  118. data/generate_tutorial_stubs.rb +126 -0
  119. data/lib/rooibos/cli/commands/new.rb +373 -0
  120. data/lib/rooibos/cli/commands/run.rb +98 -0
  121. data/lib/rooibos/cli.rb +78 -0
  122. data/lib/rooibos/command/all.rb +76 -23
  123. data/lib/rooibos/command/batch.rb +61 -34
  124. data/lib/rooibos/command/custom.rb +84 -1
  125. data/lib/rooibos/command/http.rb +121 -55
  126. data/lib/rooibos/command/lifecycle.rb +5 -5
  127. data/lib/rooibos/command/open.rb +93 -0
  128. data/lib/rooibos/command/outlet.rb +105 -3
  129. data/lib/rooibos/command/wait.rb +9 -6
  130. data/lib/rooibos/command.rb +114 -89
  131. data/lib/rooibos/message/batch.rb +39 -0
  132. data/lib/rooibos/message/canceled.rb +51 -0
  133. data/lib/rooibos/message/error.rb +48 -0
  134. data/lib/rooibos/message/open.rb +30 -0
  135. data/lib/rooibos/message.rb +84 -4
  136. data/lib/rooibos/router.rb +11 -14
  137. data/lib/rooibos/runtime.rb +40 -43
  138. data/lib/rooibos/shortcuts.rb +47 -0
  139. data/lib/rooibos/test_helper.rb +71 -6
  140. data/lib/rooibos/version.rb +1 -1
  141. data/lib/rooibos/welcome.rb +237 -0
  142. data/lib/rooibos.rb +4 -3
  143. data/mise.toml +1 -1
  144. data/rbs_collection.lock.yaml +2 -2
  145. data/sig/concurrent.rbs +4 -0
  146. data/sig/gem.rbs +20 -0
  147. data/sig/rooibos/cli.rbs +42 -0
  148. data/sig/rooibos/command.rbs +59 -7
  149. data/sig/rooibos/message.rbs +66 -2
  150. data/sig/rooibos/shortcuts.rbs +14 -0
  151. data/sig/rooibos/test_helper.rbs +6 -2
  152. data/sig/rooibos/welcome.rbs +75 -0
  153. data/tasks/install.rake +29 -0
  154. data/tasks/resources/build.yml.erb +2 -0
  155. metadata +274 -38
  156. data/doc/concepts/application_architecture.md +0 -197
  157. data/doc/concepts/application_testing.md +0 -49
  158. data/doc/concepts/async_work.md +0 -164
  159. data/doc/concepts/commands.md +0 -530
  160. data/doc/concepts/message_processing.md +0 -51
  161. data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
  162. data/doc/contributors/WIP/implementation_plan.md +0 -409
  163. data/doc/contributors/WIP/init_callable_proposal.md +0 -344
  164. data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
  165. data/doc/contributors/WIP/task.md +0 -36
  166. data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
  167. data/doc/contributors/kit-no-outlet.md +0 -238
  168. data/doc/contributors/priorities.md +0 -38
  169. data/doc/images/.gitkeep +0 -0
  170. data/exe/.gitkeep +0 -0
  171. /data/doc/contributors/{WIP → design}/mvu_tea_implementations_research.md +0 -0
@@ -0,0 +1,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, *rest]
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, *rest]
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, *rest]
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.envelope, 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.envelope, 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.envelope, 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.envelope, 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 cancelled: or submitted: in model for parent to detect.
11
+ # Handles text entry. Sets canceled: or submitted: in model for parent to detect.
12
12
  module CustomShellInput
13
- Model = Data.define(:text, :cancelled, :submitted)
13
+ Model = Data.define(:text, :canceled, :submitted)
14
14
 
15
15
  Init = -> do
16
- Ractor.make_shareable(Model.new(text: "", cancelled: false, submitted: false))
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(cancelled: true), nil]
60
+ [model.with(canceled: true), nil]
61
61
 
62
62
  in _ if message.respond_to?(:enter?) && message.enter?
63
- return [model.with(cancelled: true), nil] if model.text.strip.empty?
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.cancelled
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 [{ type: :system, envelope: :disk_usage, status: 0, stdout: }]
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 [{ type: :system, envelope: :disk_usage, stderr: }]
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 [:ping, *rest]
36
- new_child, command = Ping::Update.call(rest, model.ping)
35
+ in { envelope: :ping, ** }
36
+ new_child, command = Ping::Update.call(message, model.ping)
37
37
  [model.with(ping: new_child), command]
38
- in [:uptime, *rest]
39
- new_child, command = Uptime::Update.call(rest, model.uptime)
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 [{ type: :system, envelope: :ping, status: 0, stdout: }]
35
+ in { type: :system, envelope: :ping, status: 0, stdout: }
36
36
  [model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
37
- in [{ type: :system, envelope: :ping, stderr: }]
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 [:system_info, *rest]
36
- new_child, command = SystemInfo::Update.call(rest, model.system_info)
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 [:disk_usage, *rest]
39
- new_child, command = DiskUsage::Update.call(rest, model.disk_usage)
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 [{ type: :system, envelope: :system_info, status: 0, stdout: }]
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 [{ type: :system, envelope: :system_info, stderr: }]
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 [{ type: :system, envelope: :uptime, status: 0, stdout: }]
35
+ in { type: :system, envelope: :uptime, status: 0, stdout: }
36
36
  [model.with(output: Ractor.make_shareable(stdout.strip), loading: false), nil]
37
- in [{ type: :system, envelope: :uptime, stderr: }]
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
- INITIAL = Model.new(
29
- result: "Press a key to run a command...",
30
- loading: false,
31
- last_command: nil
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
- VIEW = -> (model, tui) do
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
- UPDATE = -> (message, model) do
102
+ Update = -> (message, model) do
101
103
  case message
102
- # Handle command results
103
- in [:got_output, { stdout:, status: 0 }]
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 [:got_output, { stderr:, status: }]
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
- command = "sleep 3 && echo 'Slept for 3s'"
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
- command = "ls /nonexistent_path_12345"
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(model: INITIAL, view: VIEW, update: UPDATE)
130
+ Rooibos.run(WidgetCommandSystem)
129
131
  end
130
132
  end
131
133
 
data/exe/rooibos ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #--
5
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
6
+ # SPDX-License-Identifier: LGPL-3.0-or-later
7
+ #++
8
+
9
+ require "rooibos/cli"
10
+ Rooibos::CLI.call(ARGV)