Help us improve
Share bugs, ideas, or general feedback.
From ratatui-ruby
Guides building terminal UIs with RatatuiRuby Ruby gem, including managed loops, widgets like paragraphs/tables/charts, event handling, full-screen/inline modes, and testing.
npx claudepluginhub hoblin/claude-ruby-marketplace --plugin ratatui-rubyHow this skill is triggered — by the user, by Claude, or both
Slash command
/ratatui-ruby:ratatui-rubyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
Provides design patterns for terminal user interfaces: layout paradigms, keyboard navigation, visual systems, and TUI anti-pattern validation. Works with Ratatui, Ink, Textual, Bubbletea, or any TUI framework.
Provides CLI design patterns for arguments/flags/subcommands and TUI patterns for frameworks in Go/Python/JS/Rust, interactions, and colors in terminal apps.
Builds Textual TUI apps in Python: creates widgets, lays out screens with CSS, handles events/actions/bindings, manages reactivity, tests with Pilot, runs workers.
Share bugs, ideas, or general feedback.
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.
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
| 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) |
| 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.
The managed loop pattern handles terminal lifecycle:
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
| 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 (Paragraph, Block, Gauge):
widget = tui.paragraph(text: "Hello")
frame.render_widget(widget, frame.area)
Stateful (List, Table):
# 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?
Wrap widgets with Block for borders and titles:
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:)
Split areas using constraints:
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
| 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 |
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 = tui.poll_event
break if event.ctrl_c?
list_state.select_next if event.down? || event.j?
Hash-based syntax for colors and modifiers:
tui.paragraph(
text: "Styled text",
style: {fg: "green", bold: true},
block: tui.block(border_style: {fg: "cyan"})
)
line = tui.line([
tui.span("Normal "),
tui.span("Bold", style: {bold: true}),
tui.span(" Red", style: {fg: "red"})
])
tui.paragraph(text: line)
"red", "green", "cyan", "white""#FF5733"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
Functional, Elm-style architecture for predictable state:
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
RatatuiRuby.run for managed terminal lifecycleRatatuiRuby::TestHelperevent.ctrl_c? helper)For detailed API documentation and patterns:
references/core-concepts.md - Managed loop, terminal lifecycle, inline vs full-screenreferences/widgets.md - Complete widget catalog, composition patternsreferences/layout.md - Constraints, directions, nested layoutsreferences/events.md - Keyboard, mouse, event handling patternsreferences/styling.md - Colors, modifiers, text compositionreferences/testing.md - TestHelper, snapshots, event injectionreferences/frameworks.md - Tea MVU, Kit components