diff --git a/src/main.rs b/src/main.rs index a198cf3..4f51136 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ static MY_KEYS: Lazy = Lazy::new(|| match fs::read_to_string("keys") { } }); +#[derive(Debug, Clone)] struct EventSender { tx: Sender, keys: Keys, @@ -155,12 +156,13 @@ async fn main() { } println!(); + let mut lines = stdin().lines(); 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(); - match stdin().lines().next() { + match lines.next() { Some(Ok(input)) => { while let Ok(notification) = notifications.try_recv() { if let RelayPoolNotification::Event { @@ -208,21 +210,7 @@ async fn main() { Some('?') => { let arg = &input[1..]; - tasks.move_to(tasks.get_position()); - 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(), - ); + tasks.set_state_filter(Some(arg.to_string()).filter(|s| !s.is_empty())); } Some('-') => tasks.add_note(&input[1..]), @@ -247,7 +235,7 @@ async fn main() { let mut pos = tasks.get_position(); for _ in iter.take_while(|c| c == &'.') { dots += 1; - pos = tasks.parent(pos); + pos = tasks.get_parent(pos); } let slice = &input[dots..]; if slice.is_empty() { diff --git a/src/task.rs b/src/task.rs index 6180475..bc643a0 100644 --- a/src/task.rs +++ b/src/task.rs @@ -5,6 +5,7 @@ use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, Timestamp}; use crate::EventSender; +#[derive(Debug, Clone, PartialEq)] pub(crate) struct Task { pub(crate) event: Event, pub(crate) children: HashSet, @@ -152,8 +153,8 @@ impl Task { } pub(crate) struct TaskState { - name: Option, state: State, + name: Option, time: Timestamp, } impl TaskState { diff --git a/src/tasks.rs b/src/tasks.rs index 66a4a60..55b73c2 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -8,6 +8,7 @@ use crate::{EventSender, TASK_KIND}; use crate::task::{State, Task}; type TaskMap = HashMap; +#[derive(Debug, Clone)] pub(crate) struct Tasks { /// The Tasks tasks: TaskMap, @@ -22,6 +23,8 @@ pub(crate) struct Tasks { position: Option, /// Currently active tags tags: BTreeSet, + /// Current active state + state: Option, /// A filtered view of the current tasks view: Vec, @@ -36,6 +39,7 @@ impl Tasks { position: None, view: Default::default(), tags: Default::default(), + state: Some(State::Open.to_string()), depth: 1, sender, } @@ -45,6 +49,10 @@ impl Tasks { impl Tasks { // Accessors + pub(crate) fn get_by_id(&self, id: &EventId) -> Option<&Task> { + self.tasks.get(id) + } + pub(crate) fn get_position(&self) -> Option { self.position } @@ -62,19 +70,22 @@ impl Tasks { // Parents - pub(crate) fn parent(&self, id: Option) -> Option { + pub(crate) fn get_parent(&self, id: Option) -> Option { id.and_then(|id| self.tasks.get(&id)) .and_then(|t| t.parent_id()) } - pub(crate) fn taskpath(&self, id: Option) -> 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::>() + .join("") + } + + pub(crate) fn get_task_path(&self, id: Option) -> String { join_tasks(self.traverse_up_from(id)) - + &self - .tags - .iter() - .map(|t| format!(" #{}", t.content().unwrap())) - .collect::>() - .join("") } pub(crate) fn traverse_up_from(&self, id: Option) -> ParentIterator { @@ -140,48 +151,29 @@ impl Tasks { } let res: Vec<&Task> = self.resolve_tasks(self.view.iter()); if res.len() > 0 { + // Currently ignores filter when it matches nothing return res; } - let tasks = self.position.map_or_else( - || { - if self.depth > 8 { - self.tasks.values().collect() - } else if self.depth == 1 { - self.tasks - .values() - .filter(|t| t.parent_id() == None) - .collect() - } else { - self.resolve_tasks( - self.tasks - .values() - .filter(|t| t.parent_id() == None) - .map(|t| &t.event.id), - ) - } - }, - |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() - } + self.resolve_tasks( + self.tasks + .values() + .filter(|t| t.parent_id() == self.position) + .map(|t| t.get_id()), + ) + .into_iter() + .filter(|t| { + self.state.as_ref().map_or(true, |state| { + t.state().is_some_and(|t| t.matches_label(state)) + }) && (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)) + })) + }) + .collect() } - pub(crate) fn print_current_tasks(&self) { + pub(crate) fn print_tasks(&self) { println!("{}", self.properties.join("\t")); for task in self.current_tasks() { println!( @@ -189,7 +181,7 @@ impl Tasks { self.properties .iter() .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( self.traverse_up_from(Some(task.event.id)) .take_while(|t| Some(t.event.id) != self.position) @@ -207,7 +199,7 @@ impl Tasks { // Movement and Selection pub(crate) fn set_filter(&mut self, view: Vec) { - self.view = view + self.view = view; } pub(crate) fn add_tag(&mut self, tag: String) { @@ -215,6 +207,11 @@ impl Tasks { self.tags.insert(Hashtag(tag)); } + pub(crate) fn set_state_filter(&mut self, state: Option) { + self.view.clear(); + self.state = state; + } + pub(crate) fn move_up(&mut self) { self.move_to( self.position @@ -229,6 +226,7 @@ impl Tasks { if id == self.position { return; } + // TODO: erases previous state comment - do not track active via state self.update_state("", |s| { if s.pure_state() == State::Active { Some(State::Open) @@ -264,6 +262,8 @@ impl Tasks { self.sender.submit(self.build_task(input)).map(|e| { let id = e.id; self.add_task(e); + let state = self.state.clone().unwrap_or("Open".to_string()); + self.set_state_for(&id, &state); id }) } @@ -293,23 +293,28 @@ impl Tasks { }); } + fn set_state_for(&mut self, id: &EventId, comment: &str) -> Option { + 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(&mut self, id: &EventId, comment: &str, f: F) -> Option where F: FnOnce(&Task) -> Option, { - self.tasks.get_mut(id).and_then(|task| { - f(task) - .and_then(|state| { - self.sender.submit(EventBuilder::new( - state.kind(), - comment, - vec![Tag::event(task.event.id)], - )) - }) - .inspect(|e| { - task.props.insert(e.clone()); - }) - }) + self.tasks + .get_mut(id) + .and_then(|task| f(task).and_then(|state| task.set_state(&self.sender, state, comment))) } pub(crate) fn update_state(&mut self, comment: &str, f: F) -> Option @@ -368,13 +373,16 @@ impl<'a> Iterator for ParentIterator<'a> { fn test_depth() { use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); + let (tx, _rx) = mpsc::channel(); let mut tasks = Tasks::from(EventSender { tx, keys: Keys::generate(), }); let t1 = tasks.make_task("t1"); + let task1 = tasks.get_by_id(&t1.unwrap()).unwrap(); assert_eq!(tasks.depth, 1); + assert_eq!(task1.state().unwrap().get_label(), "Open"); + //eprintln!("{:?}", tasks); assert_eq!(tasks.current_tasks().len(), 1); tasks.depth = 0; assert_eq!(tasks.current_tasks().len(), 0);