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/README.md
ADDED
@@ -0,0 +1,382 @@
|
|
1
|
+
# Milktea
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/milktea)
|
4
|
+
[](https://github.com/elct9620/milktea/actions)
|
5
|
+
|
6
|
+
A Terminal User Interface (TUI) framework for Ruby, inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) from Go. Milktea brings the power of the Elm Architecture to Ruby, enabling you to build rich, interactive command-line applications with composable components and reactive state management.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- 🏗️ **Elm Architecture**: Immutable state management with predictable message flow
|
11
|
+
- 📦 **Container Layouts**: Flexbox-style layouts for terminal interfaces
|
12
|
+
- 🔄 **Hot Reloading**: Instant feedback during development (similar to web frameworks)
|
13
|
+
- 📱 **Responsive Design**: Automatic adaptation to terminal resize events
|
14
|
+
- 🧩 **Composable Components**: Build complex UIs from simple, reusable models
|
15
|
+
- 🎨 **Rich Terminal Support**: Leverage TTY gems for advanced terminal features
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add Milktea to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'milktea'
|
23
|
+
```
|
24
|
+
|
25
|
+
Or install directly:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
gem install milktea
|
29
|
+
```
|
30
|
+
|
31
|
+
For development versions:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem 'milktea', git: 'https://github.com/elct9620/milktea'
|
35
|
+
```
|
36
|
+
|
37
|
+
## Quick Start
|
38
|
+
|
39
|
+
Here's a simple "Hello World" application:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require 'milktea'
|
43
|
+
|
44
|
+
class HelloModel < Milktea::Model
|
45
|
+
def view
|
46
|
+
"Hello, #{state[:name]}! Count: #{state[:count]}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def update(message)
|
50
|
+
case message
|
51
|
+
when Milktea::Message::KeyPress
|
52
|
+
case message.value
|
53
|
+
when "+"
|
54
|
+
[with(count: state[:count] + 1), Milktea::Message::None.new]
|
55
|
+
when "q"
|
56
|
+
[self, Milktea::Message::Exit.new]
|
57
|
+
else
|
58
|
+
[self, Milktea::Message::None.new]
|
59
|
+
end
|
60
|
+
else
|
61
|
+
[self, Milktea::Message::None.new]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def default_state
|
68
|
+
{ name: "World", count: 0 }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Simple approach using Application class
|
73
|
+
class MyApp < Milktea::Application
|
74
|
+
root "HelloModel"
|
75
|
+
end
|
76
|
+
|
77
|
+
MyApp.boot
|
78
|
+
```
|
79
|
+
|
80
|
+
## Core Concepts
|
81
|
+
|
82
|
+
### Models & Elm Architecture
|
83
|
+
|
84
|
+
Milktea follows the Elm Architecture pattern with three core concepts:
|
85
|
+
|
86
|
+
- **Model**: Immutable state container
|
87
|
+
- **View**: Pure function that renders state to string
|
88
|
+
- **Update**: Handles messages and returns new state + side effects
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class CounterModel < Milktea::Model
|
92
|
+
def view
|
93
|
+
"Count: #{state[:count]} (Press +/- to change, q to quit)"
|
94
|
+
end
|
95
|
+
|
96
|
+
def update(message)
|
97
|
+
case message
|
98
|
+
when Milktea::Message::KeyPress
|
99
|
+
handle_keypress(message)
|
100
|
+
when Milktea::Message::Resize
|
101
|
+
# Rebuild model with fresh class for new screen dimensions
|
102
|
+
[with, Milktea::Message::None.new]
|
103
|
+
else
|
104
|
+
[self, Milktea::Message::None.new]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def default_state
|
111
|
+
{ count: 0 }
|
112
|
+
end
|
113
|
+
|
114
|
+
def handle_keypress(message)
|
115
|
+
case message.value
|
116
|
+
when "+"
|
117
|
+
[with(count: state[:count] + 1), Milktea::Message::None.new]
|
118
|
+
when "-"
|
119
|
+
[with(count: state[:count] - 1), Milktea::Message::None.new]
|
120
|
+
when "q"
|
121
|
+
[self, Milktea::Message::Exit.new]
|
122
|
+
else
|
123
|
+
[self, Milktea::Message::None.new]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Container Layout System
|
130
|
+
|
131
|
+
Milktea provides a flexbox-inspired layout system for building complex terminal interfaces:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class AppLayout < Milktea::Container
|
135
|
+
direction :column
|
136
|
+
child HeaderModel, flex: 1
|
137
|
+
child ContentModel, flex: 3
|
138
|
+
child FooterModel, flex: 1
|
139
|
+
end
|
140
|
+
|
141
|
+
class SidebarLayout < Milktea::Container
|
142
|
+
direction :row
|
143
|
+
child SidebarModel, flex: 1
|
144
|
+
child MainContentModel, flex: 3
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
#### Key Container Features:
|
149
|
+
|
150
|
+
- **Direction**: `:row` or `:column` (default: `:column`)
|
151
|
+
- **Flex Properties**: Control size ratios between children
|
152
|
+
- **State Mapping**: Pass specific state portions to children
|
153
|
+
- **Bounds Calculation**: Automatic layout calculation and propagation
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class AdvancedContainer < Milktea::Container
|
157
|
+
direction :row
|
158
|
+
|
159
|
+
# Pass specific state to children with state mappers
|
160
|
+
child SidebarModel, ->(state) { { items: state[:sidebar_items] } }, flex: 1
|
161
|
+
child ContentModel, ->(state) { state.slice(:title, :content) }, flex: 2
|
162
|
+
child InfoModel, flex: 1
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
### Hot Reloading (Development Feature)
|
167
|
+
|
168
|
+
Milktea supports hot reloading for rapid development iteration:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# Configure hot reloading
|
172
|
+
Milktea.configure do |config|
|
173
|
+
config.autoload_dirs = ["app/models", "lib/components"]
|
174
|
+
config.hot_reloading = true
|
175
|
+
end
|
176
|
+
|
177
|
+
class DevelopmentModel < Milktea::Model
|
178
|
+
def update(message)
|
179
|
+
case message
|
180
|
+
when Milktea::Message::Reload
|
181
|
+
# Hot reload detected - rebuild with fresh class
|
182
|
+
[with, Milktea::Message::None.new]
|
183
|
+
# ... other message handling
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
When files change, Milktea automatically detects the changes and sends `Message::Reload` events. Simply handle this message by rebuilding your model with `[with, Milktea::Message::None.new]` to pick up the latest code changes.
|
190
|
+
|
191
|
+
### Terminal Resize Handling
|
192
|
+
|
193
|
+
Milktea automatically detects terminal resize events and provides a simple pattern for responsive layouts:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class ResponsiveApp < Milktea::Container
|
197
|
+
direction :column
|
198
|
+
child HeaderModel, flex: 1
|
199
|
+
child DynamicContentModel, flex: 4
|
200
|
+
|
201
|
+
def update(message)
|
202
|
+
case message
|
203
|
+
when Milktea::Message::Resize
|
204
|
+
# Only root model needs resize handling
|
205
|
+
# All children automatically recalculate bounds
|
206
|
+
[with, Milktea::Message::None.new]
|
207
|
+
when Milktea::Message::KeyPress
|
208
|
+
handle_keypress(message)
|
209
|
+
else
|
210
|
+
[self, Milktea::Message::None.new]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
#### Resize Handling Key Points:
|
217
|
+
|
218
|
+
- **Root-Level Only**: Only the root model needs to handle `Message::Resize`
|
219
|
+
- **Automatic Cascading**: Child components automatically adapt to new dimensions
|
220
|
+
- **Bounds Recalculation**: Container layouts automatically recalculate flex distributions
|
221
|
+
- **Screen Methods**: Use `screen_width`, `screen_height`, `screen_size` for responsive logic
|
222
|
+
|
223
|
+
## Examples
|
224
|
+
|
225
|
+
Explore the `examples/` directory for comprehensive demonstrations:
|
226
|
+
|
227
|
+
- **[Container Layout](examples/container_layout.rb)**: Flexbox-style layouts with resize support
|
228
|
+
- **[Hot Reload Demo](examples/hot_reload_demo.rb)**: Development workflow with instant updates
|
229
|
+
|
230
|
+
Run examples:
|
231
|
+
|
232
|
+
```bash
|
233
|
+
ruby examples/container_layout.rb
|
234
|
+
ruby examples/hot_reload_demo.rb
|
235
|
+
```
|
236
|
+
|
237
|
+
## Advanced Features
|
238
|
+
|
239
|
+
### Dynamic Child Resolution
|
240
|
+
|
241
|
+
Use symbols to dynamically resolve child components:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
class DynamicContainer < Milktea::Container
|
245
|
+
direction :column
|
246
|
+
child :header_component, flex: 1 # Calls header_component method
|
247
|
+
child ContentModel, flex: 3 # Direct class reference
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
def header_component
|
252
|
+
state[:show_advanced] ? AdvancedHeader : SimpleHeader
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
### Custom Message Handling
|
258
|
+
|
259
|
+
Create custom messages for complex interactions:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
# Define custom message
|
263
|
+
CustomAction = Data.define(:action_type, :payload)
|
264
|
+
|
265
|
+
class CustomModel < Milktea::Model
|
266
|
+
def update(message)
|
267
|
+
case message
|
268
|
+
when CustomAction
|
269
|
+
handle_custom_action(message)
|
270
|
+
# ... standard message handling
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
def handle_custom_action(message)
|
277
|
+
case message.action_type
|
278
|
+
when :save
|
279
|
+
# Handle save action
|
280
|
+
[with(saved: true), Milktea::Message::None.new]
|
281
|
+
when :load
|
282
|
+
# Handle load action
|
283
|
+
[with(data: message.payload), Milktea::Message::None.new]
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
### Application vs Manual Setup
|
290
|
+
|
291
|
+
Choose between high-level Application class or manual setup:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
# High-level Application approach (recommended)
|
295
|
+
class MyApp < Milktea::Application
|
296
|
+
root "MainModel"
|
297
|
+
end
|
298
|
+
|
299
|
+
MyApp.boot
|
300
|
+
|
301
|
+
# Manual setup (advanced)
|
302
|
+
config = Milktea.configure do |c|
|
303
|
+
c.autoload_dirs = ["app/models"]
|
304
|
+
c.hot_reloading = true
|
305
|
+
end
|
306
|
+
|
307
|
+
loader = Milktea::Loader.new(config)
|
308
|
+
loader.hot_reload if config.hot_reloading?
|
309
|
+
|
310
|
+
model = MainModel.new
|
311
|
+
program = Milktea::Program.new(model, config: config)
|
312
|
+
program.run
|
313
|
+
```
|
314
|
+
|
315
|
+
## API Reference
|
316
|
+
|
317
|
+
### Core Classes
|
318
|
+
|
319
|
+
- **`Milktea::Model`**: Base class for all UI components
|
320
|
+
- **`Milktea::Container`**: Layout container with flexbox-style properties
|
321
|
+
- **`Milktea::Application`**: High-level application wrapper
|
322
|
+
- **`Milktea::Program`**: Main application runtime
|
323
|
+
- **`Milktea::Message`**: Standard message types (KeyPress, Exit, Resize, Reload)
|
324
|
+
|
325
|
+
### Message System
|
326
|
+
|
327
|
+
- **`Message::KeyPress`**: Keyboard input events
|
328
|
+
- **`Message::Exit`**: Application termination
|
329
|
+
- **`Message::Resize`**: Terminal size changes
|
330
|
+
- **`Message::Reload`**: Hot reload events
|
331
|
+
- **`Message::None`**: No-operation message
|
332
|
+
|
333
|
+
For detailed API documentation, see the [documentation website](https://rubydoc.info/gems/milktea).
|
334
|
+
|
335
|
+
## Development
|
336
|
+
|
337
|
+
After checking out the repo:
|
338
|
+
|
339
|
+
```bash
|
340
|
+
bin/setup # Install dependencies
|
341
|
+
bundle exec rake spec # Run tests
|
342
|
+
bundle exec rake rubocop # Check code style
|
343
|
+
bundle exec rake # Run all checks
|
344
|
+
bin/console # Interactive prompt
|
345
|
+
```
|
346
|
+
|
347
|
+
### Testing
|
348
|
+
|
349
|
+
Milktea uses RSpec for testing. Run specific tests:
|
350
|
+
|
351
|
+
```bash
|
352
|
+
bundle exec rspec spec/milktea/model_spec.rb
|
353
|
+
bundle exec rspec spec/milktea/container_spec.rb:42 # Specific line
|
354
|
+
```
|
355
|
+
|
356
|
+
### Code Quality
|
357
|
+
|
358
|
+
The project uses RuboCop for code formatting:
|
359
|
+
|
360
|
+
```bash
|
361
|
+
bundle exec rake rubocop:autocorrect # Fix auto-correctable issues
|
362
|
+
```
|
363
|
+
|
364
|
+
## Contributing
|
365
|
+
|
366
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/elct9620/milktea.
|
367
|
+
|
368
|
+
1. Fork the repository
|
369
|
+
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
370
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
371
|
+
4. Push to the branch (`git push origin feature/my-new-feature`)
|
372
|
+
5. Create a Pull Request
|
373
|
+
|
374
|
+
## License
|
375
|
+
|
376
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
377
|
+
|
378
|
+
## Acknowledgments
|
379
|
+
|
380
|
+
- Inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) - Go TUI framework
|
381
|
+
- Built on the [TTY toolkit](https://ttytoolkit.org/) ecosystem
|
382
|
+
- Follows [Elm Architecture](https://guide.elm-lang.org/architecture/) principles
|
data/Rakefile
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Development Log - 2025-07-03
|
2
|
+
|
3
|
+
## What's New
|
4
|
+
|
5
|
+
#### Renderer Abstraction for Clean Separation of Concerns
|
6
|
+
|
7
|
+
The Milktea framework now features a dedicated `Renderer` class that handles all terminal user interface rendering operations. This new abstraction brings several benefits to TUI developers:
|
8
|
+
|
9
|
+
- **Screen Management**: Automatic screen clearing and cursor positioning using the `tty-cursor` gem ensures clean rendering without visual artifacts
|
10
|
+
- **Output Flexibility**: Support for custom output streams allows easy testing with `StringIO` and potential future support for different output formats
|
11
|
+
- **Lifecycle Methods**: Clear `setup_screen()` and `restore_screen()` methods handle terminal state management, ensuring proper cleanup when programs exit
|
12
|
+
- **Clean API**: Simple `render(model)` method that takes care of all rendering complexities internally
|
13
|
+
|
14
|
+
This separation allows developers to focus on their application logic while the renderer handles the intricacies of terminal manipulation.
|
15
|
+
|
16
|
+
#### Full Keyboard Event Handling
|
17
|
+
|
18
|
+
Interactive TUI applications now have access to comprehensive keyboard input through integration with `TTY::Reader`. The new `KeyPress` message type provides rich event data:
|
19
|
+
|
20
|
+
- **Complete Key Information**: Access to both raw key values and character representations
|
21
|
+
- **Modifier Key Support**: Built-in tracking of Ctrl, Alt, and Shift states enables complex keyboard shortcuts
|
22
|
+
- **Non-blocking Input**: Keyboard events are read without blocking the main event loop, maintaining responsive applications
|
23
|
+
- **Graceful Interrupt Handling**: Ctrl+C is properly handled through TTY::Reader's error mode, preventing abrupt terminations
|
24
|
+
|
25
|
+
This enhancement empowers developers to create sophisticated keyboard-driven interfaces with minimal effort.
|
26
|
+
|
27
|
+
#### Counter Example Application
|
28
|
+
|
29
|
+
A new example demonstrates best practices for building Milktea applications. The counter showcases:
|
30
|
+
|
31
|
+
- **State Management**: Proper use of immutable state updates through the `Model#with` method
|
32
|
+
- **User Interaction**: Intuitive keyboard controls (+/k for increment, -/j for decrement, r for reset, q for quit)
|
33
|
+
- **Clean UI**: Clear instructions and visual feedback demonstrate effective TUI design
|
34
|
+
- **Architecture Patterns**: Real-world application of the Elm Architecture within the Ruby ecosystem
|
35
|
+
|
36
|
+
This example serves as both a learning resource and a template for new TUI applications.
|
37
|
+
|
38
|
+
## What's Fixed
|
39
|
+
|
40
|
+
#### Screen Clearing and Rendering Artifacts
|
41
|
+
|
42
|
+
**Problem**: Previous renders would leave visual artifacts on screen, creating a messy user experience as old content remained visible beneath new renders.
|
43
|
+
|
44
|
+
**Solution**: The new Renderer class now properly clears the entire screen and resets the cursor to position (0,0) before each render cycle. This ensures each frame starts with a clean slate, eliminating any overlap or ghosting effects from previous renders.
|
45
|
+
|
46
|
+
**Impact**: TUI applications now provide a clean, professional appearance with smooth visual updates that don't leave traces of previous states.
|
47
|
+
|
48
|
+
#### Hardcoded Exit Key Limitations
|
49
|
+
|
50
|
+
**Problem**: The framework previously hardcoded certain keys for exiting applications, limiting developer control over keyboard interactions and preventing custom exit strategies.
|
51
|
+
|
52
|
+
**Solution**: Removed all hardcoded exit keys from the Program class. Models now have complete control over keyboard event handling, including when and how to terminate the application.
|
53
|
+
|
54
|
+
**Impact**: Developers can now implement custom exit confirmations, save prompts, or completely different key bindings for application termination, providing full control over the user experience.
|
55
|
+
|
56
|
+
## Design Decisions
|
57
|
+
|
58
|
+
#### Renderer as a Separate Component
|
59
|
+
|
60
|
+
**Context**: The Program class was becoming complex, handling both event loop management and rendering responsibilities. This violated the single responsibility principle and made testing difficult.
|
61
|
+
|
62
|
+
**Choice**: Extract all rendering logic into a dedicated Renderer class that can be injected into the Program.
|
63
|
+
|
64
|
+
**Rationale**: This separation provides multiple benefits:
|
65
|
+
- Easier unit testing through dependency injection
|
66
|
+
- Potential for alternative renderer implementations (e.g., web-based output)
|
67
|
+
- Cleaner Program class focused solely on event loop management
|
68
|
+
- Better adherence to SOLID principles
|
69
|
+
|
70
|
+
This decision aligns with the framework's clean architecture approach and makes the codebase more maintainable and extensible.
|
71
|
+
|
72
|
+
#### Enhanced Testing Philosophy
|
73
|
+
|
74
|
+
**Context**: The existing RSpec tests were using various patterns, some of which tested implementation details rather than behavior.
|
75
|
+
|
76
|
+
**Choice**: Established comprehensive testing guidelines emphasizing behavioral testing over implementation testing.
|
77
|
+
|
78
|
+
**Rationale**: The new guidelines promote:
|
79
|
+
- Using spy patterns for cleaner delegation tests
|
80
|
+
- Avoiding private instance variable testing
|
81
|
+
- Leveraging RSpec's built-in matchers like `output`
|
82
|
+
- Focusing on public API behavior
|
83
|
+
|
84
|
+
These practices lead to more maintainable tests that don't break with internal refactoring, while still ensuring correctness.
|
85
|
+
|
86
|
+
#### Non-blocking Keyboard Input
|
87
|
+
|
88
|
+
**Context**: TUI applications need to remain responsive while waiting for user input, but blocking I/O operations can freeze the interface.
|
89
|
+
|
90
|
+
**Choice**: Implement keyboard reading using TTY::Reader's non-blocking `read_keypress` method within the main event loop.
|
91
|
+
|
92
|
+
**Rationale**: This approach ensures:
|
93
|
+
- The event loop continues processing messages and timers while waiting for input
|
94
|
+
- Applications can implement animations or real-time updates
|
95
|
+
- Better user experience with responsive interfaces
|
96
|
+
- Alignment with modern event-driven programming patterns
|
97
|
+
|
98
|
+
## Impact
|
99
|
+
|
100
|
+
These changes significantly enhance the Milktea framework's usability and architecture. Ruby developers building TUI applications now have:
|
101
|
+
|
102
|
+
- **Better Separation of Concerns**: Clear boundaries between rendering, input handling, and application logic
|
103
|
+
- **More Control**: Full ownership of keyboard handling and application behavior
|
104
|
+
- **Improved Testing**: Cleaner patterns for writing maintainable test suites
|
105
|
+
- **Professional Output**: Artifact-free rendering for polished user interfaces
|
106
|
+
- **Learning Resources**: Practical examples demonstrating framework best practices
|
107
|
+
|
108
|
+
The framework now provides a more solid foundation for building sophisticated terminal applications while maintaining the simplicity and elegance of the Elm Architecture.
|
109
|
+
|
110
|
+
## Files Modified
|
111
|
+
|
112
|
+
- `lib/milktea/renderer.rb` - New Renderer class for TUI rendering
|
113
|
+
- `lib/milktea/program.rb` - Refactored to use Renderer and handle keyboard events
|
114
|
+
- `lib/milktea/message.rb` - Added KeyPress message type
|
115
|
+
- `examples/counter.rb` - New counter example application
|
116
|
+
- `examples/simple.rb` - Updated with keyboard interaction
|
117
|
+
- `spec/milktea/renderer_spec.rb` - Tests for new Renderer class
|
118
|
+
- `spec/milktea/program_spec.rb` - Updated tests for refactored Program
|
119
|
+
- `CLAUDE.md` - Enhanced RSpec testing guidelines
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# Development Log - 2025-07-04 (Session 2)
|
2
|
+
|
3
|
+
## What's New
|
4
|
+
|
5
|
+
#### Working Hot Reloading System with Production-Ready Example
|
6
|
+
Successfully implemented a fully functional hot reloading system for Milktea applications, complete with a comprehensive demonstration example. The system enables developers to modify TUI components in real-time while applications are running, dramatically improving development velocity and iteration speed.
|
7
|
+
|
8
|
+
Key features include:
|
9
|
+
- **Live code reloading**: Edit model files and see changes immediately without restarting
|
10
|
+
- **Automatic class refresh**: Uses `Kernel.const_get` to ensure fresh class definitions are loaded
|
11
|
+
- **Graceful degradation**: Works with or without the Listen gem for file watching
|
12
|
+
- **Interactive demo**: Complete example showing parent-child model reloading with clear instructions
|
13
|
+
|
14
|
+
#### Independent Loader Architecture
|
15
|
+
Redesigned the Loader system to be completely self-contained, eliminating problematic circular dependencies that were preventing proper Model loading. The new architecture separates concerns cleanly and provides a simpler API for developers.
|
16
|
+
|
17
|
+
The Loader now:
|
18
|
+
- Takes a config object and extracts what it needs internally
|
19
|
+
- Automatically handles hot reloading based on configuration
|
20
|
+
- Operates independently from the core framework components
|
21
|
+
- Provides a single `start` method that handles all initialization
|
22
|
+
|
23
|
+
#### Comprehensive Hot Reloading Documentation
|
24
|
+
Added extensive documentation to CLAUDE.md covering all critical aspects of hot reloading setup and troubleshooting. This includes configuration requirements, implementation gotchas, and step-by-step testing procedures that will be essential for future development.
|
25
|
+
|
26
|
+
## What's Fixed
|
27
|
+
|
28
|
+
#### Circular Dependency Resolution
|
29
|
+
Resolved critical circular dependencies in the original Loader design where Config needed Runtime to create Loader, but Loader needed Config's runtime and app_path. This was causing Model loading failures and tight coupling between components.
|
30
|
+
|
31
|
+
**Root cause**: The original design tried to inject dependencies through Config, creating a circular reference loop.
|
32
|
+
|
33
|
+
**Solution**: Made Loader completely independent by having it extract needed values from a passed config object, eliminating the circular dependency chain.
|
34
|
+
|
35
|
+
#### Hot Reloading Class Reference Issues
|
36
|
+
Fixed fundamental issue where hot reloading wasn't working because `self.class.new` was returning cached/stale class objects instead of freshly reloaded classes.
|
37
|
+
|
38
|
+
**Root cause**: Ruby's `self.class` returns the class object that was loaded when the instance was created, not the current definition.
|
39
|
+
|
40
|
+
**Solution**: Replaced `self.class.new` with `Kernel.const_get(self.class.name).new` in the Model#with method to ensure fresh class definitions are used.
|
41
|
+
|
42
|
+
#### Zeitwerk Configuration for Examples
|
43
|
+
Corrected app_dir configuration for examples directory to work with Zeitwerk's autoloading requirements. Examples lack Gemfile and other project structure, requiring special path handling.
|
44
|
+
|
45
|
+
**Impact**: Hot reloading now works correctly for standalone examples, demonstrating the system's flexibility across different project structures.
|
46
|
+
|
47
|
+
#### Message::Reload Event Handling
|
48
|
+
Implemented proper reload event handling in models using the `with` method to rebuild instances with fresh class definitions. This ensures the entire model tree is updated when code changes are detected.
|
49
|
+
|
50
|
+
## Design Decisions
|
51
|
+
|
52
|
+
#### Loader Independence from Framework Core
|
53
|
+
**Context**: The original design embedded Loader configuration within the Config class, creating tight coupling and circular dependencies.
|
54
|
+
|
55
|
+
**Decision**: Make Loader a completely independent utility that takes a config object and operates autonomously.
|
56
|
+
|
57
|
+
**Rationale**: This design provides several key benefits:
|
58
|
+
- Eliminates circular dependencies that were causing loading failures
|
59
|
+
- Separates development tooling from core framework functionality
|
60
|
+
- Simplifies the API for users (single `loader.start()` call)
|
61
|
+
- Allows Program to focus solely on TUI concerns
|
62
|
+
- Makes testing easier with clear dependency boundaries
|
63
|
+
|
64
|
+
This decision reinforces the Clean Architecture principle of dependency inversion, where high-level modules (core framework) don't depend on low-level modules (development tools).
|
65
|
+
|
66
|
+
#### Kernel.const_get for Dynamic Class Loading
|
67
|
+
**Context**: Hot reloading requires getting fresh class definitions after code changes, but `self.class` returns cached objects.
|
68
|
+
|
69
|
+
**Decision**: Use `Kernel.const_get(self.class.name).new(merged_state)` instead of `self.class.new(merged_state)` in Model#with.
|
70
|
+
|
71
|
+
**Rationale**: This approach ensures that:
|
72
|
+
- Fresh class definitions are used after Zeitwerk reloads classes
|
73
|
+
- The hot reloading system works reliably across all model types
|
74
|
+
- No additional complexity is introduced to the Model API
|
75
|
+
- The change is backward compatible and doesn't affect normal operation
|
76
|
+
|
77
|
+
The trade-off is slightly more dynamic code, but this is isolated to the development scenario and provides essential functionality.
|
78
|
+
|
79
|
+
#### App Directory Configuration Strategy
|
80
|
+
**Context**: Zeitwerk requires specific directory structures, and examples have different project layouts than full applications.
|
81
|
+
|
82
|
+
**Decision**: Require app_dir to point directly to the models directory, with full paths for examples.
|
83
|
+
|
84
|
+
**Rationale**: This decision acknowledges current Zeitwerk limitations while providing a working solution:
|
85
|
+
- Enables immediate hot reloading functionality
|
86
|
+
- Provides clear path configuration examples
|
87
|
+
- Documents the constraint for future improvement
|
88
|
+
- Allows examples to work alongside full applications
|
89
|
+
|
90
|
+
Noted in documentation that this will be improved in future refactoring to be more flexible.
|
91
|
+
|
92
|
+
#### Message-Driven Reload Communication
|
93
|
+
**Context**: Hot reloading needs to trigger model rebuilding when code changes are detected.
|
94
|
+
|
95
|
+
**Decision**: Use the existing Message system with `Message::Reload` events rather than creating a separate callback mechanism.
|
96
|
+
|
97
|
+
**Rationale**: This maintains consistency with the framework's event-driven architecture:
|
98
|
+
- Leverages existing message processing infrastructure
|
99
|
+
- Keeps reload handling within the normal update cycle
|
100
|
+
- Provides predictable behavior that follows Elm Architecture patterns
|
101
|
+
- Allows models to handle reloading as part of their normal message processing
|
102
|
+
|
103
|
+
## Impact
|
104
|
+
|
105
|
+
The completion of the hot reloading system represents a major milestone for the Milktea framework's developer experience. These changes provide:
|
106
|
+
|
107
|
+
**For TUI Application Developers**: Hot reloading dramatically reduces development time by eliminating the need to restart applications when making changes. Developers can now iterate on UI layouts, business logic, and interactions in real-time, seeing results immediately.
|
108
|
+
|
109
|
+
**For Framework Architecture**: The Loader independence eliminates a significant architectural debt and provides a clean foundation for future development tooling. The separation of concerns makes the framework more modular and testable.
|
110
|
+
|
111
|
+
**For Example Development**: The working hot reloading example serves as both a demonstration and a template for developers setting up their own applications with hot reloading capabilities.
|
112
|
+
|
113
|
+
**For Project Documentation**: The comprehensive CLAUDE.md documentation ensures that critical setup knowledge is preserved and accessible to future contributors and users.
|
114
|
+
|
115
|
+
The changes demonstrate the framework's maturity in supporting professional development workflows while maintaining the simplicity and elegance of the core TUI architecture.
|
116
|
+
|
117
|
+
## Files Modified
|
118
|
+
|
119
|
+
- `lib/milktea/loader.rb` - Redesigned to be independent from Config, automatic hot_reload handling
|
120
|
+
- `lib/milktea/model.rb` - Updated Model#with to use Kernel.const_get for fresh class definitions
|
121
|
+
- `lib/milktea/config.rb` - Removed loader from Config class to eliminate circular dependencies
|
122
|
+
- `lib/milktea/program.rb` - Removed loader logic to focus on core TUI functionality
|
123
|
+
- `examples/hot_reload_demo.rb` - Updated to use new independent Loader pattern
|
124
|
+
- `examples/hot_reload_demo/models/demo_model.rb` - Added Message::Reload handling with proper rebuilding
|
125
|
+
- `examples/hot_reload_demo/models/status_model.rb` - Child model demonstrating nested reloading
|
126
|
+
- `spec/milktea/loader_spec.rb` - Updated tests for new constructor and automatic hot_reload behavior
|
127
|
+
- `spec/milktea/config_spec.rb` - Removed loader-related tests to match simplified Config
|
128
|
+
- `CLAUDE.md` - Added comprehensive Hot Reloading Development section with setup and troubleshooting
|
129
|
+
- `ARCHITECTURE.md` - Updated to reflect new optional Loader system design patterns
|