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.
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.