milktea 0.1.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 +7 -0
- data/.claude/commands/devlog.md +103 -0
- data/.claude/settings.json +29 -0
- data/.rspec +3 -0
- data/.rubocop.yml +17 -0
- data/ARCHITECTURE.md +621 -0
- data/CLAUDE.md +537 -0
- data/LICENSE.txt +21 -0
- data/README.md +382 -0
- data/Rakefile +12 -0
- data/docs/devlog/20250703.md +119 -0
- data/docs/devlog/20250704-2.md +129 -0
- data/docs/devlog/20250704.md +90 -0
- data/docs/devlog/20250705.md +81 -0
- data/examples/container_layout.rb +288 -0
- data/examples/container_simple.rb +66 -0
- data/examples/counter.rb +60 -0
- data/examples/dashboard.rb +121 -0
- data/examples/hot_reload_demo/models/demo_model.rb +91 -0
- data/examples/hot_reload_demo/models/status_model.rb +34 -0
- data/examples/hot_reload_demo.rb +64 -0
- data/examples/simple.rb +39 -0
- data/lib/milktea/application.rb +64 -0
- data/lib/milktea/bounds.rb +10 -0
- data/lib/milktea/config.rb +35 -0
- data/lib/milktea/container.rb +124 -0
- data/lib/milktea/loader.rb +45 -0
- data/lib/milktea/message.rb +39 -0
- data/lib/milktea/model.rb +112 -0
- data/lib/milktea/program.rb +81 -0
- data/lib/milktea/renderer.rb +44 -0
- data/lib/milktea/runtime.rb +71 -0
- data/lib/milktea/version.rb +5 -0
- data/lib/milktea.rb +69 -0
- data/sig/milktea.rbs +4 -0
- metadata +151 -0
data/ARCHITECTURE.md
ADDED
@@ -0,0 +1,621 @@
|
|
1
|
+
# Milktea: Terminal UI Framework Architecture
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Milktea is a Ruby Terminal User Interface (TUI) framework inspired by The Elm Architecture. It provides a functional, reactive approach to building interactive command-line applications with predictable state management and composable components.
|
6
|
+
|
7
|
+
## Core Architecture
|
8
|
+
|
9
|
+
### Elm Architecture Implementation
|
10
|
+
|
11
|
+
Milktea implements the classic Elm Architecture pattern with three core concepts:
|
12
|
+
|
13
|
+
```mermaid
|
14
|
+
graph TB
|
15
|
+
User[User Input] --> Message[Message]
|
16
|
+
Message --> Update[Model#update]
|
17
|
+
Update --> NewModel[New Model Instance]
|
18
|
+
Update --> SideEffect[Side Effect]
|
19
|
+
|
20
|
+
NewModel --> View[Model#view]
|
21
|
+
View --> Terminal[Terminal Output]
|
22
|
+
Terminal --> User
|
23
|
+
|
24
|
+
SideEffect --> Runtime[Runtime]
|
25
|
+
Runtime --> Message
|
26
|
+
|
27
|
+
subgraph "Elm Architecture Cycle"
|
28
|
+
Message
|
29
|
+
Update
|
30
|
+
NewModel
|
31
|
+
View
|
32
|
+
end
|
33
|
+
|
34
|
+
subgraph "Side Effects"
|
35
|
+
SideEffect
|
36
|
+
Runtime
|
37
|
+
end
|
38
|
+
|
39
|
+
style Message fill:#e1f5fe
|
40
|
+
style Update fill:#fff3e0
|
41
|
+
style NewModel fill:#e8f5e8
|
42
|
+
style View fill:#f3e5f5
|
43
|
+
style SideEffect fill:#fff8e1
|
44
|
+
```
|
45
|
+
|
46
|
+
1. **Model**: Immutable state container with view rendering and update logic
|
47
|
+
2. **Update**: Pure functions that process messages and return new state
|
48
|
+
3. **View**: Functions that render the current state to terminal output
|
49
|
+
|
50
|
+
### Component Tree
|
51
|
+
|
52
|
+
```mermaid
|
53
|
+
graph TB
|
54
|
+
App[Application] --> Program[Program<br/>Event Loop]
|
55
|
+
App --> Runtime[Runtime<br/>Message Processing]
|
56
|
+
App --> Renderer[Renderer<br/>Terminal Output]
|
57
|
+
App --> ModelTree[Model Tree]
|
58
|
+
|
59
|
+
ModelTree --> Model[Model<br/>Base Component]
|
60
|
+
ModelTree --> Container[Container<br/>Layout Component]
|
61
|
+
ModelTree --> Dynamic[Dynamic Children<br/>Symbol Resolution]
|
62
|
+
|
63
|
+
Program --> |delegates| Runtime
|
64
|
+
Program --> |delegates| Renderer
|
65
|
+
Runtime --> |tick| Model
|
66
|
+
Model --> |view| Renderer
|
67
|
+
|
68
|
+
style App fill:#e1f5fe
|
69
|
+
style Program fill:#f3e5f5
|
70
|
+
style Runtime fill:#fff3e0
|
71
|
+
style Renderer fill:#e8f5e8
|
72
|
+
style ModelTree fill:#fce4ec
|
73
|
+
```
|
74
|
+
|
75
|
+
## Core Components
|
76
|
+
|
77
|
+
### Milktea Module (lib/milktea.rb)
|
78
|
+
|
79
|
+
The main module provides:
|
80
|
+
- **Zeitwerk autoloading** for automatic class loading
|
81
|
+
- **Thread-safe app registry** using mutex for concurrent access
|
82
|
+
- **Configuration management** with global config instance
|
83
|
+
- **Root path detection** for project structure awareness
|
84
|
+
- **Environment detection** (development/production)
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
module Milktea
|
88
|
+
# Thread-safe app registry
|
89
|
+
MUTEX = Mutex.new
|
90
|
+
|
91
|
+
# Global configuration
|
92
|
+
def self.config
|
93
|
+
MUTEX.synchronize { @config ||= Config.new }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Environment detection
|
97
|
+
def self.env
|
98
|
+
(ENV.fetch("MILKTEA_ENV", nil) || ENV.fetch("APP_ENV", "production")).to_sym
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
### Model Base Class (lib/milktea/model.rb)
|
104
|
+
|
105
|
+
The foundation component implementing:
|
106
|
+
- **Immutable state management** with frozen state objects
|
107
|
+
- **Child component DSL** for declarative composition
|
108
|
+
- **Dynamic child resolution** using Symbol-to-method pattern
|
109
|
+
- **State merging** with `with` method for immutable updates
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class Model
|
113
|
+
# Child definition DSL
|
114
|
+
def self.child(klass, mapper = nil)
|
115
|
+
@children ||= []
|
116
|
+
@children << { class: klass, mapper: mapper || ->(_state) { {} } }
|
117
|
+
end
|
118
|
+
|
119
|
+
# Dynamic child resolution
|
120
|
+
def resolve_child(klass, state)
|
121
|
+
klass = send(klass) if klass.is_a?(Symbol)
|
122
|
+
raise ArgumentError, "Child must be a Model class" unless klass.is_a?(Class) && klass <= Model
|
123
|
+
klass.new(state)
|
124
|
+
rescue NoMethodError
|
125
|
+
raise ArgumentError, "Method #{klass} not found for dynamic child resolution"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
### Container Layout System (lib/milktea/container.rb)
|
131
|
+
|
132
|
+
Provides CSS-like flexbox layout for terminal interfaces:
|
133
|
+
- **Flexible direction** (column/row) with proportional sizing
|
134
|
+
- **Bounds calculation** for precise positioning
|
135
|
+
- **Automatic view composition** with default `children_views` rendering
|
136
|
+
- **Flex-based space distribution** using flex ratios
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class Container < Model
|
140
|
+
# Layout direction
|
141
|
+
def self.direction(dir)
|
142
|
+
@direction = dir
|
143
|
+
end
|
144
|
+
|
145
|
+
# Child with flex properties
|
146
|
+
def self.child(klass, mapper = nil, flex: 1)
|
147
|
+
@children ||= []
|
148
|
+
@children << { class: klass, mapper: mapper, flex: flex }
|
149
|
+
end
|
150
|
+
|
151
|
+
# Default view shows all children
|
152
|
+
def view = children_views
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Runtime Message Processing (lib/milktea/runtime.rb)
|
157
|
+
|
158
|
+
Manages the application's message queue and execution lifecycle:
|
159
|
+
- **Message queue processing** with thread-safe operations
|
160
|
+
- **Side effect execution** for handling special messages
|
161
|
+
- **Rendering optimization** by tracking message-based changes
|
162
|
+
- **State management** for running/stopped application states
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
class Runtime
|
166
|
+
def tick(model)
|
167
|
+
has_render_messages = false
|
168
|
+
|
169
|
+
until @queue.empty?
|
170
|
+
message = @queue.pop(true)
|
171
|
+
model, side_effect = model.update(message)
|
172
|
+
execute_side_effect(side_effect)
|
173
|
+
has_render_messages = true unless message.is_a?(Message::None)
|
174
|
+
end
|
175
|
+
|
176
|
+
@should_render = has_render_messages
|
177
|
+
model
|
178
|
+
end
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
### Program Event Loop (lib/milktea/program.rb)
|
183
|
+
|
184
|
+
The main application controller that:
|
185
|
+
- **Manages the event loop** at 60 FPS using timers
|
186
|
+
- **Handles keyboard input** with TTY-Reader integration
|
187
|
+
- **Coordinates rendering** through the renderer
|
188
|
+
- **Uses delegation pattern** for clean interface separation
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
class Program
|
192
|
+
extend Forwardable
|
193
|
+
FPS = 60
|
194
|
+
REFRESH_INTERVAL = 1.0 / FPS
|
195
|
+
|
196
|
+
# Delegate to runtime and renderer
|
197
|
+
delegate %i[start stop running? tick render? enqueue] => :runtime
|
198
|
+
delegate %i[setup_screen restore_screen render] => :renderer
|
199
|
+
|
200
|
+
def run
|
201
|
+
start
|
202
|
+
setup_screen
|
203
|
+
render(@model)
|
204
|
+
setup_timers
|
205
|
+
@timers.wait while running?
|
206
|
+
ensure
|
207
|
+
restore_screen
|
208
|
+
end
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
### Renderer Terminal Management (lib/milktea/renderer.rb)
|
213
|
+
|
214
|
+
Handles all terminal interaction:
|
215
|
+
- **Screen management** with setup/restore capabilities
|
216
|
+
- **Cursor control** using TTY-Cursor for positioning
|
217
|
+
- **Output optimization** with atomic screen updates
|
218
|
+
- **Clean rendering** with screen clearing and flushing
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class Renderer
|
222
|
+
def render(model)
|
223
|
+
@output.print @cursor.clear_screen
|
224
|
+
@output.print @cursor.move_to(0, 0)
|
225
|
+
@output.print model.view
|
226
|
+
@output.flush
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
### Configuration System (lib/milktea/config.rb)
|
232
|
+
|
233
|
+
Centralized configuration management:
|
234
|
+
- **Autoload directories** for Zeitwerk integration
|
235
|
+
- **Hot reloading settings** with environment-based defaults
|
236
|
+
- **Lazy initialization** of runtime and renderer instances
|
237
|
+
- **Path resolution** for autoload directories
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class Config
|
241
|
+
def initialize
|
242
|
+
@autoload_dirs = ["app/models"]
|
243
|
+
@output = $stdout
|
244
|
+
@hot_reloading = nil
|
245
|
+
@runtime = nil
|
246
|
+
@renderer = nil
|
247
|
+
yield(self) if block_given?
|
248
|
+
end
|
249
|
+
|
250
|
+
def hot_reloading?
|
251
|
+
@hot_reloading || (Milktea.env == :development)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
### Message System (lib/milktea/message.rb)
|
257
|
+
|
258
|
+
Immutable message definitions using Ruby's Data.define:
|
259
|
+
- **System messages** (None, Exit, Tick, Reload)
|
260
|
+
- **Input messages** (KeyPress with modifier keys)
|
261
|
+
- **Batch processing** for multiple messages
|
262
|
+
- **Type safety** with structured data objects
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
module Message
|
266
|
+
None = Data.define
|
267
|
+
Exit = Data.define
|
268
|
+
Tick = Data.define
|
269
|
+
Reload = Data.define
|
270
|
+
KeyPress = Data.define(:key, :value, :ctrl, :alt, :shift)
|
271
|
+
Batch = Data.define(:messages)
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
### Loader System (lib/milktea/loader.rb)
|
276
|
+
|
277
|
+
Optional development-time features:
|
278
|
+
- **Zeitwerk integration** for automatic class loading
|
279
|
+
- **Hot reloading** with Listen gem integration
|
280
|
+
- **Multi-directory support** for complex project structures
|
281
|
+
- **Graceful degradation** when Listen gem is unavailable
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
class Loader
|
285
|
+
def setup
|
286
|
+
@loader = Zeitwerk::Loader.new
|
287
|
+
@autoload_paths.each { |path| @loader.push_dir(path) }
|
288
|
+
@loader.enable_reloading
|
289
|
+
@loader.setup
|
290
|
+
end
|
291
|
+
|
292
|
+
def hot_reload
|
293
|
+
gem "listen"
|
294
|
+
require "listen"
|
295
|
+
|
296
|
+
@listeners = @autoload_paths.map do |path|
|
297
|
+
Listen.to(path, only: /\.rb$/) { |modified, added, removed|
|
298
|
+
reload if modified.any? || added.any? || removed.any?
|
299
|
+
}
|
300
|
+
end
|
301
|
+
|
302
|
+
@listeners.each(&:start)
|
303
|
+
rescue Gem::LoadError
|
304
|
+
# Listen gem not available, skip file watching
|
305
|
+
end
|
306
|
+
end
|
307
|
+
```
|
308
|
+
|
309
|
+
### Application Abstraction (lib/milktea/application.rb)
|
310
|
+
|
311
|
+
High-level interface for creating Milktea applications:
|
312
|
+
- **Auto-registration** when inherited
|
313
|
+
- **Root model definition** with DSL
|
314
|
+
- **Integrated loader setup** for development workflow
|
315
|
+
- **Simplified boot process** with error handling
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
class Application
|
319
|
+
def self.inherited(subclass)
|
320
|
+
super
|
321
|
+
Milktea.app = subclass
|
322
|
+
end
|
323
|
+
|
324
|
+
def self.root(model_name = nil)
|
325
|
+
return @root_model_name if model_name.nil?
|
326
|
+
@root_model_name = model_name
|
327
|
+
end
|
328
|
+
|
329
|
+
def self.boot
|
330
|
+
return new.run if @root_model_name
|
331
|
+
raise Error, "No root model defined. Use 'root \"ModelName\"' in your Application class."
|
332
|
+
end
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
## Data Flow Architecture
|
337
|
+
|
338
|
+
### Message Processing Cycle
|
339
|
+
|
340
|
+
```mermaid
|
341
|
+
sequenceDiagram
|
342
|
+
participant User
|
343
|
+
participant Program
|
344
|
+
participant Runtime
|
345
|
+
participant Model
|
346
|
+
participant Renderer
|
347
|
+
participant Terminal
|
348
|
+
|
349
|
+
User->>Program: Keyboard Input
|
350
|
+
Program->>Program: Convert to Message
|
351
|
+
Program->>Runtime: enqueue(message)
|
352
|
+
|
353
|
+
loop Every 16.67ms (60 FPS)
|
354
|
+
Program->>Runtime: tick(model)
|
355
|
+
Runtime->>Runtime: Process message queue
|
356
|
+
Runtime->>Model: update(message)
|
357
|
+
Model->>Model: Create new state
|
358
|
+
Model-->>Runtime: [new_model, side_effect]
|
359
|
+
Runtime->>Runtime: Execute side effects
|
360
|
+
Runtime-->>Program: updated_model
|
361
|
+
|
362
|
+
alt If render needed
|
363
|
+
Program->>Model: view()
|
364
|
+
Model-->>Program: rendered_content
|
365
|
+
Program->>Renderer: render(model)
|
366
|
+
Renderer->>Terminal: Display output
|
367
|
+
end
|
368
|
+
end
|
369
|
+
```
|
370
|
+
|
371
|
+
1. **Input Capture**: Program reads keyboard input via TTY-Reader
|
372
|
+
2. **Message Creation**: Input converted to structured Message objects
|
373
|
+
3. **Queue Processing**: Runtime processes message queue
|
374
|
+
4. **Model Updates**: Each message triggers model.update() calls
|
375
|
+
5. **Side Effects**: Runtime executes side effects (Exit, Batch, etc.)
|
376
|
+
6. **Rendering**: Renderer updates screen if changes occurred
|
377
|
+
|
378
|
+
### State Management
|
379
|
+
|
380
|
+
- **Immutable State**: All model state is frozen and immutable
|
381
|
+
- **Functional Updates**: State changes create new instances via `with` method
|
382
|
+
- **Hot Reload Compatibility**: Uses `Kernel.const_get` for fresh class references
|
383
|
+
- **Child State Mapping**: Parent state mapped to child state through lambdas
|
384
|
+
|
385
|
+
### Component Composition
|
386
|
+
|
387
|
+
```mermaid
|
388
|
+
graph TB
|
389
|
+
Parent[Parent Model] --> Child1[Child Model 1]
|
390
|
+
Parent --> Child2[Child Model 2]
|
391
|
+
Parent --> ChildN[Child Model N]
|
392
|
+
|
393
|
+
Parent --> |"child :dynamic_child"| DynamicMethod[dynamic_child method]
|
394
|
+
DynamicMethod --> |returns Class| DynamicChild[Dynamic Child Model]
|
395
|
+
|
396
|
+
subgraph "State Flow"
|
397
|
+
ParentState[Parent State] --> |mapper lambda| ChildState1[Child 1 State]
|
398
|
+
ParentState --> |mapper lambda| ChildState2[Child 2 State]
|
399
|
+
ParentState --> |mapper lambda| ChildStateN[Child N State]
|
400
|
+
end
|
401
|
+
|
402
|
+
subgraph "View Composition"
|
403
|
+
Child1 --> |view| View1[Child 1 View]
|
404
|
+
Child2 --> |view| View2[Child 2 View]
|
405
|
+
ChildN --> |view| ViewN[Child N View]
|
406
|
+
View1 --> |join| ParentView[Parent View]
|
407
|
+
View2 --> |join| ParentView
|
408
|
+
ViewN --> |join| ParentView
|
409
|
+
end
|
410
|
+
|
411
|
+
style Parent fill:#e1f5fe
|
412
|
+
style DynamicMethod fill:#fff3e0
|
413
|
+
style ParentView fill:#e8f5e8
|
414
|
+
```
|
415
|
+
|
416
|
+
- **Declarative DSL**: Components defined using `child` class method
|
417
|
+
- **Dynamic Resolution**: Symbol-based children resolved at runtime
|
418
|
+
- **Bounds Propagation**: Container layout automatically calculates child bounds
|
419
|
+
- **View Composition**: Parent views composed from child views
|
420
|
+
|
421
|
+
## Dynamic Child Resolution
|
422
|
+
|
423
|
+
The framework supports runtime component selection through Symbol-based definitions:
|
424
|
+
|
425
|
+
```mermaid
|
426
|
+
flowchart TB
|
427
|
+
Child["`child :current_view`"] --> Resolve[resolve_child]
|
428
|
+
Resolve --> |Symbol?| CheckSymbol{Is Symbol?}
|
429
|
+
CheckSymbol --> |Yes| CallMethod["`send(:current_view)`"]
|
430
|
+
CheckSymbol --> |No| CheckClass[Check if Class]
|
431
|
+
|
432
|
+
CallMethod --> |Returns Class| CheckClass
|
433
|
+
CallMethod --> |NoMethodError| ErrorMethod[ArgumentError:<br/>Method not found]
|
434
|
+
|
435
|
+
CheckClass --> |Valid Model Class| CreateInstance["`Class.new(state)`"]
|
436
|
+
CheckClass --> |Invalid| ErrorType[ArgumentError:<br/>Not a Model class]
|
437
|
+
|
438
|
+
CreateInstance --> ModelInstance[Model Instance]
|
439
|
+
|
440
|
+
subgraph "Example Flow"
|
441
|
+
State["`state[:mode] = :edit`"] --> Method[current_view method]
|
442
|
+
Method --> |returns| EditViewClass[EditView]
|
443
|
+
EditViewClass --> Instance[EditView.new]
|
444
|
+
end
|
445
|
+
|
446
|
+
style Child fill:#e1f5fe
|
447
|
+
style ErrorMethod fill:#ffebee
|
448
|
+
style ErrorType fill:#ffebee
|
449
|
+
style ModelInstance fill:#e8f5e8
|
450
|
+
```
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
class DynamicApp < Milktea::Model
|
454
|
+
child :current_view # Resolves to current_view method
|
455
|
+
child StatusBar # Direct class reference
|
456
|
+
|
457
|
+
def current_view
|
458
|
+
case state[:mode]
|
459
|
+
when :edit then EditView
|
460
|
+
when :view then DisplayView
|
461
|
+
else DefaultView
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
```
|
466
|
+
|
467
|
+
### Error Handling
|
468
|
+
|
469
|
+
- **NoMethodError**: Missing methods converted to ArgumentError with clear message
|
470
|
+
- **Type Validation**: Ensures resolved classes inherit from Model
|
471
|
+
- **Debugging Support**: Clear distinction between missing methods and invalid types
|
472
|
+
|
473
|
+
## Layout System
|
474
|
+
|
475
|
+
### Flexbox-Style Layout
|
476
|
+
|
477
|
+
Containers use CSS-like flexbox for terminal interfaces:
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
class Layout < Milktea::Container
|
481
|
+
direction :row # or :column (default)
|
482
|
+
child Header, flex: 1
|
483
|
+
child Content, flex: 3
|
484
|
+
child Footer, flex: 1
|
485
|
+
end
|
486
|
+
```
|
487
|
+
|
488
|
+
### Bounds Management
|
489
|
+
|
490
|
+
```mermaid
|
491
|
+
graph TB
|
492
|
+
Container[Container<br/>80x24 @(0,0)] --> FlexCalc[Flex Calculator]
|
493
|
+
FlexCalc --> |flex: 1| Child1[Child 1<br/>80x4.8 @(0,0)]
|
494
|
+
FlexCalc --> |flex: 3| Child2[Child 2<br/>80x14.4 @(0,4.8)]
|
495
|
+
FlexCalc --> |flex: 1| Child3[Child 3<br/>80x4.8 @(0,19.2)]
|
496
|
+
|
497
|
+
subgraph "Column Layout (direction: :column)"
|
498
|
+
Container
|
499
|
+
Child1
|
500
|
+
Child2
|
501
|
+
Child3
|
502
|
+
end
|
503
|
+
|
504
|
+
subgraph "Row Layout (direction: :row)"
|
505
|
+
ContainerRow[Container<br/>80x24 @(0,0)] --> FlexCalcRow[Flex Calculator]
|
506
|
+
FlexCalcRow --> |flex: 1| ChildR1[Child 1<br/>16x24 @(0,0)]
|
507
|
+
FlexCalcRow --> |flex: 3| ChildR2[Child 2<br/>48x24 @(16,0)]
|
508
|
+
FlexCalcRow --> |flex: 1| ChildR3[Child 3<br/>16x24 @(64,0)]
|
509
|
+
end
|
510
|
+
|
511
|
+
style Container fill:#e1f5fe
|
512
|
+
style ContainerRow fill:#e1f5fe
|
513
|
+
style FlexCalc fill:#fff3e0
|
514
|
+
style FlexCalcRow fill:#fff3e0
|
515
|
+
```
|
516
|
+
|
517
|
+
- **Automatic Calculation**: Parent containers calculate child bounds
|
518
|
+
- **Proportional Sizing**: Flex ratios determine space distribution
|
519
|
+
- **Coordinate Propagation**: X/Y positions calculated relative to parent
|
520
|
+
- **Dynamic Compatibility**: Bounds preserved through Symbol resolution
|
521
|
+
|
522
|
+
## Development Features
|
523
|
+
|
524
|
+
### Hot Reloading
|
525
|
+
|
526
|
+
```mermaid
|
527
|
+
sequenceDiagram
|
528
|
+
participant Dev as Developer
|
529
|
+
participant File as File System
|
530
|
+
participant Listen as Listen Gem
|
531
|
+
participant Zeitwerk as Zeitwerk
|
532
|
+
participant Runtime as Runtime
|
533
|
+
participant Model as Model
|
534
|
+
|
535
|
+
Dev->>File: Edit Ruby file
|
536
|
+
File->>Listen: File change detected
|
537
|
+
Listen->>Zeitwerk: Trigger reload
|
538
|
+
Zeitwerk->>Zeitwerk: Reload class definitions
|
539
|
+
Zeitwerk->>Runtime: Send Reload message
|
540
|
+
Runtime->>Model: Process reload
|
541
|
+
Model->>Model: Rebuild with fresh classes<br/>(Kernel.const_get)
|
542
|
+
Model->>Runtime: Continue with updated model
|
543
|
+
|
544
|
+
Note over Dev,Model: State preserved during reload
|
545
|
+
```
|
546
|
+
|
547
|
+
When enabled in development:
|
548
|
+
1. Listen gem watches Ruby files for changes
|
549
|
+
2. Zeitwerk reloads changed class definitions
|
550
|
+
3. Reload message sent to runtime
|
551
|
+
4. Models rebuilt with fresh classes using `Kernel.const_get`
|
552
|
+
|
553
|
+
### Configuration
|
554
|
+
|
555
|
+
```ruby
|
556
|
+
Milktea.configure do |config|
|
557
|
+
config.autoload_dirs = ["app/models"]
|
558
|
+
config.hot_reloading = true
|
559
|
+
config.output = $stdout
|
560
|
+
end
|
561
|
+
```
|
562
|
+
|
563
|
+
## Performance Characteristics
|
564
|
+
|
565
|
+
### Rendering Optimization
|
566
|
+
|
567
|
+
- **Message-based rendering**: Only render on meaningful state changes
|
568
|
+
- **Atomic updates**: Screen cleared and redrawn in single operation
|
569
|
+
- **60 FPS event loop**: Consistent timing for smooth interaction
|
570
|
+
- **Non-blocking input**: Keyboard input processed without blocking
|
571
|
+
|
572
|
+
### Memory Management
|
573
|
+
|
574
|
+
- **Immutable objects**: Prevents accidental mutation
|
575
|
+
- **Structural sharing**: Ruby's copy-on-write for efficiency
|
576
|
+
- **Frozen state**: Explicit immutability enforcement
|
577
|
+
- **Minimal allocation**: Efficient object creation patterns
|
578
|
+
|
579
|
+
## Testing Architecture
|
580
|
+
|
581
|
+
The framework is designed for testability:
|
582
|
+
|
583
|
+
- **Dependency injection**: Runtime and renderer can be mocked
|
584
|
+
- **Pure functions**: View and update methods are deterministic
|
585
|
+
- **Immutable state**: Easy to test state transitions
|
586
|
+
- **Message-based**: Clear input/output for testing
|
587
|
+
|
588
|
+
## Design Principles
|
589
|
+
|
590
|
+
### Functional Programming
|
591
|
+
|
592
|
+
- **Immutable data**: All state changes create new objects
|
593
|
+
- **Pure functions**: Predictable behavior without side effects
|
594
|
+
- **Composable components**: Build complex UIs from simple parts
|
595
|
+
- **Declarative style**: Describe what you want, not how to achieve it
|
596
|
+
|
597
|
+
### Developer Experience
|
598
|
+
|
599
|
+
- **Hot reloading**: Instant feedback during development
|
600
|
+
- **Clear error messages**: Descriptive errors for debugging
|
601
|
+
- **Minimal boilerplate**: Sensible defaults reduce code
|
602
|
+
- **Consistent patterns**: Unified approach across framework
|
603
|
+
|
604
|
+
### Architecture Simplicity
|
605
|
+
|
606
|
+
- **Single responsibility**: Each component has a clear purpose
|
607
|
+
- **Loose coupling**: Components interact through well-defined interfaces
|
608
|
+
- **Dependency injection**: Easy to test and customize
|
609
|
+
- **Event-driven**: Clean separation of concerns through messages
|
610
|
+
|
611
|
+
## Current Implementation Status
|
612
|
+
|
613
|
+
This framework is actively developed with:
|
614
|
+
- **Complete Elm Architecture implementation**
|
615
|
+
- **Functional Container layout system**
|
616
|
+
- **Dynamic child resolution framework-wide**
|
617
|
+
- **Hot reloading with Zeitwerk integration**
|
618
|
+
- **Thread-safe configuration management**
|
619
|
+
- **Comprehensive test coverage**
|
620
|
+
|
621
|
+
The architecture provides a solid foundation for building maintainable, interactive terminal applications using functional programming principles while maintaining Ruby's dynamic capabilities.
|