Project

ratatat

0.0
The project is in a healthy, maintained state
Build terminal user interfaces with a reactive, component-based architecture. Features reactive properties, CSS-like styling, message-driven communication, and a rich widget library.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

~> 3.12
~> 0.5
~> 0.11

Runtime

 Project Readme

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.run

Project Structure

Widgets

Layout:

Input:

Display:

Overlay:

Examples

Documentation

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
end

Key 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
end

Async 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
end

Querying Widgets

query("#my-id")           # By ID
query(".my-class")        # By class
query(Button)             # By type
query_one("#unique")      # Single result
query(".items").remove    # Bulk operations

Performance

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 example

Colors

# 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,
])