compact_calendar_cli/
models.rs

1use chrono::{Datelike, NaiveDate};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum WeekStart {
6    Monday,
7    Sunday,
8}
9
10impl WeekStart {
11    pub fn from_sunday_flag(sunday: bool) -> Self {
12        if sunday {
13            Self::Sunday
14        } else {
15            Self::Monday
16        }
17    }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum WeekendDisplay {
22    Dimmed,
23    Normal,
24}
25
26impl WeekendDisplay {
27    pub fn from_no_dim_flag(no_dim_weekends: bool) -> Self {
28        if no_dim_weekends {
29            Self::Normal
30        } else {
31            Self::Dimmed
32        }
33    }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ColorMode {
38    Normal,
39    Work,
40}
41
42impl ColorMode {
43    pub fn from_work_flag(work: bool) -> Self {
44        if work {
45            Self::Work
46        } else {
47            Self::Normal
48        }
49    }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum PastDateDisplay {
54    Strikethrough,
55    Normal,
56}
57
58impl PastDateDisplay {
59    pub fn from_no_strikethrough_flag(no_strikethrough: bool) -> Self {
60        if no_strikethrough {
61            Self::Normal
62        } else {
63            Self::Strikethrough
64        }
65    }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum MonthFilter {
70    All,                       // Default: show all months
71    Single(u32),               // --month N: show specific month (1-12)
72    Current,                   // --month current
73    CurrentWithFollowing(u32), // --month current --following-months N
74}
75
76impl MonthFilter {
77    /// Create MonthFilter from CLI arguments
78    pub fn from_cli_args(
79        month: Option<&str>,
80        following_months: Option<u32>,
81    ) -> Result<Self, String> {
82        match (month, following_months) {
83            (None, None) => Ok(MonthFilter::All),
84            (None, Some(_)) => Err("--following-months requires --month current".to_string()),
85            (Some(month_str), following) => {
86                let base_filter = Self::parse_month(month_str)?;
87                Self::apply_following_months(base_filter, following)
88            }
89        }
90    }
91
92    /// Apply following_months modifier to a base filter
93    fn apply_following_months(base: Self, following: Option<u32>) -> Result<Self, String> {
94        match (base, following) {
95            (filter, None) => Ok(filter),
96            (MonthFilter::Current, Some(n)) => {
97                if n > 11 {
98                    Err("--following-months cannot exceed 11".to_string())
99                } else {
100                    Ok(MonthFilter::CurrentWithFollowing(n))
101                }
102            }
103            (_, Some(_)) => {
104                Err("--following-months can only be used with --month current".to_string())
105            }
106        }
107    }
108
109    /// Parse month from string (number, name, or "current")
110    fn parse_month(input: &str) -> Result<Self, String> {
111        // Check for "current" first
112        if input.eq_ignore_ascii_case("current") {
113            return Ok(MonthFilter::Current);
114        }
115
116        // Try parsing as number
117        if let Ok(num) = input.parse::<u32>() {
118            return Self::validate_month_number(num);
119        }
120
121        // Parse as month name
122        Self::parse_month_name(input)
123    }
124
125    fn validate_month_number(num: u32) -> Result<Self, String> {
126        if (1..=12).contains(&num) {
127            Ok(MonthFilter::Single(num))
128        } else {
129            Err(format!("Month number must be 1-12, got {}", num))
130        }
131    }
132
133    fn parse_month_name(input: &str) -> Result<Self, String> {
134        let month_num = match input.to_lowercase().as_str() {
135            "january" | "jan" => 1,
136            "february" | "feb" => 2,
137            "march" | "mar" => 3,
138            "april" | "apr" => 4,
139            "may" => 5,
140            "june" | "jun" => 6,
141            "july" | "jul" => 7,
142            "august" | "aug" => 8,
143            "september" | "sep" | "sept" => 9,
144            "october" | "oct" => 10,
145            "november" | "nov" => 11,
146            "december" | "dec" => 12,
147            _ => {
148                return Err(format!(
149                    "Invalid month: '{}'. Use 1-12, month name (e.g., 'march'), or 'current'",
150                    input
151                ))
152            }
153        };
154
155        Ok(MonthFilter::Single(month_num))
156    }
157
158    /// Get the range of months to display (start_month, end_month) for the given year
159    pub fn get_month_range(&self, _year: i32) -> (u32, u32) {
160        match self {
161            MonthFilter::All => (1, 12),
162            MonthFilter::Single(m) => (*m, *m),
163            MonthFilter::Current => {
164                let month = Self::get_current_month_number();
165                (month, month)
166            }
167            MonthFilter::CurrentWithFollowing(n) => {
168                let start_month = Self::get_current_month_number();
169                let end_month = (start_month + n).min(12);
170                (start_month, end_month)
171            }
172        }
173    }
174
175    fn get_current_month_number() -> u32 {
176        chrono::Local::now().date_naive().month()
177    }
178
179    /// Check if a specific month should be displayed
180    pub fn should_display_month(&self, month: u32, year: i32) -> bool {
181        let (start, end) = self.get_month_range(year);
182        month >= start && month <= end
183    }
184
185    /// Get the filtered date range (start_date, end_date) for rendering
186    pub fn get_date_range(&self, year: i32) -> (NaiveDate, NaiveDate) {
187        let (start_month, end_month) = self.get_month_range(year);
188
189        let start_date = NaiveDate::from_ymd_opt(year, start_month, 1).unwrap();
190        let end_date = Self::get_last_day_of_month(year, end_month);
191
192        (start_date, end_date)
193    }
194
195    fn get_last_day_of_month(year: i32, month: u32) -> NaiveDate {
196        if month == 12 {
197            NaiveDate::from_ymd_opt(year, 12, 31).unwrap()
198        } else {
199            // Get first day of next month, then go back one day
200            NaiveDate::from_ymd_opt(year, month + 1, 1)
201                .unwrap()
202                .pred_opt()
203                .unwrap()
204        }
205    }
206}
207
208#[derive(Debug, Clone)]
209pub struct DateDetail {
210    pub description: String,
211    pub color: Option<String>,
212}
213
214#[derive(Debug, Clone)]
215pub struct DateRange {
216    pub start: NaiveDate,
217    pub end: NaiveDate,
218    pub color: String,
219    pub description: Option<String>,
220}
221
222#[derive(Debug, Clone)]
223pub struct CalendarOptions {
224    pub week_start: WeekStart,
225    pub weekend_display: WeekendDisplay,
226    pub color_mode: ColorMode,
227    pub past_date_display: PastDateDisplay,
228    pub month_filter: MonthFilter,
229}
230
231pub struct Calendar {
232    pub year: i32,
233    pub week_start: WeekStart,
234    pub weekend_display: WeekendDisplay,
235    pub color_mode: ColorMode,
236    pub past_date_display: PastDateDisplay,
237    pub month_filter: MonthFilter,
238    pub details: HashMap<NaiveDate, DateDetail>,
239    pub ranges: Vec<DateRange>,
240}
241
242impl Calendar {
243    pub fn new(
244        year: i32,
245        options: CalendarOptions,
246        details: HashMap<NaiveDate, DateDetail>,
247        ranges: Vec<DateRange>,
248    ) -> Self {
249        Calendar {
250            year,
251            week_start: options.week_start,
252            weekend_display: options.weekend_display,
253            color_mode: options.color_mode,
254            past_date_display: options.past_date_display,
255            month_filter: options.month_filter,
256            details,
257            ranges,
258        }
259    }
260
261    pub fn get_weekday_num(&self, date: NaiveDate) -> u32 {
262        match self.week_start {
263            WeekStart::Monday => date.weekday().num_days_from_monday(),
264            WeekStart::Sunday => date.weekday().num_days_from_sunday(),
265        }
266    }
267}