8.5 KiB · text History 2c2327e
use crate::data::{CalendarOps, CalendarReminder, MotdRemindEnum, ReminderTarget};
use crate::entities::calendar;
use crate::holidays::{self, CalendarDef, GROUPS};
use crate::PplError;
use chrono::{Datelike, Local};
use clap::{Args, Subcommand};
use sea_orm::DatabaseConnection;
use std::io;

#[derive(Args, Debug)]
pub struct CalendarArgs {
    #[command(subcommand)]
    pub action: Option<CalendarAction>,
}

#[derive(Subcommand, Debug)]
pub enum CalendarAction {
    /// List the built-in calendars, and which are on
    List {
        /// Also list the states and provinces of countries that are off
        #[arg(long, short)]
        all: bool,
    },
    /// Show a calendar's holidays in the calendar tab, e.g. `enable us us-ny jewish`
    Enable {
        #[arg(required = true)]
        keys: Vec<String>,
    },
    /// Stop showing a calendar's holidays
    Disable {
        #[arg(required = true)]
        keys: Vec<String>,
    },
    /// Mention a calendar's holidays in the motd DAYS days ahead (0 is on the day).
    /// TARGET is a calendar key, or a group's default: countries, religious, other
    Set { target: String, days: u32 },
    /// How often the motd mentions them: daily, hourly, random:N, always, never
    Remind {
        target: String,
        mode: MotdRemindEnum,
    },
    /// Go back to the defaults (a state's country, then the group, then on the day, daily)
    Clear { target: String },
    /// Add an iCal calendar from a URL (http, https, webcal) or an .ics file, switched on
    Subscribe {
        /// What to call it, like work; lowercase letters, digits and dashes
        name: String,
        url: String,
    },
    /// Remove a subscribed calendar and its settings
    Unsubscribe { name: String },
    /// Fetch subscribed calendars now, rather than when they're 6 hours old
    Sync { names: Vec<String> },
    /// Print a year's holidays from the calendars that are on, or the ones named
    Show {
        keys: Vec<String>,
        /// Defaults to this year
        #[arg(long, short)]
        year: Option<i32>,
    },
}

pub async fn do_calendars(args: &CalendarArgs, db: &DatabaseConnection) -> Result<(), PplError> {
    let subscriptions = CalendarOps::subscriptions(db).await?;
    if matches!(&args.action, Some(CalendarAction::Show { .. })) {
        crate::ical::refresh_stale(&subscriptions);
    }
    let enabled = CalendarOps::enabled(db).await?;
    let rows = CalendarOps::rows(db).await?;
    match &args.action {
        None => list(&enabled, &rows, false),
        Some(CalendarAction::List { all }) => list(&enabled, &rows, *all),
        Some(CalendarAction::Set { target, days }) => {
            let t = ReminderTarget::parse(target)?;
            let r = t.reminder(&rows);
            CalendarOps::set_reminder(db, t, Some(*days), r.remind).await?;
            println!("{}: {}", t.label(), window(*days));
        }
        Some(CalendarAction::Remind { target, mode }) => {
            let t = ReminderTarget::parse(target)?;
            let r = t.reminder(&rows);
            CalendarOps::set_reminder(db, t, r.delta, Some(*mode)).await?;
            println!("{}: remind {}", t.label(), mode.label());
        }
        Some(CalendarAction::Subscribe { name, url }) => {
            let count = CalendarOps::subscribe(db, name, url).await?;
            println!(
                "subscribed to {} ({} events); it's on, and refreshes every 6 hours",
                name.trim().to_lowercase(),
                count
            );
        }
        Some(CalendarAction::Unsubscribe { name }) => {
            CalendarOps::unsubscribe(db, name).await?;
            println!("unsubscribed from {}", name);
        }
        Some(CalendarAction::Sync { names }) => {
            for n in names {
                if !subscriptions.iter().any(|(k, _)| k.eq_ignore_ascii_case(n)) {
                    return Err(io::Error::new(
                        io::ErrorKind::NotFound,
                        format!("not subscribed to '{}'", n),
                    )
                    .into());
                }
            }
            if subscriptions.is_empty() {
                println!("no subscriptions; add one with `ppl calendars subscribe <name> <url>`");
            }
            for (key, source) in &subscriptions {
                if !names.is_empty() && !names.iter().any(|n| n.eq_ignore_ascii_case(key)) {
                    continue;
                }
                match crate::ical::sync(key, source) {
                    Ok(count) => println!("{}: {} events", key, count),
                    Err(e) => println!("{}: couldn't fetch, keeping what was there ({})", key, e),
                }
            }
        }
        Some(CalendarAction::Clear { target }) => {
            let t = ReminderTarget::parse(target)?;
            CalendarOps::set_reminder(db, t, None, None).await?;
            let r = t.reminder(&CalendarOps::rows(db).await?);
            println!("{}: back to the defaults, {}", t.label(), describe(&r));
        }
        Some(CalendarAction::Enable { keys }) => toggle(db, keys, true).await?,
        Some(CalendarAction::Disable { keys }) => toggle(db, keys, false).await?,
        Some(CalendarAction::Show { keys, year }) => {
            let year = year.unwrap_or_else(|| Local::now().year());
            let keys = match keys.is_empty() {
                true => enabled,
                false => {
                    for k in keys {
                        lookup(k)?;
                    }
                    keys.clone()
                }
            };
            if keys.is_empty() {
                println!("no calendars are on; try `ppl calendars enable us`");
            }
            for h in holidays::holidays_in(year, &keys) {
                println!(
                    "{}  {:<9} {}",
                    h.date.format("%a %Y-%m-%d"),
                    h.calendar.key,
                    h.name
                );
            }
        }
    }
    Ok(())
}

