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 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<Tag>) -> 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<Tag>, 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<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 {

View File

@ -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;
}

View File

@ -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<Item=TaskState> + '_ {
self.props.iter().filter_map(|event| {
event.kind.try_into().ok().map(|s| TaskState {

View File

@ -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<F: Fn(&mut Task)>(&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<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 {
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<Item=Tag>, 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<Event>> {
@ -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::<EventId>::new());
let sub_id = tasks.make_task("sub");
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([sub_id]));