23.1 KiB · text History 2803012
use clap::{Parser, Subcommand};
use color_eyre::eyre::ErrReport;
use interim::DateError;
use log::{debug, info};
use sea_orm::*;
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
use std::io::{self};

mod color;
mod data;
mod db;
mod do_add;
mod do_edit;
mod do_init;
mod do_shell;
mod do_tiers;
mod do_traits;
mod do_tui;
mod entities;
mod migrator;
mod tui_add;

use crate::data::{anniversary_in, MotdRemindEnum};
use crate::do_add::{do_add, AddArgs};
use crate::do_edit::{do_edit, EditArgs};
use crate::do_shell::{do_shell, ShellArgs};
use crate::do_tiers::{do_tiers, TierArgs};
use crate::do_traits::{do_traits, TraitArgs};
use crate::entities::ppl::Column::Me;
use entities::prelude::*;

#[derive(Debug)]
enum PplError {
    DateError(DateError),
    DbError(DbErr),
    EyreError(ErrReport),
    Std(io::Error),
    XDG(xdg::BaseDirectoriesError),
}

impl std::fmt::Display for PplError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PplError::DateError(e) => write!(f, "{:?}", e),
            PplError::DbError(e) => write!(f, "{}", e),
            PplError::EyreError(e) => write!(f, "{}", e),
            PplError::Std(e) => write!(f, "{}", e),
            PplError::XDG(e) => write!(f, "{}", e),
        }
    }
}

impl From<io::Error> for PplError {
    fn from(value: io::Error) -> Self {
        PplError::Std(value)
    }
}
impl From<DbErr> for PplError {
    fn from(value: DbErr) -> Self {
        PplError::DbError(value)
    }
}

impl From<ErrReport> for PplError {
    fn from(value: ErrReport) -> Self {
        PplError::EyreError(value)
    }
}

impl From<DateError> for PplError {
    fn from(value: DateError) -> Self {
        PplError::DateError(value)
    }
}

impl From<xdg::BaseDirectoriesError> for PplError {
    fn from(value: xdg::BaseDirectoriesError) -> Self {
        PplError::XDG(value)
    }
}

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Initialize ppl
    Init,
    /// TUI
    Tui,
    /// Calendar of upcoming events
    Calendar,
    /// Show MOTD Version
    MOTD,
    /// Add `ppl motd` to your shell's rc file (bash, zsh, fish)
    Shell(ShellArgs),
    /// Add ppl
    Add(AddArgs),
    /// Aug ppl
    Aug,
    /// Edit a person's name or nickname
    Edit(EditArgs),
    /// Show ppl
    Show,
    /// Tiers and their motd reminder windows
    Tiers(TierArgs),
    /// Traits ppl can have (dates and contacts too)
    Traits(TraitArgs),
    /// Stats
    Stats,
}

#[tokio::main]
async fn main() -> Result<(), PplError> {
    let xdg_dirs = xdg::BaseDirectories::with_prefix("pplapp")?;
    xdg_dirs.create_data_directory("")?;
    let data_home_path = xdg_dirs.get_data_home();
    let database_url: String = format!(
        "sqlite://{}/db.db?mode=rwc",
        data_home_path.to_str().unwrap()
    );

    env_logger::init();
    color_eyre::install()?;
    let db = Database::connect(database_url).await?;
    db::check_migrations(&db).await?;

    let cli = Cli::parse();

    let is_init = Ppl::find().filter(Me.eq(true)).one(&db).await?.is_some();

    match &cli.command {
        Some(Commands::Edit(args)) => {
            do_edit(args, &db).await?;
        }
        None => {
            println!("ppl");
            // show().expect("failed2draw")
        }
        Some(Commands::MOTD) => {
            motd(&db).await?;
        }
        Some(Commands::Shell(args)) => {
            do_shell(args)?;
        }
        Some(Commands::Init) => match is_init {
            false => {
                info!("Uninitialized");

                let terminal = ratatui::init();
                let result = do_init::run_init(terminal, db).await;
                ratatui::restore();
                drop(result);
                info!("Init complete");
            }
            true => {
                println!("ppl has been initialized already");
            }
        },
        Some(Commands::Add(args)) => {
            let _ = do_add(args, db).await;
        }
        Some(Commands::Aug) => {}
        Some(Commands::Tui) => {
            let terminal = ratatui::init();
            let result = do_tui::run_tui(terminal, db).await;
            ratatui::restore();
            drop(result);
        }
        Some(Commands::Calendar) => {}
        Some(Commands::Show) => match is_init {
            true => {
                let ppl = Ppl::find().all(&db).await?;
                for p in ppl {
                    println!("{:?}", p)
                }
            }
            false => {
                info!("pls run init");
            }
        },
        Some(Commands::Tiers(args)) => match is_init {
            true => {
                do_tiers(args, &db).await?;
            }
            false => {
                info!("pls run init");
            }
        },
        Some(Commands::Traits(args)) => match is_init {
            true => {
                do_traits(args, &db).await?;
            }
            false => {
                info!("pls run init");
            }
        },
        Some(Commands::Stats) => match is_init {
            true => {
                let cnt = Ppl::find().all(&db).await?.len();
                println!("{:?} ppl", cnt)
            }
            false => {
                info!("pls run init");
            }
        },
    }
    Ok(())
}

