tomodoro 0.1.1

Terminal Pomodoro timer with animated backgrounds
use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph},
    Frame,
};

use crate::animation::{Animation, RenderMode};
use crate::timer::{Phase, Timer};

pub fn draw(f: &mut Frame, timer: &Timer, anim: &Animation, show_help: bool) {
    let area = f.area();
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),
            Constraint::Min(0),
            Constraint::Length(1),
        ])
        .split(area);

    draw_header(f, timer, rows[0]);
    draw_animation(f, timer, anim, rows[1]);
    draw_progress(f, timer, anim, rows[2]);

    if show_help {
        draw_help(f, area);
    }
}

fn draw_header(f: &mut Frame, timer: &Timer, area: Rect) {
    let color = phase_color(&timer.phase);
    let phase_str = match timer.phase {
        Phase::Work => "F",
        Phase::ShortBreak => "B",
        Phase::LongBreak => "LB",
    };
    let filled = (timer.sessions_completed % 4) as usize;
    let dots: String = "".repeat(filled) + &"".repeat(4 - filled);

    let cols = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(5)])
        .split(area);

    f.render_widget(
        Paragraph::new(Span::styled(phase_str, Style::default().fg(color).add_modifier(Modifier::BOLD))),
        cols[0],
    );
    f.render_widget(
        Paragraph::new(Span::styled(timer.format_remaining(), Style::default().fg(color).add_modifier(Modifier::BOLD)))
            .alignment(Alignment::Center),
        cols[1],
    );
    f.render_widget(
        Paragraph::new(Span::styled(dots, Style::default().fg(color)))
            .alignment(Alignment::Right),
        cols[2],
    );
}

fn draw_animation(f: &mut Frame, timer: &Timer, anim: &Animation, area: Rect) {
    let lines = anim.render_lines(&timer.phase, area.width as usize, area.height as usize);
    f.render_widget(Paragraph::new(lines).alignment(Alignment::Center), area);
}

fn draw_progress(f: &mut Frame, timer: &Timer, anim: &Animation, area: Rect) {
    let hint = " ? for help";
    let hint_width = hint.len() as u16 + 1;
    let cols = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Min(0), Constraint::Length(hint_width)])
        .split(area);

    f.render_widget(
        Paragraph::new(Span::styled(hint, Style::default().fg(Color::Rgb(60, 60, 60)))),
        cols[1],
    );

    let area = cols[0];
    let width = area.width as usize;
    let progress = timer.progress();
    let filled_color = anim.theme_color();
    let empty_color = Color::Rgb(35, 35, 35);

    let line = if anim.render_mode == RenderMode::Braille {
        // Braille bar: 2 pixels per char, dots on the top row (bits 0x01 left, 0x08 right)
        let total_px = width * 2;
        let filled_px = (progress * total_px as f64) as usize;
        let mut spans: Vec<Span<'static>> = Vec::new();
        let mut run = String::new();
        let mut in_filled = true;

        for px in (0..total_px).step_by(2) {
            let l = px < filled_px;
            let r = (px + 1) < filled_px;
            // row-0 braille: dot1=0x01 (left), dot4=0x08 (right)
            let mask = (l as u8) | ((r as u8) << 3);
            let ch = char::from_u32(0x2800 | mask as u32).unwrap_or(' ');
            let this_filled = l || r;
            if this_filled == in_filled {
                run.push(ch);
            } else {
                let color = if in_filled { filled_color } else { empty_color };
                spans.push(Span::styled(run.clone(), Style::default().fg(color)));
                run.clear();
                in_filled = this_filled;
                run.push(ch);
            }
        }
        if !run.is_empty() {
            let color = if in_filled { filled_color } else { empty_color };
            spans.push(Span::styled(run, Style::default().fg(color)));
        }
        Line::from(spans)
    } else {
        // Centered bar: ━ (heavy horizontal) filled, ─ (light horizontal) empty
        let filled = (progress * width as f64) as usize;
        let mut spans: Vec<Span<'static>> = Vec::new();
        if filled > 0 {
            spans.push(Span::styled("".repeat(filled), Style::default().fg(filled_color)));
        }
        if filled < width {
            spans.push(Span::styled("".repeat(width - filled), Style::default().fg(empty_color)));
        }
        Line::from(spans)
    };

    f.render_widget(Paragraph::new(line), area);
}

fn draw_help(f: &mut Frame, area: Rect) {
    let rows: &[(&str, &str)] = &[
        ("space",  "pause / resume"),
        ("n",      "next phase"),
        ("r",      "restart phase"),
        ("← →",   "cycle theme"),
        ("↑ ↓",   "cycle render mode"),
        ("q",      "quit"),
        ("?",      "close help"),
    ];

    let w = 32u16;
    let h = rows.len() as u16 + 2;
    let x = area.x + area.width.saturating_sub(w) / 2;
    let y = area.y + area.height.saturating_sub(h) / 2;
    let popup = Rect { x, y, width: w.min(area.width), height: h.min(area.height) };

    let lines: Vec<Line> = rows.iter().map(|(key, desc)| {
        Line::from(vec![
            Span::styled(format!("  {:<6}", key), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
            Span::styled(format!("  {}", desc), Style::default().fg(Color::White)),
        ])
    }).collect();

    f.render_widget(Clear, popup);
    f.render_widget(
        Paragraph::new(lines)
            .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray))),
        popup,
    );
}

fn phase_color(phase: &Phase) -> Color {
    match phase {
        Phase::Work => Color::Red,
        Phase::ShortBreak => Color::Green,
        Phase::LongBreak => Color::Cyan,
    }
}