forked from janek/mostr
feat: quick filter for all task states
This commit is contained in:
parent
ff74ac216b
commit
b03ad00b6a
|
@ -112,7 +112,7 @@ Property Filters:
|
||||||
- `#TAG` - set tag filter (empty: list all used tags)
|
- `#TAG` - set tag filter (empty: list all used tags)
|
||||||
- `+TAG` - add tag filter
|
- `+TAG` - add tag filter
|
||||||
- `-TAG` - remove tag filters
|
- `-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.
|
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.
|
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.
|
because they need conceptualization.
|
||||||
Suggestions welcome!
|
Suggestions welcome!
|
||||||
|
|
||||||
- Task Dependencies (change from tags to properties so they can be added later, or maybe as a state?)
|
- Priorities
|
||||||
- Task Templates
|
- Dependencies (change from tags to properties so they can be added later? or maybe as a state?)
|
||||||
- Task Ownership
|
- Templates
|
||||||
|
- Ownership
|
||||||
- Combined formatting and recursion specifiers
|
- Combined formatting and recursion specifiers
|
||||||
+ progress count/percentage and recursive or not
|
+ progress count/percentage and recursive or not
|
||||||
+ Subtask progress immediate/all/leafs
|
+ Subtask progress immediate/all/leafs
|
||||||
|
|
|
@ -21,7 +21,7 @@ use xdg::BaseDirectories;
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
||||||
use crate::task::State;
|
use crate::task::State;
|
||||||
use crate::tasks::Tasks;
|
use crate::tasks::{StateFilter, Tasks};
|
||||||
|
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod task;
|
mod task;
|
||||||
|
@ -343,7 +343,11 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('?') => {
|
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('!') =>
|
Some('!') =>
|
||||||
|
|
76
src/tasks.rs
76
src/tasks.rs
|
@ -1,4 +1,5 @@
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::io::{Error, stdout, Write};
|
use std::io::{Error, stdout, Write};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::ops::{Div, Rem};
|
use std::ops::{Div, Rem};
|
||||||
|
@ -40,13 +41,66 @@ pub(crate) struct Tasks {
|
||||||
/// Currently active tags
|
/// Currently active tags
|
||||||
tags: BTreeSet<Tag>,
|
tags: BTreeSet<Tag>,
|
||||||
/// Current active state
|
/// Current active state
|
||||||
state: Option<String>,
|
state: StateFilter,
|
||||||
/// A filtered view of the current tasks
|
/// A filtered view of the current tasks
|
||||||
view: Vec<EventId>,
|
view: Vec<EventId>,
|
||||||
|
|
||||||
sender: EventSender,
|
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<String> {
|
||||||
|
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 {
|
impl Tasks {
|
||||||
pub(crate) fn from(url: Option<Url>, tx: &Sender<(Url, Events)>, keys: &Keys) -> Self {
|
pub(crate) fn from(url: Option<Url>, tx: &Sender<(Url, Events)>, keys: &Keys) -> Self {
|
||||||
Self::with_sender(EventSender {
|
Self::with_sender(EventSender {
|
||||||
|
@ -72,7 +126,7 @@ impl Tasks {
|
||||||
position: None, // TODO persist position
|
position: None, // TODO persist position
|
||||||
view: Default::default(),
|
view: Default::default(),
|
||||||
tags: Default::default(),
|
tags: Default::default(),
|
||||||
state: None,
|
state: Default::default(),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
sender,
|
sender,
|
||||||
}
|
}
|
||||||
|
@ -208,8 +262,7 @@ impl Tasks {
|
||||||
self.tags
|
self.tags
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| format!(" #{}", t.content().unwrap()))
|
.map(|t| format!(" #{}", t.content().unwrap()))
|
||||||
.chain(self.state.as_ref().map(|s| format!(" ?{s}")).into_iter())
|
.chain(once(self.state.indicator()))
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("")
|
.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,14 +359,7 @@ impl Tasks {
|
||||||
.filter(|t| {
|
.filter(|t| {
|
||||||
// TODO apply filters in transit
|
// TODO apply filters in transit
|
||||||
let state = t.pure_state();
|
let state = t.pure_state();
|
||||||
self.state.as_ref().map_or_else(|| {
|
self.state.matches(t) && (self.tags.is_empty()
|
||||||
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()
|
|
||||||
|| t.tags.as_ref().map_or(false, |tags| {
|
|| t.tags.as_ref().map_or(false, |tags| {
|
||||||
let mut iter = tags.iter();
|
let mut iter = tags.iter();
|
||||||
self.tags.iter().all(|tag| iter.any(|t| t == tag))
|
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<String>) {
|
pub(crate) fn set_state_filter(&mut self, state: StateFilter) {
|
||||||
self.view.clear();
|
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;
|
self.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,7 +612,7 @@ impl Tasks {
|
||||||
self.parse_task(input.trim())
|
self.parse_task(input.trim())
|
||||||
.add_tags(tag.into_iter())
|
.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
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue