ratatui-ruby
Terminal UI development — widgets, layouts, events, Tea MVU, terminal rendering.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "ratatui-ruby"
description: "Terminal UI development — widgets, layouts, events, Tea MVU, terminal rendering."
license: "MIT"
---
# RatatuiRuby
This skill provides guidance for building terminal user interfaces with RatatuiRuby, a Ruby gem wrapping Rust's Ratatui library. Use for TUI development, terminal widgets, layout systems, event handling, and testing terminal applications.
## Quick Reference
### Minimal Application
```ruby
require "ratatui_ruby"
RatatuiRuby.run do |tui|
loop do
tui.draw do |frame|
widget = tui.paragraph(text: "Hello, TUI!", block: tui.block(title: "App"))
frame.render_widget(widget, frame.area)
end
case tui.poll_event
in {type: :key, code: "q"}
break
in {type: :key, code: "c", modifiers: ["ctrl"]}
break
else
# Continue
end
end
end
```
### Key Concepts
| Concept | Purpose |
|---------|---------|
| `RatatuiRuby.run` | Managed loop handling terminal setup/teardown |
| `tui.draw` | Render widgets each frame |
| `tui.poll_event` | Capture keyboard/mouse input |
| `frame.area` | Available rendering area (Rect) |
| `frame.render_widget` | Render stateless widgets |
| `frame.render_stateful_widget` | Render widgets with state (List, Table) |
### Two Operating Modes
| Mode | Use Case | Setup |
|------|----------|-------|
| **Full-Screen** | Complete TUI applications | `RatatuiRuby.run { }` (default) |
| **Inline Viewport** | Rich CLI moments (spinners, progress) | `RatatuiRuby.run(viewport: :inline, height: 5) { }` |
**Full-Screen**: Takes over terminal, alternate screen, restored on exit.
**Inline Viewport**: Preserves scrollback, fixed-height widget area, output remains visible after exit.
## Core Pattern
The managed loop pattern handles terminal lifecycle:
```ruby
RatatuiRuby.run do |tui|
loop do
# 1. Draw UI
tui.draw do |frame|
# Render widgets
end
# 2. Handle events
case tui.poll_event
in {type: :key, code: "q"}
break
end
end
end
```
## Common Widgets
| Widget | Factory | Purpose |
|--------|---------|---------|
| Paragraph | `tui.paragraph(text:)` | Text display |
| Block | `tui.block(title:, borders:)` | Borders, titles, padding |
| List | `tui.list(items:)` | Selectable item list |
| Table | `tui.table(rows:, widths:)` | Tabular data |
| Gauge | `tui.gauge(ratio:)` | Progress indication |
| Tabs | `tui.tabs(titles:)` | Tab navigation |
| Chart | `tui.chart(datasets:)` | Data visualization |
| Canvas | `tui.canvas { }` | Custom drawing |
| Scrollbar | `tui.scrollbar` | Scroll indication |
### Stateless vs Stateful Widgets
**Stateless** (Paragraph, Block, Gauge):
```ruby
widget = tui.paragraph(text: "Hello")
frame.render_widget(widget, frame.area)
```
**Stateful** (List, Table):
```ruby
# Create state once (outside draw loop)
list_state = tui.list_state(0)
# In draw block
list = tui.list(items: ["Item A", "Item B", "Item C"])
frame.render_stateful_widget(list, frame.area, list_state)
# Update state on input
list_state.select_next if event_down?
```
### Block Composition
Wrap widgets with Block for borders and titles:
```ruby
block = tui.block(
title: "Main",
titles: [
{content: "Help: q", position: :bottom, alignment: :right}
],
borders: [:all],
border_style: {fg: "cyan"}
)
paragraph = tui.paragraph(text: "Content", block:)
```
## Layout
Split areas using constraints:
```ruby
layout = tui.layout(
direction: :vertical,
constraints: [
tui.constraint(:percentage, 20), # Header: 20%
tui.constraint(:min, 0), # Body: remaining
tui.constraint(:length, 3) # Footer: 3 rows
]
)
chunks = layout.split(frame.area)
# chunks[0] -> header area
# chunks[1] -> body area
# chunks[2] -> footer area
```
### Constraint Types
| Type | Syntax | Behavior |
|------|--------|----------|
| Length | `tui.constraint(:length, 5)` | Fixed 5 rows/cols |
| Percentage | `tui.constraint(:percentage, 50)` | 50% of parent |
| Min | `tui.constraint(:min, 10)` | At least 10 |
| Max | `tui.constraint(:max, 20)` | At most 20 |
| Ratio | `tui.constraint(:ratio, 1, 3)` | 1/3 of space |
| Fill | `tui.constraint(:fill)` | Expand into excess |
## Event Handling
### Pattern Matching
```ruby
case tui.poll_event
in {type: :key, code: "q"}
break
in {type: :key, code: "j"} | {type: :key, code: "down"}
list_state.select_next
in {type: :key, code: "k"} | {type: :key, code: "up"}
list_state.select_previous
in {type: :key, code: "c", modifiers: ["ctrl"]}
break
in {type: :mouse, kind: "down", button: "left", x:, y:}
handle_click(x, y)
in {type: :resize}
# Terminal resized, next draw adapts
end
```
### Event Helper Methods
```ruby
event = tui.poll_event
break if event.ctrl_c?
list_state.select_next if event.down? || event.j?
```
## Styling
Hash-based syntax for colors and modifiers:
```ruby
tui.paragraph(
text: "Styled text",
style: {fg: "green", bold: true},
block: tui.block(border_style: {fg: "cyan"})
)
```
### Text Composition
```ruby
line = tui.line([
tui.span("Normal "),
tui.span("Bold", style: {bold: true}),
tui.span(" Red", style: {fg: "red"})
])
tui.paragraph(text: line)
```
### Color Options
- Named: `"red"`, `"green"`, `"cyan"`, `"white"`
- Hex: `"#FF5733"`
- Indexed: 0-255 palette
## Testing
```ruby
require "ratatui_ruby/test_helper"
class MyAppTest < Minitest::Test
include RatatuiRuby::TestHelper
def test_renders_greeting
with_test_terminal(80, 24) do
RatatuiRuby.draw do |frame|
widget = RatatuiRuby::Widgets::Paragraph.new(text: "Hello")
frame.render_widget(widget, frame.area)
end
assert_snapshots("greeting") # Creates/compares snapshots/greeting.txt
end
end
def test_keyboard_navigation
with_test_terminal do
inject_keys("j", "j", "k") # Down, down, up
# Assert state changes
end
end
end
```
## Frameworks
### Tea (MVU Architecture)
Functional, Elm-style architecture for predictable state:
```ruby
require "ratatui_ruby/tea"
class Counter
include RatatuiRuby::Tea::App
def init
[Model.new(count: 0), nil]
end
def view(model, tui)
tui.paragraph(text: "Count: #{model.count}")
end
def update(message, model)
case message
in {type: :key, code: "q"}
[model, RatatuiRuby::Tea::Command.exit]
in {type: :key, code: "j"}
[model.with(count: model.count + 1), nil]
else
[model, nil]
end
end
end
Counter.new.run
```
## Best Practices
### Do
- Use `RatatuiRuby.run` for managed terminal lifecycle
- Create state objects outside the draw loop
- Use pattern matching for event handling
- Use inline viewports for CLI "rich moments"
- Test with `RatatuiRuby::TestHelper`
### Don't
- Handle Ctrl+C manually (use `event.ctrl_c?` helper)
- Forget to break the loop (leads to CPU spin)
- Render widgets before poll_event (blocks input)
- Use full-screen for simple progress indicators
## Additional Resources
### Reference Files
For detailed API documentation and patterns:
- **`references/core-concepts.md`** - Managed loop, terminal lifecycle, inline vs full-screen
- **`references/widgets.md`** - Complete widget catalog, composition patterns
- **`references/layout.md`** - Constraints, directions, nested layouts
- **`references/events.md`** - Keyboard, mouse, event handling patterns
- **`references/styling.md`** - Colors, modifiers, text composition
- **`references/testing.md`** - TestHelper, snapshots, event injection
- **`references/frameworks.md`** - Tea MVU, Kit componentsMore General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

