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, Single(u32), Current, CurrentWithFollowing(u32), }
75
76impl MonthFilter {
77 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 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 fn parse_month(input: &str) -> Result<Self, String> {
111 if input.eq_ignore_ascii_case("current") {
113 return Ok(MonthFilter::Current);
114 }
115
116 if let Ok(num) = input.parse::<u32>() {
118 return Self::validate_month_number(num);
119 }
120
121 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 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 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 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 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}