toru/src/main.rs

389 lines
12 KiB
Rust
Raw Normal View History

2022-08-24 10:38:47 +00:00
mod vcs;
2022-08-21 00:59:09 +00:00
mod edit;
2022-08-20 04:01:43 +00:00
mod vault;
2022-08-25 00:44:22 +00:00
mod index;
2022-08-20 04:01:43 +00:00
mod error;
mod tasks;
mod state;
mod graph;
mod stats;
2022-08-20 04:01:43 +00:00
mod config;
mod colour;
2022-08-21 00:59:09 +00:00
use tasks::Id;
2022-08-20 04:01:43 +00:00
use std::path;
#[derive(clap::Parser, Debug)]
struct Args {
#[clap(subcommand)]
command : Command,
}
2022-08-22 08:51:54 +00:00
#[derive(clap::Subcommand, Debug, PartialEq, Eq)]
#[clap(version, help_short='h', about, author, global_setting=clap::AppSettings::DisableHelpSubcommand)]
2022-08-20 04:01:43 +00:00
enum Command {
/// Create a new task.
New {
#[clap(short, long)]
name : String,
#[clap(short, long)]
info : Option<String>,
#[clap(short, long)]
tags : Vec<String>,
#[clap(short, long)]
2022-08-21 00:59:09 +00:00
dependencies : Vec<Id>,
2022-08-20 04:01:43 +00:00
#[clap(short, long, value_enum)]
priority : Option<tasks::Priority>,
/// Due date, expecting format yyyy-mm-ddThh:mm:ss
#[clap(long)]
due : Option<chrono::NaiveDateTime>,
2022-08-20 04:01:43 +00:00
},
/// Displays the specified task in detail.
View {
id_or_name : String,
2022-08-21 00:59:09 +00:00
},
/// Edit a task directly.
2022-08-21 00:59:09 +00:00
Edit {
id_or_name : String,
2022-08-21 02:50:03 +00:00
/// Edit the info specifically in its own file.
#[clap(short, long)]
info : bool,
},
/// Delete a task (move file to trash).
2022-08-20 04:01:43 +00:00
Delete {
id_or_name : String,
2022-08-20 04:01:43 +00:00
},
/// Mark a task as complete.
Complete {
id_or_name : String,
2022-08-20 04:01:43 +00:00
},
2022-08-20 07:06:55 +00:00
/// Run Git commands at the root of the vault.
#[clap(trailing_var_arg=true)]
Git {
args : Vec<String>,
},
2022-08-24 10:38:47 +00:00
/// Run Subversion commands at the root of the vault.
#[clap(trailing_var_arg=true)]
Svn {
args : Vec<String>,
},
2022-08-22 08:51:54 +00:00
/// Adds the recommended .gitignore file to the vault.
#[clap(name="gitignore")]
GitIgnore,
2022-08-25 10:44:10 +00:00
/// Lists tasks according to the specified fields, ordering and filters.
List {
2022-08-25 10:44:10 +00:00
#[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,
},
/// For statistics about the state of your vault.
#[clap(subcommand)]
Stats(StatsCommand),
2022-08-21 07:34:44 +00:00
/// For making changes to global configuration.
#[clap(subcommand)]
Config(ConfigCommand),
2022-08-20 04:01:43 +00:00
/// Commands for interacting with vaults.
#[clap(subcommand)]
Vault(VaultCommand),
/// Switches to the specified vault.
Switch {
name : String,
},
2022-08-20 04:01:43 +00:00
}
2022-08-25 10:44:10 +00:00
#[derive(clap::StructOpt, Debug, PartialEq, Eq)]
pub struct ListOptions {
2022-08-25 10:55:28 +00:00
/// Include name column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
name : bool,
2022-08-25 10:55:28 +00:00
/// Include tracked time column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
tracked : bool,
2022-08-25 10:55:28 +00:00
/// Include due date column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
due : bool,
2022-08-25 10:55:28 +00:00
/// Include tags column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
tags : bool,
2022-08-25 10:55:28 +00:00
/// Include priority column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
priority : bool,
2022-08-25 10:55:28 +00:00
/// Include status column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
status : bool,
2022-08-25 10:55:28 +00:00
/// Include created date column.
2022-08-25 10:44:10 +00:00
#[clap(long)]
created : bool,
2022-08-25 10:55:28 +00:00
/// The field to sort by.
2022-08-25 10:44:10 +00:00
#[clap(long, value_enum, default_value_t=SortBy::Id)]
sort_by : SortBy,
2022-08-25 10:55:28 +00:00
/// Sort ascending on descending.
2022-08-25 10:44:10 +00:00
#[clap(long, value_enum, default_value_t=SortType::Asc)]
sort_type : SortType,
2022-08-25 10:55:28 +00:00
/// Only include tasks created before a certain date.
2022-08-25 10:44:10 +00:00
#[clap(long)]
before : Option<chrono::NaiveDateTime>,
2022-08-25 10:55:28 +00:00
/// Only include tasks created after a certain date.
2022-08-25 10:44:10 +00:00
#[clap(long)]
after : Option<chrono::NaiveDateTime>,
2022-08-25 10:55:28 +00:00
/// Only include tasks due within a certain number of days.
2022-08-25 10:44:10 +00:00
#[clap(long)]
due_in : Option<u16>,
2022-08-25 10:55:28 +00:00
/// Include completed tasks in the list.
2022-08-25 10:44:10 +00:00
#[clap(long)]
2022-08-25 10:55:28 +00:00
include_completed : bool,
2022-08-25 10:44:10 +00:00
}
impl Default for ListOptions {
fn default() -> Self {
Self {
name : true,
tracked : true,
due : true,
tags : true,
priority : true,
status : false,
created : false,
sort_by : SortBy::Created,
sort_type : SortType::Desc,
before : None,
after : None,
due_in : None,
2022-08-25 10:55:28 +00:00
include_completed : false,
2022-08-25 10:44:10 +00:00
}
}
}
#[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum SortType {
#[default]
Asc,
Desc,
}
#[derive(Default, Clone, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum SortBy {
#[default]
Id,
Name,
Due,
Priority,
Created,
}
#[derive(clap::Subcommand, Debug, PartialEq, Eq)]
enum StatsCommand {
2022-08-24 23:15:15 +00:00
/// View time tracked per tag recently.
Tracked {
#[clap(short, long, default_value_t=7)]
days : u16,
},
2022-08-24 23:15:15 +00:00
/// View recently completed tasks.
Completed {
#[clap(short, long, default_value_t=7)]
days : u16,
},
}
2022-08-22 08:51:54 +00:00
#[derive(clap::Subcommand, Debug, PartialEq, Eq)]
2022-08-21 07:34:44 +00:00
enum ConfigCommand {
/// For checking or changing default text editor command.
Editor {
/// Command to launch editor. Omit to view current editor.
editor : Option<String>,
}
}
2022-08-22 08:51:54 +00:00
#[derive(clap::Subcommand, Debug, PartialEq, Eq)]
2022-08-20 04:01:43 +00:00
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,
2022-08-23 11:05:28 +00:00
/// For renaming an already set up vault.
Rename {
old_name : String,
new_name : String,
}
2022-08-20 04:01:43 +00:00
}
fn main() {
let result = program();
match result {
Ok(()) => (),
Err(err) => {
println!("{}", err);
2022-08-20 04:01:43 +00:00
}
}
}
fn program() -> Result<(), error::Error> {
let command = {
use clap::Parser;
Args::parse().command
};
let mut config = config::Config::load()?;
use Command::*;
if let Vault(command) = command {
use VaultCommand::*;
match command {
New { name, path } => {
vault::new(name.clone(), path, &mut config)?;
println!("Created vault {}", colour::vault(&name));
},
Disconnect { name } => {
vault::disconnect(&name, &mut config)?;
println!("Disconnected vault {}", colour::vault(&name));
},
Connect { name , path } => {
vault::connect(name.clone(), path, &mut config)?;
println!("Connected vault {}", colour::vault(&name));
},
Delete { name } => {
vault::delete(&name, &mut config)?;
println!("Deleted vault {}", colour::vault(&name));
},
List => {
config.list_vaults()?;
},
2022-08-23 11:05:28 +00:00
Rename { old_name, new_name } => {
config.rename_vault(&old_name, new_name.clone())?;
println!("Renamed vault {} to {}", colour::vault(&old_name), colour::vault(&new_name));
}
}
}
else if let Config(command) = command {
use ConfigCommand::*;
match command {
Editor { editor } => {
match editor {
Some(editor) => {
config.editor = editor;
println!("Updated editor command to: {}", config.editor);
},
None => {
println!("Current editor command: {}", config.editor);
2022-08-21 07:34:44 +00:00
}
}
}
}
}
else if let Switch { name } = command {
config.switch(&name)?;
println!("Switched to vault {}", colour::vault(&name));
}
else if let Git { args } = command {
let vault_folder = &config.current_vault()?.1;
2022-08-24 10:38:47 +00:00
vcs::command(args, vcs::Vcs::Git, vault_folder)?;
}
2022-08-22 08:51:54 +00:00
else if command == GitIgnore {
let vault_folder = &config.current_vault()?.1;
2022-08-24 10:38:47 +00:00
vcs::create_gitignore(vault_folder)?;
println!("Default {} file created", colour::file(".gitignore"));
2022-08-22 08:51:54 +00:00
}
2022-08-24 10:38:47 +00:00
else if let Svn { args } = command {
let vault_folder = &config.current_vault()?.1;
vcs::command(args, vcs::Vcs::Svn, vault_folder)?;
}
// Commands that require loading in the state.
else {
let vault_folder = &config.current_vault()?.1;
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::task_name(&task.data.name), colour::id(task.data.id));
},
Delete { id_or_name } => {
2022-08-25 00:44:22 +00:00
let id = state.data.index.lookup(&id_or_name)?;
let task = tasks::Task::load(id, vault_folder, false)?;
let name = task.data.name.clone();
2022-08-25 00:44:22 +00:00
state.data.index.remove(task.data.name.clone(), task.data.id);
state.data.deps.remove_node(task.data.id);
task.delete()?;
println!("Deleted task {} (ID: {})", colour::task_name(&name), colour::id(id));
},
View { id_or_name } => {
2022-08-25 00:44:22 +00:00
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 } => {
2022-08-25 00:44:22 +00:00
let id = state.data.index.lookup(&id_or_name)?;
if info {
edit::edit_info(id, vault_folder.clone(), &config.editor)?;
}
else {
edit::edit_raw(id, vault_folder.clone(), &config.editor, &mut state)?;
}
println!("Updated task {}", colour::id(id));
},
Track { id_or_name, hours, minutes } => {
2022-08-25 00:44:22 +00:00
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);
task.data.time_entries.push(entry);
task.save()?;
},
Stats(command) => {
use StatsCommand::*;
match command {
Tracked { days } => {
stats::time_per_tag(days, vault_folder)?;
2022-08-24 23:15:15 +00:00
},
Completed { days } => {
stats::completed_tasks(days, vault_folder)?;
}
}
},
Complete { id_or_name } => {
2022-08-25 00:44:22 +00:00
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::id(id));
},
2022-08-25 10:44:10 +00:00
List { options } => {
tasks::list(options, vault_folder)?;
},
// All commands which are dealt with in if let chain at start.
2022-08-24 10:38:47 +00:00
Vault(_) | Config(_) | Git { args : _ } | Svn { args : _ } | Switch { name : _ } | GitIgnore => unreachable!(),
2022-08-20 04:01:43 +00:00
}
state.save()?;
2022-08-20 04:01:43 +00:00
}
config.save()?;
Ok(())
}