From 7a8a048d6c1f4d545d089fbbe736687fc8a71a1f Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Thu, 8 Aug 2024 21:10:17 +0300 Subject: [PATCH] feat: add procedures for dependency lists --- README.md | 5 +++++ src/kinds.rs | 3 ++- src/main.rs | 22 ++++++++++++++++++++++ src/task.rs | 47 +++++++++++++++++++++++++++++++++++------------ src/tasks.rs | 37 +++++++++++++++++++++++++++++++------ 5 files changed, 95 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5425dbf..5c66fa5 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ To stop time-tracking completely, simply move to the root of all tasks. + no match: create & activate task - `.2` - set view depth to `2`, which can be substituted for any number (how many subtask levels to show, default 1) - `/[TEXT]` - like `.`, but never creates a task +- `|[TASK]` - (un)mark current task as procedure or create and activate a new task procedure (where subtasks automatically depend on the previously created task) Dots can be repeated to move to parent tasks. @@ -132,6 +133,7 @@ An active tag or state filter will also set that attribute for newly created tas - `rtime` - time tracked on this tasks and all recursive subtasks - `progress` - recursive subtask completion in percent - `subtasks` - how many direct subtasks are complete +- TBI `depends` For debugging: `props`, `alltags`, `descriptions` @@ -151,6 +153,7 @@ Considering to use Calendar: https://github.com/nostr-protocol/nips/blob/master/ ## Plans +- Remove state filter when moving up? - Task markdown support? - colored - Time tracking: Ability to postpone task and add planned timestamps (calendar entry) - Parse Hashtag tags from task name @@ -168,6 +171,8 @@ The following features are not ready to be implemented because they need conceptualization. Suggestions welcome! +- Task Dependencies +- Task Templates - Task Ownership - Combined formatting and recursion specifiers + progress count/percentage and recursive or not diff --git a/src/kinds.rs b/src/kinds.rs index 1bf2aee..cd03dd0 100644 --- a/src/kinds.rs +++ b/src/kinds.rs @@ -3,8 +3,9 @@ use log::info; use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard}; pub const TASK_KIND: u16 = 1621; +pub const PROCEDURE_KIND: u16 = 1639; pub const TRACKING_KIND: u16 = 1650; -pub const KINDS: [u16; 7] = [1, TASK_KIND, TRACKING_KIND, 1630, 1631, 1632, 1633]; +pub const KINDS: [u16; 8] = [1, TASK_KIND, TRACKING_KIND, PROCEDURE_KIND, 1630, 1631, 1632, 1633]; pub const PROPERTY_COLUMNS: &str = "Available properties: - `id` diff --git a/src/main.rs b/src/main.rs index 41a383d..3ca4aa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -320,6 +320,28 @@ async fn main() { tasks.undo(); } + Some('|') => + match arg { + None => match tasks.get_position() { + None => { + tasks.set_filter( + tasks.current_tasks().into_iter() + .filter(|t| t.pure_state() == State::Procedure) + .map(|t| t.event.id) + .collect() + ); + } + Some(id) => { + tasks.set_state_for(id, "", State::Procedure); + } + }, + Some(arg) => { + let id = tasks.make_task(arg); + tasks.set_state_for(id, "", State::Procedure); + tasks.move_to(Some(id)); + } + } + Some('?') => { tasks.set_state_filter(arg.map(|s| s.to_string())); } diff --git a/src/task.rs b/src/task.rs index d47714b..044da9a 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,4 +1,5 @@ use fmt::Display; +use std::cmp::Ordering; use std::collections::{BTreeSet, HashSet}; use std::fmt; @@ -8,29 +9,47 @@ use log::{debug, error, info, trace, warn}; use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, TagStandard, Timestamp}; use crate::helpers::some_non_empty; -use crate::kinds::is_hashtag; +use crate::kinds::{is_hashtag, PROCEDURE_KIND}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct Task { + /// Event that defines this task pub(crate) event: Event, - pub(crate) children: HashSet, - pub(crate) props: BTreeSet, - /// Cached sorted tags of the event + /// Cached sorted tags of the event with references remove - do not modify! pub(crate) tags: Option>, + /// Parent task references derived from the event tags parents: Vec, + + /// Reference to children, populated dynamically + pub(crate) children: HashSet, + /// Events belonging to this task, such as state updates and notes + pub(crate) props: BTreeSet, +} + +impl PartialOrd for Task { + fn partial_cmp(&self, other: &Self) -> Option { + self.event.partial_cmp(&other.event) + } +} + +impl Ord for Task { + fn cmp(&self, other: &Self) -> Ordering { + self.event.cmp(&other.event) + } } impl Task { pub(crate) fn new(event: Event) -> Task { - let (parents, tags) = event.tags.iter().partition_map(|tag| match tag.as_standardized() { + let (refs, tags) = event.tags.iter().partition_map(|tag| match tag.as_standardized() { Some(TagStandard::Event { event_id, .. }) => return Left(event_id), _ => Right(tag.clone()), }); + // Separate refs for dependencies Task { children: Default::default(), props: Default::default(), tags: Some(tags).filter(|t: &BTreeSet| !t.is_empty()), - parents, + parents: refs, event, } } @@ -117,6 +136,7 @@ impl Task { "hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }), "tags" => self.filter_tags(|_| true), "alltags" => Some(format!("{:?}", self.tags)), + "parents" => Some(format!("{:?}", self.parents.iter().map(|id| id.to_string()).collect_vec())), "props" => Some(format!( "{:?}", self.props @@ -172,18 +192,20 @@ impl Display for TaskState { pub(crate) enum State { Closed, Open, - Active, + Procedure, + Pending, Done, } impl TryFrom for State { type Error = (); fn try_from(value: Kind) -> Result { - match value.as_u32() { + match value.as_u16() { 1630 => Ok(State::Open), 1631 => Ok(State::Done), 1632 => Ok(State::Closed), - 1633 => Ok(State::Active), + 1633 => Ok(State::Pending), + PROCEDURE_KIND => Ok(State::Procedure), _ => Err(()), } } @@ -191,7 +213,7 @@ impl TryFrom for State { impl State { pub(crate) fn is_open(&self) -> bool { match self { - State::Open | State::Active => true, + State::Open | State::Procedure => true, _ => false, } } @@ -201,7 +223,8 @@ impl State { State::Open => 1630, State::Done => 1631, State::Closed => 1632, - State::Active => 1633, + State::Pending => 1633, + State::Procedure => PROCEDURE_KIND, } } } diff --git a/src/tasks.rs b/src/tasks.rs index a5fcd90..1a0dfa6 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -11,8 +11,9 @@ use chrono::LocalResult::Single; use colored::Colorize; use itertools::Itertools; use log::{debug, error, info, trace, warn}; -use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, TagStandard, Timestamp, Url}; +use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url}; use nostr_sdk::base64::write::StrConsumer; +use nostr_sdk::prelude::Marker; use TagStandard::Hashtag; use crate::{Events, EventSender}; @@ -306,7 +307,7 @@ impl Tasks { // TODO apply filters in transit let state = t.pure_state(); self.state.as_ref().map_or_else(|| { - state == State::Open || ( + state.is_open() || ( state == State::Done && t.parent_id() != None ) @@ -382,13 +383,13 @@ impl Tasks { .map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)), "path" => self.get_task_path(Some(task.event.id)), "rpath" => self.relative_path(task.event.id), - // TODO format strings as config + // TODO format strings configurable "time" => display_time("MMMm", self.time_tracked(*task.get_id())), "rtime" => { let time = self.total_time_tracked(*task.get_id()); total_time += time; display_time("HH:MM", time) - }, + } prop => task.get(prop).unwrap_or(String::new()), }) .collect::>() @@ -540,7 +541,31 @@ impl Tasks { /// Sanitizes input pub(crate) fn make_task(&mut self, input: &str) -> EventId { - let id = self.submit(self.parse_task(input.trim())); + let tag: Option = self.get_current_task() + .and_then(|t| { + println!("{:?}", t); + if t.pure_state() == State::Procedure { + t.children.iter() + .filter_map(|id| self.get_by_id(id)) + .max() + .map(|t| { + Tag::from( + TagStandard::Event { + event_id: t.event.id, + relay_url: self.sender.url.as_ref().map(|url| UncheckedUrl::new(url.as_str())), + marker: Some(Marker::Custom("depends".to_string())), + public_key: Some(t.event.pubkey), + } + ) + }) + } else { + None + } + }); + let id = self.submit( + self.parse_task(input.trim()) + .add_tags(tag.into_iter()) + ); self.state.clone().inspect(|s| self.set_state_for_with(id, s)); id } @@ -604,7 +629,7 @@ impl Tasks { t.children.insert(event.id); }); if self.tasks.contains_key(&event.id) { - warn!("Did not insert duplicate event {}", event.id); + debug!("Did not insert duplicate event {}", event.id); // TODO warn in next sdk version } else { self.tasks.insert(event.id, Task::new(event)); }