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