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

View File

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

View File

@ -2,7 +2,9 @@ use fmt::Display;
use std::cmp::Ordering;
use std::collections::{BTreeSet, HashSet};
use std::fmt;
use std::string::ToString;
use colored::Colorize;
use itertools::Either::{Left, Right};
use itertools::Itertools;
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::kinds::{is_hashtag, PROCEDURE_KIND};
pub static MARKER_PARENT: &str = "parent";
pub static MARKER_DEPENDS: &str = "depends";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Task {
/// Event that defines this task
pub(crate) event: Event,
/// Cached sorted tags of the event with references remove - do not modify!
pub(crate) tags: Option<BTreeSet<Tag>>,
/// Parent task references derived from the event tags
parents: Vec<EventId>,
/// Task references derived from the event tags
refs: Vec<(String, EventId)>,
/// Reference to children, populated dynamically
pub(crate) children: HashSet<EventId>,
@ -41,7 +46,7 @@ impl Ord for Task {
impl Task {
pub(crate) fn new(event: Event) -> Task {
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()),
});
// Separate refs for dependencies
@ -49,7 +54,7 @@ impl Task {
children: Default::default(),
props: Default::default(),
tags: Some(tags).filter(|t: &BTreeSet<Tag>| !t.is_empty()),
parents: refs,
refs,
event,
}
}
@ -58,8 +63,17 @@ impl Task {
&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> {
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 {
@ -129,14 +143,24 @@ impl Task {
match property {
"id" => Some(self.event.id.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()),
"desc" => self.descriptions().last().cloned(),
"description" => Some(self.descriptions().join(" ")),
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
"tags" => self.filter_tags(|_| true),
"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!(
"{:?}",
self.props
@ -190,11 +214,11 @@ impl Display for TaskState {
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum State {
Closed,
Open,
Procedure,
Pending,
Done,
Closed,
Pending,
Procedure,
}
impl TryFrom<Kind> for State {
type Error = ();

View File

@ -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<Tag>,
/// Current active state
state: Option<String>,
state: StateFilter,
/// A filtered view of the current tasks
view: Vec<EventId>,
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 {
pub(crate) fn from(url: Option<Url>, 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::<Vec<String>>()
.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<String>) {
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
}