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
data/lib/rooibos/router.rb
CHANGED
|
@@ -1,527 +1,513 @@
|
|
|
1
|
-
#--
|
|
2
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
1
|
# frozen_string_literal: true
|
|
4
2
|
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
5
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
+
require_relative "router/route"
|
|
9
|
+
require_relative "router/registry/routes"
|
|
10
|
+
require_relative "router/guard"
|
|
11
|
+
require_relative "router/rule"
|
|
12
|
+
require_relative "router/predicate"
|
|
13
|
+
require_relative "router/registry"
|
|
14
|
+
require_relative "router/action"
|
|
15
|
+
require_relative "router/registry/actions"
|
|
16
|
+
require_relative "router/rule/forward"
|
|
17
|
+
require_relative "router/registry/forwards"
|
|
18
|
+
require_relative "router/rule/receive"
|
|
19
|
+
require_relative "router/registry/receives"
|
|
20
|
+
require_relative "router/rule/observe"
|
|
21
|
+
require_relative "router/registry/observes"
|
|
22
|
+
require_relative "router/rule/otherwise"
|
|
23
|
+
require_relative "router/registry/otherwises"
|
|
24
|
+
require_relative "router/flow/dispatch"
|
|
25
|
+
require_relative "router/flow/inward"
|
|
26
|
+
require_relative "router/flow/outward"
|
|
27
|
+
require_relative "router/router_update"
|
|
28
|
+
|
|
8
29
|
module Rooibos
|
|
9
|
-
#
|
|
30
|
+
# Fractal routing DSL for composing hierarchical updates.
|
|
10
31
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# Writing this routing logic by hand is tedious and error-prone.
|
|
32
|
+
# A growing app accumulates message-handling logic. One Update handles
|
|
33
|
+
# dozens of cases. Model fields multiply. View code sprawls.
|
|
14
34
|
#
|
|
15
|
-
# Include
|
|
16
|
-
#
|
|
35
|
+
# Include Router in a fragment module. It decomposes your Update
|
|
36
|
+
# into declarative rules: routes bind nested fragments to model slices,
|
|
37
|
+
# forwards route messages inward, receives handle them exclusively,
|
|
38
|
+
# and observers process without stopping the flow.
|
|
17
39
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# delegate to child fragments.
|
|
40
|
+
# Use it to build tab containers, panel layouts, or any hierarchy
|
|
41
|
+
# where messages flow inward through nested fragments.
|
|
21
42
|
#
|
|
22
43
|
# === Example
|
|
23
44
|
#
|
|
24
|
-
#
|
|
45
|
+
# module Dashboard
|
|
25
46
|
# include Rooibos::Router
|
|
26
47
|
#
|
|
27
|
-
# route :
|
|
28
|
-
# route :
|
|
48
|
+
# route :sidebar, to: Sidebar
|
|
49
|
+
# route :main, to: MainPanel
|
|
50
|
+
#
|
|
51
|
+
# receive_events :ctrl_c, :quit
|
|
52
|
+
# action :quit, -> { Rooibos::Command.exit }
|
|
29
53
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# key "q", -> { Command.exit }
|
|
33
|
-
# end
|
|
54
|
+
# forward_events :enter, to: :main, as: :submit
|
|
55
|
+
# otherwise route_to: :main
|
|
34
56
|
#
|
|
35
|
-
# Model = Data.define(:stats, :network)
|
|
36
|
-
# Init = -> { Model.new(stats: StatsPanel::Init.(), network: NetworkPanel::Init.()) }
|
|
37
|
-
# View = ->(model, tui) { ... }
|
|
38
57
|
# Update = from_router
|
|
39
58
|
# end
|
|
40
59
|
module Router
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
super
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Configuration for scroll handlers (no coordinates).
|
|
49
|
-
ScrollHandlerConfig = Data.define(:handler, :action) do
|
|
50
|
-
def initialize(handler: nil, action: nil)
|
|
51
|
-
super
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Configuration for click handlers (x, y coordinates).
|
|
56
|
-
ClickHandlerConfig = Data.define(:handler, :action) do
|
|
57
|
-
def initialize(handler: nil, action: nil)
|
|
58
|
-
super
|
|
59
|
-
end
|
|
60
|
-
end
|
|
60
|
+
# Sentinel for "all routes" - unique object prevents accidental collision
|
|
61
|
+
ALL_ROUTES = Object.new.freeze
|
|
62
|
+
private_constant :ALL_ROUTES
|
|
61
63
|
|
|
62
64
|
def self.included(base) # :nodoc:
|
|
63
65
|
base.extend(ClassMethods)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
#
|
|
68
|
+
# The Router declaration surface.
|
|
69
|
+
#
|
|
70
|
+
# Fragments grow. One Update handles dozens of cases. Routing logic,
|
|
71
|
+
# keybindings, and guard conditions tangle together.
|
|
72
|
+
#
|
|
73
|
+
# These class methods decompose that logic into declarative rules.
|
|
74
|
+
# Declare routes, forwards, receives, observes, and otherwises.
|
|
75
|
+
# Call <tt>from_router</tt> to freeze them into an Update callable.
|
|
76
|
+
#
|
|
77
|
+
# Use it inside any module that includes <tt>Rooibos::Router</tt>.
|
|
67
78
|
module ClassMethods
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# [fragment_model_instance_attr] Symbol naming the attr on the parent's model
|
|
71
|
-
# that holds this fragment's model instance (normalized via +.to_s.to_sym+).
|
|
72
|
-
# [to] The child fragment module (must have Update and Init constants).
|
|
73
|
-
def route(fragment_model_instance_attr, to:)
|
|
74
|
-
routes[fragment_model_instance_attr.to_s.to_sym] = to
|
|
79
|
+
private def routes
|
|
80
|
+
@routes ||= Routes.new
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@routes ||= {}
|
|
83
|
+
private def actions
|
|
84
|
+
@actions ||= Actions.new
|
|
80
85
|
end
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Actions are shared handlers that keymap and mousemap can reference.
|
|
85
|
-
# This avoids duplicating logic for keys and mouse events that do
|
|
86
|
-
# the same thing.
|
|
87
|
-
#
|
|
88
|
-
# Supports both positional and keyword syntax:
|
|
89
|
-
# action :scroll_up, -> { Command.scroll(-1) } # Positional
|
|
90
|
-
# action scroll_up: -> { Command.scroll(-1) } # Keyword
|
|
91
|
-
#
|
|
92
|
-
# [name] Symbol or String identifying the action (normalized via +.to_s.to_sym+).
|
|
93
|
-
# [value] Callable that returns a command or message.
|
|
94
|
-
def action(name = nil, value = nil, keymap: nil, key: nil, keys: nil, mousemap: nil, **kwargs)
|
|
95
|
-
# key: and keys: are aliases for keymap:
|
|
96
|
-
effective_keymap = keymap || key || keys
|
|
97
|
-
action_name, action_value = if name && value
|
|
98
|
-
# Positional: action :name, handler
|
|
99
|
-
[name, value]
|
|
100
|
-
elsif name.respond_to?(:call) && value.nil?
|
|
101
|
-
# Anonymous: action -> { ... }, keymap: %i[...]
|
|
102
|
-
# No name, just handler with bindings
|
|
103
|
-
[nil, name]
|
|
104
|
-
elsif kwargs.size == 1
|
|
105
|
-
# Keyword: action name: handler
|
|
106
|
-
kwargs.first
|
|
107
|
-
else
|
|
108
|
-
raise ArgumentError, "action requires (name, value) or (name: value)"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# @type var action_name: Symbol?
|
|
112
|
-
# @type var action_value: (^() -> Command::execution? | Module)?
|
|
113
|
-
register_action(action_name, action_value) if action_name && action_value
|
|
114
|
-
|
|
115
|
-
# For anonymous actions, store handler directly in keymap
|
|
116
|
-
handler_for_keymap = action_name.nil? ? action_value : nil
|
|
117
|
-
|
|
118
|
-
# Register keymap bindings if provided
|
|
119
|
-
if effective_keymap
|
|
120
|
-
Array(effective_keymap).each do |key_name|
|
|
121
|
-
key_handlers[key_name.to_s.to_sym] = Router::KeyHandlerConfig.new(
|
|
122
|
-
handler: handler_for_keymap,
|
|
123
|
-
action: action_name&.to_s&.to_sym,
|
|
124
|
-
guard: nil,
|
|
125
|
-
route: nil
|
|
126
|
-
)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Register mousemap bindings if provided
|
|
131
|
-
if mousemap
|
|
132
|
-
Array(mousemap).each do |mouse_event|
|
|
133
|
-
scroll_handlers[mouse_event.to_s.to_sym] = Router::ScrollHandlerConfig.new(
|
|
134
|
-
handler: handler_for_keymap,
|
|
135
|
-
action: action_name&.to_s&.to_sym
|
|
136
|
-
)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
87
|
+
private def forwards
|
|
88
|
+
@forwards ||= Forwards.new
|
|
139
89
|
end
|
|
140
90
|
|
|
141
|
-
private def
|
|
142
|
-
|
|
143
|
-
case value
|
|
144
|
-
when Module
|
|
145
|
-
routed_actions[key] = value
|
|
146
|
-
else
|
|
147
|
-
actions[key] = value
|
|
148
|
-
end
|
|
91
|
+
private def receives
|
|
92
|
+
@receives ||= Receives.new(actions:)
|
|
149
93
|
end
|
|
150
94
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@actions ||= {}
|
|
95
|
+
private def observes
|
|
96
|
+
@observes ||= Observes.new(actions:)
|
|
154
97
|
end
|
|
155
98
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@routed_actions ||= {}
|
|
99
|
+
private def otherwises
|
|
100
|
+
@otherwises ||= Otherwises.new
|
|
159
101
|
end
|
|
160
102
|
|
|
161
|
-
#
|
|
103
|
+
# Assembles all declared routes, forwards, receives, observes, and
|
|
104
|
+
# otherwises into a frozen RouterUpdate callable.
|
|
105
|
+
#
|
|
106
|
+
# Call this once at the end of your Router declarations. Assign the
|
|
107
|
+
# result to <tt>Update</tt> so the runtime dispatches messages through
|
|
108
|
+
# your router.
|
|
109
|
+
#
|
|
110
|
+
# Raises Rooibos::Error::Invariant if any forward or otherwise target
|
|
111
|
+
# is ambiguous (e.g. two routes share the same prefix or fragment).
|
|
162
112
|
#
|
|
163
113
|
# === Example
|
|
164
114
|
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
115
|
+
# module MyFragment
|
|
116
|
+
# include Rooibos::Router
|
|
117
|
+
#
|
|
118
|
+
# route :child, to: ChildFragment
|
|
119
|
+
# forward_events :enter, to: :child, as: :submit
|
|
120
|
+
#
|
|
121
|
+
# Update = from_router
|
|
168
122
|
# end
|
|
169
|
-
def
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
123
|
+
def from_router
|
|
124
|
+
RouterUpdate.new(
|
|
125
|
+
inward: Flow::Inward.new(observes:, receives:, forwards:, otherwises:, routes:),
|
|
126
|
+
outward: Flow::Outward.new(observes:, receives:, routes:)
|
|
127
|
+
)
|
|
173
128
|
end
|
|
174
129
|
|
|
175
|
-
# Declares
|
|
130
|
+
# Declares a child route binding a nested fragment to a model slice.
|
|
131
|
+
#
|
|
132
|
+
# The simplest form names a model attribute. <tt>:sidebar</tt> means
|
|
133
|
+
# "read from <tt>model.sidebar</tt>, write back with
|
|
134
|
+
# <tt>model.with(sidebar: ...)</tt>."
|
|
135
|
+
#
|
|
136
|
+
# When your model stores fragments in hashes or other structures, pass
|
|
137
|
+
# <tt>read:</tt> and <tt>write:</tt> lambdas for custom extraction and
|
|
138
|
+
# merging. A route with lambdas has no prefix symbol.
|
|
139
|
+
#
|
|
140
|
+
# Returns the Route object. Capture it when neither the prefix symbol
|
|
141
|
+
# nor the fragment module can unambiguously identify the route.
|
|
142
|
+
#
|
|
143
|
+
# [prefix] Symbol or String naming the model attribute. Optional when
|
|
144
|
+
# using <tt>read:</tt>/<tt>write:</tt>.
|
|
145
|
+
# [to] The fragment module whose <tt>Update</tt> handles messages.
|
|
146
|
+
# [read] Lambda <tt>->(model) -> nested_model</tt>. Overrides prefix-based extraction.
|
|
147
|
+
# [write] Lambda <tt>->(model, value) -> model</tt>. Overrides prefix-based merging.
|
|
176
148
|
#
|
|
177
149
|
# === Example
|
|
178
150
|
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
151
|
+
# # Named attribute (most common)
|
|
152
|
+
# route :sidebar, to: Sidebar
|
|
153
|
+
#
|
|
154
|
+
# # Custom accessors for hash-stored fragments
|
|
155
|
+
# route read: ->(model) { model.panels[:sidebar] },
|
|
156
|
+
# write: ->(model, value) { model.with(panels: model.panels.merge(sidebar: value)) },
|
|
157
|
+
# to: Sidebar
|
|
158
|
+
#
|
|
159
|
+
# # Capture for disambiguation
|
|
160
|
+
# ACTIVE = route read: ->(m) { m.tabs[m.active_tab] },
|
|
161
|
+
# write: ->(m, v) { m.with(tabs: m.tabs.merge(m.active_tab => v)) },
|
|
162
|
+
# to: TabContent
|
|
163
|
+
# forward_events :enter, to: ACTIVE, as: :submit
|
|
164
|
+
def route(prefix = nil, to:, read: nil, write: nil, **)
|
|
165
|
+
routes.add(Route.new(prefix: prefix&.to_s&.to_sym, fragment: to, read:, write:))
|
|
188
166
|
end
|
|
189
167
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
|
|
168
|
+
# Forwards all instances of a class to routes.
|
|
169
|
+
#
|
|
170
|
+
# Matches messages by class. Ideal for custom message types or
|
|
171
|
+
# RatatuiRuby event classes like <tt>Event::Resize</tt>.
|
|
172
|
+
#
|
|
173
|
+
# Use <tt>broadcast: true</tt> to send to all declared routes, or
|
|
174
|
+
# <tt>broadcast_to:</tt> with an array of specific route targets.
|
|
175
|
+
#
|
|
176
|
+
# === Example
|
|
177
|
+
#
|
|
178
|
+
# forward_instances_of RatatuiRuby::Event::Resize, to: :main_layout
|
|
179
|
+
# forward_instances_of ThemeChanged, broadcast: true
|
|
180
|
+
def forward_instances_of(klass, ...)
|
|
181
|
+
forwards.add_instances_of(klass, ...)
|
|
193
182
|
end
|
|
194
183
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
# Defines a named action referenceable by symbol.
|
|
185
|
+
#
|
|
186
|
+
# Actions are reusable handlers. Reference them by name in
|
|
187
|
+
# <tt>receive*</tt>, <tt>intercept*</tt>, and <tt>observe*</tt>
|
|
188
|
+
# methods anywhere a handler lambda is accepted.
|
|
189
|
+
#
|
|
190
|
+
# Lambda actions run directly. Routed actions dispatch a
|
|
191
|
+
# <tt>Message::Routed</tt> to a fragment, using the action name as
|
|
192
|
+
# the envelope.
|
|
193
|
+
#
|
|
194
|
+
# [name] Symbol identifying the action.
|
|
195
|
+
# [handler] A lambda or a fragment Module for routed dispatch.
|
|
196
|
+
#
|
|
197
|
+
# === Example
|
|
198
|
+
#
|
|
199
|
+
# # Lambda action
|
|
200
|
+
# action :quit, -> { Rooibos::Command.exit }
|
|
201
|
+
#
|
|
202
|
+
# # Keyword form
|
|
203
|
+
# action scroll_up: ->(_, model) { model.with(offset: model.offset - 1) }
|
|
204
|
+
#
|
|
205
|
+
# # Routed action (dispatches :go_back to HistoryPanel)
|
|
206
|
+
# action :go_back, HistoryPanel
|
|
207
|
+
def action(name = nil, handler = nil, **kwargs)
|
|
208
|
+
if name && handler
|
|
209
|
+
actions.add(name, handler)
|
|
210
|
+
elsif kwargs.any?
|
|
211
|
+
kwargs.each { |k, v| actions.add(k, v) }
|
|
212
|
+
else
|
|
213
|
+
raise ArgumentError, "action requires name and handler, or keyword arguments"
|
|
214
|
+
end
|
|
198
215
|
end
|
|
199
216
|
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
# Handles matching key events directly. Stops further processing.
|
|
218
|
+
#
|
|
219
|
+
# Matches raw RatatuiRuby events by their <tt>to_sym</tt> value.
|
|
220
|
+
# The second argument is an action name (Symbol) or a handler lambda.
|
|
221
|
+
# The first matching receive wins; later handlers do not run.
|
|
222
|
+
#
|
|
223
|
+
# <tt>intercept_events</tt> is an alias. Use <tt>receive</tt> when the
|
|
224
|
+
# message is addressed to you. Use <tt>intercept</tt> when stopping a
|
|
225
|
+
# bubbled message mid-chain.
|
|
226
|
+
#
|
|
227
|
+
# === Example
|
|
228
|
+
#
|
|
229
|
+
# receive_events :ctrl_c, :quit
|
|
230
|
+
# receive_events :q, :quit
|
|
231
|
+
# receive_events :enter, ->(_, model) { model.with(submitted: true) }
|
|
232
|
+
def receive_events(...)
|
|
233
|
+
receives.add_events(...)
|
|
203
234
|
end
|
|
204
235
|
|
|
205
|
-
#
|
|
236
|
+
# Handles matching routed messages. Stops further processing.
|
|
206
237
|
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
click_handler:
|
|
220
|
-
)
|
|
238
|
+
# Matches <tt>Message::Routed</tt> messages by their envelope symbol.
|
|
239
|
+
# Use this when an outer fragment has forwarded a message with
|
|
240
|
+
# <tt>as:</tt> and your fragment handles it.
|
|
241
|
+
#
|
|
242
|
+
# <tt>intercept_routed</tt> is an alias.
|
|
243
|
+
#
|
|
244
|
+
# === Example
|
|
245
|
+
#
|
|
246
|
+
# receive_routed :panel_self,
|
|
247
|
+
# ->(_, model) { model.with(count: model.count + 1) }
|
|
248
|
+
def receive_routed(...)
|
|
249
|
+
receives.add_routed(...)
|
|
221
250
|
end
|
|
222
|
-
end
|
|
223
251
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
# Handles matching class instances. Stops further processing.
|
|
253
|
+
#
|
|
254
|
+
# Matches messages by class. Use <tt>receive</tt> for messages
|
|
255
|
+
# addressed to you. Use <tt>intercept</tt> to stop a bubbled message
|
|
256
|
+
# mid-chain.
|
|
257
|
+
#
|
|
258
|
+
# <tt>intercept_instances_of</tt> is an alias.
|
|
259
|
+
#
|
|
260
|
+
# === Example
|
|
261
|
+
#
|
|
262
|
+
# receive_instances_of FatalError,
|
|
263
|
+
# ->(msg, model) { [model.with(error: msg), Rooibos::Command.exit] }
|
|
264
|
+
def receive_instances_of(...)
|
|
265
|
+
receives.add_instances_of(...)
|
|
233
266
|
end
|
|
234
267
|
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if message.is_a?(RatatuiRuby::Event::Key)
|
|
249
|
-
@key_handlers.each do |key_name, config|
|
|
250
|
-
predicate = :"#{key_name}?"
|
|
251
|
-
next unless message.respond_to?(predicate) && message.public_send(predicate)
|
|
252
|
-
|
|
253
|
-
# Check guard if present
|
|
254
|
-
if (config.guard) && !config.guard.call(model)
|
|
255
|
-
next
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Get handler - either inline or from actions registry
|
|
259
|
-
handler = config.handler
|
|
260
|
-
if handler.nil? && config.action
|
|
261
|
-
handler = @actions[config.action]
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Check for routed action if no handler found
|
|
265
|
-
if handler.nil? && config.action
|
|
266
|
-
routed_fragment = @routed_actions[config.action]
|
|
267
|
-
if routed_fragment
|
|
268
|
-
# Find the model attr for this fragment
|
|
269
|
-
fragment_model_instance_attr = @routes.key(routed_fragment)
|
|
270
|
-
next unless fragment_model_instance_attr
|
|
271
|
-
|
|
272
|
-
# Synthesize Message::Routed and dispatch to child
|
|
273
|
-
routed_message = Rooibos::Message::Routed.new(envelope: config.action, event: message)
|
|
274
|
-
child_update = routed_fragment.const_get(:Update)
|
|
275
|
-
previous_child_fragment_model_instance = model.public_send(fragment_model_instance_attr)
|
|
276
|
-
updated_child_fragment_model_instance, command = child_update.call(routed_message, previous_child_fragment_model_instance)
|
|
277
|
-
return [model.with(fragment_model_instance_attr => updated_child_fragment_model_instance), command]
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
next unless handler
|
|
282
|
-
|
|
283
|
-
command = handler.call
|
|
284
|
-
if command && config.route
|
|
285
|
-
command = Rooibos.route(command, config.route)
|
|
286
|
-
end
|
|
287
|
-
return [model, command] #: [_DataModel, Command::execution?]
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# 3. Try mousemap handlers (message is an Event::Mouse)
|
|
292
|
-
if message.is_a?(RatatuiRuby::Event::Mouse)
|
|
293
|
-
# Scroll events (handler takes no arguments)
|
|
294
|
-
if message.scroll_up?
|
|
295
|
-
config = @scroll_handlers[:scroll_up]
|
|
296
|
-
if config
|
|
297
|
-
scroll_handler = config.handler
|
|
298
|
-
if scroll_handler.nil? && config.action
|
|
299
|
-
scroll_handler = @actions[config.action]
|
|
300
|
-
end
|
|
301
|
-
return [model, scroll_handler&.call] #: [_DataModel, Command::execution?]
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
if message.scroll_down?
|
|
305
|
-
config = @scroll_handlers[:scroll_down]
|
|
306
|
-
if config
|
|
307
|
-
scroll_handler = config.handler
|
|
308
|
-
if scroll_handler.nil? && config.action
|
|
309
|
-
scroll_handler = @actions[config.action]
|
|
310
|
-
end
|
|
311
|
-
return [model, scroll_handler&.call] #: [_DataModel, Command::execution?]
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
# Click events (handler takes x, y coordinates)
|
|
315
|
-
click_config = @click_handler
|
|
316
|
-
if message.down? && click_config
|
|
317
|
-
click_handler_proc = click_config.handler
|
|
318
|
-
if click_handler_proc.nil? && click_config.action
|
|
319
|
-
# Actions don't take coordinates, so just call without args
|
|
320
|
-
action_handler = @actions[click_config.action]
|
|
321
|
-
return [model, action_handler&.call] #: [_DataModel, Command::execution?]
|
|
322
|
-
elsif click_handler_proc
|
|
323
|
-
return [model, click_handler_proc.call(message.x, message.y)] #: [_DataModel, Command::execution?]
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# 4. Unhandled - return model unchanged
|
|
329
|
-
[model, nil] #: [_DataModel, Command::execution?]
|
|
268
|
+
# Handles any message. Stops further processing.
|
|
269
|
+
#
|
|
270
|
+
# Matches every message. Combine with guards to create conditional
|
|
271
|
+
# catch-alls. For example, block all input when a fragment is inactive.
|
|
272
|
+
#
|
|
273
|
+
# <tt>intercept_all</tt> is an alias.
|
|
274
|
+
#
|
|
275
|
+
# === Example
|
|
276
|
+
#
|
|
277
|
+
# receive_all ->(msg, model) { [model, nil] },
|
|
278
|
+
# unless: ->(_, model) { model.active }
|
|
279
|
+
def receive_all(...)
|
|
280
|
+
receives.add_all(...)
|
|
330
281
|
end
|
|
331
|
-
end
|
|
332
|
-
private_constant :RouterUpdate
|
|
333
|
-
|
|
334
|
-
# Builder for keymap DSL.
|
|
335
|
-
class KeymapBuilder
|
|
336
|
-
# Returns the registered handlers hash.
|
|
337
|
-
attr_reader :handlers
|
|
338
282
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
283
|
+
# Handles messages matching a custom predicate. Stops further processing.
|
|
284
|
+
#
|
|
285
|
+
# The predicate lambda receives <tt>(message, model)</tt>. If it returns
|
|
286
|
+
# a truthy value, the handler runs and no later handlers execute.
|
|
287
|
+
#
|
|
288
|
+
# <tt>intercept</tt> is an alias.
|
|
289
|
+
#
|
|
290
|
+
# === Example
|
|
291
|
+
#
|
|
292
|
+
# receive ->(msg, _) { msg.key? && msg.text? },
|
|
293
|
+
# ->(msg, model) { model.with(buffer: model.buffer + msg.char) }
|
|
294
|
+
def receive(...)
|
|
295
|
+
receives.add_custom(...)
|
|
342
296
|
end
|
|
343
297
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# key :down, :j, action: :move_down # Multiple keys with action
|
|
350
|
-
# key :enter, -> { ... }, route: :foo # With options
|
|
351
|
-
#
|
|
352
|
-
# [*key_names] One or more key names (String or Symbol).
|
|
353
|
-
# [handler_or_action] Callable or Symbol (action name) - optional if action: given.
|
|
354
|
-
# [action] Action name as keyword arg (alternative to positional).
|
|
355
|
-
# [route] Optional route prefix for the command result.
|
|
356
|
-
# [when/if/only/guard] Guard that runs if truthy (aliases).
|
|
357
|
-
# [unless/except/skip] Guard that runs if falsy (negative aliases).
|
|
358
|
-
def key(*args, action: nil, route: nil, when: nil, if: nil, only: nil, guard: nil, unless: nil, except: nil, skip: nil, **bindings)
|
|
359
|
-
# Parse args: all symbols/strings are keys, last callable is handler
|
|
360
|
-
key_names = [] #: Array[Symbol | String]
|
|
361
|
-
handler = nil
|
|
362
|
-
action_name = action
|
|
363
|
-
|
|
364
|
-
args.each do |arg|
|
|
365
|
-
if arg.is_a?(Hash)
|
|
366
|
-
# Hash passed positionally: key({q: -> { ... }})
|
|
367
|
-
bindings.merge!(arg)
|
|
368
|
-
elsif arg.respond_to?(:call)
|
|
369
|
-
handler = arg
|
|
370
|
-
elsif arg.is_a?(Symbol) || arg.is_a?(String)
|
|
371
|
-
# Could be a key name or action name (positional action from old API)
|
|
372
|
-
key_names << arg
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Keyword syntax: key ctrl_c: -> { ... }
|
|
377
|
-
# Each kwarg is key_name => handler
|
|
378
|
-
bindings.each do |key_name, handler_or_action|
|
|
379
|
-
if handler_or_action.respond_to?(:call)
|
|
380
|
-
register_key_handler(key_name, handler_or_action, nil, route, nil)
|
|
381
|
-
else
|
|
382
|
-
register_key_handler(key_name, nil, handler_or_action, route, nil)
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
# If we had keyword bindings, skip positional processing
|
|
387
|
-
return if bindings.any?
|
|
388
|
-
|
|
389
|
-
# Old API: key :q, :quit - last symbol is the action
|
|
390
|
-
if handler.nil? && action_name.nil? && key_names.size >= 2
|
|
391
|
-
# Check if last "key" is actually an action by seeing if it looks like a handler
|
|
392
|
-
action_name = key_names.pop
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
guards = @guard_stack.dup
|
|
396
|
-
|
|
397
|
-
# Positive guards (when, if, only, guard)
|
|
398
|
-
positive = binding.local_variable_get(:when) ||
|
|
399
|
-
binding.local_variable_get(:if) ||
|
|
400
|
-
only ||
|
|
401
|
-
guard
|
|
402
|
-
guards << positive if positive
|
|
403
|
-
|
|
404
|
-
# Negative guards (unless, except, skip) - wrap to invert
|
|
405
|
-
negative = binding.local_variable_get(:unless) || except || skip
|
|
406
|
-
if negative
|
|
407
|
-
guards << -> (model) { !negative.call(model) }
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
combined_guard = if guards.any?
|
|
411
|
-
-> (model) { guards.all? { |g| g.call(model) } }
|
|
412
|
-
end
|
|
298
|
+
alias intercept_events receive_events
|
|
299
|
+
alias intercept_routed receive_routed
|
|
300
|
+
alias intercept_instances_of receive_instances_of
|
|
301
|
+
alias intercept_all receive_all
|
|
302
|
+
alias intercept receive
|
|
413
303
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
304
|
+
# Routes matching key events to a declared route.
|
|
305
|
+
#
|
|
306
|
+
# Matches raw RatatuiRuby events by their <tt>to_sym</tt> value.
|
|
307
|
+
# Pass a symbol for a single event or an array for multiple events
|
|
308
|
+
# that route to the same destination.
|
|
309
|
+
#
|
|
310
|
+
# The <tt>to:</tt> parameter accepts a symbol (model attribute), a
|
|
311
|
+
# module (fragment), or a Route (return value of <tt>route</tt>).
|
|
312
|
+
#
|
|
313
|
+
# Use <tt>as:</tt> to wrap the event in a <tt>Message::Routed</tt>
|
|
314
|
+
# with a semantic envelope. This decouples keybindings from nested
|
|
315
|
+
# fragment internals.
|
|
316
|
+
#
|
|
317
|
+
# === Example
|
|
318
|
+
#
|
|
319
|
+
# forward_events :enter, to: :active_form, as: :submit
|
|
320
|
+
# forward_events [:up, :k], to: :list, as: :move_up
|
|
321
|
+
def forward_events(keys, to: @_scoped_target, **)
|
|
322
|
+
forwards.add_events(keys, to:, **)
|
|
418
323
|
end
|
|
419
324
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
325
|
+
# Routes matching routed messages to a declared route.
|
|
326
|
+
#
|
|
327
|
+
# Matches <tt>Message::Routed</tt> messages by envelope. Use this
|
|
328
|
+
# when an outer fragment has already routed an event and you need
|
|
329
|
+
# to route it further to a nested fragment.
|
|
330
|
+
#
|
|
331
|
+
# Use <tt>as:</tt> to transform the envelope before forwarding.
|
|
332
|
+
# Each layer speaks its inner fragment's API without knowing what
|
|
333
|
+
# lies deeper.
|
|
334
|
+
#
|
|
335
|
+
# === Example
|
|
336
|
+
#
|
|
337
|
+
# forward_routed :leaf_1, to: :top_leaf, as: :increment
|
|
338
|
+
# forward_routed :leaf_2, to: :bottom_leaf, as: :increment
|
|
339
|
+
def forward_routed(envelopes, to: @_scoped_target, **)
|
|
340
|
+
forwards.add_routed(envelopes, to:, **)
|
|
427
341
|
end
|
|
428
342
|
|
|
429
|
-
#
|
|
430
|
-
alias_method :keys, :key
|
|
431
|
-
|
|
432
|
-
# Applies a guard to all keys in the block.
|
|
343
|
+
# Routes any message to a declared route.
|
|
433
344
|
#
|
|
434
|
-
#
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
positive = binding.local_variable_get(:when) ||
|
|
447
|
-
binding.local_variable_get(:if) ||
|
|
448
|
-
only ||
|
|
449
|
-
guard
|
|
450
|
-
with_guard(positive, &)
|
|
345
|
+
# Matches every message. Combine with guards to conditionally route
|
|
346
|
+
# unhandled messages. Without guards, acts as a catch-all forward.
|
|
347
|
+
#
|
|
348
|
+
# === Example
|
|
349
|
+
#
|
|
350
|
+
# only when: -> (_, model) { model.active_tab == :counter_tab } do
|
|
351
|
+
# forward_all to: :counter_tab
|
|
352
|
+
# end
|
|
353
|
+
# forward_all to: :active_panel
|
|
354
|
+
def forward_all(to: @_scoped_target, **guard_opts)
|
|
355
|
+
forwards.add_custom(Predicate::Always.new, to:, **guard_opts)
|
|
451
356
|
end
|
|
452
357
|
|
|
453
|
-
#
|
|
358
|
+
# Routes messages matching a custom predicate to a declared route.
|
|
454
359
|
#
|
|
455
|
-
#
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
360
|
+
# The predicate lambda receives <tt>(message, model)</tt>. If it
|
|
361
|
+
# returns a truthy value, the message is forwarded. Use this for
|
|
362
|
+
# complex matching logic that the specialized variants cannot express.
|
|
363
|
+
#
|
|
364
|
+
# === Example
|
|
365
|
+
#
|
|
366
|
+
# forward ->(msg, _) { msg.key? && msg.ctrl? }, to: :editor
|
|
367
|
+
# forward ->(msg, _) { msg.key? && msg.shift? }, to: Sidebar
|
|
368
|
+
def forward(predicate, to: @_scoped_target, **)
|
|
369
|
+
forwards.add_custom(predicate, to:, **)
|
|
370
|
+
end
|
|
462
371
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
372
|
+
# Observes matching key events. Does not stop further processing.
|
|
373
|
+
#
|
|
374
|
+
# Matches raw RatatuiRuby events by <tt>to_sym</tt>. All matching
|
|
375
|
+
# observers run in declaration order. The message continues to later
|
|
376
|
+
# handlers. Use observe for side effects that should not block other
|
|
377
|
+
# handlers: logging, counting, updating derived state.
|
|
378
|
+
#
|
|
379
|
+
# === Example
|
|
380
|
+
#
|
|
381
|
+
# observe_events :enter,
|
|
382
|
+
# ->(_, model) { [model, Rooibos::Command.custom(Logger.log("Enter pressed"))] }
|
|
383
|
+
def observe_events(...)
|
|
384
|
+
observes.add_events(...)
|
|
385
|
+
end
|
|
466
386
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
387
|
+
# Observes matching routed messages. Does not stop further processing.
|
|
388
|
+
#
|
|
389
|
+
# Matches <tt>Message::Routed</tt> by envelope. The message continues
|
|
390
|
+
# to later handlers after this observer runs.
|
|
391
|
+
#
|
|
392
|
+
# === Example
|
|
393
|
+
#
|
|
394
|
+
# observe_routed :submit,
|
|
395
|
+
# ->(_, model) { model.with(submissions: model.submissions + 1) }
|
|
396
|
+
def observe_routed(...)
|
|
397
|
+
observes.add_routed(...)
|
|
398
|
+
end
|
|
471
399
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
400
|
+
# Observes matching class instances. Does not stop further processing.
|
|
401
|
+
#
|
|
402
|
+
# Matches messages by class. Use it to react to custom message types
|
|
403
|
+
# while allowing them to continue to other handlers.
|
|
404
|
+
#
|
|
405
|
+
# === Example
|
|
406
|
+
#
|
|
407
|
+
# observe_instances_of LeafReset,
|
|
408
|
+
# ->(_, model) { model.with(nested_resets: model.nested_resets + 1) }
|
|
409
|
+
def observe_instances_of(...)
|
|
410
|
+
observes.add_instances_of(...)
|
|
475
411
|
end
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
412
|
+
|
|
413
|
+
# Observes any message. Does not stop further processing.
|
|
414
|
+
#
|
|
415
|
+
# Matches every message. Useful for metrics, debugging, or global
|
|
416
|
+
# state updates that apply regardless of message type.
|
|
417
|
+
#
|
|
418
|
+
# === Example
|
|
419
|
+
#
|
|
420
|
+
# observe_all ->(msg, model) {
|
|
421
|
+
# model.with(message_count: model.message_count + 1)
|
|
422
|
+
# }
|
|
423
|
+
def observe_all(...)
|
|
424
|
+
observes.add_all(...)
|
|
487
425
|
end
|
|
488
|
-
end
|
|
489
426
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
#
|
|
493
|
-
|
|
427
|
+
# Observes messages matching a custom predicate. Does not stop further
|
|
428
|
+
# processing.
|
|
429
|
+
#
|
|
430
|
+
# The predicate lambda receives <tt>(message, model)</tt>. All matching
|
|
431
|
+
# observers run. The message continues to later handlers.
|
|
432
|
+
#
|
|
433
|
+
# === Example
|
|
434
|
+
#
|
|
435
|
+
# observe ->(msg, _) { msg.leaf_reset? || msg.panel_reset? },
|
|
436
|
+
# ->(_, model) { model.with(total_resets: model.total_resets + 1) }
|
|
437
|
+
def observe(...)
|
|
438
|
+
observes.add_custom(...)
|
|
439
|
+
end
|
|
494
440
|
|
|
495
|
-
#
|
|
496
|
-
|
|
441
|
+
# Catches unhandled messages as a router-level fallback.
|
|
442
|
+
#
|
|
443
|
+
# Messages not handled by <tt>receive</tt>, <tt>intercept</tt>, or
|
|
444
|
+
# <tt>forward</tt> fall through to <tt>otherwise</tt>. The
|
|
445
|
+
# <tt>route_to:</tt> parameter accepts the same three forms as
|
|
446
|
+
# <tt>to:</tt> in the forward family. Multiple <tt>otherwise</tt>
|
|
447
|
+
# declarations with guards create a conditional fallthrough chain.
|
|
448
|
+
#
|
|
449
|
+
# This keeps outer fragments minimal. Declare what you handle;
|
|
450
|
+
# everything else flows to the nested fragment.
|
|
451
|
+
#
|
|
452
|
+
# === Example
|
|
453
|
+
#
|
|
454
|
+
# otherwise route_to: :counter_tab,
|
|
455
|
+
# when: ->(_, model) { model.active_tab == :counter }
|
|
456
|
+
# otherwise route_to: :color_tab,
|
|
457
|
+
# when: ->(_, model) { model.active_tab == :color }
|
|
458
|
+
# otherwise route_to: :dashboard
|
|
459
|
+
def otherwise(...)
|
|
460
|
+
otherwises.add(...)
|
|
461
|
+
end
|
|
497
462
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
463
|
+
# Scopes a positive guard over declarations within the block.
|
|
464
|
+
#
|
|
465
|
+
# Every forward, receive, intercept, observe, and otherwise inside
|
|
466
|
+
# the block runs only when the guard returns truthy. Blocks nest;
|
|
467
|
+
# inner guards combine with outer ones.
|
|
468
|
+
#
|
|
469
|
+
# === Example
|
|
470
|
+
#
|
|
471
|
+
# only when: -> (_, model) { model.focused? } do
|
|
472
|
+
# forward_events :j, to: :list, as: :move_down
|
|
473
|
+
# end
|
|
474
|
+
private def only(when: nil, if: nil, &)
|
|
475
|
+
Guard.scoped(binding.local_variable_get(:when) || binding.local_variable_get(:if), &)
|
|
501
476
|
end
|
|
502
477
|
|
|
503
|
-
#
|
|
478
|
+
# Scopes a negative guard over declarations within the block.
|
|
504
479
|
#
|
|
505
|
-
#
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
480
|
+
# The inverse of <tt>only</tt>. Declarations inside the block run
|
|
481
|
+
# only when the guard returns falsy.
|
|
482
|
+
#
|
|
483
|
+
# === Example
|
|
484
|
+
#
|
|
485
|
+
# skip when: -> (_, model) { model.locked? } do
|
|
486
|
+
# receive_events :d, :delete
|
|
487
|
+
# end
|
|
488
|
+
private def skip(when: nil, if: nil, &)
|
|
489
|
+
positive = binding.local_variable_get(:when) || binding.local_variable_get(:if)
|
|
490
|
+
Guard.scoped(-> (msg, model) { !positive.call(msg, model) }, &)
|
|
512
491
|
end
|
|
513
492
|
|
|
514
|
-
#
|
|
493
|
+
# Scopes the default <tt>to:</tt> target for forwards and receives
|
|
494
|
+
# within the block.
|
|
515
495
|
#
|
|
516
|
-
#
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
496
|
+
# Avoids repeating <tt>to: :some_route</tt> on every declaration.
|
|
497
|
+
# The scoped target resets after the block.
|
|
498
|
+
#
|
|
499
|
+
# === Example
|
|
500
|
+
#
|
|
501
|
+
# route_to :left_panel do
|
|
502
|
+
# forward_events :a, as: :panel_self
|
|
503
|
+
# forward_events :"1", as: :leaf_1
|
|
504
|
+
# forward_events :"2", as: :leaf_2
|
|
505
|
+
# end
|
|
506
|
+
private def route_to(target)
|
|
507
|
+
@_scoped_target = target
|
|
508
|
+
yield if block_given?
|
|
509
|
+
ensure
|
|
510
|
+
@_scoped_target = nil
|
|
525
511
|
end
|
|
526
512
|
end
|
|
527
513
|
end
|