hitman 0.12.1

A command line tool for hitting API endpoints
Documentation
use chrono::{Datelike, Days, Local, Months, NaiveDate, Weekday};
use hitman::substitute::SubstitutionValue;
use ratatui::{
    layout::{Constraint, Layout},
    prelude::{Alignment::Center, Margin},
    style::{Style, Stylize},
    text::{Line, Span},
    widgets::{Block, Clear, Paragraph},
};

use super::{
    centered,
    keymap::{mapkey, KeyMapping},
    Component, PromptComponent, PromptIntent,
};

pub struct DatePicker {
    title: String,
    selected: NaiveDate,
}

impl DatePicker {
    pub fn new(title: String) -> Self {
        Self {
            title,
            selected: Local::now().date_naive(),
        }
    }

    pub fn with_fallback(self, fallback: Option<String>) -> Self {
        Self {
            selected: fallback
                .and_then(|f| f.parse::<NaiveDate>().ok())
                .unwrap_or(self.selected),
            ..self
        }
    }
}

impl Component for DatePicker {
    fn render_ui(
        &mut self,
        frame: &mut ratatui::Frame,
        area: ratatui::prelude::Rect,
    ) {
        let area = centered(area, 32, 14);

        let today = Local::now().date_naive();
        let date = self.selected;

        let start = NaiveDate::from_ymd_opt(date.year(), date.month(), 1)
            .expect("start of month to be valid");

        let end = start.checked_add_months(Months::new(1)).expect("add month");

        let start = start.week(Weekday::Mon).first_day();

        let weekdays = Line::from(
            start
                .iter_days()
                .take(7)
                .map(|d| {
                    Span::raw(format!(" {}", d.format("%a")))
                        .style(Style::new().white())
                })
                .collect::<Vec<_>>(),
        );

        let calendar: Vec<_> = start
            .iter_weeks()
            .take_while(|d| *d < end)
            .map(|d| {
                let days: Vec<_> = d
                    .iter_days()
                    .take(7)
                    .map(|d| {
                        let style = if d == date {
                            Style::new().reversed()
                        } else if d == today {
                            Style::new().cyan()
                        } else if d.month() != date.month() {
                            Style::new().dark_gray()
                        } else {
                            Style::new().white()
                        };
                        Span::raw(format!(" {} ", d.format("%_d"))).style(style)
                    })
                    .collect();

                Line::from(days)
            })
            .collect();

        let block = Block::bordered().cyan().title(self.title.clone());

        let inner_layout = Layout::vertical([
            Constraint::Length(1),
            Constraint::Length(2),
            Constraint::Length(1),
            Constraint::Fill(1),
            Constraint::Length(1),
        ])
        .split(block.inner(area));

        frame.render_widget(Clear, area);
        frame.render_widget(block, area);

        let heading =
            Line::raw(format!("{}", date.format("%B %Y"))).alignment(Center);
        frame.render_widget(Paragraph::new(vec![heading]), inner_layout[1]);

        frame.render_widget(Paragraph::new(weekdays), inner_layout[2]);
        frame.render_widget(
            Paragraph::new(calendar),
            inner_layout[3].inner(Margin::new(1, 0)),
        );

        let current = Line::from(format!("{date}"))
            .alignment(Center)
            .style(Style::new().white());
        frame.render_widget(Paragraph::new(vec![current]), inner_layout[4]);
    }
}

impl PromptComponent for DatePicker {
    fn handle_prompt(
        &mut self,
        event: &crossterm::event::Event,
    ) -> Option<super::PromptIntent> {
        match mapkey(event) {
            KeyMapping::ScrollUp => {
                self.selected =
                    self.selected.checked_sub_months(Months::new(1)).unwrap();
            }
            KeyMapping::ScrollDown => {
                self.selected =
                    self.selected.checked_add_months(Months::new(1)).unwrap();
            }
            KeyMapping::Up => {
                self.selected =
                    self.selected.checked_sub_days(Days::new(7)).unwrap();
            }
            KeyMapping::Down => {
                self.selected =
                    self.selected.checked_add_days(Days::new(7)).unwrap();
            }
            KeyMapping::Left => {
                self.selected =
                    self.selected.checked_sub_days(Days::new(1)).unwrap();
            }
            KeyMapping::Right => {
                self.selected =
                    self.selected.checked_add_days(Days::new(1)).unwrap();
            }
            KeyMapping::Accept => {
                return Some(PromptIntent::Accept(SubstitutionValue::Single(
                    format!("{}", self.selected),
                )));
            }
            KeyMapping::Abort => {
                return Some(PromptIntent::Abort);
            }
            _ => (),
        }

        None
    }
}