2026-02-17 23:42:26 +01:00
|
|
|
use super::calendar::{create_timetable, get_todays_date};
|
2026-02-12 23:52:24 +01:00
|
|
|
|
2026-02-19 21:29:27 +01:00
|
|
|
use chrono::Local;
|
2026-02-12 23:52:24 +01:00
|
|
|
use chrono::NaiveDate;
|
|
|
|
|
use chrono::NaiveTime;
|
2026-02-19 21:29:27 +01:00
|
|
|
use chrono::TimeZone;
|
2026-02-17 23:42:26 +01:00
|
|
|
use chrono_tz::Tz;
|
2026-02-12 21:41:18 +01:00
|
|
|
use clap::Parser;
|
2026-02-17 23:42:43 +01:00
|
|
|
use colored::Colorize;
|
2026-02-12 23:52:24 +01:00
|
|
|
use std::error::Error;
|
|
|
|
|
use std::fmt;
|
|
|
|
|
use std::iter;
|
2026-02-12 21:41:18 +01:00
|
|
|
|
|
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
#[command(version, about, long_about = None)]
|
|
|
|
|
pub struct Args {
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
pub timezone: Vec<String>,
|
|
|
|
|
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
pub date: Option<String>,
|
|
|
|
|
|
2026-02-12 23:52:24 +01:00
|
|
|
#[arg(short, long, default_value = "Local")]
|
|
|
|
|
pub local: String,
|
|
|
|
|
|
2026-02-12 21:41:18 +01:00
|
|
|
pub time: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 23:52:24 +01:00
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
|
pub enum CliError {
|
|
|
|
|
CouldNotParseTime,
|
|
|
|
|
CouldNotParseDate,
|
2026-02-17 23:42:26 +01:00
|
|
|
CouldNotParseTimezone(String),
|
2026-02-19 21:29:27 +01:00
|
|
|
CouldNotGenerateTimetable(String),
|
2026-02-12 23:52:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for CliError {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
|
let message = match *self {
|
|
|
|
|
Self::CouldNotParseTime => "Couldn't parse time".to_string(),
|
|
|
|
|
Self::CouldNotParseDate => "Couldn't parse date".to_string(),
|
2026-02-17 23:42:26 +01:00
|
|
|
Self::CouldNotParseTimezone(ref tz) => {
|
|
|
|
|
format!("Couldn't parse timezone {}", tz).to_string()
|
|
|
|
|
}
|
2026-02-19 21:29:27 +01:00
|
|
|
Self::CouldNotGenerateTimetable(ref er) => {
|
2026-02-12 23:52:24 +01:00
|
|
|
format!("Could not generate timetable: {}", er)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
f.write_str(&message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Error for CliError {}
|
|
|
|
|
|
2026-02-19 21:29:27 +01:00
|
|
|
// struct Timetable {
|
|
|
|
|
// pub name: String,
|
|
|
|
|
// pub values: Vec<NaiveTime>,
|
|
|
|
|
// }
|
|
|
|
|
|
2026-02-12 23:52:24 +01:00
|
|
|
pub fn print_timezones(
|
|
|
|
|
timezones: &Vec<String>,
|
|
|
|
|
time: &String,
|
|
|
|
|
date: Option<&String>,
|
|
|
|
|
local_timezone: &String,
|
2026-02-19 21:29:27 +01:00
|
|
|
padding_hours: i8,
|
2026-02-12 23:52:24 +01:00
|
|
|
) -> Result<(), CliError> {
|
|
|
|
|
let todays_date = &get_todays_date();
|
|
|
|
|
let date = date.unwrap_or(todays_date);
|
|
|
|
|
|
|
|
|
|
let parsed_time = if let Ok(hour) = time.parse::<u32>() {
|
|
|
|
|
NaiveTime::from_hms_opt(hour, 0, 0).ok_or(CliError::CouldNotParseTime)?
|
|
|
|
|
} else {
|
|
|
|
|
NaiveTime::parse_from_str(time, "%H:%M").map_err(|_| CliError::CouldNotParseTime)?
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let parsed_date = NaiveDate::parse_from_str(date, "%d-%m-%Y")
|
|
|
|
|
.or_else(|_| NaiveDate::parse_from_str(date, "%d-%m"))
|
|
|
|
|
.map_err(|_| CliError::CouldNotParseDate)?;
|
|
|
|
|
|
2026-02-19 21:29:27 +01:00
|
|
|
if local_timezone == "Local" {
|
|
|
|
|
print_calendar(
|
|
|
|
|
parsed_time,
|
|
|
|
|
parsed_date,
|
|
|
|
|
Local,
|
|
|
|
|
&"Local".to_string(),
|
|
|
|
|
&timezones,
|
|
|
|
|
padding_hours,
|
|
|
|
|
)
|
2026-02-17 23:42:26 +01:00
|
|
|
} else {
|
2026-02-19 21:29:27 +01:00
|
|
|
let timezone = local_timezone
|
2026-02-17 23:42:26 +01:00
|
|
|
.parse::<Tz>()
|
2026-02-19 21:29:27 +01:00
|
|
|
.map_err(|_| CliError::CouldNotGenerateTimetable(local_timezone.to_string()))?;
|
|
|
|
|
print_calendar(
|
|
|
|
|
parsed_time,
|
|
|
|
|
parsed_date,
|
|
|
|
|
timezone,
|
|
|
|
|
&timezone.name().to_string(),
|
|
|
|
|
&timezones,
|
|
|
|
|
padding_hours,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn print_calendar<TzLocal: TimeZone>(
|
|
|
|
|
parsed_time: NaiveTime,
|
|
|
|
|
parsed_date: NaiveDate,
|
|
|
|
|
parsed_local_timezone: TzLocal,
|
|
|
|
|
local_timezone_name: &String,
|
|
|
|
|
timezones: &Vec<String>,
|
|
|
|
|
padding_hours: i8,
|
|
|
|
|
) -> Result<(), CliError> {
|
|
|
|
|
let local_timetable = create_timetable(
|
|
|
|
|
&parsed_time,
|
|
|
|
|
&parsed_date,
|
|
|
|
|
&parsed_local_timezone,
|
|
|
|
|
&parsed_local_timezone,
|
|
|
|
|
padding_hours,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|_| CliError::CouldNotParseTimezone(local_timezone_name.to_string()))?;
|
|
|
|
|
|
|
|
|
|
let longest_name_length = iter::once(local_timezone_name.to_string())
|
|
|
|
|
.chain(timezones.iter().map(|tz| tz.to_string()))
|
|
|
|
|
.map(|t| t.len())
|
|
|
|
|
.max()
|
|
|
|
|
.unwrap_or(0) as u32;
|
2026-02-17 23:42:26 +01:00
|
|
|
|
|
|
|
|
let parsed_timezones: Vec<Tz> = timezones
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|tz| {
|
|
|
|
|
tz.parse::<Tz>()
|
|
|
|
|
.map_err(|_| CliError::CouldNotParseTimezone(tz.to_string()))
|
|
|
|
|
})
|
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
2026-02-12 23:52:24 +01:00
|
|
|
|
2026-02-19 21:29:27 +01:00
|
|
|
let target_timetables = parsed_timezones
|
2026-02-17 23:42:26 +01:00
|
|
|
.iter()
|
2026-02-19 21:29:27 +01:00
|
|
|
.map(|tz| {
|
|
|
|
|
create_timetable(
|
|
|
|
|
&parsed_time,
|
|
|
|
|
&parsed_date,
|
|
|
|
|
&parsed_local_timezone,
|
|
|
|
|
tz,
|
|
|
|
|
padding_hours,
|
|
|
|
|
)
|
|
|
|
|
.map(|tt| (tz.name().to_string(), tt))
|
|
|
|
|
.map_err(|_| CliError::CouldNotGenerateTimetable(tz.name().to_string()))
|
|
|
|
|
})
|
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
2026-02-12 23:52:24 +01:00
|
|
|
|
2026-02-19 21:29:27 +01:00
|
|
|
let all_timetables: Vec<(String, Vec<NaiveTime>)> =
|
|
|
|
|
iter::once((local_timezone_name.to_string(), local_timetable))
|
|
|
|
|
.chain(target_timetables)
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
for timezone in all_timetables {
|
|
|
|
|
let name: String = timezone.0;
|
|
|
|
|
let timetable = timezone.1;
|
|
|
|
|
let name_padding_len = longest_name_length - name.len() as u32;
|
|
|
|
|
print_timetable(&name, name_padding_len, &timetable)
|
2026-02-12 21:41:18 +01:00
|
|
|
}
|
2026-02-12 23:52:24 +01:00
|
|
|
Ok(())
|
2026-02-12 21:41:18 +01:00
|
|
|
}
|
2026-02-17 23:42:26 +01:00
|
|
|
|
|
|
|
|
fn print_timetable(name: &String, name_padding_len: u32, timetable: &Vec<NaiveTime>) {
|
2026-02-17 23:42:43 +01:00
|
|
|
let len = timetable.len();
|
|
|
|
|
let mid = (len - 1) / 2;
|
|
|
|
|
let acceptable_time_from = NaiveTime::from_hms_opt(8, 0, 0).unwrap();
|
|
|
|
|
let acceptable_time_to = NaiveTime::from_hms_opt(20, 0, 0).unwrap();
|
2026-02-17 23:42:26 +01:00
|
|
|
let formatted: Vec<String> = timetable
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
2026-02-17 23:42:43 +01:00
|
|
|
.map(|(i, t)| {
|
|
|
|
|
let stringified = t.format("%H:%M").to_string();
|
|
|
|
|
|
|
|
|
|
let acceptable_hours_formatted = if t < &acceptable_time_from || t > &acceptable_time_to
|
|
|
|
|
{
|
|
|
|
|
stringified.color("#fea201")
|
|
|
|
|
} else {
|
|
|
|
|
stringified.green()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if i == mid {
|
|
|
|
|
acceptable_hours_formatted.reversed().to_string()
|
|
|
|
|
} else {
|
|
|
|
|
acceptable_hours_formatted.to_string()
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-02-17 23:42:26 +01:00
|
|
|
.collect();
|
|
|
|
|
let name_padding = (0..name_padding_len)
|
|
|
|
|
.map(|_| " ".to_string())
|
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
|
.join("");
|
|
|
|
|
|
|
|
|
|
println!("{}:{}\t{}", name, name_padding, formatted.join("\t"));
|
|
|
|
|
}
|