mps-rs 1.0.0

MPS — plain-text personal productivity CLI (Rust)
Documentation
mod cli;
mod config;
mod constants;
mod date_parse;
mod elements;
mod error;
mod parser;
mod ref_resolver;
mod store;
mod commands;

use anyhow::{Context, Result};
use chrono::NaiveDate;
use clap::Parser as ClapParser;
use cli::{Cli, Commands};
use config::{Config, default_config_path};

fn main() {
    if let Err(e) = run() {
        eprintln!("error: {:#}", e);
        std::process::exit(1);
    }
}

fn run() -> Result<()> {
    let cli = Cli::parse();

    let config_path = cli.config_path
        .as_deref()
        .map(std::path::PathBuf::from)
        .unwrap_or_else(default_config_path);

    if !config_path.exists() || cli.force {
        Config::init(&config_path)
            .with_context(|| format!("failed to init config at {}", config_path.display()))?;
    }

    let config = Config::load(&config_path)
        .with_context(|| format!("failed to load config from {}", config_path.display()))?;

    config.ensure_dirs()
        .context("failed to create MPS directories")?;

    // Resolve the default command from config for bare invocations.
    let default_cmd = config.default_command.to_lowercase();

    let command = cli.command.unwrap_or_else(|| {
        match default_cmd.as_str() {
            "list" => Commands::List {
                datesign: None,
                r#type:   None,
                tag:      None,
                status:   None,
                since:    None,
                refs:     false,
                all:      false,
            },
            _ => Commands::Open { datesign: None },
        }
    });

    match command {
        Commands::Version => {
            println!("mps (v{})", env!("CARGO_PKG_VERSION"));
        }

        Commands::Open { datesign } => {
            let date = resolve_date(datesign.as_deref().unwrap_or("today"))?;
            commands::open::run(&config, date)?;
        }

        Commands::List { datesign, r#type, tag, status, since, refs, all } => {
            let date  = resolve_date(datesign.as_deref().unwrap_or("today"))?;
            let dates = resolve_date_range(since.as_deref(), date)?;
            commands::list::run(&config, date, dates, r#type, tag, status, refs, all)?;
        }

        Commands::Append { kind, body, tags, status, at, start_time, end_time } => {
            let body_str  = body.join(" ");
            let kind_resolved = resolve_alias(&kind, &config);
            let tags_vec: Vec<String> = tags
                .as_deref()
                .unwrap_or("")
                .split(',')
                .map(|t| t.trim().to_string())
                .filter(|t| !t.is_empty())
                .collect();
            let date = chrono::Local::now().date_naive();
            commands::append::run(
                &config, &kind_resolved, &body_str, tags_vec,
                status, at, start_time, end_time, date,
            )?;
        }

        Commands::Update { ref_path, status, start_time, end_time, at, date } => {
            commands::update::run(&config, &ref_path, status, start_time, end_time, at, date)?;
        }

        Commands::Done { ref_path, date } => {
            commands::update::run_done(&config, &ref_path, date)?;
        }

        Commands::Search { query, r#type, tag, since } => {
            let since_date = since.as_deref().map(date_parse::parse_date).transpose()?;
            commands::search::run(&config, &query, r#type, tag, since_date)?;
        }

        Commands::Stats { datesign, since, all } => {
            let date  = resolve_date(datesign.as_deref().unwrap_or("today"))?;
            let dates = resolve_date_range(since.as_deref(), date)?;
            commands::stats::run(&config, dates, all)?;
        }

        Commands::Tags { datesign, r#type, status, since, all } => {
            let date  = resolve_date(datesign.as_deref().unwrap_or("today"))?;
            let dates = resolve_date_range(since.as_deref(), date)?;
            commands::tags::run(&config, dates, all, r#type, status)?;
        }

        Commands::Export { datesign, format, r#type, since } => {
            let date  = resolve_date(datesign.as_deref().unwrap_or("today"))?;
            let dates = resolve_date_range(since.as_deref(), date)?;
            commands::export::run(&config, dates, &format, r#type)?;
        }

        Commands::Config { subcommand } => {
            commands::config_cmd::run(&config, &config_path, subcommand.as_deref())?;
        }

        Commands::Git { args } => {
            commands::git::run_git(&config, &args)?;
        }

        Commands::Autogit => {
            commands::git::run_autogit(&config)?;
        }

        Commands::Cmd { args } => {
            commands::git::run_cmd(&config, &args)?;
        }
    }

    Ok(())
}

/// Resolve a type string through the config's alias map.
/// e.g. "t" → "task" if aliases contains {"t": "task"}.
fn resolve_alias(kind: &str, config: &Config) -> String {
    let normalized = kind.to_lowercase();
    config.aliases
        .get(&normalized)
        .cloned()
        .unwrap_or(normalized)
}

fn resolve_date(datesign: &str) -> Result<NaiveDate> {
    date_parse::parse_date(datesign)
        .with_context(|| format!("cannot parse date '{}'", datesign))
}

fn resolve_date_range(since: Option<&str>, to_date: NaiveDate) -> Result<Vec<NaiveDate>> {
    match since {
        None => Ok(vec![to_date]),
        Some(s) => {
            let since_date = resolve_date(s)?;
            let mut dates = Vec::new();
            let mut d = since_date;
            while d <= to_date {
                dates.push(d);
                d += chrono::Duration::days(1);
            }
            Ok(dates)
        }
    }
}