ratatui_ruby-tea 0.2.0 → 0.3.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +8 -0
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +1 -1
  5. data/doc/concepts/application_architecture.md +182 -3
  6. data/examples/app_fractal_dashboard/README.md +60 -0
  7. data/examples/app_fractal_dashboard/app.rb +67 -0
  8. data/examples/app_fractal_dashboard/bags/custom_shell_input.rb +77 -0
  9. data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +73 -0
  10. data/examples/app_fractal_dashboard/bags/custom_shell_output.rb +86 -0
  11. data/examples/app_fractal_dashboard/bags/disk_usage.rb +44 -0
  12. data/examples/app_fractal_dashboard/bags/network_panel.rb +45 -0
  13. data/examples/app_fractal_dashboard/bags/ping.rb +43 -0
  14. data/examples/app_fractal_dashboard/bags/stats_panel.rb +45 -0
  15. data/examples/app_fractal_dashboard/bags/system_info.rb +43 -0
  16. data/examples/app_fractal_dashboard/bags/uptime.rb +43 -0
  17. data/examples/app_fractal_dashboard/dashboard/base.rb +74 -0
  18. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +86 -0
  19. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +87 -0
  20. data/examples/app_fractal_dashboard/dashboard/update_router.rb +43 -0
  21. data/examples/verify_readme_usage/README.md +1 -1
  22. data/examples/verify_readme_usage/app.rb +1 -1
  23. data/examples/{widget_cmd_exec → widget_command_system}/app.rb +18 -18
  24. data/lib/ratatui_ruby/tea/command.rb +145 -0
  25. data/lib/ratatui_ruby/tea/router.rb +337 -0
  26. data/lib/ratatui_ruby/tea/runtime.rb +99 -39
  27. data/lib/ratatui_ruby/tea/shortcuts.rb +51 -0
  28. data/lib/ratatui_ruby/tea/version.rb +1 -1
  29. data/lib/ratatui_ruby/tea.rb +59 -1
  30. data/sig/ratatui_ruby/tea/command.rbs +47 -0
  31. data/sig/ratatui_ruby/tea/router.rbs +99 -0
  32. metadata +26 -8
  33. data/lib/ratatui_ruby/tea/cmd.rb +0 -88
  34. data/sig/ratatui_ruby/tea/cmd.rbs +0 -32
  35. /data/examples/{widget_cmd_exec → widget_command_system}/README.md +0 -0
  36. /data/sig/examples/{widget_cmd_exec → widget_command_system}/app.rbs +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.0"
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,47 @@
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
+ # Sentinel value for application termination.
10
+ class Exit < Data
11
+ def self.new: () -> instance
12
+ end
13
+
14
+ # Command to run a shell command via Open3.
15
+ class System < Data
16
+ attr_reader command: String
17
+ attr_reader tag: Symbol | Class
18
+ attr_reader stream: bool
19
+
20
+ # Returns true if streaming mode is enabled.
21
+ def stream?: () -> bool
22
+
23
+ def self.new: (command: String, tag: (Symbol | Class), stream: bool) -> instance
24
+ end
25
+
26
+ # Command that wraps another command's result with a transformation.
27
+ class Mapped < Data
28
+ attr_reader inner_command: execution
29
+ attr_reader mapper: ^(untyped) -> untyped
30
+
31
+ def self.new: (inner_command: execution, mapper: ^(untyped) -> untyped) -> instance
32
+ end
33
+
34
+ # Union type for all valid commands
35
+ type execution = Execute | Mapped
36
+
37
+ # Creates a quit command.
38
+ def self.exit: () -> Quit
39
+
40
+ # Creates a shell execution command.
41
+ def self.system: (String command, (Symbol | Class) tag, ?stream: bool) -> System
42
+
43
+ # Creates a mapped command for Fractal Architecture composition.
44
+ def self.map: (execution inner_command) { (untyped) -> untyped } -> Mapped
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,99 @@
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
+ # Interface for child bag modules (required by route).
9
+ interface _Bag[M]
10
+ def self.UPDATE: ^(top, M) -> [M, Command::execution?]
11
+ def self.INITIAL: M
12
+ end
13
+
14
+ # Declarative DSL for Fractal Architecture.
15
+ module Router
16
+ def self.included: (Class base) -> void
17
+
18
+ # Guard callable: receives model, returns truthy/falsy.
19
+ type guard[M] = ^(M model) -> boolish
20
+
21
+ # Handler callable: returns a command or nil.
22
+ type handler = ^() -> Command::execution?
23
+
24
+ # Click handler: receives x, y coordinates.
25
+ type click_handler = ^(Integer x, Integer y) -> Command::execution?
26
+
27
+ # Key handler config stored in handlers hash.
28
+ type key_config[M] = {
29
+ handler: handler?,
30
+ action: Symbol?,
31
+ route: Symbol?,
32
+ guard: guard[M]?
33
+ }
34
+
35
+ # Mouse handler config.
36
+ type mouse_config = {
37
+ handler: handler | click_handler,
38
+ action: Symbol?
39
+ }
40
+
41
+ # Class methods added when Router is included.
42
+ module ClassMethods
43
+ def route: [M] (Symbol | String prefix, to: _Bag[M]) -> void
44
+ def routes: () -> Hash[Symbol, Module]
45
+
46
+ def action: (Symbol | String name, handler) -> void
47
+ def actions: () -> Hash[Symbol, handler]
48
+
49
+ def keymap: () { () -> void } -> void
50
+ def key_handlers: [M] () -> Hash[String, key_config[M]]
51
+
52
+ def mousemap: () { () -> void } -> void
53
+ def mouse_handlers: () -> Hash[Symbol, mouse_config]
54
+
55
+ # Generates an UPDATE lambda from routes, keymap, and mousemap.
56
+ def from_router: [M, Msg] () -> ^(Msg message, M model) -> [M, Command::execution?]
57
+ end
58
+
59
+ # Builder for keymap DSL.
60
+ class KeymapBuilder
61
+ @handlers: Hash[String, key_config[top]]
62
+
63
+ attr_reader handlers: Hash[String, key_config[top]]
64
+
65
+ def initialize: () -> void
66
+
67
+ # Registers a key handler with optional guards.
68
+ def key: [M] (
69
+ String | Symbol key_name,
70
+ handler | Symbol handler_or_action,
71
+ ?route: Symbol?,
72
+ ?when: guard[M]?,
73
+ ?if: guard[M]?,
74
+ ?only: guard[M]?,
75
+ ?guard: guard[M]?,
76
+ ?unless: guard[M]?,
77
+ ?except: guard[M]?,
78
+ ?skip: guard[M]?
79
+ ) -> void
80
+ end
81
+
82
+ # Builder for mousemap DSL.
83
+ class MousemapBuilder
84
+ @handlers: Hash[Symbol, mouse_config]
85
+
86
+ attr_reader handlers: Hash[Symbol, mouse_config]
87
+
88
+ def initialize: () -> void
89
+
90
+ def click: (click_handler | Symbol handler_or_action) -> void
91
+ def scroll: (:up | :down direction, handler | Symbol handler_or_action) -> void
92
+
93
+ private
94
+
95
+ def register: (Symbol key, handler | click_handler | Symbol handler_or_action) -> void
96
+ end
97
+ end
98
+ end
99
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratatui_ruby-tea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerrick Long
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.9'
18
+ version: 0.9.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.9'
25
+ version: 0.9.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: ostruct
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -101,20 +101,38 @@ files:
101
101
  - doc/images/verify_readme_usage.png
