feat: list existing tracked times

This commit is contained in:
xeruf 2024-08-08 15:09:39 +03:00
parent 79b42b8df0
commit 43c62bf742
4 changed files with 67 additions and 17 deletions

View File

@ -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

View File

@ -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<String> {
if str.is_empty() { None } else { Some(str.to_owned()) }
if str.is_empty() { None } else { Some(str.to_string()) }
}
pub fn or_print<T, U: Display>(result: Result<T, U>) -> Option<T> {

View File

@ -367,8 +367,8 @@ async fn main() {
}
}
None => {
// TODO time tracked list
// continue
println!("{}", tasks.times_tracked());
continue
}
}

View File

@ -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::<Duration>().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<String> = 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<EventId>) -> 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<Item=&'a Event>, ids: &'a Vec<EventId>) -> impl Iterator<Item=(&Timestamp, Option<&EventId>)> {
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<dyn Iterator<Item=&'a Event> + 'a>,
@ -712,19 +768,15 @@ impl Iterator for TimesTracked<'_> {
fn next(&mut self) -> Option<Self::Item> {
let mut start: Option<u64> = 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) => {
if matching_tag_id(event, self.ids).is_some() {
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))
} 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));
}
}