ratatui_ruby-tea 0.3.1 → 0.4.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +42 -2
  3. data/CHANGELOG.md +76 -0
  4. data/README.md +8 -5
  5. data/doc/concepts/async_work.md +164 -0
  6. data/doc/concepts/commands.md +528 -0
  7. data/doc/concepts/message_processing.md +51 -0
  8. data/doc/contributors/WIP/decomposition_strategies_analysis.md +258 -0
  9. data/doc/contributors/WIP/implementation_plan.md +405 -0
  10. data/doc/contributors/WIP/init_callable_proposal.md +341 -0
  11. data/doc/contributors/WIP/mvu_tea_implementations_research.md +372 -0
  12. data/doc/contributors/WIP/runtime_refactoring_status.md +47 -0
  13. data/doc/contributors/WIP/task.md +36 -0
  14. data/doc/contributors/WIP/v0.4.0_todo.md +468 -0
  15. data/doc/contributors/design/commands_and_outlets.md +11 -1
  16. data/doc/contributors/priorities.md +22 -24
  17. data/examples/app_fractal_dashboard/app.rb +3 -7
  18. data/examples/app_fractal_dashboard/dashboard/base.rb +15 -16
  19. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +8 -8
  20. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +11 -11
  21. data/examples/app_fractal_dashboard/dashboard/update_router.rb +4 -4
  22. data/examples/app_fractal_dashboard/{bags → fragments}/custom_shell_input.rb +8 -4
  23. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +82 -0
  24. data/examples/app_fractal_dashboard/{bags → fragments}/custom_shell_output.rb +8 -4
  25. data/examples/app_fractal_dashboard/{bags → fragments}/disk_usage.rb +13 -10
  26. data/examples/app_fractal_dashboard/{bags → fragments}/network_panel.rb +12 -12
  27. data/examples/app_fractal_dashboard/{bags → fragments}/ping.rb +12 -8
  28. data/examples/app_fractal_dashboard/{bags → fragments}/stats_panel.rb +12 -12
  29. data/examples/app_fractal_dashboard/{bags → fragments}/system_info.rb +11 -7
  30. data/examples/app_fractal_dashboard/{bags → fragments}/uptime.rb +11 -7
  31. data/examples/verify_readme_usage/README.md +7 -4
  32. data/examples/verify_readme_usage/app.rb +7 -4
  33. data/lib/ratatui_ruby/tea/command/all.rb +71 -0
  34. data/lib/ratatui_ruby/tea/command/batch.rb +79 -0
  35. data/lib/ratatui_ruby/tea/command/custom.rb +1 -1
  36. data/lib/ratatui_ruby/tea/command/http.rb +194 -0
  37. data/lib/ratatui_ruby/tea/command/lifecycle.rb +136 -0
  38. data/lib/ratatui_ruby/tea/command/outlet.rb +59 -27
  39. data/lib/ratatui_ruby/tea/command/wait.rb +82 -0
  40. data/lib/ratatui_ruby/tea/command.rb +245 -64
  41. data/lib/ratatui_ruby/tea/message/all.rb +47 -0
  42. data/lib/ratatui_ruby/tea/message/http_response.rb +63 -0
  43. data/lib/ratatui_ruby/tea/message/system/batch.rb +63 -0
  44. data/lib/ratatui_ruby/tea/message/system/stream.rb +69 -0
  45. data/lib/ratatui_ruby/tea/message/timer.rb +48 -0
  46. data/lib/ratatui_ruby/tea/message.rb +40 -0
  47. data/lib/ratatui_ruby/tea/router.rb +11 -11
  48. data/lib/ratatui_ruby/tea/runtime.rb +320 -185
  49. data/lib/ratatui_ruby/tea/shortcuts.rb +2 -2
  50. data/lib/ratatui_ruby/tea/test_helper.rb +58 -0
  51. data/lib/ratatui_ruby/tea/version.rb +1 -1
  52. data/lib/ratatui_ruby/tea.rb +44 -10
  53. data/rbs_collection.lock.yaml +1 -17
  54. data/sig/concurrent.rbs +72 -0
  55. data/sig/ratatui_ruby/tea/command.rbs +141 -37
  56. data/sig/ratatui_ruby/tea/message.rbs +123 -0
  57. data/sig/ratatui_ruby/tea/router.rbs +1 -1
  58. data/sig/ratatui_ruby/tea/runtime.rbs +39 -6
  59. data/sig/ratatui_ruby/tea/test_helper.rbs +12 -0
  60. data/sig/ratatui_ruby/tea.rbs +24 -4
  61. metadata +63 -11
  62. data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +0 -73
  63. data/lib/ratatui_ruby/tea/command/cancellation_token.rb +0 -135
