compact_calendar_cli/
rendering.rs

1use crate::formatting::{MonthInfo, WeekLayout};
2use crate::models::{Calendar, ColorMode, DateDetail, PastDateDisplay, WeekStart, WeekendDisplay};
3use anstyle::{AnsiColor, Color, Effects, RgbColor, Style};
4use chrono::Weekday;
5use chrono::{Datelike, NaiveDate};
6
7#[derive(Debug, Clone)]
8pub struct RenderState {
9    pub week_num: i32,
10    pub current_month: Option<u32>,
11    pub is_first_month: bool,
12    pub current_date: NaiveDate,
13}
14
15impl RenderState {
16    pub fn new(start_date: NaiveDate) -> Self {
17        Self {
18            week_num: 1,
19            current_month: None,
20            is_first_month: true,
21            current_date: start_date,
22        }
23    }
24
25    pub fn advance_week(&mut self, days: i64) {
26        self.week_num += 1;
27        self.current_date = self
28            .current_date
29            .checked_add_signed(chrono::Duration::days(days))
30            .unwrap();
31    }
32
33    pub fn set_current_month(&mut self, month: u32) {
34        self.current_month = Some(month);
35        self.is_first_month = false;
36    }
37}
38
39#[derive(Debug, Clone)]
40pub struct WeekRenderContext<'a> {
41    pub layout: &'a WeekLayout,
42    pub next_layout: Option<&'a WeekLayout>,
43    pub week_num: i32,
44    pub current_month: Option<u32>,
45    pub is_last_week: bool,
46}
47
48impl<'a> WeekRenderContext<'a> {
49    pub fn new(
50        layout: &'a WeekLayout,
51        next_layout: Option<&'a WeekLayout>,
52        week_num: i32,
53        current_month: Option<u32>,
54        is_last_week: bool,
55    ) -> Self {
56        Self {
57            layout,
58            next_layout,
59            week_num,
60            current_month,
61            is_last_week,
62        }
63    }
64
65    pub fn get_month_name(&self) -> &'static str {
66        if let Some((_, month)) = self.layout.month_start_idx {
67            MonthInfo::from_month(month).name
68        } else {
69            ""
70        }
71    }
72
73    pub fn has_month_boundary_at(&self, idx: usize) -> bool {
74        if idx > 0 && idx < self.layout.dates.len() {
75            let prev_date = self.layout.dates[idx - 1];
76            let date = self.layout.dates[idx];
77            date.month() != prev_date.month() || date.year() != prev_date.year()
78        } else {
79            false
80        }
81    }
82
83    pub fn next_is_boundary_after(&self, idx: usize) -> bool {
84        if idx < 6 {
85            let date = self.layout.dates[idx];
86            let next_date = self.layout.dates[idx + 1];
87            date.month() != next_date.month() || date.year() != next_date.year()
88        } else {
89            false
90        }
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct DateStyle {
96    pub color: Option<String>,
97    pub is_today: bool,
98    pub is_past: bool,
99    pub is_weekend: bool,
100    pub effects: Effects,
101}
102
103impl DateStyle {
104    pub fn new(
105        date: NaiveDate,
106        color: Option<String>,
107        today: NaiveDate,
108        past_date_display: PastDateDisplay,
109        weekend_display: WeekendDisplay,
110    ) -> Self {
111        let is_today = date == today;
112        let is_past = past_date_display == PastDateDisplay::Strikethrough && date < today;
113        let is_weekend = weekend_display == WeekendDisplay::Dimmed
114            && (date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun);
115
116        let mut effects = Effects::new();
117        if is_past {
118            effects |= Effects::STRIKETHROUGH;
119        }
120        if is_today {
121            effects |= Effects::UNDERLINE;
122        }
123        if is_weekend && color.is_none() {
124            effects |= Effects::DIMMED;
125        }
126
127        Self {
128            color,
129            is_today,
130            is_past,
131            is_weekend,
132            effects,
133        }
134    }
135
136    pub fn to_style(&self) -> Style {
137        if let Some(ref color) = self.color {
138            let base = if self.is_weekend {
139                ColorCodes::get_dimmed_bg_color(color)
140            } else {
141                ColorCodes::get_bg_color(color)
142            };
143            base.fg_color(ColorCodes::black_text().get_fg_color())
144                .effects(self.effects)
145        } else {
146            Style::new().effects(self.effects)
147        }
148    }
149}
150
151#[derive(Debug, Clone)]
152pub struct BorderContext {
153    pub month_boundary_idx: Option<usize>,
154    pub first_bar_idx: Option<usize>,
155}
156
157impl BorderContext {
158    pub fn from_layout(layout: &WeekLayout, current_month: Option<u32>, year: i32) -> Self {
159        let month_boundary_idx = Self::find_month_boundary(layout);
160        let first_bar_idx = Self::find_first_bar(layout, current_month, year);
161
162        Self {
163            month_boundary_idx,
164            first_bar_idx,
165        }
166    }
167
168    fn find_month_boundary(layout: &WeekLayout) -> Option<usize> {
169        for (idx, &date) in layout.dates.iter().enumerate() {
170            if idx > 0 {
171                let prev_date = layout.dates[idx - 1];
172                if date.month() != prev_date.month() || date.year() != prev_date.year() {
173                    return Some(idx);
174                }
175            }
176        }
177        None
178    }
179
180    fn find_first_bar(layout: &WeekLayout, current_month: Option<u32>, year: i32) -> Option<usize> {
181        for (idx, &date) in layout.dates.iter().enumerate() {
182            let in_month = date.year() == year && Some(date.month()) == current_month;
183            let prev_in_month = if idx > 0 {
184                let prev_date = layout.dates[idx - 1];
185                prev_date.year() == year && Some(prev_date.month()) == current_month
186            } else {
187                false
188            };
189
190            if in_month && !prev_in_month {
191                return Some(idx);
192            }
193        }
194        None
195    }
196
197    pub fn calculate_dashes_before(&self, boundary_idx: usize) -> usize {
198        (boundary_idx - 1) * 5 + 4
199    }
200
201    pub fn calculate_dashes_after(&self, boundary_idx: usize, days_in_week: usize) -> usize {
202        (days_in_week - boundary_idx) * 5 - 1
203    }
204}
205
206#[derive(Debug, Clone, Default)]
207pub struct AnnotationContext {
208    pub details_queue: Vec<(NaiveDate, DateDetail)>,
209    pub shown_ranges: Vec<usize>,
210}
211
212impl AnnotationContext {
213    pub fn new() -> Self {
214        Self::default()
215    }
216
217    pub fn add_detail(&mut self, date: NaiveDate, detail: DateDetail) {
218        if !self.details_queue.iter().any(|(d, _)| d == &date) {
219            self.details_queue.push((date, detail));
220        }
221    }
222
223    pub fn pop_next_detail(&mut self) -> Option<(NaiveDate, DateDetail)> {
224        if !self.details_queue.is_empty() {
225            Some(self.details_queue.remove(0))
226        } else {
227            None
228        }
229    }
230
231    pub fn mark_range_shown(&mut self, idx: usize) {
232        if !self.shown_ranges.contains(&idx) {
233            self.shown_ranges.push(idx);
234        }
235    }
236
237    pub fn is_range_shown(&self, idx: usize) -> bool {
238        self.shown_ranges.contains(&idx)
239    }
240}
241
242#[derive(Debug, Clone, Copy)]
243pub struct ColorValue {
244    pub normal: RgbColor,
245    pub dimmed: RgbColor,
246}
247
248impl ColorValue {
249    pub const fn new(normal: RgbColor, dimmed: RgbColor) -> Self {
250        Self { normal, dimmed }
251    }
252
253    pub fn get_normal_style(&self) -> Style {
254        Style::new().bg_color(Some(Color::Rgb(self.normal)))
255    }
256
257    pub fn get_dimmed_style(&self) -> Style {
258        Style::new().bg_color(Some(Color::Rgb(self.dimmed)))
259    }
260}
261
262#[derive(Debug, Clone)]
263pub struct ColorPalette {
264    colors_enabled: bool,
265}
266
267impl Default for ColorPalette {
268    fn default() -> Self {
269        Self {
270            colors_enabled: !Self::is_color_disabled(),
271        }
272    }
273}
274
275impl ColorPalette {
276    pub fn new() -> Self {
277        Self::default()
278    }
279
280    fn is_color_disabled() -> bool {
281        std::env::var("NO_COLOR").is_ok()
282    }
283
284    pub fn are_colors_enabled(&self) -> bool {
285        self.colors_enabled
286    }
287
288    pub fn get_color_value(name: &str) -> Option<ColorValue> {
289        match name {
290            "orange" => Some(ColorValue::new(
291                RgbColor(255, 143, 64),
292                RgbColor(178, 100, 45),
293            )),
294            "yellow" => Some(ColorValue::new(
295                RgbColor(230, 180, 80),
296                RgbColor(161, 126, 56),
297            )),
298            "green" => Some(ColorValue::new(
299                RgbColor(170, 217, 76),
300                RgbColor(119, 152, 53),
301            )),
302            "blue" => Some(ColorValue::new(
303                RgbColor(89, 194, 255),
304                RgbColor(62, 136, 179),
305            )),
306            "purple" => Some(ColorValue::new(
307                RgbColor(210, 166, 255),
308                RgbColor(147, 116, 179),
309            )),
310            "red" => Some(ColorValue::new(
311                RgbColor(240, 113, 120),
312                RgbColor(168, 79, 84),
313            )),
314            "cyan" => Some(ColorValue::new(
315                RgbColor(149, 230, 203),
316                RgbColor(104, 161, 142),
317            )),
318            "gray" => Some(ColorValue::new(RgbColor(95, 99, 110), RgbColor(67, 69, 77))),
319            "light_orange" => Some(ColorValue::new(
320                RgbColor(255, 180, 84),
321                RgbColor(179, 126, 59),
322            )),
323            "light_yellow" => Some(ColorValue::new(
324                RgbColor(249, 175, 79),
325                RgbColor(174, 123, 55),
326            )),
327            "light_green" => Some(ColorValue::new(
328                RgbColor(145, 179, 98),
329                RgbColor(102, 125, 69),
330            )),
331            "light_blue" => Some(ColorValue::new(
332                RgbColor(83, 189, 250),
333                RgbColor(58, 132, 175),
334            )),
335            "light_purple" => Some(ColorValue::new(
336                RgbColor(210, 166, 255),
337                RgbColor(147, 116, 179),
338            )),
339            "light_red" => Some(ColorValue::new(
340                RgbColor(234, 108, 115),
341                RgbColor(164, 76, 81),
342            )),
343            "light_cyan" => Some(ColorValue::new(
344                RgbColor(144, 225, 198),
345                RgbColor(101, 158, 139),
346            )),
347            _ => None,
348        }
349    }
350
351    pub fn get_style(&self, color_name: &str, dimmed: bool) -> Style {
352        if !self.colors_enabled {
353            return Style::new();
354        }
355
356        if let Some(color_value) = Self::get_color_value(color_name) {
357            if dimmed {
358                color_value.get_dimmed_style()
359            } else {
360                color_value.get_normal_style()
361            }
362        } else {
363            Style::new()
364        }
365    }
366
367    pub fn black_text() -> Style {
368        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Black)))
369    }
370}
371
372struct ColorCodes;
373
374impl ColorCodes {
375    fn is_color_disabled() -> bool {
376        std::env::var("NO_COLOR").is_ok()
377    }
378
379    fn get_bg_color(color: &str) -> Style {
380        if Self::is_color_disabled() {
381            return Style::new();
382        }
383        let palette = ColorPalette::new();
384        palette.get_style(color, false)
385    }
386
387    fn get_dimmed_bg_color(color: &str) -> Style {
388        if Self::is_color_disabled() {
389            return Style::new();
390        }
391        let palette = ColorPalette::new();
392        palette.get_style(color, true)
393    }
394
395    fn black_text() -> Style {
396        ColorPalette::black_text()
397    }
398
399    fn underline() -> Effects {
400        Effects::UNDERLINE
401    }
402
403    fn strikethrough() -> Effects {
404        Effects::STRIKETHROUGH
405    }
406
407    fn dim() -> Effects {
408        Effects::DIMMED
409    }
410}
411
412const DAYS_IN_WEEK: usize = 7;
413const CALENDAR_WIDTH: usize = 34;
414const HEADER_WIDTH: usize = 48;
415
416pub struct CalendarRenderer<'a> {
417    calendar: &'a Calendar,
418}
419
420impl<'a> CalendarRenderer<'a> {
421    pub fn new(calendar: &'a Calendar) -> Self {
422        CalendarRenderer { calendar }
423    }
424
425    pub fn render(&self) {
426        self.print_header();
427        self.print_weeks();
428        println!();
429    }
430
431    pub fn render_to_string(&self) -> String {
432        let mut output = String::new();
433
434        let prev_no_color = std::env::var("NO_COLOR").ok();
435        std::env::set_var("NO_COLOR", "1");
436
437        output.push_str(&self.header_to_string());
438        output.push_str(&self.weeks_to_string());
439        output.push('\n');
440
441        match prev_no_color {
442            Some(val) => std::env::set_var("NO_COLOR", val),
443            None => std::env::remove_var("NO_COLOR"),
444        }
445
446        output
447    }
448
449    fn header_to_string(&self) -> String {
450        let mut output = String::new();
451        output.push_str(&format!("┌{:─<width$}┐\n", "", width = HEADER_WIDTH));
452        output.push_str(&format!(
453            "│                   COMPACT CALENDAR {}        │\n",
454            self.calendar.year
455        ));
456        output.push_str(&format!("├{:─<width$}┤\n", "", width = HEADER_WIDTH));
457        output.push_str("│              ");
458        match self.calendar.week_start {
459            WeekStart::Monday => output.push_str("Mon  Tue  Wed  Thu  Fri  Sat  Sun │\n"),
460            WeekStart::Sunday => output.push_str("Sun  Mon  Tue  Wed  Thu  Fri  Sat │\n"),
461        }
462        output
463    }
464
465    fn weeks_to_string(&self) -> String {
466        let mut output = String::new();
467        let start_date = NaiveDate::from_ymd_opt(self.calendar.year, 1, 1).unwrap();
468        let end_date = NaiveDate::from_ymd_opt(self.calendar.year, 12, 31).unwrap();
469
470        let mut current_date = self.align_to_week_start(start_date);
471        let mut week_num = 1;
472        let mut current_month: Option<u32> = None;
473
474        let mut details_queue: Vec<(NaiveDate, DateDetail)> = Vec::new();
475        let mut shown_ranges: Vec<usize> = Vec::new();
476
477        let mut is_first_month = true;
478
479        while current_date <= end_date {
480            let layout = WeekLayout::new(current_date);
481
482            let next_week_date = current_date
483                .checked_add_signed(chrono::Duration::days(DAYS_IN_WEEK as i64))
484                .unwrap();
485            let next_layout = WeekLayout::new(next_week_date);
486
487            if let Some((_, month)) = layout.month_start_idx {
488                current_month = Some(month);
489                if is_first_month {
490                    output.push_str(&self.month_border_to_string(&layout, current_month));
491                    is_first_month = false;
492                }
493            }
494
495            self.collect_details(&layout, &mut details_queue);
496
497            output.push_str(&self.week_row_to_string(week_num, &layout, current_month));
498
499            output.push_str(&self.annotations_to_string(
500                &layout,
501                &mut details_queue,
502                &mut shown_ranges,
503            ));
504
505            output.push('\n');
506
507            let is_last_week =
508                next_week_date.year() > self.calendar.year || next_week_date > end_date;
509
510            if is_last_week {
511                let mut month_boundary_idx = None;
512                for (idx, &date) in layout.dates.iter().enumerate() {
513                    if idx > 0 {
514                        let prev_date = layout.dates[idx - 1];
515                        if date.month() != prev_date.month() || date.year() != prev_date.year() {
516                            month_boundary_idx = Some(idx);
517                            break;
518                        }
519                    }
520                }
521
522                if let Some(boundary_idx) = month_boundary_idx {
523                    let dashes_before = (boundary_idx - 1) * 5 + 4;
524                    let dashes_after = (DAYS_IN_WEEK - boundary_idx) * 5 - 1;
525                    output.push_str(&format!(
526                        "└{:─<13}┴{:─<before$}┴{:─<after$}┘\n",
527                        "",
528                        "",
529                        "",
530                        before = dashes_before,
531                        after = dashes_after
532                    ));
533                } else {
534                    output.push_str(&format!(
535                        "└{:─<13}┴{:─<width$}┘\n",
536                        "",
537                        "",
538                        width = CALENDAR_WIDTH
539                    ));
540                }
541            } else if let Some((idx, _)) = layout.month_start_idx {
542                if idx > 0 {
543                    output.push_str(&self.separator_to_string(&layout, current_month));
544                }
545            } else if next_layout.month_start_idx.is_some()
546                && next_week_date <= end_date
547                && next_week_date.year() == self.calendar.year
548            {
549                output.push_str(&self.separator_before_month_to_string(
550                    &layout,
551                    current_month,
552                    &next_layout,
553                ));
554            }
555
556            current_date = next_week_date;
557            week_num += 1;
558
559            if current_date.year() > self.calendar.year {
560                break;
561            }
562        }
563
564        output
565    }
566
567    fn month_border_to_string(&self, layout: &WeekLayout, _current_month: Option<u32>) -> String {
568        let mut output = String::new();
569        if let Some((idx, _)) = layout.month_start_idx {
570            if idx > 0 {
571                output.push_str("│             ┌");
572                let dashes_before = (idx - 1) * 5 + 4;
573                for _ in 0..dashes_before {
574                    output.push('─');
575                }
576                output.push('┬');
577                let dashes_after = (DAYS_IN_WEEK - idx) * 5 - 1;
578                output.push_str(&format!("{:─<width$}┤\n", "", width = dashes_after));
579            }
580        }
581        output
582    }
583
584    fn week_row_to_string(
585        &self,
586        week_num: i32,
587        layout: &WeekLayout,
588        _current_month: Option<u32>,
589    ) -> String {
590        let mut output = String::new();
591        let month_name = if let Some((_, month)) = layout.month_start_idx {
592            MonthInfo::from_month(month).name
593        } else {
594            ""
595        };
596
597        if !month_name.is_empty() {
598            output.push_str(&format!("│W{:02} {:<9}", week_num, month_name));
599        } else {
600            output.push_str(&format!("│W{:02}          ", week_num));
601        }
602
603        output.push('│');
604
605        for (idx, &date) in layout.dates.iter().enumerate() {
606            let is_month_boundary = if idx > 0 {
607                let prev_date = layout.dates[idx - 1];
608                date.month() != prev_date.month() || date.year() != prev_date.year()
609            } else {
610                false
611            };
612
613            if is_month_boundary {
614                output.push('│');
615            }
616
617            output.push_str(&format!(" {:02}", date.day()));
618
619            if idx < 6 {
620                let next_date = layout.dates[idx + 1];
621                let next_is_boundary =
622                    date.month() != next_date.month() || date.year() != next_date.year();
623                if next_is_boundary {
624                    output.push(' ');
625                } else {
626                    output.push_str("  ");
627                }
628            } else {
629                output.push(' ');
630            }
631        }
632
633        output.push('│');
634        output
635    }
636
637    fn annotations_to_string(
638        &self,
639        layout: &WeekLayout,
640        details_queue: &mut Vec<(NaiveDate, DateDetail)>,
641        shown_ranges: &mut Vec<usize>,
642    ) -> String {
643        let mut output = String::new();
644        let week_end = layout.dates[DAYS_IN_WEEK - 1];
645        let mut printed_range = false;
646
647        for (idx, range) in self.calendar.ranges.iter().enumerate() {
648            if range.start >= layout.dates[0]
649                && range.start <= week_end
650                && !shown_ranges.contains(&idx)
651            {
652                if let Some(desc) = &range.description {
653                    output.push_str(&format!(
654                        "{} to {} - {}",
655                        range.start.format("%m/%d"),
656                        range.end.format("%m/%d"),
657                        desc
658                    ));
659                } else {
660                    output.push_str(&format!(
661                        "{} to {}",
662                        range.start.format("%m/%d"),
663                        range.end.format("%m/%d")
664                    ));
665                }
666                shown_ranges.push(idx);
667                printed_range = true;
668                break;
669            }
670        }
671
672        if !printed_range && !details_queue.is_empty() {
673            let (detail_date, detail) = &details_queue[0];
674            output.push_str(&format!(
675                "{} - {}",
676                detail_date.format("%m/%d"),
677                detail.description
678            ));
679            details_queue.remove(0);
680        }
681
682        output
683    }
684
685    fn separator_to_string(&self, layout: &WeekLayout, current_month: Option<u32>) -> String {
686        let mut output = String::new();
687        output.push_str("│             ├");
688
689        let mut first_bar_idx = None;
690        for (idx, &date) in layout.dates.iter().enumerate() {
691            let in_month = date.year() == self.calendar.year && Some(date.month()) == current_month;
692            let prev_in_month = if idx > 0 {
693                let prev_date = layout.dates[idx - 1];
694                prev_date.year() == self.calendar.year && Some(prev_date.month()) == current_month
695            } else {
696                false
697            };
698
699            if in_month && !prev_in_month {
700                first_bar_idx = Some(idx);
701            }
702        }
703
704        if let Some(bar_idx) = first_bar_idx {
705            if bar_idx > 0 {
706                let dashes = (bar_idx - 1) * 5 + 4;
707                output.push_str(&format!("{:─<width$}┘", "", width = dashes));
708                let spaces = (DAYS_IN_WEEK - bar_idx) * 5 - 1;
709                output.push_str(&format!("{: <width$}│\n", "", width = spaces));
710            } else {
711                output.push_str("───────────────────────────────┤│\n");
712            }
713        } else {
714            output.push_str("───────────────────────────────┤│\n");
715        }
716
717        output
718    }
719
720    fn separator_before_month_to_string(
721        &self,
722        _current_layout: &WeekLayout,
723        _current_month: Option<u32>,
724        next_layout: &WeekLayout,
725    ) -> String {
726        let mut output = String::new();
727        if let Some((next_month_start_idx, _)) = next_layout.month_start_idx {
728            if next_month_start_idx == 0 {
729                output.push_str("│             ├");
730                output.push_str(&format!("{:─<width$}┤", "", width = CALENDAR_WIDTH));
731            } else {
732                output.push_str("│             │");
733                let spaces_before = (next_month_start_idx - 1) * 5 + 4;
734                output.push_str(&format!("{: <width$}┌", "", width = spaces_before));
735                let dashes = (DAYS_IN_WEEK - 1 - next_month_start_idx) * 5 + 4;
736                output.push_str(&format!("{:─<width$}┤", "", width = dashes));
737            }
738        } else {
739            output.push_str("│             │");
740            output.push_str(&format!("{: <width$}", "", width = DAYS_IN_WEEK * 4 + 3));
741        }
742
743        output.push('\n');
744        output
745    }
746
747    fn print_header(&self) {
748        println!("┌{:─<width$}┐", "", width = HEADER_WIDTH);
749        println!(
750            "│                   COMPACT CALENDAR {}        │",
751            self.calendar.year
752        );
753        println!("├{:─<width$}┤", "", width = HEADER_WIDTH);
754        print!("│              ");
755        match self.calendar.week_start {
756            WeekStart::Monday => println!("Mon  Tue  Wed  Thu  Fri  Sat  Sun │"),
757            WeekStart::Sunday => println!("Sun  Mon  Tue  Wed  Thu  Fri  Sat │"),
758        }
759    }
760
761    fn print_weeks(&self) {
762        let start_date = NaiveDate::from_ymd_opt(self.calendar.year, 1, 1).unwrap();
763        let end_date = NaiveDate::from_ymd_opt(self.calendar.year, 12, 31).unwrap();
764
765        let mut current_date = self.align_to_week_start(start_date);
766        let mut week_num = 1;
767        let mut current_month: Option<u32> = None;
768
769        let mut details_queue: Vec<(NaiveDate, DateDetail)> = Vec::new();
770        let mut shown_ranges: Vec<usize> = Vec::new();
771
772        let mut is_first_month = true;
773
774        while current_date <= end_date {
775            let layout = WeekLayout::new(current_date);
776
777            let next_week_date = current_date
778                .checked_add_signed(chrono::Duration::days(DAYS_IN_WEEK as i64))
779                .unwrap();
780            let next_layout = WeekLayout::new(next_week_date);
781
782            if let Some((_, month)) = layout.month_start_idx {
783                current_month = Some(month);
784                if is_first_month {
785                    self.print_month_border(&layout, current_month);
786                    is_first_month = false;
787                }
788            }
789
790            self.collect_details(&layout, &mut details_queue);
791
792            self.print_week_row(week_num, &layout, current_month);
793
794            self.print_annotations(&layout, &mut details_queue, &mut shown_ranges);
795
796            println!();
797
798            let is_last_week =
799                next_week_date.year() > self.calendar.year || next_week_date > end_date;
800
801            if is_last_week {
802                let mut month_boundary_idx = None;
803                for (idx, &date) in layout.dates.iter().enumerate() {
804                    if idx > 0 {
805                        let prev_date = layout.dates[idx - 1];
806                        if date.month() != prev_date.month() || date.year() != prev_date.year() {
807                            month_boundary_idx = Some(idx);
808                            break;
809                        }
810                    }
811                }
812
813                if let Some(boundary_idx) = month_boundary_idx {
814                    let dashes_before = (boundary_idx - 1) * 5 + 4;
815                    let dashes_after = (DAYS_IN_WEEK - boundary_idx) * 5 - 1;
816                    println!(
817                        "└{:─<13}┴{:─<before$}┴{:─<after$}┘",
818                        "",
819                        "",
820                        "",
821                        before = dashes_before,
822                        after = dashes_after
823                    );
824                } else {
825                    println!("└{:─<13}┴{:─<width$}┘", "", "", width = CALENDAR_WIDTH);
826                }
827            } else if let Some((idx, _)) = layout.month_start_idx {
828                if idx > 0 {
829                    self.print_separator(&layout, current_month);
830                }
831            } else if next_layout.month_start_idx.is_some()
832                && next_week_date <= end_date
833                && next_week_date.year() == self.calendar.year
834            {
835                self.print_separator_before_month(&layout, current_month, &next_layout);
836            }
837
838            current_date = next_week_date;
839            week_num += 1;
840
841            if current_date.year() > self.calendar.year {
842                break;
843            }
844        }
845    }
846
847    fn align_to_week_start(&self, date: NaiveDate) -> NaiveDate {
848        let mut aligned = date;
849        while self.calendar.get_weekday_num(aligned) != 0 {
850            aligned = aligned.pred_opt().unwrap();
851        }
852        aligned
853    }
854
855    fn get_date_color(&self, date: NaiveDate) -> Option<String> {
856        // In work mode, never color weekends
857        if self.calendar.color_mode == ColorMode::Work
858            && (date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun)
859        {
860            return None;
861        }
862
863        // Check if date has a specific color
864        if let Some(detail) = self.calendar.details.get(&date) {
865            if let Some(color) = &detail.color {
866                return Some(color.clone());
867            }
868        }
869
870        // Check if date is in a range
871        for range in &self.calendar.ranges {
872            if date >= range.start && date <= range.end {
873                return Some(range.color.clone());
874            }
875        }
876
877        None
878    }
879
880    fn print_month_border(&self, layout: &WeekLayout, _current_month: Option<u32>) {
881        if let Some((idx, _)) = layout.month_start_idx {
882            if idx > 0 {
883                print!("│             ┌");
884                let dashes_before = (idx - 1) * 5 + 4;
885                for _ in 0..dashes_before {
886                    print!("─");
887                }
888                print!("┬");
889                let dashes_after = (DAYS_IN_WEEK - idx) * 5 - 1;
890                println!("{:─<width$}┤", "", width = dashes_after);
891            }
892        }
893    }
894
895    fn collect_details(
896        &self,
897        layout: &WeekLayout,
898        details_queue: &mut Vec<(NaiveDate, DateDetail)>,
899    ) {
900        for &date in &layout.dates {
901            if let Some(detail) = self.calendar.details.get(&date) {
902                if !details_queue.iter().any(|(d, _)| d == &date) {
903                    details_queue.push((date, detail.clone()));
904                }
905            }
906        }
907    }
908
909    fn print_week_row(&self, week_num: i32, layout: &WeekLayout, _current_month: Option<u32>) {
910        let month_name = if let Some((_, month)) = layout.month_start_idx {
911            MonthInfo::from_month(month).name
912        } else {
913            ""
914        };
915
916        if !month_name.is_empty() {
917            print!("│W{:02} {:<9}", week_num, month_name);
918        } else {
919            print!("│W{:02}          ", week_num);
920        }
921
922        print!("│");
923
924        for (idx, &date) in layout.dates.iter().enumerate() {
925            let is_month_boundary = if idx > 0 {
926                let prev_date = layout.dates[idx - 1];
927                date.month() != prev_date.month() || date.year() != prev_date.year()
928            } else {
929                false
930            };
931
932            if is_month_boundary {
933                print!("│");
934            }
935
936            let today = chrono::Local::now().date_naive();
937            let is_today = date == today;
938            let is_past =
939                self.calendar.past_date_display == PastDateDisplay::Strikethrough && date < today;
940
941            let is_weekend = self.calendar.weekend_display == WeekendDisplay::Dimmed
942                && (date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun);
943
944            if let Some(color) = self.get_date_color(date) {
945                let mut style = if is_weekend {
946                    ColorCodes::get_dimmed_bg_color(&color)
947                } else {
948                    ColorCodes::get_bg_color(&color)
949                };
950
951                if ColorCodes::is_color_disabled() {
952                    print!(" {:02}", date.day());
953                } else {
954                    style = style.fg_color(ColorCodes::black_text().get_fg_color());
955
956                    let mut effects = Effects::new();
957                    if is_past {
958                        effects |= ColorCodes::strikethrough();
959                    }
960                    if is_today {
961                        effects |= ColorCodes::underline();
962                    }
963                    style = style.effects(effects);
964
965                    print!(
966                        " {}{:02}{}",
967                        style.render(),
968                        date.day(),
969                        style.render_reset()
970                    );
971                }
972            } else if ColorCodes::is_color_disabled() {
973                print!(" {:02}", date.day());
974            } else {
975                let mut style = Style::new();
976                let mut effects = Effects::new();
977
978                if is_past {
979                    effects |= ColorCodes::strikethrough();
980                }
981                if is_today {
982                    effects |= ColorCodes::underline();
983                }
984                if is_weekend {
985                    effects |= ColorCodes::dim();
986                }
987
988                style = style.effects(effects);
989
990                if effects == Effects::new() {
991                    print!(" {:02}", date.day());
992                } else {
993                    print!(
994                        " {}{:02}{}",
995                        style.render(),
996                        date.day(),
997                        style.render_reset()
998                    );
999                }
1000            }
1001
1002            if idx < 6 {
1003                let next_date = layout.dates[idx + 1];
1004                let next_is_boundary =
1005                    date.month() != next_date.month() || date.year() != next_date.year();
1006                if next_is_boundary {
1007                    print!(" ");
1008                } else {
1009                    print!("  ");
1010                }
1011            } else {
1012                print!(" ");
1013            }
1014        }
1015
1016        print!("│");
1017    }
1018
1019    fn print_annotations(
1020        &self,
1021        layout: &WeekLayout,
1022        details_queue: &mut Vec<(NaiveDate, DateDetail)>,
1023        shown_ranges: &mut Vec<usize>,
1024    ) {
1025        let week_end = layout.dates[DAYS_IN_WEEK - 1];
1026        let mut printed_range = false;
1027
1028        for (idx, range) in self.calendar.ranges.iter().enumerate() {
1029            if range.start >= layout.dates[0]
1030                && range.start <= week_end
1031                && !shown_ranges.contains(&idx)
1032            {
1033                if ColorCodes::is_color_disabled() {
1034                    if let Some(desc) = &range.description {
1035                        print!(
1036                            "{} to {} - {}",
1037                            range.start.format("%m/%d"),
1038                            range.end.format("%m/%d"),
1039                            desc
1040                        );
1041                    } else {
1042                        print!(
1043                            "{} to {}",
1044                            range.start.format("%m/%d"),
1045                            range.end.format("%m/%d")
1046                        );
1047                    }
1048                } else {
1049                    let style = ColorCodes::get_bg_color(&range.color)
1050                        .fg_color(ColorCodes::black_text().get_fg_color());
1051
1052                    if let Some(desc) = &range.description {
1053                        print!(
1054                            "{}{} to {} - {}{}",
1055                            style.render(),
1056                            range.start.format("%m/%d"),
1057                            range.end.format("%m/%d"),
1058                            desc,
1059                            style.render_reset()
1060                        );
1061                    } else {
1062                        print!(
1063                            "{}{} to {}{}",
1064                            style.render(),
1065                            range.start.format("%m/%d"),
1066                            range.end.format("%m/%d"),
1067                            style.render_reset()
1068                        );
1069                    }
1070                }
1071                shown_ranges.push(idx);
1072                printed_range = true;
1073                break;
1074            }
1075        }
1076
1077        if !printed_range && !details_queue.is_empty() {
1078            let (detail_date, detail) = &details_queue[0];
1079            if ColorCodes::is_color_disabled() {
1080                print!("{} - {}", detail_date.format("%m/%d"), detail.description);
1081            } else if let Some(color) = &detail.color {
1082                let style = ColorCodes::get_bg_color(color)
1083                    .fg_color(ColorCodes::black_text().get_fg_color());
1084                print!(
1085                    "{}{} - {}{}",
1086                    style.render(),
1087                    detail_date.format("%m/%d"),
1088                    detail.description,
1089                    style.render_reset()
1090                );
1091            } else {
1092                print!("{} - {}", detail_date.format("%m/%d"), detail.description);
1093            }
1094            details_queue.remove(0);
1095        }
1096    }
1097
1098    fn print_separator(&self, layout: &WeekLayout, current_month: Option<u32>) {
1099        print!("│             ├");
1100        let mut first_bar_idx = None;
1101        for (idx, &date) in layout.dates.iter().enumerate() {
1102            let in_month = date.year() == self.calendar.year && Some(date.month()) == current_month;
1103            let prev_in_month = if idx > 0 {
1104                let prev_date = layout.dates[idx - 1];
1105                prev_date.year() == self.calendar.year && Some(prev_date.month()) == current_month
1106            } else {
1107                false
1108            };
1109
1110            if in_month && !prev_in_month {
1111                first_bar_idx = Some(idx);
1112            }
1113        }
1114
1115        if let Some(bar_idx) = first_bar_idx {
1116            if bar_idx > 0 {
1117                let dashes = (bar_idx - 1) * 5 + 4;
1118                print!("{:─<width$}┘", "", width = dashes);
1119                let spaces = (DAYS_IN_WEEK - bar_idx) * 5 - 1;
1120                println!("{: <width$}│", "", width = spaces);
1121            } else {
1122                println!("{:─<31}┤│", "");
1123            }
1124        } else {
1125            println!("{:─<31}┤│", "");
1126        }
1127    }
1128
1129    fn print_separator_before_month(
1130        &self,
1131        _current_layout: &WeekLayout,
1132        _current_month: Option<u32>,
1133        next_layout: &WeekLayout,
1134    ) {
1135        if let Some((next_month_start_idx, _)) = next_layout.month_start_idx {
1136            if next_month_start_idx == 0 {
1137                print!("│             ├");
1138                print!("{:─<width$}┤", "", width = CALENDAR_WIDTH);
1139            } else {
1140                print!("│             │");
1141                let spaces_before = (next_month_start_idx - 1) * 5 + 4;
1142                print!("{: <width$}┌", "", width = spaces_before);
1143                let dashes = (DAYS_IN_WEEK - 1 - next_month_start_idx) * 5 + 4;
1144                print!("{:─<width$}┤", "", width = dashes);
1145            }
1146        } else {
1147            print!("│             │");
1148            print!("{: <width$}", "", width = DAYS_IN_WEEK * 4 + 3);
1149        }
1150
1151        println!();
1152    }
1153}