//! TUI rendering.
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
Frame,
};
use super::app::{App, InputMode, ViewType};
use super::event::get_help_text;
/// Main render function
pub fn render(frame: &mut Frame, app: &App) {
// Create main layout
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Min(10), // Main content
Constraint::Length(3), // Status/Help
])
.split(frame.area());
render_header(frame, app, chunks[0]);
render_main_content(frame, app, chunks[1]);
render_footer(frame, app, chunks[2]);
// Render confirmation dialog if in confirm mode
if app.input_mode == InputMode::Confirm {
render_confirm_dialog(frame, app);
}
}
/// Render header with title and search bar
fn render_header(frame: &mut Frame, app: &App, area: Rect) {
let title = match app.view_type {
ViewType::Tasks => "Tasks",
ViewType::Contracts => "Contracts",
ViewType::Files => "Files",
};
let header_text = if app.input_mode == InputMode::Search || !app.search_query.is_empty() {
format!("{} [Search: {}]", title, app.search_query)
} else {
format!("{} ({} items)", title, app.filtered_items.len())
};
let header = Paragraph::new(header_text)
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
.block(Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(if app.input_mode == InputMode::Search {
Color::Yellow
} else {
Color::White
})));
frame.render_widget(header, area);
}
/// Render main content (list + optional preview)
fn render_main_content(frame: &mut Frame, app: &App, area: Rect) {
if app.preview_visible && !app.preview_content.is_empty() {
// Split horizontally: list on left, preview on right
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(area);
render_list(frame, app, chunks[0]);
render_preview(frame, app, chunks[1]);
} else {
render_list(frame, app, area);
}
}
/// Render the item list
fn render_list(frame: &mut Frame, app: &App, area: Rect) {
let items: Vec<ListItem> = app.filtered_items
.iter()
.enumerate()
.map(|(i, item)| {
let is_selected = i == app.selected_index;
// Build the display line
let status_str = item.status
.as_ref()
.map(|s| format!(" [{}]", s))
.unwrap_or_default();
let content = format!("{}{}", item.name, status_str);
let style = if is_selected {
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
let status_color = item.status.as_ref().map(|s| {
match s.to_lowercase().as_str() {
"running" | "active" => Color::Green,
"pending" | "waiting" => Color::Yellow,
"completed" | "done" => Color::Blue,
"failed" | "error" => Color::Red,
_ => Color::White,
}
}).unwrap_or(Color::White);
Style::default().fg(status_color)
};
ListItem::new(Line::from(vec![
Span::styled(content, style),
]))
})
.collect();
let list = List::new(items)
.block(Block::default()
.borders(Borders::ALL)
.title(format!(" {} ", app.view_type.as_str())));
frame.render_widget(list, area);
}
/// Render the preview panel
fn render_preview(frame: &mut Frame, app: &App, area: Rect) {
let preview = Paragraph::new(Text::raw(&app.preview_content))
.wrap(Wrap { trim: false })
.block(Block::default()
.borders(Borders::ALL)
.title(" Preview "));
frame.render_widget(preview, area);
}
/// Render footer with help text and status
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
let help_text = get_help_text(app.input_mode);
let status_text = app.status_message
.as_ref()
.map(|s| format!(" | {}", s))
.unwrap_or_default();
let footer_text = format!("{}{}", help_text, status_text);
let footer = Paragraph::new(footer_text)
.style(Style::default().fg(Color::DarkGray))
.block(Block::default().borders(Borders::ALL));
frame.render_widget(footer, area);
}
/// Render confirmation dialog as a centered popup
fn render_confirm_dialog(frame: &mut Frame, app: &App) {
let item_name = app.get_pending_delete_name()
.unwrap_or_else(|| "this item".to_string());
// Calculate popup size and position
let area = frame.area();
let popup_width = 50.min(area.width.saturating_sub(4));
let popup_height = 7;
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
let popup_area = Rect {
x: popup_x,
y: popup_y,
width: popup_width,
height: popup_height,
};
// Clear the area behind the popup
frame.render_widget(Clear, popup_area);
// Build popup content
let text = vec![
Line::from(""),
Line::from(Span::styled(
"Delete Confirmation",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(format!("Delete '{}'?", item_name)),
Line::from(""),
Line::from(vec![
Span::styled("y", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::raw(": confirm "),
Span::styled("n", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw(": cancel"),
]),
];
let popup = Paragraph::new(text)
.alignment(Alignment::Center)
.block(Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Red))
.title(" Confirm "));
frame.render_widget(popup, popup_area);
}