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.
- checksums.yaml +4 -4
- data/AGENTS.md +17 -5
- data/CHANGELOG.md +74 -0
- data/README.md +18 -1
- data/Rakefile +1 -1
- data/Steepfile +3 -3
- data/doc/concepts/application_architecture.md +182 -3
- data/doc/contributors/design/commands_and_outlets.md +204 -0
- data/doc/contributors/kit-no-outlet.md +237 -0
- data/examples/app_fractal_dashboard/README.md +60 -0
- data/examples/app_fractal_dashboard/app.rb +67 -0
- data/examples/app_fractal_dashboard/bags/custom_shell_input.rb +77 -0
- data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +73 -0
- data/examples/app_fractal_dashboard/bags/custom_shell_output.rb +86 -0
- data/examples/app_fractal_dashboard/bags/disk_usage.rb +44 -0
- data/examples/app_fractal_dashboard/bags/network_panel.rb +45 -0
- data/examples/app_fractal_dashboard/bags/ping.rb +43 -0
- data/examples/app_fractal_dashboard/bags/stats_panel.rb +45 -0
- data/examples/app_fractal_dashboard/bags/system_info.rb +43 -0
- data/examples/app_fractal_dashboard/bags/uptime.rb +43 -0
- data/examples/app_fractal_dashboard/dashboard/base.rb +74 -0
- data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +86 -0
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +87 -0
- data/examples/app_fractal_dashboard/dashboard/update_router.rb +43 -0
- data/examples/verify_readme_usage/README.md +1 -1
- data/examples/verify_readme_usage/app.rb +1 -1
- data/examples/{widget_cmd_exec → widget_command_system}/app.rb +18 -18
- data/lib/ratatui_ruby/tea/command/cancellation_token.rb +135 -0
- data/lib/ratatui_ruby/tea/command/custom.rb +106 -0
- data/lib/ratatui_ruby/tea/command/outlet.rb +127 -0
- data/lib/ratatui_ruby/tea/command.rb +367 -0
- data/lib/ratatui_ruby/tea/router.rb +405 -0
- data/lib/ratatui_ruby/tea/runtime.rb +147 -43
- data/lib/ratatui_ruby/tea/shortcuts.rb +51 -0
- data/lib/ratatui_ruby/tea/version.rb +1 -1
- data/lib/ratatui_ruby/tea.rb +59 -1
- data/rbs_collection.lock.yaml +124 -0
- data/rbs_collection.yaml +15 -0
- data/sig/examples/verify_readme_usage/app.rbs +1 -1
- data/sig/examples/{widget_cmd_exec → widget_command_system}/app.rbs +1 -1
- data/sig/open3.rbs +17 -0
- data/sig/ratatui_ruby/tea/command.rbs +163 -0
- data/sig/ratatui_ruby/tea/router.rbs +155 -0
- data/sig/ratatui_ruby/tea/runtime.rbs +29 -11
- data/sig/ratatui_ruby/tea/shortcuts.rbs +18 -0
- data/sig/ratatui_ruby/tea/version.rbs +10 -0
- data/sig/ratatui_ruby/tea.rbs +19 -7
- data/tasks/steep.rake +11 -0
- metadata +37 -8
- data/lib/ratatui_ruby/tea/cmd.rb +0 -88
- data/sig/ratatui_ruby/tea/cmd.rbs +0 -32
- /data/examples/{widget_cmd_exec → widget_command_system}/README.md +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c83e509aceb1826713abd9e4835055c1d28397627438d3041783564195b1821b
|
|
4
|
+
data.tar.gz: d452ab612362d0d3216101ab9d454c950a8af07afcd913c3fa8627e69eb09887
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c211329907d95912192da187769d97ef56142cb4b1abe87ed2ed38c61b519d31f6823ab7f1154bc930e430b5fb19f1301c9497a9c68856c3c136c1ec0f6b962
|
|
7
|
+
data.tar.gz: 12b22830443dcc0783323bcccb8f2c1337a416cfe0cebaf89b0988d949f68e3ab73c4fbc37a6c140bc8b103465c8e467dad443654259961987dd91de7fa95767
|
data/AGENTS.md
CHANGED
|
@@ -31,6 +31,13 @@ Description: Part of the RatatuiRuby ecosystem.
|
|
|
31
31
|
- **Setup:** `bin/setup` must handle Bundler dependencies.
|
|
32
32
|
- **Git:** ALWAYS set `PAGER=cat` with `git`. **THIS IS CRITICAL!**
|
|
33
33
|
|
|
34
|
+
### Tea-Specific Vocabulary
|
|
35
|
+
|
|
36
|
+
- **BANNED WORD: "component"** — Reserved for Kit.
|
|
37
|
+
- **Avoid "widget" for Tea units** — "Widget" refers to Engine/Ratatui render primitives. In Tea, call them **bags**.
|
|
38
|
+
- **Bag:** A module containing `Model`, `INITIAL`, `UPDATE`, and `VIEW` constants. Bags compose: parent bags delegate to child bags.
|
|
39
|
+
- Use "model", "update", "view" for the MVU pattern. Use "message" (not "msg") and "command" (not "cmd").
|
|
40
|
+
|
|
34
41
|
### Ruby Standards
|
|
35
42
|
|
|
36
43
|
- Use `Data.define` for all value objects (Ruby 3.2+).
|
|
@@ -48,9 +55,14 @@ Description: Part of the RatatuiRuby ecosystem.
|
|
|
48
55
|
|
|
49
56
|
## 3. Definition of Done (DoD)
|
|
50
57
|
|
|
51
|
-
Before considering a task complete:
|
|
58
|
+
Before considering a task complete and returning control to the user, you **MUST** ensure:
|
|
59
|
+
|
|
60
|
+
0. **Production Ready:** RBS types are complete and accurate (no `untyped`), errors are handled with good DX, documentation follows guidelines, high code quality (no "pre-existing debt" excuses).
|
|
61
|
+
1. **Default Rake Task Passes:** Run `bundle exec agent_rake` (no args). Confirm it passes with ZERO errors **or warnings**.
|
|
62
|
+
- You will save time if you run `bundle exec agent_rake rubocop:autocorrect` first.
|
|
63
|
+
- If you think the rake is looking for deleted files, STOP EVERYTHING and tell the user.
|
|
64
|
+
2. **Documentation Updated:** If public APIs or observable behavior changed, update relevant RDoc, rustdoc, `doc/` files, `README.md`, and/or `ratatui_ruby-wiki` files.
|
|
65
|
+
3. **Changelog Updated:** If public APIs, observable behavior, or gemspec dependencies have changed, update [CHANGELOG.md](CHANGELOG.md)'s **Unreleased** section.
|
|
66
|
+
4. **Commit Message Suggested:** You **MUST** ensure the final message to the user includes a suggested commit message block. This is NOT optional.
|
|
67
|
+
- You MUST also check `git log -n1` to see the current standard AI footer ("Generated with" and "Co-Authored-By") and include it in your suggested message.
|
|
52
68
|
|
|
53
|
-
1. **Default Rake Task Passes:** Run `bundle exec agent_rake` (no args). Confirm it passes with ZERO errors.
|
|
54
|
-
2. **Documentation Updated:** If public APIs changed, update relevant docs.
|
|
55
|
-
3. **Changelog Updated:** If public APIs changed, update CHANGELOG.md's **Unreleased** section.
|
|
56
|
-
4. **Commit Message Suggested:** Include a suggested commit message block.
|
data/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
21
|
|
|
22
22
|
### Removed
|
|
23
23
|
|
|
24
|
+
## [0.3.1] - 2026-01-11
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **CancellationToken**: Cooperative cancellation mechanism for long-running custom commands. Commands check `cancelled?` periodically and stop gracefully when `cancel!` is called. Includes `CancellationToken::NONE` null object for commands that ignore cancellation.
|
|
29
|
+
|
|
30
|
+
- **Command::Custom Mixin**: Include in your class to mark it as a custom command. Provides `tea_command?` brand predicate and `tea_cancellation_grace_period` (default 2.0 seconds) for configuring cleanup time after cancellation.
|
|
31
|
+
|
|
32
|
+
- **Command::Outlet**: Messaging gateway for custom commands. Use `put(tag, *payload)` to send results back to the update function. Validates Ractor-shareability in debug mode.
|
|
33
|
+
|
|
34
|
+
- **Custom Command Dispatch**: Runtime now dispatches custom commands (objects with `tea_command?` returning true) in background threads. Commands receive an `Outlet` for messaging and a `CancellationToken` for cooperative shutdown.
|
|
35
|
+
|
|
36
|
+
- **Command.custom Factory**: Wraps lambdas/procs to give them unique identity for dispatch tracking. Each `Command.custom(callable)` call produces a distinct wrapper, enabling targeted cancellation. Accepts optional `grace_period:` to override the default 2.0 second cleanup window.
|
|
37
|
+
|
|
38
|
+
- **Command.cancel Factory**: Request cancellation of a running command. Returns a `Command::Cancel` sentinel that the runtime routes to the appropriate command's CancellationToken.
|
|
39
|
+
|
|
40
|
+
- **Runtime Cancellation Dispatch**: The runtime now handles `Command::Cancel` by signaling the target command's `CancellationToken`, enabling cooperative cancellation of long-running commands. Respects `tea_cancellation_grace_period`: waits for the grace period, then force-kills unresponsive threads. Use `Float::INFINITY` to never force-kill.
|
|
41
|
+
|
|
42
|
+
- **Graceful Shutdown**: On exit, runtime signals all active commands then respects each command's grace period. Commands with `Float::INFINITY` grace are waited on indefinitely (user has SIGKILL). Final queue messages are processed before returning.
|
|
43
|
+
|
|
44
|
+
- **Automatic Error Propagation**: Custom commands that raise unhandled exceptions now produce a `Command::Error` message instead of corrupting the TUI display. The runtime catches exceptions and pushes `Command::Error.new(command:, exception:)` to the queue. Pattern match on `Command::Error` in your update function to handle failures uniformly. Factory method `Command.error(command, exception)` is available for testing.
|
|
45
|
+
|
|
46
|
+
- **Command::System Cancellation**: Streaming shell commands now respect cooperative cancellation. When cancelled, sends `SIGTERM` for graceful shutdown, then `SIGKILL` if the child process doesn't exit. Prevents orphaned child processes from lingering after app exit.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Ractor Enforcement is Debug-Only**: The Ractor-shareability check now only runs in debug mode (and automated tests). Production skips this check for performance, matching the original specification. Previously, the check ran unconditionally.
|
|
53
|
+
|
|
54
|
+
### Removed
|
|
55
|
+
|
|
56
|
+
## [0.3.0] - 2026-01-08
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **Router DSL**: New `Tea::Router` module provides declarative routing for Fractal Architecture:
|
|
61
|
+
- `route :prefix, to: ChildBag` — declares a child bag route
|
|
62
|
+
- `keymap { key "q", -> { Command.exit } }` — declares keyboard handlers
|
|
63
|
+
- `keymap { key "x", handler, when: -> (m) { m.ready? } }` — guards (also: `if:`, `only:`, `guard:`, `unless:`, `except:`, `skip:`)
|
|
64
|
+
- `keymap { only when: guard do ... end }` — nested guard blocks apply to all keys within (also: `skip when: ...`)
|
|
65
|
+
- `mousemap { click -> (x, y) { ... } }` — declares mouse handlers
|
|
66
|
+
- `action :name, handler` — declares reusable actions for key/mouse handlers
|
|
67
|
+
- `from_router` — generates an UPDATE lambda from routes and handlers
|
|
68
|
+
|
|
69
|
+
- **Composition Helpers**: New helper methods for Fractal Architecture reduce boilerplate:
|
|
70
|
+
- `Tea.route(command, :prefix)` — wraps a command to route results to a child bag
|
|
71
|
+
- `Tea.delegate(message, :prefix, child_update, child_model)` — dispatches prefixed messages to child bags
|
|
72
|
+
|
|
73
|
+
- **Command Mapping**: `Command.map(inner_command, &mapper)` wraps a child command and transforms its result message. Essential for parent bags routing child command results.
|
|
74
|
+
|
|
75
|
+
- **Shortcuts Module**: `require "ratatui_ruby/tea/shortcuts"` and `include Tea::Shortcuts` for short aliases:
|
|
76
|
+
- `Cmd.exit` — alias for `Command.exit`
|
|
77
|
+
- `Cmd.sh(command, tag)` — alias for `Command.system`
|
|
78
|
+
- `Cmd.map(command, &block)` — alias for `Command.map`
|
|
79
|
+
|
|
80
|
+
- **Sync Event Integration**: Runtime now handles `Event::Sync` from `RatatuiRuby::SyntheticEvents`. When a Sync event is received, the runtime waits for all pending async threads and processes their results before continuing. Use `inject_sync` in tests for deterministic async verification.
|
|
81
|
+
|
|
82
|
+
- **Streaming Command Output**: `Command.system` now accepts a `stream:` keyword argument. When `stream: true`, the runtime sends incremental messages (`[:tag, :stdout, line]`, `[:tag, :stderr, line]`) as output arrives, followed by `[:tag, :complete, {status:}]` when the command finishes. Invalid commands send `[:tag, :error, {message:}]`. Default behavior (`stream: false`) remains unchanged.
|
|
83
|
+
|
|
84
|
+
- **Custom Shell Modal Example**: Added `examples/app_fractal_dashboard/bags/custom_shell_modal.rb` demonstrating a 3-bag fractal architecture for a modal that runs arbitrary shell commands with streaming output. Features interleaved stdout/stderr, exit status indication, and Ractor-safe implementation using `tui.overlay` for opaque rendering.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- **Command Module Rename (Breaking)**: The `Cmd` module is now `Command` with Rubyish naming:
|
|
89
|
+
- `Cmd::Quit` → `Command::Exit` (use `Command.exit` factory)
|
|
90
|
+
- `Cmd::Exec` → `Command::System` (use `Command.system(cmd, tag)` factory)
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
### Removed
|
|
95
|
+
|
|
24
96
|
## [0.2.0] - 2026-01-08
|
|
25
97
|
|
|
26
98
|
### Added
|
|
@@ -43,6 +115,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
43
115
|
- **First Release**: Empty release of `ratatui_ruby-tea`, a Ruby implementation of The Elm Architecture (TEA) for `ratatui_ruby`. Scaffolding generated by `ratatui_ruby-devtools`.
|
|
44
116
|
|
|
45
117
|
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/HEAD
|
|
118
|
+
[0.3.1]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.3.1
|
|
119
|
+
[0.3.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.3.0
|
|
46
120
|
[0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
|
|
47
121
|
[0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
|
|
48
122
|
[0.1.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.1.0
|
data/README.md
CHANGED
|
@@ -26,6 +26,23 @@ Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcem
|
|
|
26
26
|
|
|
27
27
|
Please join the **announce** mailing list at https://lists.sr.ht/~kerrick/ratatui_ruby-announce to stay up-to-date on new releases and announcements. See the [`trunk` branch](https://git.sr.ht/~kerrick/ratatui_ruby-tea/tree/trunk) for pre-release updates.
|
|
28
28
|
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Links
|
|
32
|
+
|
|
33
|
+
### The Ecosystem
|
|
34
|
+
|
|
35
|
+
**RatatuiRuby:** [Core engine](https://git.sr.ht/~kerrick/ratatui_ruby) • **Tea:** [MVU architecture](https://git.sr.ht/~kerrick/ratatui_ruby-tea) • **Kit:** [Component architecture](https://git.sr.ht/~kerrick/ratatui_ruby-kit) (Planned) • **DSL:** [Glimmer syntax](https://sr.ht/~kerrick/ratatui_ruby/#chapter-4-the-syntax) (Planned) • **Framework:** [Omakase framework](https://git.sr.ht/~kerrick/ratatui_ruby-framework) (Planned) • **UI:** [Polished widgets](https://git.sr.ht/~kerrick/ratatui_ruby-ui) (Planned) • **UI Pro:** [More polished widgets](https://sr.ht/~kerrick/ratatui_ruby#chapter-6-licensing) (Planned)
|
|
36
|
+
|
|
37
|
+
### For App Developers
|
|
38
|
+
|
|
39
|
+
**Get Started:** [Quickstart](https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/doc/getting_started/quickstart.md) • [Examples](https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples) ⸺ **Stay Informed:** [Announce List](https://lists.sr.ht/~kerrick/ratatui_ruby-announce) • [FAQ](https://man.sr.ht/~kerrick/ratatui_ruby/troubleshooting.md) ⸺ **Reach Out:** [Discuss List](https://lists.sr.ht/~kerrick/ratatui_ruby-discuss) • [Bug Tracker](https://todo.sr.ht/~kerrick/ratatui_ruby)
|
|
40
|
+
|
|
41
|
+
### For Contributors
|
|
42
|
+
|
|
43
|
+
**Get Started:** [Contributing Guide](https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md) • [Code of Conduct](https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md) ⸺ **Stay Informed:** [Announce List](https://lists.sr.ht/~kerrick/ratatui_ruby-announce) • [Project History](https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md) ⸺ **Reach Out:** [Development List](https://lists.sr.ht/~kerrick/ratatui_ruby-devel) • [Bug Tracker](https://todo.sr.ht/~kerrick/ratatui_ruby)
|
|
44
|
+
|
|
45
|
+
---
|
|
29
46
|
|
|
30
47
|
## Compatibility
|
|
31
48
|
|
|
@@ -106,7 +123,7 @@ end
|
|
|
106
123
|
|
|
107
124
|
UPDATE = -> (msg, model) do
|
|
108
125
|
if msg.q? || msg.ctrl_c?
|
|
109
|
-
RatatuiRuby::Tea::
|
|
126
|
+
RatatuiRuby::Tea::Command.exit
|
|
110
127
|
else
|
|
111
128
|
model
|
|
112
129
|
end
|
data/Rakefile
CHANGED
data/Steepfile
CHANGED
|
@@ -5,12 +5,191 @@
|
|
|
5
5
|
|
|
6
6
|
# Application Architecture
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Build robust TUI applications with Tea patterns.
|
|
9
9
|
|
|
10
10
|
## Core Concepts
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
_This section is incomplete. Check the source files._
|
|
13
13
|
|
|
14
14
|
## Thread and Ractor Safety
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
### The Strategic Context
|
|
17
|
+
|
|
18
|
+
Ruby 4.0 introduces [Ractors](https://docs.ruby-lang.org/en/4.0/Ractor.html)—
|
|
19
|
+
true parallel actors that forbid shared mutable state. Code that passes
|
|
20
|
+
mutable objects between threads crashes in a Ractor world.
|
|
21
|
+
|
|
22
|
+
Tea prepares you today. The runtime enforces Ractor-shareability on every
|
|
23
|
+
Model and Message *now*, using standard threads. Pass a mutable object,
|
|
24
|
+
and it raises an error immediately. Write Ractor-safe code today; upgrade
|
|
25
|
+
to Ruby 4.0 without changes tomorrow.
|
|
26
|
+
|
|
27
|
+
Enforce immutability rules before you strictly need them, and the migration
|
|
28
|
+
is invisible.
|
|
29
|
+
|
|
30
|
+
### The Problem
|
|
31
|
+
|
|
32
|
+
Ruby's Ractor model prevents data races by forbidding shared mutable state.
|
|
33
|
+
Mutable objects cause runtime errors:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
RatatuiRuby::Error::Invariant: Model is not Ractor-shareable.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### The Solution
|
|
40
|
+
|
|
41
|
+
Use `Ractor.make_shareable`. It recursively freezes everything:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
Ractor.make_shareable(model.with(text: "#{model.text}#{char}"))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For constants, wrap INITIAL:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
INITIAL = Ractor.make_shareable(
|
|
51
|
+
Model.new(text: "", running: false, chunks: [])
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For collection updates:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
new_chunks = Ractor.make_shareable([*model.chunks, new_item])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### The Lightweight Alternative
|
|
62
|
+
|
|
63
|
+
When you know exactly what's mutable, `.freeze` is shorter:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
[model.with(text: "#{model.text}#{char}".freeze), nil]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Foot-Guns
|
|
70
|
+
|
|
71
|
+
#### frozen_string_literal Only Affects Literals
|
|
72
|
+
|
|
73
|
+
The magic comment freezes strings that appear directly in source code.
|
|
74
|
+
Computed strings are mutable.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# frozen_string_literal: true
|
|
78
|
+
|
|
79
|
+
"literal" # frozen ✓
|
|
80
|
+
"#{var}" # mutable ✗
|
|
81
|
+
str.chop # mutable ✗
|
|
82
|
+
str + other # mutable ✗
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Data.define Needs Shareable Values
|
|
86
|
+
|
|
87
|
+
`Data.define` creates frozen instances. The instance is Ractor-shareable
|
|
88
|
+
only when all its values are shareable.
|
|
89
|
+
|
|
90
|
+
### Quick Reference
|
|
91
|
+
|
|
92
|
+
| Pattern | Code |
|
|
93
|
+
|---------|------|
|
|
94
|
+
| Make anything shareable | `Ractor.make_shareable(obj)` |
|
|
95
|
+
| Freeze a string | `str.freeze` |
|
|
96
|
+
| INITIAL constant | `Ractor.make_shareable(Model.new(...))` |
|
|
97
|
+
| Array update | `Ractor.make_shareable([*old, new])` |
|
|
98
|
+
|
|
99
|
+
### Debugging
|
|
100
|
+
|
|
101
|
+
See this error?
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
RatatuiRuby::Error::Invariant: Model is not Ractor-shareable.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Wrap the returned model with `Ractor.make_shareable`.
|
|
108
|
+
|
|
109
|
+
## Modals and Command Result Routing
|
|
110
|
+
|
|
111
|
+
Modals capture keyboard input. They overlay the main UI and intercept keypresses until dismissed. But async commands keep running in the background. When their results arrive, they look like any other message.
|
|
112
|
+
|
|
113
|
+
It's tempting to intercept *all* messages when the modal is active. This swallows those command results.
|
|
114
|
+
|
|
115
|
+
### The Scenario
|
|
116
|
+
|
|
117
|
+
1. User presses "u" to fetch uptime. The runtime dispatches an async command.
|
|
118
|
+
2. User presses "c" to open a modal dialog.
|
|
119
|
+
3. The uptime command completes. It sends `[:network, :uptime, { stdout:, ... }]`.
|
|
120
|
+
4. The modal intercept sees the modal is active. It routes that message to the modal.
|
|
121
|
+
5. The modal ignores it.
|
|
122
|
+
6. The uptime panel never updates.
|
|
123
|
+
|
|
124
|
+
### The Fix
|
|
125
|
+
|
|
126
|
+
Route command results before modal interception. Modals intercept user input, not async results.
|
|
127
|
+
|
|
128
|
+
<!-- SPDX-SnippetBegin -->
|
|
129
|
+
<!--
|
|
130
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
131
|
+
SPDX-License-Identifier: MIT-0
|
|
132
|
+
-->
|
|
133
|
+
```ruby
|
|
134
|
+
# Wrong: modal intercepts everything
|
|
135
|
+
UPDATE = lambda do |message, model|
|
|
136
|
+
if Modal.active?(model.modal)
|
|
137
|
+
# Swallows command results
|
|
138
|
+
return Modal::UPDATE.call(message, model.modal)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
case message
|
|
142
|
+
in [:network, *rest]
|
|
143
|
+
# Never reached while modal is open
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Correct: route command results first
|
|
148
|
+
UPDATE = lambda do |message, model|
|
|
149
|
+
# 1. Route async command results (always)
|
|
150
|
+
case message
|
|
151
|
+
in [:network, *rest]
|
|
152
|
+
return [model.with(network: new_network), command]
|
|
153
|
+
in [:stats, *rest]
|
|
154
|
+
return [model.with(stats: new_stats), command]
|
|
155
|
+
else
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 2. Modal intercepts user input
|
|
160
|
+
if Modal.active?(model.modal)
|
|
161
|
+
return Modal::UPDATE.call(message, model.modal)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 3. Handle other input
|
|
165
|
+
case message
|
|
166
|
+
in _ if message.q? then Command.exit
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
<!-- SPDX-SnippetEnd -->
|
|
171
|
+
|
|
172
|
+
### The Router DSL
|
|
173
|
+
|
|
174
|
+
`Tea::Router` handles this correctly. Routes declared with `route :prefix, to: ChildModule` process before keymap handlers. Command results flow through even when guards block keyboard input.
|
|
175
|
+
|
|
176
|
+
<!-- SPDX-SnippetBegin -->
|
|
177
|
+
<!--
|
|
178
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
179
|
+
SPDX-License-Identifier: MIT-0
|
|
180
|
+
-->
|
|
181
|
+
```ruby
|
|
182
|
+
module Dashboard
|
|
183
|
+
include Tea::Router
|
|
184
|
+
|
|
185
|
+
route :stats, to: StatsPanel
|
|
186
|
+
route :network, to: NetworkPanel
|
|
187
|
+
|
|
188
|
+
keymap do
|
|
189
|
+
only when: MODAL_INACTIVE do
|
|
190
|
+
key "u", -> { Uptime.fetch_command }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
<!-- SPDX-SnippetEnd -->
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Custom Commands Design (`ratatui_ruby-tea`)
|
|
7
|
+
|
|
8
|
+
This document describes the architectural design and guiding principles of custom commands in `ratatui_ruby-tea`. It is intended for contributors, architects, and AI agents working on the codebase.
|
|
9
|
+
|
|
10
|
+
## Core Abstractions
|
|
11
|
+
|
|
12
|
+
Custom commands extend Tea with user-defined side effects: WebSockets, gRPC, database polling, background workers. The architecture provides four key components:
|
|
13
|
+
|
|
14
|
+
| Component | Purpose |
|
|
15
|
+
|-----------|---------|
|
|
16
|
+
| `Command::Custom` | Mixin for command identification |
|
|
17
|
+
| `Command::Outlet` | Messaging gateway for result delivery |
|
|
18
|
+
| `Command::CancellationToken` | Cooperative cancellation mechanism |
|
|
19
|
+
| `Command.custom { ... }` | Wrapper giving callables unique identity |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Guiding Design Principles
|
|
24
|
+
|
|
25
|
+
### 1. Messaging Gateway over Raw Queue
|
|
26
|
+
|
|
27
|
+
Commands produce messages. Those messages cross threads. Without abstraction, queue manipulation scatters across the codebase.
|
|
28
|
+
|
|
29
|
+
The Outlet wraps the internal queue with a domain-specific API. Commands call `out.put(:tag, data)` instead of managing queue details. Debug mode validates Ractor-shareability automatically—commands cannot accidentally push unshareable data.
|
|
30
|
+
|
|
31
|
+
| Aspect | Raw Queue | Outlet |
|
|
32
|
+
|--------|-----------|--------|
|
|
33
|
+
| Ractor safety | Manual | Automatic |
|
|
34
|
+
| Error messages | Generic | Contextual |
|
|
35
|
+
| API | `queue << Ractor.make_shareable([...])` | `out.put(:tag, data)` |
|
|
36
|
+
| Pattern | Implementation detail | **Messaging Gateway** |
|
|
37
|
+
|
|
38
|
+
This implements the **Messaging Gateway** pattern from Enterprise Integration Patterns. The Outlet is a **Facade** (Gang of Four) over the queue, and acts as a **Channel Adapter** translating command results into update function messages.
|
|
39
|
+
|
|
40
|
+
### 2. Cooperative Cancellation over Thread Termination
|
|
41
|
+
|
|
42
|
+
Long-running commands block the event loop. WebSocket listeners, database pollers, and streaming connections run indefinitely until stopped. Stopping them abruptly corrupts state.
|
|
43
|
+
|
|
44
|
+
`Thread#kill` terminates immediately. Mutexes may deadlock. Resources may leak. Database transactions may abort mid-write.
|
|
45
|
+
|
|
46
|
+
The `CancellationToken` signals cancellation requests. Commands check `token.cancelled?` periodically and stop at safe points. Cleanup code runs. Resources release. Transactions commit.
|
|
47
|
+
|
|
48
|
+
| Aspect | `Thread#kill` | CancellationToken |
|
|
49
|
+
|--------|---------------|------------------|
|
|
50
|
+
| Cleanup | None (immediate termination) | Command controls cleanup |
|
|
51
|
+
| Resource safety | May corrupt state | Clean shutdown |
|
|
52
|
+
| Mutexes | May deadlock | Released properly |
|
|
53
|
+
| Pattern | Forceful | **Cooperative** |
|
|
54
|
+
|
|
55
|
+
**Configurable grace periods** accommodate different cleanup needs:
|
|
56
|
+
|
|
57
|
+
- `0.5` seconds — Quick HTTP abort, minimal cleanup
|
|
58
|
+
- `2.0` seconds — Default, suitable for most commands
|
|
59
|
+
- `5.0` seconds — WebSocket close handshake with remote server
|
|
60
|
+
- `Float::INFINITY` — Never force-kill (database transactions)
|
|
61
|
+
|
|
62
|
+
### 3. Command Identity via Object Reference
|
|
63
|
+
|
|
64
|
+
The runtime tracks active commands by their object identity. When the update function returns `Command.cancel(handle)`, the runtime looks up that exact object in its registry and signals its token.
|
|
65
|
+
|
|
66
|
+
Class-based commands get unique identity automatically—each `MyCommand.new` produces a distinct object. Reusable lambdas and procs share identity. Dispatch them twice, and cancellation would affect both.
|
|
67
|
+
|
|
68
|
+
`Command.custom(callable)` wraps a callable in a unique container. Each call produces a distinct handle. Store it in your model. Cancel it later by returning `Command.cancel(handle)`.
|
|
69
|
+
|
|
70
|
+
### 4. Automatic Error Propagation
|
|
71
|
+
|
|
72
|
+
Commands run in threads. Exceptions bubble up silently. The update function never sees them. Backtraces written to STDERR corrupt the TUI display.
|
|
73
|
+
|
|
74
|
+
The runtime catches unhandled exceptions and pushes `Command::Error` to the queue. This is automatic—commands do not rescue exceptions unless they want custom handling. The update function pattern-matches on `Command::Error` and reacts appropriately.
|
|
75
|
+
|
|
76
|
+
This mirrors the sentinel pattern used for `Command::Exit` and `Command::Cancel`.
|
|
77
|
+
|
|
78
|
+
### 5. Ractor Readiness
|
|
79
|
+
|
|
80
|
+
This design is forward-compatible with Ruby's Ractor-based parallelism.
|
|
81
|
+
|
|
82
|
+
**What's already shareable:**
|
|
83
|
+
|
|
84
|
+
| Pattern | Shareable? | Why |
|
|
85
|
+
|---------|------------|-----|
|
|
86
|
+
| Module-constant lambda | ✅ Yes | `self` is the frozen module |
|
|
87
|
+
| Closure-free block | ✅ Yes | No captured variables |
|
|
88
|
+
| `Data.define` command | ✅ Yes | Immutable by definition |
|
|
89
|
+
| Frozen class instance | ✅ Yes | Deeply frozen |
|
|
90
|
+
|
|
91
|
+
**Debug mode validation:**
|
|
92
|
+
|
|
93
|
+
In debug mode, Tea validates Ractor shareability at dispatch time. `Ractor.shareable?(command)` catches most issues. The Outlet's `put` method validates messages before pushing them to the queue.
|
|
94
|
+
|
|
95
|
+
**Why Thread dispatch is Ractor-safe:**
|
|
96
|
+
|
|
97
|
+
Commands execute in Threads within the **main Ractor**. Only messages (via Outlet) cross the shareability boundary. The `CancellationToken` stays within its Thread—never shared across Ractors. The `@active_commands` hash is local to the Runtime (main Ractor).
|
|
98
|
+
|
|
99
|
+
When Ruby evolves to true Ractor parallelism, this design upgrades transparently: commands are already validated as shareable, so they could be sent to worker Ractors without code changes.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Pattern Lineage
|
|
104
|
+
|
|
105
|
+
This design implements several established patterns from the software architecture literature.
|
|
106
|
+
|
|
107
|
+
### Design Patterns (Gang of Four)
|
|
108
|
+
|
|
109
|
+
| Component | Pattern |
|
|
110
|
+
|-----------|---------|
|
|
111
|
+
| Custom Mixin | **Command** — Encapsulates a request as an object |
|
|
112
|
+
| Outlet | **Facade** — Simplified interface to a complex subsystem |
|
|
113
|
+
| Runtime | **Mediator** — Coordinates command dispatch and message routing |
|
|
114
|
+
| Update Function | **State** — Behavior changes based on internal state |
|
|
115
|
+
|
|
116
|
+
### Enterprise Integration Patterns (Hohpe & Woolf)
|
|
117
|
+
|
|
118
|
+
| Component | Pattern |
|
|
119
|
+
|-----------|---------|
|
|
120
|
+
| Outlet | **Messaging Gateway** — Wraps message channel access |
|
|
121
|
+
| Outlet | **Channel Adapter** — Connects applications to messaging systems |
|
|
122
|
+
| Queue | **Point-to-Point Channel** — Single consumer receives each message |
|
|
123
|
+
|
|
124
|
+
### Cancellation Patterns
|
|
125
|
+
|
|
126
|
+
| Platform | Mechanism | Relationship |
|
|
127
|
+
|----------|-----------|--------------|
|
|
128
|
+
| **.NET** | `CancellationToken` | Direct inspiration for API design |
|
|
129
|
+
| **Go** | `context.Context` | Cooperative cancellation via `ctx.Done()` |
|
|
130
|
+
| **JavaScript** | `AbortController` | Web Fetch API cancellation |
|
|
131
|
+
| **Java** | `Thread.interrupt()` | Flag-based cooperative cancellation |
|
|
132
|
+
| **Kotlin** | `Job.cancel()` | Coroutine cancellation |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Prior Art
|
|
137
|
+
|
|
138
|
+
### The Elm Architecture (TEA)
|
|
139
|
+
|
|
140
|
+
| Framework | Language | Notes |
|
|
141
|
+
|-----------|----------|-------|
|
|
142
|
+
| **Elm** | Elm | Original TEA; `Cmd` + `Sub` primitives |
|
|
143
|
+
| **BubbleTea** | Go | TUI implementation; "Subscriptions are Loops" philosophy |
|
|
144
|
+
| **Iced** | Rust | GUI TEA with Command + Subscription |
|
|
145
|
+
| **Miso** | Haskell | Web TEA with Effect + Sub |
|
|
146
|
+
| **Bolero** | F# | Web TEA with Cmd + Sub |
|
|
147
|
+
|
|
148
|
+
### Redux Ecosystem
|
|
149
|
+
|
|
150
|
+
| Library | Pattern | Mapping |
|
|
151
|
+
|---------|---------|---------|
|
|
152
|
+
| **Redux Thunk** | Raw dispatch access | Direct queue access (rejected) |
|
|
153
|
+
| **Redux Saga** | `put()` effect dispatches actions | **Outlet.put** ← adopted |
|
|
154
|
+
| **Redux Observable** | RxJS Observables | RxRuby (rejected for complexity) |
|
|
155
|
+
| **redux-loop** | Elm-style Cmd | Recursive commands |
|
|
156
|
+
|
|
157
|
+
Redux Saga's `put()` is the direct inspiration for `out.put()`. The Saga pattern—long-running processes that listen for actions and dispatch new ones—maps directly to Tea's custom commands.
|
|
158
|
+
|
|
159
|
+
### Ruby Ecosystem
|
|
160
|
+
|
|
161
|
+
| Library | Pattern | Relationship |
|
|
162
|
+
|---------|---------|--------------|
|
|
163
|
+
| **Observable** (stdlib) | Observer pattern | Similar push semantics, lacks thread-safety |
|
|
164
|
+
| **Concurrent Ruby** | Actor, Promises | More complex than needed |
|
|
165
|
+
| **Celluloid** | Actor mailboxes | Inspiration for internal terminology |
|
|
166
|
+
| **Sidekiq** | Background jobs | Similar "fire command, receive result" model |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Key Influences Summary
|
|
171
|
+
|
|
172
|
+
| Influence | What We Adopted |
|
|
173
|
+
|-----------|-----------------|
|
|
174
|
+
| **Elm** | MVU architecture, Cmd/Msg pattern |
|
|
175
|
+
| **BubbleTea** | "Subscriptions are Loops" philosophy |
|
|
176
|
+
| **Redux Saga** | `put()` for message dispatch |
|
|
177
|
+
| **.NET CancellationToken** | Cooperative cancellation with grace periods |
|
|
178
|
+
| **EIP Messaging Gateway** | Outlet as infrastructure abstraction |
|
|
179
|
+
| **Go context.Context** | Cancellation propagation pattern |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Further Reading
|
|
184
|
+
|
|
185
|
+
### External Resources
|
|
186
|
+
|
|
187
|
+
- [Elm Guide](https://guide.elm-lang.org/) — Official Elm documentation covering commands and effects
|
|
188
|
+
- [BubbleTea](https://github.com/charmbracelet/bubbletea) — Go TUI framework based on The Elm Architecture
|
|
189
|
+
- [Redux Saga](https://redux-saga.js.org/) — Saga pattern and `put()` effect documentation
|
|
190
|
+
- [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/) — Messaging patterns reference by Hohpe & Woolf
|
|
191
|
+
|
|
192
|
+
### Academic References
|
|
193
|
+
|
|
194
|
+
1. **Gamma, Helm, Johnson, Vlissides** (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
|
|
195
|
+
- Command, Facade, Mediator, State patterns
|
|
196
|
+
|
|
197
|
+
2. **Fowler, M.** (2002). *Patterns of Enterprise Application Architecture*. Addison-Wesley.
|
|
198
|
+
- Gateway pattern
|
|
199
|
+
|
|
200
|
+
3. **Hohpe, G. & Woolf, B.** (2003). *Enterprise Integration Patterns*. Addison-Wesley.
|
|
201
|
+
- Messaging Gateway, Message Channel, Point-to-Point Channel, Channel Adapter
|
|
202
|
+
|
|
203
|
+
4. **Joshi, U.** (2023). *Patterns of Distributed Systems*. Addison-Wesley.
|
|
204
|
+
- Write-Ahead Log, Thread Pool
|