🎨 General code improvements and fixes

This commit is contained in:
Maddie H 2023-02-26 20:21:39 +00:00
parent b53ce1de5f
commit 81d6fd91e5
No known key found for this signature in database
GPG Key ID: 64FAA9959751687D
11 changed files with 321 additions and 201 deletions

View File

@ -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,

View File

@ -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"),
},

View File

@ -4,6 +4,7 @@ use crate::cli::tables;
use crate::tasks::{Task, Tasks, TasksError};
fn parse_tags(tags: Option<String>) -> Option<Vec<String>> {
// 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<usize>) -> 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(())
}

View File

@ -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<String>) -> Option<NaiveDateTime> {
if let Some(date_string) = date_string {
@ -11,24 +11,3 @@ pub fn parse_fuzzy_date(date_string: Option<String>) -> Option<NaiveDateTime> {
None
}
}
pub fn date_as_string(date: &Option<NaiveDateTime>) -> 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()
}
}

View File

@ -1,30 +1 @@
use std::error::Error;
use std::process::Command;
use crate::cli::output;
pub fn execute(path: &str, command: String) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
execute(
repo_path,
format!("pull --ff --no-rebase --no-edit --commit {remote}"),
)?;
execute(repo_path, format!("push {remote}"))?;
Ok(())
}

View File

@ -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()
)
}

View File

@ -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
}

View File

@ -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<P: AsRef<Path>>(path: P, tasks: &Tasks) -> Result<(), Box<dyn Error>> {
// 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<P: AsRef<Path> + ToString>(
path: P,
tasks_file: &str,
) -> Result<Tasks, Box<dyn Error>> {
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<dyn Error>> {
// 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")
}

View File

@ -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();
}

97
src/repo.rs Normal file
View File

@ -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<dyn Error>> {
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<P: AsRef<Path>>(path: P, tasks: &Tasks) -> Result<(), Box<dyn Error>> {
// 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<P: AsRef<Path> + ToString>(path: P) -> Result<Tasks, Box<dyn Error>> {
// 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<dyn Error>> {
// 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<dyn Error>> {
execute(
repo_path,
format!("pull --ff --no-rebase --no-edit --commit {remote}"),
)?;
execute(repo_path, format!("push {remote}"))?;
Ok(())
}

View File

@ -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<NaiveDateTime>, // 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<Vec<Task>>, // 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() {
impl Task {
pub fn inbox(&mut self) {
self.status = Status::Inbox;
} else {
self.status = Status::Pending;
}
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<NaiveDateTime>) -> 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<Vec<Task>>, // 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(),
}
}
}