ratatui-ruby

Terminal UI development — widgets, layouts, events, Tea MVU, terminal rendering.

hoblin/anima1 installsMITSynced Aug 27

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI

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 components

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY