Compare commits

...

6 commits

Author SHA1 Message Date
xeruf
516acadd4a fix: properly determine current task state 2024-09-23 01:20:24 +02:00
xeruf
945eb6906a feat: add ability to schedule any action 2024-09-22 20:05:05 +02:00
xeruf
34657540de refactor: remove Tasks to TasksRelay 2024-09-22 16:48:15 +02:00
xeruf
afe3fa8670 fix: remove cached task children list 2024-09-22 16:47:26 +02:00
xeruf
753afad2fd fix: obtain children on the fly 2024-09-22 16:24:07 +02:00
xeruf
7755967a7a feat: requeue events with missing references 2024-09-22 14:44:20 +02:00
5 changed files with 193 additions and 125 deletions

View file

@ -106,6 +106,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
- `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up - `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up
Dot or slash can be repeated to move to parent tasks before acting. Dot or slash can be repeated to move to parent tasks before acting.
Append `@TIME` to any task creation or change command to record the action with the given time.
- `:[IND][PROP]` - add property column PROP at IND or end, - `:[IND][PROP]` - add property column PROP at IND or end,
if it already exists remove property column PROP or IND; empty: list properties if it already exists remove property column PROP or IND; empty: list properties

View file

@ -11,6 +11,25 @@ pub fn some_non_empty(str: &str) -> Option<String> {
if str.is_empty() { None } else { Some(str.to_string()) } if str.is_empty() { None } else { Some(str.to_string()) }
} }
pub fn trim_start_count(str: &str, char: char) -> (&str, usize) {
let len = str.len();
let result = str.trim_start_matches(char);
let dots = len - result.len();
(result, dots)
}
pub trait ToTimestamp {
fn to_timestamp(&self) -> Timestamp;
}
impl<T: TimeZone> ToTimestamp for DateTime<T> {
fn to_timestamp(&self) -> Timestamp {
let stamp = self.to_utc().timestamp();
if let Some(t) = 0u64.checked_add_signed(stamp) {
Timestamp::from(t)
} else { Timestamp::zero() }
}
}
/// Parses the hour from a plain number in the String, /// Parses the hour from a plain number in the String,
/// with max of max_future hours into the future. /// with max of max_future hours into the future.
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> { pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
@ -57,7 +76,7 @@ pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
/// - Otherwise try to parse a relative date /// - Otherwise try to parse a relative date
pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> { pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
if let Some(num) = parse_hour(str, 6) { if let Some(num) = parse_hour(str, 6) {
return Some(Timestamp::from(num.to_utc().timestamp() as u64)); return Some(num.to_timestamp());
} }
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in "); let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
if let Ok(num) = stripped.parse::<i64>() { if let Ok(num) = stripped.parse::<i64>() {

View file

@ -30,7 +30,7 @@ use xdg::BaseDirectories;
use crate::helpers::*; use crate::helpers::*;
use crate::kinds::{BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND}; use crate::kinds::{BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND};
use crate::task::{State, MARKER_DEPENDS}; use crate::task::{State, MARKER_DEPENDS};
use crate::tasks::{PropertyCollection, StateFilter, Tasks}; use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
mod helpers; mod helpers;
mod task; mod task;
@ -251,8 +251,8 @@ async fn main() -> Result<()> {
let moved_metadata = metadata.clone(); let moved_metadata = metadata.clone();
let (tx, mut rx) = mpsc::channel::<MostrMessage>(64); let (tx, mut rx) = mpsc::channel::<MostrMessage>(64);
let tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys, metadata.clone()); let tasks_for_url = |url: Option<Url>| TasksRelay::from(url, &tx, &keys, metadata.clone());
let mut relays: HashMap<Option<Url>, Tasks> = let mut relays: HashMap<Option<Url>, TasksRelay> =
client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect(); client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect();
let sender = tokio::spawn(async move { let sender = tokio::spawn(async move {
@ -326,10 +326,6 @@ async fn main() -> Result<()> {
} }
loop { loop {
trace!("All Root Tasks:\n{}", relays.iter().map(|(url, tasks)|
format!("{}: [{}]",
url.as_ref().map(ToString::to_string).unwrap_or(LOCAL_RELAY_NAME.to_string()),
tasks.children_of(None).map(|id| tasks.get_task_title(id)).join("; "))).join("\n"));
println!(); println!();
let tasks = relays.get(&selected_relay).unwrap(); let tasks = relays.get(&selected_relay).unwrap();
let prompt = format!( let prompt = format!(
@ -361,36 +357,53 @@ async fn main() -> Result<()> {
} }
if count > 0 { if count > 0 {
info!("Received {count} Updates"); info!("Received {count} Updates");
} else {
relays.values_mut().for_each(|tasks| tasks.process_overflow());
} }
let mut iter = input.chars(); let tasks = relays.get_mut(&selected_relay).unwrap();
let op = iter.next();
let arg = if input.len() > 1 { let operator = input.chars().next();
Some(input[1..].trim()) let mut command = input;
match operator {
None => {
debug!("Flushing Tasks because of empty command");
tasks.flush();
or_warn!(tasks.print_tasks());
continue;
}
Some('@') => {}
Some(_) => {
if let Some((left, arg)) = command.split_once("@") {
if let Some(time) = parse_hour(arg, 20)
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) {
command = left.to_string();
tasks.custom_time = Some(time.to_timestamp());
}
}
}
}
let arg = if command.len() > 1 {
Some(command[1..].trim())
} else { } else {
None None
}; };
let arg_default = arg.unwrap_or(""); let arg_default = arg.unwrap_or("");
let tasks = relays.get_mut(&selected_relay).unwrap(); match operator {
match op {
None => {
debug!("Flushing Tasks because of empty command");
tasks.flush();
}
Some(':') => { Some(':') => {
let mut iter = arg_default.chars();
let next = iter.next(); let next = iter.next();
let remaining = iter.collect::<String>().trim().to_string();
if let Some(':') = next { if let Some(':') = next {
let str: String = iter.collect(); let props = remaining.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>();
let result = str.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>(); if props.len() == 1 {
if result.len() == 1 { tasks.add_sorting_property(remaining)
tasks.add_sorting_property(str.trim().to_string())
} else { } else {
tasks.set_sorting(result) tasks.set_sorting(props)
} }
} else if let Some(digit) = next.and_then(|s| s.to_digit(10)) { } else if let Some(digit) = next.and_then(|s| s.to_digit(10)) {
let index = (digit as usize).saturating_sub(1); let index = (digit as usize).saturating_sub(1);
let remaining = iter.collect::<String>().trim().to_string();
if remaining.is_empty() { if remaining.is_empty() {
tasks.get_columns().remove_at(index); tasks.get_columns().remove_at(index);
} else { } else {
@ -469,11 +482,7 @@ async fn main() -> Result<()> {
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) .or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
.map(|time| { .map(|time| {
info!("Filtering for tasks from {}", format_datetime_relative(time)); info!("Filtering for tasks from {}", format_datetime_relative(time));
let threshold = time.to_utc().timestamp(); tasks.set_filter_from(time.to_timestamp())
tasks.set_filter_from(
if let Some(t) = 0u64.checked_add_signed(threshold) {
Timestamp::from(t)
} else { Timestamp::zero() })
}) })
.unwrap_or(false) .unwrap_or(false)
} }
@ -609,59 +618,49 @@ async fn main() -> Result<()> {
} }
Some('.') => { Some('.') => {
let mut dots = 1; let (remaining, dots) = trim_start_count(&command, '.');
let mut pos = tasks.get_position_ref(); let pos = tasks.up_by(dots - 1);
for _ in iter.take_while(|c| c == &'.') {
dots += 1;
pos = tasks.get_parent(pos);
}
let slice = input[dots..].trim(); if remaining.is_empty() {
if slice.is_empty() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
if dots > 1 { if dots > 1 {
info!("Moving up {} tasks", dots - 1) info!("Moving up {} tasks", dots - 1)
} else { } else {
tasks.clear_filters(); tasks.clear_filters();
} }
} else if let Ok(depth) = slice.parse::<usize>() { } else if let Ok(depth) = remaining.parse::<usize>() {
if pos != tasks.get_position_ref() { if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
} }
tasks.set_depth(depth); tasks.set_depth(depth);
} else { } else {
tasks.filter_or_create(pos.cloned().as_ref(), slice).map(|id| tasks.move_to(Some(id))); tasks.filter_or_create(pos.cloned().as_ref(), &remaining).map(|id| tasks.move_to(Some(id)));
} }
} }
Some('/') => if arg.is_none() { Some('/') => if arg.is_none() {
tasks.move_to(None); tasks.move_to(None);
} else { } else {
let mut dots = 1; let (remaining, dots) = trim_start_count(&command, '/');
let mut pos = tasks.get_position_ref(); let pos = tasks.up_by(dots - 1);
for _ in iter.take_while(|c| c == &'/') {
dots += 1;
pos = tasks.get_parent(pos);
}
let slice = input[dots..].trim(); if remaining.is_empty() {
if slice.is_empty() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
if dots > 1 { if dots > 1 {
info!("Moving up {} tasks", dots - 1) info!("Moving up {} tasks", dots - 1)
} }
} else { } else {
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string()); let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
if !slice.chars().any(|c| c.is_ascii_uppercase()) { if !remaining.chars().any(|c| c.is_ascii_uppercase()) {
// Smart-case - case-sensitive if any uppercase char is entered // Smart-case - case-sensitive if any uppercase char is entered
transform = Box::new(|s| s.to_ascii_lowercase()); transform = Box::new(|s| s.to_ascii_lowercase());
} }
let filtered = let filtered =
tasks.get_filtered(|t| { tasks.get_filtered(|t| {
transform(&t.event.content).contains(slice) || transform(&t.event.content).contains(&remaining) ||
t.tags.iter().flatten().any( t.tags.iter().flatten().any(
|tag| tag.content().is_some_and(|s| transform(s).contains(slice))) |tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
}); });
if filtered.len() == 1 { if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().next()); tasks.move_to(filtered.into_iter().next());
@ -673,14 +672,14 @@ async fn main() -> Result<()> {
} }
_ => _ =>
if Regex::new("^wss?://").unwrap().is_match(input.trim()) { if Regex::new("^wss?://").unwrap().is_match(command.trim()) {
tasks.move_to(None); tasks.move_to(None);
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&input))) { if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&command))) {
selected_relay.clone_from(url); selected_relay.clone_from(url);
or_warn!(tasks.print_tasks()); or_warn!(tasks.print_tasks());
continue; continue;
} }
or_warn!(Url::parse(&input), "Failed to parse url {}", input).map(|url| { or_warn!(Url::parse(&command), "Failed to parse url {}", command).map(|url| {
match tx.try_send(MostrMessage::NewRelay(url.clone())) { match tx.try_send(MostrMessage::NewRelay(url.clone())) {
Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"), Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"),
Ok(_) => { Ok(_) => {
@ -691,16 +690,17 @@ async fn main() -> Result<()> {
} }
}); });
continue; continue;
} else if input.contains('\n') { } else if command.contains('\n') {
input.split('\n').for_each(|line| { command.split('\n').for_each(|line| {
if !line.trim().is_empty() { if !line.trim().is_empty() {
tasks.make_task(line); tasks.make_task(line);
} }
}); });
} else { } else {
tasks.filter_or_create(tasks.get_position().as_ref(), &input); tasks.filter_or_create(tasks.get_position().as_ref(), &command);
} }
} }
tasks.custom_time = None;
or_warn!(tasks.print_tasks()); or_warn!(tasks.print_tasks());
} }
Err(ReadlineError::Eof) => break, Err(ReadlineError::Eof) => break,

View file

@ -24,9 +24,6 @@ pub(crate) struct Task {
pub(crate) tags: Option<BTreeSet<Tag>>, pub(crate) tags: Option<BTreeSet<Tag>>,
/// Task references derived from the event tags /// Task references derived from the event tags
refs: Vec<(String, EventId)>, refs: Vec<(String, EventId)>,
/// Reference to children, populated dynamically
pub(crate) children: HashSet<EventId>,
/// Events belonging to this task, such as state updates and notes /// Events belonging to this task, such as state updates and notes
pub(crate) props: BTreeSet<Event>, pub(crate) props: BTreeSet<Event>,
} }
@ -52,7 +49,6 @@ impl Task {
}); });
// Separate refs for dependencies // Separate refs for dependencies
Task { Task {
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()),
refs, refs,
@ -94,12 +90,16 @@ impl Task {
self.description_events().map(|e| &e.content) self.description_events().map(|e| &e.content)
} }
pub(crate) fn is_task_kind(&self) -> bool {
self.event.kind == TASK_KIND
}
pub(crate) fn is_task(&self) -> bool { pub(crate) fn is_task(&self) -> bool {
self.event.kind == TASK_KIND || self.is_task_kind() ||
self.states().next().is_some() self.states().next().is_some()
} }
fn states(&self) -> impl Iterator<Item=TaskState> + '_ { fn states(&self) -> impl DoubleEndedIterator<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 {
name: some_non_empty(&event.content), name: some_non_empty(&event.content),
@ -114,7 +114,16 @@ impl Task {
} }
pub(crate) fn state(&self) -> Option<TaskState> { pub(crate) fn state(&self) -> Option<TaskState> {
self.states().last() let now = Timestamp::now();
// TODO do not iterate constructed state objects
let state = self.states().rev().take_while_inclusive(|ts| ts.time > now);
state.last().map(|ts| {
if ts.time <= now {
ts
} else {
self.default_state()
}
})
} }
pub(crate) fn pure_state(&self) -> State { pub(crate) fn pure_state(&self) -> State {

View file

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::{stdout, Error, Write}; use std::io::{stdout, Error, Write};
use std::iter::{empty, once, FusedIterator}; use std::iter::{empty, once, FusedIterator};
@ -6,26 +6,47 @@ use std::ops::{Div, Rem};
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
use crate::kinds::*;
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
use crate::{EventSender, MostrMessage};
use colored::Colorize; use colored::Colorize;
use itertools::{Either, Itertools}; use itertools::{Either, Itertools};
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use nostr_sdk::prelude::Marker; use nostr_sdk::prelude::Marker;
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url}; use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
use tokio::sync::mpsc::Sender;
use TagStandard::Hashtag; use TagStandard::Hashtag;
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
use crate::kinds::*;
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
use crate::{EventSender, MostrMessage};
const MAX_OFFSET: u64 = 9; const MAX_OFFSET: u64 = 9;
fn now() -> Timestamp { fn now() -> Timestamp {
Timestamp::now() + MAX_OFFSET Timestamp::now() + MAX_OFFSET
} }
type TaskMap = HashMap<EventId, Task>; type TaskMap = HashMap<EventId, Task>;
trait TaskMapMethods {
fn children_of<'a>(&'a self, task: &'a Task) -> impl Iterator<Item=&Task> + 'a;
fn children_for<'a>(&'a self, id: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a;
fn children_ids_for<'a>(&'a self, id: &'a EventId) -> impl Iterator<Item=&EventId> + 'a;
}
impl TaskMapMethods for TaskMap {
fn children_of<'a>(&'a self, task: &'a Task) -> impl Iterator<Item=&Task> + 'a {
self.children_for(Some(task.get_id()))
}
fn children_for<'a>(&'a self, id: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a {
self.values()
.filter(move |t| t.parent_id() == id)
}
fn children_ids_for<'a>(&'a self, id: &'a EventId) -> impl Iterator<Item=&EventId> + 'a {
self.children_for(Some(id))
.map(|t| t.get_id())
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Tasks { pub(crate) struct TasksRelay {
/// The Tasks /// The Tasks
tasks: TaskMap, tasks: TaskMap,
/// History of active tasks by PubKey /// History of active tasks by PubKey
@ -54,6 +75,8 @@ pub(crate) struct Tasks {
state: StateFilter, state: StateFilter,
sender: EventSender, sender: EventSender,
overflow: VecDeque<Event>,
pub(crate) custom_time: Option<Timestamp>,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -102,10 +125,10 @@ impl Display for StateFilter {
} }
} }
impl Tasks { impl TasksRelay {
pub(crate) fn from( pub(crate) fn from(
url: Option<Url>, url: Option<Url>,
tx: &tokio::sync::mpsc::Sender<MostrMessage>, tx: &Sender<MostrMessage>,
keys: &Keys, keys: &Keys,
metadata: Option<Metadata>, metadata: Option<Metadata>,
) -> Self { ) -> Self {
@ -115,7 +138,7 @@ impl Tasks {
} }
pub(crate) fn with_sender(sender: EventSender) -> Self { pub(crate) fn with_sender(sender: EventSender) -> Self {
Tasks { TasksRelay {
tasks: Default::default(), tasks: Default::default(),
history: Default::default(), history: Default::default(),
users: Default::default(), users: Default::default(),
@ -142,7 +165,25 @@ impl Tasks {
tags_excluded: Default::default(), tags_excluded: Default::default(),
state: Default::default(), state: Default::default(),
depth: 1, depth: 1,
sender, sender,
overflow: Default::default(),
custom_time: None,
}
}
pub(crate) fn process_overflow(&mut self) {
let elements = self.overflow.len();
for _ in 0..elements {
if let Some(event) = self.overflow.pop_front() {
if let Some(event) = self.add_prop(event) {
warn!("Unable to sort Event {:?}", event);
//self.overflow.push_back(event);
}
}
}
if elements > 0 {
info!("Reprocessed {elements} updates{}", self.sender.url.clone().map(|url| format!(" from {url}")).unwrap_or_default());
} }
} }
@ -247,13 +288,13 @@ impl Tasks {
} }
fn total_progress(&self, id: &EventId) -> Option<f32> { fn total_progress(&self, id: &EventId) -> Option<f32> {
self.get_by_id(id).and_then(|t| match t.pure_state() { self.get_by_id(id).and_then(|task| match task.pure_state() {
State::Closed => None, State::Closed => None,
State::Done => Some(1.0), State::Done => Some(1.0),
_ => { _ => {
let mut sum = 0f32; let mut sum = 0f32;
let mut count = 0; let mut count = 0;
for prog in t.children.iter().filter_map(|e| self.total_progress(e)) { for prog in self.tasks.children_ids_for(task.get_id()).filter_map(|e| self.total_progress(e)) {
sum += prog; sum += prog;
count += 1; count += 1;
} }
@ -270,6 +311,14 @@ impl Tasks {
// Parents // Parents
pub(crate) fn up_by(&self, count: usize) -> Option<&EventId> {
let mut pos = self.get_position_ref();
for _ in 0..count {
pos = self.get_parent(pos);
}
pos
}
pub(crate) fn get_parent(&self, id: Option<&EventId>) -> Option<&EventId> { pub(crate) fn get_parent(&self, id: Option<&EventId>) -> Option<&EventId> {
id.and_then(|id| self.get_by_id(id)) id.and_then(|id| self.get_by_id(id))
.and_then(|t| t.parent_id()) .and_then(|t| t.parent_id())
@ -296,7 +345,6 @@ impl Tasks {
ParentIterator { ParentIterator {
tasks: &self.tasks, tasks: &self.tasks,
current: id, current: id,
prev: None,
} }
} }
@ -312,7 +360,7 @@ impl Tasks {
fn resolve_tasks<'a>( fn resolve_tasks<'a>(
&'a self, &'a self,
iter: impl Iterator<Item=&'a EventId>, iter: impl Iterator<Item=&'a Task>,
sparse: bool, sparse: bool,
) -> Vec<&'a Task> { ) -> Vec<&'a Task> {
self.resolve_tasks_rec(iter, sparse, self.depth) self.resolve_tasks_rec(iter, sparse, self.depth)
@ -320,15 +368,14 @@ impl Tasks {
fn resolve_tasks_rec<'a>( fn resolve_tasks_rec<'a>(
&'a self, &'a self,
iter: impl Iterator<Item=&'a EventId>, iter: impl Iterator<Item=&'a Task>,
sparse: bool, sparse: bool,
depth: usize, depth: usize,
) -> Vec<&'a Task> { ) -> Vec<&'a Task> {
iter.filter_map(|id| self.get_by_id(id)) iter.flat_map(move |task| {
.flat_map(move |task| {
let new_depth = depth - 1; let new_depth = depth - 1;
if new_depth > 0 { if new_depth > 0 {
let mut children = self.resolve_tasks_rec(task.children.iter(), sparse, new_depth); let mut children = self.resolve_tasks_rec(self.tasks.children_of(&task), sparse, new_depth);
if !children.is_empty() { if !children.is_empty() {
if !sparse { if !sparse {
children.push(task); children.push(task);
@ -363,13 +410,6 @@ impl Tasks {
self.get_position_ref().and_then(|id| self.get_by_id(id)) self.get_position_ref().and_then(|id| self.get_by_id(id))
} }
pub(crate) fn children_of<'a>(&'a self, id: Option<&'a EventId>) -> impl Iterator<Item=&EventId> + 'a {
self.tasks
.values()
.filter(move |t| t.parent_id() == id)
.map(|t| t.get_id())
}
fn filter(&self, task: &Task) -> bool { fn filter(&self, task: &Task) -> bool {
self.state.matches(task) && self.state.matches(task) &&
task.tags.as_ref().map_or(true, |tags| { task.tags.as_ref().map_or(true, |tags| {
@ -383,10 +423,10 @@ impl Tasks {
} }
pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>, sparse: bool) -> Vec<&'a Task> { pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>, sparse: bool) -> Vec<&'a Task> {
let mut current = self.resolve_tasks(self.children_of(position), sparse); let mut current = self.resolve_tasks(self.tasks.children_for(position), sparse);
if current.is_empty() { if current.is_empty() {
if !self.tags.is_empty() { if !self.tags.is_empty() {
let mut children = self.children_of(self.get_position_ref()).peekable(); let mut children = self.tasks.children_for(self.get_position_ref()).peekable();
if children.peek().is_some() { if children.peek().is_some() {
current = self.resolve_tasks_rec(children, true, 9); current = self.resolve_tasks_rec(children, true, 9);
if sparse { if sparse {
@ -492,15 +532,16 @@ impl Tasks {
} }
fn get_property(&self, task: &Task, str: &str) -> String { fn get_property(&self, task: &Task, str: &str) -> String {
let mut children = self.tasks.children_of(task).peekable();
let progress = let progress =
self.total_progress(task.get_id()) self.total_progress(task.get_id())
.filter(|_| !task.children.is_empty()); .filter(|_| children.peek().is_some());
let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)); let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0));
match str { match str {
"subtasks" => { "subtasks" => {
let mut total = 0; let mut total = 0;
let mut done = 0; let mut done = 0;
for subtask in task.children.iter().filter_map(|id| self.get_by_id(id)) { for subtask in children {
let state = subtask.pure_state(); let state = subtask.pure_state();
total += &(state != State::Closed).into(); total += &(state != State::Closed).into();
done += &(state == State::Done).into(); done += &(state == State::Done).into();
@ -566,6 +607,7 @@ impl Tasks {
} }
pub(crate) fn set_filter_from(&mut self, time: Timestamp) -> bool { pub(crate) fn set_filter_from(&mut self, time: Timestamp) -> bool {
// TODO filter at both ends
self.set_filter(|t| t.last_state_update() > time) self.set_filter(|t| t.last_state_update() > time)
} }
@ -798,10 +840,9 @@ impl Tasks {
let mut tags = Vec::with_capacity(2); let mut tags = Vec::with_capacity(2);
tags.push(self.make_event_tag_from_id(*pos, MARKER_PARENT)); tags.push(self.make_event_tag_from_id(*pos, MARKER_PARENT));
self.get_by_id(pos) self.get_by_id(pos)
.map(|t| { .map(|task| {
if t.pure_state() == State::Procedure { if task.pure_state() == State::Procedure {
t.children.iter() self.tasks.children_of(task)
.filter_map(|id| self.get_by_id(id))
.max() .max()
.map(|t| tags.push(self.make_event_tag(&t.event, MARKER_DEPENDS))); .map(|t| tags.push(self.make_event_tag(&t.event, MARKER_DEPENDS)));
} }
@ -886,7 +927,10 @@ impl Tasks {
} }
/// Sign and queue the event to the relay, returning its id /// Sign and queue the event to the relay, returning its id
fn submit(&mut self, builder: EventBuilder) -> EventId { fn submit(&mut self, mut builder: EventBuilder) -> EventId {
if let Some(stamp) = self.custom_time {
builder = builder.custom_created_at(stamp);
}
let event = self.sender.submit(builder).unwrap(); let event = self.sender.submit(builder).unwrap();
let id = event.id; let id = event.id;
self.add(event); self.add(event);
@ -913,7 +957,10 @@ impl Tasks {
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); } None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
} }
} else { } else {
self.add_prop(event) if let Some(event) = self.add_prop(event) {
debug!("Requeueing unknown Event {:?}", event);
self.overflow.push_back(event);
}
} }
} }
} }
@ -925,24 +972,23 @@ impl Tasks {
} else { } else {
let id = event.id; let id = event.id;
let task = Task::new(event); let task = Task::new(event);
task.find_refs(MARKER_PARENT).for_each(|parent| {
self.tasks.get_mut(parent).map(|t| { t.children.insert(id); });
});
self.tasks.insert(id, task); self.tasks.insert(id, task);
} }
} }
fn add_prop(&mut self, event: Event) { /// Add event as prop, returning it if not processable
fn add_prop(&mut self, event: Event) -> Option<Event> {
let found = 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 !found {
if event.kind.as_u16() == 1 { if event.kind.as_u16() == 1 {
self.add_task(event); self.add_task(event);
return; } else {
return Some(event)
} }
warn!("Unknown event {:?}", event)
} }
None
} }
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> { fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {
@ -1246,7 +1292,7 @@ impl<'a> ChildIterator<'a> {
} }
} }
fn from(tasks: &'a Tasks, id: &'a EventId) -> Self { fn from(tasks: &'a TasksRelay, id: &'a EventId) -> Self {
let mut queue = Vec::with_capacity(30); let mut queue = Vec::with_capacity(30);
queue.push(id); queue.push(id);
ChildIterator { ChildIterator {
@ -1330,8 +1376,7 @@ impl<'a> ChildIterator<'a> {
if let Some(task) = self.tasks.get(id) { if let Some(task) = self.tasks.get(id) {
let take = filter(task); let take = filter(task);
if take.takes_children() { if take.takes_children() {
self.queue.reserve(task.children.len()); self.queue_children_of(&task);
self.queue.extend(task.children.iter());
} }
if take.takes_self() { if take.takes_self() {
self.check_depth(); self.check_depth();
@ -1342,6 +1387,10 @@ impl<'a> ChildIterator<'a> {
self.next_filtered(filter) self.next_filtered(filter)
}) })
} }
fn queue_children_of(&mut self, task: &'a Task) {
self.queue.extend(self.tasks.children_ids_for(task.get_id()));
}
} }
impl FusedIterator for ChildIterator<'_> {} impl FusedIterator for ChildIterator<'_> {}
impl<'a> Iterator for ChildIterator<'a> { impl<'a> Iterator for ChildIterator<'a> {
@ -1359,8 +1408,7 @@ impl<'a> Iterator for ChildIterator<'a> {
} }
} }
Some(task) => { Some(task) => {
self.queue.reserve(task.children.len()); self.queue_children_of(&task);
self.queue.extend(task.children.iter());
} }
} }
self.check_depth(); self.check_depth();
@ -1372,23 +1420,12 @@ impl<'a> Iterator for ChildIterator<'a> {
struct ParentIterator<'a> { struct ParentIterator<'a> {
tasks: &'a TaskMap, tasks: &'a TaskMap,
current: Option<EventId>, current: Option<EventId>,
/// Inexpensive helper to assert correctness
prev: Option<EventId>,
} }
impl<'a> Iterator for ParentIterator<'a> { impl<'a> Iterator for ParentIterator<'a> {
type Item = &'a Task; type Item = &'a Task;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.current.and_then(|id| self.tasks.get(&id)).map(|t| { self.current.and_then(|id| self.tasks.get(&id)).map(|t| {
self.prev.inspect(|id| {
// Fails if child is discovered before parent
// Need to reverse add as well
//assert!(t.children.contains(id))
if !t.children.contains(id) {
warn!("\"{}\" is missing child \"{}\"", t.get_title(), self.tasks.get(id).map_or(id.to_string(), |cht| cht.get_title()))
}
});
self.prev = self.current;
self.current = t.parent_id().cloned(); self.current = t.parent_id().cloned();
t t
}) })
@ -1401,12 +1438,12 @@ mod tasks_test {
use super::*; use super::*;
fn stub_tasks() -> Tasks { fn stub_tasks() -> TasksRelay {
use tokio::sync::mpsc; use tokio::sync::mpsc;
use nostr_sdk::Keys; use nostr_sdk::Keys;
let (tx, _rx) = mpsc::channel(16); let (tx, _rx) = mpsc::channel(16);
Tasks::with_sender(EventSender { TasksRelay::with_sender(EventSender {
url: None, url: None,
tx, tx,
keys: Keys::generate(), keys: Keys::generate(),
@ -1472,9 +1509,11 @@ mod tasks_test {
tasks.make_task_and_enter("proc: tags", State::Procedure); tasks.make_task_and_enter("proc: tags", State::Procedure);
assert_eq!(tasks.get_own_events_history().count(), 1); assert_eq!(tasks.get_own_events_history().count(), 1);
let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)], None)); 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.visible_tasks(),
Vec::<&Task>::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.visible_tasks().iter().map(|t| t.event.id).collect_vec(),
Vec::from([sub_id]));
assert_eq!(tasks.len(), 3); assert_eq!(tasks.len(), 3);
let sub = tasks.get_by_id(&sub_id).unwrap(); let sub = tasks.get_by_id(&sub_id).unwrap();
assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new()); assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new());