ratado 0.2.0

A fast, keyboard-driven terminal task manager built with Rust and Ratatui
Documentation
//! User interface module.
//!
//! This module contains all Ratatui widgets and views for the terminal UI.
//! It handles rendering the application state to the terminal.
//!
//! ## Submodules
//!
//! - [`layout`] - Screen layout and panel management
//! - [`header`] - Application header with title and badges
//! - [`sidebar`] - Projects and tags sidebar
//! - [`task_list`] - Main task list display
//! - [`status_bar`] - Bottom status bar with keybindings
//! - [`help`] - Help screen with keybinding reference
//! - [`debug`] - Debug log viewer
//! - [`dialogs`] - Modal dialogs for task operations
//! - [`input`] - Text input widget
//! - [`tag_input`] - Tag input widget with autocomplete
//! - [`date_picker`] - Calendar date picker widget
//! - [`description_textarea`] - Multi-line textarea with link support
//! - [`search`] - Search view for finding tasks
//! - [`task_detail`] - Task detail view
//! - [`calendar`] - Weekly calendar view
//! - [`theme`] - Color palette and styling system

pub mod calendar;
pub mod date_picker;
mod debug;
pub mod description_textarea;
pub mod dialogs;
pub mod effects;
mod header;
mod help;
pub mod input;
mod layout;
pub mod search;
mod sidebar;
pub mod splash;
mod status_bar;
pub mod tag_input;
mod task_detail;
pub mod task_list;
pub mod theme;

use ratatui::{
    layout::{Alignment, Rect},
    style::Style,
    text::{Line, Span},
    widgets::{Block, Paragraph, Widget},
    Frame,
};

use crate::app::{App, View};

/// Minimum terminal width required for the application to render properly.
pub const MIN_WIDTH: u16 = 100;

/// Minimum terminal height required for the application to render properly.
pub const MIN_HEIGHT: u16 = 20;

/// Main draw function - entry point for all UI rendering.
///
/// Routes to the appropriate view renderer based on the current view.
/// If a dialog is active, it renders on top of the main view.
pub fn draw(frame: &mut Frame, app: &App) {
    let area = frame.area();

    // Apply consistent dark background to entire terminal area
    // This ensures the app looks the same regardless of terminal theme
    let background = Block::default().style(Style::default().bg(theme::BG_DARK));
    background.render(area, frame.buffer_mut());

    // Check minimum terminal size
    if area.width < MIN_WIDTH || area.height < MIN_HEIGHT {
        render_size_warning(frame, area);
        return;
    }

    // First render the base view
    match app.current_view {
        View::Splash => splash::render_splash(frame, area),
        View::Main => layout::render_main_view(frame, app, frame.area()),
        View::Help => help::render_help(frame, app, frame.area()),
        View::DebugLogs => debug::render_debug_logs(frame, app, frame.area()),
        View::Search => search::render_search_with_context(
            frame,
            &app.input_buffer,
            app.input_cursor,
            &app.search_results,
            app.selected_search_index,
            frame.area(),
            Some(app.selected_project_name()),
        ),
        View::TaskDetail => task_detail::render_task_detail(frame, app, frame.area()),
        View::Calendar => calendar::render_calendar(frame, app, frame.area()),
    }

    // Render any active dialog on top
    if let Some(ref dialog) = app.dialog {
        dialog.render(frame);
    }
}

/// Renders a warning message when the terminal is too small.
fn render_size_warning(frame: &mut Frame, area: Rect) {
    use theme::{icons, WARNING, SUCCESS, ERROR, TEXT_MUTED, PRIMARY_LIGHT};

    let width_ok = area.width >= MIN_WIDTH;
    let height_ok = area.height >= MIN_HEIGHT;

    let mut lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(format!("{} ", icons::WARNING_ICON), Style::default().fg(WARNING)),
            Span::styled("Terminal Too Small", Style::default().fg(WARNING)),
        ]),
        Line::from(""),
        Line::from(vec![
            Span::raw("  Width:  "),
            Span::styled(
                format!("{:>3}", area.width),
                Style::default().fg(if width_ok { SUCCESS } else { ERROR }),
            ),
            Span::styled(format!(" / {} ", MIN_WIDTH), Style::default().fg(TEXT_MUTED)),
            Span::styled(
                if width_ok { icons::CHECK } else { icons::CROSS },
                Style::default().fg(if width_ok { SUCCESS } else { ERROR }),
            ),
        ]),
        Line::from(vec![
            Span::raw("  Height: "),
            Span::styled(
                format!("{:>3}", area.height),
                Style::default().fg(if height_ok { SUCCESS } else { ERROR }),
            ),
            Span::styled(format!(" / {} ", MIN_HEIGHT), Style::default().fg(TEXT_MUTED)),
            Span::styled(
                if height_ok { icons::CHECK } else { icons::CROSS },
                Style::default().fg(if height_ok { SUCCESS } else { ERROR }),
            ),
        ]),
        Line::from(""),
        Line::from(Span::styled(
            "Please resize your terminal window.",
            Style::default().fg(TEXT_MUTED),
        )),
    ];

    // Add hint about current size
    if area.width >= 40 && area.height >= 8 {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            "Ratado",
            Style::default().fg(PRIMARY_LIGHT),
        )));
    }

    // Calculate line count before consuming lines
    let line_count = lines.len() as u16;

    // Center vertically
    let y_offset = area.height.saturating_sub(line_count) / 2;
    let centered_area = Rect {
        x: area.x,
        y: area.y + y_offset,
        width: area.width,
        height: area.height.saturating_sub(y_offset),
    };

    let paragraph = Paragraph::new(lines).alignment(Alignment::Center);
    frame.render_widget(paragraph, centered_area);
}