compact-calendar-cli 0.2.0

A compact calendar CLI with TOML-based date details
Documentation
use chrono::{Datelike, NaiveDate};

#[derive(Debug, Clone, Copy)]
pub struct MonthInfo {
    pub month: u32,
    pub name: &'static str,
    pub short_name: &'static str,
    pub days: u32,
}

impl MonthInfo {
    pub fn from_month(month: u32) -> Self {
        let (name, short_name, days) = match month {
            1 => ("January", "Jan", 31),
            2 => ("February", "Feb", 28),
            3 => ("March", "Mar", 31),
            4 => ("April", "Apr", 30),
            5 => ("May", "May", 31),
            6 => ("June", "Jun", 30),
            7 => ("July", "Jul", 31),
            8 => ("August", "Aug", 31),
            9 => ("September", "Sep", 30),
            10 => ("October", "Oct", 31),
            11 => ("November", "Nov", 30),
            12 => ("December", "Dec", 31),
            _ => ("", "", 0),
        };
        MonthInfo {
            month,
            name,
            short_name,
            days,
        }
    }

    pub fn from_date(date: NaiveDate) -> Self {
        Self::from_month(date.month())
    }

    pub fn is_leap_year(year: i32) -> bool {
        (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
    }

    pub fn days_in_month(month: u32, year: i32) -> u32 {
        if month == 2 && Self::is_leap_year(year) {
            29
        } else {
            Self::from_month(month).days
        }
    }
}

const DAYS_IN_WEEK: i64 = 7;

#[derive(Debug, Clone)]
pub struct WeekLayout {
    pub dates: Vec<NaiveDate>,
    pub month_start_idx: Option<(usize, u32)>,
    pub month_end_idx: Option<(usize, u32)>,
    pub year_boundary_idx: Option<usize>,
}

impl WeekLayout {
    pub fn new(start_date: NaiveDate) -> Self {
        let dates: Vec<NaiveDate> = (0..DAYS_IN_WEEK)
            .map(|day_offset| {
                start_date
                    .checked_add_signed(chrono::Duration::days(day_offset))
                    .unwrap()
            })
            .collect();

        let month_start_idx = dates
            .iter()
            .enumerate()
            .find(|(_, date)| date.day() == 1)
            .map(|(idx, date)| (idx, date.month()));

        let month_end_idx = Self::find_month_end(&dates);
        let year_boundary_idx = Self::find_year_boundary(&dates);

        WeekLayout {
            dates,
            month_start_idx,
            month_end_idx,
            year_boundary_idx,
        }
    }

    fn find_month_end(dates: &[NaiveDate]) -> Option<(usize, u32)> {
        for (idx, &date) in dates.iter().enumerate() {
            if idx < dates.len() - 1 {
                let next_date = dates[idx + 1];
                if date.month() != next_date.month() || date.year() != next_date.year() {
                    return Some((idx, date.month()));
                }
            }
        }
        None
    }

    fn find_year_boundary(dates: &[NaiveDate]) -> Option<usize> {
        for (idx, &date) in dates.iter().enumerate() {
            if idx > 0 {
                let prev_date = dates[idx - 1];
                if date.year() != prev_date.year() {
                    return Some(idx);
                }
            }
        }
        None
    }

    pub fn get_date(&self, idx: usize) -> Option<NaiveDate> {
        self.dates.get(idx).copied()
    }

    pub fn get_first_date(&self) -> NaiveDate {
        self.dates[0]
    }

    pub fn get_last_date(&self) -> NaiveDate {
        self.dates[self.dates.len() - 1]
    }

    pub fn contains_month_start(&self) -> bool {
        self.month_start_idx.is_some()
    }

    pub fn contains_month_end(&self) -> bool {
        self.month_end_idx.is_some()
    }

    pub fn is_in_current_month(&self, idx: usize, year: i32, current_month: Option<u32>) -> bool {
        if let Some(date) = self.get_date(idx) {
            date.year() == year && Some(date.month()) == current_month
        } else {
            false
        }
    }

    pub fn was_prev_in_month(&self, idx: usize, year: i32, current_month: Option<u32>) -> bool {
        if idx > 0 {
            if let Some(prev_date) = self.get_date(idx - 1) {
                return prev_date.year() == year && Some(prev_date.month()) == current_month;
            }
        }
        false
    }

    pub fn will_next_be_in_month(&self, idx: usize, year: i32, current_month: Option<u32>) -> bool {
        if idx < 6 {
            if let Some(next_date) = self.get_date(idx + 1) {
                return next_date.year() == year && Some(next_date.month()) == current_month;
            }
        }
        false
    }

    pub fn count_days_in_month(&self, month: u32) -> usize {
        self.dates.iter().filter(|d| d.month() == month).count()
    }
}

#[derive(Debug, Clone, Copy)]
pub struct SpacingConfig {
    pub idx: usize,
    pub in_month: bool,
    pub prev_in_month: bool,
    pub next_in_month: bool,
    pub first_out_of_month: bool,
}

impl SpacingConfig {
    pub fn new(
        idx: usize,
        in_month: bool,
        prev_in_month: bool,
        next_in_month: bool,
        first_out_of_month: bool,
    ) -> Self {
        Self {
            idx,
            in_month,
            prev_in_month,
            next_in_month,
            first_out_of_month,
        }
    }

    pub fn is_last_in_week(&self) -> bool {
        self.idx >= 6
    }

    pub fn is_first_in_week(&self) -> bool {
        self.idx == 0
    }
}

#[derive(Debug, Clone, Copy)]
pub struct SpacingCalculator;

impl SpacingCalculator {
    pub fn date_spacing(config: SpacingConfig) -> &'static str {
        match (
            config.is_last_in_week(),
            config.in_month,
            config.prev_in_month,
            config.next_in_month,
            config.first_out_of_month,
        ) {
            (true, true, _, _, _) => "   ",
            (true, false, _, _, _) => "",
            (false, true, false, _, _) if config.is_first_in_week() => "    ",
            (false, true, _, _, _) => "   ",
            (false, false, _, true, _) => "  ",
            (false, false, _, false, true) => "    ",
            _ => "   ",
        }
    }

    pub fn date_spacing_legacy(
        idx: usize,
        in_month: bool,
        prev_in_month: bool,
        next_in_month: bool,
        first_out_of_month: bool,
    ) -> &'static str {
        let config = SpacingConfig::new(
            idx,
            in_month,
            prev_in_month,
            next_in_month,
            first_out_of_month,
        );
        Self::date_spacing(config)
    }

    pub fn border_width_before(bar_idx: usize) -> usize {
        if bar_idx == 0 {
            0
        } else if bar_idx == 1 {
            5
        } else {
            6 + (bar_idx - 2) * 5 + 4
        }
    }

    pub fn border_width_after(bar_idx: usize) -> usize {
        (7 - bar_idx) * 5
    }
}

#[derive(Debug, Clone)]
pub struct BorderState {
    pub before_width: usize,
    pub after_width: usize,
    pub has_boundary: bool,
    pub boundary_position: Option<usize>,
}

impl BorderState {
    pub fn new(boundary_position: Option<usize>) -> Self {
        let (before_width, after_width, has_boundary) = if let Some(pos) = boundary_position {
            (
                SpacingCalculator::border_width_before(pos),
                SpacingCalculator::border_width_after(pos),
                true,
            )
        } else {
            (0, 0, false)
        };

        Self {
            before_width,
            after_width,
            has_boundary,
            boundary_position,
        }
    }

    pub fn total_width(&self) -> usize {
        self.before_width + self.after_width
    }
}