@@ -6,6 +6,7 @@
6
6
  #++
7
7
 
8
8
  require_relative "tea/version"
9
+ require_relative "tea/message"
9
10
  require_relative "tea/command"
10
11
  require_relative "tea/runtime"
11
12
  require_relative "tea/router"
@@ -24,35 +25,68 @@ module RatatuiRuby # :nodoc: Documented in the ratatui_ruby gem.
24
25
  # Starts the MVU event loop.
25
26
  #
26
27
  # Convenience delegator to Runtime.run. See Runtime for full documentation.
27
- def self.run(...)
28
- Runtime.run(...)
28
+ def self.run(root_fragment = nil, **)
29
+ Runtime.run(root_fragment, **)
30
+ end
31
+
32
+ # Normalizes Init callable return value to <tt>[model, command]</tt> tuple.
33
+ #
34
+ # Init callables use DWIM syntax. They can return just a model, just a command,
35
+ # or a full <tt>[model, command]</tt> tuple.
36
+ #
37
+ # This method handles all formats. Use it when composing child fragment Inits
38
+ # in fractal architecture.
39
+ #
40
+ # [result] The Init return value.
41
+ #
42
+ # === Examples
43
+ #
44
+ #--
45
+ # SPDX-SnippetBegin
46
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
47
+ # SPDX-License-Identifier: MIT-0
48
+ #++
49
+ # # Parent fragment composes children
50
+ # Init = ->(theme:) do
51
+ # stats_model, stats_cmd = Tea.normalize_init(StatsPanel::Init.(theme: theme))
52
+ # network_model, network_cmd = Tea.normalize_init(NetworkPanel::Init.(theme: theme))
53
+ #
54
+ # model = Model.new(stats: stats_model, network: network_model)
55
+ # command = Command.batch(stats_cmd, network_cmd)
56
+ # [model, command]
57
+ # end
58
+ #--
59
+ # SPDX-SnippetEnd
60
+ #++
61
+ def self.normalize_init(result)
62
+ Runtime.normalize_init(result)
29
63
  end
30
64
 
31
65
  # Wraps a command with a routing prefix.
32
66
  #
33
- # Parent bags trigger child bag commands. The results need routing back
34
- # to the correct child bag. Manually wrapping every command is tedious.
67
+ # Parent fragments trigger child fragment commands. The results need routing back
68
+ # to the correct child fragment. Manually wrapping every command is tedious.
35
69
  #
36
70
  # This method prefixes command results automatically. Use it to route
37
- # child bag command results in Fractal Architecture.
71
+ # child fragment command results in Fractal Architecture.
38
72
  #
39
- # [command] The child bag command to wrap.
73
+ # [command] The child fragment command to wrap.
40
74
  # [prefix] Symbol prepended to results (e.g., <tt>:stats</tt>).
41
75
  #
42
76
  # === Example
43
77
  #
44
78
  # # Verbose:
45
- # Command.map(child_bag.fetch_command) { |r| [:stats, *r] }
79
+ # Command.map(child_fragment.fetch_command) { |r| [:stats, *r] }
46
80
  #
47
81
  # # Concise:
48
- # Tea.route(child_bag.fetch_command, :stats)
82
+ # Tea.route(child_fragment.fetch_command, :stats)
49
83
  def self.route(command, prefix)
50
84
  Command.map(command) { |result| [prefix, *result] }
51
85
  end
52
86
 
53
- # Delegates a prefixed message to a child bag's UPDATE.
87
+ # Delegates a prefixed message to a child fragment's UPDATE.
54
88
  #
55
- # Parent bag UPDATE functions route messages to child bags. Each route
89
+ # Parent fragment UPDATE functions route messages to child fragments. Each route
56
90
  # requires pattern matching, calling the child, and rewrapping any returned
57
91
  # command. The boilerplate adds up fast.
58
92
  #
@@ -17,14 +17,6 @@ gems:
17
17
  version: '0'
18
18
  source:
19
19
  type: stdlib
