void-focus 0.3.0-alpha.3

A feature-rich terminal focus timer with task tracking
Documentation
use super::*;

pub(crate) fn draw_popup(f: &mut Frame, app: &App, popup: &crate::app::Popup) {
    let theme = &app.theme;
    let icons = app.icons;
    let area = f.area();
    let popup_area = centered_rect(68, 78, area);
    f.render_widget(Clear, popup_area);
    let block = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(theme.accent))
        .title(Span::styled(
            match popup {
                crate::app::Popup::AddTask => format!(" {} Add Task ", icons.plus),
                crate::app::Popup::EditTask(_) => format!(" {} Edit Task ", icons.edit),
                crate::app::Popup::ConfirmDelete(_) => format!(" {} Confirm Delete ", icons.delete),
                crate::app::Popup::EmptyQueueChoice => format!(" {} All Tasks Done ", icons.check),
            },
            Style::default()
                .fg(theme.accent)
                .add_modifier(Modifier::BOLD),
        ));
    f.render_widget(block, popup_area);

    let inner = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints([Constraint::Min(8), Constraint::Length(2)])
        .split(popup_area);

    match popup {
        crate::app::Popup::AddTask | crate::app::Popup::EditTask(_) => {
            let (left_area, right_area) = if matches!(app.input_field, InputField::DueDate) {
                let chunks = Layout::default()
                    .direction(Direction::Horizontal)
                    .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
                    .split(inner[0]);
                (chunks[0], Some(chunks[1]))
            } else {
                (inner[0], None)
            };

            let cursor = |active: bool, text: &str| -> String {
                if active {
                    if text.is_empty() {
                        "|".to_string()
                    } else {
                        format!("{}|", text)
                    }
                } else if text.is_empty() {
                    "".to_string()
                } else {
                    text.to_string()
                }
            };
            let due_display = if matches!(app.input_field, InputField::DueDate) {
                cursor(true, &app.input_due_date)
            } else if app.input_due_date.is_empty() {
                "".to_string()
            } else {
                app.input_due_date.clone()
            };
            let tags_display = cursor(matches!(app.input_field, InputField::Tags), &app.input_tags);
            let p = Paragraph::new(vec![
                popup_field_line(
                    theme,
                    "Title",
                    cursor(
                        matches!(app.input_field, InputField::Title),
                        &app.input_buffer,
                    ),
                    matches!(app.input_field, InputField::Title),
                ),
                popup_field_line(
                    theme,
                    "Notes",
                    cursor(
                        matches!(app.input_field, InputField::Notes),
                        &app.input_buffer2,
                    ),
                    matches!(app.input_field, InputField::Notes),
                ),
                popup_field_line(
                    theme,
                    "Estimate (min)",
                    if matches!(app.input_field, InputField::Estimate) {
                        format!("{}|", app.input_number)
                    } else {
                        app.input_number.to_string()
                    },
                    matches!(app.input_field, InputField::Estimate),
                ),
                popup_field_line(
                    theme,
                    "Priority",
                    app.input_priority.label().to_string(),
                    matches!(app.input_field, InputField::Priority),
                ),
                popup_field_line(
                    theme,
                    "Due (YYYY-MM-DD)",
                    due_display,
                    matches!(app.input_field, InputField::DueDate),
                ),
                popup_field_line(
                    theme,
                    "Tags (comma-sep)",
                    tags_display,
                    matches!(app.input_field, InputField::Tags),
                ),
            ]);
            f.render_widget(p, left_area);

            if let Some(r) = right_area {
                let d = app.calendar_date;
                if let Ok(time_date) = time::Date::from_calendar_date(
                    d.year(),
                    time::Month::try_from(d.month() as u8).unwrap_or(time::Month::January),
                    d.day() as u8,
                ) {
                    let mut store = ratatui::widgets::calendar::CalendarEventStore::default();
                    store.add(
                        time_date,
                        Style::default()
                            .bg(theme.accent)
                            .fg(theme.on_accent)
                            .add_modifier(Modifier::BOLD),
                    );

                    let monthly = ratatui::widgets::calendar::Monthly::new(time_date, store)
                        .show_month_header(
                            Style::default()
                                .fg(theme.accent)
                                .add_modifier(Modifier::BOLD),
                        )
                        .show_weekdays_header(Style::default().fg(theme.dim));
                    f.render_widget(monthly, r);
                }
            }
        }
        crate::app::Popup::ConfirmDelete(id) => {
            let title = app
                .data
                .tasks
                .iter()
                .find(|t| t.id == *id)
                .map(|t| t.title.as_str())
                .unwrap_or("Unknown task");
            let p = Paragraph::new(vec![
                Line::from(Span::styled(
                    format!("Delete \"{}\"?", title),
                    Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
                )),
                Line::from(""),
                Line::from(Span::styled(
                    "Press y or Enter to confirm, n or Esc to cancel.",
                    Style::default().fg(theme.dim),
                )),
                Line::from(Span::styled(
                    "This cannot be undone.",
                    Style::default().fg(theme.error),
                )),
            ]);
            f.render_widget(p, inner[0]);
        }
        crate::app::Popup::EmptyQueueChoice => {
            let p = Paragraph::new(vec![
                Line::from(Span::styled(
                    "You've completed every task in your queue.",
                    Style::default().fg(theme.text),
                )),
                Line::from(""),
                Line::from(Span::styled(
                    "[Enter]  Continue free focus (log general sessions)",
                    Style::default().fg(theme.success),
                )),
                Line::from(Span::styled(
                    "[p]      Pause the timer",
                    Style::default().fg(theme.warning),
                )),
                Line::from(Span::styled(
                    "[a]      Add another task",
                    Style::default().fg(theme.accent),
                )),
                Line::from(Span::styled(
                    "[Esc]    Dismiss",
                    Style::default().fg(theme.dim),
                )),
            ]);
            f.render_widget(p, inner[0]);
        }
    }
    if matches!(
        popup,
        crate::app::Popup::AddTask | crate::app::Popup::EditTask(_)
    ) {
        let hint = if matches!(app.input_field, InputField::DueDate) {
            "←→: day · ↑↓: week · Tab: field · Enter: save · Esc: cancel"
        } else {
            "Tab/Shift+Tab: field · Enter: save · Esc: cancel"
        };
        f.render_widget(
            Paragraph::new(Span::styled(hint, Style::default().fg(theme.dim))),
            inner[1],
        );
    }
}

pub(crate) fn popup_field_line(
    theme: &crate::app::Theme,
    label: &str,
    value: String,
    active: bool,
) -> Line<'static> {
    let label_style = if active {
        Style::default()
            .fg(theme.accent)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(theme.dim)
    };
    let value_style = if active {
        Style::default().fg(theme.text).add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(theme.text)
    };
    Line::from(vec![
        Span::styled(format!("{:<20} ", label), label_style),
        Span::styled(value, value_style),
    ])
}

pub(crate) fn draw_input(f: &mut Frame, app: &App, area: Rect) {
    let theme = &app.theme;
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints([Constraint::Length(3), Constraint::Min(1)])
        .split(area);
    let block = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(theme.accent))
        .title(Span::styled(" Input ", Style::default().fg(theme.accent)));
    let p = Paragraph::new(format!("{}|", app.input_buffer))
        .style(Style::default().fg(theme.text))
        .block(block);
    f.render_widget(p, chunks[0]);
}