diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/Cargo.lock b/Cargo.lock old mode 100644 new mode 100755 index 9a934d3..eb622d1 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,7 +775,7 @@ dependencies = [ [[package]] name = "toru" -version = "0.2.0" +version = "0.2.1" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/src/colour.rs b/src/colour.rs old mode 100644 new mode 100755 diff --git a/src/config.rs b/src/config.rs old mode 100644 new mode 100755 diff --git a/src/edit.rs b/src/edit.rs old mode 100644 new mode 100755 diff --git a/src/error.rs b/src/error.rs old mode 100644 new mode 100755 diff --git a/src/graph.rs b/src/graph.rs old mode 100644 new mode 100755 index 4f9caed..214b062 --- a/src/graph.rs +++ b/src/graph.rs @@ -71,6 +71,19 @@ impl Graph { } } + /// Gets all tasks which have dependents. + pub fn get_tasks_with_dependents(&self) -> HashSet { + let mut tasks_with_dependents = HashSet::new(); + + for (_, outgoing) in &self.edges { + for edge in outgoing { + tasks_with_dependents.insert(*edge); + } + } + + tasks_with_dependents + } + pub fn find_cycle(&self) -> Option> { // All unvisited nodes, populated with all nodes at the start, to not miss disconnected diff --git a/src/index.rs b/src/index.rs old mode 100644 new mode 100755 diff --git a/src/main.rs b/src/main.rs old mode 100644 new mode 100755 index 376f200..c2ababb --- a/src/main.rs +++ b/src/main.rs @@ -101,82 +101,68 @@ enum Command { #[derive(clap::StructOpt, Debug, PartialEq, Eq)] pub struct ListOptions { - /// Include name column. - #[clap(long)] - name : bool, - /// Include tracked time column. - #[clap(long)] - tracked : bool, - /// Include due date column. - #[clap(long)] - due : bool, - /// Include tags column. - #[clap(long)] - tags : bool, - /// Include priority column. - #[clap(long)] - priority : bool, - /// Include status column. - #[clap(long)] - status : bool, - /// Include created date column. - #[clap(long)] - created : bool, - /// The field to sort by. - #[clap(long, value_enum, default_value_t=SortBy::Id)] - sort_by : SortBy, + /// Which columns to include. + #[clap(short, value_enum)] + column : Vec, + /// Field to order by. + #[clap(long, value_enum, default_value_t=OrderBy::Id)] + order_by : OrderBy, /// Sort ascending on descending. - #[clap(long, value_enum, default_value_t=SortType::Asc)] - sort_type : SortType, - /// Only include tasks created before a certain date. + #[clap(long, value_enum, default_value_t=Order::Asc)] + order : Order, + /// Tags to include. + #[clap(short, long)] + tag : Vec, + /// Only include tasks due before a certain date (inclusive). #[clap(long)] - before : Option, - /// Only include tasks created after a certain date. + due_before : Option, + /// Only include tasks due after a certain date (inclusive). #[clap(long)] - after : Option, - /// Only include tasks due within a certain number of days. + due_after : Option, + /// Only include tasks created before a certain date (inclusive). #[clap(long)] - due_in : Option, + created_before : Option, + /// Only include tasks created after a certain date (inclusive). + #[clap(long)] + created_after : Option, /// Include completed tasks in the list. #[clap(long)] include_completed : bool, -} - -impl Default for ListOptions { - fn default() -> Self { - Self { - name : true, - tracked : true, - due : true, - tags : true, - priority : true, - status : false, - created : false, - sort_by : SortBy::Created, - sort_type : SortType::Desc, - before : None, - after : None, - due_in : None, - include_completed : false, - } - } + /// Only include notes with no dependencies [alias: bottom-level]. + #[clap(long, alias="bottom-level")] + no_dependencies : bool, + /// Only include notes with no dependents [alias: top-level]. + #[clap(long, alias="top-level")] + no_dependents : bool, } #[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)] -pub enum SortType { +pub enum Order { #[default] Asc, Desc, } +#[derive(Default, Hash, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)] +pub enum Column { + #[default] + Due, + Priority, + Created, + Tags, + Status, + Tracked, +} + #[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)] -pub enum SortBy { +pub enum OrderBy { #[default] Id, Name, Due, Priority, Created, + Tracked, } #[derive(clap::Subcommand, Debug, PartialEq, Eq)] @@ -373,7 +359,7 @@ fn program() -> Result<(), error::Error> { println!("Marked task {} as complete", colour::id(id)); }, List { options } => { - tasks::list(options, vault_folder)?; + tasks::list(options, vault_folder, &state)?; }, // All commands which are dealt with in if let chain at start. Vault(_) | Config(_) | Git { args : _ } | Svn { args : _ } | Switch { name : _ } | GitIgnore => unreachable!(), diff --git a/src/state.rs b/src/state.rs old mode 100644 new mode 100755 diff --git a/src/stats.rs b/src/stats.rs old mode 100644 new mode 100755 diff --git a/src/tasks.rs b/src/tasks.rs old mode 100644 new mode 100755 index 240f405..fe95e68 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -8,6 +8,7 @@ use std::fs; use std::fmt; use std::ops; use std::mem; +use std::cmp; use std::path; use std::io::{Write, Seek}; use std::collections::{HashSet, HashMap}; @@ -61,7 +62,7 @@ pub struct TimeEntry { } // Needs to preserve representation invariant of minutes < 60 -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct Duration { hours : u16, minutes : u16, @@ -427,35 +428,17 @@ fn format_due_date(due : &chrono::NaiveDateTime, include_fuzzy_period : bool, co } } - - - - -pub fn list(options : super::ListOptions, vault_folder : &path::Path) -> Result<(), error::Error> { - - let expected = super::ListOptions { - name : false, - tracked : false, - due : false, - tags : false, - priority : false, - status : false, - created : false, - sort_by : super::SortBy::Id, - sort_type : super::SortType::Asc, - before : None, - after : None, - due_in : None, - include_completed : false, - }; - - // If the arguments are not given, use a set of defaults. - let options = if options == expected { - super::ListOptions::default() +fn compare_due_dates(first : &Option, second : &Option) -> cmp::Ordering { + match (first, second) { + (None, None) => cmp::Ordering::Equal, + (Some(_), None) => cmp::Ordering::Less, + (None, Some(_)) => cmp::Ordering::Greater, + (Some(first), Some(second)) => first.cmp(second), } - else { - options - }; +} + +pub fn list(mut options : super::ListOptions, vault_folder : &path::Path, state : &state::State) -> Result<(), error::Error> { + let mut table = comfy_table::Table::new(); table @@ -466,130 +449,214 @@ pub fn list(options : super::ListOptions, vault_folder : &path::Path) -> Result< let mut tasks : Box> = Box::new(Task::load_all(vault_folder, true)?.into_iter()); - // Filter the tasks - if let Some(before) = options.before { - tasks = Box::new(tasks.filter(move |t| t.data.created < before)); + // Filter the tasks. + if let Some(date) = options.created_before { + tasks = Box::new(tasks.filter(move |t| t.data.created.date() <= date)); } - if let Some(after) = options.after { - tasks = Box::new(tasks.filter(move |t| t.data.created > after)); + if let Some(date) = options.created_after { + tasks = Box::new(tasks.filter(move |t| t.data.created.date() >= date)); } - if let Some(due_in) = options.due_in { - let now = chrono::Local::now().naive_local(); + + if let Some(date) = options.due_before { tasks = Box::new(tasks.filter(move |t| { - if let Some(due) = t.data.due { - due < now + chrono::Duration::days(i64::from(due_in)) - } - else { - false + match compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) { + cmp::Ordering::Less | cmp::Ordering::Equal => true, + cmp::Ordering::Greater => false, } })); } + + if let Some(date) = options.due_after { + tasks = Box::new(tasks.filter(move |t| { + match compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) { + cmp::Ordering::Greater | cmp::Ordering::Equal => true, + cmp::Ordering::Less => false, + } + })); + } + if !options.include_completed { tasks = Box::new(tasks.filter(|t| t.data.completed.is_none())); } + if !options.tag.is_empty() { + let specified_tags : HashSet<_> = options.tag.iter().collect(); + + tasks = Box::new(tasks.filter(move |t| { + let task_tags : HashSet<_> = t.data.tags.iter().collect(); + + // Non empty intersection of tags means the task should be displayed + specified_tags.intersection(&task_tags).next().is_some() + })); + } + + if options.no_dependencies { + tasks = Box::new(tasks.filter(move |t| { + t.data.dependencies.is_empty() + })); + } + + if options.no_dependents { + let tasks_with_dependents = state.data.deps.get_tasks_with_dependents(); + + tasks = Box::new(tasks.filter(move |t| { + !tasks_with_dependents.contains(&t.data.id) + })); + } + let mut tasks : Vec = tasks.collect(); - // Sort the tasks - use super::{SortBy, SortType}; - match options.sort_by { - SortBy::Id => { - match options.sort_type { - SortType::Asc => { + // Sort the tasks. + use super::{OrderBy, Order}; + match options.order_by { + OrderBy::Id => { + match options.order { + Order::Asc => { tasks.sort_by(|t1, t2| t1.data.id.cmp(&t2.data.id)); }, - SortType::Desc => { + Order::Desc => { tasks.sort_by(|t1, t2| t2.data.id.cmp(&t1.data.id)); }, } }, - SortBy::Name => { - match options.sort_type { - SortType::Asc => { + OrderBy::Name => { + match options.order { + Order::Asc => { tasks.sort_by(|t1, t2| t1.data.name.cmp(&t2.data.name)); }, - SortType::Desc => { + Order::Desc => { tasks.sort_by(|t1, t2| t2.data.name.cmp(&t1.data.name)); }, } }, - SortBy::Due => { - match options.sort_type { - SortType::Asc => { - tasks.sort_by(|t1, t2| t1.data.due.cmp(&t2.data.due)); + OrderBy::Due => { + match options.order { + Order::Asc => { + tasks.sort_by(|t1, t2| compare_due_dates(&t1.data.due, &t2.data.due)); }, - SortType::Desc => { - tasks.sort_by(|t1, t2| t2.data.due.cmp(&t1.data.due)); + Order::Desc => { + tasks.sort_by(|t1, t2| compare_due_dates(&t2.data.due, &t1.data.due)); }, } }, - SortBy::Priority => { - match options.sort_type { - SortType::Asc => { + OrderBy::Priority => { + match options.order { + Order::Asc => { tasks.sort_by(|t1, t2| t1.data.priority.cmp(&t2.data.priority)); }, - SortType::Desc => { + Order::Desc => { tasks.sort_by(|t1, t2| t2.data.priority.cmp(&t1.data.priority)); }, } }, - SortBy::Created => { - match options.sort_type { - SortType::Asc => { + OrderBy::Created => { + match options.order { + Order::Asc => { tasks.sort_by(|t1, t2| t1.data.created.cmp(&t2.data.created)); }, - SortType::Desc => { + Order::Desc => { tasks.sort_by(|t1, t2| t2.data.created.cmp(&t1.data.created)); }, } }, + OrderBy::Tracked => { + match options.order { + Order::Asc => { + tasks.sort_by(|t1, t2| TimeEntry::total(&t1.data.time_entries).cmp(&TimeEntry::total(&t2.data.time_entries))); + }, + Order::Desc => { + tasks.sort_by(|t1, t2| TimeEntry::total(&t2.data.time_entries).cmp(&TimeEntry::total(&t1.data.time_entries))); + }, + } + } } // Include the required columns - let mut headers = vec!["Id"]; + let mut headers = vec!["Id", "Name"]; - if options.name { headers.push("Name") }; - if options.tracked { headers.push("Tracked") }; - if options.due { headers.push("Due") }; - if options.tags { headers.push("Tags") }; - if options.priority { headers.push("Priority") }; - if options.status { headers.push("Status") }; - if options.created { headers.push("Created") }; + // Remove duplicate columns. + { + let mut columns = HashSet::new(); + + options.column = options.column + .into_iter() + .filter(|c| { + if columns.contains(c) { + false + } + else { + columns.insert(c.clone()); + true + } + }) + .collect(); + } + + use super::Column; + for column in &options.column { + match column { + Column::Tracked => { + headers.push("Tracked"); + }, + Column::Due => { + headers.push("Due"); + }, + Column::Tags => { + headers.push("Tags"); + }, + Column::Priority => { + headers.push("Priority"); + }, + Column::Status => { + headers.push("Status"); + }, + Column::Created => { + headers.push("Created"); + }, + } + } table.set_header(headers); for task in tasks { - let mut row = vec![task.data.id.to_string()]; + let mut row = vec![task.data.id.to_string(), task.data.name.clone()]; - if options.name { row.push(task.data.name); } - if options.tracked { - let duration = TimeEntry::total(&task.data.time_entries); - row.push( - if duration == Duration::zero() { String::new() } else { duration.to_string() } - ); - } - if options.due { - row.push(match task.data.due { - Some(due) => format_due_date(&due, task.data.completed.is_none(), false), - None => String::new() - }); - } - if options.tags { row.push(format_hash_set(&task.data.tags)?); } - if options.priority { row.push(task.data.priority.to_string()); } - if options.status { - row.push( - if task.data.completed.is_some() { - String::from("complete") - } - else { - String::from("incomplete") - } - ); - } - if options.created { - row.push(task.data.created.round_subsecs(0).to_string()); + for column in &options.column { + match column { + Column::Tracked => { + let duration = TimeEntry::total(&task.data.time_entries); + row.push( + if duration == Duration::zero() { String::new() } else { duration.to_string() } + ); + }, + Column::Due => { + row.push(match task.data.due { + Some(due) => format_due_date(&due, task.data.completed.is_none(), false), + None => String::new() + }); + }, + Column::Tags => { + row.push(format_hash_set(&task.data.tags)?); + }, + Column::Priority => { + row.push(task.data.priority.to_string()); + }, + Column::Status => { + row.push( + if task.data.completed.is_some() { + String::from("complete") + } + else { + String::from("incomplete") + } + ); + }, + Column::Created => { + row.push(task.data.created.round_subsecs(0).to_string()); + }, + } } table.add_row(row); @@ -651,9 +718,7 @@ impl TimeEntry { .map(|e| e.duration) .fold(Duration::zero(), |a, d| a + d) } -} -impl TimeEntry { pub fn new(hours : u16, minutes : u16) -> Self { let (hours, minutes) = { diff --git a/src/vault.rs b/src/vault.rs old mode 100644 new mode 100755 diff --git a/src/vcs.rs b/src/vcs.rs old mode 100644 new mode 100755