feat: add notes as stateless tasks

This commit is contained in:
xeruf 2024-08-18 22:24:14 +03:00
parent 2255abc1b8
commit eaeeebca7b
4 changed files with 79 additions and 57 deletions

View File

@ -1,6 +1,7 @@
use itertools::Itertools; use itertools::Itertools;
use log::info; use log::info;
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard}; use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
use nostr_sdk::TagStandard::Hashtag;
pub const METADATA_KIND: u16 = 0; pub const METADATA_KIND: u16 = 0;
pub const NOTE_KIND: u16 = 1; pub const NOTE_KIND: u16 = 1;
@ -53,9 +54,39 @@ where
) )
} }
pub(crate) fn build_task(name: &str, tags: Vec<Tag>) -> EventBuilder { /// Build a task with informational output and optional labeled kind
info!("Created task \"{name}\" with tags [{}]", tags.iter().map(|tag| format_tag(tag)).join(", ")); pub(crate) fn build_task(name: &str, tags: Vec<Tag>, kind: Option<(&str, Kind)>) -> EventBuilder {
EventBuilder::new(Kind::from(TASK_KIND), name, tags) 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<Tag>) {
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 { fn format_tag(tag: &Tag) -> String {

View File

@ -405,8 +405,7 @@ async fn main() {
tasks.move_up(); tasks.move_up();
tasks.make_task_with( tasks.make_task_with(
arg, arg,
once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS)) once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS)),
.chain(tasks.parent_tag()),
true); true);
break 'arm; break 'arm;
} }

View File

@ -11,7 +11,7 @@ use log::{debug, error, info, trace, warn};
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp}; use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
use crate::helpers::{local_datetimestamp, some_non_empty}; 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_PARENT: &str = "parent";
pub static MARKER_DEPENDS: &str = "depends"; pub static MARKER_DEPENDS: &str = "depends";
@ -95,6 +95,11 @@ impl Task {
self.description_events().map(|e| &e.content) 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<Item=TaskState> + '_ { fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
self.props.iter().filter_map(|event| { self.props.iter().filter_map(|event| {
event.kind.try_into().ok().map(|s| TaskState { event.kind.try_into().ok().map(|s| TaskState {

View File

@ -1,7 +1,7 @@
use std::collections::{BTreeSet, HashMap, VecDeque}; use std::collections::{BTreeSet, HashMap, VecDeque};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::{Error, stdout, Write}; use std::io::{Error, stdout, Write};
use std::iter::once; use std::iter::{empty, once};
use std::ops::{Div, Rem}; use std::ops::{Div, Rem};
use std::str::FromStr; use std::str::FromStr;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
@ -333,16 +333,18 @@ impl Tasks {
.into() .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. /// Returns true if any task was found.
pub(crate) fn referenced_tasks<F: Fn(&mut Task)>(&mut self, event: &Event, f: F) -> bool { pub(crate) fn referenced_tasks<F: Fn(&mut Task)>(&mut self, event: &Event, f: F) -> bool {
let mut found = false; let mut found = false;
for tag in event.tags.iter() { for tag in event.tags.iter() {
if let Some(TagStandard::Event { event_id, .. }) = tag.as_standardized() { if let Some(TagStandard::Event { event_id, marker, .. }) = tag.as_standardized() {
self.tasks.get_mut(event_id).map(|t| { if marker.is_none() {
found = true; self.tasks.get_mut(event_id).map(|t| {
f(t) found = true;
}); f(t)
});
}
} }
} }
found found
@ -616,24 +618,6 @@ impl Tasks {
// Updates // Updates
/// Expects sanitized input
pub(crate) fn parse_task(&self, input: &str) -> EventBuilder {
let mut tags: Vec<Tag> = 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 { pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
Tag::from(TagStandard::Event { Tag::from(TagStandard::Event {
event_id: id, event_id: id,
@ -674,20 +658,23 @@ impl Tasks {
/// Creates a task following the current state /// Creates a task following the current state
/// Sanitizes input /// Sanitizes input
pub(crate) fn make_task(&mut self, input: &str) -> EventId { 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) { 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.set_state_for(id, "", state);
self.move_to(Some(id)); self.move_to(Some(id));
} }
/// Creates a task /// Creates a task with tags from filter and position
/// Sanitizes input /// Sanitizes input
pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId { pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId {
let (input, input_tags) = extract_tags(input.trim());
let id = self.submit( 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()) .add_tags(tags.into_iter())
); );
if set_state { if set_state {
@ -696,19 +683,6 @@ impl Tasks {
id 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 { pub(crate) fn get_task_title(&self, id: &EventId) -> String {
self.tasks.get(id).map_or(id.to_string(), |t| t.get_title()) 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); } Ok(metadata) => { self.users.insert(event.pubkey, metadata); }
Err(e) => warn!("Cannot parse metadata: {} from {:?}", e, event) 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) { fn add_prop(&mut self, event: Event) {
self.referenced_tasks(&event, |t| { let found = self.referenced_tasks(&event, |t| {
t.props.insert(event.clone()); 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<Event>> { fn get_own_history(&mut self) -> Option<&mut BTreeSet<Event>> {
@ -836,7 +817,7 @@ impl Tasks {
} }
pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId { 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(), state.into(),
comment, comment,
id, id,
@ -851,13 +832,19 @@ impl Tasks {
} }
pub(crate) fn make_note(&mut self, note: &str) { pub(crate) fn make_note(&mut self, note: &str) {
match self.position { if let Some(id) = self.position {
None => warn!("Cannot add note \"{}\" without active task", note), if self.get_by_id(&id).is_some_and(|t| t.is_task()) {
Some(id) => { let prop = build_prop(Kind::TextNote, note.trim(), id);
let prop = self.build_prop(Kind::TextNote, note, id);
self.submit(prop); 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 // Properties
@ -1099,7 +1086,7 @@ mod tasks_test {
fn test_procedures() { fn test_procedures() {
let mut tasks = stub_tasks(); let mut tasks = stub_tasks();
tasks.make_task_and_enter("proc: tags", State::Procedure); 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::<EventId>::new()); assert_eq!(tasks.get_current_task().unwrap().children, HashSet::<EventId>::new());
let sub_id = tasks.make_task("sub"); let sub_id = tasks.make_task("sub");
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([sub_id])); assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([sub_id]));