Compare commits
3 Commits
7a8a048d6c
...
b03ad00b6a
Author | SHA1 | Date |
---|---|---|
xeruf | b03ad00b6a | |
xeruf | ff74ac216b | |
xeruf | c48355e5da |
|
@ -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
|
||||
|
|
|
@ -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('!') =>
|
||||
|
|
44
src/task.rs
44
src/task.rs
|
@ -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 = ();
|
||||
|
|
76
src/tasks.rs
76
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<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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue