ratatui_ruby-tea 0.2.0 → 0.3.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +17 -5
  3. data/CHANGELOG.md +74 -0
  4. data/README.md +18 -1
  5. data/Rakefile +1 -1
  6. data/Steepfile +3 -3
  7. data/doc/concepts/application_architecture.md +182 -3
  8. data/doc/contributors/design/commands_and_outlets.md +204 -0
  9. data/doc/contributors/kit-no-outlet.md +237 -0
  10. data/examples/app_fractal_dashboard/README.md +60 -0
  11. data/examples/app_fractal_dashboard/app.rb +67 -0
  12. data/examples/app_fractal_dashboard/bags/custom_shell_input.rb +77 -0
  13. data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +73 -0
  14. data/examples/app_fractal_dashboard/bags/custom_shell_output.rb +86 -0
  15. data/examples/app_fractal_dashboard/bags/disk_usage.rb +44 -0
  16. data/examples/app_fractal_dashboard/bags/network_panel.rb +45 -0
  17. data/examples/app_fractal_dashboard/bags/ping.rb +43 -0
  18. data/examples/app_fractal_dashboard/bags/stats_panel.rb +45 -0
  19. data/examples/app_fractal_dashboard/bags/system_info.rb +43 -0
  20. data/examples/app_fractal_dashboard/bags/uptime.rb +43 -0
  21. data/examples/app_fractal_dashboard/dashboard/base.rb +74 -0
  22. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +86 -0
  23. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +87 -0
  24. data/examples/app_fractal_dashboard/dashboard/update_router.rb +43 -0
  25. data/examples/verify_readme_usage/README.md +1 -1
  26. data/examples/verify_readme_usage/app.rb +1 -1
  27. data/examples/{widget_cmd_exec → widget_command_system}/app.rb +18 -18
  28. data/lib/ratatui_ruby/tea/command/cancellation_token.rb +135 -0
  29. data/lib/ratatui_ruby/tea/command/custom.rb +106 -0
  30. data/lib/ratatui_ruby/tea/command/outlet.rb +127 -0
  31. data/lib/ratatui_ruby/tea/command.rb +367 -0
  32. data/lib/ratatui_ruby/tea/router.rb +405 -0
  33. data/lib/ratatui_ruby/tea/runtime.rb +147 -43
  34. data/lib/ratatui_ruby/tea/shortcuts.rb +51 -0
  35. data/lib/ratatui_ruby/tea/version.rb +1 -1
  36. data/lib/ratatui_ruby/tea.rb +59 -1
  37. data/rbs_collection.lock.yaml +124 -0
  38. data/rbs_collection.yaml +15 -0
  39. data/sig/examples/verify_readme_usage/app.rbs +1 -1
  40. data/sig/examples/{widget_cmd_exec → widget_command_system}/app.rbs +1 -1
  41. data/sig/open3.rbs +17 -0
  42. data/sig/ratatui_ruby/tea/command.rbs +163 -0
  43. data/sig/ratatui_ruby/tea/router.rbs +155 -0
  44. data/sig/ratatui_ruby/tea/runtime.rbs +29 -11
  45. data/sig/ratatui_ruby/tea/shortcuts.rbs +18 -0
  46. data/sig/ratatui_ruby/tea/version.rbs +10 -0
  47. data/sig/ratatui_ruby/tea.rbs +19 -7
  48. data/tasks/steep.rake +11 -0
  49. metadata +37 -8
  50. data/lib/ratatui_ruby/tea/cmd.rb +0 -88
  51. data/sig/ratatui_ruby/tea/cmd.rbs +0 -32
  52. /data/examples/{widget_cmd_exec → widget_command_system}/README.md +0 -0
