From b03ad00b6a2939583d8f0f1042f39f439f461d4c Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Sat, 10 Aug 2024 15:44:52 +0300 Subject: [PATCH] feat: quick filter for all task states --- README.md | 9 ++++--- src/main.rs | 8 ++++-- src/tasks.rs | 76 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2d75b98..d367d32 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Property Filters: - `#TAG` - set tag filter (empty: list all used tags) - `+TAG` - add tag filter - `-TAG` - remove tag filters -- `?STATE` - filter by state (type or description) - plain `?` to reset +- `?STATE` - filter by state (type or description) - plain `?` to reset, `??` to show all State descriptions can be used for example for Kanban columns or review flows. An active tag or state filter will also set that attribute for newly created tasks. @@ -171,9 +171,10 @@ The following features are not ready to be implemented because they need conceptualization. Suggestions welcome! -- Task Dependencies (change from tags to properties so they can be added later, or maybe as a state?) -- Task Templates -- Task Ownership +- Priorities +- Dependencies (change from tags to properties so they can be added later? or maybe as a state?) +- Templates +- Ownership - Combined formatting and recursion specifiers + progress count/percentage and recursive or not + Subtask progress immediate/all/leafs diff --git a/src/main.rs b/src/main.rs index 3ca4aa0..8686c1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use xdg::BaseDirectories; use crate::helpers::*; use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND}; use crate::task::State; -use crate::tasks::Tasks; +use crate::tasks::{StateFilter, Tasks}; mod helpers; mod task; @@ -343,7 +343,11 @@ async fn main() { } Some('?') => { - tasks.set_state_filter(arg.map(|s| s.to_string())); + match arg { + None => tasks.set_state_filter(StateFilter::Default), + Some("?") => tasks.set_state_filter(StateFilter::All), + Some(arg) => tasks.set_state_filter(StateFilter::State(arg.to_string())), + } } Some('!') => diff --git a/src/tasks.rs b/src/tasks.rs index 1a0dfa6..2fc763c 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeSet, HashMap}; +use std::fmt::{Display, Formatter}; use std::io::{Error, stdout, Write}; use std::iter::once; use std::ops::{Div, Rem}; @@ -40,13 +41,66 @@ pub(crate) struct Tasks { /// Currently active tags tags: BTreeSet, /// Current active state - state: Option, + state: StateFilter, /// A filtered view of the current tasks view: Vec, sender: EventSender, } +#[derive(Clone, Debug)] +pub(crate) enum StateFilter { + Default, + All, + State(String), +} +impl StateFilter { + fn indicator(&self) -> String { + match self { + StateFilter::Default => "".to_string(), + StateFilter::All => " ?ALL".to_string(), + StateFilter::State(str) => format!(" ?{str}"), + } + } + + fn matches(&self, task: &Task) -> bool { + match self { + StateFilter::Default => { + let state = task.pure_state(); + state.is_open() || (state == State::Done && task.parent_id() != None) + }, + StateFilter::All => true, + StateFilter::State(filter) => task.state().is_some_and(|t| t.matches_label(filter)), + } + } + + fn as_option(&self) -> Option { + if let StateFilter::State(str) = self { + Some(str.to_string()) + } else { + None + } + } +} +impl Default for StateFilter { + fn default() -> Self { + StateFilter::Default + } +} +impl Display for StateFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + StateFilter::Default => "relevant tasks".to_string(), + StateFilter::All => "all tasks".to_string(), + StateFilter::State(s) => format!("state {s}"), + } + ) + } +} + impl Tasks { pub(crate) fn from(url: Option, tx: &Sender<(Url, Events)>, keys: &Keys) -> Self { Self::with_sender(EventSender { @@ -72,7 +126,7 @@ impl Tasks { position: None, // TODO persist position view: Default::default(), tags: Default::default(), - state: None, + state: Default::default(), depth: 1, sender, } @@ -208,8 +262,7 @@ impl Tasks { self.tags .iter() .map(|t| format!(" #{}", t.content().unwrap())) - .chain(self.state.as_ref().map(|s| format!(" ?{s}")).into_iter()) - .collect::>() + .chain(once(self.state.indicator())) .join("") } @@ -306,14 +359,7 @@ impl Tasks { .filter(|t| { // TODO apply filters in transit let state = t.pure_state(); - self.state.as_ref().map_or_else(|| { - state.is_open() || ( - state == State::Done && - t.parent_id() != None - ) - }, |filter| { - t.state().is_some_and(|t| t.matches_label(filter)) - }) && (self.tags.is_empty() + self.state.matches(t) && (self.tags.is_empty() || t.tags.as_ref().map_or(false, |tags| { let mut iter = tags.iter(); self.tags.iter().all(|tag| iter.any(|t| t == tag)) @@ -436,9 +482,9 @@ impl Tasks { } } - pub(crate) fn set_state_filter(&mut self, state: Option) { + pub(crate) fn set_state_filter(&mut self, state: StateFilter) { self.view.clear(); - info!("Filtering for {}", state.as_ref().map_or("open tasks".to_string(), |s| format!("state {s}"))); + info!("Filtering for {}", state); self.state = state; } @@ -566,7 +612,7 @@ impl Tasks { self.parse_task(input.trim()) .add_tags(tag.into_iter()) ); - self.state.clone().inspect(|s| self.set_state_for_with(id, s)); + self.state.as_option().inspect(|s| self.set_state_for_with(id, s)); id }