use crate::config::{Config, NotifyConfig};
use crate::meta::{MetaLocal, MetaShared};
use anyhow::{bail, Result};
use colored::Colorize;
use std::path::Path;
pub fn run(config: &Config, config_path: &Path, args: &[String]) -> Result<()> {
let sub = args.first().map(|s| s.as_str()).unwrap_or("show");
match sub.to_lowercase().as_str() {
"show" => show(&config.storage_dir),
"clear" => clear(&config.storage_dir),
"edit" => edit(&config.storage_dir),
"sync" => {
let direction = args.get(1).map(|s| s.as_str()).unwrap_or("");
match direction.to_lowercase().as_str() {
"up" => sync_up(config_path, &config.storage_dir),
"down" => sync_down(config_path, &config.storage_dir),
other => bail!(
"Unknown sync direction '{}'. Use: sync up, sync down",
other
),
}
}
other => bail!(
"Unknown meta subcommand '{}'. Use: show, clear, edit, sync up, sync down",
other
),
}
}
fn show(storage_dir: &Path) -> Result<()> {
let shared = MetaShared::load(storage_dir);
let local = MetaLocal::load(storage_dir);
println!(
"{}",
"── .mps.meta (git-tracked config layer) ─────────────".white()
);
println!(" path : {}", MetaShared::path(storage_dir).display());
println!(" version : {}", shared.version);
if !shared.config.type_aliases.is_empty() {
let mut pairs: Vec<_> = shared
.config
.type_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
pairs.sort();
println!(" type_aliases : {}", pairs.join(", "));
}
if !shared.config.command_aliases.is_empty() {
let mut pairs: Vec<_> = shared
.config
.command_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
pairs.sort();
println!(" command_aliases : {}", pairs.join(", "));
}
if let Some(ref dc) = shared.config.default_command {
println!(" default_command : {}", dc);
}
if !shared.config.custom_tags.is_empty() {
println!(
" custom_tags : {}",
shared.config.custom_tags.join(", ")
);
}
let n = &shared.config.notify;
println!(" notify.enabled : {}", n.enabled);
println!(" notify.window_minutes : {}", n.window_minutes);
println!(" notify.notify_open_tasks: {}", n.notify_open_tasks);
if let Some(ref t) = n.task_notify_at {
println!(" notify.task_notify_at : {}", t);
}
if !n.open_task_tags.is_empty() {
println!(
" notify.open_task_tags : {}",
n.open_task_tags.join(", ")
);
}
println!(" notify.overdue_days : {}", n.overdue_days);
println!();
println!(
"{}",
"── .mps.local (gitignored local state) ───────────────".white()
);
println!(
" path : {}",
MetaLocal::path(storage_dir).display()
);
println!(" notified entries : {}", local.notified.len());
if let Some(ref d) = local.last_task_date {
println!(" last_task_date : {}", d);
}
if !local.cache.tag_counts.is_empty() {
println!(" cached tag counts: {} tags", local.cache.tag_counts.len());
}
Ok(())
}
fn clear(storage_dir: &Path) -> Result<()> {
let path = MetaLocal::path(storage_dir);
if path.exists() {
std::fs::remove_file(&path)?;
println!(" {} {}", "cleared".green(), path.display());
} else {
println!(" (nothing to clear — .mps.local does not exist)");
}
Ok(())
}
fn sync_up(config_path: &Path, storage_dir: &Path) -> Result<()> {
let raw = Config::load(config_path)?;
let mut meta = MetaShared::load(storage_dir);
meta.version = meta.version.max(1);
meta.config.type_aliases = raw.type_aliases.clone();
meta.config.command_aliases = raw.command_aliases.clone();
meta.config.default_command = Some(raw.default_command.clone());
meta.config.custom_tags = raw.custom_tags.clone();
meta.config.notify = raw.notify.clone();
meta.save(storage_dir)?;
println!(" {} YAML config → .mps.meta", "synced:".green());
if !meta.config.type_aliases.is_empty() {
let mut pairs: Vec<_> = meta
.config
.type_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
pairs.sort();
println!(" type_aliases : {}", pairs.join(", "));
}
if !meta.config.command_aliases.is_empty() {
let mut pairs: Vec<_> = meta
.config
.command_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
pairs.sort();
println!(" command_aliases : {}", pairs.join(", "));
}
if let Some(ref dc) = meta.config.default_command {
println!(" default_command : {}", dc);
}
if !meta.config.custom_tags.is_empty() {
println!(" custom_tags : {}", meta.config.custom_tags.join(", "));
}
let n = &meta.config.notify;
println!(
" notify.task_notify_at : {}",
n.task_notify_at.as_deref().unwrap_or("(not set)")
);
println!(" notify.window_minutes : {}", n.window_minutes);
println!(" notify.overdue_days : {}", n.overdue_days);
println!(
" {} run {} to push to other devices",
"hint:".cyan(),
"mps autogit".bold()
);
Ok(())
}
fn sync_down(config_path: &Path, storage_dir: &Path) -> Result<()> {
let meta = MetaShared::load(storage_dir);
let mut cfg = Config::load(config_path)?;
for (k, v) in &meta.config.type_aliases {
cfg.type_aliases.insert(k.clone(), v.clone());
}
for (k, v) in &meta.config.command_aliases {
cfg.command_aliases.insert(k.clone(), v.clone());
}
if let Some(ref dc) = meta.config.default_command {
cfg.default_command = dc.clone();
}
for t in &meta.config.custom_tags {
if !cfg.custom_tags.contains(t) {
cfg.custom_tags.push(t.clone());
}
}
let n = &meta.config.notify;
let def = NotifyConfig::default();
if !n.enabled {
cfg.notify.enabled = false;
}
if !n.notify_open_tasks {
cfg.notify.notify_open_tasks = false;
}
if n.task_notify_at.is_some() {
cfg.notify.task_notify_at = n.task_notify_at.clone();
}
if !n.open_task_tags.is_empty() {
cfg.notify.open_task_tags = n.open_task_tags.clone();
}
if n.window_minutes != def.window_minutes {
cfg.notify.window_minutes = n.window_minutes;
}
if n.task_cooldown_minutes != def.task_cooldown_minutes {
cfg.notify.task_cooldown_minutes = n.task_cooldown_minutes;
}
if n.overdue_days != def.overdue_days {
cfg.notify.overdue_days = n.overdue_days;
}
cfg.save(config_path)?;
println!(
" {} .mps.meta → {}",
"synced:".green(),
config_path.display()
);
println!(
" {} machine-specific paths (storage_dir, mps_dir, log_file) were not changed",
"note:".cyan()
);
Ok(())
}
fn edit(storage_dir: &Path) -> Result<()> {
let path = MetaShared::path(storage_dir);
if !path.exists() {
let empty = MetaShared::default();
empty.save(storage_dir)?;
}
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| "vim".to_string());
println!(
"{}",
format!("Opening {} in editor", path.display()).white()
);
std::process::Command::new(&editor)
.arg(&path)
.status()
.map_err(|e| anyhow::anyhow!("failed to launch editor '{}': {}", editor, e))?;
match std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str::<MetaShared>(&s).ok())
{
Some(_) => println!(" {} .mps.meta is valid JSON", "ok:".green()),
None => eprintln!(
" {} .mps.meta contains invalid JSON — it will be ignored until fixed",
"warn:".yellow()
),
}
println!(
" {} run {} to sync this config to other devices",
"hint:".cyan(),
"mps autogit".bold()
);
Ok(())
}