summaryrefslogblamecommitdiff
path: root/makima/src/daemon/tui/ui.rs
blob: 40033440e0bacd8a17958035f2c2a81ee206e5cd (plain) (tree)
















































































































































































































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