trackWork 0.15.0

A terminal-based time tracking application for managing work sessions
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph},
    Frame,
};

use crate::app::{App, InputMode};
use crate::cursor::render_with_cursor;

/// Draw the passphrase unlock prompt (startup).
pub fn draw_passphrase_prompt(f: &mut Frame, app: &App) {
    if let InputMode::PassphrasePrompt {
        passphrase,
        cursor_pos,
        error_message,
        ..
    } = &app.input_mode
    {
        let area = centered_rect(55, 11, f.area());
        f.render_widget(Clear, area);

        let mut lines = vec![
            Line::from(""),
            Line::from(Span::styled(
                "  Your secrets file is encrypted.",
                Style::default().fg(Color::Gray),
            )),
            Line::from(Span::styled(
                "  Enter your passphrase to decrypt and continue.",
                Style::default().fg(Color::Gray),
            )),
            Line::from(""),
        ];

        // Masked passphrase field
        let masked = "*".repeat(passphrase.chars().count());
        let rendered = render_with_cursor(&masked, *cursor_pos, true);
        lines.push(Line::from(vec![
            Span::raw("  "),
            Span::styled(
                rendered,
                Style::default().bg(Color::DarkGray).fg(Color::White),
            ),
        ]));
        lines.push(Line::from(""));

        if let Some(err) = error_message {
            lines.push(Line::from(Span::styled(
                format!("  {}", err),
                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
            )));
        } else {
            lines.push(Line::from(""));
        }

        lines.push(Line::from(""));
        lines.push(Line::from(vec![
            Span::styled("  Enter", Style::default().fg(Color::Green)),
            Span::raw(": Unlock  "),
            Span::styled("Del", Style::default().fg(Color::Yellow)),
            Span::raw(": Remove secrets  "),
            Span::styled("Esc", Style::default().fg(Color::Red)),
            Span::raw(": Quit"),
        ]));

        let block = Block::default()
            .borders(Borders::ALL)
            .title("Unlock Secrets")
            .border_style(Style::default().fg(Color::Cyan));

        let paragraph = Paragraph::new(lines).block(block);
        f.render_widget(paragraph, area);
    }
}

/// Draw the passphrase change/setup screen.
pub fn draw_passphrase_change(f: &mut Frame, app: &App) {
    if let InputMode::PassphraseChange {
        old_passphrase,
        new_passphrase,
        confirm_passphrase,
        current_field,
        cursor_pos,
        error_message,
        is_initial_setup,
    } = &app.input_mode
    {
        let height = if *is_initial_setup { 14 } else { 14 };
        let area = centered_rect(55, height, f.area());
        f.render_widget(Clear, area);

        let title = if *is_initial_setup {
            "Set Passphrase"
        } else {
            "Change Passphrase"
        };

        let mut lines = vec![Line::from("")];

        if *is_initial_setup {
            lines.push(Line::from(Span::styled(
                "  Your API token will be encrypted at rest.",
                Style::default().fg(Color::Gray),
            )));
            lines.push(Line::from(Span::styled(
                "  Choose a passphrase to protect your secrets.",
                Style::default().fg(Color::Gray),
            )));
            lines.push(Line::from(""));
        }

        let mut field_idx = 0;

        if !*is_initial_setup {
            // Old passphrase field
            let is_active = *current_field == field_idx;
            lines.push(Line::from(Span::styled(
                "  Current Passphrase:",
                field_label_style(is_active),
            )));
            let masked = "*".repeat(old_passphrase.chars().count());
            let rendered = render_with_cursor(&masked, *cursor_pos, is_active);
            let style = if is_active {
                Style::default().bg(Color::DarkGray).fg(Color::White)
            } else {
                Style::default()
            };
            lines.push(Line::from(vec![
                Span::raw("  "),
                Span::styled(rendered, style),
            ]));
            lines.push(Line::from(""));
            field_idx += 1;
        }

        // New passphrase field
        {
            let is_active = *current_field == field_idx;
            lines.push(Line::from(Span::styled(
                "  New Passphrase:",
                field_label_style(is_active),
            )));
            let masked = "*".repeat(new_passphrase.chars().count());
            let rendered = render_with_cursor(&masked, *cursor_pos, is_active);
            let style = if is_active {
                Style::default().bg(Color::DarkGray).fg(Color::White)
            } else {
                Style::default()
            };
            lines.push(Line::from(vec![
                Span::raw("  "),
                Span::styled(rendered, style),
            ]));
            lines.push(Line::from(""));
            field_idx += 1;
        }

        // Confirm passphrase field
        {
            let is_active = *current_field == field_idx;
            lines.push(Line::from(Span::styled(
                "  Confirm Passphrase:",
                field_label_style(is_active),
            )));
            let masked = "*".repeat(confirm_passphrase.chars().count());
            let rendered = render_with_cursor(&masked, *cursor_pos, is_active);
            let style = if is_active {
                Style::default().bg(Color::DarkGray).fg(Color::White)
            } else {
                Style::default()
            };
            lines.push(Line::from(vec![
                Span::raw("  "),
                Span::styled(rendered, style),
            ]));
            lines.push(Line::from(""));
        }

        if let Some(err) = error_message {
            lines.push(Line::from(Span::styled(
                format!("  {}", err),
                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
            )));
        } else {
            lines.push(Line::from(""));
        }

        lines.push(Line::from(vec![
            Span::styled("  Tab", Style::default().fg(Color::Cyan)),
            Span::raw(": Next  "),
            Span::styled("Enter", Style::default().fg(Color::Green)),
            Span::raw(": Save  "),
            Span::styled("Esc", Style::default().fg(Color::Red)),
            Span::raw(": Cancel"),
        ]));

        let block = Block::default()
            .borders(Borders::ALL)
            .title(title)
            .border_style(Style::default().fg(Color::Cyan));

        let paragraph = Paragraph::new(lines).block(block);
        f.render_widget(paragraph, area);
    }
}

fn field_label_style(is_active: bool) -> Style {
    if is_active {
        Style::default()
            .fg(Color::Yellow)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(Color::Gray)
    }
}

/// Create a centered rect of given percentage width and fixed height.
fn centered_rect(percent_x: u16, height: u16, r: Rect) -> Rect {
    let popup_layout = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(r.height.saturating_sub(height) / 2),
            Constraint::Length(height),
            Constraint::Min(0),
        ])
        .split(r);

    Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage((100 - percent_x) / 2),
            Constraint::Percentage(percent_x),
            Constraint::Percentage((100 - percent_x) / 2),
        ])
        .split(popup_layout[1])[1]
}