From 5c7793f4a3a22eff2fd73766c71e6adc7745e741 Mon Sep 17 00:00:00 2001 From: xeruf <27jf@pm.me> Date: Wed, 31 Jul 2024 20:05:52 +0300 Subject: [PATCH] feat: revamp time tracking with own kind --- src/main.rs | 16 +++--- src/task.rs | 25 --------- src/tasks.rs | 150 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 113 insertions(+), 78 deletions(-) diff --git a/src/main.rs b/src/main.rs index ba0e7c3..8b9c997 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,8 +19,8 @@ use crate::tasks::Tasks; mod task; mod tasks; -static TASK_KIND: u64 = 1621; -static TRACKING_KIND: u64 = 1650; +const TASK_KIND: u64 = 1621; +const TRACKING_KIND: u64 = 1650; #[derive(Debug, Clone)] struct EventSender { @@ -33,6 +33,9 @@ impl EventSender { or_print(self.tx.send(event.clone())); }) } + pub(crate) fn pubkey(&self) -> PublicKey { + self.keys.public_key() + } } fn or_print(result: Result) -> Option { @@ -366,14 +369,7 @@ async fn main() { } println!(); - // TODO optionally continue - tasks.update_state("", |t| { - if t.pure_state() == State::Active { - Some(State::Open) - } else { - None - } - }); + tasks.move_to(None); drop(tasks); info!("Submitting pending changes..."); diff --git a/src/task.rs b/src/task.rs index 4925a2c..bda60ce 100644 --- a/src/task.rs +++ b/src/task.rs @@ -102,28 +102,6 @@ impl Task { } } - /// Total time this task has been active. - /// TODO: Consider caching - pub(crate) fn time_tracked(&self) -> u64 { - let mut total = 0; - let mut start: Option = None; - for state in self.states() { - match state.state { - State::Active => start = start.or(Some(state.time)), - _ => { - if let Some(stamp) = start { - total += (state.time - stamp).as_u64(); - start = None; - } - } - } - } - if let Some(start) = start { - total += (Timestamp::now() - start).as_u64(); - } - total - } - fn filter_tags

(&self, predicate: P) -> Option where P: FnMut(&&Tag) -> bool, @@ -143,9 +121,6 @@ impl Task { "parentid" => self.parent_id().map(|i| i.to_string()), "state" => self.state().map(|s| s.to_string()), "name" => Some(self.event.content.clone()), - "time" => Some(self.time_tracked().div(60)) - .filter(|t| t > &0) - .map(|t| format!("{}m", t)), "desc" => self.descriptions().last().cloned(), "description" => Some(self.descriptions().join(" ")), "hashtags" => self.filter_tags(|tag| { diff --git a/src/tasks.rs b/src/tasks.rs index 95f8715..8ba302f 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,18 +1,17 @@ use std::collections::{BTreeSet, HashMap}; -use std::fmt::{Display, Formatter, write}; use std::io::{Error, stdout, Write}; -use std::iter::{once, Sum}; -use std::ops::Add; +use std::iter::once; +use std::ops::{Div, Rem}; use chrono::{Local, TimeZone}; 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, Tag}; +use nostr_sdk::{Event, EventBuilder, EventId, Kind, PublicKey, Tag, Timestamp}; use nostr_sdk::Tag::Hashtag; -use crate::{EventSender, TASK_KIND}; +use crate::{EventSender, TASK_KIND, TRACKING_KIND}; use crate::task::{State, Task}; type TaskMap = HashMap; @@ -20,6 +19,8 @@ type TaskMap = HashMap; pub(crate) struct Tasks { /// The Tasks tasks: TaskMap, + /// History of active tasks by PubKey + history: HashMap>, /// The task properties currently visible pub(crate) properties: Vec, /// Negative: Only Leaf nodes @@ -43,6 +44,7 @@ impl Tasks { pub(crate) fn from(sender: EventSender) -> Self { Tasks { tasks: Default::default(), + history: Default::default(), properties: vec![ "state".into(), "progress".into(), @@ -74,15 +76,74 @@ impl Tasks { self.position } - /// Total time this task and its subtasks have been active - fn total_time_tracked(&self, id: &EventId) -> u64 { - self.get_by_id(id).map_or(0, |t| { - t.time_tracked() - + t.children - .iter() - .map(|e| self.total_time_tracked(e)) - .sum::() - }) + /// Ids of all subtasks found for id, including itself + fn get_subtasks(&self, id: EventId) -> Vec { + let mut children = Vec::with_capacity(32); + let mut index = 0; + + children.push(id); + while index < children.len() { + self.tasks.get(&children[index]).map(|t| { + children.reserve(t.children.len()); + for child in t.children.iter() { + children.push(child.clone()); + } + }); + index += 1; + } + + children + } + + /// Total time tracked on this task by the current user. + pub(crate) fn time_tracked(&self, id: &EventId) -> u64 { + let mut total = 0; + let mut start: Option = None; + for event in self.history.get(&self.sender.pubkey()).into_iter().flatten() { + match event.tags.first() { + Some(Tag::Event { + event_id, + .. + }) if event_id == id => { + start = start.or(Some(event.created_at)) + } + _ => if let Some(stamp) = start { + total += (event.created_at - stamp).as_u64(); + } + } + } + if let Some(start) = start { + total += (Timestamp::now() - start).as_u64(); + } + total + } + + /// Total time tracked on this task and its subtasks by all users. + /// TODO needs testing! + fn total_time_tracked(&self, id: EventId) -> u64 { + let mut total = 0; + + let children = self.get_subtasks(id); + for user in self.history.values() { + let mut start: Option = None; + for event in user { + match event.tags.first() { + Some(Tag::Event { + event_id, + .. + }) if children.contains(event_id) => { + start = start.or(Some(event.created_at)) + } + _ => if let Some(stamp) = start { + total += (event.created_at - stamp).as_u64(); + } + } + } + if let Some(start) = start { + total += (Timestamp::now() - start).as_u64(); + } + } + total } fn total_progress(&self, id: &EventId) -> Option { @@ -244,7 +305,7 @@ impl Tasks { } _ => state.time.to_human_datetime(), }, - t.time_tracked() / 60 + self.time_tracked(t.get_id()) / 60 )?; } writeln!(lock, "{}", t.descriptions().join("\n"))?; @@ -278,14 +339,8 @@ impl Tasks { .map_or(String::new(), |p| format!("{:2}%", p * 100.0)), "path" => self.get_task_path(Some(task.event.id)), "rpath" => self.relative_path(task.event.id), - "rtime" => { - let time = self.total_time_tracked(&task.event.id); - if time > 60 { - format!("{:02}:{:02}", time / 3600, time / 60 % 60) - } else { - String::new() - } - } + "time" => display_time("MMMm", self.time_tracked(task.get_id())), + "rtime" => display_time("HH:MMm", self.total_time_tracked(*task.get_id())), prop => task.get(prop).unwrap_or(String::new()), }) .collect::>() @@ -322,21 +377,15 @@ impl Tasks { if id == self.position { return; } - // TODO: erases previous state comment - do not track active via state - self.update_state("", |s| { - if s.pure_state() == State::Active { - Some(State::Open) - } else { - None - } - }); self.position = id; - self.update_state("", |s| { - if s.pure_state() == State::Open { - Some(State::Active) - } else { - None - } + self.sender.submit( + EventBuilder::new( + Kind::from(TRACKING_KIND), + "", + id.iter().map(|id| Tag::event(id.clone())), + ) + ).map(|e| { + self.add(e); }); } @@ -373,10 +422,14 @@ impl Tasks { } pub(crate) fn add(&mut self, event: Event) { - if event.kind.as_u64() == 1621 { - self.add_task(event) - } else { - self.add_prop(&event) + match event.kind.as_u64() { + TASK_KIND => self.add_task(event), + TRACKING_KIND => + match self.history.get_mut(&event.pubkey) { + Some(c) => { c.insert(event); } + None => { self.history.insert(event.pubkey, BTreeSet::from([event])); } + }, + _ => self.add_prop(&event), } } @@ -391,7 +444,7 @@ impl Tasks { } } - pub(crate) fn add_prop(&mut self, event: &Event) { + fn add_prop(&mut self, event: &Event) { self.referenced_tasks(&event, |t| { t.props.insert(event.clone()); }); @@ -445,8 +498,18 @@ impl Tasks { } } +fn display_time(format: &str, secs: u64) -> String { + Some(secs / 60) + .filter(|t| t > &0) + .map_or(String::new(), |mins| format + .replace("HH", &format!("{:02}", mins.div(60))) + .replace("MM", &format!("{:02}", mins.rem(60))) + .replace("MMM", &format!("{:3}", mins)), + ) +} + pub(crate) fn join_tasks<'a>( - iter: impl Iterator, + iter: impl Iterator, include_last_id: bool, ) -> Option { let tasks: Vec<&Task> = iter.collect(); @@ -489,6 +552,7 @@ impl<'a> Iterator for ParentIterator<'a> { #[test] fn test_depth() { use std::sync::mpsc; + use nostr_sdk::Keys; let (tx, _rx) = mpsc::channel(); let mut tasks = Tasks::from(EventSender {