cube-tui 0.1.9

Terminal UI timer and session manager for speedcubing, with optional web dashboard and BLE (GAN) timer support.
use chrono::{Local, TimeZone};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Modifier as StyleModifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Widget, Wrap};

use crate::model::settings::ThemeSettings;
use crate::widgets::history::{Modifier, Time};

pub struct DetailsWidget<'a> {
    time: Option<&'a Time>,
    selected_modifier_index: usize,
}

impl<'a> DetailsWidget<'a> {
    pub const fn new(time: Option<&'a Time>, selected_modifier_index: usize) -> Self {
        Self {
            time,
            selected_modifier_index,
        }
    }

    pub fn render_with_theme(self, area: Rect, buf: &mut Buffer, theme: &ThemeSettings) {
        let block = Block::default()
            .title("Time Details")
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border()));

        let lines = self.time.map_or_else(
            || {
                vec![
                    Line::from(Span::styled(
                        "No time selected.",
                        Style::default().fg(theme.text()),
                    )),
                    Line::from(Span::styled(
                        "Esc: close",
                        Style::default().fg(theme.text()),
                    )),
                ]
            },
            |time| {
                let plus_two_checked = matches!(time.modifier(), Modifier::PlusTwo);
                let dnf_checked = matches!(time.modifier(), Modifier::DNF);

                vec![
                    Line::from(Span::styled(
                        format!("Time: {time}"),
                        Style::default().fg(theme.text()),
                    )),
                    Line::from(Span::styled(
                        format!("Datetime: {}", format_datetime(time.solved_at_unix_ms())),
                        Style::default().fg(theme.text()),
                    )),
                    Line::from(""),
                    Line::from(Span::styled(
                        format!("Event: {}", time.event().name()),
                        Style::default().fg(theme.text()),
                    )),
                    Line::from(""),
                    Line::from(Span::styled(
                        format!("Scramble: {}", time.scramble()),
                        Style::default().fg(theme.text()),
                    )),
                    Line::from(""),
                    Line::from(Span::styled(
                        "Modifiers:",
                        Style::default().fg(theme.text()),
                    )),
                    checkbox_line(
                        "+2",
                        plus_two_checked,
                        self.selected_modifier_index == 0,
                        theme,
                    ),
                    checkbox_line("DNF", dnf_checked, self.selected_modifier_index == 1, theme),
                ]
            },
        );

        Paragraph::new(lines)
            .block(block)
            .wrap(Wrap { trim: true })
            .render(area, buf);
    }
}

fn checkbox_line(
    label: &str,
    checked: bool,
    selected: bool,
    theme: &ThemeSettings,
) -> Line<'static> {
    let check = if checked { "x" } else { " " };
    let style = if selected {
        Style::default()
            .fg(theme.text())
            .add_modifier(StyleModifier::BOLD)
    } else {
        Style::default().fg(theme.text())
    };
    Line::from(Span::styled(format!("[{check}] {label}"), style))
}

fn format_datetime(unix_ms: u64) -> String {
    if unix_ms == 0 {
        return "-".to_string();
    }

    Local
        .timestamp_millis_opt(i64::try_from(unix_ms).expect("Failed to parse time"))
        .single()
        .map_or_else(
            || "-".to_string(),
            |dt| dt.format("%Y-%m-%d %H:%M:%S").to_string(),
        )
}