verify duration invariants on serialize
This commit is contained in:
		@@ -86,3 +86,4 @@ Toru stores tasks and other metadata locally in the folder of the vault in the i
 | 
			
		||||
 | 
			
		||||
- Validate invariants at the point of saving, to create consistency across creating and editing notes.
 | 
			
		||||
- Convenient options with the edit command so that editing the raw file isn't the only option
 | 
			
		||||
- Check for if dependencies are complete when listing notes with no dependencies.
 | 
			
		||||
 
 | 
			
		||||
@@ -81,10 +81,7 @@ pub enum Command {
 | 
			
		||||
    /// 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,
 | 
			
		||||
        duration : tasks::Duration,
 | 
			
		||||
        /// Date for the time entry [default: Today]
 | 
			
		||||
        #[clap(short, long)]
 | 
			
		||||
        date : Option<chrono::NaiveDate>,
 | 
			
		||||
 
 | 
			
		||||
@@ -75,13 +75,6 @@ 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")))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										301
									
								
								src/list.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								src/list.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,301 @@
 | 
			
		||||
use crate::args;
 | 
			
		||||
use crate::error;
 | 
			
		||||
use crate::state;
 | 
			
		||||
use crate::tasks;
 | 
			
		||||
use crate::format;
 | 
			
		||||
 | 
			
		||||
use std::cmp;
 | 
			
		||||
use std::path;
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
use chrono::SubsecRound;
 | 
			
		||||
 | 
			
		||||
impl args::ListOptions {
 | 
			
		||||
    /// Combines list options coming from a profile and from the additional arguments given. Order
 | 
			
		||||
    /// of the arguments provided matters, hence the argument names (because optional arguments
 | 
			
		||||
    /// from the profile are overwritten by the additional arguments).
 | 
			
		||||
    pub fn combine(profile : &Self, additional : &Self) -> Self {
 | 
			
		||||
        /// Joins two vectors together one after the other, creating a new allocation.
 | 
			
		||||
        fn concat<T : Clone>(a : &Vec<T>, b : &Vec<T>) -> Vec<T> {
 | 
			
		||||
            let mut a = a.clone();
 | 
			
		||||
            a.extend(b.iter().cloned());
 | 
			
		||||
            a
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// Takes two options, and prioritises the second if it is provided in the output, using
 | 
			
		||||
        /// the first as a fallback, and returning None if both are None.
 | 
			
		||||
        fn join_options<T : Clone>(a : &Option<T>, b : &Option<T>) -> Option<T> {
 | 
			
		||||
            match (a, b) {
 | 
			
		||||
                (Some(_), Some(b)) => Some(b.clone()),
 | 
			
		||||
                (Some(a), None) => Some(a.clone()),
 | 
			
		||||
                (None, Some(b)) => Some(b.clone()),
 | 
			
		||||
                (None, None) => None,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            column : concat(&profile.column, &additional.column),
 | 
			
		||||
            order_by : join_options(&profile.order_by, &additional.order_by),
 | 
			
		||||
            order : join_options(&profile.order, &profile.order),
 | 
			
		||||
            tag : concat(&profile.tag, &additional.tag),
 | 
			
		||||
            exclude_tag : concat(&profile.exclude_tag, &additional.exclude_tag),
 | 
			
		||||
            priority : concat(&profile.priority, &additional.priority),
 | 
			
		||||
            due_before : join_options(&profile.due_before, &additional.due_before),
 | 
			
		||||
            due_after : join_options(&profile.due_after, &additional.due_after),
 | 
			
		||||
            created_before : join_options(&profile.created_before, &additional.created_before),
 | 
			
		||||
            created_after : join_options(&profile.created_after, &additional.created_after),
 | 
			
		||||
            include_completed : profile.include_completed || additional.include_completed,
 | 
			
		||||
            no_dependencies : profile.no_dependencies || additional.no_dependencies,
 | 
			
		||||
            no_dependents : profile.no_dependents || additional.no_dependents,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Lists all tasks in the specified vault.
 | 
			
		||||
pub fn list(mut options : args::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)
 | 
			
		||||
        .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
 | 
			
		||||
        .set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    let mut tasks : Box<dyn Iterator<Item = tasks::Task>> = Box::new(tasks::Task::load_all(vault_folder, true)?.into_iter());
 | 
			
		||||
 | 
			
		||||
    // Filter the tasks.
 | 
			
		||||
    if let Some(date) = options.created_before {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| t.data.created.date() <= date));
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(date) = options.created_after {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| t.data.created.date() >= date));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(date) = options.due_before {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            match tasks::compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) {
 | 
			
		||||
                cmp::Ordering::Less | cmp::Ordering::Equal => true,
 | 
			
		||||
                cmp::Ordering::Greater => false,
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(date) = options.due_after {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            match tasks::compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) {
 | 
			
		||||
                cmp::Ordering::Greater | cmp::Ordering::Equal => true,
 | 
			
		||||
                cmp::Ordering::Less => false,
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.include_completed {
 | 
			
		||||
        tasks = Box::new(tasks.filter(|t| t.data.completed.is_none()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.tag.is_empty() {
 | 
			
		||||
        let specified_tags : HashSet<_> = options.tag.iter().collect();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            let task_tags : HashSet<_> = t.data.tags.iter().collect();
 | 
			
		||||
 | 
			
		||||
            // Non empty intersection of tags means the task should be displayed
 | 
			
		||||
            specified_tags.intersection(&task_tags).next().is_some()
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.exclude_tag.is_empty() {
 | 
			
		||||
        let specified_tags : HashSet<_> = options.exclude_tag.iter().collect();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            let task_tags : HashSet<_> = t.data.tags.iter().collect();
 | 
			
		||||
 | 
			
		||||
            // If the task contains a tag which was supposed to be excluded, it should be filtered
 | 
			
		||||
            // out
 | 
			
		||||
            !specified_tags.intersection(&task_tags).next().is_some()
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.priority.is_empty() {
 | 
			
		||||
        let specified_priority_levels : HashSet<_> = options.priority.iter().collect();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            specified_priority_levels.contains(&t.data.priority)
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if options.no_dependencies {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            t.data.dependencies.is_empty()
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if options.no_dependents {
 | 
			
		||||
        let tasks_with_dependents = state.data.deps.get_tasks_with_dependents();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            !tasks_with_dependents.contains(&t.data.id)
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut tasks : Vec<tasks::Task> = tasks.collect();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Sort the tasks.
 | 
			
		||||
    use super::{OrderBy, Order};
 | 
			
		||||
    match options.order_by.unwrap_or_default() {
 | 
			
		||||
        OrderBy::Id => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.id.cmp(&t2.data.id));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.id.cmp(&t1.data.id));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Name => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.name.cmp(&t2.data.name));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.name.cmp(&t1.data.name));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Due => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| tasks::compare_due_dates(&t1.data.due, &t2.data.due));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| tasks::compare_due_dates(&t2.data.due, &t1.data.due));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Priority => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.priority.cmp(&t2.data.priority));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.priority.cmp(&t1.data.priority));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Created => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.created.cmp(&t2.data.created));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.created.cmp(&t1.data.created));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Tracked => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| tasks::TimeEntry::total(&t1.data.time_entries).cmp(&tasks::TimeEntry::total(&t2.data.time_entries)));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| tasks::TimeEntry::total(&t2.data.time_entries).cmp(&tasks::TimeEntry::total(&t1.data.time_entries)));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Include the required columns
 | 
			
		||||
    let mut headers = vec!["Id", "Name"];
 | 
			
		||||
 | 
			
		||||
    // Remove duplicate columns.
 | 
			
		||||
    options.column = {
 | 
			
		||||
        let mut columns = HashSet::new();
 | 
			
		||||
 | 
			
		||||
        options.column.clone()
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .filter(|c| {
 | 
			
		||||
                if columns.contains(c) {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    columns.insert(c.clone());
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .collect()
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    use super::Column;
 | 
			
		||||
    for column in &options.column {
 | 
			
		||||
        match column {
 | 
			
		||||
            Column::Tracked => {
 | 
			
		||||
                headers.push("Tracked");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Due => {
 | 
			
		||||
                headers.push("Due");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Tags => {
 | 
			
		||||
                headers.push("Tags");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Priority => {
 | 
			
		||||
                headers.push("Priority");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Status => {
 | 
			
		||||
                headers.push("Status");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Created => {
 | 
			
		||||
                headers.push("Created");
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table.set_header(headers);
 | 
			
		||||
 | 
			
		||||
    for task in tasks {
 | 
			
		||||
 | 
			
		||||
        use comfy_table::Cell;
 | 
			
		||||
        let mut row = vec![Cell::from(task.data.id), Cell::from(task.data.name)];
 | 
			
		||||
 | 
			
		||||
        for column in &options.column {
 | 
			
		||||
            match column {
 | 
			
		||||
                Column::Tracked => {
 | 
			
		||||
                    let duration = tasks::TimeEntry::total(&task.data.time_entries);
 | 
			
		||||
                    row.push(
 | 
			
		||||
                        Cell::from(if duration == tasks::Duration::zero() { String::new() } else { duration.to_string() })
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                Column::Due => {
 | 
			
		||||
                    row.push(match task.data.due {
 | 
			
		||||
                        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)?));
 | 
			
		||||
                },
 | 
			
		||||
                Column::Priority => {
 | 
			
		||||
                    row.push(format::cell::priority(&task.data.priority));
 | 
			
		||||
                },
 | 
			
		||||
                Column::Status => {
 | 
			
		||||
                    row.push(
 | 
			
		||||
                        Cell::new(if task.data.completed.is_some() {
 | 
			
		||||
                            String::from("complete")
 | 
			
		||||
                        }
 | 
			
		||||
                        else {
 | 
			
		||||
                            String::from("incomplete")
 | 
			
		||||
                        })
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                Column::Created => {
 | 
			
		||||
                    row.push(Cell::new(task.data.created.round_subsecs(0).to_string()));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        table.add_row(row);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    println!("{}", table);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
mod vcs;
 | 
			
		||||
mod edit;
 | 
			
		||||
mod args;
 | 
			
		||||
mod list;
 | 
			
		||||
mod vault;
 | 
			
		||||
mod index;
 | 
			
		||||
mod error;
 | 
			
		||||
@@ -146,10 +147,10 @@ fn program() -> Result<(), error::Error> {
 | 
			
		||||
                }
 | 
			
		||||
                println!("Updated task {}", format::id(id));
 | 
			
		||||
            },
 | 
			
		||||
            Command::Track { id_or_name, hours, minutes, date, message } => {
 | 
			
		||||
            Command::Track { id_or_name, duration, 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);
 | 
			
		||||
                let entry =  tasks::TimeEntry::new(duration, date, message);
 | 
			
		||||
                task.data.time_entries.push(entry);
 | 
			
		||||
                task.save()?;
 | 
			
		||||
            },
 | 
			
		||||
@@ -181,7 +182,7 @@ fn program() -> Result<(), error::Error> {
 | 
			
		||||
                        additional
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                tasks::list(options, vault_folder, &state)?;
 | 
			
		||||
                list::list(options, vault_folder, &state)?;
 | 
			
		||||
            },
 | 
			
		||||
            // All commands which are dealt with in if let chain at start.
 | 
			
		||||
            Command::Vault(_) | Command::Config(_) | Command::Git { args : _ } | Command::Svn { args : _ } | Command::Switch { name : _ } | Command::GitIgnore | Command::SvnIgnore => unreachable!(),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										580
									
								
								src/tasks.rs
									
									
									
									
									
								
							
							
						
						
									
										580
									
								
								src/tasks.rs
									
									
									
									
									
								
							@@ -1,12 +1,10 @@
 | 
			
		||||
use crate::args;
 | 
			
		||||
use crate::error;
 | 
			
		||||
use crate::state;
 | 
			
		||||
use crate::format;
 | 
			
		||||
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::ops;
 | 
			
		||||
use std::str;
 | 
			
		||||
use std::mem;
 | 
			
		||||
use std::cmp;
 | 
			
		||||
use std::path;
 | 
			
		||||
@@ -22,28 +20,6 @@ pub struct Task {
 | 
			
		||||
    pub data : InternalTask,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub enum Priority {
 | 
			
		||||
    Backlog,
 | 
			
		||||
    #[default]
 | 
			
		||||
    Low,
 | 
			
		||||
    Medium,
 | 
			
		||||
    High,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct TimeEntry {
 | 
			
		||||
    pub logged_date : chrono::NaiveDate,
 | 
			
		||||
    pub message : Option<String>,
 | 
			
		||||
    pub duration : Duration,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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,
 | 
			
		||||
@@ -58,6 +34,193 @@ pub struct InternalTask {
 | 
			
		||||
    pub time_entries : Vec<TimeEntry>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub enum Priority {
 | 
			
		||||
    Backlog,
 | 
			
		||||
    #[default]
 | 
			
		||||
    Low,
 | 
			
		||||
    Medium,
 | 
			
		||||
    High,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
 | 
			
		||||
pub struct Duration {
 | 
			
		||||
    hours : u16,
 | 
			
		||||
    minutes : u16,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Duration {
 | 
			
		||||
    pub fn zero() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            hours : 0,
 | 
			
		||||
            minutes : 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub mod duration {
 | 
			
		||||
    use super::Duration;
 | 
			
		||||
 | 
			
		||||
    use std::ops;
 | 
			
		||||
    use std::str;
 | 
			
		||||
    use std::fmt;
 | 
			
		||||
 | 
			
		||||
    /// Serialize to custom format HH:MM where MM is padded to be two characters wide and HH can be
 | 
			
		||||
    /// arbitrarily large.
 | 
			
		||||
    impl serde::Serialize for Duration {
 | 
			
		||||
        fn serialize<S : serde::Serializer>(&self, serializer : S) -> Result<S::Ok, S::Error> {
 | 
			
		||||
            serializer.serialize_str(&format!("{}:{:0>2}", self.hours, self.minutes))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Deserialize from custom format HH:MM where MM is an integer between 0 and 59 inclusive, and
 | 
			
		||||
    /// HH is some integer representable as a u16.
 | 
			
		||||
    /// The width of MM is not enforced for deserialization.
 | 
			
		||||
    impl<'de> serde::Deserialize<'de> for Duration {
 | 
			
		||||
        fn deserialize<D : serde::Deserializer<'de>>(deserializer : D) -> Result<Self, D::Error> {
 | 
			
		||||
            let raw = String::deserialize(deserializer)?;
 | 
			
		||||
 | 
			
		||||
            use std::str::FromStr;
 | 
			
		||||
            Self::from_str(&raw)
 | 
			
		||||
            .map_err(|x| serde::de::Error::invalid_value(serde::de::Unexpected::Str(&raw), &x.serde_expected()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Custom type for errors when converting duration from str, with error messages for clap and
 | 
			
		||||
    /// serde respectively.
 | 
			
		||||
    #[derive(Debug)]
 | 
			
		||||
    pub enum DurationRead {
 | 
			
		||||
        /// For when the number of minutes is not less than 60.
 | 
			
		||||
        Minutes,
 | 
			
		||||
        /// For when either value cannot be parsed into a u16.
 | 
			
		||||
        Range,
 | 
			
		||||
        /// For general formatting error (i.e. split at colon doesn't produce two values).
 | 
			
		||||
        General,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl fmt::Display for DurationRead {
 | 
			
		||||
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
            match self {
 | 
			
		||||
                DurationRead::Minutes => {
 | 
			
		||||
                    write!(f, "the number of minutes must be between 0 and 59 inclusive")
 | 
			
		||||
                },
 | 
			
		||||
                DurationRead::Range => {
 | 
			
		||||
                    write!(f, "the number of hours and minutes must be representable as a u16")
 | 
			
		||||
                },
 | 
			
		||||
                DurationRead::General => {
 | 
			
		||||
                    write!(f, "duration must be in the format HH:MM where HH is any integer (representable as a u16) and MM is an integer between 0 and 59 inclusive")
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl std::error::Error for DurationRead { }
 | 
			
		||||
 | 
			
		||||
    impl DurationRead {
 | 
			
		||||
        /// Gives a str of what was expected (and not provided) when serializing.
 | 
			
		||||
        pub fn serde_expected(&self) -> &'static str {
 | 
			
		||||
            match self {
 | 
			
		||||
                DurationRead::Minutes => {
 | 
			
		||||
                    "the number of minutes to be an integer between 0 and 59 inclusive"
 | 
			
		||||
                },
 | 
			
		||||
                DurationRead::Range => {
 | 
			
		||||
                    "the number of hours and minutes to be representable as a u16"
 | 
			
		||||
                },
 | 
			
		||||
                DurationRead::General => {
 | 
			
		||||
                    "a duration in the format HH:MM where HH is any integer (representable as a u16) and MM is an integer between 0 and 59 inclusive"
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl str::FromStr for Duration {
 | 
			
		||||
        type Err = DurationRead;
 | 
			
		||||
 | 
			
		||||
        fn from_str(s : &str) -> Result<Self, Self::Err> {
 | 
			
		||||
            if let &[h, m] = &s.split(':').collect::<Vec<&str>>()[..] {
 | 
			
		||||
                if let (Ok(hours), Ok(minutes)) = (h.parse::<u16>(), m.parse::<u16>()) {
 | 
			
		||||
                    if minutes < 60 {
 | 
			
		||||
                        Ok(Self {
 | 
			
		||||
                            hours,
 | 
			
		||||
                            minutes,
 | 
			
		||||
                        })
 | 
			
		||||
                    }
 | 
			
		||||
                    else {
 | 
			
		||||
                        Err(DurationRead::Minutes)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    Err(DurationRead::Range)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                Err(DurationRead::General)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl ops::Add for Duration {
 | 
			
		||||
        type Output = Self;
 | 
			
		||||
 | 
			
		||||
        fn add(self, other : Self) -> Self::Output {
 | 
			
		||||
 | 
			
		||||
            Self {
 | 
			
		||||
                hours : self.hours + other.hours + (self.minutes + other.minutes) / 60,
 | 
			
		||||
                minutes : (self.minutes + other.minutes) % 60,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl ops::Div<usize> for Duration {
 | 
			
		||||
        type Output = Self;
 | 
			
		||||
 | 
			
		||||
        fn div(self, divisor : usize) -> Self::Output {
 | 
			
		||||
            let total_mins = f64::from(self.hours * 60 + self.minutes);
 | 
			
		||||
            let divided_mins = total_mins / (divisor as f64);
 | 
			
		||||
            let divided_mins = divided_mins.round() as u16;
 | 
			
		||||
 | 
			
		||||
            Self {
 | 
			
		||||
                hours : divided_mins / 60,
 | 
			
		||||
                minutes : divided_mins % 60,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Same display format as serialization.
 | 
			
		||||
    impl fmt::Display for Duration {
 | 
			
		||||
        fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
            write!(f, "{}:{:0>2}", self.hours, self.minutes)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct TimeEntry {
 | 
			
		||||
    pub logged_date : chrono::NaiveDate,
 | 
			
		||||
    pub message : Option<String>,
 | 
			
		||||
    pub duration : Duration,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TimeEntry {
 | 
			
		||||
    /// Adds up the times from a collection of time entries.
 | 
			
		||||
    pub 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(duration : Duration, date : Option<chrono::NaiveDate>, message : Option<String>) -> Self {
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            logged_date : date.unwrap_or(chrono::Utc::now().naive_local().date()),
 | 
			
		||||
            message,
 | 
			
		||||
            duration,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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> {
 | 
			
		||||
@@ -306,48 +469,9 @@ impl Task {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Duration {
 | 
			
		||||
    pub fn zero() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            minutes : 0,
 | 
			
		||||
            hours : 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 {
 | 
			
		||||
pub fn compare_due_dates<T : Ord>(first : &Option<T>, second : &Option<T>) -> cmp::Ordering {
 | 
			
		||||
    match (first, second) {
 | 
			
		||||
        (None, None) => cmp::Ordering::Equal,
 | 
			
		||||
        (Some(_), None) => cmp::Ordering::Less,
 | 
			
		||||
@@ -356,329 +480,3 @@ fn compare_due_dates<T : Ord>(first : &Option<T>, second : &Option<T>) -> cmp::O
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
impl args::ListOptions {
 | 
			
		||||
    /// Combines list options coming from a profile and from the additional arguments given. Order
 | 
			
		||||
    /// of the arguments provided matters, hence the argument names (because optional arguments
 | 
			
		||||
    /// from the profile are overwritten by the additional arguments).
 | 
			
		||||
    pub fn combine(profile : &Self, additional : &Self) -> Self {
 | 
			
		||||
        /// Joins two vectors together one after the other, creating a new allocation.
 | 
			
		||||
        fn concat<T : Clone>(a : &Vec<T>, b : &Vec<T>) -> Vec<T> {
 | 
			
		||||
            let mut a = a.clone();
 | 
			
		||||
            a.extend(b.iter().cloned());
 | 
			
		||||
            a
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// Takes two options, and prioritises the second if it is provided in the output, using
 | 
			
		||||
        /// the first as a fallback, and returning None if both are None.
 | 
			
		||||
        fn join_options<T : Clone>(a : &Option<T>, b : &Option<T>) -> Option<T> {
 | 
			
		||||
            match (a, b) {
 | 
			
		||||
                (Some(_), Some(b)) => Some(b.clone()),
 | 
			
		||||
                (Some(a), None) => Some(a.clone()),
 | 
			
		||||
                (None, Some(b)) => Some(b.clone()),
 | 
			
		||||
                (None, None) => None,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            column : concat(&profile.column, &additional.column),
 | 
			
		||||
            order_by : join_options(&profile.order_by, &additional.order_by),
 | 
			
		||||
            order : join_options(&profile.order, &profile.order),
 | 
			
		||||
            tag : concat(&profile.tag, &additional.tag),
 | 
			
		||||
            exclude_tag : concat(&profile.exclude_tag, &additional.exclude_tag),
 | 
			
		||||
            priority : concat(&profile.priority, &additional.priority),
 | 
			
		||||
            due_before : join_options(&profile.due_before, &additional.due_before),
 | 
			
		||||
            due_after : join_options(&profile.due_after, &additional.due_after),
 | 
			
		||||
            created_before : join_options(&profile.created_before, &additional.created_before),
 | 
			
		||||
            created_after : join_options(&profile.created_after, &additional.created_after),
 | 
			
		||||
            include_completed : profile.include_completed || additional.include_completed,
 | 
			
		||||
            no_dependencies : profile.no_dependencies || additional.no_dependencies,
 | 
			
		||||
            no_dependents : profile.no_dependents || additional.no_dependents,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Lists all tasks in the specified vault.
 | 
			
		||||
pub fn list(mut options : args::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)
 | 
			
		||||
        .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
 | 
			
		||||
        .set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    let mut tasks : Box<dyn Iterator<Item = Task>> = Box::new(Task::load_all(vault_folder, true)?.into_iter());
 | 
			
		||||
 | 
			
		||||
    // Filter the tasks.
 | 
			
		||||
    if let Some(date) = options.created_before {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| t.data.created.date() <= date));
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(date) = options.created_after {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| t.data.created.date() >= date));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(date) = options.due_before {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            match compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) {
 | 
			
		||||
                cmp::Ordering::Less | cmp::Ordering::Equal => true,
 | 
			
		||||
                cmp::Ordering::Greater => false,
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(date) = options.due_after {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            match compare_due_dates(&t.data.due.map(|d| d.date()), &Some(date)) {
 | 
			
		||||
                cmp::Ordering::Greater | cmp::Ordering::Equal => true,
 | 
			
		||||
                cmp::Ordering::Less => false,
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.include_completed {
 | 
			
		||||
        tasks = Box::new(tasks.filter(|t| t.data.completed.is_none()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.tag.is_empty() {
 | 
			
		||||
        let specified_tags : HashSet<_> = options.tag.iter().collect();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            let task_tags : HashSet<_> = t.data.tags.iter().collect();
 | 
			
		||||
 | 
			
		||||
            // Non empty intersection of tags means the task should be displayed
 | 
			
		||||
            specified_tags.intersection(&task_tags).next().is_some()
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.exclude_tag.is_empty() {
 | 
			
		||||
        let specified_tags : HashSet<_> = options.exclude_tag.iter().collect();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            let task_tags : HashSet<_> = t.data.tags.iter().collect();
 | 
			
		||||
 | 
			
		||||
            // If the task contains a tag which was supposed to be excluded, it should be filtered
 | 
			
		||||
            // out
 | 
			
		||||
            !specified_tags.intersection(&task_tags).next().is_some()
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !options.priority.is_empty() {
 | 
			
		||||
        let specified_priority_levels : HashSet<_> = options.priority.iter().collect();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            specified_priority_levels.contains(&t.data.priority)
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if options.no_dependencies {
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            t.data.dependencies.is_empty()
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if options.no_dependents {
 | 
			
		||||
        let tasks_with_dependents = state.data.deps.get_tasks_with_dependents();
 | 
			
		||||
 | 
			
		||||
        tasks = Box::new(tasks.filter(move |t| {
 | 
			
		||||
            !tasks_with_dependents.contains(&t.data.id)
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut tasks : Vec<Task> = tasks.collect();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Sort the tasks.
 | 
			
		||||
    use super::{OrderBy, Order};
 | 
			
		||||
    match options.order_by.unwrap_or_default() {
 | 
			
		||||
        OrderBy::Id => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.id.cmp(&t2.data.id));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.id.cmp(&t1.data.id));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Name => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.name.cmp(&t2.data.name));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.name.cmp(&t1.data.name));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Due => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| compare_due_dates(&t1.data.due, &t2.data.due));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| compare_due_dates(&t2.data.due, &t1.data.due));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Priority => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.priority.cmp(&t2.data.priority));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.priority.cmp(&t1.data.priority));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Created => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t1.data.created.cmp(&t2.data.created));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| t2.data.created.cmp(&t1.data.created));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        OrderBy::Tracked => {
 | 
			
		||||
            match options.order.unwrap_or_default() {
 | 
			
		||||
                Order::Asc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| TimeEntry::total(&t1.data.time_entries).cmp(&TimeEntry::total(&t2.data.time_entries)));
 | 
			
		||||
                },
 | 
			
		||||
                Order::Desc => {
 | 
			
		||||
                    tasks.sort_by(|t1, t2| TimeEntry::total(&t2.data.time_entries).cmp(&TimeEntry::total(&t1.data.time_entries)));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Include the required columns
 | 
			
		||||
    let mut headers = vec!["Id", "Name"];
 | 
			
		||||
 | 
			
		||||
    // Remove duplicate columns.
 | 
			
		||||
    options.column = {
 | 
			
		||||
        let mut columns = HashSet::new();
 | 
			
		||||
 | 
			
		||||
        options.column.clone()
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .filter(|c| {
 | 
			
		||||
                if columns.contains(c) {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    columns.insert(c.clone());
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .collect()
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    use super::Column;
 | 
			
		||||
    for column in &options.column {
 | 
			
		||||
        match column {
 | 
			
		||||
            Column::Tracked => {
 | 
			
		||||
                headers.push("Tracked");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Due => {
 | 
			
		||||
                headers.push("Due");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Tags => {
 | 
			
		||||
                headers.push("Tags");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Priority => {
 | 
			
		||||
                headers.push("Priority");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Status => {
 | 
			
		||||
                headers.push("Status");
 | 
			
		||||
            },
 | 
			
		||||
            Column::Created => {
 | 
			
		||||
                headers.push("Created");
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table.set_header(headers);
 | 
			
		||||
 | 
			
		||||
    for task in tasks {
 | 
			
		||||
 | 
			
		||||
        use comfy_table::Cell;
 | 
			
		||||
        let mut row = vec![Cell::from(task.data.id), Cell::from(task.data.name)];
 | 
			
		||||
 | 
			
		||||
        for column in &options.column {
 | 
			
		||||
            match column {
 | 
			
		||||
                Column::Tracked => {
 | 
			
		||||
                    let duration = TimeEntry::total(&task.data.time_entries);
 | 
			
		||||
                    row.push(
 | 
			
		||||
                        Cell::from(if duration == Duration::zero() { String::new() } else { duration.to_string() })
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                Column::Due => {
 | 
			
		||||
                    row.push(match task.data.due {
 | 
			
		||||
                        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)?));
 | 
			
		||||
                },
 | 
			
		||||
                Column::Priority => {
 | 
			
		||||
                    row.push(format::cell::priority(&task.data.priority));
 | 
			
		||||
                },
 | 
			
		||||
                Column::Status => {
 | 
			
		||||
                    row.push(
 | 
			
		||||
                        Cell::new(if task.data.completed.is_some() {
 | 
			
		||||
                            String::from("complete")
 | 
			
		||||
                        }
 | 
			
		||||
                        else {
 | 
			
		||||
                            String::from("incomplete")
 | 
			
		||||
                        })
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                Column::Created => {
 | 
			
		||||
                    row.push(Cell::new(task.data.created.round_subsecs(0).to_string()));
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        table.add_row(row);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    println!("{}", table);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ops::Add for Duration {
 | 
			
		||||
    type Output = Self;
 | 
			
		||||
 | 
			
		||||
    fn add(self, other : Self) -> Self::Output {
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            hours : self.hours + other.hours + (self.minutes + other.minutes) / 60,
 | 
			
		||||
            minutes : (self.minutes + other.minutes) % 60,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ops::Div<usize> for Duration {
 | 
			
		||||
    type Output = Self;
 | 
			
		||||
 | 
			
		||||
    fn div(self, divisor : usize) -> Self::Output {
 | 
			
		||||
        let total_mins = f64::from(self.hours * 60 + self.minutes);
 | 
			
		||||
        let divided_mins = total_mins / (divisor as f64);
 | 
			
		||||
        let divided_mins = divided_mins.round() as u16;
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            hours : divided_mins / 60,
 | 
			
		||||
            minutes : divided_mins % 60,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for Duration {
 | 
			
		||||
    fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        write!(f, "{}:{:0>2}", self.hours, self.minutes)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user