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 if self.calendar.color_mode == ColorMode::Work
858 && (date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun)
859 {
860 return None;
861 }
862
863 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 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}