rooibos 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +9 -5
- data/.builds/ruby-3.3.yml +9 -5
- data/.builds/ruby-3.4.yml +9 -5
- data/.builds/ruby-4.0.0.yml +9 -5
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +57 -0
- data/README.md +2 -2
- data/README.rdoc +374 -0
- data/REUSE.toml +5 -0
- data/Rakefile +1 -1
- data/doc/best_practices/forms_and_validation.md +20 -0
- data/doc/best_practices/http_workflows.md +20 -0
- data/doc/best_practices/index.md +26 -0
- data/doc/best_practices/lists_and_tables.md +20 -0
- data/doc/best_practices/modal_dialogs.md +20 -0
- data/doc/best_practices/no_stateful_widgets.md +184 -0
- data/doc/best_practices/orchestration.md +20 -0
- data/doc/best_practices/streaming_data.md +20 -0
- data/doc/contributors/design/commands_and_outlets.md +1 -1
- data/doc/contributors/documentation_plan.md +616 -0
- data/doc/contributors/documentation_stub_audit.md +112 -0
- data/doc/contributors/documentation_style.md +275 -0
- data/doc/contributors/e2e_pty.md +168 -0
- data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
- data/doc/contributors/specs/file_browser.md +789 -0
- data/doc/contributors/specs/file_browser_stories.md +774 -0
- data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
- data/doc/contributors/todo/scrollbar.md +118 -0
- data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
- data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
- data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
- data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
- data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
- data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
- data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
- data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
- data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
- data/doc/contributors/tutorial_old/12_going_further.md +20 -0
- data/doc/contributors/tutorial_old/index.md +20 -0
- data/doc/essentials/commands.md +20 -0
- data/doc/essentials/index.md +31 -0
- data/doc/essentials/messages.md +21 -0
- data/doc/essentials/models.md +21 -0
- data/doc/essentials/shortcuts.md +19 -0
- data/doc/essentials/the_elm_architecture.md +24 -0
- data/doc/essentials/the_runtime.md +21 -0
- data/doc/essentials/update_functions.md +20 -0
- data/doc/essentials/views.md +22 -0
- data/doc/getting_started/for_go_developers.md +16 -0
- data/doc/getting_started/for_python_developers.md +16 -0
- data/doc/getting_started/for_rails_developers.md +17 -0
- data/doc/getting_started/for_ratatui_ruby_developers.md +17 -0
- data/doc/getting_started/for_react_developers.md +17 -0
- data/doc/getting_started/index.md +52 -0
- data/doc/getting_started/install.md +20 -0
- data/doc/getting_started/quickstart.md +9 -45
- data/doc/getting_started/ruby_primer.md +19 -0
- data/doc/getting_started/why_rooibos.md +20 -0
- data/doc/index.md +79 -11
- data/doc/scaling_up/async_patterns.md +20 -0
- data/doc/scaling_up/command_composition.md +20 -0
- data/doc/scaling_up/custom_commands.md +21 -0
- data/doc/scaling_up/fractal_architecture.md +20 -0
- data/doc/scaling_up/index.md +30 -0
- data/doc/scaling_up/message_routing.md +20 -0
- data/doc/scaling_up/ractor_safety.md +20 -0
- data/doc/scaling_up/testing.md +21 -0
- data/doc/troubleshooting/common_errors.md +20 -0
- data/doc/troubleshooting/debugging.md +21 -0
- data/doc/troubleshooting/index.md +23 -0
- data/doc/troubleshooting/performance.md +20 -0
- data/doc/tutorial/01_project_setup.md +44 -0
- data/doc/tutorial/02_hello_world.md +45 -0
- data/doc/tutorial/03_static_file_list.md +44 -0
- data/doc/tutorial/04_arrow_navigation.md +47 -0
- data/doc/tutorial/05_real_files.md +45 -0
- data/doc/tutorial/06_safe_refactoring.md +21 -0
- data/doc/tutorial/07_red_first_tdd.md +26 -0
- data/doc/tutorial/08_file_metadata.md +42 -0
- data/doc/tutorial/09_text_preview.md +44 -0
- data/doc/tutorial/10_directory_tree.md +42 -0
- data/doc/tutorial/11_pane_focus.md +40 -0
- data/doc/tutorial/12_sorting.md +41 -0
- data/doc/tutorial/13_filtering.md +43 -0
- data/doc/tutorial/14_toggle_hidden.md +41 -0
- data/doc/tutorial/15_text_input_widget.md +43 -0
- data/doc/tutorial/16_rename_files.md +42 -0
- data/doc/tutorial/17_confirmation_dialogs.md +43 -0
- data/doc/tutorial/18_progress_indicators.md +43 -0
- data/doc/tutorial/19_atomic_operations.md +42 -0
- data/doc/tutorial/20_external_editor.md +42 -0
- data/doc/tutorial/21_modal_overlays.md +41 -0
- data/doc/tutorial/22_error_handling.md +43 -0
- data/doc/tutorial/23_terminal_capabilities.md +53 -0
- data/doc/tutorial/24_mouse_events.md +43 -0
- data/doc/tutorial/25_resize_events.md +43 -0
- data/doc/tutorial/26_loading_states.md +42 -0
- data/doc/tutorial/27_performance.md +43 -0
- data/doc/tutorial/28_color_schemes.md +47 -0
- data/doc/tutorial/29_configuration.md +124 -0
- data/doc/tutorial/30_going_further.md +17 -0
- data/doc/tutorial/index.md +17 -0
- data/examples/app_file_browser/app.rb +40 -0
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
- data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
- data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
- data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
- data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
- data/examples/verify_website_first_app/app.rb +85 -0
- data/examples/verify_website_hello_mvu/app.rb +31 -0
- data/examples/widget_command_system/app.rb +15 -13
- data/exe/rooibos +10 -0
- data/generate_tutorial_stubs.rb +126 -0
- data/lib/rooibos/cli/commands/new.rb +373 -0
- data/lib/rooibos/cli/commands/run.rb +98 -0
- data/lib/rooibos/cli.rb +78 -0
- data/lib/rooibos/command/all.rb +76 -23
- data/lib/rooibos/command/batch.rb +61 -34
- data/lib/rooibos/command/custom.rb +84 -1
- data/lib/rooibos/command/http.rb +121 -55
- data/lib/rooibos/command/lifecycle.rb +5 -5
- data/lib/rooibos/command/open.rb +93 -0
- data/lib/rooibos/command/outlet.rb +105 -3
- data/lib/rooibos/command/wait.rb +9 -6
- data/lib/rooibos/command.rb +114 -89
- data/lib/rooibos/message/batch.rb +39 -0
- data/lib/rooibos/message/canceled.rb +51 -0
- data/lib/rooibos/message/error.rb +48 -0
- data/lib/rooibos/message/open.rb +30 -0
- data/lib/rooibos/message.rb +84 -4
- data/lib/rooibos/router.rb +11 -14
- data/lib/rooibos/runtime.rb +40 -43
- data/lib/rooibos/shortcuts.rb +47 -0
- data/lib/rooibos/test_helper.rb +71 -6
- data/lib/rooibos/version.rb +1 -1
- data/lib/rooibos/welcome.rb +237 -0
- data/lib/rooibos.rb +4 -3
- data/mise.toml +1 -1
- data/rbs_collection.lock.yaml +2 -2
- data/sig/concurrent.rbs +4 -0
- data/sig/gem.rbs +20 -0
- data/sig/rooibos/cli.rbs +42 -0
- data/sig/rooibos/command.rbs +59 -7
- data/sig/rooibos/message.rbs +66 -2
- data/sig/rooibos/shortcuts.rbs +14 -0
- data/sig/rooibos/test_helper.rbs +6 -2
- data/sig/rooibos/welcome.rbs +75 -0
- data/tasks/install.rake +29 -0
- data/tasks/resources/build.yml.erb +2 -0
- metadata +274 -38
- data/doc/concepts/application_architecture.md +0 -197
- data/doc/concepts/application_testing.md +0 -49
- data/doc/concepts/async_work.md +0 -164
- data/doc/concepts/commands.md +0 -530
- data/doc/concepts/message_processing.md +0 -51
- data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
- data/doc/contributors/WIP/implementation_plan.md +0 -409
- data/doc/contributors/WIP/init_callable_proposal.md +0 -344
- data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
- data/doc/contributors/WIP/task.md +0 -36
- data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
- data/doc/contributors/kit-no-outlet.md +0 -238
- data/doc/contributors/priorities.md +0 -38
- data/doc/images/.gitkeep +0 -0
- data/exe/.gitkeep +0 -0
- /data/doc/contributors/{WIP → design}/mvu_tea_implementations_research.md +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
module Rooibos
|
|
9
|
+
module Message
|
|
10
|
+
# Error message from a failed command.
|
|
11
|
+
#
|
|
12
|
+
# Commands run in background threads. Exceptions bubble up silently.
|
|
13
|
+
# Your update function never sees them. Backtraces in STDERR corrupt the TUI.
|
|
14
|
+
#
|
|
15
|
+
# The runtime catches exceptions and wraps them in Error messages.
|
|
16
|
+
# Pattern match on Error in your update function. Display the error, log it, or recover.
|
|
17
|
+
#
|
|
18
|
+
# Use it to surface failures from HTTP requests, file I/O, or external processes.
|
|
19
|
+
#
|
|
20
|
+
# === Examples
|
|
21
|
+
#
|
|
22
|
+
# Update = ->(message, model) {
|
|
23
|
+
# case message
|
|
24
|
+
# in { type: :error, command:, exception: }
|
|
25
|
+
# # Show error toast
|
|
26
|
+
# model.with(error: exception.message)
|
|
27
|
+
# in Message::Error
|
|
28
|
+
# # Store for later inspection
|
|
29
|
+
# model.with(last_error: message.exception)
|
|
30
|
+
# end
|
|
31
|
+
# }
|
|
32
|
+
Error = Data.define(:command, :exception) do
|
|
33
|
+
include Predicates
|
|
34
|
+
|
|
35
|
+
# Returns <tt>true</tt> for error messages.
|
|
36
|
+
def error?
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Deconstructs for pattern matching.
|
|
41
|
+
#
|
|
42
|
+
# Returns a hash with <tt>type</tt>, <tt>command</tt>, and <tt>exception</tt>.
|
|
43
|
+
def deconstruct_keys(_keys)
|
|
44
|
+
{ type: :error, command:, exception: }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
module Rooibos
|
|
9
|
+
module Message
|
|
10
|
+
# Signals that a file or URL was successfully opened.
|
|
11
|
+
#
|
|
12
|
+
# This message arrives after +Command.open+ completes with exit status 0.
|
|
13
|
+
# Non-zero exits produce +Message::Error+ instead.
|
|
14
|
+
#
|
|
15
|
+
# === Pattern Matching
|
|
16
|
+
#
|
|
17
|
+
# case message
|
|
18
|
+
# in { type: :open, envelope: path }
|
|
19
|
+
# model.with(status: "Opened #{path}")
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class Open < Data.define(:envelope)
|
|
23
|
+
include Predicates
|
|
24
|
+
|
|
25
|
+
def deconstruct_keys(_keys)
|
|
26
|
+
{ type: :open, envelope: }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/rooibos/message.rb
CHANGED
|
@@ -8,21 +8,97 @@
|
|
|
8
8
|
module Rooibos
|
|
9
9
|
# Messages sent from commands to update functions.
|
|
10
10
|
#
|
|
11
|
-
# All built-in response types live here. Each includes the
|
|
11
|
+
# All built-in response types live here. Each includes the <tt>Predicates</tt>
|
|
12
12
|
# mixin for safe predicate calls.
|
|
13
13
|
module Message
|
|
14
|
+
# Matches built-in framework message types for case/when dispatch.
|
|
15
|
+
#
|
|
16
|
+
# Returns <tt>true</tt> only for classes under <tt>Rooibos::Message::</tt>.
|
|
17
|
+
# Rejects key events and user-defined message classes.
|
|
18
|
+
#
|
|
19
|
+
# === Example
|
|
20
|
+
#
|
|
21
|
+
# case message
|
|
22
|
+
# when Rooibos::Message
|
|
23
|
+
# handle_command_response(message)
|
|
24
|
+
# when RatatuiRuby::Event::Key
|
|
25
|
+
# handle_key(message)
|
|
26
|
+
# end
|
|
27
|
+
def self.===(other)
|
|
28
|
+
other.class.name&.start_with?("Rooibos::Message::")
|
|
29
|
+
end
|
|
30
|
+
|
|
14
31
|
# Fallback predicate mixin.
|
|
15
32
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
33
|
+
# Update functions receive many message types. Checking unknown predicates
|
|
34
|
+
# crashes with NoMethodError. Verifying every predicate clutters the code.
|
|
35
|
+
#
|
|
36
|
+
# This mixin returns <tt>false</tt> for unknown predicates. It also adds
|
|
37
|
+
# symbol comparison via <tt>to_sym</tt> and <tt>==</tt>.
|
|
38
|
+
#
|
|
39
|
+
# Include in custom message types for safe predicate calls and symbol matching.
|
|
18
40
|
module Predicates
|
|
19
|
-
#
|
|
41
|
+
# Converts the message to a Symbol.
|
|
42
|
+
#
|
|
43
|
+
# Returns the <tt>:type</tt> value from <tt>deconstruct_keys</tt> prefixed
|
|
44
|
+
# with <tt>message_</tt>. The prefix avoids collision with RatatuiRuby
|
|
45
|
+
# event symbols like <tt>:resize</tt> or <tt>:mouse</tt>.
|
|
46
|
+
#
|
|
47
|
+
# === Example
|
|
48
|
+
#
|
|
49
|
+
# timer = Message::Timer.new(envelope: :tick, elapsed: 0.016)
|
|
50
|
+
# timer.to_sym # => :message_timer
|
|
51
|
+
def to_sym
|
|
52
|
+
:"message_#{deconstruct_keys(nil)[:type]}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Compares the message with another object.
|
|
56
|
+
#
|
|
57
|
+
# Symbols compare against <tt>to_sym</tt>. Other objects use default equality.
|
|
58
|
+
#
|
|
59
|
+
# === Example
|
|
60
|
+
#
|
|
61
|
+
# if message == :message_timer
|
|
62
|
+
# handle_tick(message)
|
|
63
|
+
# end
|
|
64
|
+
def ==(other)
|
|
65
|
+
case other
|
|
66
|
+
when Symbol then to_sym == other
|
|
67
|
+
else super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns <tt>false</tt> for unknown predicate methods.
|
|
20
72
|
def method_missing(name, *args, **kwargs, &block)
|
|
21
73
|
return false if name.to_s.end_with?("?") && args.empty? && kwargs.empty?
|
|
22
74
|
|
|
23
75
|
super
|
|
24
76
|
end
|
|
25
77
|
|
|
78
|
+
# Fallback pattern matching for classes without explicit deconstruct_keys.
|
|
79
|
+
#
|
|
80
|
+
# Derives <tt>:type</tt> from the class name in snake_case. Anonymous
|
|
81
|
+
# classes default to <tt>:custom</tt>.
|
|
82
|
+
#
|
|
83
|
+
# === Example
|
|
84
|
+
#
|
|
85
|
+
# class MyCustomMessage
|
|
86
|
+
# include Rooibos::Message::Predicates
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# msg = MyCustomMessage.new
|
|
90
|
+
# msg.deconstruct_keys(nil) # => { type: :my_custom_message }
|
|
91
|
+
# msg.to_sym # => :message_my_custom_message
|
|
92
|
+
def deconstruct_keys(_keys)
|
|
93
|
+
class_name = self.class.name&.split("::")&.last
|
|
94
|
+
type_name = if class_name
|
|
95
|
+
class_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
|
96
|
+
else
|
|
97
|
+
:custom
|
|
98
|
+
end
|
|
99
|
+
{ type: type_name }
|
|
100
|
+
end
|
|
101
|
+
|
|
26
102
|
# Responds to all predicate methods.
|
|
27
103
|
def respond_to_missing?(name, *)
|
|
28
104
|
name.to_s.end_with?("?")
|
|
@@ -32,7 +108,11 @@ module Rooibos
|
|
|
32
108
|
end
|
|
33
109
|
|
|
34
110
|
require_relative "message/timer"
|
|
111
|
+
require_relative "message/open"
|
|
35
112
|
require_relative "message/http_response"
|
|
36
113
|
require_relative "message/system/batch"
|
|
37
114
|
require_relative "message/system/stream"
|
|
38
115
|
require_relative "message/all"
|
|
116
|
+
require_relative "message/batch"
|
|
117
|
+
require_relative "message/error"
|
|
118
|
+
require_relative "message/canceled"
|
data/lib/rooibos/router.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
#--
|
|
4
2
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
5
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
@@ -13,10 +13,10 @@ module Rooibos
|
|
|
13
13
|
# Writing this routing logic by hand is tedious and error-prone.
|
|
14
14
|
#
|
|
15
15
|
# Include this module to declare routes and keymaps. Call +from_router+ to
|
|
16
|
-
# generate an
|
|
16
|
+
# generate an Update lambda that handles routing automatically.
|
|
17
17
|
#
|
|
18
|
-
# A *fragment* is a module containing <tt>Model</tt>, <tt>
|
|
19
|
-
# <tt>
|
|
18
|
+
# A *fragment* is a module containing <tt>Model</tt>, <tt>Init</tt>,
|
|
19
|
+
# <tt>Update</tt>, and <tt>View</tt> constants. Fragments compose: parent fragments
|
|
20
20
|
# delegate to child fragments.
|
|
21
21
|
#
|
|
22
22
|
# === Example
|
|
@@ -33,9 +33,9 @@ module Rooibos
|
|
|
33
33
|
# end
|
|
34
34
|
#
|
|
35
35
|
# Model = Data.define(:stats, :network)
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
36
|
+
# Init = -> { Model.new(stats: StatsPanel::Init.(), network: NetworkPanel::Init.()) }
|
|
37
|
+
# View = ->(model, tui) { ... }
|
|
38
|
+
# Update = from_router
|
|
39
39
|
# end
|
|
40
40
|
module Router
|
|
41
41
|
# Configuration for key handlers.
|
|
@@ -59,8 +59,7 @@ module Rooibos
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# :nodoc:
|
|
63
|
-
def self.included(base)
|
|
62
|
+
def self.included(base) # :nodoc:
|
|
64
63
|
base.extend(ClassMethods)
|
|
65
64
|
end
|
|
66
65
|
|
|
@@ -254,8 +253,7 @@ module Rooibos
|
|
|
254
253
|
# Returns the registered handlers hash.
|
|
255
254
|
attr_reader :handlers
|
|
256
255
|
|
|
257
|
-
# :nodoc:
|
|
258
|
-
def initialize
|
|
256
|
+
def initialize # :nodoc:
|
|
259
257
|
@handlers = {}
|
|
260
258
|
@guard_stack = []
|
|
261
259
|
end
|
|
@@ -369,8 +367,7 @@ module Rooibos
|
|
|
369
367
|
# Returns the registered click handler.
|
|
370
368
|
attr_reader :click_handler
|
|
371
369
|
|
|
372
|
-
# :nodoc:
|
|
373
|
-
def initialize
|
|
370
|
+
def initialize # :nodoc:
|
|
374
371
|
@scroll_handlers = {}
|
|
375
372
|
@click_handler = nil
|
|
376
373
|
end
|
data/lib/rooibos/runtime.rb
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
require "ratatui_ruby"
|
|
9
9
|
require "concurrent-edge"
|
|
10
10
|
|
|
11
|
+
# Enable inline sync mode for deterministic event ordering in tests.
|
|
12
|
+
# This ensures poll_event returns Event::Sync in sequence with key events.
|
|
13
|
+
RatatuiRuby::SyntheticEvents.inline_sync!
|
|
14
|
+
|
|
11
15
|
module Rooibos
|
|
12
16
|
# Runs the Model-View-Update event loop.
|
|
13
17
|
#
|
|
@@ -95,13 +99,17 @@ module Rooibos
|
|
|
95
99
|
#
|
|
96
100
|
# == Explicit Parameters API
|
|
97
101
|
#
|
|
98
|
-
#
|
|
102
|
+
# Tests need deterministic state. Init reads from the filesystem, network, or
|
|
103
|
+
# environment—sources that change between runs. Injecting a known model makes
|
|
104
|
+
# tests reproducible.
|
|
105
|
+
#
|
|
106
|
+
# Pass <tt>model:</tt>, <tt>view:</tt>, and <tt>update:</tt> directly. The runtime
|
|
107
|
+
# skips Init and uses your model as the starting state.
|
|
99
108
|
#
|
|
100
109
|
# Rooibos.run(
|
|
101
|
-
# model: MyApp::Model.new(count: 0),
|
|
110
|
+
# model: Ractor.make_shareable(MyApp::Model.new(count: 0)),
|
|
102
111
|
# view: MyApp::View,
|
|
103
|
-
# update: MyApp::Update
|
|
104
|
-
# command: Command.http("https://api.example.com/data")
|
|
112
|
+
# update: MyApp::Update
|
|
105
113
|
# )
|
|
106
114
|
#
|
|
107
115
|
# == Parameters
|
|
@@ -120,14 +128,8 @@ module Rooibos
|
|
|
120
128
|
@fragment = fragment_from_kwargs(root_fragment, model:, view:, update:, command:)
|
|
121
129
|
@view = @fragment::View
|
|
122
130
|
@update = @fragment::Update
|
|
123
|
-
@
|
|
124
|
-
@timeout = 1 / fps
|
|
125
|
-
|
|
126
|
-
# commands do significant work, so they run off the main thread
|
|
127
|
-
validate_ractor_shareable!(@command, "command")
|
|
128
|
-
# models get passed to and from commands on other threads
|
|
129
|
-
validate_ractor_shareable!(@model, "model")
|
|
130
|
-
# views and updates run on the main thread, so they don't need to be shareable
|
|
131
|
+
@init_callable = init_callable
|
|
132
|
+
@timeout = 1.0 / fps
|
|
131
133
|
|
|
132
134
|
start_runtime
|
|
133
135
|
end
|
|
@@ -176,13 +178,18 @@ module Rooibos
|
|
|
176
178
|
@lifecycle = Command::Lifecycle.new
|
|
177
179
|
|
|
178
180
|
catch(QUIT) do
|
|
179
|
-
dispatch_command
|
|
180
181
|
RatatuiRuby.run do |tui|
|
|
181
182
|
@tui = tui
|
|
183
|
+
|
|
184
|
+
# Init runs after terminal is ready so it can query terminal_size, etc.
|
|
185
|
+
@model, @command = @init_callable.call
|
|
186
|
+
validate_ractor_shareable!(@command, "command")
|
|
187
|
+
validate_ractor_shareable!(@model, "model")
|
|
188
|
+
dispatch_command
|
|
189
|
+
|
|
182
190
|
loop do
|
|
183
191
|
draw_view
|
|
184
192
|
handle_ratatui_event
|
|
185
|
-
handle_sync
|
|
186
193
|
send_pending_messages
|
|
187
194
|
end
|
|
188
195
|
end
|
|
@@ -198,9 +205,12 @@ module Rooibos
|
|
|
198
205
|
end
|
|
199
206
|
|
|
200
207
|
private def draw_view
|
|
208
|
+
# Build widget tree OUTSIDE draw context - queries work here
|
|
209
|
+
widget = @view.call(@model, @tui)
|
|
210
|
+
validate_view_return!(widget)
|
|
211
|
+
|
|
212
|
+
# Render INSIDE draw context - only rendering happens here
|
|
201
213
|
@tui.draw do |frame|
|
|
202
|
-
widget = @view.call(@model, @tui)
|
|
203
|
-
validate_view_return!(widget)
|
|
204
214
|
frame.render_widget(widget, frame.area)
|
|
205
215
|
end
|
|
206
216
|
end
|
|
@@ -322,37 +332,22 @@ module Rooibos
|
|
|
322
332
|
|
|
323
333
|
private def handle_ratatui_event
|
|
324
334
|
message = @tui.poll_event(timeout: @timeout)
|
|
325
|
-
return if message.none?
|
|
335
|
+
return false if message.none?
|
|
336
|
+
|
|
337
|
+
# Handle sync events: wait for pending async work before continuing
|
|
338
|
+
if message.sync?
|
|
339
|
+
@pending_futures.each(&:wait)
|
|
340
|
+
@pending_futures.clear
|
|
341
|
+
Thread.pass
|
|
342
|
+
send_pending_messages
|
|
343
|
+
return true
|
|
344
|
+
end
|
|
326
345
|
|
|
327
346
|
@model, @command = normalize_update_return(@update.call(message, @model), @model)
|
|
328
347
|
validate_ractor_shareable!(@model, "model")
|
|
329
348
|
throw QUIT if Command::Exit === @command
|
|
330
349
|
dispatch_command
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# This must come *after* handle_ratatui_event so Sync waits for commands
|
|
334
|
-
# dispatched by the preceding event. For example, in a test:
|
|
335
|
-
#
|
|
336
|
-
# inject_key("a")
|
|
337
|
-
# inject_sync
|
|
338
|
-
#
|
|
339
|
-
# We need <kbd>a</kbd> to call the Update and queue its message before
|
|
340
|
-
# processing the Sync.
|
|
341
|
-
private def handle_sync
|
|
342
|
-
if RatatuiRuby::SyntheticEvents.pending?
|
|
343
|
-
synthetic = RatatuiRuby::SyntheticEvents.pop
|
|
344
|
-
if synthetic&.sync?
|
|
345
|
-
# Wait for all pending futures to complete
|
|
346
|
-
@pending_futures.each(&:wait)
|
|
347
|
-
@pending_futures.clear
|
|
348
|
-
|
|
349
|
-
# Yield to ensure any final queue writes are visible
|
|
350
|
-
Thread.pass
|
|
351
|
-
|
|
352
|
-
# Process all pending message queue items
|
|
353
|
-
send_pending_messages
|
|
354
|
-
end
|
|
355
|
-
end
|
|
350
|
+
true # Event was processed
|
|
356
351
|
end
|
|
357
352
|
|
|
358
353
|
QUEUE_EMPTY = Object.new.freeze
|
|
@@ -380,7 +375,9 @@ module Rooibos
|
|
|
380
375
|
future = if @command.nil?
|
|
381
376
|
nil
|
|
382
377
|
elsif Command::Cancel === @command
|
|
383
|
-
@lifecycle.cancel(@command.handle)
|
|
378
|
+
entry = @lifecycle.cancel(@command.handle)
|
|
379
|
+
# Remove cancelled future from pending list so sync doesn't wait for it
|
|
380
|
+
@pending_futures.delete(entry.future) if entry
|
|
384
381
|
nil
|
|
385
382
|
elsif @command.respond_to?(:rooibos_command?) && @command.rooibos_command?
|
|
386
383
|
entry = @lifecycle.run_async(@command, @message_queue)
|
data/lib/rooibos/shortcuts.rb
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
8
|
require_relative "command"
|
|
9
|
+
require_relative "message"
|
|
9
10
|
|
|
10
11
|
module Rooibos
|
|
11
12
|
# Convenient short aliases for Rooibos APIs.
|
|
@@ -45,5 +46,51 @@ module Rooibos
|
|
|
45
46
|
Command.map(inner_command, &mapper)
|
|
46
47
|
end
|
|
47
48
|
end
|
|
49
|
+
|
|
50
|
+
# Short aliases for +Message+ types.
|
|
51
|
+
#
|
|
52
|
+
# App developers pattern-match against message types frequently.
|
|
53
|
+
# The full names (+Rooibos::Message::HttpResponse+) are verbose.
|
|
54
|
+
# These shortcuts save characters and improve readability.
|
|
55
|
+
#
|
|
56
|
+
# === Example
|
|
57
|
+
#
|
|
58
|
+
# case message
|
|
59
|
+
# in Msg::Timer[envelope: :dismiss]
|
|
60
|
+
# [model.with(notification: nil), nil]
|
|
61
|
+
# in Msg::Http[status: 200, body:]
|
|
62
|
+
# [model.with(data: JSON.parse(body)), nil]
|
|
63
|
+
# in Msg::Sh::Batch[status: 0, stdout:]
|
|
64
|
+
# [model.with(output: stdout), nil]
|
|
65
|
+
# end
|
|
66
|
+
module Msg
|
|
67
|
+
# Timer message type.
|
|
68
|
+
# Alias for +Message::Timer+.
|
|
69
|
+
Timer = Message::Timer
|
|
70
|
+
|
|
71
|
+
# HTTP response message type.
|
|
72
|
+
# Alias for +Message::HttpResponse+.
|
|
73
|
+
Http = Message::HttpResponse
|
|
74
|
+
|
|
75
|
+
# Shell command message types.
|
|
76
|
+
# Mirrors +Cmd.sh+ for symmetry.
|
|
77
|
+
module Sh
|
|
78
|
+
# Batch mode shell output.
|
|
79
|
+
# Alias for +Message::System::Batch+.
|
|
80
|
+
Batch = Message::System::Batch
|
|
81
|
+
|
|
82
|
+
# Streaming mode shell output.
|
|
83
|
+
# Alias for +Message::System::Stream+.
|
|
84
|
+
Stream = Message::System::Stream
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Aggregated parallel results.
|
|
88
|
+
# Alias for +Message::All+.
|
|
89
|
+
All = Message::All
|
|
90
|
+
|
|
91
|
+
# Batch completion signal.
|
|
92
|
+
# Alias for +Message::Batch+.
|
|
93
|
+
Batch = Message::Batch
|
|
94
|
+
end
|
|
48
95
|
end
|
|
49
96
|
end
|
data/lib/rooibos/test_helper.rb
CHANGED
|
@@ -8,11 +8,33 @@
|
|
|
8
8
|
require "ratatui_ruby/test_helper"
|
|
9
9
|
|
|
10
10
|
module Rooibos
|
|
11
|
-
#
|
|
11
|
+
# Assertions and test utilities for Rooibos applications.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Custom commands run in background threads. Forgetting to include
|
|
14
|
+
# <tt>Command::Custom</tt> causes cryptic Ractor errors. Validating
|
|
15
|
+
# protocol compliance manually is tedious.
|
|
16
|
+
#
|
|
17
|
+
# This module provides Rooibos-specific assertions. It also includes
|
|
18
|
+
# {RatatuiRuby::TestHelper}[https://www.ratatui-ruby.dev/docs/v1.0/RatatuiRuby/TestHelper.html],
|
|
19
|
+
# giving you access to <tt>with_test_terminal</tt>, <tt>inject_key</tt>, etc.
|
|
20
|
+
#
|
|
21
|
+
# Use it in Minitest classes to validate commands and control test terminals.
|
|
22
|
+
#
|
|
23
|
+
# === Example
|
|
24
|
+
#
|
|
25
|
+
# class TestMyApp < Minitest::Test
|
|
26
|
+
# include Rooibos::TestHelper
|
|
27
|
+
#
|
|
28
|
+
# def test_app_exits_on_ctrl_c
|
|
29
|
+
# with_test_terminal do
|
|
30
|
+
# inject_key(:ctrl_c)
|
|
31
|
+
# Rooibos.run(MyApp)
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
15
35
|
module TestHelper
|
|
36
|
+
include RatatuiRuby::TestHelper
|
|
37
|
+
|
|
16
38
|
# Validates a command implements the Rooibos command protocol.
|
|
17
39
|
#
|
|
18
40
|
# Custom commands run in background threads. They dispatch work and send messages.
|
|
@@ -49,8 +71,51 @@ module Rooibos
|
|
|
49
71
|
"Include Command::Custom or implement this method."
|
|
50
72
|
end
|
|
51
73
|
end
|
|
74
|
+
|
|
75
|
+
# Fails if any Message::Error is present in the messages array.
|
|
76
|
+
#
|
|
77
|
+
# Call after running the runtime and before asserting on expected messages.
|
|
78
|
+
# This ensures tests fail fast with helpful error messages instead of
|
|
79
|
+
# silently passing when errors occur.
|
|
80
|
+
#
|
|
81
|
+
# [messages] Array of messages collected from the update function.
|
|
82
|
+
# [msg] Optional custom failure message prefix.
|
|
83
|
+
#
|
|
84
|
+
# === Example
|
|
85
|
+
#
|
|
86
|
+
# def test_dashboard_loads_data
|
|
87
|
+
# messages = []
|
|
88
|
+
# update = -> (msg, m) do
|
|
89
|
+
# # ... handle keys ...
|
|
90
|
+
# messages << msg
|
|
91
|
+
# [m, nil]
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# with_test_terminal do
|
|
95
|
+
# inject_key("s")
|
|
96
|
+
# inject_sync
|
|
97
|
+
# inject_key("q")
|
|
98
|
+
# Rooibos::Runtime.run(model:, view:, update:)
|
|
99
|
+
# end
|
|
100
|
+
#
|
|
101
|
+
# assert_no_errors(messages)
|
|
102
|
+
# # ... rest of assertions
|
|
103
|
+
# end
|
|
104
|
+
#
|
|
105
|
+
def assert_no_errors(messages, msg = nil)
|
|
106
|
+
error = messages.find { |m| m.is_a?(Rooibos::Message::Error) }
|
|
107
|
+
return unless error
|
|
108
|
+
|
|
109
|
+
error_detail = "#{error.exception.class}: #{error.exception.message}"
|
|
110
|
+
failure_msg = msg ? "#{msg}\n#{error_detail}" : "Unexpected Message::Error: #{error_detail}"
|
|
111
|
+
|
|
112
|
+
if respond_to?(:flunk)
|
|
113
|
+
# rubocop:disable Style/SendWithLiteralMethodName
|
|
114
|
+
public_send(:flunk, failure_msg)
|
|
115
|
+
# rubocop:enable Style/SendWithLiteralMethodName
|
|
116
|
+
else
|
|
117
|
+
raise failure_msg
|
|
118
|
+
end
|
|
119
|
+
end
|
|
52
120
|
end
|
|
53
121
|
end
|
|
54
|
-
|
|
55
|
-
# Attach Rooibos test helpers to RatatuiRuby::TestHelper
|
|
56
|
-
RatatuiRuby::TestHelper.include(Rooibos::TestHelper)
|
data/lib/rooibos/version.rb
CHANGED