diff --git a/README.md b/README.md index afe81cf..5425dbf 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,6 @@ as you work. The currently active task is automatically time-tracked. To stop time-tracking completely, simply move to the root of all tasks. -Time-tracking is currently also stopped -when the application is terminated regularly. ## Reference @@ -100,7 +98,7 @@ when the application is terminated regularly. Dots can be repeated to move to parent tasks. - `:[IND][COL]` - add property column COL at IND or end, if it already exists remove property column COL or IND (1-indexed) -- `*[TIME]` - add timetracking with the specified offset +- `*[TIME]` - add timetracking with the specified offset (empty: list tracked times) - `>[TEXT]` - complete active task and move to parent, with optional state description - `<[TEXT]` - close active task and move to parent, with optional state description - `!TEXT` - set state for current task from text diff --git a/src/helpers.rs b/src/helpers.rs index e161e42..9d250b5 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -4,7 +4,7 @@ use std::io::{stdin, stdout, Write}; use log::{debug, error, info, trace, warn}; pub fn some_non_empty(str: &str) -> Option { - if str.is_empty() { None } else { Some(str.to_owned()) } + if str.is_empty() { None } else { Some(str.to_string()) } } pub fn or_print(result: Result) -> Option { diff --git a/src/main.rs b/src/main.rs index d37ce34..bdb186d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -367,8 +367,8 @@ async fn main() { } } None => { - // TODO time tracked list - // continue + println!("{}", tasks.times_tracked()); + continue } } diff --git a/src/tasks.rs b/src/tasks.rs index 13322d5..1e31b2f 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -2,18 +2,21 @@ use std::collections::{BTreeSet, HashMap}; use std::io::{Error, stdout, Write}; use std::iter::once; use std::ops::{Div, Rem}; +use std::str::FromStr; use std::sync::mpsc::Sender; use std::time::Duration; -use chrono::{DateTime, Local, TimeZone}; +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, PublicKey, Tag, TagStandard, Timestamp, Url}; +use nostr_sdk::base64::write::StrConsumer; use TagStandard::Hashtag; use crate::{Events, EventSender}; +use crate::helpers::some_non_empty; use crate::kinds::*; use crate::task::{State, Task, TaskState}; @@ -119,6 +122,47 @@ impl Tasks { TimesTracked::from(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![id]).sum::().as_secs() } + pub(crate) fn times_tracked(&self) -> String { + match self.get_position() { + None => { + let hist = self.history.get(&self.sender.pubkey()); + if let Some(set) = hist { + let mut full = String::with_capacity(set.len() * 40); + let mut last: Option = None; + full.push_str("Your Time Tracking History:\n"); + for event in set { + let new = some_non_empty(&event.tags.iter() + .filter_map(|t| t.content()) + .map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_title(&id))) + .join(" ")); + if new != last { + full.push_str(&format!("{} {}\n", event.created_at.to_human_datetime(), new.as_ref().unwrap_or(&"---".to_string()))); + last = new; + } + } + full + } else { + String::from("You have nothing tracked yet") + } + } + Some(id) => { + let vec = vec![id]; + let res = + once(format!("Times tracked on {}", self.get_task_title(&id))).chain( + self.history.iter().flat_map(|(key, set)| + timestamps(set.iter(), &vec) + .tuples::<(_, _)>() + .map(move |((start, _), (end, _))| { + format!("{} - {} by {}", start.to_human_datetime(), end.to_human_datetime(), key) + }) + ).sorted_unstable() + ).join("\n"); + drop(vec); + res + } + } + } + /// Total time in seconds tracked on this task and its subtasks by all users. fn total_time_tracked(&self, id: EventId) -> u64 { let mut total = 0; @@ -692,6 +736,18 @@ pub(crate) fn join_tasks<'a>( }) } +fn matching_tag_id<'a>(event: &'a Event, ids: &'a Vec) -> Option<&'a EventId> { + event.tags.iter().find_map(|tag| match tag.as_standardized() { + Some(TagStandard::Event { event_id, .. }) if ids.contains(event_id) => Some(event_id), + _ => None + }) +} + +fn timestamps<'a>(events: impl Iterator, ids: &'a Vec) -> impl Iterator)> { + events.map(|event| (&event.created_at, matching_tag_id(event, ids))) + .dedup_by(|(_, e1), (_, e2)| e1 == e2) + .skip_while(|element| element.1 == None) +} struct TimesTracked<'a> { events: Box + 'a>, @@ -712,19 +768,15 @@ impl Iterator for TimesTracked<'_> { fn next(&mut self) -> Option { let mut start: Option = None; while let Some(event) = self.events.next() { - match event.tags.first().and_then(|tag| tag.as_standardized()) { - Some(TagStandard::Event { - event_id, - .. - }) if self.ids.contains(event_id) => { - start = start.or(Some(event.created_at.as_u64())) - } - _ => if let Some(stamp) = start { - return Some(Duration::from_secs(event.created_at.as_u64() - stamp)) + if matching_tag_id(event, self.ids).is_some() { + start = start.or(Some(event.created_at.as_u64())) + } else { + if let Some(stamp) = start { + return Some(Duration::from_secs(event.created_at.as_u64() - stamp)); } } } - return start.map(|stamp| Duration::from_secs(Timestamp::now().as_u64() - stamp)) + return start.map(|stamp| Duration::from_secs(Timestamp::now().as_u64() - stamp)); } }