//! 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 = 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); }