diff --git a/src/kinds.rs b/src/kinds.rs index 77e8e2b..45c01b9 100644 --- a/src/kinds.rs +++ b/src/kinds.rs @@ -1,6 +1,7 @@ use itertools::Itertools; use log::info; use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard}; +use nostr_sdk::TagStandard::Hashtag; pub const METADATA_KIND: u16 = 0; pub const NOTE_KIND: u16 = 1; @@ -53,9 +54,39 @@ where ) } -pub(crate) fn build_task(name: &str, tags: Vec) -> EventBuilder { - info!("Created task \"{name}\" with tags [{}]", tags.iter().map(|tag| format_tag(tag)).join(", ")); - EventBuilder::new(Kind::from(TASK_KIND), name, tags) +/// Build a task with informational output and optional labeled kind +pub(crate) fn build_task(name: &str, tags: Vec, kind: Option<(&str, Kind)>) -> EventBuilder { + info!("Created {}task \"{name}\" with tags [{}]", + kind.map(|k| k.0).unwrap_or_default(), + tags.iter().map(|tag| format_tag(tag)).join(", ")); + EventBuilder::new(kind.map(|k| k.1).unwrap_or(Kind::from(TASK_KIND)), name, tags) +} + +pub(crate) fn build_prop( + kind: Kind, + comment: &str, + id: EventId, +) -> EventBuilder { + EventBuilder::new( + kind, + comment, + vec![Tag::event(id)], + ) +} + +/// Expects sanitized input +pub(crate) fn extract_tags(input: &str) -> (&str, Vec) { + match input.split_once(": ") { + None => (input, vec![]), + Some(s) => { + let tags = s + .1 + .split_ascii_whitespace() + .map(|t| Hashtag(t.to_string()).into()) + .collect(); + (s.0, tags) + } + } } fn format_tag(tag: &Tag) -> String { diff --git a/src/main.rs b/src/main.rs index 0859c5a..9edddaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -405,8 +405,7 @@ async fn main() { tasks.move_up(); tasks.make_task_with( arg, - once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS)) - .chain(tasks.parent_tag()), + once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS)), true); break 'arm; } diff --git a/src/task.rs b/src/task.rs index 80f40ed..9244c35 100644 --- a/src/task.rs +++ b/src/task.rs @@ -11,7 +11,7 @@ use log::{debug, error, info, trace, warn}; use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp}; use crate::helpers::{local_datetimestamp, some_non_empty}; -use crate::kinds::{is_hashtag, PROCEDURE_KIND}; +use crate::kinds::{is_hashtag, PROCEDURE_KIND, TASK_KIND}; pub static MARKER_PARENT: &str = "parent"; pub static MARKER_DEPENDS: &str = "depends"; @@ -95,6 +95,11 @@ impl Task { self.description_events().map(|e| &e.content) } + pub(crate) fn is_task(&self) -> bool { + self.event.kind.as_u16() == TASK_KIND || + self.states().next().is_some() + } + fn states(&self) -> impl Iterator + '_ { self.props.iter().filter_map(|event| { event.kind.try_into().ok().map(|s| TaskState { diff --git a/src/tasks.rs b/src/tasks.rs index 59c5433..ec8edb2 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeSet, HashMap, VecDeque}; use std::fmt::{Display, Formatter}; use std::io::{Error, stdout, Write}; -use std::iter::once; +use std::iter::{empty, once}; use std::ops::{Div, Rem}; use std::str::FromStr; use std::sync::mpsc::Sender; @@ -333,16 +333,18 @@ impl Tasks { .into() } - /// Executes the given function with each task referenced by this event. + /// Executes the given function with each task referenced by this event without marker. /// Returns true if any task was found. pub(crate) fn referenced_tasks(&mut self, event: &Event, f: F) -> bool { let mut found = false; for tag in event.tags.iter() { - if let Some(TagStandard::Event { event_id, .. }) = tag.as_standardized() { - self.tasks.get_mut(event_id).map(|t| { - found = true; - f(t) - }); + if let Some(TagStandard::Event { event_id, marker, .. }) = tag.as_standardized() { + if marker.is_none() { + self.tasks.get_mut(event_id).map(|t| { + found = true; + f(t) + }); + } } } found @@ -616,24 +618,6 @@ impl Tasks { // Updates - /// Expects sanitized input - pub(crate) fn parse_task(&self, input: &str) -> EventBuilder { - let mut tags: Vec = self.tags.iter().cloned().collect(); - match input.split_once(": ") { - None => build_task(input, tags), - Some(s) => { - tags.append( - &mut s - .1 - .split_ascii_whitespace() - .map(|t| Hashtag(t.to_string()).into()) - .collect(), - ); - build_task(s.0, tags) - } - } - } - pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag { Tag::from(TagStandard::Event { event_id: id, @@ -674,20 +658,23 @@ impl Tasks { /// Creates a task following the current state /// Sanitizes input pub(crate) fn make_task(&mut self, input: &str) -> EventId { - self.make_task_with(input, self.position_tags(), true) + self.make_task_with(input, empty(), true) } pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) { - let id = self.make_task_with(input, self.position_tags(), false); + let id = self.make_task_with(input, empty(), false); self.set_state_for(id, "", state); self.move_to(Some(id)); } - /// Creates a task + /// Creates a task with tags from filter and position /// Sanitizes input pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator, set_state: bool) -> EventId { + let (input, input_tags) = extract_tags(input.trim()); let id = self.submit( - self.parse_task(input.trim()) + build_task(input, input_tags, None) + .add_tags(self.tags.iter().cloned()) + .add_tags(self.position_tags()) .add_tags(tags.into_iter()) ); if set_state { @@ -696,19 +683,6 @@ impl Tasks { id } - pub(crate) fn build_prop( - &mut self, - kind: Kind, - comment: &str, - id: EventId, - ) -> EventBuilder { - EventBuilder::new( - kind, - comment, - vec![Tag::event(id)], - ) - } - pub(crate) fn get_task_title(&self, id: &EventId) -> String { self.tasks.get(id).map_or(id.to_string(), |t| t.get_title()) } @@ -784,7 +758,7 @@ impl Tasks { Ok(metadata) => { self.users.insert(event.pubkey, metadata); } Err(e) => warn!("Cannot parse metadata: {} from {:?}", e, event) } - _ => self.add_prop(&event), + _ => self.add_prop(event), } } @@ -801,10 +775,17 @@ impl Tasks { } } - fn add_prop(&mut self, event: &Event) { - self.referenced_tasks(&event, |t| { + fn add_prop(&mut self, event: Event) { + let found = self.referenced_tasks(&event, |t| { t.props.insert(event.clone()); }); + if !found { + if event.kind.as_u16() == NOTE_KIND { + self.add_task(event); + return; + } + warn!("Unknown event {:?}", event) + } } fn get_own_history(&mut self) -> Option<&mut BTreeSet> { @@ -836,7 +817,7 @@ impl Tasks { } pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId { - let prop = self.build_prop( + let prop = build_prop( state.into(), comment, id, @@ -851,13 +832,19 @@ impl Tasks { } pub(crate) fn make_note(&mut self, note: &str) { - match self.position { - None => warn!("Cannot add note \"{}\" without active task", note), - Some(id) => { - let prop = self.build_prop(Kind::TextNote, note, id); + if let Some(id) = self.position { + if self.get_by_id(&id).is_some_and(|t| t.is_task()) { + let prop = build_prop(Kind::TextNote, note.trim(), id); self.submit(prop); + return; } } + let (input, tags) = extract_tags(note.trim()); + self.submit( + build_task(input, tags, Some(("stateless ", Kind::TextNote))) + .add_tags(self.parent_tag()) + .add_tags(self.tags.iter().cloned()) + ); } // Properties @@ -1099,7 +1086,7 @@ mod tasks_test { fn test_procedures() { let mut tasks = stub_tasks(); tasks.make_task_and_enter("proc: tags", State::Procedure); - let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)])); + let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)], None)); assert_eq!(tasks.get_current_task().unwrap().children, HashSet::::new()); let sub_id = tasks.make_task("sub"); assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([sub_id]));