1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7 io::{self, Write},
8 process::ExitCode,
9};
10
11const MONTH_NAMES: [&str; 12] = [
12 "January",
13 "February",
14 "March",
15 "April",
16 "May",
17 "June",
18 "July",
19 "August",
20 "September",
21 "October",
22 "November",
23 "December",
24];
25
26const MONTH_ABBREVS: [&str; 12] = [
27 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct",
28 "nov", "dec",
29];
30
31const DAYS_HEADER_SUN: &str = "Su Mo Tu We Th Fr Sa";
32const DAYS_HEADER_MON: &str = "Mo Tu We Th Fr Sa Su";
33const MONTH_WIDTH: usize = 20;
34const MONTH_GAP: &str = " ";
35
36#[derive(Parser)]
37#[command(name = "cal", about = "Display a calendar")]
38pub struct Args {
39 #[arg(short = '1', long)]
41 one: bool,
42
43 #[arg(short = '3', long)]
45 three: bool,
46
47 #[arg(short = 'n', long)]
49 months: Option<u32>,
50
51 #[arg(short, long)]
53 sunday: bool,
54
55 #[arg(short, long)]
57 monday: bool,
58
59 #[arg(short = 'y', long)]
61 year: bool,
62
63 #[arg(short = 'Y', long)]
65 twelve: bool,
66
67 #[arg(short, long)]
69 julian: bool,
70
71 #[arg(short = 'c', long, default_value = "3")]
73 columns: u32,
74
75 #[arg(trailing_var_arg = true)]
77 args: Vec<String>,
78}
79
80fn center(s: &str, width: usize) -> String {
83 let len = s.len();
84 if len >= width {
85 return s.to_string();
86 }
87 let total_pad = width - len;
88 let left = total_pad.div_ceil(2);
89 let right = total_pad / 2;
90 format!("{:left$}{s}{:right$}", "", "")
91}
92
93fn today() -> (u32, u32, u32) {
94 let now = chrono::Local::now().date_naive();
95 use chrono::Datelike;
96 (now.year() as u32, now.month(), now.day())
97}
98
99fn is_leap_year(year: u32) -> bool {
100 (year.is_multiple_of(4) && !year.is_multiple_of(100))
101 || year.is_multiple_of(400)
102}
103
104fn days_in_month(year: u32, month: u32) -> u32 {
105 match month {
106 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
107 4 | 6 | 9 | 11 => 30,
108 2 => {
109 if is_leap_year(year) {
110 29
111 } else {
112 28
113 }
114 }
115 _ => 0,
116 }
117}
118
119fn day_of_week(year: u32, month: u32, day: u32) -> u32 {
122 let (y, m) = if month <= 2 {
123 (year as i32 - 1, month as i32 + 12)
124 } else {
125 (year as i32, month as i32)
126 };
127 let q = day as i32;
128 let k = y % 100;
129 let j = y / 100;
130 let h = (q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
131 ((h + 6) % 7) as u32
133}
134
135fn day_of_year(year: u32, month: u32, day: u32) -> u32 {
137 let mut doy = 0;
138 for m in 1..month {
139 doy += days_in_month(year, m);
140 }
141 doy + day
142}
143
144fn render_month(
147 year: u32,
148 month: u32,
149 monday_first: bool,
150 julian: bool,
151 highlight_day: Option<u32>,
152 show_year: bool,
153) -> Vec<String> {
154 let mut lines = Vec::new();
155
156 let title = if show_year {
158 format!("{} {}", MONTH_NAMES[(month - 1) as usize], year)
159 } else {
160 MONTH_NAMES[(month - 1) as usize].to_string()
161 };
162 lines.push(center(&title, MONTH_WIDTH));
163
164 if monday_first {
166 lines.push(DAYS_HEADER_MON.to_string());
167 } else {
168 lines.push(DAYS_HEADER_SUN.to_string());
169 }
170
171 let ndays = days_in_month(year, month);
172 let first_dow = day_of_week(year, month, 1);
173 let offset = if monday_first {
175 (first_dow + 6) % 7
176 } else {
177 first_dow
178 };
179
180 let mut line = String::new();
181 for _ in 0..offset {
183 line.push_str(" ");
184 }
185
186 let _ = highlight_day; for day in 1..=ndays {
189 if julian {
190 let doy = day_of_year(year, month, day);
191 line.push_str(&format!("{doy:>3}"));
192 } else {
193 line.push_str(&format!("{day:>2}"));
194 }
195
196 let col = (offset + day - 1) % 7;
197 if col == 6 || day == ndays {
198 while line.len() < MONTH_WIDTH {
200 line.push(' ');
201 }
202 lines.push(line);
203 line = String::new();
204 } else {
205 line.push(' ');
206 }
207 }
208
209 while lines.len() < 8 {
212 lines.push(" ".repeat(MONTH_WIDTH));
213 }
214
215 lines
216}
217
218fn parse_month_name(s: &str) -> Option<u32> {
219 let lower = s.to_lowercase();
220 for (i, abbrev) in MONTH_ABBREVS.iter().enumerate() {
221 if lower.starts_with(abbrev) {
222 return Some((i + 1) as u32);
223 }
224 }
225 None
226}
227
228#[allow(clippy::too_many_arguments)]
230fn print_months(
231 months: &[(u32, u32)], cols: u32,
233 monday_first: bool,
234 julian: bool,
235 highlight: Option<(u32, u32, u32)>, show_year: bool,
237 gap: &str,
238 out: &mut dyn Write,
239) -> io::Result<()> {
240 let rendered: Vec<Vec<String>> = months
241 .iter()
242 .map(|&(y, m)| {
243 let hl = highlight.and_then(|(hy, hm, hd)| {
244 if hy == y && hm == m { Some(hd) } else { None }
245 });
246 render_month(y, m, monday_first, julian, hl, show_year)
247 })
248 .collect();
249
250 for chunk in rendered.chunks(cols as usize) {
251 let max_lines = chunk.iter().map(|m| m.len()).max().unwrap_or(0);
252 for row in 0..max_lines {
253 for (ci, month) in chunk.iter().enumerate() {
254 if ci > 0 {
255 write!(out, "{gap}")?;
256 }
257 if row < month.len() {
258 write!(out, "{}", month[row])?;
259 } else {
260 write!(out, "{:MONTH_WIDTH$}", "")?;
261 }
262 }
263 writeln!(out)?;
264 }
265 }
266 Ok(())
267}
268
269pub fn run(args: Args) -> ExitCode {
270 let (cur_year, cur_month, cur_day) = today();
271
272 let monday_first = args.monday && !args.sunday;
273
274 let (target_year, target_month, target_day) = match args.args.len() {
276 0 => (cur_year, Some(cur_month), Some(cur_day)),
277 1 => {
278 let arg = &args.args[0];
279 if let Ok(n) = arg.parse::<u32>() {
280 if (1..=12).contains(&n) {
281 (n, None, None) } else {
286 (n, None, None)
287 }
288 } else if let Some(m) = parse_month_name(arg) {
289 (cur_year, Some(m), None)
290 } else if arg == "today" || arg == "now" {
291 (cur_year, Some(cur_month), Some(cur_day))
292 } else if arg == "tomorrow" {
293 let mut d = cur_day + 1;
295 let mut m = cur_month;
296 let mut y = cur_year;
297 if d > days_in_month(y, m) {
298 d = 1;
299 m += 1;
300 if m > 12 {
301 m = 1;
302 y += 1;
303 }
304 }
305 (y, Some(m), Some(d))
306 } else if arg == "yesterday" {
307 let mut d = cur_day as i32 - 1;
308 let mut m = cur_month;
309 let mut y = cur_year;
310 if d < 1 {
311 m -= 1;
312 if m < 1 {
313 m = 12;
314 y -= 1;
315 }
316 d = days_in_month(y, m) as i32;
317 }
318 (y, Some(m), Some(d as u32))
319 } else {
320 eprintln!("cal: invalid argument: {arg}");
321 return ExitCode::FAILURE;
322 }
323 }
324 2 => {
325 let month = match args.args[0].parse::<u32>() {
326 Ok(m) if (1..=12).contains(&m) => m,
327 _ => match parse_month_name(&args.args[0]) {
328 Some(m) => m,
329 None => {
330 eprintln!("cal: invalid month: {}", args.args[0]);
331 return ExitCode::FAILURE;
332 }
333 },
334 };
335 let year = match args.args[1].parse::<u32>() {
336 Ok(y) => y,
337 Err(_) => {
338 eprintln!("cal: invalid year: {}", args.args[1]);
339 return ExitCode::FAILURE;
340 }
341 };
342 (year, Some(month), None)
343 }
344 3 => {
345 let day = match args.args[0].parse::<u32>() {
346 Ok(d) => d,
347 Err(_) => {
348 eprintln!("cal: invalid day: {}", args.args[0]);
349 return ExitCode::FAILURE;
350 }
351 };
352 let month = match args.args[1].parse::<u32>() {
353 Ok(m) if (1..=12).contains(&m) => m,
354 _ => match parse_month_name(&args.args[1]) {
355 Some(m) => m,
356 None => {
357 eprintln!("cal: invalid month: {}", args.args[1]);
358 return ExitCode::FAILURE;
359 }
360 },
361 };
362 let year = match args.args[2].parse::<u32>() {
363 Ok(y) => y,
364 Err(_) => {
365 eprintln!("cal: invalid year: {}", args.args[2]);
366 return ExitCode::FAILURE;
367 }
368 };
369 (year, Some(month), Some(day))
370 }
371 _ => {
372 eprintln!("cal: too many arguments");
373 return ExitCode::FAILURE;
374 }
375 };
376
377 let stdout = io::stdout();
378 let mut out = stdout.lock();
379
380 let highlight =
381 target_day.map(|d| (target_year, target_month.unwrap_or(cur_month), d));
382
383 let cols = args.columns;
384
385 if args.year || target_month.is_none() {
386 let year_title = format!("{target_year}");
388 let year_gap = " ";
389 let total_width =
390 cols as usize * MONTH_WIDTH + (cols as usize - 1) * year_gap.len();
391 if let Err(e) = writeln!(out, "{}", center(&year_title, total_width)) {
392 eprintln!("cal: {e}");
393 return ExitCode::FAILURE;
394 }
395 if let Err(e) = writeln!(out) {
396 eprintln!("cal: {e}");
397 return ExitCode::FAILURE;
398 }
399
400 let months: Vec<(u32, u32)> =
401 (1..=12).map(|m| (target_year, m)).collect();
402 if let Err(e) = print_months(
403 &months,
404 cols,
405 monday_first,
406 args.julian,
407 highlight,
408 false,
409 year_gap,
410 &mut out,
411 ) {
412 eprintln!("cal: {e}");
413 return ExitCode::FAILURE;
414 }
415 } else if args.twelve {
416 let mut months = Vec::new();
418 let mut y = target_year;
419 let mut m = target_month.unwrap_or(cur_month);
420 for _ in 0..12 {
421 months.push((y, m));
422 m += 1;
423 if m > 12 {
424 m = 1;
425 y += 1;
426 }
427 }
428 if let Err(e) = print_months(
429 &months,
430 cols,
431 monday_first,
432 args.julian,
433 highlight,
434 true,
435 " ",
436 &mut out,
437 ) {
438 eprintln!("cal: {e}");
439 return ExitCode::FAILURE;
440 }
441 } else if args.three {
442 let tm = target_month.unwrap_or(cur_month);
444 let mut months = Vec::new();
445 for offset in [-1i32, 0, 1] {
446 let mut m = tm as i32 + offset;
447 let mut y = target_year as i32;
448 if m < 1 {
449 m += 12;
450 y -= 1;
451 } else if m > 12 {
452 m -= 12;
453 y += 1;
454 }
455 months.push((y as u32, m as u32));
456 }
457 if let Err(e) = print_months(
458 &months,
459 3,
460 monday_first,
461 args.julian,
462 highlight,
463 true,
464 MONTH_GAP,
465 &mut out,
466 ) {
467 eprintln!("cal: {e}");
468 return ExitCode::FAILURE;
469 }
470 } else if let Some(n) = args.months {
471 let tm = target_month.unwrap_or(cur_month);
473 let mut months = Vec::new();
474 let mut y = target_year;
475 let mut m = tm;
476 for _ in 0..n {
477 months.push((y, m));
478 m += 1;
479 if m > 12 {
480 m = 1;
481 y += 1;
482 }
483 }
484 if let Err(e) = print_months(
485 &months,
486 cols,
487 monday_first,
488 args.julian,
489 highlight,
490 true,
491 " ",
492 &mut out,
493 ) {
494 eprintln!("cal: {e}");
495 return ExitCode::FAILURE;
496 }
497 } else {
498 let tm = target_month.unwrap_or(cur_month);
500 let rendered = render_month(
501 target_year,
502 tm,
503 monday_first,
504 args.julian,
505 target_day,
506 true,
507 );
508 for line in &rendered {
509 if let Err(e) = writeln!(out, "{line}") {
510 eprintln!("cal: {e}");
511 return ExitCode::FAILURE;
512 }
513 }
514 }
515
516 ExitCode::SUCCESS
517}