Developing Examples

Guidelines for creating and testing examples in the examples/ directory.

Example Structure

Every interactive example should follow this pattern, living in its own directory:

examples/my_example/app.rb:

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"

class MyExampleApp
  def initialize
    # Initialize state (styles must be initialized in run when @tui is available)
  end

  def run
    RatatuiRuby.run do |tui|
      @tui = tui  # Store for use in private methods
      loop do
        render
        break if handle_input == :quit
      end
    end
  end

  private

  def render
    @tui.draw do |frame|
      # 1. Split layout using Session helpers
      areas = @tui.layout_split(frame.area, constraints: [@tui.constraint_fill(1)])
      # 2. Create and render widgets
      widget = @tui.paragraph(text: "Hello", block: @tui.block(borders: [:all]))
      frame.render_widget(widget, areas[0])
    end
  end

  def handle_input
    case @tui.poll_event
    in { type: :key, code: "q" }
      :quit
    in { type: :key, code: code }
      # Handle other keys
    else
      # Ignore unhandled events
    end
  end
end

MyExampleApp.new.run if __FILE__ == $PROGRAM_NAME

Naming Convention (Required)

Example directories must follow a prefixing convention to categorize them alphabetically: - app_: Application showcases (e.g., app_analytics). Class name: AppAnalytics. - widget_: Widget-focused demonstrations (e.g., widget_gauge_demo). Class name: WidgetGaugeDemo. - verify_: Documentation verification examples (e.g., verify_readme_usage). Class name: VerifyReadmeUsage.

The directory and class names must match (snake_case directory maps to PascalCase class).

This convention enables the terminal_preview:update rake task to automatically capture terminal output for all examples without maintaining a manual registry.

Terminal Size Constraint

All interactive examples must fit within an 80×24 terminal (standard VT100 dimensions). This ensures: - Examples work on minimal terminal configurations - Tests can use the default with_test_terminal size (80x24) - Examples remain discoverable and self-documenting through visible hotkey help

Layout pattern for 80×24: - Bottom control panel: Allocate ~5-7 lines at bottom for a full-width control block with hotkey documentation. Style hotkeys with bold and underline to make them discoverable. Use double-space or pipe separators to compress multiple controls per line. This keeps all UI text at full readability while maximizing space for the main content area.

Best practices: - Use descriptive names (e.g., “Yellow on Black” not “Yellow”) so controls are self-documenting and discoverable. - Style hotkeys visually: Use modifiers: [:bold, :underlined] on hotkey letters to make them stand out from descriptions. Example: i (bold, underlined) followed by Items. - Test early by running the example at 80×24 and verifying all content is visible without wrapping, scrolling, or clipping.

Type Signatures

Every example must also have an RBS file documenting its public methods. Type signatures live in a centralized location:

sig/examples/my_example/app.rbs:

class MyExampleApp
  # @public
  def self.new: () -> MyExampleApp

  # @public
  def run: () -> void
end

Directory Structure

Examples are organized across three locations:

examples/
  my_example/
    app.rb              ← REQUIRED: The runnable example code
    README.md           ← REQUIRED: Purpose, architecture, hotkeys, usage

test/examples/
  my_example/
    test_app.rb         ← REQUIRED: Tests (centralized, not local to example)
    snapshots/          ← Auto-created by assert_snapshot
      initial_render.txt

sig/examples/
  my_example/
    app.rbs             ← REQUIRED: Type signatures (centralized, not local to example)

Key Requirements

  1. Only run should be public. All other methods (render, handle_input, helper methods) must be private. This prevents tests from calling internal methods directly.

  2. Use RatatuiRuby.run for terminal management. Never call init_terminal or restore_terminal directly. The run block handles terminal setup/teardown automatically and safely, even if an exception occurs.

  3. Use the Session API (tui) for cleaner code. Accept the tui block parameter from RatatuiRuby.run and use it throughout your app:

  4. @tui.draw { |frame| ... } instead of RatatuiRuby.draw

  5. @tui.poll_event instead of RatatuiRuby.poll_event

  6. @tui.style(...) instead of RatatuiRuby::Style.new(...)

  7. @tui.paragraph(...) instead of RatatuiRuby::Paragraph.new(...)

  8. @tui.block(...) instead of RatatuiRuby::Block.new(...)

  9. @tui.layout_split(...) instead of RatatuiRuby::Layout.split(...)

  10. @tui.constraint_fill(...) instead of RatatuiRuby::Constraint.fill(...)

  11. @tui.text_line(...) instead of RatatuiRuby::Text::Line.new(...)

  12. @tui.text_span(...) instead of RatatuiRuby::Text::Span.new(...)

  13. Event handling must include a catch-all pattern. When using pattern matching in handle_input, always include an else clause at the end to catch unmatched events (mouse events, resize events, focus events, etc.). Without it, unmatched events will raise NoMatchingPatternError:

