code cleanup; listed invariants; enforce duration minutes invariant across edits; renamed notes to tasks everywhere
This commit is contained in:
		
							
								
								
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -775,7 +775,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "toru"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "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::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 <NAME> <PATH>"))))
 | 
			
		||||
            Err(error::Error::Generic(format!("No vaults currently set up, try running: {}", format::command("toru vault new <NAME> <PATH>"))))
 | 
			
		||||
        }
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/edit.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								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<process::ExitStatus, error::Error> {
 | 
			
		||||
@@ -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))));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								src/error.rs
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								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),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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::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<Id>) -> 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(" -> ");
 | 
			
		||||
 
 | 
			
		||||
@@ -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)))),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										294
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										294
									
								
								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<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,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
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()?;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Self, error::Error> {
 | 
			
		||||
        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::<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 {
 | 
			
		||||
                    max_id = i128::from(id);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										183
									
								
								src/tasks.rs
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								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<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()) {
 | 
			
		||||
            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<Item = u64> {
 | 
			
		||||
        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<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() {
 | 
			
		||||
            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<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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn dependency_tree(start : Id, prefix : &String, is_last_item : bool, graph : &graph::Graph, tasks : &HashMap<Id, Task>) {
 | 
			
		||||
    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<chrono::NaiveDate>, message : Option<String>) -> 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<T : Ord>(first : &Option<T>, second : &Option<T>) -> cmp::Ordering {
 | 
			
		||||
    match (first, second) {
 | 
			
		||||
        (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> {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    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<usize> 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<chrono::NaiveDate>, message : Option<String>) -> 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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								src/vault.rs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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 {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user