rooibos 0.6.2 → 0.7.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.
Files changed (217) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSES/BSD-2-Clause.txt +9 -0
  3. data/REUSE.toml +5 -0
  4. data/exe/.gitkeep +0 -0
  5. data/lib/rooibos/cli/commands/new.rb +24 -0
  6. data/lib/rooibos/command/batch.rb +10 -0
  7. data/lib/rooibos/command/bubble.rb +34 -0
  8. data/lib/rooibos/command/custom.rb +3 -2
  9. data/lib/rooibos/command/deliver.rb +50 -0
  10. data/lib/rooibos/command/http.rb +1 -1
  11. data/lib/rooibos/command/lifecycle.rb +3 -1
  12. data/lib/rooibos/command/outlet.rb +19 -9
  13. data/lib/rooibos/command.rb +107 -3
  14. data/lib/rooibos/configuration.rb +29 -0
  15. data/lib/rooibos/message/bubbled.rb +29 -0
  16. data/lib/rooibos/message.rb +24 -6
  17. data/lib/rooibos/router/action.rb +36 -0
  18. data/lib/rooibos/router/flow/dispatch.rb +39 -0
  19. data/lib/rooibos/router/flow/inward.rb +41 -0
  20. data/lib/rooibos/router/flow/outward.rb +44 -0
  21. data/lib/rooibos/router/guard.rb +56 -0
  22. data/lib/rooibos/router/predicate.rb +65 -0
  23. data/lib/rooibos/router/registry/actions.rb +41 -0
  24. data/lib/rooibos/router/registry/forwards.rb +58 -0
  25. data/lib/rooibos/router/registry/observes.rb +57 -0
  26. data/lib/rooibos/router/registry/otherwises.rb +29 -0
  27. data/lib/rooibos/router/registry/receives.rb +57 -0
  28. data/lib/rooibos/router/registry/routes.rb +59 -0
  29. data/lib/rooibos/router/registry.rb +26 -0
  30. data/lib/rooibos/router/route.rb +42 -0
  31. data/lib/rooibos/router/router_update.rb +53 -0
  32. data/lib/rooibos/router/rule/forward.rb +39 -0
  33. data/lib/rooibos/router/rule/observe.rb +22 -0
  34. data/lib/rooibos/router/rule/otherwise.rb +26 -0
  35. data/lib/rooibos/router/rule/receive.rb +22 -0
  36. data/lib/rooibos/router/rule.rb +40 -0
  37. data/lib/rooibos/router.rb +424 -438
  38. data/lib/rooibos/runtime.rb +37 -52
  39. data/lib/rooibos/test_helper.rb +22 -0
  40. data/lib/rooibos/transition.rb +92 -0
  41. data/lib/rooibos/version.rb +1 -1
  42. data/lib/rooibos.rb +2 -57
  43. data/sig/rooibos/cli.rbs +1 -0
  44. data/sig/rooibos/command.rbs +44 -0
  45. data/sig/rooibos/configuration.rbs +20 -0
  46. data/sig/rooibos/message.rbs +12 -0
  47. data/sig/rooibos/router/action.rbs +33 -0
  48. data/sig/rooibos/router/actions.rbs +27 -0
  49. data/sig/rooibos/router/flow/dispatch.rbs +29 -0
  50. data/sig/rooibos/router/flow/inward.rbs +37 -0
  51. data/sig/rooibos/router/flow/outward.rbs +36 -0
  52. data/sig/rooibos/router/forward.rbs +35 -0
  53. data/sig/rooibos/router/forwards.rbs +34 -0
  54. data/sig/rooibos/router/guard.rbs +21 -0
  55. data/sig/rooibos/router/observe.rbs +20 -0
  56. data/sig/rooibos/router/observes.rbs +38 -0
  57. data/sig/rooibos/router/otherwise.rbs +22 -0
  58. data/sig/rooibos/router/otherwises.rbs +20 -0
  59. data/sig/rooibos/router/predicate.rbs +51 -0
  60. data/sig/rooibos/router/receive.rbs +20 -0
  61. data/sig/rooibos/router/receives.rbs +38 -0
  62. data/sig/rooibos/router/registry.rbs +24 -0
  63. data/sig/rooibos/router/route.rbs +46 -0
  64. data/sig/rooibos/router/router_update.rbs +33 -0
  65. data/sig/rooibos/router/routes.rbs +41 -0
  66. data/sig/rooibos/router/rule.rbs +36 -0
  67. data/sig/rooibos/router.rbs +216 -161
  68. data/sig/rooibos/runtime.rbs +0 -1
  69. data/sig/rooibos/test_helper.rbs +6 -0
  70. data/sig/rooibos/transition.rbs +33 -0
  71. data/sig/rooibos.rbs +0 -10
  72. metadata +144 -198
  73. data/.builds/ruby-3.2.yml +0 -55
  74. data/.builds/ruby-3.3.yml +0 -55
  75. data/.builds/ruby-3.4.yml +0 -55
  76. data/.builds/ruby-4.0.0.yml +0 -55
  77. data/.pre-commit-config.yaml +0 -16
  78. data/.rubocop.yml +0 -8
  79. data/AGENTS.md +0 -108
  80. data/CHANGELOG.md +0 -308
  81. data/README.md +0 -183
  82. data/README.rdoc +0 -374
  83. data/Rakefile +0 -16
  84. data/Steepfile +0 -13
  85. data/doc/best_practices/forms_and_validation.md +0 -20
  86. data/doc/best_practices/http_workflows.md +0 -20
  87. data/doc/best_practices/index.md +0 -26
  88. data/doc/best_practices/lists_and_tables.md +0 -20
  89. data/doc/best_practices/modal_dialogs.md +0 -20
  90. data/doc/best_practices/no_stateful_widgets.md +0 -184
  91. data/doc/best_practices/orchestration.md +0 -20
  92. data/doc/best_practices/streaming_data.md +0 -20
  93. data/doc/contributors/design/commands_and_outlets.md +0 -214
  94. data/doc/contributors/design/mvu_tea_implementations_research.md +0 -373
  95. data/doc/contributors/documentation_plan.md +0 -616
  96. data/doc/contributors/documentation_stub_audit.md +0 -112
  97. data/doc/contributors/documentation_style.md +0 -275
  98. data/doc/contributors/e2e_pty.md +0 -168
  99. data/doc/contributors/maybe_stateful_router.md +0 -56
  100. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +0 -70
  101. data/doc/contributors/specs/file_browser.md +0 -789
  102. data/doc/contributors/specs/file_browser_stories.md +0 -784
  103. data/doc/contributors/specs/tutorials_to_stories.rb +0 -167
  104. data/doc/contributors/todo/scrollbar.md +0 -118
  105. data/doc/contributors/tutorial_old/01_project_setup.md +0 -20
  106. data/doc/contributors/tutorial_old/02_hello_world.md +0 -24
  107. data/doc/contributors/tutorial_old/03_adding_state.md +0 -26
  108. data/doc/contributors/tutorial_old/06_organizing_your_code.md +0 -20
  109. data/doc/contributors/tutorial_old/07_your_first_command.md +0 -21
  110. data/doc/contributors/tutorial_old/08_the_preview_pane.md +0 -20
  111. data/doc/contributors/tutorial_old/09_loading_states.md +0 -20
  112. data/doc/contributors/tutorial_old/10_testing_your_app.md +0 -20
  113. data/doc/contributors/tutorial_old/11_polish_and_refine.md +0 -20
  114. data/doc/contributors/tutorial_old/12_going_further.md +0 -20
  115. data/doc/contributors/tutorial_old/index.md +0 -20
  116. data/doc/custom.css +0 -22
  117. data/doc/essentials/commands.md +0 -20
  118. data/doc/essentials/index.md +0 -31
  119. data/doc/essentials/messages.md +0 -21
  120. data/doc/essentials/models.md +0 -21
  121. data/doc/essentials/shortcuts.md +0 -19
  122. data/doc/essentials/the_elm_architecture.md +0 -24
  123. data/doc/essentials/the_runtime.md +0 -21
  124. data/doc/essentials/update_functions.md +0 -20
  125. data/doc/essentials/views.md +0 -22
  126. data/doc/getting_started/for_go_developers.md +0 -16
  127. data/doc/getting_started/for_python_developers.md +0 -16
  128. data/doc/getting_started/for_rails_developers.md +0 -17
  129. data/doc/getting_started/for_ratatui_ruby_developers.md +0 -17
  130. data/doc/getting_started/for_react_developers.md +0 -17
  131. data/doc/getting_started/index.md +0 -52
  132. data/doc/getting_started/install.md +0 -20
  133. data/doc/getting_started/quickstart.md +0 -20
  134. data/doc/getting_started/ruby_primer.md +0 -19
  135. data/doc/getting_started/why_rooibos.md +0 -20
  136. data/doc/images/verify_readme_usage.png +0 -0
  137. data/doc/images/widget_cmd_exec.png +0 -0
  138. data/doc/index.md +0 -93
  139. data/doc/scaling_up/async_patterns.md +0 -20
  140. data/doc/scaling_up/command_composition.md +0 -20
  141. data/doc/scaling_up/custom_commands.md +0 -21
  142. data/doc/scaling_up/fractal_architecture.md +0 -20
  143. data/doc/scaling_up/index.md +0 -30
  144. data/doc/scaling_up/message_routing.md +0 -20
  145. data/doc/scaling_up/ractor_safety.md +0 -20
  146. data/doc/scaling_up/testing.md +0 -21
  147. data/doc/troubleshooting/common_errors.md +0 -20
  148. data/doc/troubleshooting/debugging.md +0 -21
  149. data/doc/troubleshooting/index.md +0 -23
  150. data/doc/troubleshooting/performance.md +0 -20
  151. data/doc/tutorial/01_project_setup.md +0 -44
  152. data/doc/tutorial/02_hello_world.md +0 -45
  153. data/doc/tutorial/03_static_file_list.md +0 -44
  154. data/doc/tutorial/04_arrow_navigation.md +0 -47
  155. data/doc/tutorial/05_real_files.md +0 -45
  156. data/doc/tutorial/06_safe_refactoring.md +0 -21
  157. data/doc/tutorial/07_red_first_tdd.md +0 -26
  158. data/doc/tutorial/08_file_metadata.md +0 -42
  159. data/doc/tutorial/09_text_preview.md +0 -44
  160. data/doc/tutorial/10_directory_tree.md +0 -42
  161. data/doc/tutorial/11_pane_focus.md +0 -40
  162. data/doc/tutorial/12_sorting.md +0 -41
  163. data/doc/tutorial/13_filtering.md +0 -43
  164. data/doc/tutorial/14_toggle_hidden.md +0 -41
  165. data/doc/tutorial/15_text_input_widget.md +0 -43
  166. data/doc/tutorial/16_rename_files.md +0 -42
  167. data/doc/tutorial/17_confirmation_dialogs.md +0 -43
  168. data/doc/tutorial/18_progress_indicators.md +0 -43
  169. data/doc/tutorial/19_atomic_operations.md +0 -42
  170. data/doc/tutorial/20_external_editor.md +0 -42
  171. data/doc/tutorial/21_modal_overlays.md +0 -41
  172. data/doc/tutorial/22_error_handling.md +0 -43
  173. data/doc/tutorial/23_terminal_capabilities.md +0 -53
  174. data/doc/tutorial/24_mouse_events.md +0 -43
  175. data/doc/tutorial/25_resize_events.md +0 -43
  176. data/doc/tutorial/26_loading_states.md +0 -42
  177. data/doc/tutorial/27_performance.md +0 -43
  178. data/doc/tutorial/28_color_schemes.md +0 -47
  179. data/doc/tutorial/29_configuration.md +0 -124
  180. data/doc/tutorial/30_going_further.md +0 -17
  181. data/doc/tutorial/index.md +0 -17
  182. data/examples/app_fractal_dashboard/README.md +0 -60
  183. data/examples/app_fractal_dashboard/app.rb +0 -63
  184. data/examples/app_fractal_dashboard/dashboard/base.rb +0 -73
  185. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +0 -86
  186. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +0 -87
  187. data/examples/app_fractal_dashboard/dashboard/update_router.rb +0 -43
  188. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +0 -81
  189. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +0 -82
  190. data/examples/app_fractal_dashboard/fragments/custom_shell_output.rb +0 -90
  191. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +0 -47
  192. data/examples/app_fractal_dashboard/fragments/network_panel.rb +0 -45
  193. data/examples/app_fractal_dashboard/fragments/ping.rb +0 -47
  194. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +0 -45
  195. data/examples/app_fractal_dashboard/fragments/system_info.rb +0 -47
  196. data/examples/app_fractal_dashboard/fragments/uptime.rb +0 -47
  197. data/examples/tutorial/01/app.rb +0 -50
  198. data/examples/tutorial/02/app.rb +0 -64
  199. data/examples/tutorial/03/app.rb +0 -91
  200. data/examples/tutorial/06_safe_refactoring/app.rb +0 -124
  201. data/examples/verify_readme_usage/README.md +0 -54
  202. data/examples/verify_readme_usage/app.rb +0 -47
  203. data/examples/verify_website_first_app/app.rb +0 -85
  204. data/examples/verify_website_hello_mvu/app.rb +0 -31
  205. data/examples/widget_command_system/README.md +0 -70
  206. data/examples/widget_command_system/app.rb +0 -134
  207. data/generate_tutorial_stubs.rb +0 -126
  208. data/mise.toml +0 -8
  209. data/rbs_collection.lock.yaml +0 -108
  210. data/rbs_collection.yaml +0 -15
  211. data/tasks/example_viewer.html.erb +0 -172
  212. data/tasks/install.rake +0 -29
  213. data/tasks/resources/build.yml.erb +0 -55
  214. data/tasks/resources/index.html.erb +0 -44
  215. data/tasks/resources/rubies.yml +0 -7
  216. data/tasks/steep.rake +0 -11
  217. /data/{vendor/goodcop/base.yml → lib/rooibos/rubocop.yml} +0 -0
