void-focus 0.3.0-alpha.3

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

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SettingsRow {
    Header(&'static str, &'static str),
    Item(usize),
}

pub(crate) fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints([Constraint::Min(8), Constraint::Length(3)])
        .split(area);

    let visible_height = chunks[0].height.saturating_sub(3) as usize;
    let selected = app.settings_state.selected;
    app.settings_state.page_size = visible_height.max(6);
    app.sync_settings_scroll();
    let scroll_offset = app.settings_state.scroll_offset;

    let theme = &app.theme;
    let icons = app.icons;
    let settings_labels: Vec<(&str, String, &str)> = vec![
        (
            "Focus minutes",
            format!("{} min", app.data.focus_minutes),
            "per focus session",
        ),
        (
            "Short break",
            format!("{} min", app.data.short_break_minutes),
            "between sessions",
        ),
        (
            "Long break",
            format!("{} min", app.data.long_break_minutes),
            "after cycle",
        ),
        (
            "Long break every",
            format!("{} sessions", app.data.long_break_every),
            "focus sessions per cycle",
        ),
        (
            "Daily goal",
            format!("{} min", app.data.daily_goal_minutes),
            "+/-15 per step",
        ),
        (
            "Sound on finish",
            if app.data.sound_enabled {
                "on".into()
            } else {
                "off".into()
            },
            "plays on completion",
        ),
        (
            "Notifications",
            if app.data.notify_on_finish {
                "on".into()
            } else {
                "off".into()
            },
            "desktop alerts",
        ),
        (
            "Auto-start breaks",
            if app.data.auto_start_breaks {
                "on".into()
            } else {
                "off".into()
            },
            "begin break automatically",
        ),
        (
            "Auto-start focus",
            if app.data.auto_start_focus {
                "on".into()
            } else {
                "off".into()
            },
            "begin focus after break",
        ),
        (
            "Active task",
            app.active_task
                .and_then(|id| app.data.tasks.iter().find(|t| t.id == id))
                .map(|t| t.title.clone())
                .unwrap_or_else(|| "(none)".into()),
            "cycle with Enter",
        ),
        ("Theme", app.data.theme.label().to_string(), "cycle themes"),
        (
            "Custom timer",
            format!("{} min", app.timer.custom_minutes),
            "freeform session",
        ),
        (
            "Auto-pick task",
            if app.data.auto_pick_task {
                "on".into()
            } else {
                "off".into()
            },
            "pick best task on start",
        ),
        (
            "Auto-advance task",
            if app.data.auto_advance_task {
                "on".into()
            } else {
                "off".into()
            },
            "next task after focus",
        ),
        (
            "When queue empty",
            app.data.empty_queue_behavior.label().to_string(),
            "free focus / pause / ask",
        ),
        (
            "Log breaks",
            if app.data.log_breaks {
                "on".into()
            } else {
                "off".into()
            },
            "record break sessions",
        ),
        (
            "Estimate reached",
            app.data.estimate_complete.label().to_string(),
            "nudge / off / auto-done",
        ),
        (
            "Export backup",
            "Enter to export".into(),
            "writes data.json for backup",
        ),
    ];

    let section_headers: &[(usize, &str, &str)] = &[
        (0, icons.timer, "Timer"),
        (5, icons.cycle, "Behavior"),
        (9, icons.tasks, "Tasks"),
        (10, icons.star, "Appearance"),
        (14, icons.play, "Sessions"),
        (17, icons.export, "Data"),
    ];

    let mut layout: Vec<SettingsRow> = Vec::new();
    for (i, _) in settings_labels.iter().enumerate() {
        for &(at, icon, name) in section_headers {
            if at == i {
                layout.push(SettingsRow::Header(icon, name));
            }
        }
        layout.push(SettingsRow::Item(i));
    }

    let visible_rows: Vec<&SettingsRow> = layout
        .iter()
        .skip(scroll_offset)
        .take(visible_height)
        .collect();

    let total_rows = layout.len();
    let can_scroll = total_rows > visible_height;

    let mut rows: Vec<Row> = Vec::new();
    for entry in &visible_rows {
        match entry {
            SettingsRow::Header(icon, name) => {
                rows.push(Row::new(vec![
                    Cell::from(""),
                    Cell::from(Span::styled(
                        format!("{icon} {name}"),
                        Style::default()
                            .fg(theme.accent)
                            .add_modifier(Modifier::BOLD),
                    )),
                    Cell::from(""),
                ]));
            }
            SettingsRow::Item(i) => {
                let (k, v, desc) = &settings_labels[*i];
                let is_selected = *i == selected;
                let marker = if is_selected { icons.chevron } else { " " };
                let row_style = if is_selected {
                    Style::default().bg(theme.select_bg).fg(theme.select_fg)
                } else {
                    Style::default().fg(theme.text)
                };
                let key_style = if is_selected {
                    Style::default()
                        .fg(theme.accent)
                        .bg(theme.select_bg)
                        .add_modifier(Modifier::BOLD)
                } else {
                    Style::default().fg(theme.text)
                };
                let val_style = if is_selected {
                    Style::default().fg(theme.success).bg(theme.select_bg)
                } else {
                    Style::default().fg(theme.dim)
                };
                let value_with_desc = if desc.is_empty() {
                    v.clone()
                } else {
                    format!("{} ({})", v, desc)
                };
                rows.push(
                    Row::new(vec![
                        Cell::from(marker.to_string()).style(key_style),
                        Cell::from(k.to_string()).style(key_style),
                        Cell::from(value_with_desc).style(val_style),
                    ])
                    .style(row_style),
                );
            }
        }
    }

    let table = Table::new(
        rows,
        [
            Constraint::Length(3),
            Constraint::Length(22),
            Constraint::Min(10),
        ],
    )
    .block(themed_panel(
        theme,
        Line::from(Span::styled(
            format!(" {} Settings ", icons.settings),
            Style::default().fg(theme.accent),
        )),
    ));
    f.render_widget(table, chunks[0]);

    let scroll_hint = if can_scroll {
        format!(
            "  {} {}/{}",
            icons.dot,
            scroll_offset + visible_rows.len(),
            total_rows
        )
    } else {
        String::new()
    };
    let hint = Paragraph::new(Line::from(vec![
        Span::styled(
            format!("{} Up/Down", icons.chevron),
            Style::default().fg(theme.accent),
        ),
        Span::styled(" scroll  ", Style::default().fg(theme.dim)),
        Span::styled("j/k", Style::default().fg(theme.accent)),
        Span::styled(" nav  ", Style::default().fg(theme.dim)),
        Span::styled("Enter", Style::default().fg(theme.accent)),
        Span::styled(" toggle  ", Style::default().fg(theme.dim)),
        Span::styled("+/-", Style::default().fg(theme.accent)),
        Span::styled(" adjust", Style::default().fg(theme.dim)),
        Span::styled(scroll_hint, Style::default().fg(theme.dim)),
    ]))
    .alignment(Alignment::Center);
    f.render_widget(hint, chunks[1]);
}