1use std::fmt::Write as _;
9
10use colored::Colorize;
11
12use crate::calendar::MonthGrid;
13use crate::persian::{
14 self, MONTH_NAMES, MONTH_NAMES_ROMANIZED, WEEKDAY_NAMES_EN, WEEKDAY_NAMES_FA,
15};
16
17const LRE: &str = "\u{202a}";
19const PDF: &str = "\u{202c}";
21
22const MONTH_WIDTH: usize = 20;
24const MONTH_WIDTH_JULIAN: usize = 27;
26const MONTHS_PER_ROW: usize = 3;
28const GAP: &str = " ";
30
31pub fn print_month(grid: &MonthGrid, today: Option<(i32, u32, u32)>, julian: bool, persian: bool) {
33 let lines = render_month(grid, today, julian, persian);
34 for line in lines {
35 println!("{}", line.trim_end());
36 }
37}
38
39pub fn print_months(
41 grids: &[MonthGrid],
42 today: Option<(i32, u32, u32)>,
43 julian: bool,
44 persian: bool,
45) {
46 let width = if julian {
47 MONTH_WIDTH_JULIAN
48 } else {
49 MONTH_WIDTH
50 };
51
52 for chunk in grids.chunks(MONTHS_PER_ROW) {
53 let rendered: Vec<Vec<String>> = chunk
54 .iter()
55 .map(|g| render_month(g, today, julian, persian))
56 .collect();
57
58 let max_lines = rendered.iter().map(|r| r.len()).max().unwrap_or(0);
59
60 for line_idx in 0..max_lines {
61 let mut combined = String::new();
62 for (i, month_lines) in rendered.iter().enumerate() {
63 if i > 0 {
64 combined.push_str(GAP);
65 }
66 if line_idx < month_lines.len() {
67 combined.push_str(&month_lines[line_idx]);
68 } else {
69 combined.push_str(&" ".repeat(width));
70 }
71 }
72 println!("{}", combined.trim_end());
73 }
74 println!();
75 }
76}
77
78pub fn print_year(year: i32, today: Option<(i32, u32, u32)>, julian: bool, persian: bool) {
80 let width = if julian {
81 MONTH_WIDTH_JULIAN
82 } else {
83 MONTH_WIDTH
84 };
85 let total_width = width * MONTHS_PER_ROW + GAP.len() * (MONTHS_PER_ROW - 1);
86
87 let year_str = if persian {
88 persian::to_persian_digits(&year.to_string())
89 } else {
90 year.to_string()
91 };
92 let padding = (total_width.saturating_sub(year_str.len())) / 2;
93 if persian {
94 println!("{}{}{}{}", LRE, " ".repeat(padding), year_str.bold(), PDF);
95 } else {
96 println!("{}{}", " ".repeat(padding), year_str.bold());
97 }
98 println!();
99
100 let grids: Vec<MonthGrid> = (1..=12).map(|m| MonthGrid::new(year, m)).collect();
101 print_months(&grids, today, julian, persian);
102}
103
104fn render_month(
106 grid: &MonthGrid,
107 today: Option<(i32, u32, u32)>,
108 julian: bool,
109 persian: bool,
110) -> Vec<String> {
111 let cell_width = if julian { 4 } else { 3 };
112 let width = if julian {
113 MONTH_WIDTH_JULIAN
114 } else {
115 MONTH_WIDTH
116 };
117
118 let weekday_names = if persian {
119 &WEEKDAY_NAMES_FA
120 } else {
121 &WEEKDAY_NAMES_EN
122 };
123
124 let is_today = |day: u32| -> bool {
125 if let Some((ty, tm, td)) = today {
126 ty == grid.year && tm == grid.month && td == day
127 } else {
128 false
129 }
130 };
131
132 let mut lines = Vec::new();
133
134 let month_name = if persian {
136 MONTH_NAMES[grid.month as usize - 1].to_string()
137 } else {
138 capitalize(MONTH_NAMES_ROMANIZED[grid.month as usize - 1])
139 };
140 let year_str = if persian {
141 persian::to_persian_digits(&grid.year.to_string())
142 } else {
143 grid.year.to_string()
144 };
145 let title = format!("{month_name} {year_str}");
146 let dw = display_width(&title);
147 let padding = width.saturating_sub(dw) / 2;
148 lines.push(format!("{}{}", " ".repeat(padding), title.bold()));
149
150 let header: String = weekday_names
152 .iter()
153 .enumerate()
154 .map(|(i, name)| {
155 let w: usize = if i == 0 { cell_width - 1 } else { cell_width };
156 let vis_len = display_width(name);
157 let pad = w.saturating_sub(vis_len);
158 format!("{}{}", " ".repeat(pad), name)
159 })
160 .collect();
161 lines.push(header);
162
163 let rows = grid.row_count();
165 for r in 0..rows {
166 let mut row_str = String::new();
167 for c in 0..7 {
168 let w = if c == 0 { cell_width - 1 } else { cell_width };
169 match grid.grid[r][c] {
170 Some(day) => {
171 let num = if julian { grid.day_of_year(day) } else { day };
172 let s = if persian {
173 persian::to_persian_digits(&num.to_string())
174 } else {
175 num.to_string()
176 };
177 if is_today(day) {
178 let formatted = format!("{s:>w$}");
179 write!(row_str, "{}", formatted.reversed()).unwrap();
180 } else {
181 write!(row_str, "{s:>w$}").unwrap();
182 }
183 }
184 None => {
185 row_str.push_str(&" ".repeat(w));
186 }
187 }
188 }
189 lines.push(row_str);
190 }
191
192 while lines.len() < 8 {
194 lines.push(String::new());
195 }
196
197 for line in &mut lines {
199 let current = visible_width(line);
200 if current < width {
201 line.push_str(&" ".repeat(width - current));
202 }
203 }
204
205 if persian {
207 for line in &mut lines {
208 *line = format!("{LRE}{line}{PDF}");
209 }
210 }
211
212 lines
213}
214
215fn capitalize(s: &str) -> String {
217 let mut chars = s.chars();
218 match chars.next() {
219 None => String::new(),
220 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
221 }
222}
223
224fn display_width(s: &str) -> usize {
226 s.chars()
227 .filter(|c| !matches!(c, '\u{200e}' | '\u{200f}' | '\u{202a}' | '\u{202c}'))
228 .count()
229}
230
231fn visible_width(s: &str) -> usize {
234 let mut width = 0;
235 let mut in_escape = false;
236 for c in s.chars() {
237 if in_escape {
238 if c == 'm' {
239 in_escape = false;
240 }
241 } else if c == '\x1b' {
242 in_escape = true;
243 } else if !matches!(c, '\u{200e}' | '\u{200f}' | '\u{202a}' | '\u{202c}') {
244 width += 1;
245 }
246 }
247 width
248}