changes to list command line options

This commit is contained in:
aaron-jack-manning 2022-08-29 17:34:22 +10:00
parent 083d276252
commit c547fb8997
16 changed files with 224 additions and 160 deletions

0
.gitignore vendored Normal file → Executable file
View File

2
Cargo.lock generated Normal file → Executable file
View File

@ -775,7 +775,7 @@ dependencies = [
[[package]] [[package]]
name = "toru" name = "toru"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",

0
Cargo.toml Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
src/colour.rs Normal file → Executable file
View File

0
src/config.rs Normal file → Executable file
View File

0
src/edit.rs Normal file → Executable file
View File

0
src/error.rs Normal file → Executable file
View File

13
src/graph.rs Normal file → Executable file
View File

@ -71,6 +71,19 @@ impl Graph {
} }
} }
/// Gets all tasks which have dependents.
pub fn get_tasks_with_dependents(&self) -> HashSet<Id> {
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<Vec<Id>> { pub fn find_cycle(&self) -> Option<Vec<Id>> {
// All unvisited nodes, populated with all nodes at the start, to not miss disconnected // All unvisited nodes, populated with all nodes at the start, to not miss disconnected

0
src/index.rs Normal file → Executable file
View File

96
src/main.rs Normal file → Executable file
View File

@ -101,82 +101,68 @@ enum Command {
#[derive(clap::StructOpt, Debug, PartialEq, Eq)] #[derive(clap::StructOpt, Debug, PartialEq, Eq)]
pub struct ListOptions { pub struct ListOptions {
/// Include name column. /// Which columns to include.
#[clap(long)] #[clap(short, value_enum)]
name : bool, column : Vec<Column>,
/// Include tracked time column. /// Field to order by.
#[clap(long)] #[clap(long, value_enum, default_value_t=OrderBy::Id)]
tracked : bool, order_by : OrderBy,
/// 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,
/// Sort ascending on descending. /// Sort ascending on descending.
#[clap(long, value_enum, default_value_t=SortType::Asc)] #[clap(long, value_enum, default_value_t=Order::Asc)]
sort_type : SortType, order : Order,
/// Only include tasks created before a certain date. /// Tags to include.
#[clap(short, long)]
tag : Vec<String>,
/// Only include tasks due before a certain date (inclusive).
#[clap(long)] #[clap(long)]
before : Option<chrono::NaiveDateTime>, due_before : Option<chrono::NaiveDate>,
/// Only include tasks created after a certain date. /// Only include tasks due after a certain date (inclusive).
#[clap(long)] #[clap(long)]
after : Option<chrono::NaiveDateTime>, due_after : Option<chrono::NaiveDate>,
/// Only include tasks due within a certain number of days. /// Only include tasks created before a certain date (inclusive).
#[clap(long)] #[clap(long)]
due_in : Option<u16>, created_before : Option<chrono::NaiveDate>,
/// Only include tasks created after a certain date (inclusive).
#[clap(long)]
created_after : Option<chrono::NaiveDate>,
/// Include completed tasks in the list. /// Include completed tasks in the list.
#[clap(long)] #[clap(long)]
include_completed : bool, include_completed : bool,
} /// Only include notes with no dependencies [alias: bottom-level].
#[clap(long, alias="bottom-level")]
impl Default for ListOptions { no_dependencies : bool,
fn default() -> Self { /// Only include notes with no dependents [alias: top-level].
Self { #[clap(long, alias="top-level")]
name : true, no_dependents : bool,
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,
}
}
} }
#[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum SortType { pub enum Order {
#[default] #[default]
Asc, Asc,
Desc, 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)] #[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum SortBy { pub enum OrderBy {
#[default] #[default]
Id, Id,
Name, Name,
Due, Due,
Priority, Priority,
Created, Created,
Tracked,
} }
#[derive(clap::Subcommand, Debug, PartialEq, Eq)] #[derive(clap::Subcommand, Debug, PartialEq, Eq)]
@ -373,7 +359,7 @@ fn program() -> Result<(), error::Error> {
println!("Marked task {} as complete", colour::id(id)); println!("Marked task {} as complete", colour::id(id));
}, },
List { options } => { 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. // All commands which are dealt with in if let chain at start.
Vault(_) | Config(_) | Git { args : _ } | Svn { args : _ } | Switch { name : _ } | GitIgnore => unreachable!(), Vault(_) | Config(_) | Git { args : _ } | Svn { args : _ } | Switch { name : _ } | GitIgnore => unreachable!(),

0
src/state.rs Normal file → Executable file
View File

0
src/stats.rs Normal file → Executable file
View File

273
src/tasks.rs Normal file → Executable file
View File

@ -8,6 +8,7 @@ use std::fs;
use std::fmt; use std::fmt;
use std::ops; use std::ops;
use std::mem; use std::mem;
use std::cmp;
use std::path; use std::path;
use std::io::{Write, Seek}; use std::io::{Write, Seek};
use std::collections::{HashSet, HashMap}; use std::collections::{HashSet, HashMap};
@ -61,7 +62,7 @@ pub struct TimeEntry {
} }
// Needs to preserve representation invariant of minutes < 60 // 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 { pub struct Duration {
hours : u16, hours : u16,
minutes : u16, minutes : u16,
@ -427,35 +428,17 @@ fn format_due_date(due : &chrono::NaiveDateTime, include_fuzzy_period : bool, co
} }
} }
fn compare_due_dates<T : Ord>(first : &Option<T>, second : &Option<T>) -> cmp::Ordering {
match (first, second) {
(None, None) => cmp::Ordering::Equal,
(Some(_), None) => cmp::Ordering::Less,
pub fn list(options : super::ListOptions, vault_folder : &path::Path) -> Result<(), error::Error> { (None, Some(_)) => cmp::Ordering::Greater,
(Some(first), Some(second)) => first.cmp(second),
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()
} }
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(); let mut table = comfy_table::Table::new();
table table
@ -466,130 +449,214 @@ pub fn list(options : super::ListOptions, vault_folder : &path::Path) -> Result<
let mut tasks : Box<dyn Iterator<Item = Task>> = Box::new(Task::load_all(vault_folder, true)?.into_iter()); let mut tasks : Box<dyn Iterator<Item = Task>> = Box::new(Task::load_all(vault_folder, true)?.into_iter());
// Filter the tasks // Filter the tasks.
if let Some(before) = options.before { if let Some(date) = options.created_before {
tasks = Box::new(tasks.filter(move |t| t.data.created < before)); tasks = Box::new(tasks.filter(move |t| t.data.created.date() <= date));
} }
if let Some(after) = options.after { if let Some(date) = options.created_after {
tasks = Box::new(tasks.filter(move |t| t.data.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| { tasks = Box::new(tasks.filter(move |t| {
if let Some(due) = t.data.due { match compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) {
due < now + chrono::Duration::days(i64::from(due_in)) cmp::Ordering::Less | cmp::Ordering::Equal => true,
} cmp::Ordering::Greater => false,
else {
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 { if !options.include_completed {
tasks = Box::new(tasks.filter(|t| t.data.completed.is_none())); 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<Task> = tasks.collect(); let mut tasks : Vec<Task> = tasks.collect();
// Sort the tasks // Sort the tasks.
use super::{SortBy, SortType}; use super::{OrderBy, Order};
match options.sort_by { match options.order_by {
SortBy::Id => { OrderBy::Id => {
match options.sort_type { match options.order {
SortType::Asc => { Order::Asc => {
tasks.sort_by(|t1, t2| t1.data.id.cmp(&t2.data.id)); 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)); tasks.sort_by(|t1, t2| t2.data.id.cmp(&t1.data.id));
}, },
} }
}, },
SortBy::Name => { OrderBy::Name => {
match options.sort_type { match options.order {
SortType::Asc => { Order::Asc => {
tasks.sort_by(|t1, t2| t1.data.name.cmp(&t2.data.name)); 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)); tasks.sort_by(|t1, t2| t2.data.name.cmp(&t1.data.name));
}, },
} }
}, },
SortBy::Due => { OrderBy::Due => {
match options.sort_type { match options.order {
SortType::Asc => { Order::Asc => {
tasks.sort_by(|t1, t2| t1.data.due.cmp(&t2.data.due)); tasks.sort_by(|t1, t2| compare_due_dates(&t1.data.due, &t2.data.due));
}, },
SortType::Desc => { Order::Desc => {
tasks.sort_by(|t1, t2| t2.data.due.cmp(&t1.data.due)); tasks.sort_by(|t1, t2| compare_due_dates(&t2.data.due, &t1.data.due));
}, },
} }
}, },
SortBy::Priority => { OrderBy::Priority => {
match options.sort_type { match options.order {
SortType::Asc => { Order::Asc => {
tasks.sort_by(|t1, t2| t1.data.priority.cmp(&t2.data.priority)); 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)); tasks.sort_by(|t1, t2| t2.data.priority.cmp(&t1.data.priority));
}, },
} }
}, },
SortBy::Created => { OrderBy::Created => {
match options.sort_type { match options.order {
SortType::Asc => { Order::Asc => {
tasks.sort_by(|t1, t2| t1.data.created.cmp(&t2.data.created)); 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)); 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 // Include the required columns
let mut headers = vec!["Id"]; let mut headers = vec!["Id", "Name"];
if options.name { headers.push("Name") }; // Remove duplicate columns.
if options.tracked { headers.push("Tracked") }; {
if options.due { headers.push("Due") }; let mut columns = HashSet::new();
if options.tags { headers.push("Tags") };
if options.priority { headers.push("Priority") }; options.column = options.column
if options.status { headers.push("Status") }; .into_iter()
if options.created { headers.push("Created") }; .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); table.set_header(headers);
for task in tasks { 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); } for column in &options.column {
if options.tracked { match column {
let duration = TimeEntry::total(&task.data.time_entries); Column::Tracked => {
row.push( let duration = TimeEntry::total(&task.data.time_entries);
if duration == Duration::zero() { String::new() } else { duration.to_string() } row.push(
); if duration == Duration::zero() { String::new() } else { duration.to_string() }
} );
if options.due { },
row.push(match task.data.due { Column::Due => {
Some(due) => format_due_date(&due, task.data.completed.is_none(), false), row.push(match task.data.due {
None => String::new() 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()); } Column::Tags => {
if options.status { row.push(format_hash_set(&task.data.tags)?);
row.push( },
if task.data.completed.is_some() { Column::Priority => {
String::from("complete") row.push(task.data.priority.to_string());
} },
else { Column::Status => {
String::from("incomplete") row.push(
} if task.data.completed.is_some() {
); String::from("complete")
} }
if options.created { else {
row.push(task.data.created.round_subsecs(0).to_string()); String::from("incomplete")
}
);
},
Column::Created => {
row.push(task.data.created.round_subsecs(0).to_string());
},
}
} }
table.add_row(row); table.add_row(row);
@ -651,9 +718,7 @@ impl TimeEntry {
.map(|e| e.duration) .map(|e| e.duration)
.fold(Duration::zero(), |a, d| a + d) .fold(Duration::zero(), |a, d| a + d)
} }
}
impl TimeEntry {
pub fn new(hours : u16, minutes : u16) -> Self { pub fn new(hours : u16, minutes : u16) -> Self {
let (hours, minutes) = { let (hours, minutes) = {

0
src/vault.rs Normal file → Executable file
View File

0
src/vcs.rs Normal file → Executable file
View File