@@ -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
+ require_relative "command"
9
+
10
+ module RatatuiRuby
11
+ module Tea
12
+ # Convenient short aliases for Tea APIs.
13
+ #
14
+ # The library uses intention-revealing names that match Ruby built-ins:
15
+ # +Command+, +System+, +Exit+. These are great for readability.
16
+ #
17
+ # This module provides the short aliases common in TEA-style code:
18
+ #
19
+ # === Example
20
+ #
21
+ # require "ratatui_ruby/tea/shortcuts"
22
+ # include RatatuiRuby::Tea::Shortcuts
23
+ #
24
+ # # Now use short names freely:
25
+ # Cmd.exit # → Command.exit
26
+ # Cmd.sh("ls", :files) # → Command.system("ls", :files)
27
+ # Cmd.map(child) { ... } # → Command.map(child) { ... }
28
+ module Shortcuts
29
+ # Short alias for +Command+.
30
+ module Cmd
31
+ # Creates an exit command.
32
+ # Alias for +Command.exit+.
33
+ def self.exit
34
+ Command.exit
35
+ end
36
+
37
+ # Creates a shell execution command.
38
+ # Short alias for +Command.system+.
39
+ def self.sh(command, tag)
40
+ Command.system(command, tag)
41
+ end
42
+
43
+ # Creates a mapped command.
44
+ # Short alias for +Command.map+.
45
+ def self.map(inner_command, &mapper)
46
+ Command.map(inner_command, &mapper)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -9,6 +9,6 @@ module RatatuiRuby # :nodoc: Documented in the ratatui_ruby gem.
9
9
  module Tea
10
10
  # The version of this gem.
11
11
  # See https://semver.org/spec/v2.0.0.html
12
- VERSION = "0.2.0"
12
+ VERSION = "0.3.1"
13
13
  end
14
14
  end
@@ -6,8 +6,9 @@
6
6
  #++
7
7
 
8
8
  require_relative "tea/version"
9
- require_relative "tea/cmd"
9
+ require_relative "tea/command"
10
10
  require_relative "tea/runtime"
11
+ require_relative "tea/router"
11
12
 
12
13
  module RatatuiRuby # :nodoc: Documented in the ratatui_ruby gem.
13
14
  # The Elm Architecture for RatatuiRuby.
@@ -26,5 +27,62 @@ module RatatuiRuby # :nodoc: Documented in the ratatui_ruby gem.
26
27
  def self.run(...)
27
28
  Runtime.run(...)
28
29
  end
30
+
31
+ # Wraps a command with a routing prefix.
32
+ #
33
+ # Parent bags trigger child bag commands. The results need routing back
34
+ # to the correct child bag. Manually wrapping every command is tedious.
35
+ #
36
+ # This method prefixes command results automatically. Use it to route
37
+ # child bag command results in Fractal Architecture.
38
+ #
39
+ # [command] The child bag command to wrap.
40
+ # [prefix] Symbol prepended to results (e.g., <tt>:stats</tt>).
41
+ #
42
+ # === Example
43
+ #
44
+ # # Verbose:
45
+ # Command.map(child_bag.fetch_command) { |r| [:stats, *r] }
46
+ #
47
+ # # Concise:
48
+ # Tea.route(child_bag.fetch_command, :stats)
49
+ def self.route(command, prefix)
50
+ Command.map(command) { |result| [prefix, *result] }
51
+ end
52
+
53
+ # Delegates a prefixed message to a child bag's UPDATE.
54
+ #
55
+ # Parent bag UPDATE functions route messages to child bags. Each route
56
+ # requires pattern matching, calling the child, and rewrapping any returned
57
+ # command. The boilerplate adds up fast.
58
+ #
59
+ # This method handles the dispatch. It checks the prefix, calls the child,
60
+ # and wraps any command. Returns <tt>nil</tt> if the prefix does not match.
61
+ #
62
+ # [message] Incoming message (e.g., <tt>[:stats, :system_info, {...}]</tt>).
63
+ # [prefix] Expected prefix symbol (e.g., <tt>:stats</tt>).
64
+ # [child_update] The child's UPDATE callable.
65
+ # [child_model] The child's current model.
66
+ #
67
+ # === Example
68
+ #
69
+ # # Verbose:
70
+ # case message
71
+ # in [:stats, *rest]
72
+ # new_child, cmd = StatsPanel::UPDATE.call(rest, model.stats)
73
+ # mapped = cmd ? Command.map(cmd) { |r| [:stats, *r] } : nil
74
+ # [new_child, mapped]
75
+ # end
76
+ #
77
+ # # Concise:
78
+ # Tea.delegate(message, :stats, StatsPanel::UPDATE, model.stats)
79
+ def self.delegate(message, prefix, child_update, child_model)
80
+ return nil unless message.is_a?(Array) && message.first == prefix
81
+
82
+ rest = message[1..]
83
+ new_child, command = child_update.call(rest, child_model)
84
+ wrapped = command ? route(command, prefix) : nil
85
+ [new_child, wrapped]
86
+ end
29
87
  end
