From 587547727c31ae13facb5deacdd696f6f5e63cba Mon Sep 17 00:00:00 2001 From: Alan Vardy Date: Sat, 4 Jan 2025 18:28:03 -0800 Subject: [PATCH] Grouping --- CHANGELOG.md | 1 + docs/usage.md | 3 +++ src/filters.rs | 20 +++++++++++++++--- src/list.rs | 56 ++++++++++++++++++++++++++++++++------------------ src/main.rs | 12 +++++------ src/todoist.rs | 28 +++++++++++++++++++------ 6 files changed, 85 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef92b8..70930e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Render attachment URLs in comments - Make `max_comment_length` configurable +- Enable grouping using filters separated by commas ## 2025-01-02 v0.6.26 diff --git a/docs/usage.md b/docs/usage.md index 10bb553..44aafee 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -97,6 +97,9 @@ tod task complete && tod task next # Get all tasks for work tod list view --project work +# Get all tasks in three groupings, overdue, today, and tomorrow +tod list view --filter overdue,today,tom + # Generate shell completions for fish tod shell completions fish > ~/.config/fish/completions/tod.fish diff --git a/src/filters.rs b/src/filters.rs index d837734..74a410c 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -10,7 +10,11 @@ use crate::{ }; pub async fn edit_task(config: &Config, filter: String) -> Result { - let tasks = todoist::tasks_for_filter(config, &filter).await?; + let tasks = todoist::tasks_for_filters(config, &filter) + .await? + .into_iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(); let task = input::select(input::TASK, tasks, config.mock_select)?; @@ -52,7 +56,12 @@ pub async fn next_task(config: Config, filter: &str) -> Result { } async fn fetch_next_task(config: &Config, filter: &str) -> Result, Error> { - let tasks = todoist::tasks_for_filter(config, filter).await?; + let tasks = todoist::tasks_for_filters(config, filter) + .await? + .into_iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(); + let tasks = tasks::sort_by_value(tasks, config); Ok(tasks.first().map(|task| (task.to_owned(), tasks.len()))) @@ -60,7 +69,12 @@ async fn fetch_next_task(config: &Config, filter: &str) -> Result Result { - let tasks = todoist::tasks_for_filter(config, filter).await?; + let tasks = todoist::tasks_for_filters(config, filter) + .await? + .into_iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(); + let tasks = tasks::sort(tasks, config, sort); if tasks.is_empty() { diff --git a/src/list.rs b/src/list.rs index 250d5de..f773b1d 100644 --- a/src/list.rs +++ b/src/list.rs @@ -28,26 +28,26 @@ impl Display for Flag { /// Get a list of all tasks pub async fn view(config: &Config, flag: Flag, sort: &SortOrder) -> Result { - let tasks = match flag.clone() { - Flag::Project(project) => todoist::tasks_for_project(config, &project).await?, - Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?, + let list_of_tasks = match flag.clone() { + Flag::Project(project) => vec![( + project.name.clone(), + todoist::tasks_for_project(config, &project).await?, + )], + Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter).await?, }; - let empty_text = format!("No tasks for {flag}"); - let title = format!("Tasks for {flag}"); - - if tasks.is_empty() { - return Ok(empty_text); - } - let mut buffer = String::new(); - buffer.push_str(&color::green_string(&title)); - buffer.push('\n'); - for task in tasks::sort(tasks, config, sort) { - let text = task.fmt(config, FormatType::List, true, false).await?; + for (query, tasks) in list_of_tasks { + let title = format!("Tasks for {query}"); + buffer.push('\n'); + buffer.push_str(&color::green_string(&title)); buffer.push('\n'); - buffer.push_str(&text); + for task in tasks::sort(tasks, config, sort) { + let text = task.fmt(config, FormatType::List, true, false).await?; + buffer.push('\n'); + buffer.push_str(&text); + } } Ok(buffer) } @@ -60,7 +60,11 @@ pub async fn prioritize(config: &Config, flag: Flag, sort: &SortOrder) -> Result .into_iter() .filter(|task| task.priority == Priority::None) .collect::>(), - Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?, + Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter) + .await? + .iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(), }; let empty_text = format!("No tasks for {flag}"); @@ -90,7 +94,11 @@ pub async fn timebox(config: &Config, flag: Flag, sort: &SortOrder) -> Result>(), - Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?, + Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter) + .await? + .into_iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(), }; let empty_text = format!("No tasks for {flag}"); @@ -122,7 +130,11 @@ pub async fn process(config: &Config, flag: Flag, sort: &SortOrder) -> Result todoist::tasks_for_filter(config, &filter).await?, + Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter) + .await? + .into_iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(), }; let tasks = tasks::reject_parent_tasks(tasks, config).await; @@ -156,7 +168,11 @@ pub async fn label( ) -> Result { let tasks = match flag.clone() { Flag::Project(project) => todoist::tasks_for_project(config, &project).await?, - Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?, + Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter) + .await? + .into_iter() + .flat_map(|(_, tasks)| tasks.to_owned()) + .collect::>(), }; let empty_text = format!("No tasks for {flag}"); @@ -474,7 +490,7 @@ mod tests { .await .unwrap(); - assert!(tasks.contains("Tasks for 'today'")); + assert!(tasks.contains("Tasks for today")); mock.assert(); } diff --git a/src/main.rs b/src/main.rs index 3991052..8212f61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -319,7 +319,7 @@ struct ListView { project: Option, #[arg(short, long)] - /// The filter containing the tasks + /// The filter containing the tasks. Can add multiple filters separated by commas. filter: Option, #[arg(short = 't', long, default_value_t = SortOrder::Datetime)] @@ -334,7 +334,7 @@ struct ListProcess { project: Option, #[arg(short, long)] - /// The filter containing the tasks + /// The filter containing the tasks. Can add multiple filters separated by commas. filter: Option, #[arg(short = 't', long, default_value_t = SortOrder::Value)] @@ -349,7 +349,7 @@ struct ListTimebox { project: Option, #[arg(short, long)] - /// The filter containing the tasks, does not filter out tasks with durations unless specified in filter + /// The filter containing the tasks, does not filter out tasks with durations unless specified in filter. Can add multiple filters separated by commas. filter: Option, #[arg(short = 't', long, default_value_t = SortOrder::Value)] @@ -364,7 +364,7 @@ struct ListPrioritize { project: Option, #[arg(short, long)] - /// The filter containing the tasks + /// The filter containing the tasks. Can add multiple filters separated by commas. filter: Option, #[arg(short = 't', long, default_value_t = SortOrder::Value)] @@ -375,7 +375,7 @@ struct ListPrioritize { #[derive(Parser, Debug, Clone)] struct ListLabel { #[arg(short, long)] - /// The filter containing the tasks + /// The filter containing the tasks. Can add multiple filters separated by commas. filter: Option, #[arg(short, long)] @@ -398,7 +398,7 @@ struct ListSchedule { project: Option, #[arg(short, long)] - /// The filter containing the tasks + /// The filter containing the tasks. Can add multiple filters separated by commas. filter: Option, #[arg(short, long, default_value_t = false)] diff --git a/src/todoist.rs b/src/todoist.rs index a4df278..f0e3ae3 100644 --- a/src/todoist.rs +++ b/src/todoist.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; - +use futures::future; use serde_json::{json, Number, Value}; - +use std::collections::HashMap; +use urlencoding::encode; mod request; use crate::comment::Comment; @@ -95,13 +95,29 @@ pub async fn tasks_for_project(config: &Config, project: &Project) -> Result Result, Error> { - use urlencoding::encode; +pub async fn tasks_for_filters( + config: &Config, + filter: &str, +) -> Result)>, Error> { + let filters: Vec<_> = filter + .split(',') + .map(|f| tasks_for_filter(config, f)) + .collect(); + + let mut acc = Vec::new(); + for result in future::join_all(filters).await { + acc.push(result?); + } + + Ok(acc) +} +pub async fn tasks_for_filter(config: &Config, filter: &str) -> Result<(String, Vec), Error> { let encoded = encode(filter); let url = format!("{TASKS_URL}?filter={encoded}"); let json = request::get_todoist_rest(config, url, true).await?; - tasks::rest_json_to_tasks(json) + let tasks = tasks::rest_json_to_tasks(json)?; + Ok((filter.to_string(), tasks)) } pub async fn sections_for_project(