“‘ruby def handle_input case @tui.poll_event in { type: :key, code: “q” } :quit in { type: :mouse, kind: “down”, x:, y: } handle_click(x, y) else # Ignore other events end end

  1. Use keyboard keys to cycle through widget attributes. Users should be able to interactively explore all widget options. Common patterns:

    • Arrow keys: Navigate or adjust values

    • Letter keys: Cycle through styles, modes, or variants. Prefer all lowercase keys to avoid confusion and simplify the UI description.

    • Space: Toggle or select

    • q or Ctrl+C: Quit

    • Arrow keys: Navigate or adjust values

    • Letter keys: Cycle through styles, modes, or variants. Prefer all lowercase keys to avoid confusion and simplify the UI description.

    • Space: Toggle or select

    • q or Ctrl+C: Quit

  2. All examples must include a README.md explaining:

  3. What problem the example solves

  4. Architecture (if applicable)

  5. Hotkeys (if interactive): Document all keyboard/mouse controls

  6. Key concepts demonstrated

  7. Usage instructions

  8. Learning outcomes

See examples/app_color_picker/README.md and examples/app_all_events/README.md for patterns. Adhere to docs/contributors/documentation_style.md.

  1. Naming Conventions for Controls

When documenting hotkeys and cycling options in the UI, use consistent naming:

# In controls: “h: Highlight Style (#{@styles[:name]})” # Outputs: “h: Highlight Style (Yellow Bold)” “‘

This keeps the UI self-documenting and users can see exact parameter names when they read the hotkey help.

  1. Hit Testing

Examples with mouse interaction should use the Frame API. By calling @tui.layout_split inside @tui.draw, you obtain the exact Rects used for rendering. Store these rects in instance variables (e.g., @sidebar_rect) to use them in your handle_input method for hit testing:

if @sidebar_rect&.contains?(event.x, event.y)
  # Handle click
end

Testing Examples

Example tests live in a centralized test tree:

test/examples/my_example/test_app.rb:

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"
require "ratatui_ruby/test_helper"
require "minitest/autorun"
require_relative "app"

class TestMyExampleApp < Minitest::Test
  include RatatuiRuby::TestHelper

  def setup
    @app = MyExampleApp.new
  end

  def test_initial_render
    with_test_terminal do
      inject_key(:q)
      @app.run
      assert_snapshot("initial_render")
    end
  end
end

Snapshot Testing Pattern (REQUIRED)

All example tests MUST use snapshot testing via the assert_snapshot API, not manual content assertions.

Why Snapshots

Basic Pattern

def test_initial_render
  with_test_terminal do
    inject_key(:q)
    @app.run
    
    assert_snapshot("initial_render")
  end
end

Snapshot auto-saved to: test/examples/widget_foo_demo/snapshots/initial_render.txt

With Normalization (for dynamic content)

For examples with timestamps, random data, or other non-deterministic output:

private def assert_normalized_snapshot(snapshot_name)
  assert_snapshot(snapshot_name) do |actual|
    actual.map do |line|
      line.gsub(/\d{2}:\d{2}:\d{2}/, "XX:XX:XX")  # Mask timestamps
           .gsub(/Random ID: \d+/, "Random ID: XXX")  # Mask random values
    end
  end
end

def test_after_event
  with_test_terminal do
    inject_key("a")
    inject_key(:q)
    @app.run
    
    assert_normalized_snapshot("after_event")
  end
end

See test/examples/app_all_events/test_app.rb for a complete example.

Regenerating Snapshots

When UI changes are intentional, regenerate all snapshots:

UPDATE_SNAPSHOTS=1 bin/agent_rake test

Widget Attribute Cycling

Examples should demonstrate widget configurability by allowing interactive cycling:

Widget Attribute Key Suggestion
Tabs highlight_style Space
Tabs divider d
Tabs style s
Block border_type Space
Block border_color c
List highlight_style Space
Sparkline direction d
Scrollbar orientation o
Scrollbar theme s

Display the current state in the UI (e.g., in a title or status bar or paragraph) so users can see what changed. Display the hotkey in the UI as well, so users can see how to change it; the hotkey should not disappear as app state changes.

Data Quality

Examples must use realistic, meaningful data—not dummy placeholder text. This ensures examples: - Demonstrate the widget’s real-world usage - Provide educational value to users reading the code - Look professional and polished

Guidelines

For small datasets (< 10 items): Use hardcoded realistic data. Examples: - Geographic coordinates with city names (see widget_map_demo/app.rb) - Real product names or person names - Meaningful status values (“Completed”, “Pending”, “Failed”)

For large datasets (≄ 10 items): Use the Faker gem with deterministic seeding so data is consistent across runs:

require "faker"

# Seed Faker for reproducible output
Faker::Config.random = Random.new(12345)

# Generate realistic data
users = Array.new(50) { Faker::Name.name }
emails = Array.new(50) { Faker::Internet.email }

In tests, set the same seed before each test to ensure snapshot consistency.

Avoid: - Repeated placeholder text like “Lorem ipsum” or “Background Layer” × 20 - Generic numbered items like “Item 1”, “Item 2” (unless the context demands it) - Nonsensical or visually jarring content