ratatui_ruby-tea 0.1.0 → 0.2.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 +6 -6
- data/CHANGELOG.md +31 -1
- data/README.md +41 -2
- data/REUSE.toml +3 -3
- data/Rakefile +1 -1
- data/Steepfile +13 -0
- 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/verify_readme_usage/README.md +51 -0
- data/examples/verify_readme_usage/app.rb +44 -0
- data/examples/widget_cmd_exec/README.md +70 -0
- data/examples/widget_cmd_exec/app.rb +132 -0
- data/lib/ratatui_ruby/tea/cmd.rb +88 -0
- data/lib/ratatui_ruby/tea/runtime.rb +159 -0
- data/lib/ratatui_ruby/tea/version.rb +1 -1
- data/lib/ratatui_ruby/tea.rb +17 -8
- data/mise.toml +2 -1
- data/sig/examples/verify_readme_usage/app.rbs +19 -0
- data/sig/examples/widget_cmd_exec/app.rbs +26 -0
- data/sig/ratatui_ruby/tea/cmd.rbs +32 -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 +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1bf31fb6ff8bacdcd6f9c54e794e0c769f5c52afde2c5e4d78cbee48be36b12
|
|
4
|
+
data.tar.gz: 1bd65539c4eace89fc1d58d857d81fb9cc03187ea7ae770d4b5c4a051ae407a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 413f42942963ef06433b683b2cf3820db1a8cd679fcf263aaccc4cbd3780fa01b7bcc66842aeaad7606377380530215a9b6a07a145d6df56ecf34a5053c998cb
|
|
7
|
+
data.tar.gz: 8edaf06fbf149723ca70b2f27157c8c2d22536f060c94facfb6d904223b48dc21613daa0a130237548a40d705074ec22f90403e27855b1b578bad0e770f27040
|
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,19 +15,19 @@ 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
|
|
|
@@ -50,7 +50,7 @@ Description: Part of the RatatuiRuby ecosystem.
|
|
|
50
50
|
|
|
51
51
|
Before considering a task complete:
|
|
52
52
|
|
|
53
|
-
1. **Default Rake Task Passes:** Run `agent_rake` (no args). Confirm it passes with ZERO errors.
|
|
53
|
+
1. **Default Rake Task Passes:** Run `bundle exec agent_rake` (no args). Confirm it passes with ZERO errors.
|
|
54
54
|
2. **Documentation Updated:** If public APIs changed, update relevant docs.
|
|
55
55
|
3. **Changelog Updated:** If public APIs changed, update CHANGELOG.md's **Unreleased** section.
|
|
56
56
|
4. **Commit Message Suggested:** Include a suggested commit message block.
|
data/CHANGELOG.md
CHANGED
|
@@ -15,4 +15,34 @@ 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.2.0] - 2026-01-08
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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.
|
|
31
|
+
- **Flexible Update Returns**: The `update` function supports multiple return signatures for developer ergonomics:
|
|
32
|
+
- `[Model, Cmd]` — Standard tuple.
|
|
33
|
+
- `Model` — Implicitly `[Model, Cmd::None]`.
|
|
34
|
+
- `Cmd` — Implicitly `[CurrentModel, Cmd]`.
|
|
35
|
+
- **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.
|
|
36
|
+
- **View Validation**: The `view` function must return a valid widget. Returning `nil` raises `RatatuiRuby::Error::Invariant` to catch bugs early.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## [0.1.0] - 2026-01-07
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **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
|
+
|
|
45
|
+
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/HEAD
|
|
46
|
+
[0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
|
|
47
|
+
[0.2.0]: https://git.sr.ht/~kerrick/ratatui_ruby-tea/refs/v0.2.0
|
|
48
|
+
[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::Cmd.quit
|
|
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
|
|
@@ -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,51 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# README Usage Verification
|
|
7
|
+
|
|
8
|
+
Verifies the primary usage example for the Tea gem.
|
|
9
|
+
|
|
10
|
+
This example exists as a documentation regression test. It ensures that the very first MVU pattern a user sees actually works.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
<!-- SPDX-SnippetBegin -->
|
|
15
|
+
<!--
|
|
16
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
17
|
+
SPDX-License-Identifier: MIT-0
|
|
18
|
+
-->
|
|
19
|
+
<!-- SYNC:START:./app.rb:mvu -->
|
|
20
|
+
```ruby
|
|
21
|
+
Model = Data.define(:text)
|
|
22
|
+
MODEL = Model.new(text: "Hello, Ratatui! Press 'q' to quit.")
|
|
23
|
+
|
|
24
|
+
VIEW = -> (model, tui) do
|
|
25
|
+
tui.paragraph(
|
|
26
|
+
text: model.text,
|
|
27
|
+
alignment: :center,
|
|
28
|
+
block: tui.block(
|
|
29
|
+
title: "My Ruby TUI App",
|
|
30
|
+
borders: [:all],
|
|
31
|
+
border_style: { fg: "cyan" }
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
UPDATE = -> (msg, model) do
|
|
37
|
+
if msg.q? || msg.ctrl_c?
|
|
38
|
+
RatatuiRuby::Tea::Cmd.quit
|
|
39
|
+
else
|
|
40
|
+
model
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run
|
|
45
|
+
RatatuiRuby::Tea.run(model: MODEL, view: VIEW, update: UPDATE)
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
<!-- SYNC:END -->
|
|
49
|
+
<!-- SPDX-SnippetEnd -->
|
|
50
|
+
|
|
51
|
+
[](../../README.md#usage)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: MIT-0
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
9
|
+
|
|
10
|
+
require "ratatui_ruby"
|
|
11
|
+
require "ratatui_ruby/tea"
|
|
12
|
+
|
|
13
|
+
class VerifyReadmeUsage
|
|
14
|
+
# [SYNC:START:mvu]
|
|
15
|
+
Model = Data.define(:text)
|
|
16
|
+
MODEL = Model.new(text: "Hello, Ratatui! Press 'q' to quit.")
|
|
17
|
+
|
|
18
|
+
VIEW = -> (model, tui) do
|
|
19
|
+
tui.paragraph(
|
|
20
|
+
text: model.text,
|
|
21
|
+
alignment: :center,
|
|
22
|
+
block: tui.block(
|
|
23
|
+
title: "My Ruby TUI App",
|
|
24
|
+
borders: [:all],
|
|
25
|
+
border_style: { fg: "cyan" }
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
UPDATE = -> (msg, model) do
|
|
31
|
+
if msg.q? || msg.ctrl_c?
|
|
32
|
+
RatatuiRuby::Tea::Cmd.quit
|
|
33
|
+
else
|
|
34
|
+
model
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run
|
|
39
|
+
RatatuiRuby::Tea.run(model: MODEL, view: VIEW, update: UPDATE)
|
|
40
|
+
end
|
|
41
|
+
# [SYNC:END:mvu]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
VerifyReadmeUsage.new.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
# Cmd.exec Example
|
|
6
|
+
|
|
7
|
+
Demonstrates running shell commands using `Cmd.exec`.
|
|
8
|
+
|
|
9
|
+
Commands in TEA produce **messages**, not callbacks. When a command completes, the runtime sends a tagged tuple to your `update` function. Pattern match on the tag to handle success and failure.
|
|
10
|
+
|
|
11
|
+
## Key Concepts
|
|
12
|
+
|
|
13
|
+
- **Message Tags:** `Cmd.exec(command, tag)` produces `[tag, {stdout:, stderr:, status:}]`.
|
|
14
|
+
- **Non-Blocking:** Commands run in background threads. The UI remains responsive.
|
|
15
|
+
- **Success Handling:** Match on `status: 0` to handle successful execution.
|
|
16
|
+
- **Error Handling:** Match on non-zero status to handle failures.
|
|
17
|
+
- **Ractor-Safe:** No callbacks means no Proc captures. Messages are shareable.
|
|
18
|
+
|
|
19
|
+
## Hotkeys
|
|
20
|
+
|
|
21
|
+
- `d`: Run `ls -la` (directory listing)
|
|
22
|
+
- `u`: Run `uname -a` (system info)
|
|
23
|
+
- `f`: Run a command that fails (demonstrates error handling)
|
|
24
|
+
- `q`: **Quit**
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
<!-- SPDX-SnippetBegin -->
|
|
29
|
+
<!--
|
|
30
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
31
|
+
SPDX-License-Identifier: MIT-0
|
|
32
|
+
-->
|
|
33
|
+
```bash
|
|
34
|
+
ruby examples/widget_cmd_exec/app.rb
|
|
35
|
+
```
|
|
36
|
+
<!-- SPDX-SnippetEnd -->
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
The update function handles both key presses and command results:
|
|
41
|
+
|
|
42
|
+
<!-- SPDX-SnippetBegin -->
|
|
43
|
+
<!--
|
|
44
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
45
|
+
SPDX-License-Identifier: MIT-0
|
|
46
|
+
-->
|
|
47
|
+
```ruby
|
|
48
|
+
UPDATE = -> (msg, model) do
|
|
49
|
+
case msg
|
|
50
|
+
# Handle command results
|
|
51
|
+
in [:got_output, {stdout:, status: 0}]
|
|
52
|
+
[model.with(result: stdout.strip, loading: false), nil]
|
|
53
|
+
in [:got_output, {stderr:, status:}]
|
|
54
|
+
[model.with(result: "Error (exit #{status}): #{stderr.strip}", loading: false), nil]
|
|
55
|
+
|
|
56
|
+
# Handle key presses
|
|
57
|
+
in _ if msg.d?
|
|
58
|
+
[model.with(loading: true), Cmd.exec("ls -la", :got_output)]
|
|
59
|
+
else
|
|
60
|
+
model
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
<!-- SPDX-SnippetEnd -->
|
|
65
|
+
|
|
66
|
+
All logic stays in `update`. The command just runs and produces a message.
|
|
67
|
+
|
|
68
|
+
[Read the source code →](app.rb)
|
|
69
|
+
|
|
70
|
+
[](app.rb)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: MIT-0
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
9
|
+
|
|
10
|
+
require "ratatui_ruby"
|
|
11
|
+
require "ratatui_ruby/tea"
|
|
12
|
+
|
|
13
|
+
# Demonstrates the Cmd.exec command for running shell commands.
|
|
14
|
+
#
|
|
15
|
+
# This example shows how to execute shell commands and handle both success
|
|
16
|
+
# and failure cases using pattern matching in update. The split layout shows
|
|
17
|
+
# command output in the main area with controls at the bottom.
|
|
18
|
+
#
|
|
19
|
+
# === Examples
|
|
20
|
+
#
|
|
21
|
+
# Run the demo from the terminal:
|
|
22
|
+
#
|
|
23
|
+
# ruby examples/widget_cmd_exec/app.rb
|
|
24
|
+
#
|
|
25
|
+
# rdoc-image:/doc/images/widget_cmd_exec.png
|
|
26
|
+
class WidgetCmdExec
|
|
27
|
+
Model = Data.define(:result, :loading, :last_command)
|
|
28
|
+
INITIAL = Model.new(
|
|
29
|
+
result: "Press a key to run a command...",
|
|
30
|
+
loading: false,
|
|
31
|
+
last_command: nil
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
VIEW = -> (model, tui) do
|
|
35
|
+
hotkey_style = tui.style(modifiers: [:bold, :underlined])
|
|
36
|
+
dim_style = tui.style(fg: :dark_gray)
|
|
37
|
+
|
|
38
|
+
# Styles
|
|
39
|
+
border_color = if model.loading
|
|
40
|
+
"yellow"
|
|
41
|
+
elsif model.result.start_with?("Error")
|
|
42
|
+
"red"
|
|
43
|
+
else
|
|
44
|
+
"cyan"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
title = model.last_command ? "Output: #{model.last_command}" : "Cmd.exec Demo"
|
|
48
|
+
content_text = model.loading ? "Running command..." : model.result
|
|
49
|
+
|
|
50
|
+
# 1. Main Output Widget
|
|
51
|
+
output_widget = tui.paragraph(
|
|
52
|
+
text: content_text,
|
|
53
|
+
block: tui.block(
|
|
54
|
+
title:,
|
|
55
|
+
borders: [:all],
|
|
56
|
+
border_style: { fg: border_color },
|
|
57
|
+
padding: 1
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 2. Control Panel Widget
|
|
62
|
+
control_widget = tui.paragraph(
|
|
63
|
+
text: [
|
|
64
|
+
tui.text_line(spans: [
|
|
65
|
+
tui.text_span(content: "d", style: hotkey_style),
|
|
66
|
+
tui.text_span(content: ": Directory listing (ls -la) "),
|
|
67
|
+
tui.text_span(content: "u", style: hotkey_style),
|
|
68
|
+
tui.text_span(content: ": System info (uname -a)"),
|
|
69
|
+
]),
|
|
70
|
+
tui.text_line(spans: [
|
|
71
|
+
tui.text_span(content: "f", style: hotkey_style),
|
|
72
|
+
tui.text_span(content: ": Force failure "),
|
|
73
|
+
tui.text_span(content: "s", style: hotkey_style),
|
|
74
|
+
tui.text_span(content: ": Sleep (3s) "),
|
|
75
|
+
tui.text_span(content: "q", style: hotkey_style),
|
|
76
|
+
tui.text_span(content: ": Quit"),
|
|
77
|
+
]),
|
|
78
|
+
],
|
|
79
|
+
block: tui.block(
|
|
80
|
+
title: "Controls",
|
|
81
|
+
borders: [:all],
|
|
82
|
+
border_style: dim_style
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Return the Root Layout Widget (Blueprint)
|
|
87
|
+
tui.layout(
|
|
88
|
+
direction: :vertical,
|
|
89
|
+
constraints: [
|
|
90
|
+
tui.constraint_fill(1),
|
|
91
|
+
tui.constraint_length(6),
|
|
92
|
+
],
|
|
93
|
+
children: [
|
|
94
|
+
output_widget,
|
|
95
|
+
control_widget,
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
UPDATE = -> (msg, model) do
|
|
101
|
+
case msg
|
|
102
|
+
# Handle command results
|
|
103
|
+
in [:got_output, { stdout:, status: 0 }]
|
|
104
|
+
[model.with(result: stdout.strip.freeze, loading: false), nil]
|
|
105
|
+
in [:got_output, { stderr:, status: }]
|
|
106
|
+
[model.with(result: "Error (exit #{status}): #{stderr.strip}".freeze, loading: false), nil]
|
|
107
|
+
|
|
108
|
+
# Handle key presses
|
|
109
|
+
in _ if msg.q? || msg.ctrl_c?
|
|
110
|
+
RatatuiRuby::Tea::Cmd.quit
|
|
111
|
+
in _ if msg.d?
|
|
112
|
+
[model.with(loading: true, last_command: "ls -la"), RatatuiRuby::Tea::Cmd.exec("ls -la", :got_output)]
|
|
113
|
+
in _ if msg.u?
|
|
114
|
+
[model.with(loading: true, last_command: "uname -a"), RatatuiRuby::Tea::Cmd.exec("uname -a", :got_output)]
|
|
115
|
+
in _ if msg.s?
|
|
116
|
+
cmd = "sleep 3 && echo 'Slept for 3s'"
|
|
117
|
+
[model.with(loading: true, last_command: cmd.freeze), RatatuiRuby::Tea::Cmd.exec(cmd, :got_output)]
|
|
118
|
+
in _ if msg.f?
|
|
119
|
+
# Intentional failure to demonstrate error handling
|
|
120
|
+
cmd = "ls /nonexistent_path_12345"
|
|
121
|
+
[model.with(loading: true, last_command: cmd.freeze), RatatuiRuby::Tea::Cmd.exec(cmd, :got_output)]
|
|
122
|
+
else
|
|
123
|
+
model
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def run
|
|
128
|
+
RatatuiRuby::Tea.run(model: INITIAL, view: VIEW, update: UPDATE)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
WidgetCmdExec.new.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
|
@@ -0,0 +1,159 @@
|
|
|
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 "ratatui_ruby"
|
|
9
|
+
|
|
10
|
+
module RatatuiRuby
|
|
11
|
+
module Tea
|
|
12
|
+
# Runs the Model-View-Update event loop.
|
|
13
|
+
#
|
|
14
|
+
# Applications need a render loop. You poll events, update state, redraw. Every frame.
|
|
15
|
+
# The boilerplate is tedious and error-prone.
|
|
16
|
+
#
|
|
17
|
+
# This class handles the loop. You provide the model, view, and update. It handles the rest.
|
|
18
|
+
#
|
|
19
|
+
# Use it to build applications with predictable state.
|
|
20
|
+
#
|
|
21
|
+
# === Example
|
|
22
|
+
#
|
|
23
|
+
#--
|
|
24
|
+
# SPDX-SnippetBegin
|
|
25
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
26
|
+
# SPDX-License-Identifier: MIT-0
|
|
27
|
+
#++
|
|
28
|
+
# RatatuiRuby::Tea.run(
|
|
29
|
+
# model: { count: 0 }.freeze,
|
|
30
|
+
# view: ->(m, tui) { tui.paragraph(text: m[:count].to_s) },
|
|
31
|
+
# update: ->(msg, m) { msg.q? ? [m, Cmd.quit] : [m, nil] }
|
|
32
|
+
# )
|
|
33
|
+
#--
|
|
34
|
+
# SPDX-SnippetEnd
|
|
35
|
+
#++
|
|
36
|
+
class Runtime
|
|
37
|
+
# Starts the MVU event loop.
|
|
38
|
+
#
|
|
39
|
+
# Runs until the update function returns a <tt>Cmd.quit</tt> command.
|
|
40
|
+
#
|
|
41
|
+
# [model] Initial application state (immutable).
|
|
42
|
+
# [view] Callable receiving <tt>(model, tui)</tt>, returns a widget.
|
|
43
|
+
# [update] Callable receiving <tt>(msg, model)</tt>, returns <tt>[new_model, cmd]</tt> or just <tt>new_model</tt>.
|
|
44
|
+
# [init] Optional callable to run at startup. Returns a message for update.
|
|
45
|
+
def self.run(model:, view:, update:, init: nil)
|
|
46
|
+
validate_ractor_shareable!(model, "model")
|
|
47
|
+
|
|
48
|
+
# Execute init command synchronously if provided
|
|
49
|
+
if init
|
|
50
|
+
init_msg = init.call
|
|
51
|
+
result = update.call(init_msg, model)
|
|
52
|
+
model, _cmd = normalize_update_result(result, model)
|
|
53
|
+
validate_ractor_shareable!(model, "model")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
queue = Queue.new
|
|
57
|
+
|
|
58
|
+
catch(:quit) do
|
|
59
|
+
RatatuiRuby.run do |tui|
|
|
60
|
+
loop do
|
|
61
|
+
tui.draw do |frame|
|
|
62
|
+
widget = view.call(model, tui)
|
|
63
|
+
validate_view_result!(widget)
|
|
64
|
+
frame.render_widget(widget, frame.area)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 1. Handle user input (blocks up to 16ms)
|
|
68
|
+
msg = tui.poll_event
|
|
69
|
+
|
|
70
|
+
# If provided, handle the event
|
|
71
|
+
unless msg.is_a?(RatatuiRuby::Event::None)
|
|
72
|
+
result = update.call(msg, model)
|
|
73
|
+
model, cmd = normalize_update_result(result, model)
|
|
74
|
+
validate_ractor_shareable!(model, "model")
|
|
75
|
+
throw :quit if cmd.is_a?(Cmd::Quit)
|
|
76
|
+
|
|
77
|
+
dispatch(cmd, queue) if cmd
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 2. Check for background outcomes
|
|
81
|
+
until queue.empty?
|
|
82
|
+
begin
|
|
83
|
+
bg_msg = queue.pop(true)
|
|
84
|
+
result = update.call(bg_msg, model)
|
|
85
|
+
model, cmd = normalize_update_result(result, model)
|
|
86
|
+
validate_ractor_shareable!(model, "model")
|
|
87
|
+
throw :quit if cmd.is_a?(Cmd::Quit)
|
|
88
|
+
|
|
89
|
+
dispatch(cmd, queue) if cmd
|
|
90
|
+
rescue ThreadError
|
|
91
|
+
break
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
model
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validates the view returned a widget.
|
|
102
|
+
#
|
|
103
|
+
# Views return widget trees. Returning +nil+ is a bug—you forgot to
|
|
104
|
+
# return something. For an intentionally empty screen, use TUI#clear.
|
|
105
|
+
private_class_method def self.validate_view_result!(widget)
|
|
106
|
+
return unless widget.nil?
|
|
107
|
+
|
|
108
|
+
raise RatatuiRuby::Error::Invariant,
|
|
109
|
+
"View returned nil. Return a widget, or use TUI#clear for an empty screen."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Detects whether +result+ is a +[model, cmd]+ tuple, a plain model, or a Cmd alone.
|
|
113
|
+
#
|
|
114
|
+
# Returns +[model, cmd]+ in all cases.
|
|
115
|
+
private_class_method def self.normalize_update_result(result, previous_model)
|
|
116
|
+
return result if result.is_a?(Array) && result.size == 2 && valid_cmd?(result[1])
|
|
117
|
+
return [previous_model, result] if valid_cmd?(result)
|
|
118
|
+
|
|
119
|
+
[result, nil]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns +true+ if +value+ is a valid command (+nil+ or a +Cmd+ type).
|
|
123
|
+
private_class_method def self.valid_cmd?(value)
|
|
124
|
+
value.nil? || value.class.name&.start_with?("RatatuiRuby::Tea::Cmd::")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validates an object is Ractor-shareable (deeply frozen).
|
|
128
|
+
#
|
|
129
|
+
# Models and messages must be shareable for future Ractor support.
|
|
130
|
+
# Mutable objects cause race conditions. Freeze your data.
|
|
131
|
+
private_class_method def self.validate_ractor_shareable!(object, name)
|
|
132
|
+
return if Ractor.shareable?(object)
|
|
133
|
+
|
|
134
|
+
raise RatatuiRuby::Error::Invariant,
|
|
135
|
+
"#{name.capitalize} is not Ractor-shareable. Use Ractor.make_shareable or Object#freeze."
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Dispatches a command to the worker pool.
|
|
139
|
+
#
|
|
140
|
+
# Spawns a thread for async commands. Pushes result to +queue+.
|
|
141
|
+
# Handles nested commands (Batch, Sequence) recursively.
|
|
142
|
+
private_class_method def self.dispatch(cmd, queue)
|
|
143
|
+
case cmd
|
|
144
|
+
when Cmd::Exec
|
|
145
|
+
Thread.new do
|
|
146
|
+
require "open3"
|
|
147
|
+
stdout, stderr, status = Open3.capture3(cmd.command)
|
|
148
|
+
msg = [cmd.tag, { stdout:, stderr:, status: status.exitstatus }]
|
|
149
|
+
queue << Ractor.make_shareable(msg)
|
|
150
|
+
rescue => e
|
|
151
|
+
# Should we send an error message? For now, crash in debug, ignore in prod?
|
|
152
|
+
# Better to rely on Open3 not raising for standard execution.
|
|
153
|
+
end
|
|
154
|
+
# TODO: Add Batch, Sequence, NetHttp
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/ratatui_ruby/tea.rb
CHANGED
|
@@ -6,16 +6,25 @@
|
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
8
|
require_relative "tea/version"
|
|
9
|
+
require_relative "tea/cmd"
|
|
10
|
+
require_relative "tea/runtime"
|
|
9
11
|
|
|
10
|
-
# Entry point for the ratatui_ruby-tea gem.
|
|
11
|
-
#
|
|
12
|
-
# Ruby libraries benefit from a clear namespace. Gems need a central module.
|
|
13
|
-
#
|
|
14
|
-
# This module serves as the namespace root. All classes and utilities live here.
|
|
15
|
-
#
|
|
16
|
-
# Require this file to load the library.
|
|
17
12
|
module RatatuiRuby # :nodoc: Documented in the ratatui_ruby gem.
|
|
18
|
-
#
|
|
13
|
+
# The Elm Architecture for RatatuiRuby.
|
|
14
|
+
#
|
|
15
|
+
# Building TUI applications means managing state, events, and rendering. Mixing them leads to
|
|
16
|
+
# spaghetti code. Bugs hide in the tangles.
|
|
17
|
+
#
|
|
18
|
+
# This module implements The Elm Architecture (TEA). It separates your application into three
|
|
19
|
+
# pure functions: model, view, and update. The runtime handles the rest.
|
|
20
|
+
#
|
|
21
|
+
# Use it to build applications with predictable, testable state management.
|
|
19
22
|
module Tea
|
|
23
|
+
# Starts the MVU event loop.
|
|
24
|
+
#
|
|
25
|
+
# Convenience delegator to Runtime.run. See Runtime for full documentation.
|
|
26
|
+
def self.run(...)
|
|
27
|
+
Runtime.run(...)
|
|
28
|
+
end
|
|
20
29
|
end
|
|
21
30
|
end
|
data/mise.toml
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
# SPDX-License-Identifier: MIT-0
|
|
4
|
+
#++
|
|
5
|
+
|
|
6
|
+
class VerifyReadmeUsage
|
|
7
|
+
class Model < Data
|
|
8
|
+
attr_reader text: String
|
|
9
|
+
def self.new: (text: String) -> instance
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
MODEL: Model
|
|
13
|
+
|
|
14
|
+
VIEW: ^(Model, RatatuiRuby::TUI) -> untyped
|
|
15
|
+
|
|
16
|
+
UPDATE: ^(RatatuiRuby::Event, Model) -> (Model | [Model, RatatuiRuby::Tea::Cmd::Quit])
|
|
17
|
+
|
|
18
|
+
def run: () -> void
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
# SPDX-License-Identifier: MIT-0
|
|
4
|
+
#++
|
|
5
|
+
|
|
6
|
+
class WidgetCmdExec
|
|
7
|
+
class Model < Data
|
|
8
|
+
attr_reader result: String
|
|
9
|
+
attr_reader loading: bool
|
|
10
|
+
attr_reader last_command: String?
|
|
11
|
+
|
|
12
|
+
def self.new: (result: String, loading: bool, last_command: String?) -> instance
|
|
13
|
+
def with: (**untyped) -> instance
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
INITIAL: Model
|
|
17
|
+
|
|
18
|
+
VIEW: ^(Model, RatatuiRuby::TUI) -> untyped
|
|
19
|
+
|
|
20
|
+
# Msg can be Event or [:got_output, Hash]
|
|
21
|
+
type msg = RatatuiRuby::Event | [Symbol, Hash[Symbol, untyped]]
|
|
22
|
+
|
|
23
|
+
UPDATE: ^(msg, Model) -> (Model | [Model, RatatuiRuby::Tea::Cmd::execution?])
|
|
24
|
+
|
|
25
|
+
def run: () -> void
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
class Runtime
|
|
9
|
+
# 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
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
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
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
# See RatatuiRuby::Tea::Runtime.run
|
|
9
|
+
def self.run: [M, Msg] (
|
|
10
|
+
model: M,
|
|
11
|
+
view: ^(M model, RatatuiRuby::TUI tui) -> untyped,
|
|
12
|
+
update: ^(Msg msg, M model) -> (M | [M, Cmd::execution?] | [M, Cmd::Quit] | [M, nil]),
|
|
13
|
+
?init: ^() -> Msg
|
|
14
|
+
) -> M
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -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,7 @@ packages:
|
|
|
13
13
|
- gdbm
|
|
14
14
|
- ncurses
|
|
15
15
|
- libffi
|
|
16
|
-
<%- if has_rust -%>
|
|
17
16
|
- clang
|
|
18
|
-
<%- end -%>
|
|
19
17
|
- git
|
|
20
18
|
artifacts:
|
|
21
19
|
- <%= gem_name %>/pkg/<%= gem_filename %>
|
|
@@ -28,9 +26,7 @@ tasks:
|
|
|
28
26
|
echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
|
|
29
27
|
echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
|
|
30
28
|
echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
|
|
31
|
-
<%- if has_rust -%>
|
|
32
29
|
echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
|
|
33
|
-
<%- end -%>
|
|
34
30
|
. ~/.buildenv
|
|
35
31
|
export CI="true"
|
|
36
32
|
cd <%= gem_name %>
|
|
@@ -41,9 +37,6 @@ tasks:
|
|
|
41
37
|
mise reshim
|
|
42
38
|
mise x -- bundle config set --local frozen 'true'
|
|
43
39
|
mise x -- bundle install
|
|
44
|
-
<%- if has_rust -%>
|
|
45
|
-
mise x -- bundle exec rake compile
|
|
46
|
-
<%- end -%>
|
|
47
40
|
- test: |
|
|
48
41
|
. ~/.buildenv
|
|
49
42
|
cd <%= gem_name %>
|
data/tasks/resources/rubies.yml
CHANGED
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ratatui_ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: ostruct
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -77,16 +91,31 @@ files:
|
|
|
77
91
|
- README.md
|
|
78
92
|
- REUSE.toml
|
|
79
93
|
- Rakefile
|
|
94
|
+
- Steepfile
|
|
80
95
|
- doc/concepts/application_architecture.md
|
|
81
96
|
- doc/concepts/application_testing.md
|
|
97
|
+
- doc/contributors/priorities.md
|
|
82
98
|
- doc/custom.css
|
|
83
99
|
- doc/getting_started/quickstart.md
|
|
84
100
|
- doc/images/.gitkeep
|
|
101
|
+
- doc/images/verify_readme_usage.png
|
|
102
|
+
- doc/images/widget_cmd_exec.png
|
|
85
103
|
- doc/index.md
|
|
104
|
+
- examples/verify_readme_usage/README.md
|
|
105
|
+
- examples/verify_readme_usage/app.rb
|
|
106
|
+
- examples/widget_cmd_exec/README.md
|
|
107
|
+
- examples/widget_cmd_exec/app.rb
|
|
86
108
|
- exe/.gitkeep
|
|
87
109
|
- lib/ratatui_ruby/tea.rb
|
|
110
|
+
- lib/ratatui_ruby/tea/cmd.rb
|
|
111
|
+
- lib/ratatui_ruby/tea/runtime.rb
|
|
88
112
|
- lib/ratatui_ruby/tea/version.rb
|
|
89
113
|
- mise.toml
|
|
114
|
+
- sig/examples/verify_readme_usage/app.rbs
|
|
115
|
+
- sig/examples/widget_cmd_exec/app.rbs
|
|
116
|
+
- sig/ratatui_ruby/tea.rbs
|
|
117
|
+
- sig/ratatui_ruby/tea/cmd.rbs
|
|
118
|
+
- sig/ratatui_ruby/tea/runtime.rbs
|
|
90
119
|
- tasks/example_viewer.html.erb
|
|
91
120
|
- tasks/resources/build.yml.erb
|
|
92
121
|
- tasks/resources/index.html.erb
|