20
- - name: dbm
21
- version: '0'
22
- source:
23
- type: stdlib
24
- - name: erb
25
- version: '0'
26
- source:
27
- type: stdlib
28
20
  - name: ffi
29
21
  version: 1.17.3
30
22
  source:
@@ -77,16 +69,8 @@ gems:
77
69
  version: 1.7.0
78
70
  source:
79
71
  type: rubygems
80
- - name: pstore
81
- version: '0'
82
- source:
83
- type: stdlib
84
- - name: psych
85
- version: '0'
86
- source:
87
- type: stdlib
88
72
  - name: ratatui_ruby
89
- version: 0.10.1
73
+ version: 0.10.2
90
74
  source:
91
75
  type: rubygems
92
76
  - name: ratatui_ruby-devtools
@@ -0,0 +1,72 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ # Minimal stubs for concurrent-ruby-edge types used by Tea.
5
+ # This is not a complete type definition — just enough for Steep.
6
+
7
+ module Concurrent
8
+ module Promises
9
+ # Factory for resolvable events.
10
+ def self.resolvable_event: () -> ResolvableEvent
11
+
12
+ # A future/event that can be resolved by user code.
13
+ class ResolvableEvent
14
+ def resolve: (?bool wait, ?bool raise_on_reassign) -> self
15
+ def resolved?: () -> bool
16
+ def wait: (?Numeric? timeout) -> self
17
+ end
18
+
19
+ # Thread-safe FIFO channel for message passing.
20
+ class Channel
21
+ def initialize: (?Integer capacity) -> void
22
+ def push: (untyped message, ?Numeric? timeout) -> self
23
+ def pop: (?Numeric? timeout, ?untyped timeout_value) -> untyped
24
+ def try_pop: (?untyped no_value) -> untyped
25
+ end
26
+
27
+ # Represents a value that may not yet be available.
28
+ class Future[T]
29
+ def pending?: () -> bool
30
+ def resolved?: () -> bool
31
+ def rejected?: () -> bool
32
+ def wait: (?Numeric? timeout) -> self
33
+ def value: () -> T?
34
+ def reason: () -> Exception?
35
+ end
36
+
37
+ # Factory method for creating futures.
38
+ def self.future: [T] () { () -> T } -> Future[T]
39
+
40
+ # Races multiple futures/events, returning when any resolves.
41
+ def self.any_event: (*Future[untyped] | ResolvableEvent) -> ResolvableEvent
42
+ end
43
+
44
+ # Single-element blocking container for synchronization.
45
+ class MVar
46
+ TIMEOUT: Symbol
47
+
48
+ def initialize: (?untyped initial_value) -> void
49
+ def put: (untyped value) -> void
50
+ def take: (?Numeric? timeout) -> untyped
51
+ end
52
+
53
+ # Cooperative cancellation abstraction.
54
+ class Cancellation
55
+ def self.new: (?Promises::ResolvableEvent origin) -> [Cancellation, Promises::ResolvableEvent]
56
+ def self.timeout: (Numeric seconds) -> Cancellation
57
+
58
+ def origin: () -> Promises::ResolvableEvent
59
+ def canceled?: () -> bool
60
+ def check!: (?Class error) -> self
61
+ def join: (*Cancellation cancellations) -> Cancellation
62
+ end
63
+
64
+ # Thread-safe hash implementation.
65
+ class Map[K, V]
66
+ def initialize: () -> void
67
+ def []: (K key) -> V?
68
+ def []=: (K key, V value) -> V
69
+ def delete: (K key) -> V?
70
+ def each: () { (K, V) -> void } -> self
71
+ end
72
+ end
@@ -14,7 +14,7 @@ module RatatuiRuby
14
14
 
15
15
  # Execute the command's side effect.
16
16
  # Push result messages via the outlet.
17
- def call: (Outlet out, CancellationToken token) -> void
17
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
18
18
 
19
19
  # Grace period for cooperative cancellation (seconds).
20
20
  # Runtime waits this long before force-killing the thread.
@@ -42,7 +42,7 @@ module RatatuiRuby
42
42
  def stream?: () -> bool
43
43
 
44
44
  # Executes the shell command and sends results via outlet.
45
- def call: (Outlet out, CancellationToken token) -> void
45
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
46
46
 
47
47
  def self.new: (command: String, tag: (Symbol | Class), stream: bool) -> instance
48
48
  end
@@ -59,7 +59,7 @@ module RatatuiRuby
59
59
  def tea_cancellation_grace_period: () -> Float
