code cleanup; listed invariants; enforce duration minutes invariant across edits; renamed notes to tasks everywhere
This commit is contained in:
parent
001cc3fa3d
commit
f3e6e75a8a
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -775,7 +775,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toru"
|
name = "toru"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
20
dev-notes/data invariants.md
Normal file
20
dev-notes/data invariants.md
Normal file
@ -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)
|
221
src/args.rs
Normal file
221
src/args.rs
Normal file
@ -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<String>,
|
||||||
|
#[clap(short, long)]
|
||||||
|
tag : Vec<String>,
|
||||||
|
#[clap(short, long)]
|
||||||
|
dependency : Vec<Id>,
|
||||||
|
#[clap(short, long, value_enum)]
|
||||||
|
priority : Option<tasks::Priority>,
|
||||||
|
/// Due date, expecting format yyyy-mm-ddThh:mm:ss
|
||||||
|
#[clap(long)]
|
||||||
|
due : Option<chrono::NaiveDateTime>,
|
||||||
|
},
|
||||||
|
/// 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<String>,
|
||||||
|
},
|
||||||
|
/// Run Subversion commands at the root of the vault.
|
||||||
|
#[clap(trailing_var_arg=true)]
|
||||||
|
Svn {
|
||||||
|
args : Vec<String>,
|
||||||
|
},
|
||||||
|
/// 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<chrono::NaiveDate>,
|
||||||
|
/// Message to identify the time entry.
|
||||||
|
#[clap(short, long)]
|
||||||
|
message : Option<String>,
|
||||||
|
},
|
||||||
|
/// 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<Column>,
|
||||||
|
/// 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<String>,
|
||||||
|
/// Only include tasks due before a certain date (inclusive).
|
||||||
|
#[clap(long)]
|
||||||
|
pub due_before : Option<chrono::NaiveDate>,
|
||||||
|
/// Only include tasks due after a certain date (inclusive).
|
||||||
|
#[clap(long)]
|
||||||
|
pub due_after : Option<chrono::NaiveDate>,
|
||||||
|
/// Only include tasks created before a certain date (inclusive).
|
||||||
|
#[clap(long)]
|
||||||
|
pub created_before : Option<chrono::NaiveDate>,
|
||||||
|
/// Only include tasks created after a certain date (inclusive).
|
||||||
|
#[clap(long)]
|
||||||
|
pub created_after : Option<chrono::NaiveDate>,
|
||||||
|
/// 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<String>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
207
src/colour.rs
207
src/colour.rs
@ -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<T : Into<comfy_table::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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
|||||||
use crate::error;
|
use crate::error;
|
||||||
|
use crate::format;
|
||||||
use crate::colour;
|
|
||||||
|
|
||||||
use std::path;
|
use std::path;
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ impl Config {
|
|||||||
|
|
||||||
for (name, _) in &mut self.vaults {
|
for (name, _) in &mut self.vaults {
|
||||||
if *name == new_name {
|
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 {
|
if name == old_name {
|
||||||
@ -60,7 +59,7 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
None => {
|
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)
|
Ok(path)
|
||||||
},
|
},
|
||||||
None => {
|
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(())
|
Ok(())
|
||||||
},
|
},
|
||||||
None => {
|
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()));
|
let width = self.vaults.iter().fold(usize::MIN, |c, (n, _)| c.max(n.len()));
|
||||||
|
|
||||||
if self.vaults.is_empty() {
|
if self.vaults.is_empty() {
|
||||||
Err(error::Error::Generic(format!("No vaults currently set up, try running: {}", colour::text::command("toru vault new <NAME> <PATH>"))))
|
Err(error::Error::Generic(format!("No vaults currently set up, try running: {}", format::command("toru vault new <NAME> <PATH>"))))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (i, (name, path)) in self.vaults.iter().enumerate() {
|
for (i, (name, path)) in self.vaults.iter().enumerate() {
|
||||||
@ -117,7 +116,7 @@ impl Config {
|
|||||||
print!(" ");
|
print!(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
print!("{}", colour::text::vault(name));
|
print!("{}", format::vault(name));
|
||||||
|
|
||||||
let padding = width - name.len() + 1;
|
let padding = width - name.len() + 1;
|
||||||
|
|
||||||
|
13
src/edit.rs
13
src/edit.rs
@ -7,7 +7,7 @@ use crate::tasks;
|
|||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::graph;
|
use crate::graph;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
use crate::colour;
|
use crate::format;
|
||||||
use crate::tasks::Id;
|
use crate::tasks::Id;
|
||||||
|
|
||||||
pub fn open_editor(path : &path::Path, editor : &str) -> Result<process::ExitStatus, error::Error> {
|
pub fn open_editor(path : &path::Path, editor : &str) -> Result<process::ExitStatus, error::Error> {
|
||||||
@ -75,9 +75,18 @@ pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str, state : &m
|
|||||||
else {
|
else {
|
||||||
let mut edited_task = tasks::Task::load_direct(temp_path.clone(), true)?;
|
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 {
|
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")))
|
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()) {
|
else if edited_task.data.name.chars().all(|c| c.is_numeric()) {
|
||||||
Err(error::Error::Generic(String::from("Name must not be purely 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)?;
|
state.data.deps.insert_edge(id, *dependency)?;
|
||||||
}
|
}
|
||||||
else {
|
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))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
src/error.rs
20
src/error.rs
@ -1,4 +1,4 @@
|
|||||||
use crate::colour;
|
use crate::format;
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -20,15 +20,15 @@ pub enum Error {
|
|||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Error::Io(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::Io(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::Confy(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::Confy(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::Trash(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::Trash(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::TomlDe(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::TomlDe(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::TomlSer(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::TomlSer(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::Utf8(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::Utf8(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::Fmt(err) => write!(f, "{} {}", colour::text::error("Internal Error:"), err),
|
Error::Fmt(err) => write!(f, "{} {}", format::error("Internal Error:"), err),
|
||||||
Error::Generic(message) => write!(f, "{} {}", colour::text::error("Error:"), message),
|
Error::Generic(message) => write!(f, "{} {}", format::error("Error:"), message),
|
||||||
Error::Internal(message) => write!(f, "{} {}", colour::text::error("Internal Error:"), message),
|
Error::Internal(message) => write!(f, "{} {}", format::error("Internal Error:"), message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
253
src/format.rs
Executable file
253
src/format.rs
Executable file
@ -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<T : fmt::Display>(set : &HashSet<T>) -> Result<String, error::Error> {
|
||||||
|
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<Id, tasks::Task>) -> 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<T : Into<comfy_table::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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::tasks;
|
use crate::tasks;
|
||||||
use crate::colour;
|
use crate::format;
|
||||||
use crate::tasks::Id;
|
use crate::tasks::Id;
|
||||||
|
|
||||||
use std::fmt::Write;
|
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")))
|
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 {
|
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 {
|
else {
|
||||||
let outgoing = self.edges.get_mut(&first).unwrap();
|
let outgoing = self.edges.get_mut(&first).unwrap();
|
||||||
@ -136,7 +136,7 @@ pub fn format_cycle(cycle : &Vec<Id>) -> String {
|
|||||||
let mut formatted = String::new();
|
let mut formatted = String::new();
|
||||||
|
|
||||||
for (index, node) in cycle.iter().enumerate() {
|
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 {
|
if index != cycle.len() - 1 {
|
||||||
formatted.push_str(" -> ");
|
formatted.push_str(" -> ");
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::tasks;
|
use crate::tasks;
|
||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::colour;
|
use crate::format;
|
||||||
use crate::tasks::Id;
|
use crate::tasks::Id;
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
@ -69,7 +69,7 @@ impl Index {
|
|||||||
else {
|
else {
|
||||||
let coloured_ids : Vec<_> =
|
let coloured_ids : Vec<_> =
|
||||||
ids.iter()
|
ids.iter()
|
||||||
.map(|i| colour::text::id(*i))
|
.map(|i| format::id(*i))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut display_ids = String::new();
|
let mut display_ids = String::new();
|
||||||
@ -83,10 +83,10 @@ impl Index {
|
|||||||
display_ids.pop();
|
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)))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
294
src/main.rs
294
src/main.rs
@ -1,5 +1,6 @@
|
|||||||
mod vcs;
|
mod vcs;
|
||||||
mod edit;
|
mod edit;
|
||||||
|
mod args;
|
||||||
mod vault;
|
mod vault;
|
||||||
mod index;
|
mod index;
|
||||||
mod error;
|
mod error;
|
||||||
@ -8,270 +9,57 @@ mod state;
|
|||||||
mod graph;
|
mod graph;
|
||||||
mod stats;
|
mod stats;
|
||||||
mod config;
|
mod config;
|
||||||
mod colour;
|
mod format;
|
||||||
|
|
||||||
use tasks::Id;
|
use args::*;
|
||||||
|
|
||||||
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<String>,
|
|
||||||
#[clap(short, long)]
|
|
||||||
tag : Vec<String>,
|
|
||||||
#[clap(short, long)]
|
|
||||||
dependency : Vec<Id>,
|
|
||||||
#[clap(short, long, value_enum)]
|
|
||||||
priority : Option<tasks::Priority>,
|
|
||||||
/// Due date, expecting format yyyy-mm-ddThh:mm:ss
|
|
||||||
#[clap(long)]
|
|
||||||
due : Option<chrono::NaiveDateTime>,
|
|
||||||
},
|
|
||||||
/// 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<String>,
|
|
||||||
},
|
|
||||||
/// Run Subversion commands at the root of the vault.
|
|
||||||
#[clap(trailing_var_arg=true)]
|
|
||||||
Svn {
|
|
||||||
args : Vec<String>,
|
|
||||||
},
|
|
||||||
/// 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<chrono::NaiveDate>,
|
|
||||||
/// Message to identify the time entry.
|
|
||||||
#[clap(short, long)]
|
|
||||||
message : Option<String>,
|
|
||||||
},
|
|
||||||
/// 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<Column>,
|
|
||||||
/// 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<String>,
|
|
||||||
/// Only include tasks due before a certain date (inclusive).
|
|
||||||
#[clap(long)]
|
|
||||||
due_before : Option<chrono::NaiveDate>,
|
|
||||||
/// Only include tasks due after a certain date (inclusive).
|
|
||||||
#[clap(long)]
|
|
||||||
due_after : Option<chrono::NaiveDate>,
|
|
||||||
/// Only include tasks created before a certain date (inclusive).
|
|
||||||
#[clap(long)]
|
|
||||||
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.
|
|
||||||
#[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<String>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let result = program();
|
let result = program();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => (),
|
Ok(()) => {
|
||||||
|
std::process::exit(0);
|
||||||
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("{}", err);
|
println!("{}", err);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn program() -> Result<(), error::Error> {
|
fn program() -> Result<(), error::Error> {
|
||||||
let command = {
|
let command = Args::accept_command();
|
||||||
use clap::Parser;
|
|
||||||
Args::parse().command
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut config = config::Config::load()?;
|
let mut config = config::Config::load()?;
|
||||||
|
|
||||||
use Command::*;
|
if let Command::Vault(command) = command {
|
||||||
if let Vault(command) = command {
|
|
||||||
use VaultCommand::*;
|
|
||||||
match command {
|
match command {
|
||||||
New { name, path } => {
|
VaultCommand::New { name, path } => {
|
||||||
vault::new(name.clone(), path, &mut config)?;
|
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)?;
|
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)?;
|
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)?;
|
vault::delete(&name, &mut config)?;
|
||||||
println!("Deleted vault {}", colour::text::vault(&name));
|
println!("Deleted vault {}", format::vault(&name));
|
||||||
},
|
},
|
||||||
List => {
|
VaultCommand::List => {
|
||||||
config.list_vaults()?;
|
config.list_vaults()?;
|
||||||
},
|
},
|
||||||
Rename { old_name, new_name } => {
|
VaultCommand::Rename { old_name, new_name } => {
|
||||||
config.rename_vault(&old_name, new_name.clone())?;
|
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::*;
|
use ConfigCommand::*;
|
||||||
match command {
|
match command {
|
||||||
Editor { editor } => {
|
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)?;
|
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;
|
let vault_folder = &config.current_vault()?.1;
|
||||||
vcs::command(args, vcs::Vcs::Git, vault_folder)?;
|
vcs::command(args, vcs::Vcs::Git, vault_folder)?;
|
||||||
}
|
}
|
||||||
else if command == GitIgnore {
|
else if command == Command::GitIgnore {
|
||||||
let vault_folder = &config.current_vault()?.1;
|
let vault_folder = &config.current_vault()?.1;
|
||||||
vcs::create_gitignore(vault_folder)?;
|
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;
|
let vault_folder = &config.current_vault()?.1;
|
||||||
vcs::command(args, vcs::Vcs::Svn, vault_folder)?;
|
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)?;
|
let mut state = state::State::load(vault_folder)?;
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
New { name, info, tags, dependencies, priority, due } => {
|
Command::New { name, info, tag, dependency, priority, due } => {
|
||||||
let task = tasks::Task::new(name, info, tags, dependencies, priority, due, vault_folder, &mut state)?;
|
let task = tasks::Task::new(name, info, tag, dependency, priority, due, vault_folder, &mut state)?;
|
||||||
println!("Created task {} (ID: {})", colour::text::task(&task.data.name), colour::text::id(task.data.id));
|
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 id = state.data.index.lookup(&id_or_name)?;
|
||||||
let task = tasks::Task::load(id, vault_folder, false)?;
|
let task = tasks::Task::load(id, vault_folder, false)?;
|
||||||
let name = task.data.name.clone();
|
let name = task.data.name.clone();
|
||||||
@ -322,14 +110,14 @@ fn program() -> Result<(), error::Error> {
|
|||||||
state.data.deps.remove_node(task.data.id);
|
state.data.deps.remove_node(task.data.id);
|
||||||
task.delete()?;
|
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 id = state.data.index.lookup(&id_or_name)?;
|
||||||
let task = tasks::Task::load(id, vault_folder, true)?;
|
let task = tasks::Task::load(id, vault_folder, true)?;
|
||||||
task.display(vault_folder, &state)?;
|
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)?;
|
let id = state.data.index.lookup(&id_or_name)?;
|
||||||
if info {
|
if info {
|
||||||
edit::edit_info(id, vault_folder.clone(), &config.editor)?;
|
edit::edit_info(id, vault_folder.clone(), &config.editor)?;
|
||||||
@ -337,16 +125,16 @@ fn program() -> Result<(), error::Error> {
|
|||||||
else {
|
else {
|
||||||
edit::edit_raw(id, vault_folder.clone(), &config.editor, &mut state)?;
|
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 id = state.data.index.lookup(&id_or_name)?;
|
||||||
let mut task = tasks::Task::load(id, vault_folder, false)?;
|
let mut task = tasks::Task::load(id, vault_folder, false)?;
|
||||||
let entry = tasks::TimeEntry::new(hours, minutes, date, message);
|
let entry = tasks::TimeEntry::new(hours, minutes, date, message);
|
||||||
task.data.time_entries.push(entry);
|
task.data.time_entries.push(entry);
|
||||||
task.save()?;
|
task.save()?;
|
||||||
},
|
},
|
||||||
Stats(command) => {
|
Command::Stats(command) => {
|
||||||
use StatsCommand::*;
|
use StatsCommand::*;
|
||||||
match command {
|
match command {
|
||||||
Tracked { days } => {
|
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 id = state.data.index.lookup(&id_or_name)?;
|
||||||
let mut task = tasks::Task::load(id, vault_folder, false)?;
|
let mut task = tasks::Task::load(id, vault_folder, false)?;
|
||||||
task.data.completed = Some(chrono::Local::now().naive_local());
|
task.data.completed = Some(chrono::Local::now().naive_local());
|
||||||
task.save()?;
|
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)?;
|
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!(),
|
Command::Vault(_) | Command::Config(_) | Command::Git { args : _ } | Command::Svn { args : _ } | Command::Switch { name : _ } | Command::GitIgnore => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
state.save()?;
|
state.save()?;
|
||||||
|
@ -23,7 +23,7 @@ pub struct InternalState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
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<Self, error::Error> {
|
pub fn load(vault_location : &path::Path) -> Result<Self, error::Error> {
|
||||||
let path = vault_location.join("state.toml");
|
let path = vault_location.join("state.toml");
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ impl State {
|
|||||||
|
|
||||||
// Calculating the next ID if necessary.
|
// Calculating the next ID if necessary.
|
||||||
let mut max_id : i128 = -1;
|
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::<Id>().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::<Id>().ok()) {
|
||||||
|
|
||||||
if i128::try_from(id).unwrap() > max_id {
|
if i128::try_from(id).unwrap() > max_id {
|
||||||
max_id = i128::from(id);
|
max_id = i128::from(id);
|
||||||
|
171
src/tasks.rs
171
src/tasks.rs
@ -1,7 +1,6 @@
|
|||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
use crate::graph;
|
use crate::format;
|
||||||
use crate::colour;
|
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -30,19 +29,6 @@ pub enum Priority {
|
|||||||
High,
|
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)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct TimeEntry {
|
pub struct TimeEntry {
|
||||||
pub logged_date : chrono::NaiveDate,
|
pub logged_date : chrono::NaiveDate,
|
||||||
@ -50,14 +36,12 @@ pub struct TimeEntry {
|
|||||||
pub duration : Duration,
|
pub duration : Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needs to preserve representation invariant of minutes < 60
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct InternalTask {
|
pub struct InternalTask {
|
||||||
pub id : Id,
|
pub id : Id,
|
||||||
@ -76,14 +60,16 @@ impl Task {
|
|||||||
/// Creates a new task from the input data.
|
/// Creates a new task from the input data.
|
||||||
pub fn new(name : String, info : Option<String>, tags : Vec<String>, dependencies : Vec<Id>, priority : Option<Priority>, due : Option<chrono::NaiveDateTime>, vault_folder : &path::Path, state : &mut state::State) -> Result<Self, error::Error> {
|
pub fn new(name : String, info : Option<String>, tags : Vec<String>, dependencies : Vec<Id>, priority : Option<Priority>, due : Option<chrono::NaiveDateTime>, vault_folder : &path::Path, state : &mut state::State) -> Result<Self, error::Error> {
|
||||||
|
|
||||||
|
// Exclude numeric names in the interest of allowing commands that take in ID or name.
|
||||||
if name.chars().all(|c| c.is_numeric()) {
|
if name.chars().all(|c| c.is_numeric()) {
|
||||||
return Err(error::Error::Generic(String::from("Name must not be purely 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;
|
let id = state.data.next_id;
|
||||||
state.data.next_id += 1;
|
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()
|
let mut file = fs::File::options()
|
||||||
.write(true)
|
.write(true)
|
||||||
@ -98,7 +84,7 @@ impl Task {
|
|||||||
state.data.deps.insert_edge(id, *dependency)?;
|
state.data.deps.insert_edge(id, *dependency)?;
|
||||||
}
|
}
|
||||||
else {
|
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.
|
/// Get an iterator over the IDs of tasks in a vault.
|
||||||
fn id_iter(vault_folder : &path::Path) -> impl Iterator<Item = u64> {
|
fn id_iter(vault_folder : &path::Path) -> impl Iterator<Item = u64> {
|
||||||
fs::read_dir(vault_folder.join("notes"))
|
fs::read_dir(vault_folder.join("tasks"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|entry| entry.unwrap().path())
|
.map(|entry| entry.unwrap().path())
|
||||||
.filter(|p| p.is_file())
|
.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
|
/// Checks that a task with the prodided ID exists in the provided vault_folder. Returns the
|
||||||
/// path of that task.
|
/// path of that task.
|
||||||
pub fn check_exists(id : Id, vault_folder : &path::Path) -> Result<path::PathBuf, error::Error> {
|
pub fn check_exists(id : Id, vault_folder : &path::Path) -> Result<path::PathBuf, error::Error> {
|
||||||
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() {
|
if path.exists() && path.is_file() {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
else {
|
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.
|
/// Displays a task to the terminal.
|
||||||
pub fn display(&self, vault_folder : &path::Path, state : &state::State) -> Result<(), error::Error> {
|
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) {
|
fn line(len : usize) {
|
||||||
for _ in 0..len {
|
for _ in 0..len {
|
||||||
print!("-");
|
print!("-");
|
||||||
@ -248,23 +235,21 @@ impl Task {
|
|||||||
println!();
|
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()
|
5 + self.data.name.chars().count() + self.data.id.to_string().chars().count()
|
||||||
)
|
);
|
||||||
};
|
|
||||||
|
|
||||||
println!("{}", heading);
|
println!("{}", heading);
|
||||||
line(heading_length);
|
line(heading_length);
|
||||||
|
|
||||||
println!("Priority: {}", colour::text::priority(&self.data.priority));
|
println!("Priority: {}", format::priority(&self.data.priority));
|
||||||
println!("Tags: [{}]", format_hash_set(&self.data.tags)?);
|
println!("Tags: [{}]", format::hash_set(&self.data.tags)?);
|
||||||
println!("Created: {}", self.data.created.round_subsecs(0));
|
println!("Created: {}", self.data.created.round_subsecs(0));
|
||||||
|
|
||||||
if let Some(due) = self.data.due {
|
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);
|
println!("Due: {}", due);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,10 +268,11 @@ impl Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display tracked time.
|
||||||
if !self.data.time_entries.is_empty() {
|
if !self.data.time_entries.is_empty() {
|
||||||
|
|
||||||
let mut entries = self.data.time_entries.clone();
|
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));
|
entries.sort_by(|e1, e2| e1.logged_date.cmp(&e2.logged_date));
|
||||||
|
|
||||||
let mut total = Duration::zero();
|
let mut total = Duration::zero();
|
||||||
@ -307,71 +293,58 @@ impl Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display dependencies as tree.
|
||||||
if !self.data.dependencies.is_empty() {
|
if !self.data.dependencies.is_empty() {
|
||||||
let tasks = Task::load_all_as_map(vault_folder, true)?;
|
|
||||||
|
|
||||||
println!("Dependencies:");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_hash_set<T : fmt::Display>(set : &HashSet<T>) -> Result<String, error::Error> {
|
impl Duration {
|
||||||
let mut output = String::new();
|
pub fn zero() -> Self {
|
||||||
|
Self {
|
||||||
for value in set.iter() {
|
minutes : 0,
|
||||||
fmt::write(&mut output, format_args!("{}, ", value))?;
|
hours : 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the trailing comma and space.
|
pub fn satisfies_invariant(&self) -> bool {
|
||||||
if !output.is_empty() {
|
self.minutes < 60
|
||||||
output.pop();
|
}
|
||||||
output.pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dependency_tree(start : Id, prefix : &String, is_last_item : bool, graph : &graph::Graph, tasks : &HashMap<Id, Task>) {
|
/// Creates a new TimeEntry, correctly validating and setting defaults.
|
||||||
let next = graph.edges.get(&start).unwrap();
|
pub fn new(hours : u16, minutes : u16, date : Option<chrono::NaiveDate>, message : Option<String>) -> Self {
|
||||||
|
|
||||||
{
|
let (hours, minutes) = {
|
||||||
let task = tasks.get(&start).unwrap();
|
(hours + minutes / 60, minutes % 60)
|
||||||
|
|
||||||
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 {
|
Self {
|
||||||
println!("{}└──{} (ID: {})", prefix, name, colour::text::id(start))
|
logged_date : date.unwrap_or(chrono::Utc::now().naive_local().date()),
|
||||||
|
message,
|
||||||
|
duration : Duration {
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
println!("{}├──{} (ID: {})", prefix, name, colour::text::id(start))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = next.len();
|
/// Compares due dates correctly, treating None as at infinity.
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn compare_due_dates<T : Ord>(first : &Option<T>, second : &Option<T>) -> cmp::Ordering {
|
fn compare_due_dates<T : Ord>(first : &Option<T>, second : &Option<T>) -> cmp::Ordering {
|
||||||
match (first, second) {
|
match (first, second) {
|
||||||
(None, None) => cmp::Ordering::Equal,
|
(None, None) => cmp::Ordering::Equal,
|
||||||
@ -381,9 +354,9 @@ fn compare_due_dates<T : Ord>(first : &Option<T>, second : &Option<T>) -> 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> {
|
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
|
||||||
.load_preset(comfy_table::presets::UTF8_FULL)
|
.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 => {
|
Column::Due => {
|
||||||
row.push(match task.data.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())
|
None => Cell::from(String::new())
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
Column::Tags => {
|
Column::Tags => {
|
||||||
row.push(Cell::new(format_hash_set(&task.data.tags)?));
|
row.push(Cell::new(format::hash_set(&task.data.tags)?));
|
||||||
},
|
},
|
||||||
Column::Priority => {
|
Column::Priority => {
|
||||||
row.push(colour::cell::priority(&task.data.priority));
|
row.push(format::cell::priority(&task.data.priority));
|
||||||
},
|
},
|
||||||
Column::Status => {
|
Column::Status => {
|
||||||
row.push(
|
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<usize> for Duration {
|
impl ops::Div<usize> for Duration {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
@ -655,31 +618,15 @@ impl fmt::Display for Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimeEntry {
|
impl fmt::Display for Priority {
|
||||||
/// Adds up the times from a collection of time entries.
|
fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fn total(entries : &[TimeEntry]) -> Duration {
|
use Priority::*;
|
||||||
entries
|
let priority = match self {
|
||||||
.iter()
|
Low => "low",
|
||||||
.map(|e| e.duration)
|
Medium => "medium",
|
||||||
.fold(Duration::zero(), |a, d| a + d)
|
High => "high",
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new TimeEntry, correctly validating and setting defaults.
|
|
||||||
pub fn new(hours : u16, minutes : u16, date : Option<chrono::NaiveDate>, message : Option<String>) -> Self {
|
|
||||||
|
|
||||||
let (hours, minutes) = {
|
|
||||||
(hours + minutes / 60, minutes % 60)
|
|
||||||
};
|
};
|
||||||
|
write!(f, "{}", priority)
|
||||||
Self {
|
|
||||||
logged_date : date.unwrap_or(chrono::Utc::now().naive_local().date()),
|
|
||||||
message,
|
|
||||||
duration : Duration {
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
10
src/vault.rs
10
src/vault.rs
@ -1,6 +1,6 @@
|
|||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
use crate::colour;
|
use crate::format;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
|
||||||
use std::fs;
|
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> {
|
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> {
|
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)?;
|
let _ = state::State::load(path)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -67,11 +67,11 @@ pub fn connect(name : String, path : path::PathBuf, config : &mut config::Config
|
|||||||
// Folder exists and contains data.
|
// Folder exists and contains data.
|
||||||
if path.exists() && path.is_dir() {
|
if path.exists() && path.is_dir() {
|
||||||
// Vault is missing required metadata files.
|
// Vault is missing required metadata files.
|
||||||
if !path.join("notes").exists() {
|
if !path.join("tasks").exists() {
|
||||||
Err(error::Error::Generic(format!("Cannot connect the vault as it is missing the {} folder", colour::text::file("notes"))))
|
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() {
|
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.
|
// Required metadata exists, so the vault is connected.
|
||||||
else {
|
else {
|
||||||
|
Loading…
Reference in New Issue
Block a user