Layouts
Layouts determine how widgets are arranged on screen. Agent Air provides four built-in templates that cover common patterns, plus the ability to create fully custom layouts. The layout system handles responsive sizing, widget visibility, and render ordering automatically, adapting to terminal size changes without manual intervention.
A well-chosen layout makes the difference between a cramped, confusing interface and one that feels natural to use. The built-in templates encode best practices for terminal UI design, while custom layouts let you create specialized interfaces when your needs go beyond the standard patterns.
Layout Templates
The LayoutTemplate enum defines the available layout options. Each template takes an options struct that configures its behavior, with sensible defaults for quick setup. Templates encapsulate layout logic so you can switch between layouts without rewriting widget positioning code.
Templates handle the dynamic aspects of layout—expanding and collapsing panels, adjusting to terminal resize events, and managing widget visibility. This lets you focus on what widgets to show rather than how to arrange them pixel by pixel.
pub enum LayoutTemplate {
Standard(StandardOptions),
Sidebar(SidebarOptions),
Split(SplitOptions),
Minimal(MinimalOptions),
Custom(Box<dyn LayoutProvider>),
CustomFn(LayoutFn),
}
Most agents use one of the built-in templates. Custom layouts are available for specialized interfaces that need precise control over widget placement.
Standard Layout
The standard layout arranges widgets vertically: chat view at top filling available space, panels in the middle when active, input area near the bottom, and status bar at the very bottom. This is the default layout and works well for most conversational agents because it prioritizes the conversation while keeping input accessible.
The vertical arrangement follows the natural flow of conversation: new content appears and scrolls up, while the input area stays fixed at the bottom. Panels for permissions and questions appear between the chat and input, drawing attention without obscuring the conversation history.
use agent_air::tui::layout::LayoutTemplate;
let layout = LayoutTemplate::standard();
StandardOptions
Fine-tune the standard layout by providing custom options. Each field controls a specific aspect of the layout, from which widgets appear in each position to minimum size constraints. Most applications can use the defaults, but these options provide flexibility when needed.
pub struct StandardOptions {
pub main_widget_id: &'static str,
pub input_widget_id: &'static str,
pub panel_widget_ids: Vec<&'static str>,
pub popup_widget_ids: Vec<&'static str>,
pub overlay_widget_ids: Vec<&'static str>,
pub min_main_height: u16,
pub fixed_input_height: Option<u16>,
pub status_bar_widget_id: Option<&'static str>,
}
| Field | Default | Purpose |
|---|---|---|
main_widget_id | "chat_view" | Primary content widget |
input_widget_id | "text_input" | Text input widget |
panel_widget_ids | Permission, Question panels | Panels shown between main and input |
popup_widget_ids | Slash popup | Popups shown above input |
overlay_widget_ids | Theme picker, Session picker | Full-screen overlays |
min_main_height | 5 | Minimum rows for main content |
fixed_input_height | None | Fixed input height (auto-sizes if None) |
status_bar_widget_id | "status_bar" | Status bar widget (None to hide) |
Customizing the Standard Layout
Adding custom panels to the standard layout is straightforward. Include your widget’s ID in the appropriate list, and the layout will manage its visibility and positioning alongside the built-in widgets.
use agent_air::tui::layout::{LayoutTemplate, StandardOptions};
use agent_air::tui::widgets::widget_ids;
let layout = LayoutTemplate::Standard(StandardOptions {
panel_widget_ids: vec![
widget_ids::PERMISSION_PANEL,
widget_ids::QUESTION_PANEL,
"my_custom_panel",
],
min_main_height: 10,
..Default::default()
});
Sidebar Layout
The sidebar layout adds a side panel to the standard layout, creating a two-column arrangement with the main content area alongside auxiliary content. This works well for file browsers, context displays, or any information that should remain visible while the user interacts with the agent.
The sidebar maintains its width as the terminal resizes, with the main content area absorbing the change. This keeps the sidebar readable regardless of terminal size, though you can configure percentage-based widths if you prefer proportional scaling.
use agent_air::tui::layout::LayoutTemplate;
let layout = LayoutTemplate::with_sidebar("file_browser", 30);
SidebarOptions
The sidebar options let you configure the sidebar’s content, size, and position. The main options are inherited from StandardOptions, so you get the same panel and overlay support in the main content area.
pub struct SidebarOptions {
pub main_options: StandardOptions,
pub sidebar_widget_id: &'static str,
pub sidebar_width: SidebarWidth,
pub sidebar_position: SidebarPosition,
}
Sidebar Width
Control how much space the sidebar occupies using one of three strategies. Fixed widths work well when the sidebar content has a natural width, while percentage-based widths adapt to different terminal sizes.
pub enum SidebarWidth {
Fixed(u16), // Exact column count
Percentage(u16), // Percentage of total width
Min(u16), // Minimum width, main gets the rest
}
Sidebar Position
Place the sidebar on either side of the main content. Left sidebars follow the convention of file explorers in IDEs, while right sidebars work well for supplementary information that doesn’t need primary attention.
pub enum SidebarPosition {
Left,
Right, // default
}
Custom Sidebar Configuration
For full control over the sidebar, create a SidebarOptions struct directly. This lets you combine a custom sidebar with modified main area settings.
use agent_air::tui::layout::{
LayoutTemplate, SidebarOptions, SidebarWidth, SidebarPosition,
StandardOptions,
};
let layout = LayoutTemplate::Sidebar(SidebarOptions {
main_options: StandardOptions::default(),
sidebar_widget_id: "context_panel",
sidebar_width: SidebarWidth::Percentage(25),
sidebar_position: SidebarPosition::Left,
});
Split Layout
The split layout divides the screen into two main areas, either side by side (horizontal) or stacked (vertical). This works well for comparison views, dual-pane interfaces, or separating distinct content areas that each need significant screen space.
Unlike the sidebar layout which has a clear primary/secondary relationship, the split layout treats both areas as equal peers. Use this when both widgets are equally important to the user’s task.
use agent_air::tui::layout::LayoutTemplate;
// Side by side
let layout = LayoutTemplate::split_horizontal("left_panel", "right_panel");
// Stacked vertically
let layout = LayoutTemplate::split_vertical("top_panel", "bottom_panel");
SplitOptions
The split options control the direction and proportions of the split. The ratio determines how space is divided between the two areas.
pub struct SplitOptions {
pub direction: Direction,
pub first_widget_id: &'static str,
pub second_widget_id: &'static str,
pub split: SplitRatio,
}
Split Ratio
Control how space is divided between the two areas. Equal splits work for symmetric interfaces, while percentage or fixed splits let you prioritize one area over the other.
pub enum SplitRatio {
Equal, // 50/50 split
Percentage(u16), // First area gets this percentage
FirstFixed(u16), // First area has fixed size
SecondFixed(u16), // Second area has fixed size
}
Minimal Layout
The minimal layout removes the status bar for a cleaner, more focused interface. It shows just the chat view and input area without any chrome, maximizing space for content. This works well for simple agents or when embedding the TUI in contexts where additional UI elements would be distracting.
The minimal layout is the simplest of the built-in templates, making it a good starting point for understanding how layouts work before moving to more complex configurations.
use agent_air::tui::layout::LayoutTemplate;
let layout = LayoutTemplate::minimal();
MinimalOptions
Even the minimal layout has some configuration options, primarily for specifying custom widgets or adjusting the minimum main content height.
pub struct MinimalOptions {
pub main_widget_id: &'static str,
pub input_widget_id: &'static str,
pub min_main_height: u16,
}
Setting the Layout
Apply a layout to your agent before starting the TUI using the with_layout method on the TuiRunner. The layout takes effect immediately when the TUI starts and adapts automatically as the terminal size changes.
use agent_air::AgentAir;
use agent_air::tui::{TuiRunner, LayoutTemplate};
AgentAir::with_config(&config)?
.into_tui()
.with_layout(LayoutTemplate::minimal())
.run()?;
Custom Layouts
For layouts beyond what the templates provide, implement the LayoutProvider trait or use a closure-based layout. Custom layouts have full control over widget placement and sizing, enabling interfaces that the built-in templates can’t express.
Custom layouts receive information about the available space and registered widgets, then return a mapping of widget IDs to screen areas. This gives you complete flexibility while still integrating with the widget system’s activation and rendering lifecycle.
LayoutProvider Trait
The LayoutProvider trait is the interface for custom layout implementations. Implement this trait to create reusable layout logic that can be shared across applications or configured at runtime.
pub trait LayoutProvider: Send + Sync + 'static {
fn compute(&self, ctx: &LayoutContext, sizes: &WidgetSizes) -> LayoutResult;
}
The trait receives context about the current frame and widget sizes, then returns a LayoutResult specifying where each widget should render.
LayoutContext
Information available during layout computation includes the frame dimensions, widget state, and theme. This context lets you make layout decisions based on current conditions rather than static configuration.
pub struct LayoutContext<'a> {
pub frame_area: Rect,
pub show_throbber: bool,
pub input_visual_lines: usize,
pub theme: &'a Theme,
pub active_widgets: HashSet<&'static str>,
}
WidgetSizes
Pre-computed size information for registered widgets helps you allocate appropriate space. Widgets report their required height, and this information is collected before layout computation begins.
pub struct WidgetSizes {
pub heights: HashMap<&'static str, u16>,
pub is_active: HashMap<&'static str, bool>,
}
LayoutResult
The output of layout computation specifies where each widget should render and in what order. The render order controls z-ordering for overlapping widgets.
pub struct LayoutResult {
pub widget_areas: HashMap<&'static str, Rect>,
pub render_order: Vec<&'static str>,
pub input_area: Option<Rect>,
}
Implementing a Custom Layout
Create a struct implementing LayoutProvider for reusable custom layouts. This approach is ideal when you need the same layout across multiple applications or when the layout logic is complex enough to benefit from encapsulation.
The example below creates a three-column layout with fixed-width side panels and a flexible center area. This pattern works well for IDE-like interfaces with a file tree, main content, and outline view.
use agent_air::tui::layout::{
LayoutContext, LayoutProvider, LayoutResult, WidgetSizes,
helpers::{vstack, hstack},
};
use agent_air::tui::widgets::widget_ids;
struct ThreeColumnLayout {
left_width: u16,
right_width: u16,
}
impl LayoutProvider for ThreeColumnLayout {
fn compute(&self, ctx: &LayoutContext, sizes: &WidgetSizes) -> LayoutResult {
let mut result = LayoutResult::default();
let area = ctx.frame_area;
let columns = hstack(area, &[
Constraint::Length(self.left_width),
Constraint::Min(20),
Constraint::Length(self.right_width),
]);
result.widget_areas.insert("left_panel", columns[0]);
result.widget_areas.insert(widget_ids::CHAT_VIEW, columns[1]);
result.widget_areas.insert("right_panel", columns[2]);
result.render_order = vec!["left_panel", widget_ids::CHAT_VIEW, "right_panel"];
result
}
}
// Use the custom layout
let layout = LayoutTemplate::custom(ThreeColumnLayout {
left_width: 30,
right_width: 25,
});
Closure-Based Layouts
For simpler one-off layouts, use custom_fn with a closure. This approach requires less boilerplate than implementing a trait and works well for layouts that don’t need to be reused or configured dynamically.
Closure layouts are evaluated on each frame, so they can adapt to changing conditions. Keep the closure logic efficient since it runs frequently.
use agent_air::tui::layout::{LayoutTemplate, LayoutResult, helpers};
let layout = LayoutTemplate::custom_fn(|area, ctx, sizes| {
let mut result = LayoutResult::default();
let chunks = helpers::vstack(area, &[
Constraint::Percentage(80),
Constraint::Percentage(20),
]);
result.widget_areas.insert("main", chunks[0]);
result.widget_areas.insert("footer", chunks[1]);
result.render_order = vec!["main", "footer"];
result
});
Layout Helper Functions
The helpers module provides utilities for common layout patterns. These functions wrap the underlying constraint system with a more ergonomic API, making it easier to express common arrangements.
vstack
Creates a vertical stack by dividing an area into horizontal slices. Use this for column layouts where widgets stack from top to bottom.
let chunks = helpers::vstack(area, &[
Constraint::Length(3), // Fixed 3 rows
Constraint::Min(10), // Fills remaining space
Constraint::Length(2), // Fixed 2 rows
]);
hstack
Creates a horizontal stack by dividing an area into vertical slices. Use this for row layouts where widgets sit side by side.
let chunks = helpers::hstack(area, &[
Constraint::Percentage(30),
Constraint::Percentage(70),
]);
centered
Creates a centered area within a parent, useful for dialogs and popups that should float in the middle of the screen.
let popup_area = helpers::centered(frame_area, 60, 20);
// Returns a 60x20 rect centered in frame_area
with_margin
Adds margin around an area, creating padding between the content and its container. This helps prevent content from touching the edges of its allocated space.
let inner = helpers::with_margin(area, 2);
// Returns area reduced by 2 on all sides
Constraint Types
Layouts use constraints to specify how space is divided. Constraints express intent—fixed size, minimum size, proportional size—and the layout system resolves them into actual pixel dimensions.
Understanding constraints is key to creating responsive layouts. Mixing fixed and flexible constraints lets you create interfaces that adapt gracefully to different terminal sizes.
| Constraint | Description |
|---|---|
Length(n) | Exactly n rows or columns |
Min(n) | At least n, fills remaining space |
Max(n) | At most n rows or columns |
Percentage(p) | p% of available space |
Ratio(a, b) | a/b of available space |
Render Order
The render_order vector controls the z-order of widgets. Widgets listed first render first and appear behind widgets listed later. This matters when widgets overlap, such as popups appearing over content.
Getting render order right is especially important for overlay widgets like dialogs and pickers, which need to appear on top of all other content.
result.render_order = vec![
"background", // Bottom layer
"main_content", // Middle layer
"popup", // Top layer
];
Overlay widgets should always be added last to appear on top of everything else.
Handling Active Widgets
Use sizes.is_active() to conditionally include widgets in the layout. Inactive widgets should typically not occupy screen space, so layouts often check activation state before allocating areas.
This dynamic behavior lets panels appear and disappear without requiring layout changes. The permission panel, for example, is only active when a permission request is pending, and the layout automatically adjusts when it activates or deactivates.
fn compute(&self, ctx: &LayoutContext, sizes: &WidgetSizes) -> LayoutResult {
let mut constraints = vec![Constraint::Min(10)];
if sizes.is_active("my_panel") {
let panel_height = sizes.height("my_panel");
constraints.push(Constraint::Length(panel_height));
}
constraints.push(Constraint::Length(3));
let chunks = helpers::vstack(ctx.frame_area, &constraints);
// Map chunks to widgets...
}
This ensures inactive widgets don’t take up screen space.
