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
-
Only
runshould be public. All other methods (render,handle_input, helper methods) must be private. This prevents tests from calling internal methods directly. -
Use
RatatuiRuby.runfor terminal management. Never callinit_terminalorrestore_terminaldirectly. Therunblock handles terminal setup/teardown automatically and safely, even if an exception occurs. -
Use the Session API (
tui) for cleaner code. Accept thetuiblock parameter fromRatatuiRuby.runand use it throughout your app: -
@tui.draw { |frame| ... }instead ofRatatuiRuby.draw -
@tui.poll_eventinstead ofRatatuiRuby.poll_event -
@tui.style(...)instead ofRatatuiRuby::Style.new(...) -
@tui.paragraph(...)instead ofRatatuiRuby::Paragraph.new(...) -
@tui.block(...)instead ofRatatuiRuby::Block.new(...) -
@tui.layout_split(...)instead ofRatatuiRuby::Layout.split(...) -
@tui.constraint_fill(...)instead ofRatatuiRuby::Constraint.fill(...) -
@tui.text_line(...)instead ofRatatuiRuby::Text::Line.new(...) -
@tui.text_span(...)instead ofRatatuiRuby::Text::Span.new(...) -
Event handling must include a catch-all pattern. When using pattern matching in
handle_input, always include anelseclause at the end to catch unmatched events (mouse events, resize events, focus events, etc.). Without it, unmatched events will raiseNoMatchingPatternError:
ââ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
-
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
-
qor 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
-
qor Ctrl+C: Quit
-
-
All examples must include a README.md explaining:
-
What problem the example solves
-
Architecture (if applicable)
-
Hotkeys (if interactive): Document all keyboard/mouse controls
-
Key concepts demonstrated
-
Usage instructions
-
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.
-
Naming Conventions for Controls
When documenting hotkeys and cycling options in the UI, use consistent naming:
-
Parameter names: Always match the actual Ruby parameter name. For example:
-
Use âScroll Paddingâ (not âScroll Padâ) for the
scroll_padding:parameter -
Use âHighlight Styleâ (not âHighlightâ) for the
highlight_style:parameter -
Use âRepeat Symbolâ (not âRepeatâ) for the
repeat_highlight_symbol:parameter -
Display names for cycled values: Create a
namefield in your options hash to keep display names paired with values. Initialize style arrays insiderunwhen@tuiis available: ââruby # In run method, after @tui = tui: @styles = [ { name: âYellow Boldâ, style: @tui.style(fg: :yellow, modifiers: [:bold]) }, { name: âBlue on Whiteâ, style: @tui.style(fg: :blue, bg: :white) } ]
# 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.
-
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
-
Exact verification: Captures complete screen state, character-by-character
-
Auto-update:
UPDATE_SNAPSHOTS=1 bin/agent_rake testregenerates all snapshots -
Auto-managed: Snapshots live in
test/examples/{name}/snapshots/{test_name}.txt -
Maintainable: No tedious manual string checks
-
Self-documenting: Snapshots show exactly what output is expected
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