diff --git a/Cargo.lock b/Cargo.lock index 799d5ab..27133b2 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,7 +775,7 @@ dependencies = [ [[package]] name = "toru" -version = "0.3.0" +version = "0.4.0" dependencies = [ "chrono", "clap", diff --git a/dev-notes/data invariants.md b/dev-notes/data invariants.md new file mode 100644 index 0000000..74de536 --- /dev/null +++ b/dev-notes/data invariants.md @@ -0,0 +1,20 @@ +# Data Invariants + +This file contains notes on invariants which must be upheld across running toru commands for the vault and configuration. + +# tasks::Task +- name cannot be purely numeric +- ID must be unique + +# tasks::Duration +- minutes should be less than 60 + +# state::State (in state.toml) +- the `next_id` should always be greater than the ID of any task within the vault + +# index::Index (in state.toml) +- the index of name to ID map should be correct given the names and IDs of all vaults + +# graph::Graph (in state.toml) +- should always have dependencies the same as specified in each task file +- no circular dependencies should be allowed to exist between tasks (and by extension should not exist in the state file) diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..d83cbe9 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,221 @@ +use crate::tasks; +use crate::tasks::Id; + +use std::path; + +impl Args { + pub fn accept_command() -> Command { + use clap::Parser; + Args::parse().command + } +} + +#[derive(clap::Parser, Debug)] +pub struct Args { + #[clap(subcommand)] + pub command : Command, +} + +#[derive(clap::Subcommand, Debug, PartialEq, Eq)] +#[clap(version, help_short='h', about, author, global_setting=clap::AppSettings::DisableHelpSubcommand)] +pub enum Command { + /// Create a new task. + New { + #[clap(short, long)] + name : String, + #[clap(short, long)] + info : Option, + #[clap(short, long)] + tag : Vec, + #[clap(short, long)] + dependency : Vec, + #[clap(short, long, value_enum)] + priority : Option, + /// Due date, expecting format yyyy-mm-ddThh:mm:ss + #[clap(long)] + due : Option, + }, + /// Displays the specified task in detail. + View { + id_or_name : String, + }, + /// Edit a task directly. + Edit { + id_or_name : String, + /// Edit the info specifically in its own file. + #[clap(short, long)] + info : bool, + }, + /// Delete a task (move file to trash). + Delete { + id_or_name : String, + }, + /// Mark a task as complete. + Complete { + id_or_name : String, + }, + /// Run Git commands at the root of the vault. + #[clap(trailing_var_arg=true)] + Git { + args : Vec, + }, + /// Run Subversion commands at the root of the vault. + #[clap(trailing_var_arg=true)] + Svn { + args : Vec, + }, + /// Adds the recommended .gitignore file to the vault. + #[clap(name="gitignore")] + GitIgnore, + /// Lists tasks according to the specified fields, ordering and filters. + List { + #[clap(flatten)] + options : ListOptions, + }, + /// For tracking time against a task. + Track { + id_or_name : String, + #[clap(short='H', default_value_t=0)] + hours : u16, + #[clap(short='M', default_value_t=0)] + minutes : u16, + /// Date for the time entry [default: Today] + #[clap(short, long)] + date : Option, + /// Message to identify the time entry. + #[clap(short, long)] + message : Option, + }, + /// For statistics about the state of your vault. + #[clap(subcommand)] + Stats(StatsCommand), + /// For making changes to global configuration. + #[clap(subcommand)] + Config(ConfigCommand), + /// Commands for interacting with vaults. + #[clap(subcommand)] + Vault(VaultCommand), + /// Switches to the specified vault. + Switch { + name : String, + }, +} + +#[derive(clap::StructOpt, Debug, PartialEq, Eq)] +pub struct ListOptions { + /// Which columns to include. + #[clap(short, value_enum)] + pub column : Vec, + /// Field to order by. + #[clap(long, value_enum, default_value_t=OrderBy::Id)] + pub order_by : OrderBy, + /// Sort ascending on descending. + #[clap(long, value_enum, default_value_t=Order::Asc)] + pub order : Order, + /// Tags to include. + #[clap(short, long)] + pub tag : Vec, + /// Only include tasks due before a certain date (inclusive). + #[clap(long)] + pub due_before : Option, + /// Only include tasks due after a certain date (inclusive). + #[clap(long)] + pub due_after : Option, + /// Only include tasks created before a certain date (inclusive). + #[clap(long)] + pub created_before : Option, + /// Only include tasks created after a certain date (inclusive). + #[clap(long)] + pub created_after : Option, + /// Include completed tasks in the list. + #[clap(long)] + pub include_completed : bool, + /// Only include tasks with no dependencies [alias: bottom-level]. + #[clap(long, alias="bottom-level")] + pub no_dependencies : bool, + /// Only include tasks with no dependents [alias: top-level]. + #[clap(long, alias="top-level")] + pub no_dependents : bool, +} + +#[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)] +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 OrderBy { + #[default] + Id, + Name, + Due, + Priority, + Created, + Tracked, +} + +#[derive(clap::Subcommand, Debug, PartialEq, Eq)] +pub enum StatsCommand { + /// View time tracked per tag recently. + Tracked { + #[clap(short, long, default_value_t=7)] + days : u16, + }, + /// View recently completed tasks. + Completed { + #[clap(short, long, default_value_t=7)] + days : u16, + }, +} + +#[derive(clap::Subcommand, Debug, PartialEq, Eq)] +pub enum ConfigCommand { + /// For checking or changing default text editor command. + Editor { + /// Command to launch editor. Omit to view current editor. + editor : Option, + } +} + +#[derive(clap::Subcommand, Debug, PartialEq, Eq)] +pub enum VaultCommand { + /// Creates a new vault at the specified location of the given name. + New { + name : String, + path : path::PathBuf, + }, + /// Disconnects the specified vault from toru, without altering the files. + Disconnect { + name : String, + }, + /// Connects an existing fault to toru. + Connect { + name : String, + path : path::PathBuf, + }, + /// Deletes the specified vault along with all of its data. + Delete { + name : String, + }, + /// Lists all configured vaults. + List, + /// For renaming an already set up vault. + Rename { + old_name : String, + new_name : String, + } +} + diff --git a/src/colour.rs b/src/colour.rs deleted file mode 100755 index 1405c0a..0000000 --- a/src/colour.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::tasks::Id; - -use colored::Colorize; - -// Yellow -pub static VAULT : (u8, u8, u8) = (243, 156, 18); -// Blue -pub static ID : (u8, u8, u8) = (52, 152, 219); -// Red -pub static ERROR : (u8, u8, u8) = (192, 57, 43); -// Purple -pub static COMMAND : (u8, u8, u8) = (155, 89, 182); -// Green -pub static TASK : (u8, u8, u8) = (39, 174, 96); -// Beige -pub static FILE : (u8, u8, u8) = (255, 184, 184); -// Grey -pub static GREY : (u8, u8, u8) = (99, 110, 114); - -mod due { - pub static OVERDUE : (u8, u8, u8) = (192, 57, 43); - pub static VERY_CLOSE : (u8, u8, u8) = (231, 76, 60); - pub static CLOSE : (u8, u8, u8) = (241, 196, 15); - pub static PLENTY_OF_TIME : (u8, u8, u8) = (46, 204, 113); -} - -pub mod priority { - pub static LOW : (u8, u8, u8) = (46, 204, 113); - pub static MEDIUM : (u8, u8, u8) = (241, 196, 15); - pub static HIGH : (u8, u8, u8) = (231, 76, 60); -} - -pub mod cell { - use crate::tasks; - - use chrono::SubsecRound; - - fn cell>(text : T, colour : (u8, u8, u8)) -> comfy_table::Cell { - text.into().fg(comfy_table::Color::from(colour)) - } - - pub fn priority(priority : &tasks::Priority) -> comfy_table::Cell { - use tasks::Priority::*; - match priority { - Low => comfy_table::Cell::new("low").fg(comfy_table::Color::from(super::priority::LOW)), - Medium => comfy_table::Cell::new("medium").fg(comfy_table::Color::from(super::priority::MEDIUM)), - High => comfy_table::Cell::new("high").fg(comfy_table::Color::from(super::priority::HIGH)), - } - } - - pub fn due_date(due : &chrono::NaiveDateTime, include_fuzzy_period : bool, colour : bool) -> comfy_table::Cell { - - let remaining = *due - chrono::Local::now().naive_local(); - - let fuzzy_period = if remaining.num_days() != 0 { - let days = remaining.num_days().abs(); - format!("{} day{}", days, if days == 1 {""} else {"s"}) - } - else if remaining.num_hours() != 0 { - let hours = remaining.num_hours().abs(); - format!("{} hour{}", hours, if hours == 1 {""} else {"s"}) - } - else if remaining.num_minutes() != 0 { - let minutes = remaining.num_minutes().abs(); - format!("{} minute{}", minutes, if minutes == 1 {""} else {"s"}) - } - else { - let seconds = remaining.num_seconds().abs(); - format!("{} second{}", seconds, if seconds == 1 {""} else {"s"}) - }; - - if include_fuzzy_period { - if colour { - if remaining < chrono::Duration::zero() { - cell(format!("{} {}", due.round_subsecs(0), format!("({} overdue)", fuzzy_period)), super::due::OVERDUE) - } - else if remaining < chrono::Duration::days(1) { - cell(format!("{} {}", due.round_subsecs(0), format!("({} remaining)", fuzzy_period)), super::due::VERY_CLOSE) - - } - else if remaining < chrono::Duration::days(5) { - cell(format!("{} {}", due.round_subsecs(0), format!("({} remaining)", fuzzy_period)), super::due::CLOSE) - - } - else { - cell(format!("{} {}", due.round_subsecs(0), format!("({} remaining)", fuzzy_period)), super::due::PLENTY_OF_TIME) - } - } - else { - if remaining < chrono::Duration::zero() { - comfy_table::Cell::new(format!("{} ({} overdue)", due.round_subsecs(0), fuzzy_period)) - } - else { - comfy_table::Cell::new(format!("{} ({} remaining)", due.round_subsecs(0), fuzzy_period)) - } - } - } - else { - comfy_table::Cell::new(format!("{}", due.round_subsecs(0))) - } - - } -} - -pub mod text { - use super::*; - use crate::tasks; - - use chrono::SubsecRound; - - fn text(string : &str, colour : (u8, u8, u8)) -> colored::ColoredString { - string.truecolor(colour.0, colour.1, colour.2) - } - - pub fn vault(string : &str) -> colored::ColoredString { - text(string, VAULT).bold() - } - - pub fn id(id : Id) -> colored::ColoredString { - text(&id.to_string(), ID) - } - - pub fn error(string : &str) -> colored::ColoredString { - text(string, ERROR).bold() - } - - pub fn command(string : &str) -> colored::ColoredString { - text(string, COMMAND).bold() - } - - pub fn task(string : &str) -> colored::ColoredString { - text(string, TASK).bold() - } - - pub fn file(string : &str) -> colored::ColoredString { - text(string, FILE).bold() - } - - - pub fn greyed_out(string : &str) -> colored::ColoredString { - text(string, GREY) - } - - pub fn priority(priority : &tasks::Priority) -> String { - use tasks::Priority::*; - let priority = match priority { - Low => text("low", super::priority::LOW), - Medium => text("medium", super::priority::MEDIUM), - High => text("high", super::priority::HIGH), - }; - format!("{}", priority) - } - - - pub fn due_date(due : &chrono::NaiveDateTime, include_fuzzy_period : bool, colour : bool) -> String { - - let remaining = *due - chrono::Local::now().naive_local(); - - let fuzzy_period = if remaining.num_days() != 0 { - let days = remaining.num_days().abs(); - format!("{} day{}", days, if days == 1 {""} else {"s"}) - } - else if remaining.num_hours() != 0 { - let hours = remaining.num_hours().abs(); - format!("{} hour{}", hours, if hours == 1 {""} else {"s"}) - } - else if remaining.num_minutes() != 0 { - let minutes = remaining.num_minutes().abs(); - format!("{} minute{}", minutes, if minutes == 1 {""} else {"s"}) - } - else { - let seconds = remaining.num_seconds().abs(); - format!("{} second{}", seconds, if seconds == 1 {""} else {"s"}) - }; - - if include_fuzzy_period { - if colour { - if remaining < chrono::Duration::zero() { - format!("{} {}", due.round_subsecs(0), text(&format!("({} overdue)", fuzzy_period), super::due::OVERDUE)) - } - else if remaining < chrono::Duration::days(1) { - format!("{} {}", due.round_subsecs(0), text(&format!("({} remaining)", fuzzy_period), super::due::VERY_CLOSE)) - - } - else if remaining < chrono::Duration::days(5) { - format!("{} {}", due.round_subsecs(0), text(&format!("({} remaining)", fuzzy_period), super::due::CLOSE)) - - } - else { - format!("{} {}", due.round_subsecs(0), text(&format!("({} remaining)", fuzzy_period), super::due::PLENTY_OF_TIME)) - } - } - else { - if remaining < chrono::Duration::zero() { - format!("{} ({} overdue)", due.round_subsecs(0), fuzzy_period) - } - else { - format!("{} ({} remaining)", due.round_subsecs(0), fuzzy_period) - } - } - } - else { - format!("{}", due.round_subsecs(0)) - } - } -} - diff --git a/src/config.rs b/src/config.rs index e251efc..dc9153f 100755 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,5 @@ use crate::error; - -use crate::colour; +use crate::format; use std::path; @@ -46,7 +45,7 @@ impl Config { for (name, _) in &mut self.vaults { if *name == new_name { - return Err(error::Error::Generic(format!("A vault named {} already exists", colour::text::vault(&new_name)))); + return Err(error::Error::Generic(format!("A vault named {} already exists", format::vault(&new_name)))); } if name == old_name { @@ -60,7 +59,7 @@ impl Config { Ok(()) }, None => { - Err(error::Error::Generic(format!("No vault named {} exists", colour::text::vault(old_name)))) + Err(error::Error::Generic(format!("No vault named {} exists", format::vault(old_name)))) } } @@ -82,7 +81,7 @@ impl Config { Ok(path) }, None => { - Err(error::Error::Generic(format!("No vault by the name {} exists", colour::text::vault(name)))) + Err(error::Error::Generic(format!("No vault by the name {} exists", format::vault(name)))) } } } @@ -94,7 +93,7 @@ impl Config { Ok(()) }, None => { - Err(error::Error::Generic(format!("No vault by the name {} exists", colour::text::vault(name)))) + Err(error::Error::Generic(format!("No vault by the name {} exists", format::vault(name)))) } } } @@ -105,7 +104,7 @@ impl Config { let width = self.vaults.iter().fold(usize::MIN, |c, (n, _)| c.max(n.len())); if self.vaults.is_empty() { - Err(error::Error::Generic(format!("No vaults currently set up, try running: {}", colour::text::command("toru vault new ")))) + Err(error::Error::Generic(format!("No vaults currently set up, try running: {}", format::command("toru vault new ")))) } else { for (i, (name, path)) in self.vaults.iter().enumerate() { @@ -117,7 +116,7 @@ impl Config { print!(" "); } - print!("{}", colour::text::vault(name)); + print!("{}", format::vault(name)); let padding = width - name.len() + 1; diff --git a/src/edit.rs b/src/edit.rs index 7a4c035..0677614 100755 --- a/src/edit.rs +++ b/src/edit.rs @@ -7,7 +7,7 @@ use crate::tasks; use crate::error; use crate::graph; use crate::state; -use crate::colour; +use crate::format; use crate::tasks::Id; pub fn open_editor(path : &path::Path, editor : &str) -> Result { @@ -75,9 +75,18 @@ pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str, state : &m else { let mut edited_task = tasks::Task::load_direct(temp_path.clone(), true)?; + // Enforce time entry duration invariant. + for entry in &edited_task.data.time_entries { + if !entry.duration.satisfies_invariant() { + return Err(error::Error::Generic(String::from("Task duration must not have a number of minutes greater than 60"))) + } + } + + // Make sure ID is not changed. if edited_task.data.id != task.data.id { Err(error::Error::Generic(String::from("You cannot change the ID of a task in a direct edit"))) } + // Enforce non numeric name invariant. else if edited_task.data.name.chars().all(|c| c.is_numeric()) { Err(error::Error::Generic(String::from("Name must not be purely numeric"))) } @@ -93,7 +102,7 @@ pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str, state : &m state.data.deps.insert_edge(id, *dependency)?; } else { - return Err(error::Error::Generic(format!("No task with an ID of {} exists", colour::text::id(*dependency)))); + return Err(error::Error::Generic(format!("No task with an ID of {} exists", format::id(*dependency)))); } } diff --git a/src/error.rs b/src/error.rs index 80b54b8..1fe5e13 100755 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use crate::colour; +use crate::format; use std::io; use std::fmt; @@ -20,15 +20,15 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::Io(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::Confy(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::Trash(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::TomlDe(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::TomlSer(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::Utf8(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::Fmt(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err), - Error::Generic(message) => write!(f, "{} {}", colour::text::error("Error:"), message), - Error::Internal(message) => write!(f, "{} {}", colour::text::error("Internal Error:"), message), + Error::Io(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::Confy(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::Trash(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::TomlDe(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::TomlSer(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::Utf8(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::Fmt(err) => write!(f, "{} {}", format::error("Internal Error:"), err), + Error::Generic(message) => write!(f, "{} {}", format::error("Error:"), message), + Error::Internal(message) => write!(f, "{} {}", format::error("Internal Error:"), message), } } } diff --git a/src/format.rs b/src/format.rs new file mode 100755 index 0000000..4e75804 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,253 @@ +use crate::tasks; +use crate::graph; +use crate::error; +use crate::tasks::Id; + +use std::fmt; +use std::path; +use std::collections::{HashSet, HashMap}; +use colored::Colorize; +use chrono::SubsecRound; + +// Yellow +pub static VAULT : (u8, u8, u8) = (243, 156, 18); +// Blue +pub static ID : (u8, u8, u8) = (52, 152, 219); +// Red +pub static ERROR : (u8, u8, u8) = (192, 57, 43); +// Purple +pub static COMMAND : (u8, u8, u8) = (155, 89, 182); +// Green +pub static TASK : (u8, u8, u8) = (39, 174, 96); +// Beige +pub static FILE : (u8, u8, u8) = (255, 184, 184); +// Grey +pub static GREY : (u8, u8, u8) = (99, 110, 114); + +mod due { + pub static OVERDUE : (u8, u8, u8) = (192, 57, 43); + pub static VERY_CLOSE : (u8, u8, u8) = (231, 76, 60); + pub static CLOSE : (u8, u8, u8) = (241, 196, 15); + pub static PLENTY_OF_TIME : (u8, u8, u8) = (46, 204, 113); +} + +pub mod priority { + pub static LOW : (u8, u8, u8) = (46, 204, 113); + pub static MEDIUM : (u8, u8, u8) = (241, 196, 15); + pub static HIGH : (u8, u8, u8) = (231, 76, 60); +} + +fn text(string : &str, colour : (u8, u8, u8)) -> colored::ColoredString { + string.truecolor(colour.0, colour.1, colour.2) +} + +pub fn vault(string : &str) -> colored::ColoredString { + text(string, VAULT).bold() +} + +pub fn id(id : Id) -> colored::ColoredString { + text(&id.to_string(), ID) +} + +pub fn error(string : &str) -> colored::ColoredString { + text(string, ERROR).bold() +} + +pub fn command(string : &str) -> colored::ColoredString { + text(string, COMMAND).bold() +} + +pub fn task(string : &str) -> colored::ColoredString { + text(string, TASK).bold() +} + +pub fn file(string : &str) -> colored::ColoredString { + text(string, FILE).bold() +} + + +pub fn greyed_out(string : &str) -> colored::ColoredString { + text(string, GREY) +} + +pub fn priority(priority : &tasks::Priority) -> String { + use tasks::Priority::*; + let priority = match priority { + Low => text("low", priority::LOW), + Medium => text("medium", priority::MEDIUM), + High => text("high", priority::HIGH), + }; + format!("{}", priority) +} + +pub fn hash_set(set : &HashSet) -> Result { + let mut output = String::new(); + + for value in set.iter() { + fmt::write(&mut output, format_args!("{}, ", value))?; + } + + // Remove the trailing comma and space. + if !output.is_empty() { + output.pop(); + output.pop(); + } + + Ok(output) +} + +pub fn due_date(due : &chrono::NaiveDateTime, include_fuzzy_period : bool) -> String { + + let remaining = *due - chrono::Local::now().naive_local(); + + let fuzzy_period = if remaining.num_days() != 0 { + let days = remaining.num_days().abs(); + format!("{} day{}", days, if days == 1 {""} else {"s"}) + } + else if remaining.num_hours() != 0 { + let hours = remaining.num_hours().abs(); + format!("{} hour{}", hours, if hours == 1 {""} else {"s"}) + } + else if remaining.num_minutes() != 0 { + let minutes = remaining.num_minutes().abs(); + format!("{} minute{}", minutes, if minutes == 1 {""} else {"s"}) + } + else { + let seconds = remaining.num_seconds().abs(); + format!("{} second{}", seconds, if seconds == 1 {""} else {"s"}) + }; + + if include_fuzzy_period { + if remaining < chrono::Duration::zero() { + format!("{} {}", due.round_subsecs(0), text(&format!("({} overdue)", fuzzy_period), due::OVERDUE)) + } + else if remaining < chrono::Duration::days(1) { + format!("{} {}", due.round_subsecs(0), text(&format!("({} remaining)", fuzzy_period), due::VERY_CLOSE)) + + } + else if remaining < chrono::Duration::days(5) { + format!("{} {}", due.round_subsecs(0), text(&format!("({} remaining)", fuzzy_period), due::CLOSE)) + + } + else { + format!("{} {}", due.round_subsecs(0), text(&format!("({} remaining)", fuzzy_period), due::PLENTY_OF_TIME)) + } + } + else { + format!("{}", due.round_subsecs(0)) + } +} + +pub fn dependencies(start : Id, vault_folder : &path::Path, graph : &graph::Graph) -> Result<(), error::Error> { + + pub fn helper(curr : Id, prefix : &String, is_last_item : bool, graph : &graph::Graph, tasks : &HashMap) -> Result<(), error::Error> { + + let next = graph.edges.get(&curr).unwrap(); + + { + let task = tasks.get(&curr).unwrap(); + + let name = if task.data.completed.is_some() { + self::greyed_out(&task.data.name) + } + else { + self::task(&task.data.name) + }; + + if is_last_item { + println!("{}└──{} (ID: {})", prefix, name, self::id(curr)) + } + else { + println!("{}├──{} (ID: {})", prefix, name, self::id(curr)) + } + } + + let count = next.len(); + + for (i, node) in next.iter().enumerate() { + let new_is_last_item = i == count - 1; + + let new_prefix = if is_last_item { + format!("{} ", prefix) + } + else { + format!("{}│ ", prefix) + }; + + helper(*node, &new_prefix, new_is_last_item, graph, tasks)?; + } + + Ok(()) + } + + let tasks = tasks::Task::load_all_as_map(vault_folder, true)?; + + helper(start, &String::new(), true, graph, &tasks) +} + + + + + +pub mod cell { + use crate::tasks; + + use chrono::SubsecRound; + + fn cell>(text : T, colour : (u8, u8, u8)) -> comfy_table::Cell { + text.into().fg(comfy_table::Color::from(colour)) + } + + pub fn priority(priority : &tasks::Priority) -> comfy_table::Cell { + use tasks::Priority::*; + match priority { + Low => comfy_table::Cell::new("low").fg(comfy_table::Color::from(super::priority::LOW)), + Medium => comfy_table::Cell::new("medium").fg(comfy_table::Color::from(super::priority::MEDIUM)), + High => comfy_table::Cell::new("high").fg(comfy_table::Color::from(super::priority::HIGH)), + } + } + + pub fn due_date(due : &chrono::NaiveDateTime, include_fuzzy_period : bool) -> comfy_table::Cell { + + let remaining = *due - chrono::Local::now().naive_local(); + + let fuzzy_period = if remaining.num_days() != 0 { + let days = remaining.num_days().abs(); + format!("{} day{}", days, if days == 1 {""} else {"s"}) + } + else if remaining.num_hours() != 0 { + let hours = remaining.num_hours().abs(); + format!("{} hour{}", hours, if hours == 1 {""} else {"s"}) + } + else if remaining.num_minutes() != 0 { + let minutes = remaining.num_minutes().abs(); + format!("{} minute{}", minutes, if minutes == 1 {""} else {"s"}) + } + else { + let seconds = remaining.num_seconds().abs(); + format!("{} second{}", seconds, if seconds == 1 {""} else {"s"}) + }; + + if include_fuzzy_period { + if remaining < chrono::Duration::zero() { + cell(format!("{} {}", due.round_subsecs(0), format!("({} overdue)", fuzzy_period)), super::due::OVERDUE) + } + else if remaining < chrono::Duration::days(1) { + cell(format!("{} {}", due.round_subsecs(0), format!("({} remaining)", fuzzy_period)), super::due::VERY_CLOSE) + + } + else if remaining < chrono::Duration::days(5) { + cell(format!("{} {}", due.round_subsecs(0), format!("({} remaining)", fuzzy_period)), super::due::CLOSE) + + } + else { + cell(format!("{} {}", due.round_subsecs(0), format!("({} remaining)", fuzzy_period)), super::due::PLENTY_OF_TIME) + } + } + else { + comfy_table::Cell::new(format!("{}", due.round_subsecs(0))) + } + + } +} + diff --git a/src/graph.rs b/src/graph.rs index 7ffae33..27af28a 100755 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,6 +1,6 @@ use crate::error; use crate::tasks; -use crate::colour; +use crate::format; use crate::tasks::Id; use std::fmt::Write; @@ -40,7 +40,7 @@ impl Graph { Err(error::Error::Internal(String::from("Attempt to insert an edge in the dependency graph with a node which wasn't present"))) } else if first == second { - Err(error::Error::Generic(format!("Note with ID {} cannot depend on itself", colour::text::id(first)))) + Err(error::Error::Generic(format!("Note with ID {} cannot depend on itself", format::id(first)))) } else { let outgoing = self.edges.get_mut(&first).unwrap(); @@ -136,7 +136,7 @@ pub fn format_cycle(cycle : &Vec) -> String { let mut formatted = String::new(); for (index, node) in cycle.iter().enumerate() { - write!(&mut formatted, "{}", colour::text::id(*node)).unwrap(); + write!(&mut formatted, "{}", format::id(*node)).unwrap(); if index != cycle.len() - 1 { formatted.push_str(" -> "); diff --git a/src/index.rs b/src/index.rs index 28bec58..9007d49 100755 --- a/src/index.rs +++ b/src/index.rs @@ -1,6 +1,6 @@ use crate::tasks; use crate::error; -use crate::colour; +use crate::format; use crate::tasks::Id; use std::fmt::Write; @@ -69,7 +69,7 @@ impl Index { else { let coloured_ids : Vec<_> = ids.iter() - .map(|i| colour::text::id(*i)) + .map(|i| format::id(*i)) .collect(); let mut display_ids = String::new(); @@ -83,10 +83,10 @@ impl Index { display_ids.pop(); } - Err(error::Error::Generic(format!("Multiple notes (Ids: [{}]) by that name exist", display_ids))) + Err(error::Error::Generic(format!("Multiple tasks (Ids: [{}]) by that name exist", display_ids))) } }, - None => Err(error::Error::Generic(format!("A note by the name {} does not exist", colour::text::task(name)))), + None => Err(error::Error::Generic(format!("A note by the name {} does not exist", format::task(name)))), } } } diff --git a/src/main.rs b/src/main.rs index bee392f..e2b1479 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod vcs; mod edit; +mod args; mod vault; mod index; mod error; @@ -8,270 +9,57 @@ mod state; mod graph; mod stats; mod config; -mod colour; +mod format; -use tasks::Id; - -use std::path; - -#[derive(clap::Parser, Debug)] -struct Args { - #[clap(subcommand)] - command : Command, -} - -#[derive(clap::Subcommand, Debug, PartialEq, Eq)] -#[clap(version, help_short='h', about, author, global_setting=clap::AppSettings::DisableHelpSubcommand)] -enum Command { - /// Create a new task. - New { - #[clap(short, long)] - name : String, - #[clap(short, long)] - info : Option, - #[clap(short, long)] - tag : Vec, - #[clap(short, long)] - dependency : Vec, - #[clap(short, long, value_enum)] - priority : Option, - /// Due date, expecting format yyyy-mm-ddThh:mm:ss - #[clap(long)] - due : Option, - }, - /// Displays the specified task in detail. - View { - id_or_name : String, - }, - /// Edit a task directly. - Edit { - id_or_name : String, - /// Edit the info specifically in its own file. - #[clap(short, long)] - info : bool, - }, - /// Delete a task (move file to trash). - Delete { - id_or_name : String, - }, - /// Mark a task as complete. - Complete { - id_or_name : String, - }, - /// Run Git commands at the root of the vault. - #[clap(trailing_var_arg=true)] - Git { - args : Vec, - }, - /// Run Subversion commands at the root of the vault. - #[clap(trailing_var_arg=true)] - Svn { - args : Vec, - }, - /// Adds the recommended .gitignore file to the vault. - #[clap(name="gitignore")] - GitIgnore, - /// Lists tasks according to the specified fields, ordering and filters. - List { - #[clap(flatten)] - options : ListOptions, - }, - /// For tracking time against a task. - Track { - id_or_name : String, - #[clap(short='H', default_value_t=0)] - hours : u16, - #[clap(short='M', default_value_t=0)] - minutes : u16, - /// Date for the time entry [default: Today] - #[clap(short, long)] - date : Option, - /// Message to identify the time entry. - #[clap(short, long)] - message : Option, - }, - /// For statistics about the state of your vault. - #[clap(subcommand)] - Stats(StatsCommand), - /// For making changes to global configuration. - #[clap(subcommand)] - Config(ConfigCommand), - /// Commands for interacting with vaults. - #[clap(subcommand)] - Vault(VaultCommand), - /// Switches to the specified vault. - Switch { - name : String, - }, -} - -#[derive(clap::StructOpt, Debug, PartialEq, Eq)] -pub struct ListOptions { - /// 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=Order::Asc)] - order : Order, - /// Tags to include. - #[clap(short, long)] - tag : Vec, - /// Only include tasks due before a certain date (inclusive). - #[clap(long)] - due_before : Option, - /// Only include tasks due after a certain date (inclusive). - #[clap(long)] - due_after : Option, - /// Only include tasks created before a certain date (inclusive). - #[clap(long)] - 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, - /// 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 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 OrderBy { - #[default] - Id, - Name, - Due, - Priority, - Created, - Tracked, -} - -#[derive(clap::Subcommand, Debug, PartialEq, Eq)] -enum StatsCommand { - /// View time tracked per tag recently. - Tracked { - #[clap(short, long, default_value_t=7)] - days : u16, - }, - /// View recently completed tasks. - Completed { - #[clap(short, long, default_value_t=7)] - days : u16, - }, -} - -#[derive(clap::Subcommand, Debug, PartialEq, Eq)] -enum ConfigCommand { - /// For checking or changing default text editor command. - Editor { - /// Command to launch editor. Omit to view current editor. - editor : Option, - } -} - -#[derive(clap::Subcommand, Debug, PartialEq, Eq)] -enum VaultCommand { - /// Creates a new vault at the specified location of the given name. - New { - name : String, - path : path::PathBuf, - }, - /// Disconnects the specified vault from toru, without altering the files. - Disconnect { - name : String, - }, - /// Connects an existing fault to toru. - Connect { - name : String, - path : path::PathBuf, - }, - /// Deletes the specified vault along with all of its data. - Delete { - name : String, - }, - /// Lists all configured vaults. - List, - /// For renaming an already set up vault. - Rename { - old_name : String, - new_name : String, - } -} +use args::*; fn main() { let result = program(); match result { - Ok(()) => (), + Ok(()) => { + std::process::exit(0); + }, Err(err) => { println!("{}", err); + std::process::exit(1); } } } fn program() -> Result<(), error::Error> { - let command = { - use clap::Parser; - Args::parse().command - }; + let command = Args::accept_command(); let mut config = config::Config::load()?; - use Command::*; - if let Vault(command) = command { - use VaultCommand::*; + if let Command::Vault(command) = command { match command { - New { name, path } => { + VaultCommand::New { name, path } => { vault::new(name.clone(), path, &mut config)?; - println!("Created vault {}", colour::text::vault(&name)); + println!("Created vault {}", format::vault(&name)); }, - Disconnect { name } => { + VaultCommand::Disconnect { name } => { vault::disconnect(&name, &mut config)?; - println!("Disconnected vault {}", colour::text::vault(&name)); + println!("Disconnected vault {}", format::vault(&name)); }, - Connect { name , path } => { + VaultCommand::Connect { name , path } => { vault::connect(name.clone(), path, &mut config)?; - println!("Connected vault {}", colour::text::vault(&name)); + println!("Connected vault {}", format::vault(&name)); }, - Delete { name } => { + VaultCommand::Delete { name } => { vault::delete(&name, &mut config)?; - println!("Deleted vault {}", colour::text::vault(&name)); + println!("Deleted vault {}", format::vault(&name)); }, - List => { + VaultCommand::List => { config.list_vaults()?; }, - Rename { old_name, new_name } => { + VaultCommand::Rename { old_name, new_name } => { config.rename_vault(&old_name, new_name.clone())?; - println!("Renamed vault {} to {}", colour::text::vault(&old_name), colour::text::vault(&new_name)); + println!("Renamed vault {} to {}", format::vault(&old_name), format::vault(&new_name)); } } } - else if let Config(command) = command { + else if let Command::Config(command) = command { use ConfigCommand::*; match command { Editor { editor } => { @@ -287,20 +75,20 @@ fn program() -> Result<(), error::Error> { } } } - else if let Switch { name } = command { + else if let Command::Switch { name } = command { config.switch(&name)?; - println!("Switched to vault {}", colour::text::vault(&name)); + println!("Switched to vault {}", format::vault(&name)); } - else if let Git { args } = command { + else if let Command::Git { args } = command { let vault_folder = &config.current_vault()?.1; vcs::command(args, vcs::Vcs::Git, vault_folder)?; } - else if command == GitIgnore { + else if command == Command::GitIgnore { let vault_folder = &config.current_vault()?.1; vcs::create_gitignore(vault_folder)?; - println!("Default {} file created", colour::text::file(".gitignore")); + println!("Default {} file created", format::file(".gitignore")); } - else if let Svn { args } = command { + else if let Command::Svn { args } = command { let vault_folder = &config.current_vault()?.1; vcs::command(args, vcs::Vcs::Svn, vault_folder)?; } @@ -310,11 +98,11 @@ fn program() -> Result<(), error::Error> { let mut state = state::State::load(vault_folder)?; match command { - New { name, info, tags, dependencies, priority, due } => { - let task = tasks::Task::new(name, info, tags, dependencies, priority, due, vault_folder, &mut state)?; - println!("Created task {} (ID: {})", colour::text::task(&task.data.name), colour::text::id(task.data.id)); + Command::New { name, info, tag, dependency, priority, due } => { + let task = tasks::Task::new(name, info, tag, dependency, priority, due, vault_folder, &mut state)?; + println!("Created task {} (ID: {})", format::task(&task.data.name), format::id(task.data.id)); }, - Delete { id_or_name } => { + Command::Delete { id_or_name } => { let id = state.data.index.lookup(&id_or_name)?; let task = tasks::Task::load(id, vault_folder, false)?; let name = task.data.name.clone(); @@ -322,14 +110,14 @@ fn program() -> Result<(), error::Error> { state.data.deps.remove_node(task.data.id); task.delete()?; - println!("Deleted task {} (ID: {})", colour::text::task(&name), colour::text::id(id)); + println!("Deleted task {} (ID: {})", format::task(&name), format::id(id)); }, - View { id_or_name } => { + Command::View { id_or_name } => { let id = state.data.index.lookup(&id_or_name)?; let task = tasks::Task::load(id, vault_folder, true)?; task.display(vault_folder, &state)?; }, - Edit { id_or_name, info } => { + Command::Edit { id_or_name, info } => { let id = state.data.index.lookup(&id_or_name)?; if info { edit::edit_info(id, vault_folder.clone(), &config.editor)?; @@ -337,16 +125,16 @@ fn program() -> Result<(), error::Error> { else { edit::edit_raw(id, vault_folder.clone(), &config.editor, &mut state)?; } - println!("Updated task {}", colour::text::id(id)); + println!("Updated task {}", format::id(id)); }, - Track { id_or_name, hours, minutes, date, message } => { + Command::Track { id_or_name, hours, minutes, date, message } => { let id = state.data.index.lookup(&id_or_name)?; let mut task = tasks::Task::load(id, vault_folder, false)?; let entry = tasks::TimeEntry::new(hours, minutes, date, message); task.data.time_entries.push(entry); task.save()?; }, - Stats(command) => { + Command::Stats(command) => { use StatsCommand::*; match command { Tracked { days } => { @@ -357,18 +145,18 @@ fn program() -> Result<(), error::Error> { } } }, - Complete { id_or_name } => { + Command::Complete { id_or_name } => { let id = state.data.index.lookup(&id_or_name)?; let mut task = tasks::Task::load(id, vault_folder, false)?; task.data.completed = Some(chrono::Local::now().naive_local()); task.save()?; - println!("Marked task {} as complete", colour::text::id(id)); + println!("Marked task {} as complete", format::id(id)); }, - List { options } => { + Command::List { options } => { 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!(), + Command::Vault(_) | Command::Config(_) | Command::Git { args : _ } | Command::Svn { args : _ } | Command::Switch { name : _ } | Command::GitIgnore => unreachable!(), } state.save()?; diff --git a/src/state.rs b/src/state.rs index c32f860..d45eca2 100755 --- a/src/state.rs +++ b/src/state.rs @@ -23,7 +23,7 @@ pub struct InternalState { } impl State { - /// This function should be called after creating or checking that the "notes" folder exists. + /// This function should be called after creating or checking that the "tasks" folder exists. pub fn load(vault_location : &path::Path) -> Result { let path = vault_location.join("state.toml"); @@ -47,7 +47,7 @@ impl State { // Calculating the next ID if necessary. let mut max_id : i128 = -1; - for id in vault_location.join("notes").read_dir()?.filter_map(|p| p.ok()).map(|p| p.path()).filter(|p| p.extension().map(|s| s.to_str()) == Some(Some("toml"))).filter_map(|p| p.file_stem().map(|x| x.to_str().map(|y| y.to_string()))).flatten().filter_map(|p| p.parse::().ok()) { + for id in vault_location.join("tasks").read_dir()?.filter_map(|p| p.ok()).map(|p| p.path()).filter(|p| p.extension().map(|s| s.to_str()) == Some(Some("toml"))).filter_map(|p| p.file_stem().map(|x| x.to_str().map(|y| y.to_string()))).flatten().filter_map(|p| p.parse::().ok()) { if i128::try_from(id).unwrap() > max_id { max_id = i128::from(id); diff --git a/src/tasks.rs b/src/tasks.rs index 256416c..73cf27d 100755 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,7 +1,6 @@ use crate::error; use crate::state; -use crate::graph; -use crate::colour; +use crate::format; use std::io; use std::fs; @@ -30,19 +29,6 @@ pub enum Priority { High, } -impl fmt::Display for Priority { - fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { - use Priority::*; - let priority = match self { - Low => "low", - Medium => "medium", - High => "high", - }; - write!(f, "{}", priority) - } -} - - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TimeEntry { pub logged_date : chrono::NaiveDate, @@ -50,14 +36,12 @@ pub struct TimeEntry { pub duration : Duration, } -// Needs to preserve representation invariant of minutes < 60 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct Duration { hours : u16, minutes : u16, } - #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct InternalTask { pub id : Id, @@ -76,14 +60,16 @@ impl Task { /// Creates a new task from the input data. pub fn new(name : String, info : Option, tags : Vec, dependencies : Vec, priority : Option, due : Option, vault_folder : &path::Path, state : &mut state::State) -> Result { + // Exclude numeric names in the interest of allowing commands that take in ID or name. if name.chars().all(|c| c.is_numeric()) { return Err(error::Error::Generic(String::from("Name must not be purely numeric"))); }; + // Update the state with the new next Id. let id = state.data.next_id; state.data.next_id += 1; - let path = vault_folder.join("notes").join(&format!("{}.toml", id)); + let path = vault_folder.join("tasks").join(&format!("{}.toml", id)); let mut file = fs::File::options() .write(true) @@ -98,7 +84,7 @@ impl Task { state.data.deps.insert_edge(id, *dependency)?; } else { - return Err(error::Error::Generic(format!("No task with an ID of {} exists", colour::text::id(*dependency)))); + return Err(error::Error::Generic(format!("No task with an ID of {} exists", format::id(*dependency)))); } } } @@ -163,7 +149,7 @@ impl Task { /// Get an iterator over the IDs of tasks in a vault. fn id_iter(vault_folder : &path::Path) -> impl Iterator { - fs::read_dir(vault_folder.join("notes")) + fs::read_dir(vault_folder.join("tasks")) .unwrap() .map(|entry| entry.unwrap().path()) .filter(|p| p.is_file()) @@ -198,12 +184,12 @@ impl Task { /// Checks that a task with the prodided ID exists in the provided vault_folder. Returns the /// path of that task. pub fn check_exists(id : Id, vault_folder : &path::Path) -> Result { - let path = vault_folder.join("notes").join(format!("{}.toml", id)); + let path = vault_folder.join("tasks").join(format!("{}.toml", id)); if path.exists() && path.is_file() { Ok(path) } else { - Err(error::Error::Generic(format!("No task with the ID {} exists", colour::text::id(id)))) + Err(error::Error::Generic(format!("No task with the ID {} exists", format::id(id)))) } } @@ -241,6 +227,7 @@ impl Task { /// Displays a task to the terminal. pub fn display(&self, vault_folder : &path::Path, state : &state::State) -> Result<(), error::Error> { + /// Displays a line of hyphens of a specified length. fn line(len : usize) { for _ in 0..len { print!("-"); @@ -248,23 +235,21 @@ impl Task { println!(); } - let (heading, heading_length) = { - + let (heading, heading_length) = ( - format!("[{}] {} {}", if self.data.completed.is_some() {"X"} else {" "}, colour::text::id(self.data.id), colour::text::task(&self.data.name)), + format!("[{}] {} {}", if self.data.completed.is_some() {"X"} else {" "}, format::id(self.data.id), format::task(&self.data.name)), 5 + self.data.name.chars().count() + self.data.id.to_string().chars().count() - ) - }; + ); println!("{}", heading); line(heading_length); - println!("Priority: {}", colour::text::priority(&self.data.priority)); - println!("Tags: [{}]", format_hash_set(&self.data.tags)?); + println!("Priority: {}", format::priority(&self.data.priority)); + println!("Tags: [{}]", format::hash_set(&self.data.tags)?); println!("Created: {}", self.data.created.round_subsecs(0)); if let Some(due) = self.data.due { - let due = colour::text::due_date(&due, self.data.completed.is_none(), true); + let due = format::due_date(&due, self.data.completed.is_none()); println!("Due: {}", due); } @@ -283,10 +268,11 @@ impl Task { } } + // Display tracked time. if !self.data.time_entries.is_empty() { let mut entries = self.data.time_entries.clone(); - // Sort entries by date. + // Sort time entries by date. entries.sort_by(|e1, e2| e1.logged_date.cmp(&e2.logged_date)); let mut total = Duration::zero(); @@ -307,71 +293,58 @@ impl Task { } } + // Display dependencies as tree. if !self.data.dependencies.is_empty() { - let tasks = Task::load_all_as_map(vault_folder, true)?; println!("Dependencies:"); - dependency_tree(self.data.id, &String::new(), true, &state.data.deps, &tasks); + format::dependencies(self.data.id, vault_folder, &state.data.deps)?; } Ok(()) } } -fn format_hash_set(set : &HashSet) -> Result { - let mut output = String::new(); - - for value in set.iter() { - fmt::write(&mut output, format_args!("{}, ", value))?; - } - - // Remove the trailing comma and space. - if !output.is_empty() { - output.pop(); - output.pop(); - } - - Ok(output) -} - -fn dependency_tree(start : Id, prefix : &String, is_last_item : bool, graph : &graph::Graph, tasks : &HashMap) { - let next = graph.edges.get(&start).unwrap(); - - { - let task = tasks.get(&start).unwrap(); - - let name = if task.data.completed.is_some() { - colour::text::greyed_out(&task.data.name) - } - else { - colour::text::task(&task.data.name) - }; - - if is_last_item { - println!("{}└──{} (ID: {})", prefix, name, colour::text::id(start)) - } - else { - println!("{}├──{} (ID: {})", prefix, name, colour::text::id(start)) +impl Duration { + pub fn zero() -> Self { + Self { + minutes : 0, + hours : 0, } } - let count = next.len(); - - for (i, node) in next.iter().enumerate() { - let new_is_last_item = i == count - 1; - - let new_prefix = if is_last_item { - format!("{} ", prefix) - } - else { - format!("{}│ ", prefix) - }; - - dependency_tree(*node, &new_prefix, new_is_last_item, graph, tasks); + pub fn satisfies_invariant(&self) -> bool { + self.minutes < 60 } } +impl TimeEntry { + /// Adds up the times from a collection of time entries. + fn total(entries : &[TimeEntry]) -> Duration { + entries + .iter() + .map(|e| e.duration) + .fold(Duration::zero(), |a, d| a + d) + } + /// Creates a new TimeEntry, correctly validating and setting defaults. + pub fn new(hours : u16, minutes : u16, date : Option, message : Option) -> Self { + + let (hours, minutes) = { + (hours + minutes / 60, minutes % 60) + }; + + Self { + logged_date : date.unwrap_or(chrono::Utc::now().naive_local().date()), + message, + duration : Duration { + hours, + minutes, + } + } + } +} + +/// Compares due dates correctly, treating None as at infinity. fn compare_due_dates(first : &Option, second : &Option) -> cmp::Ordering { match (first, second) { (None, None) => cmp::Ordering::Equal, @@ -381,9 +354,9 @@ fn compare_due_dates(first : &Option, second : &Option) -> cmp::O } } +/// Lists all tasks in the specified vault. 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 .load_preset(comfy_table::presets::UTF8_FULL) @@ -578,15 +551,15 @@ pub fn list(mut options : super::ListOptions, vault_folder : &path::Path, state }, Column::Due => { row.push(match task.data.due { - Some(due) => colour::cell::due_date(&due, task.data.completed.is_none(), true), + Some(due) => format::cell::due_date(&due, task.data.completed.is_none()), None => Cell::from(String::new()) }); }, Column::Tags => { - row.push(Cell::new(format_hash_set(&task.data.tags)?)); + row.push(Cell::new(format::hash_set(&task.data.tags)?)); }, Column::Priority => { - row.push(colour::cell::priority(&task.data.priority)); + row.push(format::cell::priority(&task.data.priority)); }, Column::Status => { row.push( @@ -624,16 +597,6 @@ impl ops::Add for Duration { } } -impl Duration { - pub fn zero() -> Self { - Self { - minutes : 0, - hours : 0, - } - } -} - - impl ops::Div for Duration { type Output = Self; @@ -655,31 +618,15 @@ impl fmt::Display for Duration { } } -impl TimeEntry { - /// Adds up the times from a collection of time entries. - fn total(entries : &[TimeEntry]) -> Duration { - entries - .iter() - .map(|e| e.duration) - .fold(Duration::zero(), |a, d| a + d) - } - - /// Creates a new TimeEntry, correctly validating and setting defaults. - pub fn new(hours : u16, minutes : u16, date : Option, message : Option) -> Self { - - let (hours, minutes) = { - (hours + minutes / 60, minutes % 60) +impl fmt::Display for Priority { + fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + use Priority::*; + let priority = match self { + Low => "low", + Medium => "medium", + High => "high", }; - - Self { - logged_date : date.unwrap_or(chrono::Utc::now().naive_local().date()), - message, - duration : Duration { - hours, - minutes, - } - } + write!(f, "{}", priority) } } - diff --git a/src/vault.rs b/src/vault.rs index affe5b4..844cf70 100755 --- a/src/vault.rs +++ b/src/vault.rs @@ -1,6 +1,6 @@ use crate::error; use crate::state; -use crate::colour; +use crate::format; use crate::config; use std::fs; @@ -9,7 +9,7 @@ use std::path; pub fn new(name : String, path : path::PathBuf, config : &mut config::Config) -> Result<(), error::Error> { fn create_all_metadata(path : &path::Path) -> Result<(), error::Error> { - fs::create_dir(path.join("notes"))?; + fs::create_dir(path.join("tasks"))?; let _ = state::State::load(path)?; Ok(()) @@ -67,11 +67,11 @@ pub fn connect(name : String, path : path::PathBuf, config : &mut config::Config // Folder exists and contains data. if path.exists() && path.is_dir() { // Vault is missing required metadata files. - if !path.join("notes").exists() { - Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} folder", colour::text::file("notes")))) + if !path.join("tasks").exists() { + Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} folder", format::file("tasks")))) } else if !path.join("state.toml").exists() { - Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} file", colour::text::file("state.toml")))) + Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} file", format::file("state.toml")))) } // Required metadata exists, so the vault is connected. else {