Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grouping #960

Merged
merged 1 commit into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 17 additions & 3 deletions src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ use crate::{
};

pub async fn edit_task(config: &Config, filter: String) -> Result<String, 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::<Vec<Task>>();

let task = input::select(input::TASK, tasks, config.mock_select)?;

Expand Down Expand Up @@ -52,15 +56,25 @@ pub async fn next_task(config: Config, filter: &str) -> Result<String, Error> {
}

async fn fetch_next_task(config: &Config, filter: &str) -> Result<Option<(Task, usize)>, 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::<Vec<Task>>();

let tasks = tasks::sort_by_value(tasks, config);

Ok(tasks.first().map(|task| (task.to_owned(), tasks.len())))
}

/// Put dates on all tasks without dates
pub async fn schedule(config: &Config, filter: &String, sort: &SortOrder) -> Result<String, 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::<Vec<Task>>();

let tasks = tasks::sort(tasks, config, sort);

if tasks.is_empty() {
Expand Down
56 changes: 36 additions & 20 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
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)
}
Expand All @@ -60,7 +60,11 @@ pub async fn prioritize(config: &Config, flag: Flag, sort: &SortOrder) -> Result
.into_iter()
.filter(|task| task.priority == Priority::None)
.collect::<Vec<Task>>(),
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::<Vec<Task>>(),
};

let empty_text = format!("No tasks for {flag}");
Expand Down Expand Up @@ -90,7 +94,11 @@ pub async fn timebox(config: &Config, flag: Flag, sort: &SortOrder) -> Result<St
.into_iter()
.filter(|task| task.duration.is_none())
.collect::<Vec<Task>>(),
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::<Vec<Task>>(),
};

let empty_text = format!("No tasks for {flag}");
Expand Down Expand Up @@ -122,7 +130,11 @@ pub async fn process(config: &Config, flag: Flag, sort: &SortOrder) -> Result<St
tasks::filter_not_in_future(tasks, config)?
}

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::<Vec<Task>>(),
};
let tasks = tasks::reject_parent_tasks(tasks, config).await;

Expand Down Expand Up @@ -156,7 +168,11 @@ pub async fn label(
) -> Result<String, Error> {
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::<Vec<Task>>(),
};

let empty_text = format!("No tasks for {flag}");
Expand Down Expand Up @@ -474,7 +490,7 @@ mod tests {
.await
.unwrap();

assert!(tasks.contains("Tasks for 'today'"));
assert!(tasks.contains("Tasks for today"));
mock.assert();
}

Expand Down
12 changes: 6 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ struct ListView {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Datetime)]
Expand All @@ -334,7 +334,7 @@ struct ListProcess {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Value)]
Expand All @@ -349,7 +349,7 @@ struct ListTimebox {
project: Option<String>,

#[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<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Value)]
Expand All @@ -364,7 +364,7 @@ struct ListPrioritize {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Value)]
Expand All @@ -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<String>,

#[arg(short, long)]
Expand All @@ -398,7 +398,7 @@ struct ListSchedule {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short, long, default_value_t = false)]
Expand Down
28 changes: 22 additions & 6 deletions src/todoist.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -95,13 +95,29 @@ pub async fn tasks_for_project(config: &Config, project: &Project) -> Result<Vec
tasks::sync_json_to_tasks(json)
}

pub async fn tasks_for_filter(config: &Config, filter: &str) -> Result<Vec<Task>, Error> {
use urlencoding::encode;
pub async fn tasks_for_filters(
config: &Config,
filter: &str,
) -> Result<Vec<(String, Vec<Task>)>, 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<Task>), 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(
Expand Down
Loading