async fn motd(db: &DatabaseConnection) -> Result<(), PplError> {
    use chrono::{Datelike, Local, NaiveDate};

    let today = Local::now().date_naive();

    debug!("DEBUG: Today is {}", today);

    // Get all people
    let people = Ppl::find().all(db).await?;
    debug!("DEBUG: Found {} people", people.len());

    // Get all tier defaults
    let tier_defaults = TierDefaults::find().all(db).await?;
    debug!("DEBUG: Found {} tier defaults", tier_defaults.len());

    // Get all trait defaults for date matching
    let trait_defaults = TraitDefaults::find()
        .filter(entities::trait_defaults::Column::IsDate.eq(true))
        .all(db)
        .await?;
    debug!("DEBUG: Found {} date trait defaults", trait_defaults.len());

    let now = Local::now();
    let this_hour = now.format("%Y-%m-%dT%H").to_string();

    for person in people {
        // Get all sigdates for this person
        let sigdates = SigDate::find()
            .filter(entities::sig_date::Column::PplId.eq(person.id))
            .all(db)
            .await?;

        debug!(
            "DEBUG: Person '{}' has {} sigdates",
            person.name,
            sigdates.len()
        );

        // Get all tiers for this person
        let person_tiers = Tier::find()
            .filter(entities::tier::Column::PplId.eq(person.id))
            .all(db)
            .await?;

        debug!(
            "DEBUG: Person '{}' has {} tiers",
            person.name,
            person_tiers.len()
        );

        // Get the tier color and symbol for display
        let mut tier_color: Option<String> = None;
        let mut tier_symbol: Option<String> = None;

        for tier in &person_tiers {
            if tier_color.is_none() {
                tier_color = tier.color.clone();
            }
            if tier_symbol.is_none() {
                tier_symbol = tier.symbol.clone();
            }
        }

        // Remind mode: the person's own tier first, then the defaults for their tiers, then OnlyOnce
        let remind = person_tiers
            .iter()
            .filter_map(|t| t.sig_remind_enum.as_deref())
            .chain(
                tier_defaults
                    .iter()
                    .filter(|td| person_tiers.iter().any(|t| t.name == td.key))
                    .filter_map(|td| td.sig_remind_enum.as_deref()),
            )
            .find_map(|r| match r.parse::<MotdRemindEnum>() {
                Ok(r) => Some(r),
                Err(e) => {
                    debug!("DEBUG: Ignoring remind mode for '{}': {}", person.name, e);
                    None
                }
            })
            .unwrap_or_default();
        debug!("DEBUG: Person '{}' remind mode {}", person.name, remind);

        let meta_str = |key: &str| {
            person
                .meta
                .as_ref()
                .and_then(|m| m.get(key))
                .and_then(|v| v.as_str())
                .map(|v| v.to_string())
        };

        let should_show = match remind {
            MotdRemindEnum::Never => false,
            MotdRemindEnum::Always => true,
            MotdRemindEnum::Randomly(percent) => {
                // std's RandomState is randomly seeded per instance, good enough for a coin flip
                let roll = RandomState::new().build_hasher().finish() % 100;
                roll < percent as u64
            }
            // Only show if last reminded was before today
            MotdRemindEnum::OnlyOnce => meta_str("last_reminded")
                .and_then(|d| NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok())
                .is_none_or(|d| d < today),
            // Only show if not already shown during this clock hour
            MotdRemindEnum::OnceHourly => {
                meta_str("last_reminded_hour").is_none_or(|h| h != this_hour)
            }
        };

        if !should_show {
            debug!(
                "DEBUG: Skipping person '{}' - remind mode {}",
                person.name, remind
            );
            continue;
        }

        // Track if we showed any sigdates for this person
        let mut showed_sigdate = false;

        for sigdate in sigdates {
            debug!(
                "DEBUG: Processing sigdate: {} on {}",
                sigdate.event, sigdate.date
            );
            if !sigdate.do_remind {
                debug!("DEBUG: Skipping sigdate - reminders are off for it");
                continue;
            }
            let mut tier_delta: Option<u32> = None;

            // First check person's assigned tiers
            for tier in &person_tiers {
                debug!(
                    "DEBUG: Checking tier '{}' with delta {:?}",
                    tier.name, tier.sig_date_delta
                );
                if let Some(delta) = tier.sig_date_delta {
                    tier_delta = Some(delta);
                    break;
                }
            }

            // If no tier delta found, check the tier_defaults for the person's tiers
            if tier_delta.is_none() {
                for tier_default in tier_defaults
                    .iter()
                    .filter(|td| person_tiers.iter().any(|t| t.name == td.key))
                {
                    debug!(
                        "DEBUG: Checking tier_default '{}' with delta {:?}",
                        tier_default.key, tier_default.sig_date_delta
                    );
                    if let Some(delta) = tier_default.sig_date_delta {
                        tier_delta = Some(delta);
                        break;
                    }
                }
            }

            // If we have a tier delta, check if we're within the reminder window for this year's anniversary
            if let Some(delta) = tier_delta {
                // Calculate this year's anniversary
                let this_year_anniversary = anniversary_in(sigdate.date, today.year());

                if let Some(anniversary) = this_year_anniversary {
                    let days_until = (anniversary - today).num_days();
                    debug!(
                        "DEBUG: This year's anniversary is {}, days until: {}",
                        anniversary, days_until
                    );

                    // Show if the anniversary is upcoming and within the delta window
                    if days_until >= 0 && days_until <= delta as i64 {
                        let years_since = today.year() - sigdate.date.year();
                        let display_name = person.nick.as_ref().unwrap_or(&person.name);

                        // Format the person's name with tier symbol and color if available
                        let formatted_name = if let Some(symbol) = &tier_symbol {
                            if let Some(color_str) = &tier_color {
                                use colored::Colorize;
                                let colored_name = match color_str.to_uppercase().as_str() {
                                    "PINK" => display_name.magenta(),
                                    "GREEN" => display_name.green(),
                                    "CYAN" => display_name.cyan(),
                                    "RED" => display_name.red(),
                                    "BLUE" => display_name.blue(),
                                    "YELLOW" => display_name.yellow(),
                                    "PURPLE" => display_name.purple(),
                                    "WHITE" => display_name.white(),
                                    "BLACK" => display_name.black(),
                                    "BRIGHT_RED" => display_name.bright_red(),
                                    "BRIGHT_GREEN" => display_name.bright_green(),
                                    "BRIGHT_YELLOW" => display_name.bright_yellow(),
                                    "BRIGHT_BLUE" => display_name.bright_blue(),
                                    "BRIGHT_MAGENTA" => display_name.bright_magenta(),
                                    "BRIGHT_CYAN" => display_name.bright_cyan(),
                                    "BRIGHT_WHITE" => display_name.bright_white(),
                                    _ => display_name.normal(), // default to no color
                                };
                                format!("{} {}", symbol, colored_name)
                            } else {
                                format!("{} {}", symbol, display_name)
                            }
                        } else {
                            display_name.to_string()
                        };

                        // Find matching trait_default for this sigdate event
                        let trait_info = trait_defaults.iter().find(|t| t.key == sigdate.event);

                        if let Some(trait_def) = trait_info {
                            println!(
                                "{} {} days until {} for {} ({}{})",
                                trait_def.symbol,
                                days_until,
                                sigdate.event,
                                formatted_name,
                                years_since,
                                if years_since == 1 { " year" } else { " years" }
                            );
                        } else {
                            println!(
                                "{} days until {} for {} ({}{})",
                                days_until,
                                sigdate.event,
                                formatted_name,
                                years_since,
                                if years_since == 1 { " year" } else { " years" }
                            );
                        }
                        showed_sigdate = true;
                    } else if days_until < 0 {
                        // Check next year's anniversary
                        let next_year_anniversary = anniversary_in(sigdate.date, today.year() + 1);
                        if let Some(next_anniversary) = next_year_anniversary {
                            let days_until_next = (next_anniversary - today).num_days();
                            debug!(
                                "DEBUG: Next year's anniversary is {}, days until: {}",
                                next_anniversary, days_until_next
                            );

                            if days_until_next >= 0 && days_until_next <= delta as i64 {
                                let years_since = (today.year() + 1) - sigdate.date.year();
                                let display_name = person.nick.as_ref().unwrap_or(&person.name);

                                // Format the person's name with tier symbol and color if available
                                let formatted_name = if let Some(symbol) = &tier_symbol {
                                    if let Some(color_str) = &tier_color {
                                        use colored::Colorize;
                                        let colored_name = match color_str.to_uppercase().as_str() {
                                            "PINK" => display_name.magenta(),
                                            "GREEN" => display_name.green(),
                                            "CYAN" => display_name.cyan(),
                                            "RED" => display_name.red(),
                                            "BLUE" => display_name.blue(),
                                            "YELLOW" => display_name.yellow(),
                                            "PURPLE" => display_name.purple(),
                                            "WHITE" => display_name.white(),
                                            "BLACK" => display_name.black(),
                                            "BRIGHT_RED" => display_name.bright_red(),
                                            "BRIGHT_GREEN" => display_name.bright_green(),
                                            "BRIGHT_YELLOW" => display_name.bright_yellow(),
                                            "BRIGHT_BLUE" => display_name.bright_blue(),
                                            "BRIGHT_MAGENTA" => display_name.bright_magenta(),
                                            "BRIGHT_CYAN" => display_name.bright_cyan(),
                                            "BRIGHT_WHITE" => display_name.bright_white(),
                                            _ => display_name.normal(), // default to no color
                                        };
                                        format!("{} {}", symbol, colored_name)
                                    } else {
                                        format!("{} {}", symbol, display_name)
                                    }
                                } else {
                                    display_name.to_string()
                                };

                                // Find matching trait_default for this sigdate event
                                let trait_info =
                                    trait_defaults.iter().find(|t| t.key == sigdate.event);

                                if let Some(trait_def) = trait_info {
                                    println!(
                                        "{} {} days until {} for {} ({}{})",
                                        trait_def.symbol,
                                        days_until_next,
                                        sigdate.event,
                                        formatted_name,
                                        years_since,
                                        if years_since == 1 { " year" } else { " years" }
                                    );
                                } else {
                                    println!(
                                        "{} days until {} for {} ({}{})",
                                        days_until_next,
                                        sigdate.event,
                                        formatted_name,
                                        years_since,
                                        if years_since == 1 { " year" } else { " years" }
                                    );
                                }
                                showed_sigdate = true;
                            } else {
                                debug!("DEBUG: Next year's anniversary days until ({}) is outside delta window (0 to {})", days_until_next, delta);
                            }
                        }
                    } else {
                        debug!(
                            "DEBUG: Days until this year ({}) is outside delta window (0 to {})",
                            days_until, delta
                        );
                    }
                } else {
                    debug!(
                        "DEBUG: Could not calculate anniversary for {}",
                        sigdate.date
                    );
                }
            } else {
                debug!("DEBUG: No tier delta found for this sigdate");
            }
        }

        // Record when we showed them, so OnlyOnce and OnceHourly can skip them next time
        if showed_sigdate
            && matches!(
                remind,
                MotdRemindEnum::OnlyOnce | MotdRemindEnum::OnceHourly
            )
        {
            use sea_orm::ActiveValue::Set;

            // Keep any other keys already in meta
            let mut meta_value = match &person.meta {
                Some(serde_json::Value::Object(m)) => m.clone(),
                _ => serde_json::Map::new(),
            };
            meta_value.insert(
                "last_reminded".to_string(),
                today.format("%Y-%m-%d").to_string().into(),
            );
            meta_value.insert("last_reminded_hour".to_string(), this_hour.clone().into());

            let mut person_active: entities::ppl::ActiveModel = person.into();
            person_active.meta = Set(Some(serde_json::Value::Object(meta_value)));

            if let Err(e) = person_active.update(db).await {
                debug!("DEBUG: Failed to update person meta: {}", e);
            } else {
                debug!("DEBUG: Updated last_reminded for person to {}", this_hour);
            }
        }
    }

    Ok(())
}

