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};
use std::collections::HashMap;
use std::path::PathBuf;
fn main() {
if let Err(e) = run() {
eprintln!("error: {:#}", e);
std::process::exit(1);
}
}
fn run() -> Result<()> {
let raw_args: Vec<String> = std::env::args().collect();
let early_config_path = extract_config_path_arg(&raw_args)
.unwrap_or_else(default_config_path);
let command_aliases: HashMap<String, String> = if early_config_path.exists() {
Config::load(&early_config_path)
.map(|c| c.command_aliases)
.unwrap_or_default()
} else {
HashMap::new()
};
let cli_args = resolve_command_alias_in_args(raw_args, &command_aliases);
let cli = Cli::parse_from(cli_args);
let config_path = cli.config_path
.as_deref()
.map(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")?;
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,
name: None,
},
_ => 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, name } => {
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, name, refs, all)?;
}
Commands::Append { kind, body, tags, status, at, start_time, end_time, name } => {
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, name, 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, name } => {
let since_date = since.as_deref().map(date_parse::parse_date).transpose()?;
commands::search::run(&config, &query, r#type, tag, name, 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, name } => {
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, name)?;
}
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(())
}
fn resolve_alias(kind: &str, config: &Config) -> String {
let normalized = kind.to_lowercase();
config.type_aliases
.get(&normalized)
.cloned()
.unwrap_or(normalized)
}
fn extract_config_path_arg(args: &[String]) -> Option<PathBuf> {
let mut iter = args.iter().skip(1);
while let Some(arg) = iter.next() {
if arg == "--config-path" {
return iter.next().map(PathBuf::from);
}
if let Some(val) = arg.strip_prefix("--config-path=") {
return Some(PathBuf::from(val));
}
}
None
}
fn resolve_command_alias_in_args(
mut args: Vec<String>,
command_aliases: &HashMap<String, String>,
) -> Vec<String> {
if command_aliases.is_empty() { return args; }
let mut i = 1usize;
while i < args.len() {
let arg = &args[i];
if arg == "--config-path" {
i += 2; continue;
}
if arg.starts_with('-') {
i += 1; continue;
}
let lower = arg.to_lowercase();
if let Some(resolved) = command_aliases.get(&lower) {
args[i] = resolved.clone();
}
break;
}
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_config_path_arg_present() {
let args = vec!["mps".into(), "--config-path".into(), "/tmp/cfg.yaml".into(), "list".into()];
assert_eq!(extract_config_path_arg(&args), Some(PathBuf::from("/tmp/cfg.yaml")));
}
#[test]
fn test_extract_config_path_arg_equals_form() {
let args = vec!["mps".into(), "--config-path=/tmp/cfg.yaml".into(), "list".into()];
assert_eq!(extract_config_path_arg(&args), Some(PathBuf::from("/tmp/cfg.yaml")));
}
#[test]
fn test_extract_config_path_arg_absent() {
let args = vec!["mps".into(), "list".into()];
assert_eq!(extract_config_path_arg(&args), None);
}
#[test]
fn test_resolve_command_alias_basic() {
let aliases = HashMap::from([("a".to_string(), "append".to_string())]);
let args = vec!["mps".into(), "a".into(), "note".into(), "body".into()];
let out = resolve_command_alias_in_args(args, &aliases);
assert_eq!(out[1], "append");
assert_eq!(out[2], "note"); }
#[test]
fn test_resolve_command_alias_skips_config_path_flag() {
let aliases = HashMap::from([("a".to_string(), "append".to_string())]);
let args = vec!["mps".into(), "--config-path".into(), "/tmp/x".into(), "a".into(), "n".into()];
let out = resolve_command_alias_in_args(args, &aliases);
assert_eq!(out[3], "append");
assert_eq!(out[1], "--config-path"); }
#[test]
fn test_resolve_command_alias_skips_boolean_flag() {
let aliases = HashMap::from([("a".to_string(), "append".to_string())]);
let args = vec!["mps".into(), "--force".into(), "a".into(), "note".into()];
let out = resolve_command_alias_in_args(args, &aliases);
assert_eq!(out[2], "append");
}
#[test]
fn test_resolve_command_alias_no_match_unchanged() {
let aliases = HashMap::from([("a".to_string(), "append".to_string())]);
let args = vec!["mps".into(), "list".into()];
let out = resolve_command_alias_in_args(args.clone(), &aliases);
assert_eq!(out, args);
}
#[test]
fn test_resolve_command_alias_empty_map() {
let aliases = HashMap::new();
let args = vec!["mps".into(), "x".into()];
let out = resolve_command_alias_in_args(args.clone(), &aliases);
assert_eq!(out, args);
}
#[test]
fn test_resolve_command_alias_symbol_like() {
let aliases = HashMap::from([("+".to_string(), "append".to_string())]);
let args = vec!["mps".into(), "+".into(), "note".into()];
let out = resolve_command_alias_in_args(args, &aliases);
assert_eq!(out[1], "append");
}
#[test]
fn test_resolve_command_alias_case_insensitive() {
let aliases = HashMap::from([("a".to_string(), "append".to_string())]);
let args = vec!["mps".into(), "A".into(), "note".into()];
let out = resolve_command_alias_in_args(args, &aliases);
assert_eq!(out[1], "append");
}
}
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)
}
}
}