30
88
  end
@@ -0,0 +1,124 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ ---
6
+ path: ".gem_rbs_collection"
7
+ gems:
8
+ - name: base64
9
+ version: 0.3.0
10
+ source:
11
+ type: rubygems
12
+ - name: bigdecimal
13
+ version: '0'
14
+ source:
15
+ type: stdlib
16
+ - name: csv
17
+ version: '0'
18
+ source:
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
+ - name: ffi
29
+ version: 1.17.3
30
+ source:
31
+ type: rubygems
32
+ - name: fileutils
33
+ version: '0'
34
+ source:
35
+ type: stdlib
36
+ - name: forwardable
37
+ version: '0'
38
+ source:
39
+ type: stdlib
40
+ - name: io-console
41
+ version: '0'
42
+ source:
43
+ type: stdlib
44
+ - name: json
45
+ version: '0'
46
+ source:
47
+ type: stdlib
48
+ - name: logger
49
+ version: '0'
50
+ source:
51
+ type: stdlib
52
+ - name: minitest
53
+ version: '0'
54
+ source:
55
+ type: stdlib
56
+ - name: monitor
57
+ version: '0'
58
+ source:
59
+ type: stdlib
60
+ - name: mutex_m
61
+ version: 0.3.0
62
+ source:
63
+ type: rubygems
64
+ - name: optparse
65
+ version: '0'
66
+ source:
67
+ type: stdlib
68
+ - name: pp
69
+ version: '0'
70
+ source:
71
+ type: stdlib
72
+ - name: prettyprint
73
+ version: '0'
74
+ source:
75
+ type: stdlib
76
+ - name: prism
77
+ version: 1.7.0
78
+ source:
79
+ 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
+ - name: ratatui_ruby
89
+ version: 0.10.1
90
+ source:
91
+ type: rubygems
92
+ - name: ratatui_ruby-devtools
93
+ version: 0.1.0
94
+ source:
95
+ type: rubygems
96
+ - name: rbs
97
+ version: 3.10.2
98
+ source:
99
+ type: rubygems
100
+ - name: rdoc
101
+ version: '0'
102
+ source:
103
+ type: stdlib
104
+ - name: securerandom
105
+ version: '0'
106
+ source:
107
+ type: stdlib
108
+ - name: stringio
109
+ version: '0'
110
+ source:
111
+ type: stdlib
112
+ - name: strscan
113
+ version: '0'
114
+ source:
115
+ type: stdlib
116
+ - name: tsort
117
+ version: '0'
118
+ source:
119
+ type: stdlib
120
+ - name: uri
121
+ version: '0'
122
+ source:
123
+ type: stdlib
124
+ gemfile_lock_path: Gemfile.lock
@@ -0,0 +1,15 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ # RBS collection configuration
6
+ # See: https://github.com/ruby/rbs/blob/master/docs/collection.md
7
+
8
+ sources:
9
+ - type: stdlib
10
+ - type: rubygems
11
+
12
+ path: .gem_rbs_collection
13
+
14
+ gems:
15
+ - name: ratatui_ruby
@@ -13,7 +13,7 @@ class VerifyReadmeUsage
13
13
 
14
14
  VIEW: ^(Model, RatatuiRuby::TUI) -> untyped
