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, Copy)]
8pub struct ColorValue {
9    pub normal: RgbColor,
10    pub dimmed: RgbColor,
11}
12
13impl ColorValue {
14    pub const fn new(normal: RgbColor, dimmed: RgbColor) -> Self {
15        Self { normal, dimmed }
16    }
17
18    pub fn get_normal_style(&self) -> Style {
19        Style::new().bg_color(Some(Color::Rgb(self.normal)))
20    }
21
22    pub fn get_dimmed_style(&self) -> Style {
23        Style::new().bg_color(Some(Color::Rgb(self.dimmed)))
24    }
25}
26
27#[derive(Debug, Clone)]
28pub struct ColorPalette {
29    colors_enabled: bool,
30}
31
32impl Default for ColorPalette {
33    fn default() -> Self {
34        Self {
35            colors_enabled: !Self::is_color_disabled(),
36        }
37    }
38}
39
40impl ColorPalette {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    fn is_color_disabled() -> bool {
46        std::env::var("NO_COLOR").is_ok()
47    }
48
49    pub fn are_colors_enabled(&self) -> bool {
50        self.colors_enabled
51    }
52
53    pub fn get_color_value(name: &str) -> Option<ColorValue> {
54        match name {
55            "orange" => Some(ColorValue::new(
56                RgbColor(255, 143, 64),
57                RgbColor(178, 100, 45),
58            )),
59            "yellow" => Some(ColorValue::new(
60                RgbColor(230, 180, 80),
61                RgbColor(161, 126, 56),
62            )),
63            "green" => Some(ColorValue::new(
64                RgbColor(170, 217, 76),
65                RgbColor(119, 152, 53),
66            )),
67            "blue" => Some(ColorValue::new(
68                RgbColor(89, 194, 255),
69                RgbColor(62, 136, 179),
70            )),
71            "purple" => Some(ColorValue::new(
72                RgbColor(210, 166, 255),
73                RgbColor(147, 116, 179),
74            )),
75            "red" => Some(ColorValue::new(
76                RgbColor(240, 113, 120),
77                RgbColor(168, 79, 84),
78            )),
79            "cyan" => Some(ColorValue::new(
80                RgbColor(149, 230, 203),
81                RgbColor(104, 161, 142),
82            )),
83            "gray" => Some(ColorValue::new(RgbColor(95, 99, 110), RgbColor(67, 69, 77))),
84            "light_orange" => Some(ColorValue::new(
85                RgbColor(255, 180, 84),
86                RgbColor(179, 126, 59),
87            )),
88            "light_yellow" => Some(ColorValue::new(
89                RgbColor(249, 175, 79),
90                RgbColor(174, 123, 55),
91            )),
92            "light_green" => Some(ColorValue::new(
93                RgbColor(145, 179, 98),
94                RgbColor(102, 125, 69),
95            )),
96            "light_blue" => Some(ColorValue::new(
97                RgbColor(83, 189, 250),
98                RgbColor(58, 132, 175),
99            )),
100            "light_purple" => Some(ColorValue::new(
101                RgbColor(210, 166, 255),
102                RgbColor(147, 116, 179),
103            )),
104            "light_red" => Some(ColorValue::new(
105                RgbColor(234, 108, 115),
106                RgbColor(164, 76, 81),
107            )),
108            "light_cyan" => Some(ColorValue::new(
109                RgbColor(144, 225, 198),
110                RgbColor(101, 158, 139),
111            )),
112            _ => None,
113        }
114    }
115
116    pub fn get_style(&self, color_name: &str, dimmed: bool) -> Style {
117        if !self.colors_enabled {
118            return Style::new();
119        }
120
121        if let Some(color_value) = Self::get_color_value(color_name) {
122            if dimmed {
123                color_value.get_dimmed_style()
124            } else {
125                color_value.get_normal_style()
126            }
127        } else {
128            Style::new()
129        }
130    }
131
132    pub fn black_text() -> Style {
133        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Black)))
134    }
135}
136
137struct ColorCodes;
138
139impl ColorCodes {
140    fn is_color_disabled() -> bool {
141        std::env::var("NO_COLOR").is_ok()
142    }
143
144    fn get_bg_color(color: &str) -> Style {
145        if Self::is_color_disabled() {
146            return Style::new();
147        }
148        let palette = ColorPalette::new();
149        palette.get_style(color, false)
150    }
151
152    fn get_dimmed_bg_color(color: &str) -> Style {
153        if Self::is_color_disabled() {
154            return Style::new();
155        }
156        let palette = ColorPalette::new();
157        palette.get_style(color, true)
158    }
159
160    fn black_text() -> Style {
161        ColorPalette::black_text()
162    }
163
164    fn underline() -> Effects {
165        Effects::UNDERLINE
166    }
167
168    fn strikethrough() -> Effects {
169        Effects::STRIKETHROUGH
170    }
171
172    fn dim() -> Effects {
173        Effects::DIMMED
174    }
175}
176
177const DAYS_IN_WEEK: usize = 7;
178const CALENDAR_WIDTH: usize = 34;
179const HEADER_WIDTH: usize = 48;
180
181pub struct CalendarRenderer<'a> {
182    calendar: &'a Calendar,
183}
184
185impl<'a> CalendarRenderer<'a> {
186    pub fn new(calendar: &'a Calendar) -> Self {
187        CalendarRenderer { calendar }
188    }
189
190    pub fn render(&self) {
191        self.print_header();
192        self.print_weeks();
193        println!();
194    }
195
196    pub fn render_to_string(&self) -> String {
197        let mut output = String::new();
198
199        let prev_no_color = std::env::var("NO_COLOR").ok();
200        std::env::set_var("NO_COLOR", "1");
201
202        output.push_str(&self.header_to_string());
203        output.push_str(&self.weeks_to_string());
204        output.push('\n');
205
206        match prev_no_color {
207            Some(val) => std::env::set_var("NO_COLOR", val),
208            None => std::env::remove_var("NO_COLOR"),
209        }
210
211        output
212    }
213
214    /// Check if a week should be rendered based on month filter
215    fn should_render_week(&self, layout: &WeekLayout) -> bool {
216        // Include week if ANY of its 7 days fall within the filtered month range
217        layout.dates.iter().any(|date| {
218            if date.year() != self.calendar.year {
219                false
220            } else {
221                self.calendar
222                    .month_filter
223                    .should_display_month(date.month(), self.calendar.year)
224            }
225        })
226    }
227
228    /// Get the filtered date range based on month filter
229    fn get_filtered_date_range(&self) -> (NaiveDate, NaiveDate) {
230        self.calendar
231            .month_filter
232            .get_date_range(self.calendar.year)
233    }
234
235    fn header_to_string(&self) -> String {
236        let mut output = String::new();
237        output.push_str(&format!("┌{:─<width$}┐\n", "", width = HEADER_WIDTH));
238
239        // Center the title
240        let title = format!("COMPACT CALENDAR {}", self.calendar.year);
241        output.push_str(&format!("│{:^width$}│\n", title, width = HEADER_WIDTH));
242
243        output.push_str(&format!("├{:─<width$}┤\n", "", width = HEADER_WIDTH));
244        output.push_str("│              ");
245        match self.calendar.week_start {
246            WeekStart::Monday => output.push_str("Mon  Tue  Wed  Thu  Fri  Sat  Sun │\n"),
247            WeekStart::Sunday => output.push_str("Sun  Mon  Tue  Wed  Thu  Fri  Sat │\n"),
248        }
249        output
250    }
251
252    fn weeks_to_string(&self) -> String {
253        let mut output = String::new();
254        let (start_date, end_date) = self.get_filtered_date_range();
255
256        let mut current_date = self.align_to_week_start(start_date);
257        let mut week_num = 1;
258        let mut current_month: Option<u32> = None;
259
260        let mut details_queue: Vec<(NaiveDate, DateDetail)> = Vec::new();
261        let mut shown_ranges: Vec<usize> = Vec::new();
262
263        let mut is_first_month = true;
264
265        while current_date <= end_date {
266            let layout = WeekLayout::new(current_date);
267
268            // Skip weeks that don't contain filtered months
269            if !self.should_render_week(&layout) {
270                current_date = current_date
271                    .checked_add_signed(chrono::Duration::days(DAYS_IN_WEEK as i64))
272                    .unwrap();
273                continue;
274            }
275
276            let next_week_date = current_date
277                .checked_add_signed(chrono::Duration::days(DAYS_IN_WEEK as i64))
278                .unwrap();
279            let next_layout = WeekLayout::new(next_week_date);
280
281            if let Some((_, month)) = layout.month_start_idx {
282                current_month = Some(month);
283                if is_first_month {
284                    output.push_str(&self.month_border_to_string(&layout, current_month));
285                    is_first_month = false;
286                }
287            }
288
289            self.collect_details(&layout, &mut details_queue);
290
291            output.push_str(&self.week_row_to_string(week_num, &layout, current_month));
292
293            output.push_str(&self.annotations_to_string(
294                &layout,
295                &mut details_queue,
296                &mut shown_ranges,
297            ));
298
299            output.push('\n');
300
301            let is_last_week =
302                next_week_date.year() > self.calendar.year || next_week_date > end_date;
303
304            if is_last_week {
305                let mut month_boundary_idx = None;
306                for (idx, &date) in layout.dates.iter().enumerate() {
307                    if idx > 0 {
308                        let prev_date = layout.dates[idx - 1];
309                        if date.month() != prev_date.month() || date.year() != prev_date.year() {
310                            month_boundary_idx = Some(idx);
311                            break;
312                        }
313                    }
314                }
315
316                if let Some(boundary_idx) = month_boundary_idx {
317                    let dashes_before = (boundary_idx - 1) * 5 + 4;
318                    let dashes_after = (DAYS_IN_WEEK - boundary_idx) * 5 - 1;
319                    output.push_str(&format!(
320                        "└{:─<13}┴{:─<before$}┴{:─<after$}┘\n",
321                        "",
322                        "",
323                        "",
324                        before = dashes_before,
325                        after = dashes_after
326                    ));
327                } else {
328                    output.push_str(&format!(
329                        "└{:─<13}┴{:─<width$}┘\n",
330                        "",
331                        "",
332                        width = CALENDAR_WIDTH
333                    ));
334                }
335            } else if let Some((idx, _)) = layout.month_start_idx {
336                if idx > 0 {
337                    output.push_str(&self.separator_to_string(&layout, current_month));
338                }
339            } else if next_layout.month_start_idx.is_some()
340                && next_week_date <= end_date
341                && next_week_date.year() == self.calendar.year
342            {
343                output.push_str(&self.separator_before_month_to_string(
344                    &layout,
345                    current_month,
346                    &next_layout,
347                ));
348            }
349
350            current_date = next_week_date;
351            week_num += 1;
352
353            if current_date.year() > self.calendar.year {
354                break;
355            }
356        }
357
358        output
359    }
360
361    fn month_border_to_string(&self, layout: &WeekLayout, _current_month: Option<u32>) -> String {
362        let mut output = String::new();
363        if let Some((idx, _)) = layout.month_start_idx {
364            if idx > 0 {
365                output.push_str("│             ┌");
366                let dashes_before = (idx - 1) * 5 + 4;
367                for _ in 0..dashes_before {
368                    output.push('─');
369                }
370                output.push('┬');
371                let dashes_after = (DAYS_IN_WEEK - idx) * 5 - 1;
372                output.push_str(&format!("{:─<width$}┤\n", "", width = dashes_after));
373            }
374        }
375        output
376    }
377
378    fn week_row_to_string(
379        &self,
380        week_num: i32,
381        layout: &WeekLayout,
382        _current_month: Option<u32>,
383    ) -> String {
384        let mut output = String::new();
385        let month_name = if let Some((_, month)) = layout.month_start_idx {
386            MonthInfo::from_month(month).name
387        } else {
388            ""
389        };
390
391        if !month_name.is_empty() {
392            output.push_str(&format!("│W{:02} {:<9}", week_num, month_name));
393        } else {
394            output.push_str(&format!("│W{:02}          ", week_num));
395        }
396
397        output.push('│');
398
399        for (idx, &date) in layout.dates.iter().enumerate() {
400            let is_month_boundary = if idx > 0 {
401                let prev_date = layout.dates[idx - 1];
402                date.month() != prev_date.month() || date.year() != prev_date.year()
403            } else {
404                false
405            };
406
407            if is_month_boundary {
408                output.push('│');
409            }
410
411            output.push_str(&format!(" {:02}", date.day()));
412
413            if idx < 6 {
414                let next_date = layout.dates[idx + 1];
415                let next_is_boundary =
416                    date.month() != next_date.month() || date.year() != next_date.year();
417                if next_is_boundary {
418                    output.push(' ');
419                } else {
420                    output.push_str("  ");
421                }
422            } else {
423                output.push(' ');
424            }
425        }
426
427        output.push('│');
428        output
429    }
430
431    fn annotations_to_string(
432        &self,
433        layout: &WeekLayout,
434        details_queue: &mut Vec<(NaiveDate, DateDetail)>,
435        shown_ranges: &mut Vec<usize>,
436    ) -> String {
437        let mut output = String::new();
438        let week_start = layout.dates[0];
439        let week_end = layout.dates[DAYS_IN_WEEK - 1];
440        let mut annotations = Vec::new();
441
442        // Collect all details that occur in this week
443        let mut details_to_remove = Vec::new();
444        for (i, (detail_date, detail)) in details_queue.iter().enumerate() {
445            if *detail_date >= week_start && *detail_date <= week_end {
446                annotations.push(format!(
447                    "{} - {}",
448                    detail_date.format("%m/%d"),
449                    detail.description
450                ));
451                details_to_remove.push(i);
452            }
453        }
454        // Remove details in reverse order to maintain indices
455        for &i in details_to_remove.iter().rev() {
456            details_queue.remove(i);
457        }
458
459        // Collect all ranges that overlap with this week
460        for (idx, range) in self.calendar.ranges.iter().enumerate() {
461            if !shown_ranges.contains(&idx) && range.start <= week_end && range.end >= week_start {
462                if let Some(desc) = &range.description {
463                    annotations.push(format!(
464                        "{} to {} - {}",
465                        range.start.format("%m/%d"),
466                        range.end.format("%m/%d"),
467                        desc
468                    ));
469                } else {
470                    annotations.push(format!(
471                        "{} to {}",
472                        range.start.format("%m/%d"),
473                        range.end.format("%m/%d")
474                    ));
475                }
476                shown_ranges.push(idx);
477            }
478        }
479
480        // Join all annotations with commas
481        output.push_str(&annotations.join(", "));
482
483        output
484    }
485
486    fn separator_to_string(&self, layout: &WeekLayout, current_month: Option<u32>) -> String {
487        let mut output = String::new();
488        output.push_str("│             ├");
489
490        let mut first_bar_idx = None;
491        for (idx, &date) in layout.dates.iter().enumerate() {
492            let in_month = date.year() == self.calendar.year && Some(date.month()) == current_month;
493            let prev_in_month = if idx > 0 {
494                let prev_date = layout.dates[idx - 1];
495                prev_date.year() == self.calendar.year && Some(prev_date.month()) == current_month
496            } else {
497                false
498            };
499
500            if in_month && !prev_in_month {
501                first_bar_idx = Some(idx);
502            }
503        }
504
505        if let Some(bar_idx) = first_bar_idx {
506            if bar_idx > 0 {
507                let dashes = (bar_idx - 1) * 5 + 4;
508                output.push_str(&format!("{:─<width$}┘", "", width = dashes));
509                let spaces = (DAYS_IN_WEEK - bar_idx) * 5 - 1;
510                output.push_str(&format!("{: <width$}│\n", "", width = spaces));
511            } else {
512                output.push_str("───────────────────────────────┤│\n");
513            }
514        } else {
515            output.push_str("───────────────────────────────┤│\n");
516        }
517
518        output
519    }
520
521    fn separator_before_month_to_string(
522        &self,
523        _current_layout: &WeekLayout,
524        _current_month: Option<u32>,
525        next_layout: &WeekLayout,
526    ) -> String {
527        let mut output = String::new();
528        if let Some((next_month_start_idx, _)) = next_layout.month_start_idx {
529            if next_month_start_idx == 0 {
530                output.push_str("│             ├");
531                output.push_str(&format!("{:─<width$}┤", "", width = CALENDAR_WIDTH));
532            } else {
533                output.push_str("│             │");
534                let spaces_before = (next_month_start_idx - 1) * 5 + 4;
535                output.push_str(&format!("{: <width$}┌", "", width = spaces_before));
536                let dashes = (DAYS_IN_WEEK - 1 - next_month_start_idx) * 5 + 4;
537                output.push_str(&format!("{:─<width$}┤", "", width = dashes));
538            }
539        } else {
540            output.push_str("│             │");
541            output.push_str(&format!("{: <width$}", "", width = DAYS_IN_WEEK * 4 + 3));
542        }
543
544        output.push('\n');
545        output
546    }
547
548    fn print_header(&self) {
549        print!("{}", self.header_to_string());
550    }
551
552    fn print_weeks(&self) {
553        let (start_date, end_date) = self.get_filtered_date_range();
554
555        let mut current_date = self.align_to_week_start(start_date);
556        let mut week_num = 1;
557        let mut current_month: Option<u32> = None;
558
559        let mut details_queue: Vec<(NaiveDate, DateDetail)> = Vec::new();
560        let mut shown_ranges: Vec<usize> = Vec::new();
561
562        let mut is_first_month = true;
563
564        while current_date <= end_date {
565            let layout = WeekLayout::new(current_date);
566
567            // Skip weeks that don't contain filtered months
568            if !self.should_render_week(&layout) {
569                current_date = current_date
570                    .checked_add_signed(chrono::Duration::days(DAYS_IN_WEEK as i64))
571                    .unwrap();
572                continue;
573            }
574
575            let next_week_date = current_date
576                .checked_add_signed(chrono::Duration::days(DAYS_IN_WEEK as i64))
577                .unwrap();
578            let next_layout = WeekLayout::new(next_week_date);
579
580            if let Some((_, month)) = layout.month_start_idx {
581                current_month = Some(month);
582                if is_first_month {
583                    self.print_month_border(&layout, current_month);
584                    is_first_month = false;
585                }
586            }
587
588            self.collect_details(&layout, &mut details_queue);
589
590            self.print_week_row(week_num, &layout, current_month);
591
592            self.print_annotations(&layout, &mut details_queue, &mut shown_ranges);
593
594            println!();
595
596            let is_last_week =
597                next_week_date.year() > self.calendar.year || next_week_date > end_date;
598
599            if is_last_week {
600                let mut month_boundary_idx = None;
601                for (idx, &date) in layout.dates.iter().enumerate() {
602                    if idx > 0 {
603                        let prev_date = layout.dates[idx - 1];
604                        if date.month() != prev_date.month() || date.year() != prev_date.year() {
605                            month_boundary_idx = Some(idx);
606                            break;
607                        }
608                    }
609                }
610
611                if let Some(boundary_idx) = month_boundary_idx {
612                    let dashes_before = (boundary_idx - 1) * 5 + 4;
613                    let dashes_after = (DAYS_IN_WEEK - boundary_idx) * 5 - 1;
614                    println!(
615                        "└{:─<13}┴{:─<before$}┴{:─<after$}┘",
616                        "",
617                        "",
618                        "",
619                        before = dashes_before,
620                        after = dashes_after
621                    );
622                } else {
623                    println!("└{:─<13}┴{:─<width$}┘", "", "", width = CALENDAR_WIDTH);
624                }
625            } else if let Some((idx, _)) = layout.month_start_idx {
626                if idx > 0 {
627                    self.print_separator(&layout, current_month);
628                }
629            } else if next_layout.month_start_idx.is_some()
630                && next_week_date <= end_date
631                && next_week_date.year() == self.calendar.year
632            {
633                self.print_separator_before_month(&layout, current_month, &next_layout);
634            }
635
636            current_date = next_week_date;
637            week_num += 1;
638
639            if current_date.year() > self.calendar.year {
640                break;
641            }
642        }
643    }
644
645    fn align_to_week_start(&self, date: NaiveDate) -> NaiveDate {
646        let mut aligned = date;
647        while self.calendar.get_weekday_num(aligned) != 0 {
648            aligned = aligned.pred_opt().unwrap();
649        }
650        aligned
651    }
652
653    fn get_date_color(&self, date: NaiveDate) -> Option<String> {
654        // In work mode, never color weekends
655        if self.calendar.color_mode == ColorMode::Work
656            && (date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun)
657        {
658            return None;
659        }
660
661        // Check if date has a specific color
662        if let Some(detail) = self.calendar.details.get(&date) {
663            if let Some(color) = &detail.color {
664                return Some(color.clone());
665            }
666        }
667
668        // Check if date is in a range
669        for range in &self.calendar.ranges {
670            if date >= range.start && date <= range.end {
671                return Some(range.color.clone());
672            }
673        }
674
675        None
676    }
677
678    fn print_month_border(&self, layout: &WeekLayout, current_month: Option<u32>) {
679        print!("{}", self.month_border_to_string(layout, current_month));
680    }
681
682    fn collect_details(
683        &self,
684        layout: &WeekLayout,
685        details_queue: &mut Vec<(NaiveDate, DateDetail)>,
686    ) {
687        for &date in &layout.dates {
688            if let Some(detail) = self.calendar.details.get(&date) {
689                if !details_queue.iter().any(|(d, _)| d == &date) {
690                    details_queue.push((date, detail.clone()));
691                }
692            }
693        }
694    }
695
696    fn print_week_row(&self, week_num: i32, layout: &WeekLayout, _current_month: Option<u32>) {
697        let month_name = if let Some((_, month)) = layout.month_start_idx {
698            MonthInfo::from_month(month).name
699        } else {
700            ""
701        };
702
703        if !month_name.is_empty() {
704            print!("│W{:02} {:<9}", week_num, month_name);
705        } else {
706            print!("│W{:02}          ", week_num);
707        }
708
709        print!("│");
710
711        for (idx, &date) in layout.dates.iter().enumerate() {
712            let is_month_boundary = if idx > 0 {
713                let prev_date = layout.dates[idx - 1];
714                date.month() != prev_date.month() || date.year() != prev_date.year()
715            } else {
716                false
717            };
718
719            if is_month_boundary {
720                print!("│");
721            }
722
723            let today = chrono::Local::now().date_naive();
724            let is_today = date == today;
725            let is_past =
726                self.calendar.past_date_display == PastDateDisplay::Strikethrough && date < today;
727
728            let is_weekend = self.calendar.weekend_display == WeekendDisplay::Dimmed
729                && (date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun);
730
731            if let Some(color) = self.get_date_color(date) {
732                let mut style = if is_weekend {
733                    ColorCodes::get_dimmed_bg_color(&color)
734                } else {
735                    ColorCodes::get_bg_color(&color)
736                };
737
738                if ColorCodes::is_color_disabled() {
739                    print!(" {:02}", date.day());
740                } else {
741                    style = style.fg_color(ColorCodes::black_text().get_fg_color());
742
743                    let mut effects = Effects::new();
744                    if is_past {
745                        effects |= ColorCodes::strikethrough();
746                    }
747                    if is_today {
748                        effects |= ColorCodes::underline();
749                    }
750                    style = style.effects(effects);
751
752                    print!(
753                        " {}{:02}{}",
754                        style.render(),
755                        date.day(),
756                        style.render_reset()
757                    );
758                }
759            } else if ColorCodes::is_color_disabled() {
760                print!(" {:02}", date.day());
761            } else {
762                let mut style = Style::new();
763                let mut effects = Effects::new();
764
765                if is_past {
766                    effects |= ColorCodes::strikethrough();
767                }
768                if is_today {
769                    effects |= ColorCodes::underline();
770                }
771                if is_weekend {
772                    effects |= ColorCodes::dim();
773                }
774
775                style = style.effects(effects);
776
777                if effects == Effects::new() {
778                    print!(" {:02}", date.day());
779                } else {
780                    print!(
781                        " {}{:02}{}",
782                        style.render(),
783                        date.day(),
784                        style.render_reset()
785                    );
786                }
787            }
788
789            if idx < 6 {
790                let next_date = layout.dates[idx + 1];
791                let next_is_boundary =
792                    date.month() != next_date.month() || date.year() != next_date.year();
793                if next_is_boundary {
794                    print!(" ");
795                } else {
796                    print!("  ");
797                }
798            } else {
799                print!(" ");
800            }
801        }
802
803        print!("│");
804    }
805
806    fn print_annotations(
807        &self,
808        layout: &WeekLayout,
809        details_queue: &mut Vec<(NaiveDate, DateDetail)>,
810        shown_ranges: &mut Vec<usize>,
811    ) {
812        let week_start = layout.dates[0];
813        let week_end = layout.dates[DAYS_IN_WEEK - 1];
814        let mut first = true;
815
816        // Collect and print all details that occur in this week
817        let mut details_to_remove = Vec::new();
818        for (i, (detail_date, detail)) in details_queue.iter().enumerate() {
819            if *detail_date >= week_start && *detail_date <= week_end {
820                if !first {
821                    print!(", ");
822                }
823                first = false;
824
825                if ColorCodes::is_color_disabled() {
826                    print!("{} - {}", detail_date.format("%m/%d"), detail.description);
827                } else if let Some(color) = &detail.color {
828                    let style = ColorCodes::get_bg_color(color)
829                        .fg_color(ColorCodes::black_text().get_fg_color());
830                    print!(
831                        "{}{} - {}{}",
832                        style.render(),
833                        detail_date.format("%m/%d"),
834                        detail.description,
835                        style.render_reset()
836                    );
837                } else {
838                    print!("{} - {}", detail_date.format("%m/%d"), detail.description);
839                }
840                details_to_remove.push(i);
841            }
842        }
843        // Remove details in reverse order to maintain indices
844        for &i in details_to_remove.iter().rev() {
845            details_queue.remove(i);
846        }
847
848        // Collect and print all ranges that overlap with this week
849        for (idx, range) in self.calendar.ranges.iter().enumerate() {
850            if !shown_ranges.contains(&idx) && range.start <= week_end && range.end >= week_start {
851                if !first {
852                    print!(", ");
853                }
854                first = false;
855
856                if ColorCodes::is_color_disabled() {
857                    if let Some(desc) = &range.description {
858                        print!(
859                            "{} to {} - {}",
860                            range.start.format("%m/%d"),
861                            range.end.format("%m/%d"),
862                            desc
863                        );
864                    } else {
865                        print!(
866                            "{} to {}",
867                            range.start.format("%m/%d"),
868                            range.end.format("%m/%d")
869                        );
870                    }
871                } else {
872                    let style = ColorCodes::get_bg_color(&range.color)
873                        .fg_color(ColorCodes::black_text().get_fg_color());
874
875                    if let Some(desc) = &range.description {
876                        print!(
877                            "{}{} to {} - {}{}",
878                            style.render(),
879                            range.start.format("%m/%d"),
880                            range.end.format("%m/%d"),
881                            desc,
882                            style.render_reset()
883                        );
884                    } else {
885                        print!(
886                            "{}{} to {}{}",
887                            style.render(),
888                            range.start.format("%m/%d"),
889                            range.end.format("%m/%d"),
890                            style.render_reset()
891                        );
892                    }
893                }
894                shown_ranges.push(idx);
895            }
896        }
897    }
898
899    fn print_separator(&self, layout: &WeekLayout, current_month: Option<u32>) {
900        print!("{}", self.separator_to_string(layout, current_month));
901    }
902
903    fn print_separator_before_month(
904        &self,
905        current_layout: &WeekLayout,
906        current_month: Option<u32>,
907        next_layout: &WeekLayout,
908    ) {
909        print!(
910            "{}",
911            self.separator_before_month_to_string(current_layout, current_month, next_layout)
912        );
913    }
914}