diff --git a/src/args.rs b/src/args.rs index 67288d6..c9e1b0c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -100,8 +100,36 @@ pub struct EditTask { pub struct ModifyTask { /// ID of the task pub id: usize, - /// Summary of the task - pub summary: String, + + /// Title of the task + #[arg(short, long)] + #[clap(default_value=None)] + pub title: Option, + + /// Any notes to help explain/remember the task + #[arg(short, long)] + #[clap(default_value=None)] + pub notes: Option, + + /// Tags for organisation, separated by commas + #[arg(short, long)] + #[clap(default_value=None)] + pub tags: Option, + + /// Date when you want to do the task + #[arg(short, long)] + #[clap(default_value=None)] + pub when: Option, + + /// Deadline when the task has to be in + #[arg(short, long)] + #[clap(default_value=None)] + pub deadline: Option, + + /// The date and time when you want to be reminded + #[arg(short, long)] + #[clap(default_value=None)] + pub reminder: Option, } #[derive(Args, PartialEq, Debug)] pub struct GitExecute { @@ -111,12 +139,12 @@ pub struct GitExecute { #[derive(Args, PartialEq, Debug)] pub struct SyncTasks { /// Git remote to use - #[clap(default_value="origin")] + #[clap(default_value = "origin")] pub remote: String, } #[derive(Args, PartialEq, Debug)] pub struct UndoExecute { /// Number of times to undo - #[clap(default_value="1")] + #[clap(default_value = "1")] pub number: String, -} \ No newline at end of file +} diff --git a/src/cli.rs b/src/cli.rs index 0c795f3..fd2dae6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,72 +1,98 @@ -use chrono::{NaiveDateTime, Local}; -use crate::args::{TasksArgs, Commands}; -use crate::args::{CreateTask, DeleteTask, ShowTask, StartTask, StopTask, CompleteTask}; -use crate::tasks::{Tasks, Task, Status, TasksError}; -use prettytable::{Table, Row, row, format}; +use crate::args::{Commands, TasksArgs}; +use crate::args::{CompleteTask, CreateTask, DeleteTask, ShowTask, StartTask, StopTask, ModifyTask}; +use crate::tasks::{Status, Task, Tasks, TasksError}; +use chrono::{Local, NaiveDateTime}; use colored::*; use fuzzydate; +use prettytable::{format, row, Row, Table}; use std::panic; - -fn success(msg: String) { - println!("{} {}", "success:".green().bold(), msg); -} - pub fn warning(msg: &str) { println!("{} {}", "warning:".yellow().bold(), msg); } -pub fn info(msg: &str) { +pub fn info(msg: String) { println!("{} {}", "info:".blue().bold(), msg); } +fn success(msg: String) { + println!("{} {}", "success:".green().bold(), msg); +} + fn task_msg(msg: &str, task: &Task, id: usize) -> String { - format!("{} task: {}({})", msg, task.title.blue(), id.to_string().cyan()) + format!( + "{} task: {}({})", + msg, + task.title.blue(), + id.to_string().cyan() + ) } fn parse_date(date_string: Option) -> Option { if date_string.is_some() { match fuzzydate::parse(date_string.unwrap()) { Ok(date) => Some(date), - Err(err) => panic!("{:?}", err), + Err(err) => panic!("{} {:?}", "error:".red().bold(), err), } } else { None } } +fn date_to_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() + } +} + fn calc_row(task: &Task, id: usize) -> Row { if task.status == Status::Complete { // Generate greyed out rows for complete tasks - Row::from([id.to_string().bright_black().italic(), + Row::from([ + id.to_string().bright_black().italic(), task.status.as_string().bright_black().italic(), - "N/A".bright_black(), - task.title.clone().bright_black().italic()]) - + task.title.clone().bright_black().italic(), + date_to_string(&task.when).bright_black().italic(), + date_to_string(&task.deadline).bright_black().italic(), + ]) } else { - let when = if task.when.is_some() { - let date = format!("{}", task.when.unwrap().format("%Y-%m-%d")); - let now = Local::now().date_naive(); - - if now == task.when.unwrap().date() { - date.bright_red() - } else if now.succ_opt().unwrap() == task.when.unwrap().date() { - date.yellow() - } else { - date.white() - } - } else { - "N/A".bright_black() - }; - // Generate normal colored rows for uncompleted tasks - Row::from([id.to_string().cyan(), task.status.as_string(), when, task.title.clone().white()]) + Row::from([ + id.to_string().cyan(), + task.status.as_string(), + task.title.clone().white(), + date_to_string(&task.when), + date_to_string(&task.deadline), + ]) } } pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<&mut Tasks, TasksError> { match arguments.command { - Commands::Add(CreateTask { title, notes, tags, when, deadline, reminder, ..}) => { + Commands::Add(CreateTask { + title, + notes, + tags, + when, + deadline, + reminder, + .. + }) => { let when = parse_date(when); let deadline = parse_date(deadline); let reminder = parse_date(reminder); @@ -80,8 +106,30 @@ pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<&mut Tasks, Ta tasks.push(task.clone()); let id = tasks.len() - 1; - success(task_msg("created", &task, id)); + success(task_msg("created", &task, id)); + Ok(tasks) + } + + Commands::Modify(ModifyTask { id, title, notes, tags, when, deadline, reminder }) => { + let when = parse_date(when); + let deadline = parse_date(deadline); + let reminder = parse_date(reminder); + let tags: Option> = if tags.is_some() { + Some(tags.unwrap().split(",").map(str::to_string).collect()) + } else { + None + }; + + let task = tasks.get_task(id)?; + + if title.is_some() { + info(task_msg("renaming task", &task, id)) + }; + + task.modify(title, notes, tags, when, deadline, reminder); + + success(task_msg("modified", &task, id)); Ok(tasks) } @@ -128,11 +176,17 @@ pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<&mut Tasks, Ta Commands::Show(ShowTask { id }) => { if id.is_none() { if tasks.is_empty() { - info("no tasks found") + info(String::from("no tasks found")) } else { // Create the table for printing let mut table = Table::new(); - table.set_titles(row!["ID".magenta().bold(), "Status".magenta().bold(), "When".magenta().bold(), "Title".magenta().bold()]); + table.set_titles(row![ + "ID".magenta().bold(), + "Status".magenta().bold(), + "Title".magenta().bold(), + "When".magenta().bold(), + "Deadline".magenta().bold(), + ]); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); // Iterate through each task @@ -152,7 +206,10 @@ pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<&mut Tasks, Ta // Generate and print the table let mut table = Table::new(); - table.set_titles(row!["ID".magenta().bold(), "Status".magenta().bold(), "When".magenta().bold(), "Title".magenta().bold()]); + 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)); println!("{}", table) @@ -161,6 +218,6 @@ pub fn execute(tasks: &mut Tasks, arguments: TasksArgs) -> Result<&mut Tasks, Ta Ok(tasks) } - _ => todo!() + _ => todo!(), } } diff --git a/src/data.rs b/src/data.rs index d95d251..d1f4885 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,12 +1,11 @@ +use dirs::home_dir; +use serde_json; use std::error::Error; use std::fs; use std::path::Path; -use dirs::home_dir; -use serde_json; use crate::tasks::Tasks; - const TASKS_FILE_PATH: &str = "/.local/share/tasks"; pub fn save_tasks>(path: P, tasks: &Tasks) -> Result<(), Box> { @@ -30,5 +29,9 @@ pub fn load_tasks>(path: P) -> Result> { } pub fn tasks_file_path() -> String { - format!("{}{}", home_dir().unwrap().to_str().unwrap(), TASKS_FILE_PATH) + format!( + "{}{}", + home_dir().unwrap().to_str().unwrap(), + TASKS_FILE_PATH + ) } diff --git a/src/main.rs b/src/main.rs index 4a7dc33..9b20c76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ -mod tasks; mod args; -mod data; mod cli; +mod data; +mod tasks; -use crate::tasks::Tasks; use crate::args::TasksArgs; -use std::path::Path; +use crate::tasks::Tasks; use clap::Parser; use colored::*; +use std::path::Path; fn main() { // Generate the file path for tasks diff --git a/src/tasks.rs b/src/tasks.rs index 9c72a4d..df7fb2c 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,7 +1,6 @@ -use serde::{Deserialize, Serialize}; use chrono::NaiveDateTime; use colored::*; - +use serde::{Deserialize, Serialize}; #[derive(Debug)] pub struct TasksError(String); @@ -16,11 +15,10 @@ pub enum Status { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Task { - pub title: String, // The required title of the task - pub status: Status, // Current status of the task + pub title: String, // The required title of the task + pub status: Status, // Current status of the task pub notes: Option, // Any notes to explain the task - pub tags: Option>, // Tasks can be tagged for organisation - pub subtasks: Option>, // Tasks can be hierarchically split into subtasks + pub tags: Option>, // Tasks can be tagged for organisation pub when: Option, // The date you want to do the task pub deadline: Option, // The latest date the task should be done pub reminder: Option, // The datetime a reminder will alert you @@ -32,13 +30,64 @@ pub struct Tasks { pub tasks: Option>, // All the tasks in one vector } -impl Task { - pub fn new(title: String, notes: Option, tags: Option>, - when: Option, deadline: Option, - reminder: Option) -> Self { - let status = if when.is_some() { Status::Pending } else { Status::Inbox }; +fn task_not_found(id: usize) -> TasksError { + TasksError(format!("couldn't find task with id {}", id)) +} - Self { title, status, notes, tags, subtasks: None, when, deadline, reminder, } +fn no_tasks_available() -> TasksError { + TasksError(String::from("no tasks available")) +} + +impl Task { + pub fn new( + title: String, + notes: Option, + tags: Option>, + when: Option, + deadline: Option, + reminder: Option, + ) -> Self { + let status = if when.is_some() { + Status::Pending + } else { + Status::Inbox + }; + + Self { + title, + status, + notes, + tags, + when, + deadline, + reminder, + } + } + + pub fn modify(&mut self, title: Option, notes: Option, tags: Option>, when: Option, deadline: Option, reminder: Option) { + if title.is_some() { + self.title = title.unwrap(); + }; + + if notes.is_some() { + self.notes = Some(notes.unwrap()); + }; + + if tags.is_some() { + self.tags = Some(tags.unwrap()); + }; + + if when.is_some() { + self.when = Some(when.unwrap()); + }; + + if deadline.is_some() { + self.deadline = Some(deadline.unwrap()); + }; + + if reminder.is_some() { + self.reminder = Some(reminder.unwrap()); + }; } pub fn start(&mut self) { @@ -62,16 +111,16 @@ impl Tasks { pub fn new(tasks_path: &str) -> Self { Self { path: String::from(tasks_path), - tasks: None + tasks: None, } } - fn task_not_found(&self, id: usize) -> TasksError { - TasksError(format!("couldn't find task with id {}", id)) - } - - fn task_exists(&self, id: usize) -> bool{ - if id >= self.len() { false } else { true } + pub fn task_exists(&self, id: usize) -> bool { + if id >= self.len() { + false + } else { + true + } } pub fn is_empty(&self) -> bool { @@ -84,13 +133,12 @@ impl Tasks { pub fn get_task(&mut self, id: usize) -> Result<&mut Task, TasksError> { if self.is_empty() { - Err(TasksError(format!("no tasks available"))) + Err(no_tasks_available()) } else { if self.task_exists(id) { - let task = &mut self.tasks.as_mut().unwrap()[id]; - Ok(task) + Ok(&mut self.tasks.as_mut().unwrap()[id]) } else { - Err(TasksError(format!("couldn't find task with id {}", id))) + Err(task_not_found(id)) } } } @@ -108,7 +156,7 @@ impl Tasks { self.tasks.as_mut().unwrap().remove(id); Ok(()) } else { - Err(self.task_not_found(id)) + Err(task_not_found(id)) } } @@ -122,7 +170,7 @@ impl Tasks { pub fn clear(&mut self) -> Result<(), TasksError> { if self.is_empty() { - Err(TasksError(String::from("no tasks available"))) + Err(no_tasks_available()) } else { self.tasks = None; Ok(()) @@ -135,9 +183,8 @@ impl Status { match self { Status::Inbox => "📮 Inbox".blue(), Status::Pending => "📅 Pending".yellow(), - Status::Active => "✍️ Active".red(), + Status::Active => "🕑 Active".red(), Status::Complete => "📗 Complete".green(), } } } -