15
15
 
16
- UPDATE: ^(RatatuiRuby::Event, Model) -> (Model | [Model, RatatuiRuby::Tea::Cmd::Quit])
16
+ UPDATE: ^(RatatuiRuby::Event, Model) -> (Model | [Model, RatatuiRuby::Tea::Command::Exit])
17
17
 
18
18
  def run: () -> void
19
19
  end
@@ -20,7 +20,7 @@ class WidgetCmdExec
20
20
  # Msg can be Event or [:got_output, Hash]
21
21
  type msg = RatatuiRuby::Event | [Symbol, Hash[Symbol, untyped]]
22
22
 
23
- UPDATE: ^(msg, Model) -> (Model | [Model, RatatuiRuby::Tea::Cmd::execution?])
23
+ UPDATE: ^(msg, Model) -> (Model | [Model, RatatuiRuby::Tea::Command::execution?])
24
24
 
25
25
  def run: () -> void
26
26
  end
data/sig/open3.rbs ADDED
@@ -0,0 +1,17 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ # Local shim for Open3 methods not in stdlib RBS.
7
+ # The stdlib open3/0/open3.rbs only defines capture2e.
8
+ # TODO: Remove when upstream RBS adds these methods.
9
+
10
+ module Open3
11
+ # Executes a command and returns stdout, stderr, and status.
12
+ def self.capture3: (*String, ?stdin_data: String, ?binmode: boolish) -> [String, String, Process::Status]
13
+
14
+ # Executes a command with pipes for stdin, stdout, stderr.
15
+ def self.popen3: (*String) { (IO stdin, IO stdout, IO stderr, Thread wait_thr) -> void } -> Process::Status
16
+ | (*String) -> [IO, IO, IO, Thread]
17
+ end
@@ -0,0 +1,163 @@
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 Command
9
+ # Interface for user-defined custom commands.
10
+ # Any callable satisfying this interface can be dispatched.
11
+ interface _Command
12
+ # Brand predicate for update return disambiguation.
13
+ def tea_command?: () -> true
14
+
15
+ # Execute the command's side effect.
16
+ # Push result messages via the outlet.
17
+ def call: (Outlet out, CancellationToken token) -> void
18
+
19
+ # Grace period for cooperative cancellation (seconds).
20
+ # Runtime waits this long before force-killing the thread.
21
+ def tea_cancellation_grace_period: () -> Float
22
+ end
23
+
24
+ # Sentinel value for application termination.
25
+ class Exit < Data
26
+ def self.new: () -> instance
27
+ end
28
+
29
+ # Runs a shell command and routes its output back as messages.
30
+ class System < Data
31
+ attr_reader command: String
32
+ attr_reader tag: Symbol | Class
33
+ attr_reader stream: bool
34
+
35
+ # Command identification for runtime dispatch.
36
+ def tea_command?: () -> true
37
+
38
+ # Grace period for cleanup after cancellation.
39
+ def tea_cancellation_grace_period: () -> Float
40
+
41
+ # Returns true if streaming mode is enabled.
42
+ def stream?: () -> bool
43
+
44
+ # Executes the shell command and sends results via outlet.
45
+ def call: (Outlet out, CancellationToken token) -> void
46
+
47
+ def self.new: (command: String, tag: (Symbol | Class), stream: bool) -> instance
48
+ end
49
+
50
+ # Command that wraps another command's result with a transformation.
51
+ class Mapped < Data
52
+ attr_reader inner_command: execution
53
+ attr_reader mapper: ^(Array[untyped]) -> Array[untyped]
54
+
55
+ # Command identification for runtime dispatch.
56
+ def tea_command?: () -> true
57
+
58
+ # Grace period delegates to inner command.
59
+ def tea_cancellation_grace_period: () -> Float
60
+
61
+ # Executes the inner command and transforms the result.
62
+ def call: (Outlet out, CancellationToken token) -> void
63
+
64
+ def self.new: (inner_command: execution, mapper: ^(Array[untyped]) -> Array[untyped]) -> instance
65
+ end
66
+
67
+ # Sentinel value for command cancellation.
68
+ class Cancel < Data
69
+ attr_reader handle: _Command
70
+
71
+ def self.new: (handle: _Command) -> instance
72
+ end
73
+
74
+ # Sentinel value for command errors.
75
+ class Error < Data
76
+ attr_reader command: _Command
77
+ attr_reader exception: Exception
78
+
79
+ def self.new: (command: _Command, exception: Exception) -> instance
80
+ end
81
+
82
+ # Union type for all valid commands
83
+ type execution = System | Mapped | _Command
84
+
85
+ # Creates a quit command.
86
+ def self.exit: () -> Exit
87
+
88
+ # Creates a shell execution command.
89
+ def self.system: (String command, (Symbol | Class) tag, ?stream: bool) -> System
90
+
91
+ # Creates a mapped command for Fractal Architecture composition.
92
+ def self.map: (execution inner_command) { (Array[untyped]) -> Array[untyped] } -> Mapped
93
+
94
+ # Request cancellation of a running command.
95
+ def self.cancel: (_Command handle) -> Cancel
96
+
97
+ # Creates an error sentinel.
98
+ def self.error: (_Command command, Exception exception) -> Error
99
+
100
+ # 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?
110
+
111
+ def self.new: (callable: ^(Outlet, CancellationToken) -> void, grace_period: Float?) -> instance
112
+ def call: (Outlet out, CancellationToken token) -> void
113
+ end
114
+
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
119
+
120
+ def self.new: () -> instance
121
+
122
+ # Signals cancellation. Thread-safe.
123
+ def cancel!: () -> void
124
+
125
+ # Checks if cancellation was requested. Thread-safe.
126
+ def cancelled?: () -> bool
127
+
128
+ # Null object for commands that ignore cancellation.
129
+ class NoneToken < Data
130
+ attr_reader cancelled?: bool
131
+
132
+ def self.new: (cancelled?: bool) -> instance
133
+
134
+ # Does nothing. Ignores cancellation requests.
135
+ def cancel!: () -> nil
136
+ end
137
+
138
+ # Singleton null token.
139
+ NONE: NoneToken
140
+ end
141
+
142
+ # Mixin for user-defined custom commands.
143
+ module Custom
144
+ # Brand predicate for command identification.
145
+ def tea_command?: () -> true
146
+
147
+ # Cleanup time after cancellation is requested. In seconds.
148
+ # Default: 0.1 seconds (100 milliseconds).
149
+ def tea_cancellation_grace_period: () -> Float
150
+ end
151
+
152
+ # Messaging gateway for custom commands.
153
+ class Outlet
154
+ @queue: Thread::Queue[Array[untyped]]
155
+
156
+ def initialize: (Thread::Queue[Array[untyped]] queue) -> void
157
+
158
+ # Sends a tagged message to the runtime.
159
+ def put: (Symbol tag, *untyped payload) -> void
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,155 @@
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
+ # Declarative DSL for Fractal Architecture.
9
+ module Router
10
+ def self.included: (Class base) -> void
11
+
12
+ # Duck type for Data-like models that support `with`.
13
+ interface _DataModel
14
+ def public_send: (Symbol, *Object) -> Object
15
+ def with: (**Object) -> self
16
+ end
17
+
18
+ # Interface for fractal child modules (bags).
19
+ # Routed modules must have an UPDATE constant that handles child messages.
20
+ # NOTE: We use Module here because RBS interfaces can't declare class methods.
21
+
22
+ # Configuration for key handlers.
23
+ class KeyHandlerConfig < Data
24
+ attr_reader handler: (^() -> Command::execution?)?
25
+ attr_reader action: Symbol?
26
+ attr_reader route: Symbol?
27
+ attr_reader guard: (^(_DataModel) -> bool)?
28
+
29
+ def initialize: (?handler: (^() -> Command::execution?)?, ?action: Symbol?, ?route: Symbol?, ?guard: (^(_DataModel) -> bool)?) -> void
30
+ end
31
+
32
+ # Configuration for scroll handlers (no coordinates).
33
+ class ScrollHandlerConfig < Data
34
+ attr_reader handler: (^() -> Command::execution?)?
35
+ attr_reader action: Symbol?
36
+
37
+ def initialize: (?handler: (^() -> Command::execution?)?, ?action: Symbol?) -> void
38
+ end
39
+
40
+ # Configuration for click handlers (x, y coordinates).
41
+ class ClickHandlerConfig < Data
42
+ attr_reader handler: (^(Integer, Integer) -> Command::execution?)?
43
+ attr_reader action: Symbol?
44
+
45
+ def initialize: (?handler: (^(Integer, Integer) -> Command::execution?)?, ?action: Symbol?) -> void
46
+ end
47
+
48
+ # Class methods added when Router is included.
49
+ module ClassMethods : Module
50
+ @routes: Hash[Symbol, Module]
51
+ @actions: Hash[Symbol, ^() -> Command::execution?]
52
+ @key_handlers: Hash[String, KeyHandlerConfig]
53
+ @scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
54
+ @click_handler: ClickHandlerConfig?
55
+
56
+ def route: (Symbol | String prefix, to: Module) -> void
57
+ def routes: () -> Hash[Symbol, Module]
58
+
59
+ def action: (Symbol | String name, ^() -> Command::execution? handler) -> void
60
+ def actions: () -> Hash[Symbol, ^() -> Command::execution?]
61
+
62
+ def keymap: () { (KeymapBuilder) [self: KeymapBuilder] -> void } -> void
63
+ def key_handlers: () -> Hash[String, KeyHandlerConfig]
64
+
65
+ def mousemap: () { (MousemapBuilder) [self: MousemapBuilder] -> void } -> void
66
+ def scroll_handlers: () -> Hash[Symbol, ScrollHandlerConfig]
67
+ def click_handler: () -> ClickHandlerConfig?
68
+
69
+ # Returns UPDATE callable that handles routing.
70
+ # Uses is_a? checks for Event::Key and Event::Mouse type narrowing.
71
+ # Model must be Data-like with `with` and field accessors.
72
+ def from_router: () -> RouterUpdate
73
+ end
74
+
75
+ # UPDATE callable returned by from_router with proper typing.
76
+ # Implements #call to satisfy Proc-like interfaces.
77
+ class RouterUpdate
78
+ @routes: Hash[Symbol, Module]
79
+ @actions: Hash[Symbol, ^() -> Command::execution?]
80
+ @key_handlers: Hash[String, KeyHandlerConfig]
81
+ @scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
82
+ @click_handler: ClickHandlerConfig?
83
+
84
+ def initialize: (
85
+ routes: Hash[Symbol, Module],
86
+ actions: Hash[Symbol, ^() -> Command::execution?],
87
+ key_handlers: Hash[String, KeyHandlerConfig],
88
+ scroll_handlers: Hash[Symbol, ScrollHandlerConfig],
89
+ click_handler: ClickHandlerConfig?
90
+ ) -> void
91
+
92
+ # Process message and return [model, command] tuple.
93
+ def call: (
94
+ (RatatuiRuby::Event::Key | RatatuiRuby::Event::Mouse | Array[Object]) message,
95
+ _DataModel model
96
+ ) -> [_DataModel, Command::execution?]
97
+ end
98
+
99
+ # Builder for keymap DSL.
100
+ class KeymapBuilder
101
+ @handlers: Hash[String, KeyHandlerConfig]
102
+ @guard_stack: Array[^(_DataModel) -> bool]
103
+
104
+ attr_reader handlers: Hash[String, KeyHandlerConfig]
105
+
106
+ def initialize: () -> void
107
+
108
+ def key: (
109
+ String | Symbol key_name,
110
+ (^() -> Command::execution?) | Symbol handler_or_action,
111
+ ?route: Symbol?,
112
+ ?when: (^(_DataModel) -> bool)?,
113
+ ?if: (^(_DataModel) -> bool)?,
114
+ ?only: (^(_DataModel) -> bool)?,
115
+ ?guard: (^(_DataModel) -> bool)?,
116
+ ?unless: (^(_DataModel) -> bool)?,
117
+ ?except: (^(_DataModel) -> bool)?,
118
+ ?skip: (^(_DataModel) -> bool)?
119
+ ) -> void
120
+
121
+ def only: (
122
+ ?when: (^(_DataModel) -> bool)?,
123
+ ?if: (^(_DataModel) -> bool)?,
124
+ ?only: (^(_DataModel) -> bool)?,
125
+ ?guard: (^(_DataModel) -> bool)?
126
+ ) { () -> void } -> void
127
+
128
+ def skip: (
129
+ ?when: (^(_DataModel) -> bool)?,
130
+ ?if: (^(_DataModel) -> bool)?,
131
+ ?skip: (^(_DataModel) -> bool)?,
132
+ ?guard: (^(_DataModel) -> bool)?
133
+ ) { () -> void } -> void
134
+
135
+ private
136
+
137
+ def with_guard: ((^(_DataModel) -> bool)?) { () -> void } -> void
138
+ end
139
+
140
+ # Builder for mousemap DSL.
141
+ class MousemapBuilder
142
+ @scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
143
+ @click_handler: ClickHandlerConfig?
144
+
145
+ attr_reader scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
146
+ attr_reader click_handler: ClickHandlerConfig?
147
+
148
+ def initialize: () -> void
149
+
150
+ def click: ((^(Integer, Integer) -> Command::execution?) | Symbol handler_or_action) -> void
151
+ def scroll: (:up | :down direction, (^() -> Command::execution?) | Symbol handler_or_action) -> void
152
+ end
153
+ end
154
+ end
155
+ end
@@ -5,22 +5,40 @@
5
5
 
