diff --git a/src/main.rs b/src/main.rs index b7bf7db..d2c506e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ struct Args { } #[derive(clap::Subcommand, Debug, PartialEq, Eq)] -#[clap(version, about, author, global_setting=clap::AppSettings::DisableHelpSubcommand)] +#[clap(version, help_short='H', about, author, global_setting=clap::AppSettings::DisableHelpSubcommand)] enum Command { /// Create a new task. New { @@ -74,6 +74,14 @@ enum Command { // - which columns to include // - filters which exclude values }, + /// For tracking time against a note. + Track { + id_or_name : String, + #[clap(short, default_value_t=0)] + hours : u16, + #[clap(short, default_value_t=0)] + minutes : u16, + }, /// For making changes to global configuration. #[clap(subcommand)] Config(ConfigCommand), @@ -227,6 +235,13 @@ fn program() -> Result<(), error::Error> { } println!("Updated task {}", colour::id(&id.to_string())); }, + Track { id_or_name, hours, minutes } => { + let id = state.name_or_id_to_id(&id_or_name)?; + let mut task = tasks::Task::load(id, vault_folder, false)?; + let entry = tasks::TimeEntry::new(hours, minutes); + task.data.time_entries.push(entry); + task.save()?; + }, Discard { id_or_name } => { let id = state.name_or_id_to_id(&id_or_name)?; let mut task = tasks::Task::load(id, vault_folder, false)?; diff --git a/src/state.rs b/src/state.rs index f2d622e..0abcf98 100644 --- a/src/state.rs +++ b/src/state.rs @@ -80,9 +80,11 @@ impl State { .create(true) .open(&path)?; + let file_contents = toml::to_string(&data)?; + file.set_len(0)?; file.seek(io::SeekFrom::Start(0))?; - file.write_all(toml::to_string(&data)?.as_bytes())?; + file.write_all(file_contents.as_bytes())?; let task = Self { file, @@ -100,9 +102,11 @@ impl State { data, } = self; + let file_contents = toml::to_string(&data)?; + file.set_len(0)?; file.seek(io::SeekFrom::Start(0))?; - file.write_all(toml::to_string(&data)?.as_bytes())?; + file.write_all(file_contents.as_bytes())?; Ok(()) } diff --git a/src/tasks.rs b/src/tasks.rs index dab7d4d..d088327 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -51,10 +51,26 @@ impl Priority { } } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TimeEntry { - hours : u32, - minutes : u8, + logged_date : chrono::NaiveDate, + hours : u16, + minutes : u16, +} + +impl TimeEntry { + pub fn new(hours : u16, minutes : u16) -> Self { + + let (hours, minutes) = { + (hours + minutes / 60, minutes % 60) + }; + + Self { + logged_date : chrono::Utc::now().naive_local().date(), + hours, + minutes, + } + } } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -66,10 +82,10 @@ pub struct InternalTask { pub dependencies : HashSet, pub priority : Priority, //due : Option, - pub time_entries : Vec, pub created : chrono::NaiveDateTime, pub complete : bool, pub discarded : bool, + pub time_entries : Vec, } impl Task { @@ -102,9 +118,11 @@ impl Task { discarded : false, }; + let file_contents = toml::to_string(&data)?; + file.set_len(0)?; file.seek(io::SeekFrom::Start(0))?; - file.write_all(toml::to_string(&data)?.as_bytes())?; + file.write_all(file_contents.as_bytes())?; state.index_insert(data.name.clone(), id); @@ -186,9 +204,11 @@ impl Task { data, } = self; + let file_contents = toml::to_string(&data)?; + file.set_len(0)?; file.seek(io::SeekFrom::Start(0))?; - file.write_all(toml::to_string(&data)?.as_bytes())?; + file.write_all(file_contents.as_bytes())?; Ok(()) } @@ -207,7 +227,7 @@ impl Task { } pub fn display(&self) -> Result<(), error::Error> { - + fn line(len : usize) { for _ in 0..len { print!("-"); @@ -215,15 +235,22 @@ impl Task { println!(); } - let id = &self.data.id.to_string(); - let discarded = if self.data.discarded { String::from(" (discarded)") } else { String::new() }; - let heading = format!("[{}] {} {}{}", if self.data.complete {"X"} else {" "}, colour::id(id), colour::task_name(&self.data.name), colour::greyed_out(&discarded)); - println!("{}", heading); + let (heading, heading_length) = { + let id = &self.data.id.to_string(); + let discarded = if self.data.discarded { String::from(" (discarded)") } else { String::new() }; - line(5 + self.data.name.chars().count() + id.chars().count() + discarded.chars().count()); - println!("Priority: {}", self.data.priority.coloured()); - println!("Tags: [{}]", format_hash_set(&self.data.tags)?); - println!("Created: {}", self.data.created); + ( + format!("[{}] {} {}{}", if self.data.complete {"X"} else {" "}, colour::id(id), colour::task_name(&self.data.name), colour::greyed_out(&discarded)), + 5 + self.data.name.chars().count() + id.chars().count() + discarded.chars().count() + ) + }; + + println!("{}", heading); + line(heading_length); + + println!("Priority: {}", self.data.priority.coloured()); + println!("Tags: [{}]", format_hash_set(&self.data.tags)?); + println!("Created: {}", self.data.created); if let Some(mut info) = self.data.info.clone() { let mut max_line_width = 0; @@ -238,10 +265,18 @@ impl Task { max_line_width = usize::max(max_line_width, line.chars().count() + 4); println!(" {}", line); } - line(usize::min(max_line_width, usize::try_from(termsize::get().map(|s| s.cols).unwrap_or(0)).unwrap())); } - else { - // Need to work out appropriate line size. + + if !self.data.time_entries.is_empty() { + + let mut entries = self.data.time_entries.clone(); + // Sort entries by date. + entries.sort_by(|e1, e2| e1.logged_date.cmp(&e2.logged_date)); + + println!("Time Entries:"); + for entry in &entries { + println!(" {}:{:0>2} [{}]", entry.hours, entry.minutes, entry.logged_date); + } } // dependencies as a tree