60
60
 
61
61
  # Executes the inner command and transforms the result.
62
- def call: (Outlet out, CancellationToken token) -> void
62
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
63
63
 
64
64
  def self.new: (inner_command: execution, mapper: ^(Array[untyped]) -> Array[untyped]) -> instance
65
65
  end
@@ -85,6 +85,9 @@ module RatatuiRuby
85
85
  # Creates a quit command.
86
86
  def self.exit: () -> Exit
87
87
 
88
+ # Creates a fresh cancellation that never fires.
89
+ def self.uncancellable: () -> Concurrent::Cancellation
90
+
88
91
  # Creates a shell execution command.
89
92
  def self.system: (String command, (Symbol | Class) tag, ?stream: bool) -> System
90
93
 
@@ -98,45 +101,37 @@ module RatatuiRuby
98
101
  def self.error: (_Command command, Exception exception) -> Error
99
102
 
100
103
  # Wraps a callable for unique identity per dispatch.
101
- def self.custom: (^(Outlet, CancellationToken) -> void callable, ?grace_period: Float?) -> _Command
102
- | (?grace_period: Float?) { (Outlet, CancellationToken) -> void } -> _Command
103
-
104
- # Private wrapper for Command.custom.
105
- class Wrapped < Data
106
- include Custom
107
-
108
- attr_reader callable: ^(Outlet, CancellationToken) -> void
109
- attr_reader grace_period: Float?
104
+ def self.custom: (^(Outlet, Concurrent::Cancellation) -> void callable, ?grace_period: Float?) -> _Command
105
+ | (?grace_period: Float?) { (Outlet, Concurrent::Cancellation) -> void } -> _Command
110
106
 
111
- def self.new: (callable: ^(Outlet, CancellationToken) -> void, grace_period: Float?) -> instance
112
- def call: (Outlet out, CancellationToken token) -> void
113
- end
107
+ # Creates a one-shot timer command.
108
+ def self.wait: (Float seconds, Symbol tag) -> Wait
114
109
 
115
- # Cooperative cancellation mechanism for long-running commands.
116
- class CancellationToken
117
- # Number of times cancel! has been called.
118
- attr_reader cancel_count: Integer
110
+ # Alias for wait, semantically used for recurring timers.
111
+ def self.tick: (Float seconds, Symbol tag) -> Wait
119
112
 
120
- def self.new: () -> instance
113
+ # One-shot timer command that waits, then sends a message.
114
+ class Wait < Data
115
+ include Custom
121
116
 
122
- # Signals cancellation. Thread-safe.
123
- def cancel!: () -> void
117
+ attr_reader seconds: Float
118
+ attr_reader tag: Symbol
124
119
 
125
- # Checks if cancellation was requested. Thread-safe.
126
- def cancelled?: () -> bool
120
+ def self.new: (seconds: Float, tag: Symbol) -> instance
127
121
 
128
- # Null object for commands that ignore cancellation.
129
- class NoneToken < Data
130
- attr_reader cancelled?: bool
122
+ # Execute the timer with cooperative cancellation.
123
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
124
+ end
131
125
 
132
- def self.new: (cancelled?: bool) -> instance
126
+ # Private wrapper for Command.custom.
127
+ class Wrapped < Data
128
+ include Custom
133
129
 
134
- # Does nothing. Ignores cancellation requests.
135
- def cancel!: () -> nil
136
- end
130
+ attr_reader callable: ^(Outlet, Concurrent::Cancellation) -> void
131
+ attr_reader grace_period: Float?
137
132
 
138
- # Singleton null token.
139
- NONE: NoneToken
133
+ def self.new: (callable: ^(Outlet, Concurrent::Cancellation) -> void, grace_period: Float?) -> instance
134
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
140
135
  end
141
136
 
142
137
  # Mixin for user-defined custom commands.
@@ -149,15 +144,124 @@ module RatatuiRuby
149
144
  def tea_cancellation_grace_period: () -> Float
150
145
  end
151
146
 