//
// fn show() -> io::Result<()> {
//     enable_raw_mode()?;
//     stdout().execute(EnterAlternateScreen)?;
//     let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
//
//     let mut should_quit = false;
//     while !should_quit {
//         terminal.draw(ui)?;
//         should_quit = handle_events()?;
//     }
//
//     disable_raw_mode()?;
//     // stdout().execute(LeaveAlternateScreen)?;
//     Ok(())
// }
//
// fn handle_events() -> io::Result<bool> {
//     if event::poll(std::time::Duration::from_millis(50))? {
//         if let Event::Key(key) = event::read()? {
//             if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
//                 return Ok(true);
//             }
//         }
//     }
//     Ok(false)
// }
//
// fn ui(frame: &mut Frame) {
//     frame.render_widget(
//         Paragraph::new("Hello World!")
//             .block(Block::default().title("Greeting").borders(Borders::ALL)),
//         frame.size(),
//     );
// }
//
//
// fn main() {
//     let cli = Cli::parse();
//
//     // You can check for the existence of subcommands, and if found use their
//     // matches just as you would the top level cmd
//     match &cli.command {
//         Some(Commands::Edit { name }) => {
//             println!("'myapp add' was used, name is: {:?}", name)
//         }
//         Some(Commands::Show {}) => {
//             println!("showing but selected");
//             show().expect("failed2draw")
//         }
//         None => {
//             println!("Showing PPL");
//             show().expect("failed2draw")
//         }
//         Some(Commands::MOTD { .. }) => {}
//     }
// }