diff --git a/src/args.rs b/src/args.rs index 853d46d..53fefbe 100644 --- a/src/args.rs +++ b/src/args.rs @@ -23,6 +23,8 @@ pub enum Commands { Start(StartTask), /// Marks a task as pending Stop(StopTask), + /// Returns a task to the inbox + Inbox(InboxTask), /// Edit a task with $EDITOR Edit(EditTask), /// Modify a task at the command line @@ -92,6 +94,11 @@ pub struct StopTask { pub id: usize, } #[derive(Args, PartialEq, Eq, Debug)] +pub struct InboxTask { + /// ID of the task + pub id: usize, +} +#[derive(Args, PartialEq, Eq, Debug)] pub struct EditTask { /// ID of the task pub id: usize, diff --git a/src/cli.rs b/src/cli.rs index 60ef5d0..d0114f4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,8 +6,10 @@ mod tables; use crate::args::{Commands, GitExecute, TasksArgs}; use crate::args::{ - CompleteTask, CreateTask, DeleteTask, ModifyTask, ShowTask, StartTask, StopTask, SyncTasks, + CompleteTask, CreateTask, DeleteTask, InboxTask, ModifyTask, ShowTask, StartTask, StopTask, + SyncTasks, }; +use crate::repo; use crate::tasks::{Tasks, TasksError}; pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<(), TasksError> { @@ -51,6 +53,10 @@ pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<(), TasksError cmds::stop(tasks, id)?; } + Commands::Inbox(InboxTask { id }) => { + cmds::inbox(tasks, id)?; + } + Commands::Clear => { cmds::clear(tasks)?; } @@ -59,12 +65,12 @@ pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<(), TasksError cmds::show(tasks, id)?; } - Commands::Git(GitExecute { command }) => match git::execute(&tasks.path, command) { + Commands::Git(GitExecute { command }) => match repo::execute(&tasks.path, command) { Ok(..) => (), Err(..) => panic!("failed to execute git cmd"), }, - Commands::Sync(SyncTasks { remote }) => match git::sync(&tasks.path, remote) { + Commands::Sync(SyncTasks { remote }) => match repo::sync(&tasks.path, remote) { Ok(..) => (), Err(..) => panic!("failed"), }, diff --git a/src/cli/cmds.rs b/src/cli/cmds.rs index 7d9b912..5ec51dd 100644 --- a/src/cli/cmds.rs +++ b/src/cli/cmds.rs @@ -4,6 +4,7 @@ use crate::cli::tables; use crate::tasks::{Task, Tasks, TasksError}; fn parse_tags(tags: Option) -> Option> { + // Split tags into a vector by commas tags.map(|tags| tags.split(',').map(str::to_string).collect()) } @@ -11,7 +12,7 @@ pub fn show(tasks: &mut Tasks, id: Option) -> Result<(), TasksError> { // If no id is given, print out all tasks if let Some(id) = id { // Get the task the user wants to see - let task = tasks.get_task(id)?; + let task = tasks.task(id)?; // Generate the table for the singular task let table = tables::task_table(task, id); @@ -75,7 +76,7 @@ pub fn modify( let tags = parse_tags(tags); // Get the task the user wants - let task = tasks.get_task(id)?; + let task = tasks.task(id)?; // If the the user changes the title, show that here if title.is_some() { @@ -93,7 +94,7 @@ pub fn modify( pub fn delete(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { // Get the task the user wants to delete for output later let mut binding = tasks.clone(); - let task = binding.get_task(id)?; + let task = binding.task(id)?; // Delete the task tasks.remove(id)?; @@ -114,7 +115,7 @@ pub fn clear(tasks: &mut Tasks) -> Result<(), TasksError> { pub fn stop(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { // Get the task the user wants to stop - let task = tasks.get_task(id)?; + let task = tasks.task(id)?; // Stop the task task.stop(); @@ -125,7 +126,7 @@ pub fn stop(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { pub fn start(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { // Get the task the user wants to start - let task = tasks.get_task(id)?; + let task = tasks.task(id)?; // Start the task task.start(); @@ -136,7 +137,7 @@ pub fn start(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { pub fn done(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { // Get the task the user wants to complete - let task = tasks.get_task(id)?; + let task = tasks.task(id)?; // Complete the task task.complete(); @@ -144,3 +145,14 @@ pub fn done(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { output::success(output::task_msg("completed", task, id)); Ok(()) } + +pub fn inbox(tasks: &mut Tasks, id: usize) -> Result<(), TasksError> { + // Get the task the user wants to return to the inbox + let task = tasks.task(id)?; + // Inbox the task + task.inbox(); + + // Success + output::success(output::task_msg("inboxed", task, id)); + Ok(()) +} diff --git a/src/cli/dates.rs b/src/cli/dates.rs index 3e15655..4b101e0 100644 --- a/src/cli/dates.rs +++ b/src/cli/dates.rs @@ -1,5 +1,5 @@ -use chrono::{Local, NaiveDateTime}; -use colored::{ColoredString, Colorize}; +use chrono::NaiveDateTime; +use colored::Colorize; pub fn parse_fuzzy_date(date_string: Option) -> Option { if let Some(date_string) = date_string { @@ -11,24 +11,3 @@ pub fn parse_fuzzy_date(date_string: Option) -> Option { None } } - -pub fn date_as_string(date: &Option) -> ColoredString { - if date.is_some() { - let date = date.unwrap().date(); - let date_string = format!("{}", date.format("%Y-%m-%d")); - let now = Local::now().date_naive(); - - if date <= now { - // If the date is today or past today - date_string.bright_red() - } else if now.succ_opt().unwrap() == date { - // If the date is tomorrow - date_string.yellow() - } else { - // Otherwise the date is too far in the past - date_string.white() - } - } else { - "N/A".bright_black() - } -} diff --git a/src/cli/git.rs b/src/cli/git.rs index e796e48..8b13789 100644 --- a/src/cli/git.rs +++ b/src/cli/git.rs @@ -1,30 +1 @@ -use std::error::Error; -use std::process::Command; -use crate::cli::output; - -pub fn execute(path: &str, command: String) -> Result<(), Box> { - let output = Command::new("git") - .args(["-C", path]) - .args(command.split(' ')) - .output()?; - - if !output.stdout.is_empty() { - output::git(String::from_utf8(output.stdout).unwrap()); - }; - if !output.stderr.is_empty() { - output::error(String::from_utf8(output.stderr).unwrap()); - }; - - Ok(()) -} - -pub fn sync(repo_path: &str, remote: String) -> Result<(), Box> { - execute( - repo_path, - format!("pull --ff --no-rebase --no-edit --commit {remote}"), - )?; - execute(repo_path, format!("push {remote}"))?; - - Ok(()) -} diff --git a/src/cli/output.rs b/src/cli/output.rs index 41ee3c5..2d31d52 100644 --- a/src/cli/output.rs +++ b/src/cli/output.rs @@ -26,7 +26,7 @@ pub fn task_msg(msg: &str, task: &Task, id: usize) -> String { format!( "{} task: {}({})", msg, - task.title.blue(), + task.title_string().blue(), id.to_string().cyan() ) } diff --git a/src/cli/tables.rs b/src/cli/tables.rs index 570837a..6b6ad17 100644 --- a/src/cli/tables.rs +++ b/src/cli/tables.rs @@ -1,29 +1,26 @@ use colored::Colorize; use prettytable::{format, row, Row, Table}; -use crate::cli::dates; -use crate::tasks::{Status, Task, Tasks}; +use crate::tasks::{Task, Tasks}; pub fn calc_row(task: &Task, id: usize) -> Row { - if task.status == Status::Complete { + if task.is_complete() { // Generate greyed out rows for complete tasks Row::from([ id.to_string().bright_black().italic(), - task.status.as_string().bright_black().italic(), - task.title.clone().bright_black().italic(), - dates::date_as_string(&task.when).bright_black().italic(), - dates::date_as_string(&task.deadline) - .bright_black() - .italic(), + task.status_string().bright_black().italic(), + task.title_string().bright_black().italic(), + task.when_string().bright_black().italic(), + task.deadline_string().bright_black().italic(), ]) } else { // Generate normal colored rows for uncompleted tasks Row::from([ id.to_string().cyan(), - task.status.as_string(), - task.title.clone().white(), - dates::date_as_string(&task.when), - dates::date_as_string(&task.deadline), + task.status_string(), + task.title_string(), + task.when_string(), + task.deadline_string(), ]) } } @@ -52,7 +49,16 @@ pub fn task_table(task: &Task, id: usize) -> Table { let mut table = Table::new(); table.set_titles(row!["Item".magenta().bold(), "Value".magenta().bold()]); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.add_row(calc_row(task, id)); + + // Add rows + table.add_row(row!["ID".white().bold(), id.to_string().cyan()]); + table.add_row(row!["Status".white().bold(), task.status_string()]); + table.add_row(row!["Title".white().bold(), task.title_string()]); + table.add_row(row!["When".white().bold(), task.when_string(),]); + table.add_row(row!["Deadline".white().bold(), task.deadline_string(),]); + table.add_row(row!["Reminder".white().bold(), task.reminder_string(),]); + table.add_row(row!["Tags".white().bold(), &task.tags_string()]); + table.add_row(row!["Notes".white().bold(), &task.notes_string()]); table } diff --git a/src/data.rs b/src/data.rs deleted file mode 100644 index 9833977..0000000 --- a/src/data.rs +++ /dev/null @@ -1,61 +0,0 @@ -use dirs::home_dir; -use std::error::Error; -use std::fs; -use std::path::Path; -use std::string::ToString; - -use crate::cli::git; -use crate::cli::output; -use crate::tasks::Tasks; - -pub fn save_tasks>(path: P, tasks: &Tasks) -> Result<(), Box> { - // Convert the tasks to TOML format - let data = toml::to_string_pretty(&tasks)?; - - // Write the TOML to the file - fs::write(path, data)?; - - Ok(()) -} - -pub fn load_tasks + ToString>( - path: P, - tasks_file: &str, -) -> Result> { - let tasks_file_path = &format!("{}/{}", path.to_string(), tasks_file); - - // Read TOML from the file - let data = fs::read_to_string(tasks_file_path)?; - - // Load the tasks from TOML form - let tasks: Tasks = toml::from_str(&data)?; - - Ok(tasks) -} - -pub fn ensure_repo(path: &str, tasks_file: &str) -> Result<(), Box> { - // Generate the path of the tasks file - let tasks_file_path = &format!("{}/{}", path, tasks_file); - - // Check if the path exists - if !Path::new(path).exists() { - output::warning(format!( - "tasks repository {path} does not exist. creating..." - )); - fs::create_dir_all(path).unwrap(); - let tasks = Tasks::new(path, tasks_file); - save_tasks(tasks_file_path, &tasks).unwrap(); - git::execute(path, String::from("init"))?; - git::execute(path, String::from("add ."))?; - output::success(format!("created tasks repo {path}")); - } - - Ok(()) -} - -pub fn tasks_repo_string() -> String { - // Generate the path for the location of tasks - let home_dir = home_dir().unwrap(); - let home_dir = home_dir.to_str().unwrap(); - format!("{home_dir}/.local/share/inertia") -} diff --git a/src/main.rs b/src/main.rs index c47a677..29d7d76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod args; mod cli; -mod data; +mod repo; mod tasks; use clap::Parser; @@ -10,29 +10,31 @@ use crate::args::TasksArgs; fn main() { // Generate the file paths for tasks - let repo_path = &data::tasks_repo_string(); - let tasks_file = "tasks"; + let repo_path = repo::tasks_repo_string(); + let tasks_file_path = repo::tasks_file_path(); // If the tasks file doesn't exist, create it first - match data::ensure_repo(repo_path, tasks_file) { + match repo::ensure_repo(&repo_path) { Ok(..) => (), Err(error) => panic!("{} {:?}", "error:".red().bold(), error), }; // Load tasks and check for any errors when loading the tasks - let mut tasks = match data::load_tasks(repo_path, tasks_file) { + let mut tasks = match repo::load_tasks(&tasks_file_path) { Ok(tasks) => tasks, Err(error) => panic!("{} {:?}", "error:".red().bold(), error), }; // Parse command line arguments let arguments = TasksArgs::parse(); + + // Execute the inputted command line arguments match cli::execute(&mut tasks, arguments) { Ok(..) => (), Err(error) => panic!("{} {:?}", "error:".red().bold(), error), }; // Save any changes - cli::git::execute(repo_path, String::from("add --all")).unwrap(); - data::save_tasks(&repo_path, &tasks).unwrap(); + repo::save_tasks(&tasks_file_path, &tasks).unwrap(); + repo::execute(&repo_path, String::from("add --all")).unwrap(); } diff --git a/src/repo.rs b/src/repo.rs new file mode 100644 index 0000000..4cea589 --- /dev/null +++ b/src/repo.rs @@ -0,0 +1,97 @@ +use dirs::home_dir; +use std::error::Error; +use std::fs; +use std::path::Path; +use std::process::Command; +use std::string::ToString; + +use crate::cli::output; +use crate::tasks::Tasks; + +const TASKS_FILE: &str = "tasks.toml"; + +pub fn execute(path: &str, command: String) -> Result<(), Box> { + let output = Command::new("git") + .args(["-C", path]) + .args(command.split(' ')) + .output()?; + + if !output.stdout.is_empty() { + output::git(String::from_utf8(output.stdout).unwrap()); + }; + if !output.stderr.is_empty() { + output::error(String::from_utf8(output.stderr).unwrap()); + }; + + Ok(()) +} + +pub fn save_tasks>(path: P, tasks: &Tasks) -> Result<(), Box> { + // Convert the tasks to TOML format + let data = toml::to_string_pretty(&tasks)?; + + // Write the TOML to the file + fs::write(path, data)?; + + Ok(()) +} + +pub fn load_tasks + ToString>(path: P) -> Result> { + // Read TOML from the file + let data = fs::read_to_string(path)?; + + // Load the tasks from TOML form + let tasks: Tasks = toml::from_str(&data)?; + + Ok(tasks) +} + +pub fn ensure_repo(path: &str) -> Result<(), Box> { + // Generate the path of the tasks file + let tasks_file_path = tasks_file_path(); + + // Check if the path exists + if !Path::new(path).exists() { + output::warning(format!( + "tasks repository {path} does not exist. creating..." + )); + + // Create the directory + fs::create_dir_all(path).unwrap(); + // Generate a new empty tasks structure + let tasks = Tasks::new(path, TASKS_FILE); + + // Save the tasks + save_tasks(tasks_file_path, &tasks).unwrap(); + + // Create the git repository + execute(path, String::from("init"))?; + execute(path, String::from("add --all"))?; + + // Success + output::success(format!("created tasks repo {path}")); + } + + Ok(()) +} + +pub fn tasks_repo_string() -> String { + // Generate the path for the location of tasks + let home_dir = home_dir().unwrap(); + let home_dir = home_dir.to_str().unwrap(); + format!("{home_dir}/.local/share/inertia") +} + +pub fn tasks_file_path() -> String { + format!("{}/{}", tasks_repo_string(), TASKS_FILE) +} + +pub fn sync(repo_path: &str, remote: String) -> Result<(), Box> { + execute( + repo_path, + format!("pull --ff --no-rebase --no-edit --commit {remote}"), + )?; + execute(repo_path, format!("push {remote}"))?; + + Ok(()) +} diff --git a/src/tasks.rs b/src/tasks.rs index 82cafa9..9d7e609 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,16 +1,37 @@ -use chrono::NaiveDateTime; -use colored::*; +use chrono::{Local, NaiveDateTime}; +use colored::{ColoredString, Colorize}; use serde::{Deserialize, Serialize}; #[derive(Debug)] pub struct TasksError(String); +impl TasksError { + pub fn no_task(id: usize) -> TasksError { + TasksError(format!("couldn't find task with id {}", id)) + } + + pub fn no_tasks() -> TasksError { + TasksError(String::from("no tasks available")) + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum Status { - Inbox, - Pending, - Active, - Complete, + Inbox, // When you create a new task without a when date + Pending, // When you give a task a when date + Active, // When you have started a task + Complete, // When a task is completed +} + +impl Status { + pub fn as_string(&self) -> ColoredString { + match self { + Status::Inbox => "📮 Inbox".blue(), + Status::Pending => "📅 Pending".yellow(), + Status::Active => "🕑 Active".red(), + Status::Complete => "📗 Complete".green(), + } + } } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -24,21 +45,6 @@ pub struct Task { pub reminder: Option, // The datetime a reminder will alert you } -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Tasks { - pub path: String, // Path to the tasks repository - pub file: String, // Path to the tasks file in the repository - pub tasks: Option>, // All the tasks in one vector -} - -fn task_not_found(id: usize) -> TasksError { - TasksError(format!("couldn't find task with id {}", id)) -} - -fn no_tasks_available() -> TasksError { - TasksError(String::from("no tasks available")) -} - impl Task { pub fn new( title: String, @@ -88,6 +94,9 @@ impl Task { if let Some(when) = when { self.when = Some(when); + if self.is_inbox() { + self.pend() + }; }; if let Some(deadline) = deadline { @@ -98,22 +107,118 @@ impl Task { self.reminder = Some(reminder); }; } +} - pub fn start(&mut self) { - self.status = Status::Active; - } - - pub fn stop(&mut self) { - if self.when.is_some() { - self.status = Status::Inbox; - } else { - self.status = Status::Pending; - } +impl Task { + pub fn inbox(&mut self) { + self.status = Status::Inbox; + self.when = None; } pub fn complete(&mut self) { self.status = Status::Complete; } + + pub fn start(&mut self) { + self.status = Status::Active; + } + + pub fn pend(&mut self) { + self.status = Status::Pending; + } + + pub fn stop(&mut self) { + if self.when.is_some() { + self.status = Status::Pending; + } else { + self.status = Status::Inbox; + } + } +} + +impl Task { + pub fn is_complete(&self) -> bool { + self.status == Status::Complete + } + + pub fn is_active(&self) -> bool { + self.status == Status::Active + } + + pub fn is_pending(&self) -> bool { + self.status == Status::Pending + } + + pub fn is_inbox(&self) -> bool { + self.status == Status::Inbox + } +} + +impl Task { + fn date_string(&self, date: &Option) -> ColoredString { + if let Some(date) = date { + let date = date.date(); + let date_string = format!("{}", date.format("%Y-%m-%d")); + let now = Local::now().date_naive(); + + if date <= now { + // If the date is today or past today + date_string.bright_red() + } else if now.succ_opt().unwrap() == date { + // If the date is tomorrow + date_string.yellow() + } else { + // Otherwise the date is too far in the past + date_string.white() + } + } else { + // No date available + "N/A".bright_black() + } + } + + pub fn when_string(&self) -> ColoredString { + self.date_string(&self.when) + } + + pub fn deadline_string(&self) -> ColoredString { + self.date_string(&self.deadline) + } + + pub fn reminder_string(&self) -> ColoredString { + self.date_string(&self.reminder) + } + + pub fn title_string(&self) -> ColoredString { + self.title.white() + } + + pub fn status_string(&self) -> ColoredString { + self.status.as_string() + } + + pub fn tags_string(&self) -> ColoredString { + if let Some(tags) = &self.tags { + tags.join(", ").white() + } else { + "N/A".bright_black() + } + } + + pub fn notes_string(&self) -> ColoredString { + if let Some(notes) = &self.notes { + notes.white() + } else { + "N/A".bright_black() + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Tasks { + pub path: String, // Path to the tasks repository + pub file: String, // Path to the tasks file in the repository + pub tasks: Option>, // All the tasks in one vector } impl Tasks { @@ -124,25 +229,32 @@ impl Tasks { tasks: None, } } +} - pub fn task_exists(&self, id: usize) -> bool { - id < self.len() - } - +impl Tasks { + /// Checks if tasks are empty pub fn is_empty(&self) -> bool { self.len() == 0 } - pub fn get_task(&mut self, id: usize) -> Result<&mut Task, TasksError> { - if self.is_empty() { - Err(no_tasks_available()) - } else if self.task_exists(id) { - Ok(&mut self.tasks.as_mut().unwrap()[id]) - } else { - Err(task_not_found(id)) - } + /// Checks if a task exists from an id + pub fn exists(&self, id: usize) -> bool { + id < self.len() } + /// Returns a task from an id + pub fn task(&mut self, id: usize) -> Result<&mut Task, TasksError> { + if self.is_empty() { + Err(TasksError::no_tasks()) + } else if self.exists(id) { + Ok(&mut self.tasks.as_mut().unwrap()[id]) + } else { + Err(TasksError::no_task(id)) + } + } +} + +impl Tasks { pub fn push(&mut self, task: Task) { if self.is_empty() { self.tasks = Some(vec![task]); @@ -152,11 +264,11 @@ impl Tasks { } pub fn remove(&mut self, id: usize) -> Result<(), TasksError> { - if self.task_exists(id) { + if self.exists(id) { self.tasks.as_mut().unwrap().remove(id); Ok(()) } else { - Err(task_not_found(id)) + Err(TasksError::no_task(id)) } } @@ -170,21 +282,10 @@ impl Tasks { pub fn clear(&mut self) -> Result<(), TasksError> { if self.is_empty() { - Err(no_tasks_available()) + Err(TasksError::no_tasks()) } else { self.tasks = None; Ok(()) } } } - -impl Status { - pub fn as_string(&self) -> ColoredString { - match self { - Status::Inbox => "📮 Inbox".blue(), - Status::Pending => "📅 Pending".yellow(), - Status::Active => "🕑 Active".red(), - Status::Complete => "📗 Complete".green(), - } - } -}