102
102
  - doc/images/widget_cmd_exec.png
103
103
  - doc/index.md
104
+ - examples/app_fractal_dashboard/README.md
105
+ - examples/app_fractal_dashboard/app.rb
106
+ - examples/app_fractal_dashboard/bags/custom_shell_input.rb
107
+ - examples/app_fractal_dashboard/bags/custom_shell_modal.rb
108
+ - examples/app_fractal_dashboard/bags/custom_shell_output.rb
109
+ - examples/app_fractal_dashboard/bags/disk_usage.rb
110
+ - examples/app_fractal_dashboard/bags/network_panel.rb
111
+ - examples/app_fractal_dashboard/bags/ping.rb
112
+ - examples/app_fractal_dashboard/bags/stats_panel.rb
113
+ - examples/app_fractal_dashboard/bags/system_info.rb
114
+ - examples/app_fractal_dashboard/bags/uptime.rb
115
+ - examples/app_fractal_dashboard/dashboard/base.rb
116
+ - examples/app_fractal_dashboard/dashboard/update_helpers.rb
117
+ - examples/app_fractal_dashboard/dashboard/update_manual.rb
118
+ - examples/app_fractal_dashboard/dashboard/update_router.rb
104
119
  - examples/verify_readme_usage/README.md
105
120
  - examples/verify_readme_usage/app.rb
106
- - examples/widget_cmd_exec/README.md
107
- - examples/widget_cmd_exec/app.rb
121
+ - examples/widget_command_system/README.md
122
+ - examples/widget_command_system/app.rb
108
123
  - exe/.gitkeep