@@ -1,60 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Cmd.map Fractal Dashboard
7
-
8
- Demonstrates **Fractal Architecture** using `Cmd.map` for component composition.
9
-
10
- ## Problem
11
-
12
- Without composition, a complex app needs one giant `case` statement handling every possible message from every child—the "God Reducer" anti-pattern. This doesn't scale.
13
-
14
- ## Solution
15
-
16
- `Cmd.map` wraps child commands so their results route through parents:
17
-
18
- ```ruby
19
- # Child produces [:system_info, {stdout:, ...}]
20
- child_cmd = SystemInfoWidget.fetch_cmd
21
-
22
- # Parent wraps to produce [:stats, :system_info, {...}]
23
- parent_cmd = Cmd.map(child_cmd) { |m| [:stats, *m] }
24
- ```
25
-
26
- Each layer handles only its own messages. Parents pattern-match on the first element to route to the correct child.
27
-
28
- ## Architecture
29
-
30
- ```
31
- Dashboard (root)
32
- ├── StatsPanel
33
- │ ├── SystemInfoWidget → Cmd.exec("uname -a", :system_info)
34
- │ └── DiskUsageWidget → Cmd.exec("df -h", :disk_usage)
35
- └── NetworkPanel
36
- ├── PingWidget → Cmd.exec("ping -c 1 localhost", :ping)
37
- └── UptimeWidget → Cmd.exec("uptime", :uptime)
38
- ```
39
-
40
- ## Hotkeys
41
-
42
- | Key | Action |
43
- |-----|--------|
44
- | `s` | Fetch system info |
45
- | `d` | Fetch disk usage |
46
- | `p` | Ping localhost |
47
- | `u` | Fetch uptime |
48
- | `q` | Quit |
49
-
50
- ## Key Concepts
51
-
52
- 1. **Widget isolation**: Each widget has its own `Model`, `UPDATE`, and `fetch_cmd`. It knows nothing about parents.
53
- 2. **Message routing**: Parents prefix child messages (`:stats`, `:network`) and pattern-match to route.
54
- 3. **Recursive dispatch**: `Cmd.map` delegates inner command execution to the runtime, then transforms the result.
55
-
56
- ## Usage
57
-
58
- ```bash
59
- ruby examples/widget_cmd_map/app.rb
60
- ```
@@ -1,63 +0,0 @@
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
- $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
9
-
10
- require "ratatui_ruby"
11
- require "rooibos"
12
-
13
- # Demonstrates three approaches to UPDATE routing in Fractal Architecture.
14
- #
15
- # == Usage
16
- #
17
- # ruby app.rb # Defaults to 'manual'
18
- # ruby app.rb manual # Verbose pattern matching
19
- # ruby app.rb helpers # Rooibos.route and Rooibos.delegate helpers
20
- # ruby app.rb router # Rooibos::Router DSL
21
- #
22
- # All three share the same fragments, Model, INITIAL, and VIEW. Only the UPDATE
23
- # implementation differs. Compare the three update_*.rb files to see the
24
- # progression from verbose to declarative.
25
- #
26
- # == Architecture
27
- #
28
- # app.rb ← Entry point (you are here)
29
- # dashboard/
30
- # ├── base.rb ← Shared: Model, INITIAL, VIEW
31
- # ├── update_manual.rb
32
- # ├── update_helpers.rb
33
- # └── update_router.rb
34
- # fragments/
35
- # ├── system_info.rb
36
- # ├── disk_usage.rb
37
- # ├── ping.rb
38
- # ├── uptime.rb
39
- # ├── stats_panel.rb
40
- # └── network_panel.rb
41
-
42
- VALID_MODES = %w[manual helpers router].freeze
43
-
44
- mode = ARGV[0] || "manual"
45
- unless VALID_MODES.include?(mode)
46
- warn "Usage: ruby app.rb [#{VALID_MODES.join('|')}]"
47
- exit 1
48
- end
49
-
50
- dashboard = case mode
51
- when "manual"
52
- require_relative "dashboard/update_manual"
53
- DashboardManual
54
- when "helpers"
55
- require_relative "dashboard/update_helpers"
56
- DashboardHelpers
57
- when "router"
58
- require_relative "dashboard/update_router"
59
- DashboardRouter
60
- end
61
-
62
- puts "Running with #{mode} UPDATE..."
63
- Rooibos.run(fragment: dashboard)
@@ -1,73 +0,0 @@
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_relative "../fragments/stats_panel"
9
- require_relative "../fragments/network_panel"
10
- require_relative "../fragments/custom_shell_modal"
11
-
12
- # Shared Model, Init, and View for the Dashboard.
13
- # Each Dashboard variation (Manual, Router, Helpers) provides its own Update.
14
- module DashboardBase
15
- Command = Rooibos::Command
16
-
17
- Model = Data.define(:stats, :network, :shell_modal)
18
-
19
- Init = -> do
20
- stats, = Rooibos.normalize_init(StatsPanel::Init.())
21
- network, = Rooibos.normalize_init(NetworkPanel::Init.())
22
- shell_modal, = Rooibos.normalize_init(CustomShellModal::Init.())
23
- Model.new(stats:, network:, shell_modal:)
24
- end
25
-
26
- View = -> (model, tui) do
27
- modal_active = CustomShellModal.active?(model.shell_modal)
28
- hotkey, label_style = if modal_active
29
- [tui.style(fg: :dark_gray), tui.style(fg: :dark_gray)]
30
- else
31
- [tui.style(modifiers: [:bold, :underlined]), nil]
32
- end
33
- dim = tui.style(fg: :dark_gray)
34
-
35
- controls = tui.paragraph(
36
- text: [
37
- tui.text_line(spans: [
38
- tui.text_span(content: "s", style: hotkey),
39
- tui.text_span(content: ": System ", style: label_style),
40
- tui.text_span(content: "d", style: hotkey),
41
- tui.text_span(content: ": Disk ", style: label_style),
42
- tui.text_span(content: "p", style: hotkey),
43
- tui.text_span(content: ": Ping ", style: label_style),
44
- tui.text_span(content: "u", style: hotkey),
45
- tui.text_span(content: ": Uptime ", style: label_style),
46
- tui.text_span(content: "c", style: hotkey),
47
- tui.text_span(content: ": Custom ", style: label_style),
48
- tui.text_span(content: "q", style: hotkey),
49
- tui.text_span(content: ": Quit", style: label_style),
50
- ]),
51
- ],
52
- block: tui.block(title: "Fractal Dashboard", borders: [:all], border_style: dim)
53
- )
54
-
55
- dashboard = tui.layout(
56
- direction: :vertical,
57
- constraints: [tui.constraint_fill(1), tui.constraint_fill(1), tui.constraint_length(3)],
58
- children: [
59
- StatsPanel::View.call(model.stats, tui, disabled: modal_active),
60
- NetworkPanel::View.call(model.network, tui, disabled: modal_active),
61
- controls,
62
- ]
63
- )
64
-
65
- # Compose modal overlay if active
66
- modal_widget = CustomShellModal::View.call(model.shell_modal, tui)
67
- if modal_widget
68
- tui.overlay(layers: [dashboard, modal_widget])
69
- else
70
- dashboard
71
- end
72
- end
73
- end
@@ -1,86 +0,0 @@
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_relative "base"
9
-
10
- # UPDATE using Rooibos.route and Rooibos.delegate helpers.
11
- #
12
- # This is the medium-verbosity approach: routing helpers reduce boilerplate
13
- # while keeping the case statement visible. A good middle ground.
14
- module DashboardHelpers
15
- Command = Rooibos::Command
16
- # Alias for readability
17
-
18
- # Shared with other UPDATE variants
19
- Model = DashboardBase::Model
20
- Init = DashboardBase::Init
21
- View = DashboardBase::View
22
-
23
- Update = -> (message, model) do
24
- # Global Force Quit
25
- return [model, Rooibos::Command.exit] if message.respond_to?(:ctrl_c?) && message.ctrl_c?
26
-
27
- # IMPORTANT: Route command results BEFORE modal intercept.
28
- # Async command results must always reach their destination, even when a
29
- # modal is active. Only user input (keys/mouse) should be blocked.
30
-
31
- # Route streaming command output to modal
32
- if (result = Rooibos.delegate(message, :shell_output, CustomShellModal::Update, model.shell_modal))
33
- new_modal, command = result
34
- return [model.with(shell_modal: new_modal), command]
35
- end
36
-
37
- # Route to child fragments
38
- if (result = Rooibos.delegate(message, :stats, StatsPanel::Update, model.stats))
39
- new_child, command = result
40
- return [model.with(stats: new_child), command && Rooibos.route(command, :stats)]
41
- end
42
-
43
- if (result = Rooibos.delegate(message, :network, NetworkPanel::Update, model.network))
44
- new_child, command = result
45
- return [model.with(network: new_child), command && Rooibos.route(command, :network)]
46
- end
47
-
48
- # Modal intercepts user input (not command results)
49
- if CustomShellModal.active?(model.shell_modal)
50
- new_modal, command = CustomShellModal::Update.call(message, model.shell_modal)
51
- return [model.with(shell_modal: new_modal), command]
52
- end
53
-
54
- # Handle user input
55
- case message
56
- in _ if message.q? || message.ctrl_c?
57
- Command.exit
58
-
59
- in _ if message.c?
60
- [model.with(shell_modal: CustomShellModal.open), nil]
61
-
62
- in _ if message.s?
63
- command = Rooibos.route(SystemInfo.fetch_command, :stats)
64
- new_stats = model.stats.with(system_info: model.stats.system_info.with(loading: true))
65
- [model.with(stats: new_stats), command]
66
-
67
- in _ if message.d?
68
- command = Rooibos.route(DiskUsage.fetch_command, :stats)
69
- new_stats = model.stats.with(disk_usage: model.stats.disk_usage.with(loading: true))
70
- [model.with(stats: new_stats), command]
71
-
72
- in _ if message.p?
73
- command = Rooibos.route(Ping.fetch_command, :network)
74
- new_network = model.network.with(ping: model.network.ping.with(loading: true))
75
- [model.with(network: new_network), command]
76
-
77
- in _ if message.u?
78
- command = Rooibos.route(Uptime.fetch_command, :network)
79
- new_network = model.network.with(uptime: model.network.uptime.with(loading: true))
80
- [model.with(network: new_network), command]
81
-
82
- else
83
- model
84
- end
85
- end
86
- end
@@ -1,87 +0,0 @@
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_relative "base"
9
-
10
- # UPDATE using verbose manual routing.
11
- #
12
- # This is the most explicit approach: full pattern matching, explicit
13
- # Command.map calls, manual model updates. Maximum control, maximum boilerplate.
14
- module DashboardManual
15
- Command = Rooibos::Command
16
-
17
- # Shared with other UPDATE variants
18
- Model = DashboardBase::Model
19
- Init = DashboardBase::Init
20
- View = DashboardBase::View
21
-
22
- Update = -> (message, model) do
23
- # Global Force Quit
24
- return [model, Rooibos::Command.exit] if message.respond_to?(:ctrl_c?) && message.ctrl_c?
25
-
26
- # IMPORTANT: Route command results BEFORE modal intercept.
27
- # Async command results must always reach their destination, even when a
28
- # modal is active. Only user input (keys/mouse) should be blocked.
29
- case message
30
- # Route command results to panels
31
- in [:stats, rest]
32
- new_panel, command = StatsPanel::Update.call(rest, model.stats)
33
- mapped_command = command ? Command.map(command) { |child_result| [:stats, *child_result] } : nil
34
- return [model.with(stats: new_panel), mapped_command]
35
-
36
- in [:network, rest]
37
- new_panel, command = NetworkPanel::Update.call(rest, model.network)
38
- mapped_command = command ? Command.map(command) { |child_result| [:network, *child_result] } : nil
39
- return [model.with(network: new_panel), mapped_command]
40
-
41
- in [:shell_output, rest]
42
- # Route streaming command output to modal
43
- new_modal, command = CustomShellModal::Update.call(message, model.shell_modal)
44
- return [model.with(shell_modal: new_modal), command]
45
- else
46
- nil # Fall through to input handling
47
- end
48
-
49
- # Modal intercepts user input (not command results)
50
- if CustomShellModal.active?(model.shell_modal)
51
- new_modal, command = CustomShellModal::Update.call(message, model.shell_modal)
52
- return [model.with(shell_modal: new_modal), command]
53
- end
54
-
55
- case message
56
- # Handle user input
57
- in _ if message.q? || message.ctrl_c?
58
- Command.exit
59
-
60
- in _ if message.c?
61
- [model.with(shell_modal: CustomShellModal.open), nil]
62
-
63
- in _ if message.s?
64
- command = Command.map(SystemInfo.fetch_command) { |batch| [:stats, batch] }
65
- new_stats = model.stats.with(system_info: model.stats.system_info.with(loading: true))
66
- [model.with(stats: new_stats), command]
67
-
68
- in _ if message.d?
69
- command = Command.map(DiskUsage.fetch_command) { |batch| [:stats, batch] }
70
- new_stats = model.stats.with(disk_usage: model.stats.disk_usage.with(loading: true))
71
- [model.with(stats: new_stats), command]
72
-
73
- in _ if message.p?
74
- command = Command.map(Ping.fetch_command) { |batch| [:network, batch] }
75
- new_network = model.network.with(ping: model.network.ping.with(loading: true))
76
- [model.with(network: new_network), command]
77
-
78
- in _ if message.u?
79
- command = Command.map(Uptime.fetch_command) { |batch| [:network, batch] }
80
- new_network = model.network.with(uptime: model.network.uptime.with(loading: true))
81
- [model.with(network: new_network), command]
82
-
83
- else
84
- model
85
- end
86
- end
87
- end
@@ -1,43 +0,0 @@
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_relative "base"
9
-
10
- # UPDATE using the declarative Rooibos::Router DSL.
11
- #
12
- # This is the minimal-boilerplate approach: declare routes and keymaps,
13
- # let from_router generate the UPDATE lambda. Maximum DX, least control.
14
- module DashboardRouter
15
- include Rooibos::Router
16
-
17
- Command = Rooibos::Command
18
-
19
- # Shared with other Update variants
20
- Model = DashboardBase::Model
21
- Init = DashboardBase::Init
22
- View = DashboardBase::View
23
-
24
- route :stats, to: StatsPanel
25
- route :network, to: NetworkPanel
26
-
27
- # Guard: only handle keys when modal is not active
28
- MODAL_INACTIVE = -> (model) { !CustomShellModal.active?(model.shell_modal) }
29
-
30
- keymap do
31
- key :ctrl_c, -> { Command.exit }
32
- only when: MODAL_INACTIVE do
33
- key :q, -> { Command.exit }
34
- key :s, -> { SystemInfo.fetch_command }
35
- key :d, -> { DiskUsage.fetch_command }
36
- key :p, -> { Ping.fetch_command }
37
- key :u, -> { Uptime.fetch_command }
38
- key :c, -> { CustomShellModal.open }
39
- end
40
- end
41
-
42
- Update = from_router
43
- end
@@ -1,81 +0,0 @@
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 "rooibos"
9
- # Text input fragment for custom shell command modal.
10
- #
11
- # Handles text entry. Sets canceled: or submitted: in model for parent to detect.
12
- module CustomShellInput
13
- Model = Data.define(:text, :canceled, :submitted)
14
-
15
- Init = -> do
16
- Ractor.make_shareable(Model.new(text: "", canceled: false, submitted: false))
17
- end
18
-
19
- View = -> (model, tui) do
20
- content = if model.text.empty?
21
- tui.paragraph(text: tui.text_span(content: "Type a command...", style: { fg: :dark_gray }))
22
- else
23
- tui.paragraph(text: model.text)
24
- end
25
-
26
- tui.layout(
27
- direction: :vertical,
28
- constraints: [
29
- tui.constraint_length(1),
30
- tui.constraint_length(3),
31
- tui.constraint_min(0),
32
- ],
33
- children: [
34
- nil,
35
- tui.center(
36
- width_percent: 80,
37
- child: tui.overlay(
38
- layers: [
39
- tui.clear,
40
- tui.block(
41
- title: "Run Command",
42
- titles: [
43
- { content: "ESC: Cancel", position: :bottom, alignment: :left },
44
- { content: "ENTER: Run", position: :bottom, alignment: :right },
45
- ],
46
- borders: [:all],
47
- children: [content]
48
- ),
49
- ]
50
- )
51
- ),
52
- nil,
53
- ]
54
- )
55
- end
56
-
57
- Update = -> (message, model) do
58
- case message
59
- in _ if message.respond_to?(:esc?) && message.esc?
60
- [model.with(canceled: true), nil]
61
-
62
- in _ if message.respond_to?(:enter?) && message.enter?
63
- return [model.with(canceled: true), nil] if model.text.strip.empty?
64
- [model.with(submitted: true), nil]
65
-
66
- in _ if message.respond_to?(:backspace?) && message.backspace?
67
- [model.with(text: model.text.chop.freeze), nil]
68
-
69
- in RatatuiRuby::Event::Paste
70
- # Handle continuation backslashes: "foo \\\nbar" → "foo bar"
71
- normalized = message.content.gsub(/\\\r?\n/, "").gsub(/[\r\n]/, " ")
72
- [model.with(text: "#{model.text}#{normalized}".freeze), nil]
73
-
74
- in RatatuiRuby::Event::Key if message.text? && message.code.length == 1
75
- [model.with(text: "#{model.text}#{message.code}".freeze), nil]
76
-
77
- else
78
- [model, nil]
79
- end
80
- end
81
- end
@@ -1,82 +0,0 @@
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 "custom_shell_input"
9
- require_relative "custom_shell_output"
10
-
11
- # Modal overlay for custom shell command execution.
12
- module CustomShellModal
13
- Command = Rooibos::Command
14
-
15
- Model = Data.define(:mode, :input, :output)
16
-
17
- Init = -> do
18
- input, = Rooibos.normalize_init(CustomShellInput::Init.())
19
- output, = Rooibos.normalize_init(CustomShellOutput::Init.())
20
- Ractor.make_shareable(Model.new(mode: :none, input:, output:))
21
- end
22
-
23
- View = -> (model, tui) do
24
- case model.mode
25
- when :input
26
- CustomShellInput::View.call(model.input, tui)
27
- when :output
28
- CustomShellOutput::View.call(model.output, tui)
29
- else
30
- nil
31
- end
32
- end
33
-
34
- Update = -> (message, model) do
35
- case [model.mode, message]
36
- in [:input, _]
37
- # Delegate first, then check if user wants to close
38
- new_input, _cmd = CustomShellInput::Update.call(message, model.input)
39
-
40
- if new_input.canceled
41
- [Init.(), nil]
42
- elsif new_input.submitted
43
- shell_cmd = new_input.text
44
- new_output = CustomShellOutput::Init.().with(command: shell_cmd, running: true)
45
- reset_input, = Rooibos.normalize_init(CustomShellInput::Init.())
46
- [
47
- model.with(mode: :output, input: reset_input, output: new_output),
48
- Command.system(shell_cmd, :shell_output, stream: true),
49
- ]
50
- else
51
- [model.with(input: new_input), nil]
52
- end
53
-
54
- in [:output, _]
55
- # Delegate first, then check if user wants to close
56
- case message
57
- in RatatuiRuby::Event::Key if message.ctrl_c?
58
- [Init.(), nil]
59
- else
60
- new_output, _cmd = CustomShellOutput::Update.call(message, model.output)
61
-
62
- if new_output.dismissed
63
- [Init.(), nil]
64
- else
65
- [model.with(output: new_output), nil]
66
- end
67
- end
68
-
69
- else
70
- [model, nil]
71
- end
72
- end
73
-
74
- def self.open
75
- input, = Rooibos.normalize_init(CustomShellInput::Init.())
76
- Init.().with(mode: :input, input:)
77
- end
78
-
79
- def self.active?(model)
80
- model.mode != :none
81
- end
82
- end
@@ -1,90 +0,0 @@
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 "rooibos"
9
- # Streaming output fragment for custom shell command modal.
10
- #
11
- # Displays interleaved stdout/stderr. Border color reflects exit status.
12
- # Sets dismissed: in model for parent to detect.
13
- module CustomShellOutput
14
- Chunk = Data.define(:stream, :text)
15
- Model = Data.define(:command, :chunks, :running, :exit_status, :dismissed)
16
-
17
- Init = -> do
18
- Ractor.make_shareable(Model.new(command: "", chunks: [].freeze, running: false, exit_status: nil, dismissed: false))
19
- end
20
-
21
- View = -> (model, tui) do
22
- # Build styled spans from chunks
23
- spans = if model.chunks.empty? && model.running
24
- [tui.text_span(content: "Running...", style: tui.style(fg: :dark_gray))]
25
- else
26
- model.chunks.map do |chunk|
27
- style = (chunk.stream == :stderr) ? tui.style(fg: :yellow) : nil
28
- tui.text_span(content: chunk.text, style:)
29
- end
30
- end
31
-
32
- # Border color: green if exited 0, red if exited non-zero, default if running
33
- border_style = case model.exit_status
34
- when nil then nil
35
- when 0 then tui.style(fg: :green)
36
- else tui.style(fg: :red)
37
- end
38
-
39
- left_title = model.running ? "ESC: Cancel" : "ESC: Dismiss"
40
- display_cmd = (model.command.length > 60) ? "#{model.command[0..57]}..." : model.command
41
-
42
- tui.center(
43
- width_percent: 80,
44
- height_percent: 80,
45
- child: tui.overlay(
46
- layers: [
47
- tui.clear,
48
- tui.block(
49
- title: display_cmd,
50
- titles: [
51
- { content: left_title, position: :bottom, alignment: :left },
52
- { content: "ENTER: Dismiss", position: :bottom, alignment: :right },
53
- ],
54
- borders: [:all],
55
- border_style:,
56
- children: [tui.paragraph(text: spans)]
57
- ),
58
- ]
59
- )
60
- )
61
- end
62
-
63
- Update = -> (message, model) do
64
- case message
65
- in [:stdout, chunk]
66
- new_chunks = Ractor.make_shareable([*model.chunks, Chunk.new(stream: :stdout, text: chunk)].freeze)
67
- [model.with(chunks: new_chunks), nil]
68
-
69
- in [:stderr, chunk]
70
- new_chunks = Ractor.make_shareable([*model.chunks, Chunk.new(stream: :stderr, text: chunk)].freeze)
71
- [model.with(chunks: new_chunks), nil]
72
-
73
- in [:complete, { status: }]
74
- [model.with(running: false, exit_status: status), nil]
75
-
76
- in [:error, { message: error_msg }]
77
- new_chunks = Ractor.make_shareable([*model.chunks, Chunk.new(stream: :stderr, text: "Error: #{error_msg}\n")].freeze)
78
- [model.with(chunks: new_chunks, running: false, exit_status: 1), nil]
79
-
80
- in _ if message.respond_to?(:esc?) && message.esc?
81
- [model.with(dismissed: true), nil]
82
-
83
- in _ if message.respond_to?(:enter?) && message.enter?
84
- [model.with(dismissed: true), nil]
85
-
86
- else
87
- [model, nil]
88
- end
89
- end
90
- end