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/CLAUDE.md
ADDED
@@ -0,0 +1,537 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
Milktea is a Terminal User Interface (TUI) framework for Ruby, inspired by the Bubble Tea framework for Go. It's designed to help developers create interactive command-line applications with rich terminal interfaces.
|
8
|
+
|
9
|
+
## Development Commands
|
10
|
+
|
11
|
+
### Testing
|
12
|
+
- `bundle exec rake spec` - Run all RSpec tests
|
13
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb` - Run a specific test file
|
14
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb:42` - Run a specific test at line 42
|
15
|
+
|
16
|
+
### Code Quality
|
17
|
+
- `bundle exec rake rubocop` - Run RuboCop linting
|
18
|
+
- `bundle exec rake rubocop:autocorrect` - Auto-fix safe violations
|
19
|
+
- `bundle exec rake` - Run default task (specs + RuboCop)
|
20
|
+
|
21
|
+
### Building and Installation
|
22
|
+
- `bundle exec rake build` - Build gem into pkg/ directory
|
23
|
+
- `bundle exec rake install:local` - Install gem locally for testing
|
24
|
+
|
25
|
+
### Development Tools
|
26
|
+
- `bin/console` - Interactive Ruby console with gem loaded
|
27
|
+
|
28
|
+
## Application Development
|
29
|
+
|
30
|
+
### Using Milktea::Application (Recommended)
|
31
|
+
|
32
|
+
The simplest way to create Milktea applications is using the Application class:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# Configure Milktea
|
36
|
+
Milktea.configure do |c|
|
37
|
+
c.autoload_dirs = ["app/models"]
|
38
|
+
c.hot_reloading = true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Define Application class
|
42
|
+
class MyApp < Milktea::Application
|
43
|
+
root "MainModel"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Start the application
|
47
|
+
MyApp.boot
|
48
|
+
```
|
49
|
+
|
50
|
+
### Manual Setup (Advanced)
|
51
|
+
|
52
|
+
For advanced use cases, you can manually configure the components:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# Configure with models directory paths
|
56
|
+
config = Milktea.configure do |c|
|
57
|
+
# IMPORTANT: autoload_dirs must point to directories containing models for Zeitwerk
|
58
|
+
c.autoload_dirs = ["examples/hot_reload_demo/models"]
|
59
|
+
c.hot_reloading = true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create independent loader
|
63
|
+
loader = Milktea::Loader.new(config)
|
64
|
+
loader.hot_reload # Manually start hot reloading
|
65
|
+
|
66
|
+
# Create and run program
|
67
|
+
model = DemoModel.new
|
68
|
+
program = Milktea::Program.new(model, config: config)
|
69
|
+
```
|
70
|
+
|
71
|
+
## Hot Reloading Development
|
72
|
+
|
73
|
+
### Critical Implementation Details
|
74
|
+
|
75
|
+
1. **autoload_dirs Path Configuration**:
|
76
|
+
- `autoload_dirs` is an array of directories relative to project root
|
77
|
+
- For examples without Gemfile, include full path: `["examples/hot_reload_demo/models"]`
|
78
|
+
- Must point to directories containing models (Zeitwerk requirement)
|
79
|
+
- Multiple directories can be specified: `["app/models", "lib/components"]`
|
80
|
+
|
81
|
+
2. **Model Reload Handling**:
|
82
|
+
- Models must handle `Message::Reload` events
|
83
|
+
- Use `with` method to rebuild model instances with fresh classes
|
84
|
+
```ruby
|
85
|
+
when Milktea::Message::Reload
|
86
|
+
# Hot reload detected - rebuild model with fresh class
|
87
|
+
[with, Milktea::Message::None.new]
|
88
|
+
```
|
89
|
+
|
90
|
+
3. **Class Reference in Model#with**:
|
91
|
+
- Use `Kernel.const_get(self.class.name).new(merged_state)` instead of `self.class.new`
|
92
|
+
- This ensures fresh class definitions are used after reload
|
93
|
+
- `self.class` returns cached/old class objects during hot reload
|
94
|
+
|
95
|
+
4. **File Structure Requirements**:
|
96
|
+
- Zeitwerk requires models to be in a `models/` directory
|
97
|
+
- File names must match class names (e.g., `demo_model.rb` for `DemoModel`)
|
98
|
+
- Use conventional Ruby file naming (snake_case files, PascalCase classes)
|
99
|
+
|
100
|
+
### Testing Hot Reloading
|
101
|
+
|
102
|
+
1. Run the hot reload demo: `ruby examples/hot_reload_demo.rb`
|
103
|
+
2. Edit files in `examples/hot_reload_demo/models/` while program is running
|
104
|
+
3. Save changes and observe immediate updates in the running program
|
105
|
+
4. Try modifying:
|
106
|
+
- View content and layout
|
107
|
+
- Key bindings and behavior
|
108
|
+
- State structure and default values
|
109
|
+
- Child model interactions
|
110
|
+
|
111
|
+
### Application Class Features
|
112
|
+
|
113
|
+
- **Auto-registration**: Inheriting from `Application` automatically sets `Milktea.app`
|
114
|
+
- **Root model definition**: Use `root "ModelName"` to specify the entry point model
|
115
|
+
- **Simple startup**: Call `MyApp.boot` instead of manual instantiation
|
116
|
+
- **Automatic loader setup**: Loader configuration and setup handled automatically
|
117
|
+
- **Hot reloading integration**: Automatically starts hot reloading if configured
|
118
|
+
|
119
|
+
## Dynamic Child Resolution
|
120
|
+
|
121
|
+
### Symbol-Based Child Definitions
|
122
|
+
|
123
|
+
All Model classes support dynamic child resolution using Symbols that reference methods returning Class objects:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class DynamicModel < Milktea::Model
|
127
|
+
child :dynamic_child # References dynamic_child method
|
128
|
+
child SomeClass # Traditional class reference
|
129
|
+
|
130
|
+
def dynamic_child
|
131
|
+
state[:use_special] ? SpecialModel : RegularModel
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
### Container Dynamic Layouts
|
137
|
+
|
138
|
+
Containers can dynamically switch between layout types while preserving bounds:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class LayoutContainer < Milktea::Container
|
142
|
+
direction :column
|
143
|
+
child :status_bar, flex: 1
|
144
|
+
child :dynamic_layout, flex: 5
|
145
|
+
|
146
|
+
def status_bar
|
147
|
+
StatusBarModel
|
148
|
+
end
|
149
|
+
|
150
|
+
def dynamic_layout
|
151
|
+
state[:show_column] ? ColumnLayoutModel : RowLayoutModel
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Error Handling
|
157
|
+
|
158
|
+
- **NoMethodError**: Thrown when Symbol references non-existent method
|
159
|
+
- **ArgumentError**: Thrown when method returns non-Model class
|
160
|
+
- Clear error messages distinguish between missing methods and invalid types
|
161
|
+
|
162
|
+
### Troubleshooting
|
163
|
+
|
164
|
+
- **Models not reloading**: Check `autoload_dirs` points to correct models directories
|
165
|
+
- **Old behavior persists**: Ensure `Kernel.const_get` is used in `Model#with`
|
166
|
+
- **Reload events ignored**: Verify `Message::Reload` handling in model `update` method
|
167
|
+
- **Listen gem not found**: Install with `gem install listen` for file watching
|
168
|
+
|
169
|
+
## Architecture
|
170
|
+
|
171
|
+
This project follows Clean Architecture and Domain-Driven Design (DDD) principles. The codebase structure:
|
172
|
+
|
173
|
+
- `/lib/milktea.rb` - Main module entry point with Zeitwerk autoloading and app registry
|
174
|
+
- `/lib/milktea/application.rb` - High-level Application abstraction with auto-registration
|
175
|
+
- `/lib/milktea/config.rb` - Configuration system with autoload_dirs array support
|
176
|
+
- `/lib/milktea/loader.rb` - Zeitwerk autoloading and hot reloading with multiple directory support
|
177
|
+
- `/lib/milktea/model.rb` - Base Model class for Elm Architecture components with child model DSL and dynamic child resolution
|
178
|
+
- `/lib/milktea/runtime.rb` - Message processing and execution state management
|
179
|
+
- `/lib/milktea/program.rb` - Main TUI program with event loop and dependency injection
|
180
|
+
- `/lib/milktea/message.rb` - Message definitions for events
|
181
|
+
- Uses TTY gems for terminal interaction (tty-box, tty-cursor, tty-reader, tty-screen)
|
182
|
+
- Event handling through the `timers` gem
|
183
|
+
|
184
|
+
### Core Components
|
185
|
+
|
186
|
+
- **Application**: High-level abstraction that encapsulates Loader and Program setup for simplified usage
|
187
|
+
- **Model**: Base class implementing Elm Architecture with immutable state and dynamic child resolution
|
188
|
+
- **Runtime**: Manages message queue and execution state with dependency injection support
|
189
|
+
- **Program**: Handles terminal setup, rendering, and main event loop
|
190
|
+
- **Loader**: Manages Zeitwerk autoloading and hot reloading with Listen gem integration
|
191
|
+
- **Config**: Configuration system supporting multiple autoload directories and hot reloading settings
|
192
|
+
- **Message**: Event system using Ruby's Data.define for immutable messages
|
193
|
+
|
194
|
+
## Testing Approach
|
195
|
+
|
196
|
+
- RSpec 3.x for unit testing
|
197
|
+
- Test files mirror the lib structure in the spec/ directory
|
198
|
+
- Monkey patching is disabled for cleaner tests
|
199
|
+
- Run individual tests with line numbers for focused development
|
200
|
+
|
201
|
+
### RSpec Style Guide
|
202
|
+
|
203
|
+
Follow these conventions when writing RSpec tests:
|
204
|
+
|
205
|
+
1. **ALWAYS prefer one-line `it { ... }` syntax**:
|
206
|
+
```ruby
|
207
|
+
# Preferred - Always use this when possible
|
208
|
+
it { expect(model.state[:count]).to eq(0) }
|
209
|
+
it { expect(model.state).to be_frozen }
|
210
|
+
it { is_expected.to be_running }
|
211
|
+
|
212
|
+
# Avoid - Multi-line blocks should be rare
|
213
|
+
it "has expected state" do
|
214
|
+
expect(model.state[:count]).to eq(0)
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
2. **Use `subject` to define test targets**:
|
219
|
+
```ruby
|
220
|
+
subject(:program) { described_class.new }
|
221
|
+
subject(:new_model) { model.with(count: 5) }
|
222
|
+
subject(:custom_model) { test_model_class.new(count: 5) }
|
223
|
+
|
224
|
+
# For simple cases, use implicit subject
|
225
|
+
subject { described_class.root }
|
226
|
+
subject { described_class.env }
|
227
|
+
```
|
228
|
+
|
229
|
+
3. **Use `let` for test dependencies and lazy evaluation**:
|
230
|
+
```ruby
|
231
|
+
let(:output) { StringIO.new }
|
232
|
+
let(:new_model) { result.first }
|
233
|
+
let(:message) { result.last }
|
234
|
+
let(:original_children) { parent_model.children }
|
235
|
+
```
|
236
|
+
|
237
|
+
4. **Use `context` to group related scenarios and enable one-liners**:
|
238
|
+
```ruby
|
239
|
+
# Good - Use context to set up scenarios for one-line tests
|
240
|
+
context "when merging provided state with default state" do
|
241
|
+
subject(:custom_model) { test_model_class.new(count: 5) }
|
242
|
+
|
243
|
+
it { expect(custom_model.state[:count]).to eq(5) }
|
244
|
+
end
|
245
|
+
|
246
|
+
# Good - Group related one-line tests
|
247
|
+
context "when checking child states" do
|
248
|
+
subject(:child_count_model) { parent_model.children[0] }
|
249
|
+
|
250
|
+
it { expect(child_count_model.state[:count]).to eq(5) }
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
5. **Use `is_expected` when testing the subject directly**:
|
255
|
+
```ruby
|
256
|
+
it { is_expected.to be_running } # Preferred
|
257
|
+
it { is_expected.not_to be(model) }
|
258
|
+
it { is_expected.to be_a(Pathname) }
|
259
|
+
it { is_expected.to eq(:test) }
|
260
|
+
|
261
|
+
# vs - Avoid these when subject is available
|
262
|
+
it { expect(subject).to be_running }
|
263
|
+
it { expect(described_class.root).to be_a(Pathname) }
|
264
|
+
```
|
265
|
+
|
266
|
+
6. **Use `before` blocks for setup actions, not variable assignments**:
|
267
|
+
```ruby
|
268
|
+
# Good - Setup actions in before blocks
|
269
|
+
context "when configuring with block" do
|
270
|
+
before do
|
271
|
+
described_class.configure do |config|
|
272
|
+
config.autoload_dirs = ["custom"]
|
273
|
+
config.hot_reloading = false
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
it { expect(config.autoload_dirs).to eq(["custom"]) }
|
278
|
+
it { expect(config.hot_reloading).to be(false) }
|
279
|
+
end
|
280
|
+
|
281
|
+
# Avoid - Variable assignments should use let/subject
|
282
|
+
before do
|
283
|
+
@config = described_class.configure { |c| c.autoload_dirs = ["custom"] }
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
7. **Use `.to change()` for testing immutability**:
|
288
|
+
```ruby
|
289
|
+
it { expect { model.update(:increment) }.not_to change(model, :state) }
|
290
|
+
it { expect { model.with(count: 5) }.not_to change(model, :state) }
|
291
|
+
```
|
292
|
+
|
293
|
+
7. **Each `it` block should have only one expectation**:
|
294
|
+
```ruby
|
295
|
+
# Good - Separate one-line tests
|
296
|
+
it { expect(new_model).not_to be(model) }
|
297
|
+
it { expect(new_model.state[:count]).to eq(5) }
|
298
|
+
|
299
|
+
# Avoid - Multiple expectations in one block
|
300
|
+
it "creates new instance with updated state" do
|
301
|
+
new_model = model.with(count: 5)
|
302
|
+
expect(new_model).not_to be(model)
|
303
|
+
expect(new_model.state[:count]).to eq(5)
|
304
|
+
end
|
305
|
+
```
|
306
|
+
|
307
|
+
8. **Transform multi-line tests into context + one-liners**:
|
308
|
+
```ruby
|
309
|
+
# Good - Use context to enable one-liner
|
310
|
+
context "when called on base class" do
|
311
|
+
subject(:base_model) { Milktea::Model.new }
|
312
|
+
|
313
|
+
it { expect { base_model.view }.to raise_error(NotImplementedError) }
|
314
|
+
end
|
315
|
+
|
316
|
+
# Avoid - Multi-line when one-liner is possible
|
317
|
+
it "raises NotImplementedError for base class" do
|
318
|
+
base_model = Milktea::Model.new
|
319
|
+
expect { base_model.view }.to raise_error(NotImplementedError)
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
9. **Use descriptive blocks ONLY when one-liners are impossible**:
|
324
|
+
```ruby
|
325
|
+
# Only use this when absolutely necessary (very rare)
|
326
|
+
it "handles complex scenario with multiple setup steps" do
|
327
|
+
# Multiple expectations or complex setup that cannot be simplified
|
328
|
+
end
|
329
|
+
```
|
330
|
+
|
331
|
+
10. **PRIORITY: Transform ANY multi-line test into context + one-liner**:
|
332
|
+
```ruby
|
333
|
+
# If you find yourself writing this:
|
334
|
+
it "merges provided state with default state" do
|
335
|
+
custom_model = test_model_class.new(count: 5)
|
336
|
+
expect(custom_model.state[:count]).to eq(5)
|
337
|
+
end
|
338
|
+
|
339
|
+
# Transform it to this:
|
340
|
+
context "when merging provided state with default state" do
|
341
|
+
subject(:custom_model) { test_model_class.new(count: 5) }
|
342
|
+
|
343
|
+
it { expect(custom_model.state[:count]).to eq(5) }
|
344
|
+
end
|
345
|
+
```
|
346
|
+
|
347
|
+
11. **Never test private instance variables directly**:
|
348
|
+
```ruby
|
349
|
+
# Bad - Testing implementation details
|
350
|
+
it { expect(subject.instance_variable_get(:@output)).to eq($stdout) }
|
351
|
+
|
352
|
+
# Good - Testing public behavior with one-liner
|
353
|
+
it { expect(output.string).to include("expected content") }
|
354
|
+
```
|
355
|
+
|
356
|
+
12. **Focus on observable behavior, not implementation**:
|
357
|
+
```ruby
|
358
|
+
# Bad - Checking internal state
|
359
|
+
it { expect(program.instance_variable_get(:@renderer)).to be_a(Milktea::Renderer) }
|
360
|
+
|
361
|
+
# Good - Testing actual public behavior with one-liner
|
362
|
+
it { expect(runtime).to have_received(:stop) }
|
363
|
+
```
|
364
|
+
|
365
|
+
13. **Prefer `instance_double` over extensive `allow` calls**:
|
366
|
+
```ruby
|
367
|
+
# Bad - Multiple allow calls
|
368
|
+
let(:runtime) { instance_double(Milktea::Runtime) }
|
369
|
+
before do
|
370
|
+
allow(runtime).to receive(:start)
|
371
|
+
allow(runtime).to receive(:running?).and_return(false)
|
372
|
+
allow(runtime).to receive(:tick).and_return(model)
|
373
|
+
allow(runtime).to receive(:render?).and_return(false)
|
374
|
+
end
|
375
|
+
|
376
|
+
# Good - Configure instance_double with expected methods
|
377
|
+
let(:runtime) do
|
378
|
+
instance_double(Milktea::Runtime,
|
379
|
+
start: nil,
|
380
|
+
running?: false,
|
381
|
+
tick: model,
|
382
|
+
render?: false
|
383
|
+
)
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
13. **Use spies for testing delegation instead of expect().to receive()**:
|
388
|
+
```ruby
|
389
|
+
# Bad - Pre-setting expectations
|
390
|
+
it "delegates to runtime stop" do
|
391
|
+
expect(runtime).to receive(:stop)
|
392
|
+
program.stop
|
393
|
+
end
|
394
|
+
|
395
|
+
# Good - Using spy pattern
|
396
|
+
let(:runtime) { spy('runtime', running?: false) }
|
397
|
+
|
398
|
+
it "delegates to runtime stop" do
|
399
|
+
program.stop
|
400
|
+
expect(runtime).to have_received(:stop)
|
401
|
+
end
|
402
|
+
```
|
403
|
+
|
404
|
+
14. **Use RSpec's `output` matcher for testing stdout/stderr**:
|
405
|
+
```ruby
|
406
|
+
# Good - Using output matcher
|
407
|
+
it "prints to stdout" do
|
408
|
+
expect { renderer.render(model) }.to output("Hello!").to_stdout
|
409
|
+
end
|
410
|
+
|
411
|
+
# Also good - Testing with regex
|
412
|
+
it "prints to stdout" do
|
413
|
+
expect { renderer.render(model) }.to output(/Hello/).to_stdout
|
414
|
+
end
|
415
|
+
|
416
|
+
# Bad - Manual stdout capture
|
417
|
+
it "prints to stdout" do
|
418
|
+
stdout_capture = StringIO.new
|
419
|
+
renderer = described_class.new(stdout_capture)
|
420
|
+
renderer.render(model)
|
421
|
+
expect(stdout_capture.string).to include("Hello!")
|
422
|
+
end
|
423
|
+
```
|
424
|
+
|
425
|
+
15. **Use `allow(ENV).to receive(:fetch)` for environment variable mocking**:
|
426
|
+
```ruby
|
427
|
+
# Good - Mock ENV.fetch calls
|
428
|
+
before { allow(ENV).to receive(:fetch).with("MILKTEA_ENV", nil).and_return("test") }
|
429
|
+
|
430
|
+
# Avoid - Direct ENV manipulation
|
431
|
+
before { ENV["MILKTEA_ENV"] = "test" }
|
432
|
+
after { ENV.delete("MILKTEA_ENV") }
|
433
|
+
```
|
434
|
+
|
435
|
+
16. **Structure module/class method tests with clear subject definitions**:
|
436
|
+
```ruby
|
437
|
+
describe ".root" do
|
438
|
+
subject { described_class.root }
|
439
|
+
|
440
|
+
it { is_expected.to be_a(Pathname) }
|
441
|
+
end
|
442
|
+
|
443
|
+
describe ".env" do
|
444
|
+
subject { described_class.env }
|
445
|
+
|
446
|
+
it { is_expected.to be_a(Symbol) }
|
447
|
+
|
448
|
+
context "when MILKTEA_ENV is set" do
|
449
|
+
before { allow(ENV).to receive(:fetch).with("MILKTEA_ENV", nil).and_return("test") }
|
450
|
+
|
451
|
+
it { is_expected.to eq(:test) }
|
452
|
+
end
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
456
|
+
17. **Use named subjects for configuration testing**:
|
457
|
+
```ruby
|
458
|
+
describe ".configure" do
|
459
|
+
subject(:config) { described_class.config } # Named subject for clarity
|
460
|
+
|
461
|
+
context "when configuring with block" do
|
462
|
+
before do
|
463
|
+
described_class.configure do |config|
|
464
|
+
config.autoload_dirs = ["custom"]
|
465
|
+
config.hot_reloading = false
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
it { expect(config.autoload_dirs).to eq(["custom"]) }
|
470
|
+
it { expect(config.hot_reloading).to be(false) }
|
471
|
+
end
|
472
|
+
end
|
473
|
+
```
|
474
|
+
|
475
|
+
### RSpec Style Summary
|
476
|
+
|
477
|
+
**CRITICAL RULE**: Always prefer `it { ... }` one-line syntax. If you find yourself writing a multi-line `it` block, immediately refactor it into a `context` with a `subject` to enable one-line tests.
|
478
|
+
|
479
|
+
**The Golden Pattern**:
|
480
|
+
```ruby
|
481
|
+
context "when [scenario description]" do
|
482
|
+
subject(:target) { SomeClass.new(params) }
|
483
|
+
|
484
|
+
it { expect(target.method).to eq(expected) }
|
485
|
+
end
|
486
|
+
```
|
487
|
+
|
488
|
+
**Remember**: Every multi-line test can and should be transformed into this pattern.
|
489
|
+
|
490
|
+
## Container Layout System
|
491
|
+
|
492
|
+
### Flexbox-Style Layout
|
493
|
+
|
494
|
+
Container provides CSS-like flexbox layout for terminal interfaces:
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
class MyContainer < Milktea::Container
|
498
|
+
direction :row # or :column (default)
|
499
|
+
child HeaderModel, flex: 1
|
500
|
+
child ContentModel, flex: 3
|
501
|
+
child FooterModel, flex: 1
|
502
|
+
end
|
503
|
+
```
|
504
|
+
|
505
|
+
### Bounds Management
|
506
|
+
|
507
|
+
- Containers automatically calculate and propagate bounds (width, height, x, y)
|
508
|
+
- Child components receive properly sized layout areas
|
509
|
+
- Supports nested containers with accurate bounds calculation
|
510
|
+
- Dynamic components maintain proper bounds through Symbol resolution
|
511
|
+
|
512
|
+
### Default Container Behavior
|
513
|
+
|
514
|
+
- Container automatically displays `children_views` (no need for manual `view` method)
|
515
|
+
- Subclasses can override `view` for custom display logic
|
516
|
+
- Layout direction defaults to `:column` if not specified
|
517
|
+
|
518
|
+
## Important Notes
|
519
|
+
|
520
|
+
- Ruby version requirement: >= 3.1.0
|
521
|
+
- Uses conventional commits format in English
|
522
|
+
- The gemspec uses git to determine which files to include in the gem
|
523
|
+
- Currently in early development (v0.1.0) with core architecture implemented
|
524
|
+
- Runtime-based architecture allows dependency injection for testing and customization
|
525
|
+
- Program uses dependency injection pattern: `Program.new(model, runtime: custom_runtime)`
|
526
|
+
- Application auto-registers itself when inherited: `class MyApp < Milktea::Application` sets `Milktea.app = MyApp`
|
527
|
+
- Thread-safe app registry using mutex for concurrent access
|
528
|
+
- Dynamic child resolution available framework-wide through Symbol-based definitions
|
529
|
+
|
530
|
+
## Repository Management
|
531
|
+
|
532
|
+
- Keep ARCHITECTURE.md updated when we change anything
|
533
|
+
|
534
|
+
## Development Preferences
|
535
|
+
|
536
|
+
- Prefer using `return ... if` style instead of `if ... else` for early returns or value determination
|
537
|
+
- When using `return .. if`, prioritize returning error conditions first, use `unless` when necessary
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Aotokitsuruya
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|