fn lookup(key: &str) -> Result<&'static CalendarDef, PplError> {
    holidays::find(key).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::NotFound,
            format!("no calendar '{}', see `ppl calendars list --all`", key),
        )
        .into()
    })
}

async fn toggle(db: &DatabaseConnection, keys: &[String], on: bool) -> Result<(), PplError> {
    // Check them all first, so a typo doesn't leave half of them changed
    for k in keys {
        lookup(k)?;
    }
    for k in keys {
        let changed = CalendarOps::set(db, k, on).await?;
        if changed.is_empty() {
            println!("{} is already {}", k, if on { "on" } else { "off" });
        }
        for c in changed {
            println!(
                "{} {} ({})",
                if on { "on: " } else { "off:" },
                c.name,
                c.key
            );
        }
    }
    Ok(())
}

fn window(days: u32) -> String {
    match days {
        0 => "on the day".to_string(),
        1 => "1 day ahead".to_string(),
        n => format!("{} days ahead", n),
    }
}

/// "7 days ahead · daily (default)"
fn describe(r: &CalendarReminder) -> String {
    let mark = |own: bool| if own { "" } else { " (default)" };
    format!(
        "{}{} · {}{}",
        window(r.window()),
        mark(r.delta.is_some()),
        r.mode().label(),
        mark(r.remind.is_some())
    )
}

fn list(enabled: &[String], rows: &[calendar::Model], all: bool) {
    let is_on = |c: &CalendarDef| enabled.iter().any(|k| k == c.key);
    let line = |c: &'static CalendarDef, indent: &str| {
        let motd = match is_on(c) {
            true => format!(
                "  motd: {}",
                describe(&ReminderTarget::Calendar(c).reminder(rows))
            ),
            false => String::new(),
        };
        println!(
            "{}[{}] {:<16} {}{}{}",
            indent,
            if is_on(c) { "x" } else { " " },
            c.key,
            c.name,
            c.note.map(|n| format!(" ({})", n)).unwrap_or_default(),
            motd
        );
    };
    for g in &GROUPS {
        let r = ReminderTarget::Group(g).reminder(rows);
        println!("{}  (`{}`, motd: {})", g.name, g.key, describe(&r));
        for c in holidays::in_group(g) {
            line(c, "  ");
            let regions = holidays::regions(c.key).collect::<Vec<_>>();
            if all || is_on(c) {
                for r in regions {
                    line(r, "      ");
                }
            } else if !regions.is_empty() {
                println!("      ({} more, see --all)", regions.len());
            }
        }
    }
}