Application Architecture

Architect robust TUI applications using core lifecycle patterns and API best practices.

Core Concepts

Your app lives inside a terminal. You need to respect its rules.

Lifecycle Management

Terminals have state. They remember cursor positions, input modes, and screen buffers.

The Problem: If your app crashes or exits without cleaning up, it “breaks” the user’s terminal. The cursor vanishes. Input echoes constantly. The alternate screen doesn’t clear.

The Solution: The library’s lifecycle manager handles this for you. It enters “raw mode” on startup and guarantees restoration on exit.

Use RatatuiRuby.run

This method acts as a safety net. It initializes the terminal, yields control to your block, and restores the terminal afterwards—even if your code raises an exception.

RatatuiRuby.run do |tui|
  loop do
    tui.draw do |frame|
      frame.render_widget(tui.paragraph(text: "Hello"), frame.area)
    end
    break if tui.poll_event == "q"
  end
end
# Terminal is restored here

Manual Management

Need granular control? You can initialize and restore the terminal yourself. Use ensure blocks to guarantee cleanup.

RatatuiRuby.init_terminal
begin
  RatatuiRuby.draw do |frame|
    frame.render_widget(RatatuiRuby::Paragraph.new(text: "Hello"), frame.area)
  end
ensure
  RatatuiRuby.restore_terminal
  # Terminal is restored here
end

API Convenience

Writing UI trees involves nesting many widgets.

The Problem: Explicitly namespacing RatatuiRuby:: for every widget (e.g., RatatuiRuby::Paragraph.new) is tedious. It creates visual noise that hides your layout structure.

The Solution: The Session API (tui) provides shorthand factories for every widget. It yields a session object to your block.

RatatuiRuby.run do |tui|
  loop do
    tui.draw do |frame|
      # Split layout using Session helpes
      sidebar_area, content_area = tui.layout_split(
        frame.area,
        direction: :horizontal,
        constraints: [
          tui.constraint_length(20),
          tui.constraint_min(0)
        ]
      )

      # Render sidebar
      frame.render_widget(
        tui.paragraph(
          text: tui.text_line(spans: [
            tui.text_span(content: "Side", style: tui.style(fg: :blue)),
            tui.text_span(content: "bar")
          ]),
          block: tui.block(borders: [:all], title: "Nav")
        ),
        sidebar_area
      )

      # Render main content
      frame.render_widget(
        tui.paragraph(
          text: "Main Content",
          style: tui.style(fg: :green),
          block: tui.block(borders: [:all], title: "Content")
        ),
        content_area
      )
    end
    
    event = tui.poll_event
    break if event == "q" || event == :ctrl_c
  end
end

Raw API

Building your own abstractions? You might prefer explicit class instantiation. The raw constants are always available.

RatatuiRuby.run do
  loop do
    RatatuiRuby.draw do |frame|
      # Manual split
      rects = RatatuiRuby::Layout.split(
        frame.area,
        direction: :horizontal,
        constraints: [
          RatatuiRuby::Constraint.length(20),
          RatatuiRuby::Constraint.min(0)
        ]
      )

      frame.render_widget(
        RatatuiRuby::Paragraph.new(
          text: RatatuiRuby::Text::Line.new(spans: [
            RatatuiRuby::Text::Span.new(content: "Side", style: RatatuiRuby::Style.new(fg: :blue)),
            RatatuiRuby::Text::Span.new(content: "bar")
          ]),
          block: RatatuiRuby::Block.new(borders: [:all], title: "Nav")
        ),
        rects[0]
      )

      frame.render_widget(
        RatatuiRuby::Paragraph.new(
          text: "Main Content",
          style: RatatuiRuby::Style.new(fg: :green),
          block: RatatuiRuby::Block.new(borders: [:all], title: "Content")
        ),
        rects[1]
      )
    end

    event = RatatuiRuby.poll_event
    break if event == "q" || event == :ctrl_c
  end
end

Reference Architectures

Simple scripts work well with valid linear code. Complex apps need structure.

We provide these reference architectures to inspire you:

MVVM (Model-View-ViewModel)

Source: examples/app_all_events

This pattern strictly separates concerns: * Model: Pure business logic. It handles data and events. * View State (ViewModel): An immutable data structure built fresh every frame. It calculates logic-dependent properties (like border_color) so the View doesn’t have to. * View: Pure rendering logic. It takes the View State and draws it.

Use this when your app has complex state rules that clutter your rendering code.

Scene-Orchestrated MVC

Source: examples/app_color_picker

This pattern addresses the difficulty of mouse interaction and layout management: * Scene: A specialized View that owns the layout and hit testing. It caches the screen coordinates of widgets during the draw phase. * App (Controller): Handles events by querying the Scene (e.g., scene.rect_at(x, y)).

Use this when you need rich interactivity (mouse clicks, drag-and-drop) or complex dynamic layouts.