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
@@ -0,0 +1,90 @@
|
|
1
|
+
# Development Log - 2025-07-04
|
2
|
+
|
3
|
+
## What's New
|
4
|
+
|
5
|
+
#### Optional Loader System for Flexible Development Patterns
|
6
|
+
The framework now provides an optional Loader system that enables developers to choose when autoloading and hot reloading capabilities are needed. This addresses the fundamental difference between standalone examples and full application development workflows.
|
7
|
+
|
8
|
+
Key capabilities include:
|
9
|
+
- **Selective activation**: Examples can run without unnecessary autoloading overhead
|
10
|
+
- **Explicit configuration**: Applications can enable loading with `config.loader = Milktea::Loader.new(app_dir, runtime)`
|
11
|
+
- **Graceful degradation**: Hot reloading works when Listen gem is available, falls back to basic autoloading when not
|
12
|
+
- **Development-focused**: Optimized for the common pattern where examples are lightweight and apps need full development features
|
13
|
+
|
14
|
+
#### Enhanced Hot Reloading Architecture
|
15
|
+
The hot reloading system now provides more granular control over development workflows:
|
16
|
+
|
17
|
+
- **Dual-mode operation**: Basic autoloading (always available) and file watching (conditionally enabled)
|
18
|
+
- **Optional Listen integration**: Detects Listen gem availability at runtime using `gem "listen"` with proper error handling
|
19
|
+
- **Message-driven reload**: Uses `Message::Reload` for clean communication between Loader and Runtime
|
20
|
+
- **Zeitwerk integration**: Leverages Zeitwerk's robust autoloading capabilities as the foundation
|
21
|
+
|
22
|
+
## What's Fixed
|
23
|
+
|
24
|
+
#### Method Naming Consistency
|
25
|
+
Renamed `hot_reload!` to `hot_reload` to better reflect the method's non-throwing behavior. The method gracefully handles cases where the Listen gem is unavailable, making the exclamation mark suffix misleading. This change improves API consistency and developer expectations.
|
26
|
+
|
27
|
+
#### Configuration System Clarity
|
28
|
+
Resolved the automatic initialization of the loader in the Config class, which was creating unnecessary overhead for simple use cases. The loader is now explicitly `nil` by default, requiring conscious activation for development scenarios that need it.
|
29
|
+
|
30
|
+
#### Test Suite Alignment
|
31
|
+
Updated all test files to follow the project's strict RSpec guidelines from CLAUDE.md:
|
32
|
+
- Transformed multi-line tests into context + one-liner patterns
|
33
|
+
- Applied spy pattern instead of `expect().to receive()` for better test isolation
|
34
|
+
- Maintained comprehensive coverage while improving readability and maintainability
|
35
|
+
|
36
|
+
## Design Decisions
|
37
|
+
|
38
|
+
#### Reloader → Loader Rename and Optional Architecture
|
39
|
+
**Context**: The original Reloader was automatically initialized for all applications, creating overhead for simple examples and unclear boundaries between autoloading and hot reloading.
|
40
|
+
|
41
|
+
**Decision**: Renamed to Loader and made it optional by default, requiring explicit configuration.
|
42
|
+
|
43
|
+
**Rationale**: This design better aligns with the framework's dual usage patterns:
|
44
|
+
- Examples focus on demonstrating TUI concepts without development overhead
|
45
|
+
- Applications explicitly enable development features when needed
|
46
|
+
- Clear separation of concerns between framework core and development tooling
|
47
|
+
|
48
|
+
This decision supports the Clean Architecture principle of dependency inversion, where the framework core doesn't depend on development-specific features.
|
49
|
+
|
50
|
+
#### Runtime Detection of Optional Dependencies
|
51
|
+
**Context**: The Listen gem provides file watching capabilities but shouldn't be a hard dependency for the framework.
|
52
|
+
|
53
|
+
**Decision**: Use `gem "listen"` with `Gem::LoadError` rescue for runtime detection rather than gemspec dependencies.
|
54
|
+
|
55
|
+
**Rationale**: This approach provides:
|
56
|
+
- Zero additional dependencies for basic usage
|
57
|
+
- Graceful enhancement when Listen is available
|
58
|
+
- Clear error boundaries that don't affect core functionality
|
59
|
+
- Better compatibility with different deployment environments
|
60
|
+
|
61
|
+
#### Dependency Injection for Loader
|
62
|
+
**Context**: The Loader needs access to Runtime for message communication and app_dir for file watching.
|
63
|
+
|
64
|
+
**Decision**: Use constructor injection rather than setter injection or service location.
|
65
|
+
|
66
|
+
**Rationale**: Constructor injection makes dependencies explicit and ensures the Loader is properly configured at creation time. This aligns with the framework's overall dependency injection pattern and makes testing more straightforward.
|
67
|
+
|
68
|
+
## Impact
|
69
|
+
|
70
|
+
The optional Loader system significantly improves the developer experience by:
|
71
|
+
|
72
|
+
**For Example Development**: Examples now run with minimal overhead, focusing attention on TUI concepts rather than development tooling. This makes the framework more accessible to newcomers and reduces cognitive load when exploring features.
|
73
|
+
|
74
|
+
**For Application Development**: Developers get explicit control over when and how autoloading capabilities are enabled, leading to more predictable development workflows and better understanding of their application's loading behavior.
|
75
|
+
|
76
|
+
**For Framework Architecture**: The changes reinforce the Clean Architecture patterns by separating core framework functionality from development-specific features. This makes the codebase more maintainable and easier to extend.
|
77
|
+
|
78
|
+
**For Testing Strategy**: The refactoring demonstrates and reinforces the project's commitment to RSpec best practices, creating a stronger foundation for future test development and maintaining high code quality standards.
|
79
|
+
|
80
|
+
## Files Modified
|
81
|
+
|
82
|
+
- `lib/milktea/loader.rb` - Renamed from reloader.rb, implements optional loading system
|
83
|
+
- `lib/milktea/config.rb` - Made loader optional with attr_accessor instead of automatic initialization
|
84
|
+
- `lib/milktea/program.rb` - Updated to handle optional loader with early return pattern
|
85
|
+
- `lib/milktea/message.rb` - Added Message::Reload for hot reload communication
|
86
|
+
- `lib/milktea/runtime.rb` - Added Message::Reload handling in execute_side_effect
|
87
|
+
- `spec/milktea/loader_spec.rb` - Renamed from reloader_spec.rb, updated all tests to follow RSpec guidelines
|
88
|
+
- `spec/milktea/config_spec.rb` - Updated to expect nil loader by default and test optional behavior
|
89
|
+
- `spec/milktea/program_spec.rb` - Updated delegation tests to reflect removed loader delegation
|
90
|
+
- `CLAUDE.md` - Updated with comprehensive RSpec style guidelines and development commands
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Development Log - 2025-07-05
|
2
|
+
|
3
|
+
## What's New
|
4
|
+
|
5
|
+
#### Dynamic Child Resolution Framework-Wide
|
6
|
+
The dynamic child resolution mechanism has been elevated from a Container-specific feature to a core Model capability. This fundamental enhancement allows any Model subclass to use Symbol-based child definitions that resolve to methods returning Class objects. Developers can now implement dynamic behavior patterns across the entire framework, not just in layout containers.
|
7
|
+
|
8
|
+
The implementation provides robust error handling with clear distinction between missing methods (NoMethodError) and invalid return types (ArgumentError). This enables sophisticated patterns like state-driven component switching while maintaining type safety and developer experience.
|
9
|
+
|
10
|
+
#### Container Default View Implementation
|
11
|
+
Container classes now automatically display their children without requiring boilerplate view methods. Since containers are fundamentally layout mechanisms designed to organize and present child components, the default behavior of `view = children_views` eliminates repetitive code while maintaining the ability to override for custom implementations.
|
12
|
+
|
13
|
+
#### Enhanced Flexbox Layout System
|
14
|
+
The Container layout system has been significantly expanded with bidirectional flexbox support. Developers can now create both column and row layouts with proportional sizing, providing CSS-like flexbox behavior in terminal interfaces. The system includes comprehensive bounds calculation that correctly propagates dimensions and positions to child components.
|
15
|
+
|
16
|
+
#### Interactive Layout Examples
|
17
|
+
Two new comprehensive examples demonstrate the framework's layout capabilities:
|
18
|
+
- `container_layout.rb`: An interactive demo showcasing dynamic switching between column and row layouts with real-time bounds visualization
|
19
|
+
- `container_simple.rb`: A basic demonstration of row layout with visual box rendering
|
20
|
+
|
21
|
+
These examples integrate with tty-box for professional terminal UI presentation and serve as practical references for developers building TUI applications.
|
22
|
+
|
23
|
+
## What's Fixed
|
24
|
+
|
25
|
+
#### Hot Reloading and Dynamic Layout Integration
|
26
|
+
Resolved a critical issue where dynamically created layout components (Row/Column) weren't receiving proper bounds information from their parent containers. Previously, dynamic layouts would use full screen dimensions instead of allocated flex space, breaking layout calculations during hot reload scenarios. The solution involved implementing Symbol-based child resolution that preserves bounds propagation throughout the component hierarchy.
|
27
|
+
|
28
|
+
#### View Rendering Consistency
|
29
|
+
Fixed children_views joining behavior to eliminate unnecessary newlines between child views. This prevents layout breaking in Container scenarios where child components should be positioned adjacently without automatic line separation. The change maintains flexibility for components that specifically need newline separation while providing sensible defaults for layout containers.
|
30
|
+
|
31
|
+
#### Test Organization and Coverage
|
32
|
+
Reorganized test suites to properly reflect the architectural changes. Dynamic child resolution tests were moved from container_spec to model_spec, aligning test coverage with the new framework-wide availability of the feature. This ensures maintainability and prevents confusion about where functionality is implemented.
|
33
|
+
|
34
|
+
## Design Decisions
|
35
|
+
|
36
|
+
#### Symbol-Based Dynamic Resolution Architecture
|
37
|
+
**Context**: The original dynamic child resolution was implemented only in Container, limiting its use to layout scenarios. However, the pattern of dynamically selecting child components based on state is valuable across all Model types.
|
38
|
+
|
39
|
+
**Decision**: Move the resolution mechanism to the base Model class with a clean API: `resolve_child(klass_or_symbol, state)`.
|
40
|
+
|
41
|
+
**Rationale**: This promotes consistency across the framework and enables sophisticated component composition patterns. The Symbol-to-method resolution pattern provides a clean DSL while maintaining Ruby's dynamic capabilities. Error handling distinguishes between missing methods and type validation, providing clear debugging guidance.
|
42
|
+
|
43
|
+
#### Container as Pure Layout Component
|
44
|
+
**Context**: Container subclasses consistently implemented identical `view` methods that simply called `children_views`, representing boilerplate code with no variation.
|
45
|
+
|
46
|
+
**Decision**: Implement `def view = children_views` as the default Container behavior.
|
47
|
+
|
48
|
+
**Rationale**: Containers exist to organize and present child components, so displaying children is their natural purpose. This reduces cognitive overhead for developers while maintaining override capability for specialized containers. The decision aligns with the principle that containers should be transparent layout mechanisms.
|
49
|
+
|
50
|
+
#### Framework-Wide Symbol Resolution
|
51
|
+
**Context**: Initially, Symbol-based child definitions were Container-specific, creating inconsistency in the framework's component definition patterns.
|
52
|
+
|
53
|
+
**Decision**: Standardize Symbol resolution across all Model classes through the base class implementation.
|
54
|
+
|
55
|
+
**Rationale**: Consistency improves developer experience and enables advanced patterns like state-driven component composition throughout the framework. The unified approach reduces learning curve and provides predictable behavior regardless of component type.
|
56
|
+
|
57
|
+
#### Bounds-Aware Dynamic Components
|
58
|
+
**Context**: Dynamic layout switching required maintaining proper parent-child bounds relationships while enabling runtime component type changes.
|
59
|
+
|
60
|
+
**Decision**: Ensure all dynamic resolution paths preserve bounds information propagation through the unified `resolve_child` method.
|
61
|
+
|
62
|
+
**Rationale**: Terminal UI layout requires precise coordinate and dimension management. Breaking bounds propagation creates visual artifacts and layout inconsistencies. The solution maintains flexbox semantics while enabling dynamic behavior.
|
63
|
+
|
64
|
+
## Impact
|
65
|
+
|
66
|
+
These changes significantly enhance the framework's flexibility and developer experience. The dynamic child resolution system enables sophisticated component composition patterns previously impossible, while the enhanced Container system provides CSS-like layout capabilities for terminal interfaces.
|
67
|
+
|
68
|
+
Developers can now build more maintainable TUI applications with less boilerplate code and greater architectural flexibility. The unified Symbol resolution pattern creates consistency across the framework, reducing learning curve and improving code predictability.
|
69
|
+
|
70
|
+
The improvements particularly benefit complex applications requiring dynamic layouts, state-driven component switching, and sophisticated terminal interface designs. Hot reloading reliability ensures smooth development experience even with complex dynamic component hierarchies.
|
71
|
+
|
72
|
+
## Files Modified
|
73
|
+
|
74
|
+
- `lib/milktea/model.rb` - Added dynamic child resolution framework-wide
|
75
|
+
- `lib/milktea/container.rb` - Added default view implementation, removed Container-specific resolution
|
76
|
+
- `spec/milktea/model_spec.rb` - Added comprehensive dynamic resolution tests
|
77
|
+
- `spec/milktea/container_spec.rb` - Reorganized tests, removed duplicated dynamic tests
|
78
|
+
- `examples/container_layout.rb` - Interactive layout demo with dynamic switching
|
79
|
+
- `examples/container_simple.rb` - Basic layout demonstration
|
80
|
+
- `Gemfile` - Added tty-box dependency for enhanced examples
|
81
|
+
- `lib/milktea/renderer.rb` - Enhanced cursor management for cleaner terminal experience
|
@@ -0,0 +1,288 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "milktea"
|
6
|
+
require "tty-box"
|
7
|
+
|
8
|
+
# Container Layout Demo with Terminal Resize Support
|
9
|
+
#
|
10
|
+
# This example demonstrates:
|
11
|
+
# - Flexbox-style container layouts (column and row)
|
12
|
+
# - Dynamic layout switching
|
13
|
+
# - Terminal resize handling at the root level
|
14
|
+
#
|
15
|
+
# When the terminal is resized, only the root model (LayoutDemoModel) needs
|
16
|
+
# to handle Message::Resize by calling `with` to rebuild itself. All child
|
17
|
+
# containers and components will automatically recalculate their bounds and
|
18
|
+
# adapt to the new screen dimensions.
|
19
|
+
|
20
|
+
# Box model that renders a tty-box with content
|
21
|
+
class BoxModel < Milktea::Container
|
22
|
+
def view
|
23
|
+
TTY::Box.frame(
|
24
|
+
top: bounds.y,
|
25
|
+
left: bounds.x,
|
26
|
+
width: bounds.width,
|
27
|
+
height: bounds.height,
|
28
|
+
title: { top_left: " #{state[:title]} " },
|
29
|
+
border: :light,
|
30
|
+
padding: 1,
|
31
|
+
align: :center
|
32
|
+
) do
|
33
|
+
content_lines.join("\n")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(message)
|
38
|
+
case message
|
39
|
+
when Milktea::Message::KeyPress
|
40
|
+
handle_keypress(message)
|
41
|
+
else
|
42
|
+
[self, Milktea::Message::None.new]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def default_state
|
49
|
+
{ title: "Box", content: "Content", value: 0 }
|
50
|
+
end
|
51
|
+
|
52
|
+
def content_lines
|
53
|
+
lines = []
|
54
|
+
lines << state[:content] if state[:content]
|
55
|
+
lines << "Value: #{state[:value]}" if state.key?(:value)
|
56
|
+
lines << ""
|
57
|
+
lines << bounds_info
|
58
|
+
lines
|
59
|
+
end
|
60
|
+
|
61
|
+
def bounds_info
|
62
|
+
"#{bounds.width}×#{bounds.height} @(#{bounds.x},#{bounds.y})"
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_keypress(message)
|
66
|
+
case message.value
|
67
|
+
when "+"
|
68
|
+
[with(value: state[:value] + 1), Milktea::Message::None.new]
|
69
|
+
when "-"
|
70
|
+
[with(value: state[:value] - 1), Milktea::Message::None.new]
|
71
|
+
else
|
72
|
+
[self, Milktea::Message::None.new]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Column layout container
|
78
|
+
class ColumnLayoutModel < Milktea::Container
|
79
|
+
direction :column
|
80
|
+
child BoxModel, ->(state) { { title: "Header", content: "Top Section", value: state[:header_value] } }, flex: 1
|
81
|
+
child BoxModel, ->(state) { { title: "Content", content: "Main Area", value: state[:content_value] } }, flex: 3
|
82
|
+
child BoxModel, ->(state) { { title: "Footer", content: "Bottom Status", value: state[:footer_value] } }, flex: 1
|
83
|
+
|
84
|
+
def update(message)
|
85
|
+
case message
|
86
|
+
when Milktea::Message::KeyPress
|
87
|
+
handle_keypress(message)
|
88
|
+
else
|
89
|
+
[self, Milktea::Message::None.new]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def default_state
|
96
|
+
{ header_value: 1, content_value: 10, footer_value: 5 }
|
97
|
+
end
|
98
|
+
|
99
|
+
def handle_keypress(message)
|
100
|
+
case message.value
|
101
|
+
when "+"
|
102
|
+
[with(
|
103
|
+
header_value: state[:header_value] + 1,
|
104
|
+
content_value: state[:content_value] + 1,
|
105
|
+
footer_value: state[:footer_value] + 1
|
106
|
+
), Milktea::Message::None.new]
|
107
|
+
when "-"
|
108
|
+
[with(
|
109
|
+
header_value: [state[:header_value] - 1, 0].max,
|
110
|
+
content_value: [state[:content_value] - 1, 0].max,
|
111
|
+
footer_value: [state[:footer_value] - 1, 0].max
|
112
|
+
), Milktea::Message::None.new]
|
113
|
+
when "q"
|
114
|
+
[self, Milktea::Message::Exit.new]
|
115
|
+
else
|
116
|
+
[self, Milktea::Message::None.new]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Row layout container
|
122
|
+
class RowLayoutModel < Milktea::Container
|
123
|
+
direction :row
|
124
|
+
child BoxModel, ->(state) { { title: "Left", content: "Sidebar", value: state[:left_value] } }, flex: 1
|
125
|
+
child BoxModel, ->(state) { { title: "Center", content: "Main Content", value: state[:center_value] } }, flex: 2
|
126
|
+
child BoxModel, ->(state) { { title: "Right", content: "Info Panel", value: state[:right_value] } }, flex: 1
|
127
|
+
|
128
|
+
def update(message)
|
129
|
+
case message
|
130
|
+
when Milktea::Message::KeyPress
|
131
|
+
handle_keypress(message)
|
132
|
+
else
|
133
|
+
[self, Milktea::Message::None.new]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def default_state
|
140
|
+
{ left_value: 3, center_value: 7, right_value: 2 }
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_keypress(message)
|
144
|
+
case message.value
|
145
|
+
when "+"
|
146
|
+
[with(
|
147
|
+
left_value: state[:left_value] + 1,
|
148
|
+
center_value: state[:center_value] + 1,
|
149
|
+
right_value: state[:right_value] + 1
|
150
|
+
), Milktea::Message::None.new]
|
151
|
+
when "-"
|
152
|
+
[with(
|
153
|
+
left_value: [state[:left_value] - 1, 0].max,
|
154
|
+
center_value: [state[:center_value] - 1, 0].max,
|
155
|
+
right_value: [state[:right_value] - 1, 0].max
|
156
|
+
), Milktea::Message::None.new]
|
157
|
+
when "q"
|
158
|
+
[self, Milktea::Message::Exit.new]
|
159
|
+
else
|
160
|
+
[self, Milktea::Message::None.new]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Status bar model
|
166
|
+
class StatusBar < Milktea::Model
|
167
|
+
def view
|
168
|
+
layout_type = state[:show_column] ? "Column" : "Row"
|
169
|
+
TTY::Box.frame(
|
170
|
+
top: bounds.y,
|
171
|
+
left: bounds.x,
|
172
|
+
width: bounds.width,
|
173
|
+
height: bounds.height,
|
174
|
+
title: { top_left: " Layout Demo " },
|
175
|
+
border: :light,
|
176
|
+
align: :center
|
177
|
+
) do
|
178
|
+
"Current: #{layout_type} Layout | 't' toggle | '+/-' change values | 'q' quit"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def update(_message)
|
183
|
+
[self, Milktea::Message::None.new]
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def bounds
|
189
|
+
@bounds ||= Milktea::Bounds.new(
|
190
|
+
width: state[:width] || screen_width,
|
191
|
+
height: state[:height] || screen_height,
|
192
|
+
x: state[:x] || 0,
|
193
|
+
y: state[:y] || 0
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Main application that toggles between layouts
|
199
|
+
class LayoutDemoModel < Milktea::Container
|
200
|
+
direction :column
|
201
|
+
child :status_bar, flex: 1
|
202
|
+
child :dynamic_layout, lambda { |state|
|
203
|
+
state.slice(:header_value, :content_value, :footer_value, :left_value, :center_value, :right_value)
|
204
|
+
}, flex: 5
|
205
|
+
|
206
|
+
def update(message)
|
207
|
+
case message
|
208
|
+
when Milktea::Message::KeyPress
|
209
|
+
handle_keypress(message)
|
210
|
+
when Milktea::Message::Resize
|
211
|
+
# Rebuild model with fresh class to pick up new screen dimensions
|
212
|
+
[with, Milktea::Message::None.new]
|
213
|
+
else
|
214
|
+
[self, Milktea::Message::None.new]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def default_state
|
221
|
+
{
|
222
|
+
show_column: true,
|
223
|
+
header_value: 1,
|
224
|
+
content_value: 10,
|
225
|
+
footer_value: 5,
|
226
|
+
left_value: 3,
|
227
|
+
center_value: 7,
|
228
|
+
right_value: 2
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
def status_bar
|
233
|
+
StatusBar
|
234
|
+
end
|
235
|
+
|
236
|
+
def dynamic_layout
|
237
|
+
state[:show_column] ? ColumnLayoutModel : RowLayoutModel
|
238
|
+
end
|
239
|
+
|
240
|
+
def handle_keypress(message)
|
241
|
+
case message.value
|
242
|
+
when "t"
|
243
|
+
[with(show_column: !state[:show_column]), Milktea::Message::None.new]
|
244
|
+
when "+"
|
245
|
+
increment_values
|
246
|
+
when "-"
|
247
|
+
decrement_values
|
248
|
+
when "q"
|
249
|
+
[self, Milktea::Message::Exit.new]
|
250
|
+
else
|
251
|
+
[self, Milktea::Message::None.new]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def increment_values
|
256
|
+
[with(
|
257
|
+
header_value: state[:header_value] + 1,
|
258
|
+
content_value: state[:content_value] + 1,
|
259
|
+
footer_value: state[:footer_value] + 1,
|
260
|
+
left_value: state[:left_value] + 1,
|
261
|
+
center_value: state[:center_value] + 1,
|
262
|
+
right_value: state[:right_value] + 1
|
263
|
+
), Milktea::Message::None.new]
|
264
|
+
end
|
265
|
+
|
266
|
+
def decrement_values
|
267
|
+
[with(
|
268
|
+
header_value: [state[:header_value] - 1, 0].max,
|
269
|
+
content_value: [state[:content_value] - 1, 0].max,
|
270
|
+
footer_value: [state[:footer_value] - 1, 0].max,
|
271
|
+
left_value: [state[:left_value] - 1, 0].max,
|
272
|
+
center_value: [state[:center_value] - 1, 0].max,
|
273
|
+
right_value: [state[:right_value] - 1, 0].max
|
274
|
+
), Milktea::Message::None.new]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Create and run the layout demo
|
279
|
+
puts "Container Layout Demo with Terminal Resize Support"
|
280
|
+
puts "Press 't' to toggle between column and row layouts"
|
281
|
+
puts "Use +/- to change values, 'q' to quit"
|
282
|
+
puts "Try resizing your terminal window to see automatic layout adaptation"
|
283
|
+
puts "Press any key to start..."
|
284
|
+
$stdin.gets
|
285
|
+
|
286
|
+
model = LayoutDemoModel.new
|
287
|
+
program = Milktea::Program.new(model)
|
288
|
+
program.run
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "milktea"
|
6
|
+
require "tty-box"
|
7
|
+
|
8
|
+
# Simple box component
|
9
|
+
class SimpleBox < Milktea::Container
|
10
|
+
def view
|
11
|
+
TTY::Box.frame(
|
12
|
+
top: bounds.y,
|
13
|
+
left: bounds.x,
|
14
|
+
width: bounds.width,
|
15
|
+
height: bounds.height,
|
16
|
+
title: { top_left: " #{state[:title]} " },
|
17
|
+
border: :light,
|
18
|
+
padding: 1
|
19
|
+
) do
|
20
|
+
"#{state[:content]}\n#{bounds.width}×#{bounds.height}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update(_message)
|
25
|
+
[self, Milktea::Message::None.new]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def default_state
|
31
|
+
{ title: "Box", content: "Hello" }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Row layout example
|
36
|
+
class RowDemo < Milktea::Container
|
37
|
+
direction :row
|
38
|
+
child SimpleBox, ->(_state) { { title: "Left", content: "Panel 1" } }, flex: 1
|
39
|
+
child SimpleBox, ->(_state) { { title: "Center", content: "Main Area" } }, flex: 2
|
40
|
+
child SimpleBox, ->(_state) { { title: "Right", content: "Panel 3" } }, flex: 1
|
41
|
+
|
42
|
+
def view
|
43
|
+
children_views
|
44
|
+
end
|
45
|
+
|
46
|
+
def update(message)
|
47
|
+
case message
|
48
|
+
when Milktea::Message::KeyPress
|
49
|
+
if message.value == "q"
|
50
|
+
[self, Milktea::Message::Exit.new]
|
51
|
+
else
|
52
|
+
[self, Milktea::Message::None.new]
|
53
|
+
end
|
54
|
+
else
|
55
|
+
[self, Milktea::Message::None.new]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Test the layout
|
61
|
+
puts "Simple Container Row Layout Test"
|
62
|
+
puts "Press 'q' to quit, any other key to continue..."
|
63
|
+
|
64
|
+
model = RowDemo.new(width: 60, height: 15)
|
65
|
+
program = Milktea::Program.new(model)
|
66
|
+
program.run
|
data/examples/counter.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "milktea"
|
6
|
+
|
7
|
+
# Counter model demonstrating state management
|
8
|
+
class CounterModel < Milktea::Model
|
9
|
+
def initialize(state = {})
|
10
|
+
default_state = { count: 0 }
|
11
|
+
super(default_state.merge(state))
|
12
|
+
end
|
13
|
+
|
14
|
+
def view
|
15
|
+
<<~VIEW
|
16
|
+
Counter: #{state[:count]}
|
17
|
+
|
18
|
+
Press:
|
19
|
+
- '+' or 'k' to increment
|
20
|
+
- '-' or 'j' to decrement
|
21
|
+
- 'r' to reset
|
22
|
+
- 'q' to quit
|
23
|
+
|
24
|
+
Ctrl+C to exit
|
25
|
+
VIEW
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(message)
|
29
|
+
case message
|
30
|
+
when Milktea::Message::Exit
|
31
|
+
[self, message]
|
32
|
+
when Milktea::Message::KeyPress
|
33
|
+
handle_keypress(message)
|
34
|
+
else
|
35
|
+
[self, Milktea::Message::None.new]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def handle_keypress(message)
|
42
|
+
case message.value
|
43
|
+
when "+", "k"
|
44
|
+
[with(count: state[:count] + 1), Milktea::Message::None.new]
|
45
|
+
when "-", "j"
|
46
|
+
[with(count: state[:count] - 1), Milktea::Message::None.new]
|
47
|
+
when "r"
|
48
|
+
[with(count: 0), Milktea::Message::None.new]
|
49
|
+
when "q"
|
50
|
+
[self, Milktea::Message::Exit.new]
|
51
|
+
else
|
52
|
+
[self, Milktea::Message::None.new]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create and run the counter program
|
58
|
+
model = CounterModel.new
|
59
|
+
program = Milktea::Program.new(model)
|
60
|
+
program.run
|