Ratatat
A pure Ruby TUI framework inspired by Python's Textual. Build terminal user interfaces with a reactive, component-based architecture. No FFI required.
Ratatat provides a Textual-like development experience in Ruby: reactive properties, CSS-like styling, message-driven communication, and a rich widget library. It uses double-buffered rendering with cell-based diffing for flicker-free 60+ fps output.
Core Concepts
Architecture: "Attributes Down, Messages Up"
- Parents set child attributes or call child methods directly
- Children communicate to parents ONLY via messages (bubbling)
- Siblings communicate through parent intermediary
Reactive Properties: Declare with reactive :name, default: value, repaint: true. Changes automatically trigger re-renders.
Widget Tree: Apps compose widgets via compose method. Query with CSS-like selectors: query("#id"), query(".class"), query(WidgetClass).
Message System: Key presses, focus changes, and custom events bubble up the tree. Handle with on_<message_type> methods.
Quick Start
require "ratatat"
class MyApp < Ratatat::App
def compose
[
Ratatat::Vertical.new.tap do |v|
v.mount(
Ratatat::Static.new("Hello, World!"),
Ratatat::Button.new("Click me", id: "btn")
)
end
]
end
def on_button_pressed(message)
query_one("#btn").label = "Clicked!"
end
end
MyApp.new.runProject Structure
- lib/ratatat.rb: Main entry point, requires all components
- lib/ratatat/app.rb: Application base class with event loop
- lib/ratatat/widget.rb: Base widget class
- lib/ratatat/buffer.rb: Double-buffered rendering with diffing
- lib/ratatat/cell.rb: Terminal cell (symbol, colors, modifiers)
- lib/ratatat/terminal.rb: Terminal abstraction layer
- lib/ratatat/message.rb: Message types (Key, Focus, Blur, Quit)
- lib/ratatat/reactive.rb: Reactive property system
- lib/ratatat/dom_query.rb: jQuery-like chainable queries
- lib/ratatat/styles.rb: Inline styling system
- lib/ratatat/css_parser.rb: CSS stylesheet parser
Widgets
Layout:
- Vertical: Stack children vertically with optional ratios
- Horizontal: Stack children horizontally with optional ratios
- Grid: CSS Grid-like layout
- Container: Generic container
- ScrollableContainer: Scrollable viewport
Input:
- Button: Clickable button with variants
- TextInput: Single-line text input
- TextArea: Multi-line text editor
- Checkbox: Toggle checkbox
- RadioSet: Radio button group
- Select: Dropdown selection
Display:
- Static: Static text display
- ProgressBar: Progress indicator
- Sparkline: Inline charts
- Spinner: Animated loading indicator
- DataTable: Tabular data display
- Tree: Hierarchical tree view
- Log: Scrolling log output
Overlay:
- Modal: Modal dialogs
- Toast: Notification toasts
- Tooltip: Hover tooltips
- TabbedContent: Tabbed panels
Examples
- examples/log_tailer.rb: Two-pane log viewer with filtering
Documentation
- docs/textual-features-plan.md: Full feature implementation plan and status
API Patterns
Creating Widgets
class MyWidget < Ratatat::Widget
CAN_FOCUS = true # Enable focus for this widget
reactive :count, default: 0, repaint: true
def render(buffer, x:, y:, width:, height:)
buffer.put_string(x, y, "Count: #{count}")
end
def on_key(message)
case message.key
when "up" then self.count += 1
when "down" then self.count -= 1
end
end
endKey Bindings
class MyApp < Ratatat::App
BINDINGS = [
Ratatat::Binding.new("q", "quit", "Quit application"),
Ratatat::Binding.new("r", "refresh", "Refresh data"),
]
def action_quit = exit
def action_refresh = reload_data
endAsync Operations
# Timers
set_timer(2.0) { show_message("2 seconds passed") }
set_interval(1.0) { update_clock }
# Background workers
run_worker(:fetch_data) { HTTP.get(url) }
def on_worker_done(message)
return unless message.name == :fetch_data
self.data = message.result
endQuerying Widgets
query("#my-id") # By ID
query(".my-class") # By class
query(Button) # By type
query_one("#unique") # Single result
query(".items").remove # Bulk operationsPerformance
Buffer diffing optimized for 60+ fps:
- 80x24 terminal: 0.6ms diff time
- 180x48 terminal: 2.6ms diff time
- Rendering: 1600+ fps achievable
Optional
Development
bundle install
bundle exec rspec # Run tests (419 specs)
bundle exec ruby examples/log_tailer.rb # Run exampleColors
# Named colors
Ratatat::Color::Named::Red
Ratatat::Color::Named::BrightBlue
# 256-color palette
Ratatat::Color::Indexed.new(196)
# True color (RGB)
Ratatat::Color::Rgb.new(255, 128, 0)Modifiers
modifiers = Set.new([
Ratatat::Modifier::Bold,
Ratatat::Modifier::Italic,
Ratatat::Modifier::Underline,
])