147
+ # Minimal interface for callables accepted by Lifecycle.run_sync.
148
+ interface _Callable
149
+ def call: (Outlet out, Concurrent::Cancellation token) -> void
150
+ def respond_to?: (Symbol, ?bool) -> bool
151
+ def tea_cancellation_grace_period: () -> Float
152
+ end
153
+
154
+ # Manages command execution with lifecycle support.
155
+ class Lifecycle
156
+ # Internal representation of a tracked async command.
157
+ class Entry < Data
158
+ attr_reader future: Concurrent::Promises::Future[void]
159
+ attr_reader origin: Concurrent::Promises::ResolvableEvent
160
+
161
+ def self.new: (future: Concurrent::Promises::Future[void], origin: Concurrent::Promises::ResolvableEvent) -> instance
162
+ end
163
+
164
+ @active: Concurrent::Map[_Callable, Entry]
165
+
166
+ def initialize: () -> void
167
+
168
+ # Runs a command synchronously, returning its result.
169
+ def run_sync: (_Callable command, Concurrent::Cancellation token, timeout: Float) -> Object?
170
+
171
+ # Runs a command asynchronously, tracking it for later cancellation.
172
+ def run_async: (_Callable command, Concurrent::Promises::Channel channel) -> Entry
173
+
174
+ # Cancels a running command, waiting for its grace period.
175
+ def cancel: (_Callable command) -> void
176
+
177
+ # Cancels all active commands and waits for them to complete.
178
+ def shutdown: () -> void
179
+ end
180
+
152
181
  # Messaging gateway for custom commands.
153
182
  class Outlet
154
- @queue: Thread::Queue[Array[untyped]]
183
+ @channel: Concurrent::Promises::Channel
184
+ @live: Lifecycle
155
185
 
156
- def initialize: (Thread::Queue[Array[untyped]] queue) -> void
186
+ def initialize: (Concurrent::Promises::Channel channel, lifecycle: Lifecycle) -> void
157
187
 
158
- # Sends a tagged message to the runtime.
159
- def put: (Symbol tag, *untyped payload) -> void
188
+ # Internal infrastructure for nested command lifecycle sharing.
189
+ attr_reader live: Lifecycle
190
+
191
+ # Sends a message to the runtime.
192
+ def put: (*Object args) -> void
193
+
194
+ # Runs a child command synchronously, returning its result.
195
+ def source: (_Callable command, Concurrent::Cancellation token, ?timeout: Float) -> Object?
196
+ end
197
+
198
+ # A fire-and-forget parallel command.
199
+ class Batch
200
+ include Custom
201
+
202
+ attr_reader commands: Array[Custom]
203
+
204
+ def self.new: (*Custom | Array[Custom]) -> instance
205
+
206
+ def call: (Outlet outlet, Concurrent::Cancellation token) -> void
207
+ end
208
+
209
+ # Creates a parallel batch command.
210
+ def self.batch: (*Custom | Array[Custom]) -> Batch
211
+
212
+ # An aggregating parallel command.
213
+ class All
214
+ include Custom
215
+
216
+ attr_reader tag: Symbol
217
+ attr_reader commands: Array[Custom]
218
+ attr_reader nested: bool
219
+
220
+ def self.new: (Symbol tag, *Custom | Array[Custom]) -> instance
221
+
222
+ def call: (Outlet outlet, Concurrent::Cancellation token) -> void
223
+ end
224
+
225
+ # Creates an aggregating parallel command.
226
+ def self.all: (Symbol tag, *Custom | Array[Custom]) -> All
227
+
228
+ # HTTP response shapes for pattern matching
229
+ type http_success_shape = { type: :http, envelope: Symbol, status: Integer, body: String, headers: Hash[String, String] }
230
+ type http_error_shape = { type: :http, envelope: Symbol, error: String }
231
+
232
+ # HTTP response with deconstruct_keys for pattern matching.
233
+ class HttpResponse < Data
234
+ attr_reader envelope: Symbol
235
+ attr_reader status: Integer?
236
+ attr_reader body: String?
237
+ attr_reader headers: Hash[String, String]?
238
+ attr_reader error: String?
239
+
240
+ def self.new: (envelope: Symbol, status: Integer?, body: String?, headers: Hash[String, String]?, error: String?) -> instance
241
+
242
+ def deconstruct_keys: (Array[Symbol]?) -> (http_success_shape | http_error_shape)
160
243
  end
