Search

PythonReactSEOPDF
GitHub Skill

ratatui-ruby

This skill should be used when the user asks to "create a TUI", "terminal interface", "terminal UI", "ratatui", "ratatui-ruby", "inline viewport", "full-screen terminal app", "terminal widgets", "tui.draw", "tui.poll_event", or mentions RatatuiRuby.run, managed loop, terminal rendering, Tea MVU, or building CLI applications with rich UI elements. Should also be used when editing RatatuiRuby application files, working with terminal widgets, or discussing TUI architecture patterns.

hoblin☆ 21 Repository stars1 Repository forks

Installation

Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.

View source ↗Guides

Skill instructions

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
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:

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):

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?
Block Composition

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:)

Layout

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
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
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
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:

tui.paragraph(
  text: "Styled text",
  style: {fg: "green", bold: true},
  block: tui.block(border_style: {fg: "cyan"})
)
Text Composition
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

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:

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

Examples

Read the usage examples in the source repository.

View source ↗

Source history

Indexed on
2026-03-11
Index updated
2026-03-11
Branch
main

Index and sync dates describe this directory. View the source repository for code history.

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!