rooibos 0.5.0 → 0.6.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/.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 +46 -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_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 +25 -20
- data/lib/rooibos/command/batch.rb +26 -25
- data/lib/rooibos/command/custom.rb +84 -1
- data/lib/rooibos/command/http.rb +59 -55
- data/lib/rooibos/command/lifecycle.rb +5 -5
- data/lib/rooibos/command/open.rb +86 -0
- data/lib/rooibos/command/outlet.rb +105 -3
- data/lib/rooibos/command/wait.rb +5 -5
- data/lib/rooibos/command.rb +57 -74
- 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 +3 -0
- data/sig/gem.rbs +20 -0
- data/sig/rooibos/cli.rbs +42 -0
- data/sig/rooibos/command.rbs +48 -0
- data/sig/rooibos/message.rbs +60 -0
- 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 +272 -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
data/lib/rooibos/command.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "command/wait"
|
|
|
13
13
|
require_relative "command/batch"
|
|
14
14
|
require_relative "command/all"
|
|
15
15
|
require_relative "command/http"
|
|
16
|
+
require_relative "command/open"
|
|
16
17
|
|
|
17
18
|
module Rooibos
|
|
18
19
|
# Commands represent side effects.
|
|
@@ -67,11 +68,11 @@ module Rooibos
|
|
|
67
68
|
|
|
68
69
|
# Creates a fresh cancellation that never fires.
|
|
69
70
|
#
|
|
70
|
-
# Some I/O operations cannot be
|
|
71
|
+
# Some I/O operations cannot be canceled mid-execution. Ruby's <tt>Net::HTTP</tt>
|
|
71
72
|
# blocks until completion or timeout — there is no way to interrupt it.
|
|
72
73
|
#
|
|
73
74
|
# A shared singleton would be unsafe. If any code path accidentally resolves
|
|
74
|
-
# the origin, all commands using it become
|
|
75
|
+
# the origin, all commands using it become canceled.
|
|
75
76
|
#
|
|
76
77
|
# Use it for commands that wrap non-cancellable blocking I/O.
|
|
77
78
|
#
|
|
@@ -94,6 +95,7 @@ module Rooibos
|
|
|
94
95
|
# on <tt>Command::Cancel</tt> and signals the token.
|
|
95
96
|
class Cancel < Data.define(:handle)
|
|
96
97
|
include Custom
|
|
98
|
+
include Message::Predicates
|
|
97
99
|
|
|
98
100
|
# Stub - Cancel is a sentinel handled by runtime before dispatch.
|
|
99
101
|
def call(_out, _token)
|
|
@@ -121,55 +123,6 @@ module Rooibos
|
|
|
121
123
|
Cancel.new(handle:)
|
|
122
124
|
end
|
|
123
125
|
|
|
124
|
-
# Error message from a failed command.
|
|
125
|
-
#
|
|
126
|
-
# Commands run in background threads. Exceptions bubble up silently.
|
|
127
|
-
# Your update function never sees them. Backtraces in STDERR corrupt the TUI.
|
|
128
|
-
#
|
|
129
|
-
# The runtime catches exceptions and wraps them in Error messages.
|
|
130
|
-
# Pattern match on Error in your update function. Display the error, log it, or recover.
|
|
131
|
-
#
|
|
132
|
-
# Use it to surface failures from HTTP requests, file I/O, or external processes.
|
|
133
|
-
#
|
|
134
|
-
# === Examples
|
|
135
|
-
#
|
|
136
|
-
# Update = ->(message, model) {
|
|
137
|
-
# case message
|
|
138
|
-
# in Command::Error[command:, exception:]
|
|
139
|
-
# # Show error toast
|
|
140
|
-
# [model.with(error: exception.message), nil]
|
|
141
|
-
# in Command::Error[command: Command::Http, exception:]
|
|
142
|
-
# # Retry HTTP request
|
|
143
|
-
# [model, command]
|
|
144
|
-
# in Command::Error
|
|
145
|
-
# # Log and continue
|
|
146
|
-
# warn "Command failed: #{message.exception}"
|
|
147
|
-
# [model, nil]
|
|
148
|
-
# end
|
|
149
|
-
# }
|
|
150
|
-
class Error < Data.define(:command, :exception); end
|
|
151
|
-
|
|
152
|
-
# Creates an error sentinel.
|
|
153
|
-
#
|
|
154
|
-
# The runtime produces this automatically when a command raises.
|
|
155
|
-
# Use this factory for testing or for commands that want to signal
|
|
156
|
-
# error completion without raising.
|
|
157
|
-
#
|
|
158
|
-
# [command] The command that failed.
|
|
159
|
-
# [exception] The exception that was raised.
|
|
160
|
-
#
|
|
161
|
-
# === Example
|
|
162
|
-
#
|
|
163
|
-
# def update(message, model)
|
|
164
|
-
# case message
|
|
165
|
-
# in Command::Error(command:, exception:)
|
|
166
|
-
# model.with(error: "#{command.class} failed: #{exception.message}")
|
|
167
|
-
# end
|
|
168
|
-
# end
|
|
169
|
-
def self.error(command, exception)
|
|
170
|
-
Error.new(command:, exception:)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
126
|
# Runs a shell command and routes its output back as messages.
|
|
174
127
|
#
|
|
175
128
|
# Apps run external tools: linters, compilers, scripts, system utilities.
|
|
@@ -320,9 +273,9 @@ module Rooibos
|
|
|
320
273
|
# # Then handle it later:
|
|
321
274
|
# def update(message, model)
|
|
322
275
|
# case message
|
|
323
|
-
# in
|
|
276
|
+
# in { type: :system, envelope: :got_files, stdout:, status: 0 }
|
|
324
277
|
# [model.with(files: stdout.lines), nil]
|
|
325
|
-
# in
|
|
278
|
+
# in { type: :system, envelope: :got_files, stderr:, status: }
|
|
326
279
|
# [model.with(error: stderr), nil]
|
|
327
280
|
# end
|
|
328
281
|
# end
|
|
@@ -335,14 +288,14 @@ module Rooibos
|
|
|
335
288
|
# # Then handle incremental messages:
|
|
336
289
|
# def update(message, model)
|
|
337
290
|
# case message
|
|
338
|
-
# in
|
|
291
|
+
# in { type: :system, envelope: :log, stream: :stdout, content: line }
|
|
339
292
|
# [model.with(lines: [*model.lines, line]), nil]
|
|
340
|
-
# in
|
|
293
|
+
# in { type: :system, envelope: :log, stream: :stderr, content: line }
|
|
341
294
|
# [model.with(errors: [*model.errors, line]), nil]
|
|
342
|
-
# in
|
|
295
|
+
# in { type: :system, envelope: :log, stream: :complete, status: }
|
|
343
296
|
# [model.with(loading: false, exit_status: status), nil]
|
|
344
|
-
# in
|
|
345
|
-
# [model.with(loading: false, error:
|
|
297
|
+
# in { type: :system, envelope: :log, stream: :error, content: msg }
|
|
298
|
+
# [model.with(loading: false, error: msg), nil]
|
|
346
299
|
# end
|
|
347
300
|
# end
|
|
348
301
|
def self.system(command, envelope, stream: false)
|
|
@@ -364,28 +317,35 @@ module Rooibos
|
|
|
364
317
|
class Mapped < Data.define(:inner_command, :mapper)
|
|
365
318
|
include Custom
|
|
366
319
|
|
|
320
|
+
DONE = Object.new.freeze
|
|
321
|
+
private_constant :DONE
|
|
322
|
+
|
|
367
323
|
# Grace period delegates to inner command.
|
|
368
324
|
def rooibos_cancellation_grace_period
|
|
369
325
|
inner_command.respond_to?(:rooibos_cancellation_grace_period) ?
|
|
370
326
|
inner_command.rooibos_cancellation_grace_period : 0.1
|
|
371
327
|
end
|
|
372
328
|
|
|
373
|
-
# Executes the inner command
|
|
329
|
+
# Executes the inner command and transforms each message.
|
|
374
330
|
def call(out, token)
|
|
375
331
|
inner_channel = Concurrent::Promises::Channel.new
|
|
376
332
|
inner_outlet = Outlet.new(inner_channel, lifecycle: out.live)
|
|
377
333
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
334
|
+
Concurrent::Promises.future do
|
|
335
|
+
if inner_command.respond_to?(:call)
|
|
336
|
+
inner_command.call(inner_outlet, token)
|
|
337
|
+
else
|
|
338
|
+
raise ArgumentError, "Inner command must respond to #call"
|
|
339
|
+
end
|
|
340
|
+
inner_channel.push(DONE)
|
|
383
341
|
end
|
|
384
342
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
343
|
+
loop do
|
|
344
|
+
msg = inner_channel.pop
|
|
345
|
+
break if msg.equal?(DONE)
|
|
346
|
+
transformed = mapper.call(msg)
|
|
347
|
+
out.put(*transformed) if transformed
|
|
348
|
+
end
|
|
389
349
|
end
|
|
390
350
|
end
|
|
391
351
|
|
|
@@ -405,8 +365,14 @@ module Rooibos
|
|
|
405
365
|
#
|
|
406
366
|
# # Parent wraps to route as [:sidebar, :got_files, {...}]
|
|
407
367
|
# parent_command = Command.map(child_command) { |child_result| [:sidebar, *child_result] }
|
|
408
|
-
def self.map(inner_command, &
|
|
409
|
-
|
|
368
|
+
def self.map(inner_command, mapper = nil, &block)
|
|
369
|
+
if mapper && block
|
|
370
|
+
raise ArgumentError, "Pass either a mapper callable or a block, not both"
|
|
371
|
+
end
|
|
372
|
+
unless mapper || block
|
|
373
|
+
raise ArgumentError, "Pass a mapper callable or a block"
|
|
374
|
+
end
|
|
375
|
+
Mapped.new(inner_command:, mapper: mapper || block)
|
|
410
376
|
end
|
|
411
377
|
|
|
412
378
|
# Gives a callable unique identity for cancellation.
|
|
@@ -529,15 +495,32 @@ module Rooibos
|
|
|
529
495
|
Http.new(*, **)
|
|
530
496
|
end
|
|
531
497
|
|
|
532
|
-
#
|
|
533
|
-
|
|
498
|
+
# Opens a file or URL with the system's default application.
|
|
499
|
+
# Cross-platform: uses +open+ on macOS, +xdg-open+ on Linux, +start+ on Windows.
|
|
500
|
+
#
|
|
501
|
+
# On success (exit 0), sends +Message::Open+.
|
|
502
|
+
# On failure (non-zero), sends +Message::Error+.
|
|
503
|
+
#
|
|
504
|
+
# === Example
|
|
505
|
+
#
|
|
506
|
+
# case message
|
|
507
|
+
# in { type: :open, envelope: path }
|
|
508
|
+
# model.with(status: "Opened #{path}")
|
|
509
|
+
# in { type: :error, envelope: path }
|
|
510
|
+
# model.with(error: "Could not open #{path}")
|
|
511
|
+
# end
|
|
512
|
+
#
|
|
513
|
+
def self.open(path, envelope = path)
|
|
514
|
+
Open.new(path:, envelope:)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
class Wrapped < Data.define(:callable, :grace_period) # :nodoc:
|
|
534
518
|
include Custom
|
|
535
519
|
def rooibos_cancellation_grace_period
|
|
536
520
|
grace_period || super
|
|
537
521
|
end
|
|
538
522
|
|
|
539
|
-
# :nodoc:
|
|
540
|
-
def call(out, token)
|
|
523
|
+
def call(out, token) # :nodoc:
|
|
541
524
|
callable.call(out, token)
|
|
542
525
|
end
|
|
543
526
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
# Completion sentinel from Command.batch.
|
|
11
|
+
#
|
|
12
|
+
# Batch commands stream child messages live. When all children finish,
|
|
13
|
+
# this message signals completion. Use it to trigger follow-up actions
|
|
14
|
+
# or UI updates after parallel work completes.
|
|
15
|
+
#
|
|
16
|
+
# === Example
|
|
17
|
+
#
|
|
18
|
+
# case msg
|
|
19
|
+
# in Message::Batch
|
|
20
|
+
# out.put(:all_done)
|
|
21
|
+
# in [:progress, pct]
|
|
22
|
+
# model.with(progress: pct)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
Batch = Data.define(:command) do
|
|
26
|
+
include Predicates
|
|
27
|
+
|
|
28
|
+
# Returns <tt>true</tt> for batch completion messages.
|
|
29
|
+
def batch?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Deconstructs for pattern matching.
|
|
34
|
+
def deconstruct_keys(_keys)
|
|
35
|
+
{ type: :batch, command: }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
# Cancellation notification from a canceled command.
|
|
11
|
+
#
|
|
12
|
+
# Long-running commands respond to cancellation cooperatively. When the
|
|
13
|
+
# runtime signals cancellation, the command finishes current work and sends
|
|
14
|
+
# this message.
|
|
15
|
+
#
|
|
16
|
+
# Pattern match on Canceled in your update function to clean up state,
|
|
17
|
+
# stop animations, or acknowledge the cancellation.
|
|
18
|
+
#
|
|
19
|
+
# Use it to handle timer cancellations, aborted HTTP requests, or
|
|
20
|
+
# stopped background processes.
|
|
21
|
+
#
|
|
22
|
+
# === Example
|
|
23
|
+
#
|
|
24
|
+
# Update = ->(message, model) {
|
|
25
|
+
# case message
|
|
26
|
+
# in { type: :canceled, command: }
|
|
27
|
+
# # Timer was canceled, clear the notification
|
|
28
|
+
# model.with(notification: nil)
|
|
29
|
+
# in Message::Canceled
|
|
30
|
+
# # Generic cancellation handling
|
|
31
|
+
# model
|
|
32
|
+
# end
|
|
33
|
+
# }
|
|
34
|
+
Canceled = Data.define(:command) do
|
|
35
|
+
include Predicates
|
|
36
|
+
|
|
37
|
+
# Returns <tt>true</tt> for cancellation messages.
|
|
38
|
+
def canceled?
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
alias_method :cancelled?, :canceled?
|
|
42
|
+
|
|
43
|
+
# Deconstructs for pattern matching.
|
|
44
|
+
#
|
|
45
|
+
# Returns a hash with <tt>type</tt> and <tt>command</tt>.
|
|
46
|
+
def deconstruct_keys(_keys)
|
|
47
|
+
{ type: :canceled, command: }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -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
|