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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +9 -5
  3. data/.builds/ruby-3.3.yml +9 -5
  4. data/.builds/ruby-3.4.yml +9 -5
  5. data/.builds/ruby-4.0.0.yml +9 -5
  6. data/AGENTS.md +1 -1
  7. data/CHANGELOG.md +46 -0
  8. data/README.md +2 -2
  9. data/README.rdoc +374 -0
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +1 -1
  12. data/doc/best_practices/forms_and_validation.md +20 -0
  13. data/doc/best_practices/http_workflows.md +20 -0
  14. data/doc/best_practices/index.md +26 -0
  15. data/doc/best_practices/lists_and_tables.md +20 -0
  16. data/doc/best_practices/modal_dialogs.md +20 -0
  17. data/doc/best_practices/no_stateful_widgets.md +184 -0
  18. data/doc/best_practices/orchestration.md +20 -0
  19. data/doc/best_practices/streaming_data.md +20 -0
  20. data/doc/contributors/design/commands_and_outlets.md +1 -1
  21. data/doc/contributors/documentation_plan.md +616 -0
  22. data/doc/contributors/documentation_stub_audit.md +112 -0
  23. data/doc/contributors/documentation_style.md +275 -0
  24. data/doc/contributors/e2e_pty.md +168 -0
  25. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
  26. data/doc/contributors/specs/file_browser.md +789 -0
  27. data/doc/contributors/specs/file_browser_stories.md +774 -0
  28. data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
  29. data/doc/contributors/todo/scrollbar.md +118 -0
  30. data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
  31. data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
  32. data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
  33. data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
  34. data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
  35. data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
  36. data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
  37. data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
  38. data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
  39. data/doc/contributors/tutorial_old/12_going_further.md +20 -0
  40. data/doc/contributors/tutorial_old/index.md +20 -0
  41. data/doc/essentials/commands.md +20 -0
  42. data/doc/essentials/index.md +31 -0
  43. data/doc/essentials/messages.md +21 -0
  44. data/doc/essentials/models.md +21 -0
  45. data/doc/essentials/shortcuts.md +19 -0
  46. data/doc/essentials/the_elm_architecture.md +24 -0
  47. data/doc/essentials/the_runtime.md +21 -0
  48. data/doc/essentials/update_functions.md +20 -0
  49. data/doc/essentials/views.md +22 -0
  50. data/doc/getting_started/for_go_developers.md +16 -0
  51. data/doc/getting_started/for_python_developers.md +16 -0
  52. data/doc/getting_started/for_react_developers.md +17 -0
  53. data/doc/getting_started/index.md +52 -0
  54. data/doc/getting_started/install.md +20 -0
  55. data/doc/getting_started/quickstart.md +9 -45
  56. data/doc/getting_started/ruby_primer.md +19 -0
  57. data/doc/getting_started/why_rooibos.md +20 -0
  58. data/doc/index.md +79 -11
  59. data/doc/scaling_up/async_patterns.md +20 -0
  60. data/doc/scaling_up/command_composition.md +20 -0
  61. data/doc/scaling_up/custom_commands.md +21 -0
  62. data/doc/scaling_up/fractal_architecture.md +20 -0
  63. data/doc/scaling_up/index.md +30 -0
  64. data/doc/scaling_up/message_routing.md +20 -0
  65. data/doc/scaling_up/ractor_safety.md +20 -0
  66. data/doc/scaling_up/testing.md +21 -0
  67. data/doc/troubleshooting/common_errors.md +20 -0
  68. data/doc/troubleshooting/debugging.md +21 -0
  69. data/doc/troubleshooting/index.md +23 -0
  70. data/doc/troubleshooting/performance.md +20 -0
  71. data/doc/tutorial/01_project_setup.md +44 -0
  72. data/doc/tutorial/02_hello_world.md +45 -0
  73. data/doc/tutorial/03_static_file_list.md +44 -0
  74. data/doc/tutorial/04_arrow_navigation.md +47 -0
  75. data/doc/tutorial/05_real_files.md +45 -0
  76. data/doc/tutorial/06_safe_refactoring.md +21 -0
  77. data/doc/tutorial/07_red_first_tdd.md +26 -0
  78. data/doc/tutorial/08_file_metadata.md +42 -0
  79. data/doc/tutorial/09_text_preview.md +44 -0
  80. data/doc/tutorial/10_directory_tree.md +42 -0
  81. data/doc/tutorial/11_pane_focus.md +40 -0
  82. data/doc/tutorial/12_sorting.md +41 -0
  83. data/doc/tutorial/13_filtering.md +43 -0
  84. data/doc/tutorial/14_toggle_hidden.md +41 -0
  85. data/doc/tutorial/15_text_input_widget.md +43 -0
  86. data/doc/tutorial/16_rename_files.md +42 -0
  87. data/doc/tutorial/17_confirmation_dialogs.md +43 -0
  88. data/doc/tutorial/18_progress_indicators.md +43 -0
  89. data/doc/tutorial/19_atomic_operations.md +42 -0
  90. data/doc/tutorial/20_external_editor.md +42 -0
  91. data/doc/tutorial/21_modal_overlays.md +41 -0
  92. data/doc/tutorial/22_error_handling.md +43 -0
  93. data/doc/tutorial/23_terminal_capabilities.md +53 -0
  94. data/doc/tutorial/24_mouse_events.md +43 -0
  95. data/doc/tutorial/25_resize_events.md +43 -0
  96. data/doc/tutorial/26_loading_states.md +42 -0
  97. data/doc/tutorial/27_performance.md +43 -0
  98. data/doc/tutorial/28_color_schemes.md +47 -0
  99. data/doc/tutorial/29_configuration.md +124 -0
  100. data/doc/tutorial/30_going_further.md +17 -0
  101. data/doc/tutorial/index.md +17 -0
  102. data/examples/app_file_browser/app.rb +40 -0
  103. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
  104. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
  105. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
  106. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
  107. data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
  108. data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
  109. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
  110. data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
  111. data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
  112. data/examples/verify_website_first_app/app.rb +85 -0
  113. data/examples/verify_website_hello_mvu/app.rb +31 -0
  114. data/examples/widget_command_system/app.rb +15 -13
  115. data/exe/rooibos +10 -0
  116. data/generate_tutorial_stubs.rb +126 -0
  117. data/lib/rooibos/cli/commands/new.rb +373 -0
  118. data/lib/rooibos/cli/commands/run.rb +98 -0
  119. data/lib/rooibos/cli.rb +78 -0
  120. data/lib/rooibos/command/all.rb +25 -20
  121. data/lib/rooibos/command/batch.rb +26 -25
  122. data/lib/rooibos/command/custom.rb +84 -1
  123. data/lib/rooibos/command/http.rb +59 -55
  124. data/lib/rooibos/command/lifecycle.rb +5 -5
  125. data/lib/rooibos/command/open.rb +86 -0
  126. data/lib/rooibos/command/outlet.rb +105 -3
  127. data/lib/rooibos/command/wait.rb +5 -5
  128. data/lib/rooibos/command.rb +57 -74
  129. data/lib/rooibos/message/batch.rb +39 -0
  130. data/lib/rooibos/message/canceled.rb +51 -0
  131. data/lib/rooibos/message/error.rb +48 -0
  132. data/lib/rooibos/message/open.rb +30 -0
  133. data/lib/rooibos/message.rb +84 -4
  134. data/lib/rooibos/router.rb +11 -14
  135. data/lib/rooibos/runtime.rb +40 -43
  136. data/lib/rooibos/shortcuts.rb +47 -0
  137. data/lib/rooibos/test_helper.rb +71 -6
  138. data/lib/rooibos/version.rb +1 -1
  139. data/lib/rooibos/welcome.rb +237 -0
  140. data/lib/rooibos.rb +4 -3
  141. data/mise.toml +1 -1
  142. data/rbs_collection.lock.yaml +2 -2
  143. data/sig/concurrent.rbs +3 -0
  144. data/sig/gem.rbs +20 -0
  145. data/sig/rooibos/cli.rbs +42 -0
  146. data/sig/rooibos/command.rbs +48 -0
  147. data/sig/rooibos/message.rbs +60 -0
  148. data/sig/rooibos/shortcuts.rbs +14 -0
  149. data/sig/rooibos/test_helper.rbs +6 -2
  150. data/sig/rooibos/welcome.rbs +75 -0
  151. data/tasks/install.rake +29 -0
  152. data/tasks/resources/build.yml.erb +2 -0
  153. metadata +272 -38
  154. data/doc/concepts/application_architecture.md +0 -197
  155. data/doc/concepts/application_testing.md +0 -49
  156. data/doc/concepts/async_work.md +0 -164
  157. data/doc/concepts/commands.md +0 -530
  158. data/doc/concepts/message_processing.md +0 -51
  159. data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
  160. data/doc/contributors/WIP/implementation_plan.md +0 -409
  161. data/doc/contributors/WIP/init_callable_proposal.md +0 -344
  162. data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
  163. data/doc/contributors/WIP/task.md +0 -36
  164. data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
  165. data/doc/contributors/kit-no-outlet.md +0 -238
  166. data/doc/contributors/priorities.md +0 -38
  167. data/doc/images/.gitkeep +0 -0
  168. data/exe/.gitkeep +0 -0
  169. /data/doc/contributors/{WIP → design}/mvu_tea_implementations_research.md +0 -0
