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.