109
124
  - lib/ratatui_ruby/tea.rb
110
- - lib/ratatui_ruby/tea/cmd.rb
125
+ - lib/ratatui_ruby/tea/command.rb
126
+ - lib/ratatui_ruby/tea/router.rb
111
127
  - lib/ratatui_ruby/tea/runtime.rb
128
+ - lib/ratatui_ruby/tea/shortcuts.rb
112
129
  - lib/ratatui_ruby/tea/version.rb
113
130
  - mise.toml
114
131
  - sig/examples/verify_readme_usage/app.rbs
115
- - sig/examples/widget_cmd_exec/app.rbs
132
+ - sig/examples/widget_command_system/app.rbs
116
133
  - sig/ratatui_ruby/tea.rbs
117
- - sig/ratatui_ruby/tea/cmd.rbs
134
+ - sig/ratatui_ruby/tea/command.rbs
135
+ - sig/ratatui_ruby/tea/router.rbs
118
136
  - sig/ratatui_ruby/tea/runtime.rbs
119
137
  - tasks/example_viewer.html.erb
120
138
  - tasks/resources/build.yml.erb
@@ -1,88 +0,0 @@
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 RatatuiRuby
9
- module Tea
10
- # Commands represent side effects.
11
- #
12
- # The MVU pattern separates logic from effects. Your update function returns a pure
13
- # model transformation. Side effects go in commands. The runtime executes them.
14
- #
15
- # Commands produce **messages**, not callbacks. The +tag+ argument names the message
16
- # so your update function can pattern-match on it. This keeps all logic in +update+
17
- # and ensures messages are Ractor-shareable.
18
- #
19
- # === Examples
20
- #
21
- # # Terminate the application
22
- # [model, Cmd.quit]
23
- #
24
- # # Run a shell command; produces [:got_files, {stdout:, stderr:, status:}]
25
- # [model, Cmd.exec("ls -la", :got_files)]
26
- #
27
- # # No side effect
28
- # [model, nil]
29
- module Cmd
30
- # Sentinel value for application termination.
31
- #
32
- # The runtime detects this before dispatching. It breaks the loop immediately.
33
- Quit = Data.define
34
-
35
- # Creates a quit command.
36
- #
37
- # Returns a sentinel the runtime detects to terminate the application.
38
- #
39
- # === Example
40
- #
41
- # def update(msg, model)
42
- # case msg
43
- # in { type: :key, code: "q" }
44
- # [model, Cmd.quit]
45
- # else
46
- # [model, nil]
47
- # end
48
- # end
49
- def self.quit
50
- Quit.new
51
- end
52
-
53
- # Command to run a shell command via Open3.
54
- #
55
- # The runtime executes the command and produces a message:
56
- # <tt>[tag, {stdout:, stderr:, status:}]</tt>
57
- #
58
- # The +status+ is the integer exit code (0 = success).
59
- Exec = Data.define(:command, :tag)
60
-
61
- # Creates a shell execution command.
62
- #
63
- # [command] Shell command string to execute.
64
- # [tag] Symbol or class to tag the result message.
65
- #
66
- # When the command completes, the runtime sends
67
- # <tt>[tag, {stdout:, stderr:, status:}]</tt> to update.
68
- #
69
- # === Example
70
- #
71
- # # Return this from update:
72
- # [model.with(loading: true), Cmd.exec("ls -la", :got_files)]
73
- #
74
- # # Then handle it later:
75
- # def update(msg, model)
76
- # case msg
77
- # in [:got_files, {stdout:, status: 0}]
78
- # [model.with(files: stdout.lines), nil]
79
- # in [:got_files, {stderr:, status:}]
80
- # [model.with(error: stderr), nil]
81
- # end
82
- # end
83
- def self.exec(command, tag)
84
- Exec.new(command:, tag:)
85
- end
86
- end
87
- end
88
- end
@@ -1,32 +0,0 @@
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 Cmd
9
- # Sentinel value for application termination.
10
- class Quit < Data
11
- def self.new: () -> instance
12
- end
13
-
14
- # Command to run a shell command via Open3.
15
- class Exec < Data
16
- attr_reader command: String
17
- attr_reader tag: Symbol | Class
18
-
19
- def self.new: (command: String, tag: (Symbol | Class)) -> instance
20
- end
21
-
22
- # Union type for all valid commands
23
- type execution = Exec
24
-
25
- # Creates a quit command.
26
- def self.quit: () -> Quit
27
-
28
- # Creates a shell execution command.
29
- def self.exec: (String command, (Symbol | Class) tag) -> Exec
30
- end
31
- end
32
- end