ratatui_ruby-tea 0.3.1 → 0.4.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/AGENTS.md +42 -2
- data/CHANGELOG.md +76 -0
- data/README.md +8 -5
- data/doc/concepts/async_work.md +164 -0
- data/doc/concepts/commands.md +528 -0
- data/doc/concepts/message_processing.md +51 -0
- data/doc/contributors/WIP/decomposition_strategies_analysis.md +258 -0
- data/doc/contributors/WIP/implementation_plan.md +405 -0
- data/doc/contributors/WIP/init_callable_proposal.md +341 -0
- data/doc/contributors/WIP/mvu_tea_implementations_research.md +372 -0
- data/doc/contributors/WIP/runtime_refactoring_status.md +47 -0
- data/doc/contributors/WIP/task.md +36 -0
- data/doc/contributors/WIP/v0.4.0_todo.md +468 -0
- data/doc/contributors/design/commands_and_outlets.md +11 -1
- data/doc/contributors/priorities.md +22 -24
- data/examples/app_fractal_dashboard/app.rb +3 -7
- data/examples/app_fractal_dashboard/dashboard/base.rb +15 -16
- data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +8 -8
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +11 -11
- data/examples/app_fractal_dashboard/dashboard/update_router.rb +4 -4
- data/examples/app_fractal_dashboard/{bags → fragments}/custom_shell_input.rb +8 -4
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +82 -0
- data/examples/app_fractal_dashboard/{bags → fragments}/custom_shell_output.rb +8 -4
- data/examples/app_fractal_dashboard/{bags → fragments}/disk_usage.rb +13 -10
- data/examples/app_fractal_dashboard/{bags → fragments}/network_panel.rb +12 -12
- data/examples/app_fractal_dashboard/{bags → fragments}/ping.rb +12 -8
- data/examples/app_fractal_dashboard/{bags → fragments}/stats_panel.rb +12 -12
- data/examples/app_fractal_dashboard/{bags → fragments}/system_info.rb +11 -7
- data/examples/app_fractal_dashboard/{bags → fragments}/uptime.rb +11 -7
- data/examples/verify_readme_usage/README.md +7 -4
- data/examples/verify_readme_usage/app.rb +7 -4
- data/lib/ratatui_ruby/tea/command/all.rb +71 -0
- data/lib/ratatui_ruby/tea/command/batch.rb +79 -0
- data/lib/ratatui_ruby/tea/command/custom.rb +1 -1
- data/lib/ratatui_ruby/tea/command/http.rb +194 -0
- data/lib/ratatui_ruby/tea/command/lifecycle.rb +136 -0
- data/lib/ratatui_ruby/tea/command/outlet.rb +59 -27
- data/lib/ratatui_ruby/tea/command/wait.rb +82 -0
- data/lib/ratatui_ruby/tea/command.rb +245 -64
- data/lib/ratatui_ruby/tea/message/all.rb +47 -0
- data/lib/ratatui_ruby/tea/message/http_response.rb +63 -0
- data/lib/ratatui_ruby/tea/message/system/batch.rb +63 -0
- data/lib/ratatui_ruby/tea/message/system/stream.rb +69 -0
- data/lib/ratatui_ruby/tea/message/timer.rb +48 -0
- data/lib/ratatui_ruby/tea/message.rb +40 -0
- data/lib/ratatui_ruby/tea/router.rb +11 -11
- data/lib/ratatui_ruby/tea/runtime.rb +320 -185
- data/lib/ratatui_ruby/tea/shortcuts.rb +2 -2
- data/lib/ratatui_ruby/tea/test_helper.rb +58 -0
- data/lib/ratatui_ruby/tea/version.rb +1 -1
- data/lib/ratatui_ruby/tea.rb +44 -10
- data/rbs_collection.lock.yaml +1 -17
- data/sig/concurrent.rbs +72 -0
- data/sig/ratatui_ruby/tea/command.rbs +141 -37
- data/sig/ratatui_ruby/tea/message.rbs +123 -0
- data/sig/ratatui_ruby/tea/router.rbs +1 -1
- data/sig/ratatui_ruby/tea/runtime.rbs +39 -6
- data/sig/ratatui_ruby/tea/test_helper.rbs +12 -0
- data/sig/ratatui_ruby/tea.rbs +24 -4
- metadata +63 -11
- data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +0 -73
- data/lib/ratatui_ruby/tea/command/cancellation_token.rb +0 -135
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# MVU/TEA Decomposition Strategies Analysis
|
|
8
|
+
|
|
9
|
+
## Decomposition Style Categories
|
|
10
|
+
|
|
11
|
+
### FRACTAL Style (Recursive MVU Triads)
|
|
12
|
+
Each component/module is a complete MVU unit with its own Model, Update, and View. Components nest recursively, parent delegates to children. Child components are self-contained and can be composed.
|
|
13
|
+
|
|
14
|
+
### SLICE Style (Domain-Segregated State)
|
|
15
|
+
State is partitioned by domain/feature, but update/view logic may be centralized or cross-cutting. Each slice typically has Model + Update, with views potentially pulling from multiple slices.
|
|
16
|
+
|
|
17
|
+
### HYBRID
|
|
18
|
+
Combines both patterns - some aspects use fractal composition, others use slice partitioning.
|
|
19
|
+
|
|
20
|
+
### NO DECOMPOSITION
|
|
21
|
+
Framework does not provide explicit support for breaking down complex applications.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Framework Categorization
|
|
26
|
+
|
|
27
|
+
### 🟢 FRACTAL Decomposition
|
|
28
|
+
|
|
29
|
+
#### **1. Elm (Original TEA)**
|
|
30
|
+
- **Style**: Fractal (historically, with evolution)
|
|
31
|
+
- **Terminology**: "Nested TEA", "Modules", "Translator Pattern" / "OutMsg Pattern"
|
|
32
|
+
- **Details**:
|
|
33
|
+
- Originally promoted recursive MVU triads (child modules with own Model/Msg/update/view)
|
|
34
|
+
- Parent uses `Html.map` and `Cmd.map` to transform child messages
|
|
35
|
+
- **Evolution**: Elm 0.19 de-emphasized deep nesting due to boilerplate
|
|
36
|
+
- Current recommendation: Shallow nesting, reusable view functions over deep component trees
|
|
37
|
+
- "Translator Pattern" for child-to-parent communication (child returns OutMsg for parent)
|
|
38
|
+
- **Composition**: Yes, via `Html.map` / `Cmd.map` and OutMsg pattern
|
|
39
|
+
|
|
40
|
+
#### **2. Bubble Tea (Go)**
|
|
41
|
+
- **Style**: Fractal
|
|
42
|
+
- **Terminology**: "Tree of Models", "Nested Models", "Child Components"
|
|
43
|
+
- **Details**:
|
|
44
|
+
- Each component has its own `Model` struct with `Init()`, `Update()`, `View()` methods
|
|
45
|
+
- Parent embeds child models as fields
|
|
46
|
+
- Parent routes messages to children based on state (often using session/state enum)
|
|
47
|
+
- View composition combines child view strings
|
|
48
|
+
- Uses "Bubbles" library for pre-built components
|
|
49
|
+
- **Composition**: Yes, explicit message routing in parent's Update
|
|
50
|
+
|
|
51
|
+
#### **3. Iced (Rust)**
|
|
52
|
+
- **Style**: Fractal
|
|
53
|
+
- **Terminology**: "Nested components", "Message routing", "`.map()` transformation"
|
|
54
|
+
- **Details**:
|
|
55
|
+
- Each component can have State + Message enum + update + view
|
|
56
|
+
- Parent's Message enum wraps child Message types
|
|
57
|
+
- Uses `.map()` to transform child messages/commands/elements to parent context
|
|
58
|
+
- `Subscription::batch()` for combining child subscriptions
|
|
59
|
+
- Note: `Component` trait deprecated in 0.13+ (violated single source of truth)
|
|
60
|
+
- **Composition**: Yes, via `.map()` transformation
|
|
61
|
+
|
|
62
|
+
#### **4. Elmish (F#/Fable)**
|
|
63
|
+
- **Style**: Fractal
|
|
64
|
+
- **Terminology**: "Nested TEA", "Child modules"
|
|
65
|
+
- **Details**:
|
|
66
|
+
- Direct port of Elm Architecture to F#
|
|
67
|
+
- Each module has `Model * Cmd<Msg>` init, `update`, `view`
|
|
68
|
+
- Parent maps child messages and commands
|
|
69
|
+
- Used with React for rendering
|
|
70
|
+
- **Composition**: Yes, same as Elm
|
|
71
|
+
|
|
72
|
+
#### **5. Hyperapp (JavaScript)**
|
|
73
|
+
- **Style**: Fractal-lite
|
|
74
|
+
- **Terminology**: No specific term (minimal framework)
|
|
75
|
+
- **Details**:
|
|
76
|
+
- Unified global state, but can be structured recursively
|
|
77
|
+
- Effects can be nested in init array `[state, effect1, effect2]`
|
|
78
|
+
- Very lightweight (1KB), minimal opinions
|
|
79
|
+
- **Composition**: Limited, more state-tree focused than component-focused
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 🔵 SLICE Decomposition
|
|
84
|
+
|
|
85
|
+
#### **6. Redux (JavaScript/React)**
|
|
86
|
+
- **Style**: PURE SLICE
|
|
87
|
+
- **Terminology**: "Slices", "Reducers", "State combination"
|
|
88
|
+
- **Details**:
|
|
89
|
+
- `createSlice` creates domain-specific state + reducers
|
|
90
|
+
- Each slice has `name`, `initialState`, `reducers`
|
|
91
|
+
- Slices combined via `configureStore({ reducer: { user: userSlice, posts: postsSlice } })`
|
|
92
|
+
- Views (React components) can access any slice via selectors
|
|
93
|
+
- **NOT strictly MVU** (no built-in effects), but pattern is similar
|
|
94
|
+
- **Composition**: Yes, via `combineReducers` / `configureStore`
|
|
95
|
+
- **Notable**: Slices have reducers, but no dedicated views - views are separate React components
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### 🟡 HYBRID Decomposition
|
|
100
|
+
|
|
101
|
+
#### **7. TCA (Swift - The Composable Architecture)**
|
|
102
|
+
- **Style**: HYBRID (Fractal-ish with powerful scoping)
|
|
103
|
+
- **Terminology**: "Reducers", "Scoping", "Pullback", "Child features", "Composition"
|
|
104
|
+
- **Details**:
|
|
105
|
+
- Each "feature" is a `Reducer` with nested `State` and `Action` types
|
|
106
|
+
- Uses `.scope()` to focus parent state/actions on child domain
|
|
107
|
+
- Can compose reducers horizontally (siblings) or vertically (parent-child)
|
|
108
|
+
- **More sophisticated than pure fractal**: dependency injection, effects management, testing tools
|
|
109
|
+
- Parent doesn't just delegate - uses lenses/scoping to project state
|
|
110
|
+
- **Composition**: Yes, via `Reducer` composition operators and scoping
|
|
111
|
+
|
|
112
|
+
#### **8. Flutter Bloc**
|
|
113
|
+
- **Style**: HYBRID
|
|
114
|
+
- **Terminology**: "BloC per feature/screen", "Nested BloCs", "BlocProvider", "MultiBlocProvider"
|
|
115
|
+
- **Details**:
|
|
116
|
+
- Recommended pattern: one Bloc/Cubit per screen or feature
|
|
117
|
+
- BloCs can be nested via `BlocProvider` (dependency injection)
|
|
118
|
+
- `MultiBlocProvider` for multiple BloCs in widget tree
|
|
119
|
+
- **Not pure fractal**: BloCs don't nest MVU triads, they're separate event-driven state machines
|
|
120
|
+
- **Not pure slice**: Each BloC is feature-specific, views are Flutter widgets
|
|
121
|
+
- **Composition**: Yes, via provider pattern (DI), not via direct delegation
|
|
122
|
+
|
|
123
|
+
#### **9. Android MVI (Kotlin)**
|
|
124
|
+
- **Style**: HYBRID
|
|
125
|
+
- **Terminology**: "ViewModel per feature", "StateFlow", "Intent channels"
|
|
126
|
+
- **Details**:
|
|
127
|
+
- Each ViewModel manages feature state via `StateFlow`
|
|
128
|
+
- Intents sent to ViewModel via `SharedFlow` or `Channel`
|
|
129
|
+
- ViewModels can be scoped to fragments/activities
|
|
130
|
+
- **Not pure fractal**: ViewModels are independent state machines, not nested MVU
|
|
131
|
+
- **Not pure slice**: Each ViewModel is feature-bound
|
|
132
|
+
- **Composition**: Limited, mostly via ViewModel scoping and shared state
|
|
133
|
+
|
|
134
|
+
#### **10. Meiosis (JavaScript)**
|
|
135
|
+
- **Style**: HYBRID
|
|
136
|
+
- **Terminology**: "Services", "Nested components" (optional), "Initial model composition"
|
|
137
|
+
- **Details**:
|
|
138
|
+
- Flexible pattern, not opinionated
|
|
139
|
+
- Can compose via `initialModel` merging
|
|
140
|
+
- "meiosis-setup" library helps with nested components and services
|
|
141
|
+
- View-library agnostic
|
|
142
|
+
- **Composition**: Yes, but flexible/manual
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### ⚫ NO EXPLICIT DECOMPOSITION
|
|
147
|
+
|
|
148
|
+
#### **11. SAM Pattern (JavaScript)**
|
|
149
|
+
- **Style**: No explicit decomposition support
|
|
150
|
+
- **Terminology**: N/A
|
|
151
|
+
- **Details**:
|
|
152
|
+
- Single Model (state tree)
|
|
153
|
+
- Single State Representation function
|
|
154
|
+
- Actions propose mutations
|
|
155
|
+
- Based on TLA+ formalism
|
|
156
|
+
- **Philosophical**: Focused on temporal logic and correctness, less on composition
|
|
157
|
+
- **Composition**: Not a focus
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### 🟣 OTHER / MINIMAL DOCUMENTATION
|
|
162
|
+
|
|
163
|
+
#### **12-16. .NET Implementations**
|
|
164
|
+
- **Fabulous (F#/MAUI)**: Likely **FRACTAL** (F# MVU port, similar to Elmish)
|
|
165
|
+
- **BlazorMVU (C#/Blazor)**: Likely **FRACTAL** (Elm-inspired)
|
|
166
|
+
- **MauiReactor (C#/MAUI)**: Likely **FRACTAL** (MVU for .NET MAUI)
|
|
167
|
+
- **MVUX (.NET/Uno)**: **HYBRID** (Model-View-Update eXtended, data binding focus)
|
|
168
|
+
- **ngx-mvu (Angular)**: Likely **HYBRID** (Angular + MVU concepts)
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Summary Table
|
|
173
|
+
|
|
174
|
+
| Framework | Style | Terminology |
|
|
175
|
+
|-----------|-------|-------------|
|
|
176
|
+
| **Elm** | Fractal (evolved) | Nested TEA, Modules, Translator/OutMsg Pattern |
|
|
177
|
+
| **Bubble Tea** | Fractal | Tree of Models, Nested Models, Child Components |
|
|
178
|
+
| **Iced** | Fractal | Nested components, Message routing, `.map()` |
|
|
179
|
+
| **Elmish** | Fractal | Nested TEA, Child modules |
|
|
180
|
+
| **Hyperapp** | Fractal-lite | (minimal framework, no specific term) |
|
|
181
|
+
| **Redux** | **SLICE** | **Slices**, Reducers, State combination |
|
|
182
|
+
| **TCA** | **HYBRID** | Reducers, Scoping, Pullback, Child features |
|
|
183
|
+
| **Flutter Bloc** | **HYBRID** | BloC per feature, Nested BloCs, Providers |
|
|
184
|
+
| **Android MVI** | **HYBRID** | ViewModel per feature, StateFlow, Intents |
|
|
185
|
+
| **Meiosis** | **HYBRID** | Services, Nested components (optional) |
|
|
186
|
+
| **SAM** | None | N/A (TLA+ formalism focus) |
|
|
187
|
+
| Fabulous | Fractal (likely) | (F# MVU) |
|
|
188
|
+
| BlazorMVU | Fractal (likely) | (Elm-inspired) |
|
|
189
|
+
| MauiReactor | Fractal (likely) | (MVU for MAUI) |
|
|
190
|
+
| MVUX | Hybrid (likely) | (Extended MVU, data binding) |
|
|
191
|
+
| ngx-mvu | Hybrid (likely) | (Angular + MVU) |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Key Insights
|
|
196
|
+
|
|
197
|
+
### Fractal Pattern Characteristics:
|
|
198
|
+
1. ✅ **Complete MVU triads** for each component
|
|
199
|
+
2. ✅ **Recursive composition** (components contain components)
|
|
200
|
+
3. ✅ **Message delegation** (parent forwards to child)
|
|
201
|
+
4. ✅ **View composition** (parent combines child views)
|
|
202
|
+
5. ❌ **High boilerplate** (Elm community moved away from deep nesting)
|
|
203
|
+
|
|
204
|
+
**Examples**: Elm (original), Bubble Tea, Iced, Elmish
|
|
205
|
+
|
|
206
|
+
### Slice Pattern Characteristics:
|
|
207
|
+
1. ✅ **Domain-partitioned state** (slices by feature)
|
|
208
|
+
2. ✅ **Flat composition** (slices are siblings, not nested)
|
|
209
|
+
3. ✅ **Centralized view** (views pull from multiple slices)
|
|
210
|
+
4. ✅ **Low boilerplate** (slices are simple)
|
|
211
|
+
5. ❌ **No per-slice views** (views are separate layer)
|
|
212
|
+
|
|
213
|
+
**Example**: Redux (pure slice pattern)
|
|
214
|
+
|
|
215
|
+
### Hybrid Pattern Characteristics:
|
|
216
|
+
1. ✅ **Feature-scoped state** (like slices)
|
|
217
|
+
2. ✅ **Independent update logic** (like fractal)
|
|
218
|
+
3. ✅ **Flexible composition** (dependency injection, scoping)
|
|
219
|
+
4. ✅ **Reduced boilerplate** (compared to pure fractal)
|
|
220
|
+
5. ⚠️ **Framework-specific** (each does it differently)
|
|
221
|
+
|
|
222
|
+
**Examples**: TCA (scoping/pullback), Flutter Bloc (providers), Android MVI (ViewModels)
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Where Does RatatuiRuby-TEA Fit?
|
|
227
|
+
|
|
228
|
+
### Current State: **FRACTAL**
|
|
229
|
+
- Fragments have `Model`, `INITIAL`, `UPDATE`, `VIEW`
|
|
230
|
+
- Parents delegate to children via `Tea.delegate`
|
|
231
|
+
- Message routing via `Tea.route`
|
|
232
|
+
- Router DSL for declarative composition
|
|
233
|
+
|
|
234
|
+
### With Init Callable: **ENHANCED FRACTAL**
|
|
235
|
+
Your `Init` proposal strengthens the fractal pattern by:
|
|
236
|
+
1. ✅ Enabling **parameterized initialization** (like Iced's `flags`)
|
|
237
|
+
2. ✅ Supporting **initial commands** (like Elm's `(Model, Cmd)`)
|
|
238
|
+
3. ✅ Allowing **parent-to-child props** (React-style, but fractal)
|
|
239
|
+
4. ✅ Maintaining **complete MVU triads** (Model, Init, Update, View)
|
|
240
|
+
|
|
241
|
+
This is **closer to Iced** (Rust) than Elm, as Iced explicitly supports flags for initialization and has evolved beyond Elm's original design.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Recommendation
|
|
246
|
+
|
|
247
|
+
RatatuiRuby-TEA should **embrace the modern fractal pattern** with:
|
|
248
|
+
- **Complete MVU triads per fragment**: `Model`, `Init`, `Update`, `View`
|
|
249
|
+
- **Parameterized initialization**: `Init` accepts flags/props
|
|
250
|
+
- **Explicit composition**: Parent calls child `Init`, routes messages, composes views
|
|
251
|
+
|
|
252
|
+
This positions RatatuiRuby-TEA as:
|
|
253
|
+
- **Functional** (like Elm/Elmish/Iced)
|
|
254
|
+
- **Composable** (fractal pattern)
|
|
255
|
+
- **Modern** (Init callable, not static constants)
|
|
256
|
+
- **Parameterizable** (flags/props support)
|
|
257
|
+
|
|
258
|
+
**NOT** like Redux (slice pattern) or TCA/Bloc (OOP/hybrid patterns).
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Init Callable Implementation Plan
|
|
8
|
+
|
|
9
|
+
## Problem Statement
|
|
10
|
+
|
|
11
|
+
The current codebase uses **three different names** for initial state:
|
|
12
|
+
- `MODEL` (simple examples: README, verify_readme_usage)
|
|
13
|
+
- `INITIAL` (fractal fragments: dashboard examples)
|
|
14
|
+
- `init:` (runtime parameter with different semantics)
|
|
15
|
+
|
|
16
|
+
The planned v0.4.0 transition (`INITIAL` → `Initial` PascalCase) would create visual collision with `Model`, where both look like type names despite different purposes (type vs instance).
|
|
17
|
+
|
|
18
|
+
**Critical Finding**: Research of 15+ MVU/TEA frameworks shows **zero** use static constants for initialization. All use callables/functions.
|
|
19
|
+
|
|
20
|
+
## Solution: Init Callable
|
|
21
|
+
|
|
22
|
+
Replace static `INITIAL`/`MODEL` constants with `Init` callable that:
|
|
23
|
+
1. Returns `[model, command]` tuple (or DWIM variants)
|
|
24
|
+
2. Accepts flags/props for parameterization
|
|
25
|
+
3. Enables parent-to-child initialization data flow
|
|
26
|
+
4. Supports initial command dispatch alongside state
|
|
27
|
+
|
|
28
|
+
## Proposed Changes
|
|
29
|
+
|
|
30
|
+
### Core API
|
|
31
|
+
|
|
32
|
+
#### Fragment Structure (v0.4.0)
|
|
33
|
+
```ruby
|
|
34
|
+
module MyFragment
|
|
35
|
+
Model = Data.define(:field1, :field2)
|
|
36
|
+
|
|
37
|
+
# NEW: Init callable (optional, defaults to Model.new)
|
|
38
|
+
Init = ->(flags_hash = {}) do
|
|
39
|
+
model = Model.new(...)
|
|
40
|
+
command = some_initial_command # or nil
|
|
41
|
+
[model, command]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Update = ->(message, model) { ... }
|
|
45
|
+
View = ->(model, tui) { ... }
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### DWIM Return Values
|
|
50
|
+
```ruby
|
|
51
|
+
# All valid return formats:
|
|
52
|
+
Init = -> { Model.new(...) } # Just model
|
|
53
|
+
Init = -> { [Model.new(...), nil] } # Explicit tuple, no command
|
|
54
|
+
Init = -> { [Model.new(...), some_cmd] } # With command
|
|
55
|
+
Init = -> { Command.exit } # Just command (edge case)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Normalization Helper
|
|
59
|
+
```ruby
|
|
60
|
+
module Tea
|
|
61
|
+
# Normalize Init return to [model, command] tuple
|
|
62
|
+
# Uses DWIM logic like Update (tea_command? detection)
|
|
63
|
+
def self.normalize_init(result)
|
|
64
|
+
case result
|
|
65
|
+
when Command then [nil, result] # Just command
|
|
66
|
+
in [model, command] then [model, command] # Already tuple
|
|
67
|
+
else
|
|
68
|
+
if result.respond_to?(:tea_command?) && result.tea_command?
|
|
69
|
+
[nil, result] # Command without array wrapper
|
|
70
|
+
else
|
|
71
|
+
[result, nil] # Just model
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Fractal Composition
|
|
81
|
+
|
|
82
|
+
#### Parent Init Calls Child Init
|
|
83
|
+
```ruby
|
|
84
|
+
module Dashboard
|
|
85
|
+
Model = Data.define(:stats, :network, :theme)
|
|
86
|
+
|
|
87
|
+
Init = ->(theme: :dark, env: {}) do
|
|
88
|
+
# Call child Inits with props
|
|
89
|
+
stats_model, stats_cmd = StatsPanel::Init.(theme: theme)
|
|
90
|
+
network_model, network_cmd = NetworkPanel::Init.(theme: theme)
|
|
91
|
+
|
|
92
|
+
model = Model.new(
|
|
93
|
+
stats: stats_model,
|
|
94
|
+
network: network_model,
|
|
95
|
+
theme: theme
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
command = Command.batch(
|
|
99
|
+
Tea.route(stats_cmd, :stats),
|
|
100
|
+
Tea.route(network_cmd, :network)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
[model, command]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# View composition - UNCHANGED (still manual)
|
|
107
|
+
View = ->(model, tui) do
|
|
108
|
+
tui.vertical_layout(
|
|
109
|
+
children: [
|
|
110
|
+
StatsPanel::View.call(model.stats, tui),
|
|
111
|
+
NetworkPanel::View.call(model.network, tui)
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Update routing - UNCHANGED (Router DSL)
|
|
117
|
+
include Tea::Router
|
|
118
|
+
route :stats, to: StatsPanel
|
|
119
|
+
route :network, to: NetworkPanel
|
|
120
|
+
Update = from_router
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Key Point**: Only `Update` uses Router DSL. `Init` and `View` remain explicit/manual composition.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Runtime Integration
|
|
129
|
+
|
|
130
|
+
#### Current API (Backward Compatible)
|
|
131
|
+
```ruby
|
|
132
|
+
# Still works (old style)
|
|
133
|
+
Tea.run(
|
|
134
|
+
model: MyApp::INITIAL,
|
|
135
|
+
view: MyApp::VIEW,
|
|
136
|
+
update: MyApp::UPDATE
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### New API (Fragment-First)
|
|
141
|
+
```ruby
|
|
142
|
+
# New style (preferred)
|
|
143
|
+
Tea.run(
|
|
144
|
+
fragment: MyApp,
|
|
145
|
+
argv: ARGV,
|
|
146
|
+
env: ENV
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Equivalent to:
|
|
150
|
+
# model, init_cmd = MyApp::Init.(argv: ARGV, env: ENV)
|
|
151
|
+
# Tea.run(model: model, view: MyApp::View, update: MyApp::Update, init: init_cmd)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Runtime Implementation
|
|
155
|
+
```ruby
|
|
156
|
+
module RatatuiRuby::Tea
|
|
157
|
+
def self.run(fragment: nil, model: nil, view: nil, update: nil, argv: [], env: {}, init: nil)
|
|
158
|
+
if fragment
|
|
159
|
+
# New style: fragment-first
|
|
160
|
+
init_callable = fragment.const_defined?(:Init) ? fragment::Init : -> { fragment::Model.new }
|
|
161
|
+
init_result = init_callable.call(argv: argv, env: env)
|
|
162
|
+
model, init_cmd = normalize_init(init_result)
|
|
163
|
+
view = fragment::View
|
|
164
|
+
update = fragment::Update
|
|
165
|
+
init = init_cmd # Override init param with Init-generated command
|
|
166
|
+
else
|
|
167
|
+
# Old style: explicit parameters (backward compatible)
|
|
168
|
+
# model, view, update, init already provided
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
Runtime.run(model: model, view: view, update: update, init: init)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Migration Strategy
|
|
179
|
+
|
|
180
|
+
### v0.4.0: Breaking Change (Zero Users, Zero Cruft)
|
|
181
|
+
|
|
182
|
+
Since the project has **zero external users**, we will make this a **breaking change in v0.4.0**:
|
|
183
|
+
|
|
184
|
+
- ✅ `Init` callable is the **only** way to define initial state
|
|
185
|
+
- ❌ `INITIAL` and `MODEL` constants are **removed** (no backward compatibility)
|
|
186
|
+
- ✅ `Init` is **optional** (defaults to `-> { Model.new }` if not defined)
|
|
187
|
+
- ✅ All examples and documentation use `Init` pattern
|
|
188
|
+
- ✅ Clean slate, no migration debt
|
|
189
|
+
|
|
190
|
+
**Rationale**: Adding backward compatibility for a non-existent user base creates unnecessary code complexity and testing burden. Break now while we can.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Implementation Checklist
|
|
195
|
+
|
|
196
|
+
### Core Implementation
|
|
197
|
+
- [ ] **`Tea.normalize_init` helper**
|
|
198
|
+
- [ ] Implement DWIM logic with `tea_command?` detection
|
|
199
|
+
- [ ] Handle all return formats: model, command, `[model, cmd]`
|
|
200
|
+
- [ ] Tests for all DWIM variants
|
|
201
|
+
|
|
202
|
+
- [ ] **Runtime changes**
|
|
203
|
+
- [ ] Add `fragment:`, `argv:`, `env:` parameters to `Tea.run`
|
|
204
|
+
- [ ] Auto-call `fragment::Init` when `fragment:` provided
|
|
205
|
+
- [ ] Default `Init` to `-> { Model.new }` if not defined
|
|
206
|
+
- [ ] Maintain backward compatibility with `model:`/`view:`/`update:` params
|
|
207
|
+
- [ ] Tests for both old and new API styles
|
|
208
|
+
|
|
209
|
+
### Example Migrations
|
|
210
|
+
- [ ] **Simple example** (verify_readme_usage)
|
|
211
|
+
- [ ] Replace `MODEL` constant with `Init` callable
|
|
212
|
+
- [ ] Update README code samples
|
|
213
|
+
- [ ] Verify tests still pass
|
|
214
|
+
|
|
215
|
+
- [ ] **Fractal example** (app_fractal_dashboard)
|
|
216
|
+
- [ ] Convert all fragment `INITIAL` to `Init`
|
|
217
|
+
- [ ] Update parent Init to call child Inits with props
|
|
218
|
+
- [ ] Compose initial commands with `Command.batch`
|
|
219
|
+
- [ ] Verify all dashboard variants (base, router, manual, helpers) work
|
|
220
|
+
|
|
221
|
+
### Documentation
|
|
222
|
+
- [ ] **Update AGENTS.md**
|
|
223
|
+
- [ ] Change Fragment definition to use `Init` instead of `INITIAL`
|
|
224
|
+
- [ ] Update terminology section
|
|
225
|
+
|
|
226
|
+
- [ ] **Create concept documentation files**
|
|
227
|
+
- [ ] `doc/init.md` (full documentation)
|
|
228
|
+
- [ ] Follow documentation style guide (Alexandrian Context-Problem-Solution)
|
|
229
|
+
- [ ] Explain what Init is and why it exists (vs static constants)
|
|
230
|
+
- [ ] Document Init at root level (runtime integration)
|
|
231
|
+
- [ ] Document Init at non-root level (fractal composition)
|
|
232
|
+
- [ ] Show parameterization with flags/props
|
|
233
|
+
- [ ] Document DWIM return values
|
|
234
|
+
- [ ] Show Tea.normalize_init usage
|
|
235
|
+
- [ ] Multiple complete examples (simple, fractal, with commands)
|
|
236
|
+
- [ ] Link to related concepts (Fragment, Model, Update, Command)
|
|
237
|
+
- [ ] Create stubs for concept doc series (H1 + "TODO" only):
|
|
238
|
+
- [ ] `doc/fragment.md`
|
|
239
|
+
- [ ] `doc/model.md`
|
|
240
|
+
- [ ] `doc/view.md`
|
|
241
|
+
- [ ] `doc/update.md`
|
|
242
|
+
- [ ] `doc/command.md`
|
|
243
|
+
- [ ] `doc/message.md`
|
|
244
|
+
- [ ] `doc/output.md`
|
|
245
|
+
|
|
246
|
+
- [ ] **Update design_for_v0.4.0.md**
|
|
247
|
+
- [ ] Add Init Callable section
|
|
248
|
+
- [ ] Document DWIM behavior
|
|
249
|
+
- [ ] Document `Tea.normalize_init` helper
|
|
250
|
+
- [ ] Add footnote on Iced pattern inspiration
|
|
251
|
+
- [ ] Add footnote on OutMsg pattern (future consideration)
|
|
252
|
+
|
|
253
|
+
- [ ] **Create migration guide**
|
|
254
|
+
- [ ] Before/after examples
|
|
255
|
+
- [ ] Deprecation timeline
|
|
256
|
+
- [ ] Common patterns
|
|
257
|
+
|
|
258
|
+
- [ ] **Update RDoc**
|
|
259
|
+
- [ ] Document `Tea.normalize_init`
|
|
260
|
+
- [ ] Document new `Tea.run` signature
|
|
261
|
+
- [ ] Update Fragment examples throughout
|
|
262
|
+
|
|
263
|
+
### RBS Type Signatures
|
|
264
|
+
- [ ] **Update Tea module RBS**
|
|
265
|
+
```ruby
|
|
266
|
+
module RatatuiRuby::Tea
|
|
267
|
+
def self.run: (
|
|
268
|
+
?fragment: Module,
|
|
269
|
+
?model: untyped,
|
|
270
|
+
?view: ^(untyped, untyped) -> Widget,
|
|
271
|
+
?update: ^(untyped, untyped) -> untyped,
|
|
272
|
+
?init: ^() -> untyped,
|
|
273
|
+
?argv: Array[String],
|
|
274
|
+
?env: Hash[String, String]
|
|
275
|
+
) -> void
|
|
276
|
+
|
|
277
|
+
def self.normalize_init: (untyped result) -> [untyped, Command?]
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Testing
|
|
282
|
+
- [ ] **Unit tests for `Tea.normalize_init`**
|
|
283
|
+
- [ ] Returns `[model, nil]` for model-only
|
|
284
|
+
- [ ] Returns `[nil, cmd]` for command-only
|
|
285
|
+
- [ ] Returns `[model, cmd]` for tuple
|
|
286
|
+
- [ ] Handles `tea_command?` detection
|
|
287
|
+
|
|
288
|
+
- [ ] **Integration tests for `Tea.run`**
|
|
289
|
+
- [ ] Fragment-first API works
|
|
290
|
+
- [ ] Old API still works (backward compat)
|
|
291
|
+
- [ ] Init commands are dispatched
|
|
292
|
+
- [ ] ARGV/ENV passed to Init
|
|
293
|
+
- [ ] Default Init works when not defined
|
|
294
|
+
|
|
295
|
+
- [ ] **Fractal composition tests**
|
|
296
|
+
- [ ] Parent Init calls child Inits
|
|
297
|
+
- [ ] Props passed to children
|
|
298
|
+
- [ ] Commands batched correctly
|
|
299
|
+
- [ ] Router still routes messages correctly
|
|
300
|
+
|
|
301
|
+
### CHANGELOG
|
|
302
|
+
- [ ] Add to `[UNRELEASED]` section:
|
|
303
|
+
```markdown
|
|
304
|
+
### Breaking Changes
|
|
305
|
+
- **REMOVED**: `INITIAL` and `MODEL` constants
|
|
306
|
+
- Use `Init` callable instead: `Init = -> { [Model.new(...), nil] }`
|
|
307
|
+
- `Init` is optional (defaults to `-> { Model.new }`)
|
|
308
|
+
|
|
309
|
+
### Added
|
|
310
|
+
- **Init Callable Pattern**: Fragments support `Init` callable for parameterized initialization
|
|
311
|
+
- Returns `[model, command]` tuple (DWIM supported)
|
|
312
|
+
- Accepts flags/props for parent-to-child data flow
|
|
313
|
+
- `Tea.normalize_init` helper for composing child Inits
|
|
314
|
+
- **Fragment-First Runtime API**: `Tea.run(fragment: MyApp, argv: ARGV, env: ENV)`
|
|
315
|
+
- **Concept Documentation**: Created `doc/init.md` and series stubs
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Breaking Changes
|
|
321
|
+
|
|
322
|
+
**v0.4.0 is a BREAKING RELEASE**:
|
|
323
|
+
|
|
324
|
+
- ❌ **REMOVED**: `INITIAL` and `MODEL` constants (use `Init` callable instead)
|
|
325
|
+
- ✅ **NEW**: `Init` callable is the only supported initialization pattern
|
|
326
|
+
- ✅ **NEW**: Runtime `fragment:` parameter for fragment-first API
|
|
327
|
+
|
|
328
|
+
**Rationale**: Zero external users means zero migration burden. Ship clean architecture without legacy cruft.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Open Questions
|
|
333
|
+
|
|
334
|
+
None - all design decisions finalized with user.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Design Notes (Contributor Reference)
|
|
339
|
+
|
|
340
|
+
### Concept Documentation Series
|
|
341
|
+
|
|
342
|
+
`doc/init.md` is the **first** in a series of concept documentation pages, one for each core TEA concept:
|
|
343
|
+
- `doc/init.md` (this implementation)
|
|
344
|
+
- `doc/fragment.md` (future)
|
|
345
|
+
- `doc/model.md` (future)
|
|
346
|
+
- `doc/view.md` (future)
|
|
347
|
+
- `doc/update.md` (future)
|
|
348
|
+
- `doc/command.md` (future)
|
|
349
|
+
- `doc/message.md` (future)
|
|
350
|
+
- `doc/output.md` (future)
|
|
351
|
+
|
|
352
|
+
Each follows the Alexandrian Context-Problem-Solution pattern per `doc/contributors/documentation_style.md`.
|
|
353
|
+
|
|
354
|
+
### Why Not Auto-Init in Router DSL?
|
|
355
|
+
|
|
356
|
+
The Router DSL is **message-only** by design. It handles `Update` function routing, not initialization or view composition:
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
route :stats, to: StatsPanel # Only routes messages to StatsPanel::Update
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Both `Init` and `View` remain **explicit composition** (manual), which provides:
|
|
363
|
+
- ✅ Full control over initialization order and props
|
|
364
|
+
- ✅ Clear, explicit data flow
|
|
365
|
+
- ✅ Flexibility for conditional initialization
|
|
366
|
+
- ✅ Consistency with View composition pattern
|
|
367
|
+
|
|
368
|
+
### Terminology Inspirations
|
|
369
|
+
|
|
370
|
+
This pattern draws from multiple proven MVU implementations:
|
|
371
|
+
- **Elm**: `init : () -> (Model, Cmd Msg)` tuple return
|
|
372
|
+
- **Iced (Rust)**: `fn new(flags: Flags) -> (State, Command)` parameterized init
|
|
373
|
+
- **Bubble Tea (Go)**: `func (m Model) Init() tea.Cmd` method-based init
|
|
374
|
+
- **Hyperapp (JS)**: `init: [state, ...effects]` array-based DWIM
|
|
375
|
+
|
|
376
|
+
See `doc/contributors/mvu_research_notes.md` for detailed analysis.
|
|
377
|
+
|
|
378
|
+
### Future: OutMsg Pattern
|
|
379
|
+
|
|
380
|
+
Elm's "Translator Pattern" / "OutMsg Pattern" allows children to emit events to parents beyond state updates. Not needed for v0.4.0, but may be considered for future:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# Potential future enhancement
|
|
384
|
+
Update = ->(msg, model) do
|
|
385
|
+
case msg
|
|
386
|
+
in [:data_loaded, data]
|
|
387
|
+
[[model.with(data: data), nil], [:parent_event_here]]
|
|
388
|
+
# ^^^^^^^^^^^^^^^^^^^^ OutMsg for parent
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
See `doc/contributors/outmsg_pattern_notes.md` for details.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Success Criteria
|
|
398
|
+
|
|
399
|
+
- [ ] All existing tests pass
|
|
400
|
+
- [ ] `bundle exec agent_rake` passes (0 warnings)
|
|
401
|
+
- [ ] Both old and new API styles work
|
|
402
|
+
- [ ] Fractal dashboard example uses Init pattern
|
|
403
|
+
- [ ] Simple example uses Init pattern
|
|
404
|
+
- [ ] Documentation is comprehensive and clear
|
|
405
|
+
- [ ] Migration path is documented
|