@@ -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 cancelled mid-execution. Ruby's <tt>Net::HTTP</tt>
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 cancelled.
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 [:got_files, {stdout:, status: 0}]
276
+ # in { type: :system, envelope: :got_files, stdout:, status: 0 }
324
277
  # [model.with(files: stdout.lines), nil]
325
- # in [:got_files, {stderr:, status:}]
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 [:log, :stdout, line]
291
+ # in { type: :system, envelope: :log, stream: :stdout, content: line }
339
292
  # [model.with(lines: [*model.lines, line]), nil]
340
- # in [:log, :stderr, line]
293
+ # in { type: :system, envelope: :log, stream: :stderr, content: line }
341
294
  # [model.with(errors: [*model.errors, line]), nil]
342
- # in [:log, :complete, {status:}]
295
+ # in { type: :system, envelope: :log, stream: :complete, status: }
343
296
  # [model.with(loading: false, exit_status: status), nil]
344
- # in [:log, :error, {message:}]
345
- # [model.with(loading: false, error: message), nil]
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, waits for result, and transforms it.
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
- # Dispatch inner command
379
- if inner_command.respond_to?(:call)
380
- inner_command.call(inner_outlet, token)
381
- else
382
- raise ArgumentError, "Inner command must respond to #call"
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
- # Transform result and send
386
- inner_message = inner_channel.pop
387
- transformed = mapper.call(inner_message)
388
- out.put(*transformed)
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, &mapper)
409
- Mapped.new(inner_command:, mapper:)
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
- # :nodoc:
533
- class Wrapped < Data.define(:callable, :grace_period)
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
@@ -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 +Predicates+
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
- # Returns +false+ for any unknown predicate method (ending in +?+).
17
- # Include in custom message types for safe predicate calls.
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
- # Returns +false+ for unknown predicate methods.
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"
@@ -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 UPDATE lambda that handles routing automatically.
16
+ # generate an Update lambda that handles routing automatically.
17
17
  #
18
- # A *fragment* is a module containing <tt>Model</tt>, <tt>INITIAL</tt>,
19
- # <tt>UPDATE</tt>, and <tt>VIEW</tt> constants. Fragments compose: parent fragments
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
- # INITIAL = Model.new(stats: StatsPanel::INITIAL, network: NetworkPanel::INITIAL)
37
- # VIEW = ->(model, tui) { ... }
38
- # UPDATE = from_router
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