244
+
245
+ # An HTTP request command.
246
+ class Http < Data
247
+ include Custom
248
+
249
+ attr_reader method: Symbol
250
+ attr_reader url: String
251
+ attr_reader envelope: Symbol
252
+ attr_reader headers: Hash[String, String]?
253
+ attr_reader body: String?
254
+ attr_reader timeout: (Integer | Float)?
255
+
256
+ def self.new: (method: Symbol, url: String, envelope: Symbol, headers: Hash[String, String]?, body: String?, timeout: (Integer | Float)?) -> instance
257
+
258
+ def call: (Outlet outlet, Concurrent::Cancellation token) -> void
259
+ end
260
+
261
+ # Creates an HTTP request command.
262
+ # DWIM: accepts positional args, method keywords (get:, post:, etc.), or explicit keywords.
263
+ # See Http class and documentation for all supported patterns.
264
+ def self.http: (*(Symbol | String | nil), **(Symbol | String | Integer | Float | Hash[String, String] | ^(String, Hash[String, String]?, Integer?) -> Object | nil)) -> Http
161
265
  end
162
266
  end
163
267
  end
@@ -0,0 +1,123 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module RatatuiRuby
7
+ module Tea
8
+ # Messages sent from commands to update functions.
9
+ module Message
10
+ # Fallback predicate mixin.
11
+ module Predicates
12
+ # Returns false for unknown predicate methods.
13
+ def method_missing: (Symbol name, *untyped args, **untyped kwargs) ?{ () -> untyped } -> bool
14
+
15
+ # Responds to all predicate methods.
16
+ def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
17
+ end
18
+
19
+ # System command response types.
20
+ module System
21
+ # Response from a system command (batch mode).
22
+ class Batch
23
+ include Predicates
24
+
25
+ attr_reader envelope: Symbol
26
+ attr_reader stdout: String
27
+ attr_reader stderr: String
28
+ attr_reader status: Integer
29
+
30
+ def initialize: (
31
+ envelope: Symbol,
32
+ stdout: String,
33
+ stderr: String,
34
+ status: Integer
35
+ ) -> void
36
+
37
+ def system?: () -> bool
38
+ def success?: () -> bool
39
+ def error?: () -> bool
40
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
41
+ end
42
+
43
+ # Streaming message from a system command.
44
+ class Stream
45
+ include Predicates
46
+
47
+ attr_reader envelope: Symbol
48
+ attr_reader stream: Symbol
49
+ attr_reader content: String?
50
+ attr_reader status: Integer?
51
+
52
+ def initialize: (
53
+ envelope: Symbol,
54
+ stream: Symbol,
55
+ content: String?,
56
+ status: Integer?
57
+ ) -> void
58
+
59
+ def system?: () -> bool
60
+ def stdout?: () -> bool
61
+ def stderr?: () -> bool
62
+ def complete?: () -> bool
63
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
64
+ end
65
+ end
66
+
67
+ # Response from a timer command.
68
+ class Timer
69
+ include Predicates
70
+
71
+ attr_reader envelope: Symbol
72
+ attr_reader elapsed: Float
73
+
74
+ def initialize: (envelope: Symbol, elapsed: Float) -> void
75
+
76
+ def timer?: () -> bool
77
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
78
+ end
79
+
80
+ # Response from an HTTP command.
81
+ class HttpResponse
82
+ include Predicates
83
+
84
+ attr_reader envelope: Symbol
85
+ attr_reader status: Integer?
86
+ attr_reader body: String?
87
+ attr_reader headers: Hash[String, String]?
88
+ attr_reader error: String?
89
+
90
+ def initialize: (
91
+ envelope: Symbol,
92
+ status: Integer?,
93
+ body: String?,
94
+ headers: Hash[String, String]?,
95
+ error: String?
96
+ ) -> void
97
+
98
+ def http?: () -> bool
99
+ def success?: () -> bool
100
+ def error?: () -> bool
101
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
102
+ end
103
+
104
+ # Response from Command.all aggregating parallel execution.
105
+ class All
106
+ include Predicates
107
+
108
+ attr_reader envelope: Symbol
109
+ attr_reader results: Array[untyped]
110
+ attr_reader nested: bool
111
+
112
+ def initialize: (
113
+ envelope: Symbol,
114
+ results: Array[untyped],
115
+ nested: bool
116
+ ) -> void
117
+
118
+ def all?: () -> bool
119
+ def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
120
+ end
121
+ end
122
+ end
123
+ end
@@ -15,7 +15,7 @@ module RatatuiRuby
15
15
  def with: (**Object) -> self
16
16
  end
17
17
 
18
- # Interface for fractal child modules (bags).
18
+ # Interface for fractal child modules (fragments).
19
19
  # Routed modules must have an UPDATE constant that handles child messages.
