feat: enable filtering by state

This commit is contained in:
xeruf 2024-07-26 21:45:29 +03:00
parent 54e870a93a
commit d511e9ca49
3 changed files with 79 additions and 82 deletions

View File

@ -40,6 +40,7 @@ static MY_KEYS: Lazy<Keys> = Lazy::new(|| match fs::read_to_string("keys") {
} }
}); });
#[derive(Debug, Clone)]
struct EventSender { struct EventSender {
tx: Sender<Event>, tx: Sender<Event>,
keys: Keys, keys: Keys,
@ -155,12 +156,13 @@ async fn main() {
} }
println!(); println!();
let mut lines = stdin().lines();
loop { loop {
tasks.print_current_tasks(); tasks.print_tasks();
print!(" {}) ", tasks.taskpath(tasks.get_position())); print!(" {}{}) ", tasks.get_task_path(tasks.get_position()), tasks.get_prompt_suffix());
stdout().flush().unwrap(); stdout().flush().unwrap();
match stdin().lines().next() { match lines.next() {
Some(Ok(input)) => { Some(Ok(input)) => {
while let Ok(notification) = notifications.try_recv() { while let Ok(notification) = notifications.try_recv() {
if let RelayPoolNotification::Event { if let RelayPoolNotification::Event {
@ -208,21 +210,7 @@ async fn main() {
Some('?') => { Some('?') => {
let arg = &input[1..]; let arg = &input[1..];
tasks.move_to(tasks.get_position()); tasks.set_state_filter(Some(arg.to_string()).filter(|s| !s.is_empty()));
tasks.set_filter(
tasks
.current_tasks()
.into_iter()
.filter(|t| {
if arg.is_empty() {
t.pure_state() == State::Open
} else {
t.state().is_some_and(|s| s.get_label() == arg)
}
})
.map(|t| t.event.id)
.collect(),
);
} }
Some('-') => tasks.add_note(&input[1..]), Some('-') => tasks.add_note(&input[1..]),
@ -247,7 +235,7 @@ async fn main() {
let mut pos = tasks.get_position(); let mut pos = tasks.get_position();
for _ in iter.take_while(|c| c == &'.') { for _ in iter.take_while(|c| c == &'.') {
dots += 1; dots += 1;
pos = tasks.parent(pos); pos = tasks.get_parent(pos);
} }
let slice = &input[dots..]; let slice = &input[dots..];
if slice.is_empty() { if slice.is_empty() {

View File

@ -5,6 +5,7 @@ use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, Timestamp};
use crate::EventSender; use crate::EventSender;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Task { pub(crate) struct Task {
pub(crate) event: Event, pub(crate) event: Event,
pub(crate) children: HashSet<EventId>, pub(crate) children: HashSet<EventId>,
@ -152,8 +153,8 @@ impl Task {
} }
pub(crate) struct TaskState { pub(crate) struct TaskState {
name: Option<String>,
state: State, state: State,
name: Option<String>,
time: Timestamp, time: Timestamp,
} }
impl TaskState { impl TaskState {

View File

@ -8,6 +8,7 @@ use crate::{EventSender, TASK_KIND};
use crate::task::{State, Task}; use crate::task::{State, Task};
type TaskMap = HashMap<EventId, Task>; type TaskMap = HashMap<EventId, Task>;
#[derive(Debug, Clone)]
pub(crate) struct Tasks { pub(crate) struct Tasks {
/// The Tasks /// The Tasks
tasks: TaskMap, tasks: TaskMap,
@ -22,6 +23,8 @@ pub(crate) struct Tasks {
position: Option<EventId>, position: Option<EventId>,
/// Currently active tags /// Currently active tags
tags: BTreeSet<Tag>, tags: BTreeSet<Tag>,
/// Current active state
state: Option<String>,
/// A filtered view of the current tasks /// A filtered view of the current tasks
view: Vec<EventId>, view: Vec<EventId>,
@ -36,6 +39,7 @@ impl Tasks {
position: None, position: None,
view: Default::default(), view: Default::default(),
tags: Default::default(), tags: Default::default(),
state: Some(State::Open.to_string()),
depth: 1, depth: 1,
sender, sender,
} }
@ -45,6 +49,10 @@ impl Tasks {
impl Tasks { impl Tasks {
// Accessors // Accessors
pub(crate) fn get_by_id(&self, id: &EventId) -> Option<&Task> {
self.tasks.get(id)
}
pub(crate) fn get_position(&self) -> Option<EventId> { pub(crate) fn get_position(&self) -> Option<EventId> {
self.position self.position
} }
@ -62,19 +70,22 @@ impl Tasks {
// Parents // Parents
pub(crate) fn parent(&self, id: Option<EventId>) -> Option<EventId> { pub(crate) fn get_parent(&self, id: Option<EventId>) -> Option<EventId> {
id.and_then(|id| self.tasks.get(&id)) id.and_then(|id| self.tasks.get(&id))
.and_then(|t| t.parent_id()) .and_then(|t| t.parent_id())
} }
pub(crate) fn taskpath(&self, id: Option<EventId>) -> String { pub(crate) fn get_prompt_suffix(&self) -> String {
self.tags
.iter()
.map(|t| format!(" #{}", t.content().unwrap()))
.chain(self.state.as_ref().map(|s| format!(" ?{s}")).into_iter())
.collect::<Vec<String>>()
.join("")
}
pub(crate) fn get_task_path(&self, id: Option<EventId>) -> String {
join_tasks(self.traverse_up_from(id)) join_tasks(self.traverse_up_from(id))
+ &self
.tags
.iter()
.map(|t| format!(" #{}", t.content().unwrap()))
.collect::<Vec<String>>()
.join("")
} }
pub(crate) fn traverse_up_from(&self, id: Option<EventId>) -> ParentIterator { pub(crate) fn traverse_up_from(&self, id: Option<EventId>) -> ParentIterator {
@ -140,48 +151,29 @@ impl Tasks {
} }
let res: Vec<&Task> = self.resolve_tasks(self.view.iter()); let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
if res.len() > 0 { if res.len() > 0 {
// Currently ignores filter when it matches nothing
return res; return res;
} }
let tasks = self.position.map_or_else( self.resolve_tasks(
|| { self.tasks
if self.depth > 8 { .values()
self.tasks.values().collect() .filter(|t| t.parent_id() == self.position)
} else if self.depth == 1 { .map(|t| t.get_id()),
self.tasks )
.values() .into_iter()
.filter(|t| t.parent_id() == None) .filter(|t| {
.collect() self.state.as_ref().map_or(true, |state| {
} else { t.state().is_some_and(|t| t.matches_label(state))
self.resolve_tasks( }) && (self.tags.is_empty()
self.tasks || t.tags.as_ref().map_or(false, |tags| {
.values() let mut iter = tags.iter();
.filter(|t| t.parent_id() == None) self.tags.iter().all(|tag| iter.any(|t| t == tag))
.map(|t| &t.event.id), }))
) })
} .collect()
},
|p| {
self.tasks
.get(&p)
.map_or(Vec::new(), |t| self.resolve_tasks(t.children.iter()))
},
);
if self.tags.is_empty() {
tasks
} else {
tasks
.into_iter()
.filter(|t| {
t.tags.as_ref().map_or(false, |tags| {
let mut iter = tags.iter();
self.tags.iter().all(|tag| iter.any(|t| t == tag))
})
})
.collect()
}
} }
pub(crate) fn print_current_tasks(&self) { pub(crate) fn print_tasks(&self) {
println!("{}", self.properties.join("\t")); println!("{}", self.properties.join("\t"));
for task in self.current_tasks() { for task in self.current_tasks() {
println!( println!(
@ -189,7 +181,7 @@ impl Tasks {
self.properties self.properties
.iter() .iter()
.map(|p| match p.as_str() { .map(|p| match p.as_str() {
"path" => self.taskpath(Some(task.event.id)), "path" => self.get_task_path(Some(task.event.id)),
"rpath" => join_tasks( "rpath" => join_tasks(
self.traverse_up_from(Some(task.event.id)) self.traverse_up_from(Some(task.event.id))
.take_while(|t| Some(t.event.id) != self.position) .take_while(|t| Some(t.event.id) != self.position)
@ -207,7 +199,7 @@ impl Tasks {
// Movement and Selection // Movement and Selection
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) { pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
self.view = view self.view = view;
} }
pub(crate) fn add_tag(&mut self, tag: String) { pub(crate) fn add_tag(&mut self, tag: String) {
@ -215,6 +207,11 @@ impl Tasks {
self.tags.insert(Hashtag(tag)); self.tags.insert(Hashtag(tag));
} }
pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
self.view.clear();
self.state = state;
}
pub(crate) fn move_up(&mut self) { pub(crate) fn move_up(&mut self) {
self.move_to( self.move_to(
self.position self.position
@ -229,6 +226,7 @@ impl Tasks {
if id == self.position { if id == self.position {
return; return;
} }
// TODO: erases previous state comment - do not track active via state
self.update_state("", |s| { self.update_state("", |s| {
if s.pure_state() == State::Active { if s.pure_state() == State::Active {
Some(State::Open) Some(State::Open)
@ -264,6 +262,8 @@ impl Tasks {
self.sender.submit(self.build_task(input)).map(|e| { self.sender.submit(self.build_task(input)).map(|e| {
let id = e.id; let id = e.id;
self.add_task(e); self.add_task(e);
let state = self.state.clone().unwrap_or("Open".to_string());
self.set_state_for(&id, &state);
id id
}) })
} }
@ -293,23 +293,28 @@ impl Tasks {
}); });
} }
fn set_state_for(&mut self, id: &EventId, comment: &str) -> Option<Event> {
let t = self.tasks.get_mut(id);
t.and_then(|task| {
task.set_state(
&self.sender,
match comment {
"Closed" => State::Closed,
"Done" => State::Done,
_ => State::Open,
},
comment,
)
})
}
pub(crate) fn update_state_for<F>(&mut self, id: &EventId, comment: &str, f: F) -> Option<Event> pub(crate) fn update_state_for<F>(&mut self, id: &EventId, comment: &str, f: F) -> Option<Event>
where where
F: FnOnce(&Task) -> Option<State>, F: FnOnce(&Task) -> Option<State>,
{ {
self.tasks.get_mut(id).and_then(|task| { self.tasks
f(task) .get_mut(id)
.and_then(|state| { .and_then(|task| f(task).and_then(|state| task.set_state(&self.sender, state, comment)))
self.sender.submit(EventBuilder::new(
state.kind(),
comment,
vec![Tag::event(task.event.id)],
))
})
.inspect(|e| {
task.props.insert(e.clone());
})
})
} }
pub(crate) fn update_state<F>(&mut self, comment: &str, f: F) -> Option<Event> pub(crate) fn update_state<F>(&mut self, comment: &str, f: F) -> Option<Event>
@ -368,13 +373,16 @@ impl<'a> Iterator for ParentIterator<'a> {
fn test_depth() { fn test_depth() {
use std::sync::mpsc; use std::sync::mpsc;
let (tx, rx) = mpsc::channel(); let (tx, _rx) = mpsc::channel();
let mut tasks = Tasks::from(EventSender { let mut tasks = Tasks::from(EventSender {
tx, tx,
keys: Keys::generate(), keys: Keys::generate(),
}); });
let t1 = tasks.make_task("t1"); let t1 = tasks.make_task("t1");
let task1 = tasks.get_by_id(&t1.unwrap()).unwrap();
assert_eq!(tasks.depth, 1); assert_eq!(tasks.depth, 1);
assert_eq!(task1.state().unwrap().get_label(), "Open");
//eprintln!("{:?}", tasks);
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.current_tasks().len(), 1);
tasks.depth = 0; tasks.depth = 0;
assert_eq!(tasks.current_tasks().len(), 0); assert_eq!(tasks.current_tasks().len(), 0);