Milktea
A Terminal User Interface (TUI) framework for Ruby, inspired by Bubble Tea 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.
Features
- 🏗️ Elm Architecture: Immutable state management with predictable message flow
- 📦 Container Layouts: Flexbox-style layouts for terminal interfaces
- 🔄 Hot Reloading: Instant feedback during development (similar to web frameworks)
- 📱 Responsive Design: Automatic adaptation to terminal resize events
- 🧩 Composable Components: Build complex UIs from simple, reusable models
- 🎨 Rich Terminal Support: Leverage TTY gems for advanced terminal features
Installation
Add Milktea to your application's Gemfile:
gem 'milktea'
Or install directly:
gem install milktea
For development versions:
gem 'milktea', git: 'https://github.com/elct9620/milktea'
Quick Start
Here's a simple "Hello World" application:
require 'milktea'
class HelloModel < Milktea::Model
def view
"Hello, #{state[:name]}! Count: #{state[:count]}"
end
def update(message)
case message
when Milktea::Message::KeyPress
case message.value
when "+"
[with(count: state[:count] + 1), Milktea::Message::None.new]
when "q"
[self, Milktea::Message::Exit.new]
else
[self, Milktea::Message::None.new]
end
else
[self, Milktea::Message::None.new]
end
end
private
def default_state
{ name: "World", count: 0 }
end
end
# Simple approach using Application class
class MyApp < Milktea::Application
root "HelloModel"
end
MyApp.boot
Core Concepts
Models & Elm Architecture
Milktea follows the Elm Architecture pattern with three core concepts:
- Model: Immutable state container
- View: Pure function that renders state to string
- Update: Handles messages and returns new state + side effects
class CounterModel < Milktea::Model
def view
"Count: #{state[:count]} (Press +/- to change, q to quit)"
end
def update(message)
case message
when Milktea::Message::KeyPress
handle_keypress(message)
when Milktea::Message::Resize
# Rebuild model with fresh class for new screen dimensions
[with, Milktea::Message::None.new]
else
[self, Milktea::Message::None.new]
end
end
private
def default_state
{ count: 0 }
end
def handle_keypress(message)
case message.value
when "+"
[with(count: state[:count] + 1), Milktea::Message::None.new]
when "-"
[with(count: state[:count] - 1), Milktea::Message::None.new]
when "q"
[self, Milktea::Message::Exit.new]
else
[self, Milktea::Message::None.new]
end
end
end
Container Layout System
Milktea provides a flexbox-inspired layout system for building complex terminal interfaces:
class AppLayout < Milktea::Container
direction :column
child HeaderModel, flex: 1
child ContentModel, flex: 3
child FooterModel, flex: 1
end
class SidebarLayout < Milktea::Container
direction :row
child SidebarModel, flex: 1
child MainContentModel, flex: 3
end
Key Container Features:
-
Direction:
:row
or:column
(default::column
) - Flex Properties: Control size ratios between children
- State Mapping: Pass specific state portions to children
- Bounds Calculation: Automatic layout calculation and propagation
class AdvancedContainer < Milktea::Container
direction :row
# Pass specific state to children with state mappers
child SidebarModel, ->(state) { { items: state[:sidebar_items] } }, flex: 1
child ContentModel, ->(state) { state.slice(:title, :content) }, flex: 2
child InfoModel, flex: 1
end
Hot Reloading (Development Feature)
Milktea supports hot reloading for rapid development iteration:
# Configure hot reloading
Milktea.configure do |config|
config.autoload_dirs = ["app/models", "lib/components"]
config.hot_reloading = true
end
class DevelopmentModel < Milktea::Model
def update(message)
case message
when Milktea::Message::Reload
# Hot reload detected - rebuild with fresh class
[with, Milktea::Message::None.new]
# ... other message handling
end
end
end
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.
Terminal Resize Handling
Milktea automatically detects terminal resize events and provides a simple pattern for responsive layouts:
class ResponsiveApp < Milktea::Container
direction :column
child HeaderModel, flex: 1
child DynamicContentModel, flex: 4
def update(message)
case message
when Milktea::Message::Resize
# Only root model needs resize handling
# All children automatically recalculate bounds
[with, Milktea::Message::None.new]
when Milktea::Message::KeyPress
handle_keypress(message)
else
[self, Milktea::Message::None.new]
end
end
end
Resize Handling Key Points:
-
Root-Level Only: Only the root model needs to handle
Message::Resize
- Automatic Cascading: Child components automatically adapt to new dimensions
- Bounds Recalculation: Container layouts automatically recalculate flex distributions
-
Screen Methods: Use
screen_width
,screen_height
,screen_size
for responsive logic
Examples
Explore the examples/
directory for comprehensive demonstrations:
- Container Layout: Flexbox-style layouts with resize support
- Hot Reload Demo: Development workflow with instant updates
Run examples:
ruby examples/container_layout.rb
ruby examples/hot_reload_demo.rb
Advanced Features
Dynamic Child Resolution
Use symbols to dynamically resolve child components:
class DynamicContainer < Milktea::Container
direction :column
child :header_component, flex: 1 # Calls header_component method
child ContentModel, flex: 3 # Direct class reference
private
def header_component
state[:show_advanced] ? AdvancedHeader : SimpleHeader
end
end
Custom Message Handling
Create custom messages for complex interactions:
# Define custom message
CustomAction = Data.define(:action_type, :payload)
class CustomModel < Milktea::Model
def update(message)
case message
when CustomAction
handle_custom_action(message)
# ... standard message handling
end
end
private
def handle_custom_action(message)
case message.action_type
when :save
# Handle save action
[with(saved: true), Milktea::Message::None.new]
when :load
# Handle load action
[with(data: message.payload), Milktea::Message::None.new]
end
end
end
Application vs Manual Setup
Choose between high-level Application class or manual setup:
# High-level Application approach (recommended)
class MyApp < Milktea::Application
root "MainModel"
end
MyApp.boot
# Manual setup (advanced)
config = Milktea.configure do |c|
c.autoload_dirs = ["app/models"]
c.hot_reloading = true
end
loader = Milktea::Loader.new(config)
loader.hot_reload if config.hot_reloading?
model = MainModel.new
program = Milktea::Program.new(model, config: config)
program.run
API Reference
Core Classes
-
Milktea::Model
: Base class for all UI components -
Milktea::Container
: Layout container with flexbox-style properties -
Milktea::Application
: High-level application wrapper -
Milktea::Program
: Main application runtime -
Milktea::Message
: Standard message types (KeyPress, Exit, Resize, Reload)
Message System
-
Message::KeyPress
: Keyboard input events -
Message::Exit
: Application termination -
Message::Resize
: Terminal size changes -
Message::Reload
: Hot reload events -
Message::None
: No-operation message
For detailed API documentation, see the documentation website.
Development
After checking out the repo:
bin/setup # Install dependencies
bundle exec rake spec # Run tests
bundle exec rake rubocop # Check code style
bundle exec rake # Run all checks
bin/console # Interactive prompt
Testing
Milktea uses RSpec for testing. Run specific tests:
bundle exec rspec spec/milktea/model_spec.rb
bundle exec rspec spec/milktea/container_spec.rb:42 # Specific line
Code Quality
The project uses RuboCop for code formatting:
bundle exec rake rubocop:autocorrect # Fix auto-correctable issues
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/elct9620/milktea.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin feature/my-new-feature
) - Create a Pull Request
License
The gem is available as open source under the terms of the MIT License.
Acknowledgments
- Inspired by Bubble Tea - Go TUI framework
- Built on the TTY toolkit ecosystem
- Follows Elm Architecture principles