20
20
  # NOTE: We use Module here because RBS interfaces can't declare class methods.
21
21
 
@@ -8,7 +8,7 @@ module RatatuiRuby
8
8
  # MVU runtime event loop.
9
9
  class Runtime
10
10
  # Active command tracking entry.
11
- type active_entry = { thread: Thread, token: Command::CancellationToken }
11
+ type active_entry = { future: Concurrent::Promises::Future[void], origin: Concurrent::Promises::ResolvableEvent }
12
12
 
13
13
  # Widget type accepted by view functions.
14
14
  type renderable = RatatuiRuby::_CustomWidget | RatatuiRuby::widget
@@ -17,6 +17,9 @@ module RatatuiRuby
17
17
  # Steep needs a union type (not interface) for is_a? narrowing.
18
18
  type update_result = [Object, Command::execution?] | Command::execution | Object
19
19
 
20
+ # Duck type for init result that can be normalized.
21
+ type init_result = [Object, Command::execution?] | Command::execution | Object
22
+
20
23
  # Duck type for values that can be queried for command-ness.
21
24
  interface _MaybeCommand
22
25
  def nil?: () -> bool
@@ -25,20 +28,50 @@ module RatatuiRuby
25
28
  def tea_command?: () -> bool
26
29
  end
27
30
 
28
- # Starts the MVU event loop.
31
+ # Starts the MVU event loop (positional fragment).
29
32
  def self.run: [Model] (
33
+ ?Module? root_fragment,
34
+ ?fps: Integer
35
+ ) -> Model
36
+
37
+ # Starts the MVU event loop (explicit parameters).
38
+ | [Model] (
39
+ ?Module? root_fragment,
40
+ ?fps: Integer,
30
41
  model: Model,
31
42
  view: ^(Model, RatatuiRuby::TUI) -> renderable,
32
43
  update: ^(RatatuiRuby::Event, Model) -> update_result?,
33
- ?init: (^() -> RatatuiRuby::Event)?
44
+ ?command: Command::execution?
34
45
  ) -> Model
35
46
 
47
+ # Starts the MVU event loop (explicit parameters without fps).
48
+ | [Model] (
49
+ ?Module? root_fragment,
50
+ model: Model,
51
+ view: ^(Model, RatatuiRuby::TUI) -> renderable,
52
+ update: ^(RatatuiRuby::Event, Model) -> update_result?,
53
+ ?command: Command::execution?
54
+ ) -> Model
55
+
56
+ # Normalizes Init callable return value to [model, command] tuple.
57
+ def self.normalize_init: [Model] (init_result result) -> [Model?, Command::execution?]
58
+ QUIT: Object
59
+
36
60
  private
37
61
 
38
- def self.validate_view_result!: (renderable? widget) -> void
39
- def self.normalize_update_result: [Model] (update_result? result, Model previous_model) -> [Model, Command::execution?]
62
+ def self.validate_view_return!: (renderable? widget) -> void
63
+ def self.normalize_update_return: [Model] (update_result? result, Model? previous_model) -> [Model?, Command::execution?]
40
64
  def self.validate_ractor_shareable!: [T] (T object, String name) -> void
41
- def self.dispatch: (Command::execution command, Queue[Array[Symbol | Hash[Symbol, String | Integer]]], ?Hash[Command::_Command, active_entry] active_commands) -> Thread?
65
+ def self.fragment_from_kwargs: (Module? root_fragment, ?model: untyped, ?view: untyped, ?update: untyped, ?command: untyped) -> Module
66
+ def self.fragment_invariant!: (String param) -> void
67
+ def self.init_callable: [Model] () -> ^() -> [Model?, Command::execution?]
68
+ def self.start_runtime: () -> untyped
69
+ def self.draw_view: () -> void
70
+ def self.handle_ratatui_event: () -> void
71
+ def self.handle_sync: () -> void
72
+ QUEUE_EMPTY: Object
73
+ def self.send_pending_messages: (?dispatch: bool) -> void
74
+ def self.dispatch_command: () -> void
42
75
  end
43
76
  end
44
77
  end
@@ -0,0 +1,12 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module RatatuiRuby
7
+ module Tea
8
+ module TestHelper
9
+ def validate_tea_command!: (Runtime::_MaybeCommand) -> nil
10
+ end
11
+ end
12
+ end