6
6
  module RatatuiRuby
7
7
  module Tea
8
+ # MVU runtime event loop.
8
9
  class Runtime
10
+ # Active command tracking entry.
11
+ type active_entry = { thread: Thread, token: Command::CancellationToken }
12
+
13
+ # Widget type accepted by view functions.
14
+ type renderable = RatatuiRuby::_CustomWidget | RatatuiRuby::widget
15
+
16
+ # Duck type for update result that can be normalized.
17
+ # Steep needs a union type (not interface) for is_a? narrowing.
18
+ type update_result = [Object, Command::execution?] | Command::execution | Object
19
+
20
+ # Duck type for values that can be queried for command-ness.
21
+ interface _MaybeCommand
22
+ def nil?: () -> bool
23
+ def class: () -> Class
24
+ def respond_to?: (Symbol, ?bool) -> bool
25
+ def tea_command?: () -> bool
26
+ end
27
+
9
28
  # Starts the MVU event loop.
10
- def self.run: [M, Msg] (
11
- model: M,
12
- view: ^(M model, RatatuiRuby::TUI tui) -> untyped,
13
- update: ^(Msg msg, M model) -> (M | [M, Cmd::execution?] | [M, Cmd::Quit] | [M, nil]),
14
- ?init: ^() -> Msg
15
- ) -> M
29
+ def self.run: [Model] (
30
+ model: Model,
31
+ view: ^(Model, RatatuiRuby::TUI) -> renderable,
32
+ update: ^(RatatuiRuby::Event, Model) -> update_result?,
33
+ ?init: (^() -> RatatuiRuby::Event)?
34
+ ) -> Model
16
35
 
17
36
  private
18
37
 
19
- def self.validate_view_result!: (untyped widget) -> void
20
- def self.normalize_update_result: [M] (untyped result, M previous_model) -> [M, untyped]
21
- def self.valid_cmd?: (untyped value) -> bool
22
- def self.validate_ractor_shareable!: (untyped object, String name) -> void
23
- def self.dispatch: (untyped cmd, Queue queue) -> void
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?]
40
+ 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?
24
42
  end
25
43
  end
26
44
  end