Compare commits

...

3 Commits

Author SHA1 Message Date
xeruf b03ad00b6a feat: quick filter for all task states 2024-08-10 15:44:52 +03:00
xeruf ff74ac216b feat(task): colorize state property 2024-08-10 15:14:09 +03:00
xeruf c48355e5da feat(task): parse dependees 2024-08-09 20:53:30 +03:00
4 changed files with 106 additions and 31 deletions

View File

@ -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 - 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

View File

@ -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('!') =>

View File

@ -2,7 +2,9 @@ use fmt::Display;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::{BTreeSet, HashSet}; use std::collections::{BTreeSet, HashSet};
use std::fmt; use std::fmt;
use std::string::ToString;
use colored::Colorize;
use itertools::Either::{Left, Right}; use itertools::Either::{Left, Right};
use itertools::Itertools; use itertools::Itertools;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
@ -11,14 +13,17 @@ use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, TagStandard, Timestamp}
use crate::helpers::some_non_empty; use crate::helpers::some_non_empty;
use crate::kinds::{is_hashtag, PROCEDURE_KIND}; use crate::kinds::{is_hashtag, PROCEDURE_KIND};
pub static MARKER_PARENT: &str = "parent";
pub static MARKER_DEPENDS: &str = "depends";
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Task { pub(crate) struct Task {
/// Event that defines this task /// Event that defines this task
pub(crate) event: Event, pub(crate) event: Event,
/// Cached sorted tags of the event with references remove - do not modify! /// Cached sorted tags of the event with references remove - do not modify!
pub(crate) tags: Option<BTreeSet<Tag>>, pub(crate) tags: Option<BTreeSet<Tag>>,
/// Parent task references derived from the event tags /// Task references derived from the event tags
parents: Vec<EventId>, refs: Vec<(String, EventId)>,
/// Reference to children, populated dynamically /// Reference to children, populated dynamically
pub(crate) children: HashSet<EventId>, pub(crate) children: HashSet<EventId>,
@ -41,7 +46,7 @@ impl Ord for Task {
impl Task { impl Task {
pub(crate) fn new(event: Event) -> Task { pub(crate) fn new(event: Event) -> Task {
let (refs, tags) = event.tags.iter().partition_map(|tag| match tag.as_standardized() { let (refs, tags) = event.tags.iter().partition_map(|tag| match tag.as_standardized() {
Some(TagStandard::Event { event_id, .. }) => return Left(event_id), Some(TagStandard::Event { event_id, marker, .. }) => Left((marker.as_ref().map_or(MARKER_PARENT.to_string(), |m| m.to_string()), event_id.clone())),
_ => Right(tag.clone()), _ => Right(tag.clone()),
}); });
// Separate refs for dependencies // Separate refs for dependencies
@ -49,7 +54,7 @@ impl Task {
children: Default::default(), children: Default::default(),
props: Default::default(), props: Default::default(),
tags: Some(tags).filter(|t: &BTreeSet<Tag>| !t.is_empty()), tags: Some(tags).filter(|t: &BTreeSet<Tag>| !t.is_empty()),
parents: refs, refs,
event, event,
} }
} }
@ -58,8 +63,17 @@ impl Task {
&self.event.id &self.event.id
} }
fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> {
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
}
pub(crate) fn parent_id(&self) -> Option<&EventId> { pub(crate) fn parent_id(&self) -> Option<&EventId> {
self.parents.first() self.find_refs(MARKER_PARENT).next()
}
pub(crate) fn get_dependendees(&self) -> Vec<&EventId> {
// TODO honor properly
self.find_refs(MARKER_DEPENDS).collect()
} }
pub(crate) fn get_title(&self) -> String { pub(crate) fn get_title(&self) -> String {
@ -129,14 +143,24 @@ impl Task {
match property { match property {
"id" => Some(self.event.id.to_string()), "id" => Some(self.event.id.to_string()),
"parentid" => self.parent_id().map(|i| i.to_string()), "parentid" => self.parent_id().map(|i| i.to_string()),
"state" => Some(self.state_or_default().get_label()), "state" => Some({
let state = self.state_or_default();
let label = state.get_label();
match state.state {
State::Open => label.green(),
State::Done => label.bright_black(),
State::Closed => label.magenta(),
State::Pending => label.yellow(),
State::Procedure => label.blue(),
}.to_string()
}),
"name" => Some(self.event.content.clone()), "name" => Some(self.event.content.clone()),
"desc" => self.descriptions().last().cloned(), "desc" => self.descriptions().last().cloned(),
"description" => Some(self.descriptions().join(" ")), "description" => Some(self.descriptions().join(" ")),
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }), "hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
"tags" => self.filter_tags(|_| true), "tags" => self.filter_tags(|_| true),
"alltags" => Some(format!("{:?}", self.tags)), "alltags" => Some(format!("{:?}", self.tags)),
"parents" => Some(format!("{:?}", self.parents.iter().map(|id| id.to_string()).collect_vec())), "refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())),
"props" => Some(format!( "props" => Some(format!(
"{:?}", "{:?}",
self.props self.props
@ -190,11 +214,11 @@ impl Display for TaskState {
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum State { pub(crate) enum State {
Closed,
Open, Open,
Procedure,
Pending,
Done, Done,
Closed,
Pending,
Procedure,
} }
impl TryFrom<Kind> for State { impl TryFrom<Kind> for State {
type Error = (); type Error = ();

View File

@ -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
} }