nunc 0.2.0

WIP: time tracking application
Documentation
use clap::Subcommand;
use conciliator::{
	Claw,
	Conciliator
};
use greg::Point;

use crate::error::Result;
use crate::Nunc;
use crate::cli::arg;
use crate::core::{
	Log,
	Tree,
	Zone
};


#[derive(Subcommand)]
pub(crate) enum Cmd {
	/// Start a new entry for an activity
	Start {
		activity: arg::Activity,

		when: Option<arg::TimeArg>
	},
	/// Stop a running entry
	Stop {
		when: Option<arg::TimeArg>,

		#[clap(short, long)]
		activity: Option<arg::Activity>
	},
}

pub(crate) fn cmd_start(
	con: &Claw,
	nunc: &Nunc,
	activity: arg::Activity,
	when: Option<arg::TimeArg>)
	-> Result<()>
{
	let cal = nunc.load_calendar()?;

	let when = match when {
		Some(t) => {
			let validated = t.resolve_with(&cal);
			con.status("Parsed time: ").push(cal.lookup(validated));
			validated
		},
		None => Point::now()
	};
	let act = activity.validate(nunc, con)?;

	nunc.start(&act, when)?;
	con.status("Started ")
		.push(&act)
		.push(" at ")
		.push(cal.lookup(when));

	Ok(())
}

pub(crate) fn cmd_stop(
	con: &Claw,
	nunc: &Nunc,
	activity: Option<arg::Activity>,
	when: Option<arg::TimeArg>)
	-> Result<()>
{
	let cal = nunc.load_calendar()?;
	let when = match when {
		Some(t) => {
			let validated = t.resolve_with(&cal);
			con.status("Parsed time: ").push(cal.lookup(validated));
			validated
		},
		None => Point::now()
	};

	let mut active = nunc.get_active_entries()?;

	if let Some(activity) = activity {
		let activity = activity.validate(nunc, con)?;
		active.retain(|ent| ent.activity_id == activity.id);
		if active.is_empty() {ret_errmsg!("Activity not started!")}
	}

	let (ent, act) = match active.len() {
		0 => ret_errmsg!("No started activities!"),
		1 => {
			let ent = active.first().unwrap();
			let act = nunc.get_activity(ent.activity_id)?;
			(ent, act)
		}
		_ => {
			let activities = active
				.iter()
				.map(|ent| nunc
					.get_activity(ent.activity_id)
					.and_then(|act| nunc
						.get_path_string(act.category_id)
						.map(|path| act.resolve(path))
					)
					.map_err(Into::into)
				)
				.collect::<Result<Vec<_>>>()?;
			let selected = con
				.select(activities, "activities", "Select activity")
				.unwrap();
			let entry = active
				.iter()
				.find(|ent| ent.activity_id == selected.id)
				.unwrap();
			(entry, selected.revert())
		}
	};
	if nunc.stop(ent, when)? {
		con.status("Stopped ")
			.push(&act)
			.push(" at ")
			.push(cal.lookup(when));
		Ok(())
	}
	else {
		con.error("Failed to stop ")
			.push(&act)
			.push(" at ")
			.push(cal.lookup(when));
		ret_errmsg!("Failed to stop activity!")
	}
}


impl Cmd {
	pub(crate) fn run(self, con: &Claw, nunc: &Nunc) -> Result<()> {
		match self {
			Cmd::Start {activity, when} => cmd_start(con, nunc, activity, when),
			Cmd::Stop {activity, when} => cmd_stop(con, nunc, activity, when)
		}
	}
}


#[test]
fn start_stop() -> Result<()> {
	use crate::cli::Tester;
	let tester = Tester::init()?;

	tester.run(["start", "Invalid"]).unwrap_err();

	tester.run(["create", "category", "/", "root"])?;
	tester.run(["create", "activity", "/root", "one"])?;

	tester.run(["start", "one"])?;
	tester.run(["stop", "10m"]).unwrap_err();
	tester.run(["stop"])?;
	tester.run(["stop"]).unwrap_err();

	// FLAKY: this probably fails at exactly 22:15:00
	tester.run(["start", "one", "10:15"])?;
	tester.run(["stop", "10:14:59"]).unwrap_err();
	tester.run(["stop", "10:15:01"])?;
	tester.run(["stop"]).unwrap_err();

	tester.run(["start", "one", "2022-01-01 10:15"])?;
	tester.run(["stop", "2022-01-01 10:14:59"]).unwrap_err();
	tester.run(["stop", "2022-01-02"])?;
	tester.run(["stop"]).unwrap_err();

	tester.run(["start", "one"])?;
	tester.run(["stop", "-a", "one"])?;
	tester.run(["stop", "-a", "one"]).unwrap_err();

	tester.run(["create", "activity", "/root", "two"])?;

	tester.run(["start", "one"])?;
	tester.run(["start", "one"]).unwrap_err();
	tester.run(["start", "two"])?;
	tester.run(["stop", "-a", "one"])?;
	tester.run(["start", "two"]).unwrap_err();
	tester.run(["stop", "-a", "two"])?;
	tester.run(["stop", "-a", "two"]).unwrap_err();


/*
	BUG: 0s entry touching incorrectly counts as overlap
	tester.run(["start", "one", "2022-10-10 12:34:00"])?;
	tester.run(["stop", "2022-10-10 12:34:01"])?;

	tester.run(["start", "one", "2022-10-10 12:34:01"])?;
	tester.run(["stop", "2022-10-10 12:34:01"])?;
*/

/*
	// Interactive entry disambiguation
	tester.run(["start", "one"])?;
	tester.run(["start", "two"])?;
	tester.run(["stop"])?;
*/
	Ok(())
}