Compare commits

..

No commits in common. "b03ad00b6a2939583d8f0f1042f39f439f461d4c" and "7a8a048d6c1f4d545d089fbbe736687fc8a71a1f" have entirely different histories.

4 changed files with 31 additions and 106 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, `??` to show all - `?STATE` - filter by state (type or description) - plain `?` to reset
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,10 +171,9 @@ The following features are not ready to be implemented
because they need conceptualization. because they need conceptualization.
Suggestions welcome! Suggestions welcome!
- Priorities - Task Dependencies
- Dependencies (change from tags to properties so they can be added later? or maybe as a state?) - Task Templates
- Templates - Task Ownership
- 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::{StateFilter, Tasks}; use crate::tasks::Tasks;
mod helpers; mod helpers;
mod task; mod task;
@ -343,11 +343,7 @@ async fn main() {
} }
Some('?') => { Some('?') => {
match arg { tasks.set_state_filter(arg.map(|s| s.to_string()));
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,9 +2,7 @@ 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};
@ -13,17 +11,14 @@ 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>>,
/// Task references derived from the event tags /// Parent task references derived from the event tags
refs: Vec<(String, EventId)>, parents: Vec<EventId>,
/// Reference to children, populated dynamically /// Reference to children, populated dynamically
pub(crate) children: HashSet<EventId>, pub(crate) children: HashSet<EventId>,
@ -46,7 +41,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, marker, .. }) => Left((marker.as_ref().map_or(MARKER_PARENT.to_string(), |m| m.to_string()), event_id.clone())), Some(TagStandard::Event { event_id, .. }) => return Left(event_id),
_ => Right(tag.clone()), _ => Right(tag.clone()),
}); });
// Separate refs for dependencies // Separate refs for dependencies
@ -54,7 +49,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()),
refs, parents: refs,
event, event,
} }
} }
@ -63,17 +58,8 @@ 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.find_refs(MARKER_PARENT).next() self.parents.first()
}
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 {
@ -143,24 +129,14 @@ 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({ "state" => Some(self.state_or_default().get_label()),
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)),
"refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())), "parents" => Some(format!("{:?}", self.parents.iter().map(|id| id.to_string()).collect_vec())),
"props" => Some(format!( "props" => Some(format!(
"{:?}", "{:?}",
self.props self.props
@ -214,11 +190,11 @@ impl Display for TaskState {
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum State { pub(crate) enum State {
Open,
Done,
Closed, Closed,
Pending, Open,
Procedure, Procedure,
Pending,
Done,
} }
impl TryFrom<Kind> for State { impl TryFrom<Kind> for State {
type Error = (); type Error = ();

View file

@ -1,5 +1,4 @@
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};
@ -41,66 +40,13 @@ pub(crate) struct Tasks {
/// Currently active tags /// Currently active tags
tags: BTreeSet<Tag>, tags: BTreeSet<Tag>,
/// Current active state /// Current active state
state: StateFilter, state: Option<String>,
/// 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 {
@ -126,7 +72,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: Default::default(), state: None,
depth: 1, depth: 1,
sender, sender,
} }
@ -262,7 +208,8 @@ impl Tasks {
self.tags self.tags
.iter() .iter()
.map(|t| format!(" #{}", t.content().unwrap())) .map(|t| format!(" #{}", t.content().unwrap()))
.chain(once(self.state.indicator())) .chain(self.state.as_ref().map(|s| format!(" ?{s}")).into_iter())
.collect::<Vec<String>>()
.join("") .join("")
} }
@ -359,7 +306,14 @@ 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.matches(t) && (self.tags.is_empty() 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()
|| 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))
@ -482,9 +436,9 @@ impl Tasks {
} }
} }
pub(crate) fn set_state_filter(&mut self, state: StateFilter) { pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
self.view.clear(); self.view.clear();
info!("Filtering for {}", state); info!("Filtering for {}", state.as_ref().map_or("open tasks".to_string(), |s| format!("state {s}")));
self.state = state; self.state = state;
} }
@ -612,7 +566,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.as_option().inspect(|s| self.set_state_for_with(id, s)); self.state.clone().inspect(|s| self.set_state_for_with(id, s));
id id
} }