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.
- checksums.yaml +4 -4
- data/LICENSES/BSD-2-Clause.txt +9 -0
- data/REUSE.toml +5 -0
- data/exe/.gitkeep +0 -0
- data/lib/rooibos/cli/commands/new.rb +24 -0
- data/lib/rooibos/command/batch.rb +10 -0
- data/lib/rooibos/command/bubble.rb +34 -0
- data/lib/rooibos/command/custom.rb +3 -2
- data/lib/rooibos/command/deliver.rb +50 -0
- data/lib/rooibos/command/http.rb +1 -1
- data/lib/rooibos/command/lifecycle.rb +3 -1
- data/lib/rooibos/command/outlet.rb +19 -9
- data/lib/rooibos/command.rb +107 -3
- data/lib/rooibos/configuration.rb +29 -0
- data/lib/rooibos/message/bubbled.rb +29 -0
- data/lib/rooibos/message.rb +24 -6
- data/lib/rooibos/router/action.rb +36 -0
- data/lib/rooibos/router/flow/dispatch.rb +39 -0
- data/lib/rooibos/router/flow/inward.rb +41 -0
- data/lib/rooibos/router/flow/outward.rb +44 -0
- data/lib/rooibos/router/guard.rb +56 -0
- data/lib/rooibos/router/predicate.rb +65 -0
- data/lib/rooibos/router/registry/actions.rb +41 -0
- data/lib/rooibos/router/registry/forwards.rb +58 -0
- data/lib/rooibos/router/registry/observes.rb +57 -0
- data/lib/rooibos/router/registry/otherwises.rb +29 -0
- data/lib/rooibos/router/registry/receives.rb +57 -0
- data/lib/rooibos/router/registry/routes.rb +59 -0
- data/lib/rooibos/router/registry.rb +26 -0
- data/lib/rooibos/router/route.rb +42 -0
- data/lib/rooibos/router/router_update.rb +53 -0
- data/lib/rooibos/router/rule/forward.rb +39 -0
- data/lib/rooibos/router/rule/observe.rb +22 -0
- data/lib/rooibos/router/rule/otherwise.rb +26 -0
- data/lib/rooibos/router/rule/receive.rb +22 -0
- data/lib/rooibos/router/rule.rb +40 -0
- data/lib/rooibos/router.rb +424 -438
- data/lib/rooibos/runtime.rb +37 -52
- data/lib/rooibos/test_helper.rb +22 -0
- data/lib/rooibos/transition.rb +92 -0
- data/lib/rooibos/version.rb +1 -1
- data/lib/rooibos.rb +2 -57
- data/sig/rooibos/cli.rbs +1 -0
- data/sig/rooibos/command.rbs +44 -0
- data/sig/rooibos/configuration.rbs +20 -0
- data/sig/rooibos/message.rbs +12 -0
- data/sig/rooibos/router/action.rbs +33 -0
- data/sig/rooibos/router/actions.rbs +27 -0
- data/sig/rooibos/router/flow/dispatch.rbs +29 -0
- data/sig/rooibos/router/flow/inward.rbs +37 -0
- data/sig/rooibos/router/flow/outward.rbs +36 -0
- data/sig/rooibos/router/forward.rbs +35 -0
- data/sig/rooibos/router/forwards.rbs +34 -0
- data/sig/rooibos/router/guard.rbs +21 -0
- data/sig/rooibos/router/observe.rbs +20 -0
- data/sig/rooibos/router/observes.rbs +38 -0
- data/sig/rooibos/router/otherwise.rbs +22 -0
- data/sig/rooibos/router/otherwises.rbs +20 -0
- data/sig/rooibos/router/predicate.rbs +51 -0
- data/sig/rooibos/router/receive.rbs +20 -0
- data/sig/rooibos/router/receives.rbs +38 -0
- data/sig/rooibos/router/registry.rbs +24 -0
- data/sig/rooibos/router/route.rbs +46 -0
- data/sig/rooibos/router/router_update.rbs +33 -0
- data/sig/rooibos/router/routes.rbs +41 -0
- data/sig/rooibos/router/rule.rbs +36 -0
- data/sig/rooibos/router.rbs +216 -161
- data/sig/rooibos/runtime.rbs +0 -1
- data/sig/rooibos/test_helper.rbs +6 -0
- data/sig/rooibos/transition.rbs +33 -0
- data/sig/rooibos.rbs +0 -10
- metadata +144 -198
- data/.builds/ruby-3.2.yml +0 -55
- data/.builds/ruby-3.3.yml +0 -55
- data/.builds/ruby-3.4.yml +0 -55
- data/.builds/ruby-4.0.0.yml +0 -55
- data/.pre-commit-config.yaml +0 -16
- data/.rubocop.yml +0 -8
- data/AGENTS.md +0 -108
- data/CHANGELOG.md +0 -308
- data/README.md +0 -183
- data/README.rdoc +0 -374
- data/Rakefile +0 -16
- data/Steepfile +0 -13
- data/doc/best_practices/forms_and_validation.md +0 -20
- data/doc/best_practices/http_workflows.md +0 -20
- data/doc/best_practices/index.md +0 -26
- data/doc/best_practices/lists_and_tables.md +0 -20
- data/doc/best_practices/modal_dialogs.md +0 -20
- data/doc/best_practices/no_stateful_widgets.md +0 -184
- data/doc/best_practices/orchestration.md +0 -20
- data/doc/best_practices/streaming_data.md +0 -20
- data/doc/contributors/design/commands_and_outlets.md +0 -214
- data/doc/contributors/design/mvu_tea_implementations_research.md +0 -373
- data/doc/contributors/documentation_plan.md +0 -616
- data/doc/contributors/documentation_stub_audit.md +0 -112
- data/doc/contributors/documentation_style.md +0 -275
- data/doc/contributors/e2e_pty.md +0 -168
- data/doc/contributors/maybe_stateful_router.md +0 -56
- data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +0 -70
- data/doc/contributors/specs/file_browser.md +0 -789
- data/doc/contributors/specs/file_browser_stories.md +0 -784
- data/doc/contributors/specs/tutorials_to_stories.rb +0 -167
- data/doc/contributors/todo/scrollbar.md +0 -118
- data/doc/contributors/tutorial_old/01_project_setup.md +0 -20
- data/doc/contributors/tutorial_old/02_hello_world.md +0 -24
- data/doc/contributors/tutorial_old/03_adding_state.md +0 -26
- data/doc/contributors/tutorial_old/06_organizing_your_code.md +0 -20
- data/doc/contributors/tutorial_old/07_your_first_command.md +0 -21
- data/doc/contributors/tutorial_old/08_the_preview_pane.md +0 -20
- data/doc/contributors/tutorial_old/09_loading_states.md +0 -20
- data/doc/contributors/tutorial_old/10_testing_your_app.md +0 -20
- data/doc/contributors/tutorial_old/11_polish_and_refine.md +0 -20
- data/doc/contributors/tutorial_old/12_going_further.md +0 -20
- data/doc/contributors/tutorial_old/index.md +0 -20
- data/doc/custom.css +0 -22
- data/doc/essentials/commands.md +0 -20
- data/doc/essentials/index.md +0 -31
- data/doc/essentials/messages.md +0 -21
- data/doc/essentials/models.md +0 -21
- data/doc/essentials/shortcuts.md +0 -19
- data/doc/essentials/the_elm_architecture.md +0 -24
- data/doc/essentials/the_runtime.md +0 -21
- data/doc/essentials/update_functions.md +0 -20
- data/doc/essentials/views.md +0 -22
- data/doc/getting_started/for_go_developers.md +0 -16
- data/doc/getting_started/for_python_developers.md +0 -16
- data/doc/getting_started/for_rails_developers.md +0 -17
- data/doc/getting_started/for_ratatui_ruby_developers.md +0 -17
- data/doc/getting_started/for_react_developers.md +0 -17
- data/doc/getting_started/index.md +0 -52
- data/doc/getting_started/install.md +0 -20
- data/doc/getting_started/quickstart.md +0 -20
- data/doc/getting_started/ruby_primer.md +0 -19
- data/doc/getting_started/why_rooibos.md +0 -20
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_cmd_exec.png +0 -0
- data/doc/index.md +0 -93
- data/doc/scaling_up/async_patterns.md +0 -20
- data/doc/scaling_up/command_composition.md +0 -20
- data/doc/scaling_up/custom_commands.md +0 -21
- data/doc/scaling_up/fractal_architecture.md +0 -20
- data/doc/scaling_up/index.md +0 -30
- data/doc/scaling_up/message_routing.md +0 -20
- data/doc/scaling_up/ractor_safety.md +0 -20
- data/doc/scaling_up/testing.md +0 -21
- data/doc/troubleshooting/common_errors.md +0 -20
- data/doc/troubleshooting/debugging.md +0 -21
- data/doc/troubleshooting/index.md +0 -23
- data/doc/troubleshooting/performance.md +0 -20
- data/doc/tutorial/01_project_setup.md +0 -44
- data/doc/tutorial/02_hello_world.md +0 -45
- data/doc/tutorial/03_static_file_list.md +0 -44
- data/doc/tutorial/04_arrow_navigation.md +0 -47
- data/doc/tutorial/05_real_files.md +0 -45
- data/doc/tutorial/06_safe_refactoring.md +0 -21
- data/doc/tutorial/07_red_first_tdd.md +0 -26
- data/doc/tutorial/08_file_metadata.md +0 -42
- data/doc/tutorial/09_text_preview.md +0 -44
- data/doc/tutorial/10_directory_tree.md +0 -42
- data/doc/tutorial/11_pane_focus.md +0 -40
- data/doc/tutorial/12_sorting.md +0 -41
- data/doc/tutorial/13_filtering.md +0 -43
- data/doc/tutorial/14_toggle_hidden.md +0 -41
- data/doc/tutorial/15_text_input_widget.md +0 -43
- data/doc/tutorial/16_rename_files.md +0 -42
- data/doc/tutorial/17_confirmation_dialogs.md +0 -43
- data/doc/tutorial/18_progress_indicators.md +0 -43
- data/doc/tutorial/19_atomic_operations.md +0 -42
- data/doc/tutorial/20_external_editor.md +0 -42
- data/doc/tutorial/21_modal_overlays.md +0 -41
- data/doc/tutorial/22_error_handling.md +0 -43
- data/doc/tutorial/23_terminal_capabilities.md +0 -53
- data/doc/tutorial/24_mouse_events.md +0 -43
- data/doc/tutorial/25_resize_events.md +0 -43
- data/doc/tutorial/26_loading_states.md +0 -42
- data/doc/tutorial/27_performance.md +0 -43
- data/doc/tutorial/28_color_schemes.md +0 -47
- data/doc/tutorial/29_configuration.md +0 -124
- data/doc/tutorial/30_going_further.md +0 -17
- data/doc/tutorial/index.md +0 -17
- data/examples/app_fractal_dashboard/README.md +0 -60
- data/examples/app_fractal_dashboard/app.rb +0 -63
- data/examples/app_fractal_dashboard/dashboard/base.rb +0 -73
- data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +0 -86
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +0 -87
- data/examples/app_fractal_dashboard/dashboard/update_router.rb +0 -43
- data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +0 -81
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +0 -82
- data/examples/app_fractal_dashboard/fragments/custom_shell_output.rb +0 -90
- data/examples/app_fractal_dashboard/fragments/disk_usage.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/network_panel.rb +0 -45
- data/examples/app_fractal_dashboard/fragments/ping.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/stats_panel.rb +0 -45
- data/examples/app_fractal_dashboard/fragments/system_info.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/uptime.rb +0 -47
- data/examples/tutorial/01/app.rb +0 -50
- data/examples/tutorial/02/app.rb +0 -64
- data/examples/tutorial/03/app.rb +0 -91
- data/examples/tutorial/06_safe_refactoring/app.rb +0 -124
- data/examples/verify_readme_usage/README.md +0 -54
- data/examples/verify_readme_usage/app.rb +0 -47
- data/examples/verify_website_first_app/app.rb +0 -85
- data/examples/verify_website_hello_mvu/app.rb +0 -31
- data/examples/widget_command_system/README.md +0 -70
- data/examples/widget_command_system/app.rb +0 -134
- data/generate_tutorial_stubs.rb +0 -126
- data/mise.toml +0 -8
- data/rbs_collection.lock.yaml +0 -108
- data/rbs_collection.yaml +0 -15
- data/tasks/example_viewer.html.erb +0 -172
- data/tasks/install.rake +0 -29
- data/tasks/resources/build.yml.erb +0 -55
- data/tasks/resources/index.html.erb +0 -44
- data/tasks/resources/rubies.yml +0 -7
- data/tasks/steep.rake +0 -11
- /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
|