forked from janek/mostr
feat: add procedures for dependency lists
This commit is contained in:
parent
c492d64d9e
commit
7a8a048d6c
|
@ -94,6 +94,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
||||||
+ no match: create & activate task
|
+ 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)
|
- `.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
|
- `/[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.
|
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
|
- `rtime` - time tracked on this tasks and all recursive subtasks
|
||||||
- `progress` - recursive subtask completion in percent
|
- `progress` - recursive subtask completion in percent
|
||||||
- `subtasks` - how many direct subtasks are complete
|
- `subtasks` - how many direct subtasks are complete
|
||||||
|
- TBI `depends`
|
||||||
|
|
||||||
For debugging: `props`, `alltags`, `descriptions`
|
For debugging: `props`, `alltags`, `descriptions`
|
||||||
|
|
||||||
|
@ -151,6 +153,7 @@ Considering to use Calendar: https://github.com/nostr-protocol/nips/blob/master/
|
||||||
|
|
||||||
## Plans
|
## Plans
|
||||||
|
|
||||||
|
- Remove state filter when moving up?
|
||||||
- Task markdown support? - colored
|
- Task markdown support? - colored
|
||||||
- Time tracking: Ability to postpone task and add planned timestamps (calendar entry)
|
- Time tracking: Ability to postpone task and add planned timestamps (calendar entry)
|
||||||
- Parse Hashtag tags from task name
|
- Parse Hashtag tags from task name
|
||||||
|
@ -168,6 +171,8 @@ The following features are not ready to be implemented
|
||||||
because they need conceptualization.
|
because they need conceptualization.
|
||||||
Suggestions welcome!
|
Suggestions welcome!
|
||||||
|
|
||||||
|
- Task Dependencies
|
||||||
|
- Task Templates
|
||||||
- Task Ownership
|
- Task Ownership
|
||||||
- Combined formatting and recursion specifiers
|
- Combined formatting and recursion specifiers
|
||||||
+ progress count/percentage and recursive or not
|
+ progress count/percentage and recursive or not
|
||||||
|
|
|
@ -3,8 +3,9 @@ use log::info;
|
||||||
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
|
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
|
||||||
|
|
||||||
pub const TASK_KIND: u16 = 1621;
|
pub const TASK_KIND: u16 = 1621;
|
||||||
|
pub const PROCEDURE_KIND: u16 = 1639;
|
||||||
pub const TRACKING_KIND: u16 = 1650;
|
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:
|
pub const PROPERTY_COLUMNS: &str = "Available properties:
|
||||||
- `id`
|
- `id`
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -320,6 +320,28 @@ async fn main() {
|
||||||
tasks.undo();
|
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('?') => {
|
Some('?') => {
|
||||||
tasks.set_state_filter(arg.map(|s| s.to_string()));
|
tasks.set_state_filter(arg.map(|s| s.to_string()));
|
||||||
}
|
}
|
||||||
|
|
47
src/task.rs
47
src/task.rs
|
@ -1,4 +1,5 @@
|
||||||
use fmt::Display;
|
use fmt::Display;
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::{BTreeSet, HashSet};
|
use std::collections::{BTreeSet, HashSet};
|
||||||
use std::fmt;
|
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 nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, TagStandard, Timestamp};
|
||||||
|
|
||||||
use crate::helpers::some_non_empty;
|
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 {
|
pub(crate) struct Task {
|
||||||
|
/// Event that defines this task
|
||||||
pub(crate) event: Event,
|
pub(crate) event: Event,
|
||||||
pub(crate) children: HashSet<EventId>,
|
/// Cached sorted tags of the event with references remove - do not modify!
|
||||||
pub(crate) props: BTreeSet<Event>,
|
|
||||||
/// Cached sorted tags of the event
|
|
||||||
pub(crate) tags: Option<BTreeSet<Tag>>,
|
pub(crate) tags: Option<BTreeSet<Tag>>,
|
||||||
|
/// Parent task references derived from the event tags
|
||||||
parents: Vec<EventId>,
|
parents: Vec<EventId>,
|
||||||
|
|
||||||
|
/// Reference to children, populated dynamically
|
||||||
|
pub(crate) children: HashSet<EventId>,
|
||||||
|
/// Events belonging to this task, such as state updates and notes
|
||||||
|
pub(crate) props: BTreeSet<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd<Self> for Task {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.event.partial_cmp(&other.event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Task {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.event.cmp(&other.event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
pub(crate) fn new(event: Event) -> 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),
|
Some(TagStandard::Event { event_id, .. }) => return Left(event_id),
|
||||||
_ => Right(tag.clone()),
|
_ => Right(tag.clone()),
|
||||||
});
|
});
|
||||||
|
// Separate refs for dependencies
|
||||||
Task {
|
Task {
|
||||||
children: Default::default(),
|
children: Default::default(),
|
||||||
props: Default::default(),
|
props: Default::default(),
|
||||||
tags: Some(tags).filter(|t: &BTreeSet<Tag>| !t.is_empty()),
|
tags: Some(tags).filter(|t: &BTreeSet<Tag>| !t.is_empty()),
|
||||||
parents,
|
parents: refs,
|
||||||
event,
|
event,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,6 +136,7 @@ impl Task {
|
||||||
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
|
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
|
||||||
"tags" => self.filter_tags(|_| true),
|
"tags" => self.filter_tags(|_| true),
|
||||||
"alltags" => Some(format!("{:?}", self.tags)),
|
"alltags" => Some(format!("{:?}", self.tags)),
|
||||||
|
"parents" => Some(format!("{:?}", self.parents.iter().map(|id| id.to_string()).collect_vec())),
|
||||||
"props" => Some(format!(
|
"props" => Some(format!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
self.props
|
self.props
|
||||||
|
@ -172,18 +192,20 @@ impl Display for TaskState {
|
||||||
pub(crate) enum State {
|
pub(crate) enum State {
|
||||||
Closed,
|
Closed,
|
||||||
Open,
|
Open,
|
||||||
Active,
|
Procedure,
|
||||||
|
Pending,
|
||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
impl TryFrom<Kind> for State {
|
impl TryFrom<Kind> for State {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn try_from(value: Kind) -> Result<Self, Self::Error> {
|
fn try_from(value: Kind) -> Result<Self, Self::Error> {
|
||||||
match value.as_u32() {
|
match value.as_u16() {
|
||||||
1630 => Ok(State::Open),
|
1630 => Ok(State::Open),
|
||||||
1631 => Ok(State::Done),
|
1631 => Ok(State::Done),
|
||||||
1632 => Ok(State::Closed),
|
1632 => Ok(State::Closed),
|
||||||
1633 => Ok(State::Active),
|
1633 => Ok(State::Pending),
|
||||||
|
PROCEDURE_KIND => Ok(State::Procedure),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,7 +213,7 @@ impl TryFrom<Kind> for State {
|
||||||
impl State {
|
impl State {
|
||||||
pub(crate) fn is_open(&self) -> bool {
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
State::Open | State::Active => true,
|
State::Open | State::Procedure => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +223,8 @@ impl State {
|
||||||
State::Open => 1630,
|
State::Open => 1630,
|
||||||
State::Done => 1631,
|
State::Done => 1631,
|
||||||
State::Closed => 1632,
|
State::Closed => 1632,
|
||||||
State::Active => 1633,
|
State::Pending => 1633,
|
||||||
|
State::Procedure => PROCEDURE_KIND,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
src/tasks.rs
37
src/tasks.rs
|
@ -11,8 +11,9 @@ use chrono::LocalResult::Single;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
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::base64::write::StrConsumer;
|
||||||
|
use nostr_sdk::prelude::Marker;
|
||||||
use TagStandard::Hashtag;
|
use TagStandard::Hashtag;
|
||||||
|
|
||||||
use crate::{Events, EventSender};
|
use crate::{Events, EventSender};
|
||||||
|
@ -306,7 +307,7 @@ impl Tasks {
|
||||||
// TODO apply filters in transit
|
// TODO apply filters in transit
|
||||||
let state = t.pure_state();
|
let state = t.pure_state();
|
||||||
self.state.as_ref().map_or_else(|| {
|
self.state.as_ref().map_or_else(|| {
|
||||||
state == State::Open || (
|
state.is_open() || (
|
||||||
state == State::Done &&
|
state == State::Done &&
|
||||||
t.parent_id() != None
|
t.parent_id() != None
|
||||||
)
|
)
|
||||||
|
@ -382,13 +383,13 @@ impl Tasks {
|
||||||
.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)),
|
.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)),
|
||||||
"path" => self.get_task_path(Some(task.event.id)),
|
"path" => self.get_task_path(Some(task.event.id)),
|
||||||
"rpath" => self.relative_path(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())),
|
"time" => display_time("MMMm", self.time_tracked(*task.get_id())),
|
||||||
"rtime" => {
|
"rtime" => {
|
||||||
let time = self.total_time_tracked(*task.get_id());
|
let time = self.total_time_tracked(*task.get_id());
|
||||||
total_time += time;
|
total_time += time;
|
||||||
display_time("HH:MM", time)
|
display_time("HH:MM", time)
|
||||||
},
|
}
|
||||||
prop => task.get(prop).unwrap_or(String::new()),
|
prop => task.get(prop).unwrap_or(String::new()),
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
|
@ -540,7 +541,31 @@ impl Tasks {
|
||||||
|
|
||||||
/// Sanitizes input
|
/// Sanitizes input
|
||||||
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
||||||
let id = self.submit(self.parse_task(input.trim()));
|
let tag: Option<Tag> = 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));
|
self.state.clone().inspect(|s| self.set_state_for_with(id, s));
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
@ -604,7 +629,7 @@ impl Tasks {
|
||||||
t.children.insert(event.id);
|
t.children.insert(event.id);
|
||||||
});
|
});
|
||||||
if self.tasks.contains_key(&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 {
|
} else {
|
||||||
self.tasks.insert(event.id, Task::new(event));
|
self.tasks.insert(event.id, Task::new(event));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue