ratatui_ruby-tea 0.1.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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +4 -2
- data/.builds/ruby-3.3.yml +4 -2
- data/.builds/ruby-3.4.yml +4 -2
- data/.builds/ruby-4.0.0.yml +4 -2
- data/.rubocop.yml +1 -1
- data/AGENTS.md +14 -6
- data/CHANGELOG.md +72 -1
- data/README.md +41 -2
- data/REUSE.toml +3 -3
- data/Rakefile +1 -1
- data/Steepfile +13 -0
- data/doc/concepts/application_architecture.md +182 -3
- data/doc/contributors/priorities.md +40 -0
- data/doc/custom.css +1 -1
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_cmd_exec.png +0 -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 +51 -0
- data/examples/verify_readme_usage/app.rb +44 -0
- data/examples/widget_command_system/README.md +70 -0
- data/examples/widget_command_system/app.rb +132 -0
- data/lib/ratatui_ruby/tea/command.rb +145 -0
- data/lib/ratatui_ruby/tea/router.rb +337 -0
- data/lib/ratatui_ruby/tea/runtime.rb +219 -0
- data/lib/ratatui_ruby/tea/shortcuts.rb +51 -0
- data/lib/ratatui_ruby/tea/version.rb +1 -1
- data/lib/ratatui_ruby/tea.rb +75 -8
- data/mise.toml +2 -1
- data/sig/examples/verify_readme_usage/app.rbs +19 -0
- data/sig/examples/widget_command_system/app.rbs +26 -0
- data/sig/ratatui_ruby/tea/command.rbs +47 -0
- data/sig/ratatui_ruby/tea/router.rbs +99 -0
- data/sig/ratatui_ruby/tea/runtime.rbs +26 -0
- data/sig/ratatui_ruby/tea.rbs +16 -0
- data/tasks/example_viewer.html.erb +1 -1
- data/tasks/resources/build.yml.erb +1 -8
- data/tasks/resources/index.html.erb +1 -1
- data/tasks/resources/rubies.yml +1 -1
- metadata +48 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d10ee51cb79e58e9b24dbf78e696dcbc901ddedac44dce824c4cf8817e8b07a3
|
|
4
|
+
data.tar.gz: 1043f33f9e3804748ea3fdd5eef4783b7af3a81ed51b10ce88f3447953e1874e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e25205b29e1654964c88455f9a9fed89ee3464aacbecc458e128baae8bf0a788196e9c1fbdee9cea3549b0e9f60cf1bef5c48c870baa57af453759ad4625453b
|
|
7
|
+
data.tar.gz: 121c207a7c82109eafbcdbe7ebae838cccb33f54a0f6ff96c75832294e07496284c26c89fdf7bf0f871883959951414ab52582d209c52b1fb00249a0b009ab9f
|
data/.builds/ruby-3.2.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
-
# SPDX-License-Identifier:
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
image: archlinux
|
|
5
5
|
packages:
|
|
@@ -13,9 +13,10 @@ packages:
|
|
|
13
13
|
- gdbm
|
|
14
14
|
- ncurses
|
|
15
15
|
- libffi
|
|
16
|
+
- clang
|
|
16
17
|
- git
|
|
17
18
|
artifacts:
|
|
18
|
-
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.
|
|
19
|
+
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.2.0.gem
|
|
19
20
|
sources:
|
|
20
21
|
- https://git.sr.ht/~kerrick/ratatui_ruby-tea
|
|
21
22
|
tasks:
|
|
@@ -25,6 +26,7 @@ tasks:
|
|
|
25
26
|
echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
|
|
26
27
|
echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
|
|
27
28
|
echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
|
|
29
|
+
echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
|
|
28
30
|
. ~/.buildenv
|
|
29
31
|
export CI="true"
|
|
30
32
|
cd ratatui_ruby-tea
|
data/.builds/ruby-3.3.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
-
# SPDX-License-Identifier:
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
image: archlinux
|
|
5
5
|
packages:
|
|
@@ -13,9 +13,10 @@ packages:
|
|
|
13
13
|
- gdbm
|
|
14
14
|
- ncurses
|
|
15
15
|
- libffi
|
|
16
|
+
- clang
|
|
16
17
|
- git
|
|
17
18
|
artifacts:
|
|
18
|
-
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.
|
|
19
|
+
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.2.0.gem
|
|
19
20
|
sources:
|
|
20
21
|
- https://git.sr.ht/~kerrick/ratatui_ruby-tea
|
|
21
22
|
tasks:
|
|
@@ -25,6 +26,7 @@ tasks:
|
|
|
25
26
|
echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
|
|
26
27
|
echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
|
|
27
28
|
echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
|
|
29
|
+
echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
|
|
28
30
|
. ~/.buildenv
|
|
29
31
|
export CI="true"
|
|
30
32
|
cd ratatui_ruby-tea
|
data/.builds/ruby-3.4.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
-
# SPDX-License-Identifier:
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
image: archlinux
|
|
5
5
|
packages:
|
|
@@ -13,9 +13,10 @@ packages:
|
|
|
13
13
|
- gdbm
|
|
14
14
|
- ncurses
|
|
15
15
|
- libffi
|
|
16
|
+
- clang
|
|
16
17
|
- git
|
|
17
18
|
artifacts:
|
|
18
|
-
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.
|
|
19
|
+
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.2.0.gem
|
|
19
20
|
sources:
|
|
20
21
|
- https://git.sr.ht/~kerrick/ratatui_ruby-tea
|
|
21
22
|
tasks:
|
|
@@ -25,6 +26,7 @@ tasks:
|
|
|
25
26
|
echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
|
|
26
27
|
echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
|
|
27
28
|
echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
|
|
29
|
+
echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
|
|
28
30
|
. ~/.buildenv
|
|
29
31
|
export CI="true"
|
|
30
32
|
cd ratatui_ruby-tea
|
data/.builds/ruby-4.0.0.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
-
# SPDX-License-Identifier:
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
image: archlinux
|
|
5
5
|
packages:
|
|
@@ -13,9 +13,10 @@ packages:
|
|
|
13
13
|
- gdbm
|
|
14
14
|
- ncurses
|
|
15
15
|
- libffi
|
|
16
|
+
- clang
|
|
16
17
|
- git
|
|
17
18
|
artifacts:
|
|
18
|
-
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.
|
|
19
|
+
- ratatui_ruby-tea/pkg/ratatui_ruby-tea-0.2.0.gem
|
|
19
20
|
sources:
|
|
20
21
|
- https://git.sr.ht/~kerrick/ratatui_ruby-tea
|
|
21
22
|
tasks:
|
|
@@ -25,6 +26,7 @@ tasks:
|
|
|
25
26
|
echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
|
|
26
27
|
echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
|
|
27
28
|
echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
|
|
29
|
+
echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
|
|
28
30
|
. ~/.buildenv
|
|
29
31
|
export CI="true"
|
|
30
32
|
cd ratatui_ruby-tea
|
data/.rubocop.yml
CHANGED
data/AGENTS.md
CHANGED
|
@@ -15,22 +15,29 @@ Description: Part of the RatatuiRuby ecosystem.
|
|
|
15
15
|
|
|
16
16
|
### STRICT REQUIREMENTS
|
|
17
17
|
|
|
18
|
-
- Every file MUST begin with an SPDX-compliant header. Use `
|
|
18
|
+
- Every file MUST begin with an SPDX-compliant header. Use `LGPL-3.0-or-later` for code; `CC-BY-SA-4.0` for documentation. `reuse annotate` can help you generate the header. **For Ruby files**, wrap SPDX comments in `#--` / `#++` to hide them from RDoc output.
|
|
19
19
|
- Every line of Ruby MUST be covered by tests that would stand up to mutation testing.
|
|
20
20
|
- Tests must be meaningful and verify specific behavior or rendering output; simply verifying that code "doesn't crash" is insufficient and unacceptable.
|
|
21
|
-
- **Pre-commit:** Use `agent_rake` to ensure commit-readiness. See Tools for detailed instructions.
|
|
21
|
+
- **Pre-commit:** Use `bundle exec agent_rake` to ensure commit-readiness. See Tools for detailed instructions.
|
|
22
22
|
- **Git Pager:** ALWAYS set `PAGER=cat` for ALL `git` commands (e.g., `PAGER=cat git diff`). This is mandatory.
|
|
23
23
|
|
|
24
24
|
### Tools
|
|
25
25
|
|
|
26
26
|
- **NEVER** run `bundle exec rake` directly. **NEVER** run `bundle exec ruby -Ilib:test ...` directly.
|
|
27
|
-
- **ALWAYS use `agent_rake`** (provided by the `ratatui_ruby-devtools` gem) for running tests, linting, or checking compilation.
|
|
27
|
+
- **ALWAYS use `bundle exec agent_rake`** (provided by the `ratatui_ruby-devtools` gem) for running tests, linting, or checking compilation.
|
|
28
28
|
- **Usage:**
|
|
29
|
-
- Runs default task (compile + test + lint): `agent_rake`
|
|
30
|
-
- Runs specific task: `agent_rake test:ruby` (for example)
|
|
29
|
+
- Runs default task (compile + test + lint): `bundle exec agent_rake`
|
|
30
|
+
- Runs specific task: `bundle exec agent_rake test:ruby` (for example)
|
|
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+).
|
|
@@ -50,7 +57,8 @@ Description: Part of the RatatuiRuby ecosystem.
|
|
|
50
57
|
|
|
51
58
|
Before considering a task complete:
|
|
52
59
|
|
|
53
|
-
|
|
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.
|
|
54
62
|
2. **Documentation Updated:** If public APIs changed, update relevant docs.
|
|
55
63
|
3. **Changelog Updated:** If public APIs changed, update CHANGELOG.md's **Unreleased** section.
|
|
56
64
|
4. **Commit Message Suggested:** Include a suggested commit message block.
|
data/CHANGELOG.md
CHANGED
|
@@ -15,4 +15,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
|
|
16
16
|
### Added
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
## [0.3.0] - 2026-01-08
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Router DSL**: New `Tea::Router` module provides declarative routing for Fractal Architecture:
|
|
29
|
+
- `route :prefix, to: ChildBag` — declares a child bag route
|
|
30
|
+
- `keymap { key "q", -> { Command.exit } }` — declares keyboard handlers
|
|
31
|
+
- `keymap { key "x", handler, when: -> (m) { m.ready? } }` — guards (also: `if:`, `only:`, `guard:`, `unless:`, `except:`, `skip:`)
|
|
32
|
+
- `keymap { only when: guard do ... end }` — nested guard blocks apply to all keys within (also: `skip when: ...`)
|
|
33
|
+
- `mousemap { click -> (x, y) { ... } }` — declares mouse handlers
|
|
34
|
+
- `action :name, handler` — declares reusable actions for key/mouse handlers
|
|
35
|
+
- `from_router` — generates an UPDATE lambda from routes and handlers
|
|
36
|
+
|
|
37
|
+
- **Composition Helpers**: New helper methods for Fractal Architecture reduce boilerplate:
|
|
38
|
+
- `Tea.route(command, :prefix)` — wraps a command to route results to a child bag
|
|
39
|
+
- `Tea.delegate(message, :prefix, child_update, child_model)` — dispatches prefixed messages to child bags
|
|
40
|
+
|
|
41
|
+
- **Command Mapping**: `Command.map(inner_command, &mapper)` wraps a child command and transforms its result message. Essential for parent bags routing child command results.
|
|
42
|
+
|
|
43
|
+
- **Shortcuts Module**: `require "ratatui_ruby/tea/shortcuts"` and `include Tea::Shortcuts` for short aliases:
|
|
44
|
+
- `Cmd.exit` — alias for `Command.exit`
|
|
45
|
+
- `Cmd.sh(command, tag)` — alias for `Command.system`
|
|
46
|
+
- `Cmd.map(command, &block)` — alias for `Command.map`
|
|
47
|
+
|
|
48
|
+
- **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.
|
|
49
|
+
|
|
50
|
+
- **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.
|
|
51
|
+
|
|
52
|
+
- **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.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **Command Module Rename (Breaking)**: The `Cmd` module is now `Command` with Rubyish naming:
|
|
57
|
+
- `Cmd::Quit` → `Command::Exit` (use `Command.exit` factory)
|
|
58
|
+
- `Cmd::Exec` → `Command::System` (use `Command.system(cmd, tag)` factory)
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
### Removed
|
|
63
|
+
|
|
64
|
+
## [0.2.0] - 2026-01-08
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- **The Elm Architecture (TEA)**: Implemented the core Model-View-Update (MVU) runtime. Use `RatatuiRuby::Tea.run(model, view: ..., update: ...)` to start an interactive application with predictable state management.
|
|
69
|
+
- **Async Command System**: Side effects (database, HTTP, shell) are executed asynchronously in a thread pool. Results are dispatched back to the main loop as messages, ensuring the UI never freezes.
|
|
70
|
+
- **Ractor Safety Enforcement**: The runtime strictly enforces that all `Model` and `Message` objects are Ractor-shareable (deeply frozen). This guarantees thread safety by design and prepares for future parallelism.
|
|
71
|
+
- **Flexible Update Returns**: The `update` function supports multiple return signatures for developer ergonomics:
|
|
72
|
+
- `[Model, Cmd]` — Standard tuple.
|
|
73
|
+
- `Model` — Implicitly `[Model, Cmd::None]`.
|
|
74
|
+
- `Cmd` — Implicitly `[CurrentModel, Cmd]`.
|
|
75
|
+
- **Startup Commands**: `RatatuiRuby::Tea.run` accepts an `init:` parameter to dispatch an initial command immediately after startup, useful for loading initial data without blocking the first render.
|
|
76
|
+
- **View Validation**: The `view` function must return a valid widget. Returning `nil` raises `RatatuiRuby::Error::Invariant` to catch bugs early.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## [0.1.0] - 2026-01-07
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **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`.
|
|
84
|
+
|
|
85
|
+
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/HEAD
|
|
86
|
+
[0.3.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.3.0
|
|
87
|
+
[0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
|
|
88
|
+
[0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
|
|
89
|
+
[0.1.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.1.0
|
data/README.md
CHANGED
|
@@ -80,7 +80,46 @@ gem install ratatui_ruby-tea
|
|
|
80
80
|
|
|
81
81
|
## Usage
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
**ratatui_ruby-tea** uses the Model-View-Update (MVU) pattern. You provide an immutable model, a view function, and an update function.
|
|
84
|
+
|
|
85
|
+
<!-- SPDX-SnippetBegin -->
|
|
86
|
+
<!--
|
|
87
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
88
|
+
SPDX-License-Identifier: MIT-0
|
|
89
|
+
-->
|
|
90
|
+
<!-- SYNC:START:examples/verify_readme_usage/app.rb:mvu -->
|
|
91
|
+
```ruby
|
|
92
|
+
Model = Data.define(:text)
|
|
93
|
+
MODEL = Model.new(text: "Hello, Ratatui! Press 'q' to quit.")
|
|
94
|
+
|
|
95
|
+
VIEW = -> (model, tui) do
|
|
96
|
+
tui.paragraph(
|
|
97
|
+
text: model.text,
|
|
98
|
+
alignment: :center,
|
|
99
|
+
block: tui.block(
|
|
100
|
+
title: "My Ruby TUI App",
|
|
101
|
+
borders: [:all],
|
|
102
|
+
border_style: { fg: "cyan" }
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
UPDATE = -> (msg, model) do
|
|
108
|
+
if msg.q? || msg.ctrl_c?
|
|
109
|
+
RatatuiRuby::Tea::Command.exit
|
|
110
|
+
else
|
|
111
|
+
model
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def run
|
|
116
|
+
RatatuiRuby::Tea.run(model: MODEL, view: VIEW, update: UPDATE)
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
<!-- SYNC:END -->
|
|
120
|
+
<!-- SPDX-SnippetEnd -->
|
|
121
|
+
|
|
122
|
+

|
|
84
123
|
|
|
85
124
|
For a full tutorial, see [the Quickstart](./doc/getting_started/quickstart.md). For an explanation of the application architecture, see [Application Architecture](./doc/concepts/application_architecture.md).
|
|
86
125
|
|
|
@@ -117,7 +156,7 @@ Want to help develop **ratatui_ruby-tea**? Check out the [contribution guide on
|
|
|
117
156
|
|
|
118
157
|
The library is [LGPL-3.0-or-later](./LICENSES/LGPL-3.0-or-later.txt): you can use it in proprietary applications, but you must share changes you make to **ratatui_ruby-tea** itself. Documentation snippets and widget examples are [MIT-0](./LICENSES/MIT-0.txt): copy and use them without attribution.
|
|
119
158
|
|
|
120
|
-
Documentation is [CC-BY-SA-4.0](./LICENSES/CC-BY-SA-4.0.txt). Build tooling and full app examples are [
|
|
159
|
+
Documentation is [CC-BY-SA-4.0](./LICENSES/CC-BY-SA-4.0.txt). Build tooling and full app examples are [LGPL-3.0-or-later](./LICENSES/LGPL-3.0-or-later.txt). See each file's SPDX comment for specifics.
|
|
121
160
|
|
|
122
161
|
Some parts of this program are copied from other sources under appropriate reuse licenses, and the copyright belongs to their respective owners. See the [REUSE Specification – Version 3.3](https://reuse.software/spec-3.3/) for details.
|
|
123
162
|
|
data/REUSE.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
2
|
-
# SPDX-License-Identifier:
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
version = 1
|
|
5
5
|
|
|
@@ -11,12 +11,12 @@ SPDX-License-Identifier = "CC0-1.0"
|
|
|
11
11
|
[[annotations]]
|
|
12
12
|
path = '**/snapshots/*.txt'
|
|
13
13
|
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
14
|
-
SPDX-License-Identifier = "
|
|
14
|
+
SPDX-License-Identifier = "LGPL-3.0-or-later"
|
|
15
15
|
|
|
16
16
|
[[annotations]]
|
|
17
17
|
path = '**/snapshots/*.ansi'
|
|
18
18
|
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
19
|
-
SPDX-License-Identifier = "
|
|
19
|
+
SPDX-License-Identifier = "LGPL-3.0-or-later"
|
|
20
20
|
|
|
21
21
|
[[annotations]]
|
|
22
22
|
path = 'doc/images/*'
|
data/Rakefile
CHANGED
data/Steepfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
target :lib do
|
|
7
|
+
signature "sig"
|
|
8
|
+
check "lib"
|
|
9
|
+
|
|
10
|
+
library "pathname"
|
|
11
|
+
library "fileutils"
|
|
12
|
+
library "minitest"
|
|
13
|
+
end
|
|
@@ -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,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Feature Priorities
|
|
7
|
+
|
|
8
|
+
This document outlines the critical next steps for `ratatui_ruby-tea`. Each item explains the context, the problem, and the solution, following our [Documentation Style](../ratatui_ruby/doc/contributors/documentation_style.md).
|
|
9
|
+
|
|
10
|
+
## 1. Composition (Cmd.map)
|
|
11
|
+
|
|
12
|
+
**Context:** Real applications grow. You start with a file picker. Then a modal. Then a sidebar. Each component has its own model and update function.
|
|
13
|
+
|
|
14
|
+
**Problem:** The Tea architecture naturally isolates components. A parent model holds a child model. But when the child update function returns a message (like `:selected`), the parent `update` function only understands its own messages. It cannot "hear" the child. The architecture breaks at the boundary of the first file.
|
|
15
|
+
|
|
16
|
+
**Solution:** Implement `Cmd.map(cmd) { |child_msg| ... }`. This wraps the child's effect. When the effect completes, the runtime passes the result through the block, transforming the child's message into a parent's message. This restores the flow of data up the tree.
|
|
17
|
+
|
|
18
|
+
## 2. Parallelism (Cmd.batch)
|
|
19
|
+
|
|
20
|
+
**Context:** Applications often need to do two things at once. You initialize the app. You need to load the config *and* fetch the latest data *and* start the tick timer.
|
|
21
|
+
|
|
22
|
+
**Problem:** The `update` function returns a single tuple `[Model, Cmd]`. It cannot return `[Model, Cmd1, Cmd2]`. Without a way to group them, you are forced to sequence independent operations, making the UI feel slow and linear.
|
|
23
|
+
|
|
24
|
+
**Solution:** Implement `Cmd.batch([cmd1, cmd2, ...])`. This command takes an array of commands and submits them all to the runtime. The runtime executes them in parallel (where possible) or concurrently.
|
|
25
|
+
|
|
26
|
+
## 3. Serial Execution (Cmd.sequence)
|
|
27
|
+
|
|
28
|
+
**Context:** Some effects depend on others. You cannot read a file until you have downloaded it. You cannot query the database until you have opened the connection.
|
|
29
|
+
|
|
30
|
+
**Problem:** The Tea architecture relies on async messages. You send a command, and *eventually* you get a message. To chain actions, you must handle the first success message in `update`, then return the second command. This smears a single logical transaction across multiple independent `case` clauses, creating "callback hell" but in the shape of a state machine.
|
|
31
|
+
|
|
32
|
+
**Solution:** Implement `Cmd.sequence([cmd1, cmd2, ...])`. This command executes the first command. If successful, it runs the next. If any fail, it stops. Note: This assumes commands have a standard "success/failure" result shape, or simply runs them blindly. (Design decision required: does `sequence` wait for the message, or just the execution?)
|
|
33
|
+
|
|
34
|
+
## 4. Time (Cmd.tick)
|
|
35
|
+
|
|
36
|
+
**Context:** Animations and real-time updates. A spinner rotating. A clock ticking. Evaluation metrics updating live.
|
|
37
|
+
|
|
38
|
+
**Problem:** The runtime blocks on input. If the user doesn't type or click, the screen stays frozen. You cannot implement a simple "Loading..." spinner because the frame never updates.
|
|
39
|
+
|
|
40
|
+
**Solution:** Implement `Cmd.tick(interval, tag)`. This command sleeps for the interval and then sends a message. The `update` function handles the message and returns the *same* tick command again. This creates a recursive loop, driving the frame rate independent of user input.
|
data/doc/custom.css
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Cmd.map Fractal Dashboard
|
|
7
|
+
|
|
8
|
+
Demonstrates **Fractal Architecture** using `Cmd.map` for component composition.
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Without composition, a complex app needs one giant `case` statement handling every possible message from every child—the "God Reducer" anti-pattern. This doesn't scale.
|
|
13
|
+
|
|
14
|
+
## Solution
|
|
15
|
+
|
|
16
|
+
`Cmd.map` wraps child commands so their results route through parents:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Child produces [:system_info, {stdout:, ...}]
|
|
20
|
+
child_cmd = SystemInfoWidget.fetch_cmd
|
|
21
|
+
|
|
22
|
+
# Parent wraps to produce [:stats, :system_info, {...}]
|
|
23
|
+
parent_cmd = Cmd.map(child_cmd) { |m| [:stats, *m] }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each layer handles only its own messages. Parents pattern-match on the first element to route to the correct child.
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Dashboard (root)
|
|
32
|
+
├── StatsPanel
|
|
33
|
+
│ ├── SystemInfoWidget → Cmd.exec("uname -a", :system_info)
|
|
34
|
+
│ └── DiskUsageWidget → Cmd.exec("df -h", :disk_usage)
|
|
35
|
+
└── NetworkPanel
|
|
36
|
+
├── PingWidget → Cmd.exec("ping -c 1 localhost", :ping)
|
|
37
|
+
└── UptimeWidget → Cmd.exec("uptime", :uptime)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Hotkeys
|
|
41
|
+
|
|
42
|
+
| Key | Action |
|
|
43
|
+
|-----|--------|
|
|
44
|
+
| `s` | Fetch system info |
|
|
45
|
+
| `d` | Fetch disk usage |
|
|
46
|
+
| `p` | Ping localhost |
|
|
47
|
+
| `u` | Fetch uptime |
|
|
48
|
+
| `q` | Quit |
|
|
49
|
+
|
|
50
|
+
## Key Concepts
|
|
51
|
+
|
|
52
|
+
1. **Widget isolation**: Each widget has its own `Model`, `UPDATE`, and `fetch_cmd`. It knows nothing about parents.
|
|
53
|
+
2. **Message routing**: Parents prefix child messages (`:stats`, `:network`) and pattern-match to route.
|
|
54
|
+
3. **Recursive dispatch**: `Cmd.map` delegates inner command execution to the runtime, then transforms the result.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ruby examples/widget_cmd_map/app.rb
|
|
60
|
+
```
|