use chrono::{DateTime, Datelike, Local, TimeZone};
use clap::Parser;
use config::{Config, ConfigError, Environment, File};
use icalendar::CalendarComponent::Event;
use icalendar::{Calendar, Component, DatePerhapsTime};
use std::ffi::OsString;
use std::fs::{self};
use std::io;
use std::path::Path;
#[derive(Debug)]
struct IcsReadError {
path: OsString,
}
#[derive(Debug, serde::Deserialize, Clone)]
#[allow(unused)]
pub struct Settings {
pub vdir_path: String,
pub default_filter: String,
pub default_date_range: String,
}
impl Settings {
pub fn new(config_file: String) -> Result<Self, ConfigError> {
let mut builder = Config::builder()
.set_default("vdir_path", "calendars")?
.set_default("default_filter", "")?
.set_default("default_date_range", "2025-01-01T00:00:00+00:00")?;
if Path::new(&config_file).is_file() {
builder = builder.add_source(File::with_name(&config_file))
};
let cfg = builder
.add_source(Environment::with_prefix("caltemps"))
.build()?;
cfg.try_deserialize()
}
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, env="CALTEMPS_CONFIG",
default_value_t=xdg::BaseDirectories::with_prefix("caltemps").unwrap()
.get_config_file("config.toml").into_os_string().into_string().expect("Strange path"))]
config: String,
#[arg(short, long, env = "CALTEMPS_FILTER")]
filter: Option<String>,
#[arg(short, long, env = "CALTEMPS_DATE_RANGE")]
date_range: Option<String>,
}
fn list_ics_from_dir(
path: &Path,
) -> Result<impl Iterator<Item = OsString>, Box<dyn std::error::Error>> {
match fs::read_dir(path) {
Ok(dirs) => Ok(dirs.filter_map(|res| match res {
Ok(t) => {
if let Some(x) = t.path().extension() {
if x.eq_ignore_ascii_case("ics") {
Some(t.path().into_os_string())
} else {
None
}
} else {
None
}
}
Err(_) => None,
})),
Err(error) => Err(Box::new(error)),
}
}
fn read_ics_from_dir(
path: &Path,
) -> Result<impl Iterator<Item = Result<Calendar, IcsReadError>>, Box<dyn std::error::Error>> {
match list_ics_from_dir(path) {
Ok(ics_it) => Ok(ics_it.map(|ipath| {
let ics_path = ipath.to_os_string();
match &mut fs::File::open(ipath) {
Ok(f) => {
let readable: &mut dyn io::Read = f;
let mut output = String::new();
match readable.read_to_string(&mut output) {
Ok(_) => {
match output.parse::<Calendar>() {
Ok(read) => Ok(read),
Err(_error) => Err(IcsReadError {
path: ics_path,
}),
}
}
Err(_error) => Err(IcsReadError {
path: ics_path,
}),
}
}
Err(_error) => Err(IcsReadError {
path: ics_path,
}),
}
})),
Err(error) => Err(error),
}
}
fn read_vdir_cal(path: &Path) -> Result<Calendar, Box<dyn std::error::Error>> {
let mut cal = Calendar::new();
match read_ics_from_dir(Path::new(path)) {
Ok(entries_it) => {
for entry in entries_it {
match entry {
Ok(mut partial_cal) => cal.append(&mut partial_cal),
Err(error) => eprintln!("Issue reading {:#?}\n\t{:#?}", error.path, error),
};
}
Ok(cal)
}
Err(error) => Err(error),
}
}
fn dpt_to_dt(dpt: Option<DatePerhapsTime>) -> Option<DateTime<Local>> {
match dpt {
Some(icalendar::DatePerhapsTime::DateTime(cdt)) => cdt.try_into_utc().map(|d| d.into()),
Some(icalendar::DatePerhapsTime::Date(nd)) => Some(
Local
.with_ymd_and_hms(nd.year(), nd.month(), nd.day(), 0, 0, 0)
.unwrap(),
),
_ => None,
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let cfg = Settings::new(cli.config).unwrap();
let start =
DateTime::parse_from_rfc3339(&cli.date_range.unwrap_or(cfg.default_date_range)).unwrap();
match read_vdir_cal(Path::new(&cfg.vdir_path)) {
Ok(cal) => {
let mut x = 0;
for c in cal
.components
.into_iter()
.filter_map(|c| match c {
Event(e) => Some(e),
_ => None,
})
.filter(|c| {
if let Some(summary) = c.get_summary() {
if summary
.contains(&(cli.filter.clone().unwrap_or(cfg.default_filter.clone())))
{
return match dpt_to_dt(c.get_start()) {
Some(dtl) => start <= dtl && dpt_to_dt(c.get_end()).is_some(),
_ => false,
};
}
}
false
})
{
if let Some(ds) = dpt_to_dt(c.get_start()) {
if let Some(de) = dpt_to_dt(c.get_end()) {
x += (de - ds).num_minutes();
}
}
}
println!("Accounted {:.2}h", x as f64 / 60.0);
Ok(())
}
Err(error) => {
eprintln!("Error working on:\t'{}'", cfg.vdir_path);
eprintln!("{}", error);
Err(error)
}
}
}