Compare commits

..

10 Commits

3 changed files with 214 additions and 140 deletions

View File

@ -30,6 +30,33 @@ pub fn prompt(prompt: &str) -> Option<String> {
} }
} }
pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
if let Ok(num) = stripped.parse::<i64>() {
return Some(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num * 60)));
}
// Using two libraries for better exhaustiveness, see https://github.com/uutils/parse_datetime/issues/84
match interim::parse_date_string(stripped, Local::now(), interim::Dialect::Us) {
Ok(date) => Some(date.to_utc()),
Err(e) => {
match parse_datetime::parse_datetime_at_date(Local::now(), stripped) {
Ok(date) => Some(date.to_utc()),
Err(_) => {
warn!("Could not parse time from {str}: {e}");
None
}
}
}
}.and_then(|time| {
if time.timestamp() > 0 {
Some(Timestamp::from(time.timestamp() as u64))
} else {
warn!("Can only track times after 1970!");
None
}
})
}
// For use in format strings but not possible, so need global find-replace // For use in format strings but not possible, so need global find-replace
pub const MAX_TIMESTAMP_WIDTH: u8 = 15; pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
/// Format nostr Timestamp relative to local time /// Format nostr Timestamp relative to local time

View File

@ -377,7 +377,7 @@ async fn main() {
let author = arg.and_then(|a| PublicKey::from_str(a).ok()).unwrap_or_else(|| keys.public_key()); let author = arg.and_then(|a| PublicKey::from_str(a).ok()).unwrap_or_else(|| keys.public_key());
info!("Filtering for events by {author}"); info!("Filtering for events by {author}");
tasks.set_filter( tasks.set_filter(
tasks.filtered_tasks(tasks.get_position()) tasks.filtered_tasks(tasks.get_position_ref())
.filter(|t| t.event.pubkey == author) .filter(|t| t.event.pubkey == author)
.map(|t| t.event.id) .map(|t| t.event.id)
.collect() .collect()
@ -455,11 +455,14 @@ async fn main() {
Some('(') => { Some('(') => {
if let Some(arg) = arg { if let Some(arg) = arg {
if !tasks.track_from(arg) { if tasks.track_from(arg) {
continue; let (label, times) = tasks.times_tracked();
println!("{}\n{}", label.italic(), times.rev().take(15).join("\n"));
} }
} else {
let (label, mut times) = tasks.times_tracked();
println!("{}\n{}", label.italic(), times.join("\n"));
} }
println!("{}", tasks.times_tracked());
continue; continue;
} }
@ -474,42 +477,46 @@ async fn main() {
Some('.') => { Some('.') => {
let mut dots = 1; let mut dots = 1;
let mut pos = tasks.get_position(); let mut pos = tasks.get_position_ref();
for _ in iter.take_while(|c| c == &'.') { for _ in iter.take_while(|c| c == &'.') {
dots += 1; dots += 1;
pos = tasks.get_parent(pos).cloned(); pos = tasks.get_parent(pos);
} }
let slice = input[dots..].trim(); let slice = input[dots..].trim();
if pos != tasks.get_position() || slice.is_empty() {
tasks.move_to(pos);
}
if slice.is_empty() { if slice.is_empty() {
tasks.move_to(pos.cloned());
if dots > 1 { if dots > 1 {
info!("Moving up {} tasks", dots - 1) info!("Moving up {} tasks", dots - 1)
} }
} else if let Ok(depth) = slice.parse::<i8>() { } else if let Ok(depth) = slice.parse::<i8>() {
if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned());
}
tasks.set_depth(depth); tasks.set_depth(depth);
} else { } else {
tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id))); tasks.filter_or_create(pos.cloned().as_ref(), slice).map(|id| tasks.move_to(Some(id)));
} }
} }
Some('/') => { Some('/') => {
let mut dots = 1; let mut dots = 1;
let mut pos = tasks.get_position(); let mut pos = tasks.get_position_ref();
for _ in iter.take_while(|c| c == &'/') { for _ in iter.take_while(|c| c == &'/') {
dots += 1; dots += 1;
pos = tasks.get_parent(pos).cloned(); pos = tasks.get_parent(pos);
} }
let slice = input[dots..].trim(); let slice = input[dots..].trim();
if slice.is_empty() { if slice.is_empty() {
tasks.move_to(pos); tasks.move_to(pos.cloned());
if dots > 1 { if dots > 1 {
info!("Moving up {} tasks", dots - 1) info!("Moving up {} tasks", dots - 1)
} }
} else if let Ok(depth) = slice.parse::<i8>() { } else if let Ok(depth) = slice.parse::<i8>() {
if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned());
}
tasks.set_depth(depth); tasks.set_depth(depth);
} 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());
@ -529,7 +536,7 @@ async fn main() {
if filtered.len() == 1 { if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().nth(0)); tasks.move_to(filtered.into_iter().nth(0));
} else { } else {
tasks.move_to(pos); tasks.move_to(pos.cloned());
tasks.set_filter(filtered); tasks.set_filter(filtered);
} }
} }
@ -556,7 +563,7 @@ async fn main() {
} }
continue; continue;
} else { } else {
tasks.filter_or_create(&input); tasks.filter_or_create(tasks.get_position().as_ref(), &input);
} }
} }
or_print(tasks.print_tasks()); or_print(tasks.print_tasks());

View File

@ -16,7 +16,7 @@ use nostr_sdk::prelude::Marker;
use TagStandard::Hashtag; use TagStandard::Hashtag;
use crate::{EventSender, MostrMessage}; use crate::{EventSender, MostrMessage};
use crate::helpers::{format_stamp, local_datetimestamp, relative_datetimestamp, some_non_empty}; use crate::helpers::{format_stamp, local_datetimestamp, parse_tracking_stamp, relative_datetimestamp, some_non_empty};
use crate::kinds::*; use crate::kinds::*;
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState}; use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
@ -35,11 +35,8 @@ pub(crate) struct Tasks {
/// The task properties sorted by /// The task properties sorted by
sorting: VecDeque<String>, sorting: VecDeque<String>,
/// Currently active task
position: Option<EventId>,
/// A filtered view of the current tasks /// A filtered view of the current tasks
view: Vec<EventId>, view: Vec<EventId>,
/// Negative: Only Leaf nodes
/// Zero: Only Active node /// Zero: Only Active node
/// Positive: Go down the respective level /// Positive: Go down the respective level
depth: i8, depth: i8,
@ -135,7 +132,6 @@ impl Tasks {
"rtime".into(), "rtime".into(),
"name".into(), "name".into(),
]), ]),
position: None, // TODO persist position
view: Default::default(), view: Default::default(),
tags: Default::default(), tags: Default::default(),
tags_excluded: Default::default(), tags_excluded: Default::default(),
@ -150,15 +146,22 @@ impl Tasks {
#[inline] #[inline]
pub(crate) fn get_by_id(&self, id: &EventId) -> Option<&Task> { self.tasks.get(id) } pub(crate) fn get_by_id(&self, id: &EventId) -> Option<&Task> { self.tasks.get(id) }
#[inline]
pub(crate) fn get_position(&self) -> Option<EventId> { self.position }
#[inline] #[inline]
pub(crate) fn len(&self) -> usize { self.tasks.len() } pub(crate) fn len(&self) -> usize { self.tasks.len() }
pub(crate) fn get_position(&self) -> Option<EventId> {
self.get_position_ref().cloned()
}
pub(crate) fn get_position_ref(&self) -> Option<&EventId> {
self.history_until(Timestamp::from(Timestamp::now() + Self::MAX_OFFSET))
.last()
.and_then(|e| referenced_events(e))
}
/// Ids of all subtasks recursively found for id, including itself /// Ids of all subtasks recursively found for id, including itself
pub(crate) fn get_task_tree<'a>(&'a self, id: &'a EventId) -> ChildIterator { pub(crate) fn get_task_tree<'a>(&'a self, id: &'a EventId) -> ChildIterator {
ChildIterator::from(&self.tasks, id) ChildIterator::from(self, id)
} }
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> { pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
@ -172,32 +175,31 @@ impl Tasks {
} }
/// Dynamic time tracking overview for current task or current user. /// Dynamic time tracking overview for current task or current user.
pub(crate) fn times_tracked(&self) -> String { pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String>>) {
match self.get_position() { match self.get_position_ref() {
None => { None => {
let hist = self.history.get(&self.sender.pubkey()); if let Some(set) = self.history.get(&self.sender.pubkey()) {
if let Some(set) = hist { let mut last = None;
let mut full = String::with_capacity(set.len() * 40); let mut full = Vec::with_capacity(set.len());
let mut last: Option<String> = None;
full.push_str("Your Time Tracking History:\n");
for event in set { for event in set {
let new = some_non_empty(&event.tags.iter() let new = some_non_empty(&event.tags.iter()
.filter_map(|t| t.content()) .filter_map(|t| t.content())
.map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_title(&id))) .map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_title(&id)))
.join(" ")); .join(" "));
if new != last { if new != last {
full.push_str(&format!("{:>15} {}\n", relative_datetimestamp(&event.created_at), new.as_ref().unwrap_or(&"---".to_string()))); // TODO alternate color with grey between days
full.push(format!("{:>15} {}", relative_datetimestamp(&event.created_at), new.as_ref().unwrap_or(&"---".to_string())));
last = new; last = new;
} }
} }
full ("Your Time-Tracking History:".to_string(), Box::from(full.into_iter()))
} else { } else {
String::from("You have nothing tracked yet") ("You have nothing tracked yet".to_string(), Box::from(empty()))
} }
} }
Some(id) => { Some(id) => {
let ids = vec![&id]; let ids = vec![id];
once(format!("Times tracked on {}", self.get_task_title(&id))).chain( let history =
self.history.iter().flat_map(|(key, set)| { self.history.iter().flat_map(|(key, set)| {
let mut vec = Vec::with_capacity(set.len() / 2); let mut vec = Vec::with_capacity(set.len() / 2);
let mut iter = timestamps(set.iter(), &ids).tuples(); let mut iter = timestamps(set.iter(), &ids).tuples();
@ -205,7 +207,7 @@ impl Tasks {
vec.push(format!("{} - {} by {}", vec.push(format!("{} - {} by {}",
local_datetimestamp(start), local_datetimestamp(start),
// Only use full stamp when ambiguous (>1day) // Only use full stamp when ambiguous (>1day)
if end.as_u64() - start.as_u64() > 86400 { if end.as_u64() - start.as_u64() > 80_000 {
local_datetimestamp(end) local_datetimestamp(end)
} else { } else {
format_stamp(end, "%H:%M") format_stamp(end, "%H:%M")
@ -216,15 +218,15 @@ impl Tasks {
.for_each(|(stamp, _)| .for_each(|(stamp, _)|
vec.push(format!("{} started by {}", local_datetimestamp(stamp), key))); vec.push(format!("{} started by {}", local_datetimestamp(stamp), key)));
vec vec
}).sorted_unstable() // TODO sorting depends on timestamp format - needed to interleave different people }).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people
).join("\n") (format!("Times Tracked on {:?}", self.get_task_title(&id)), Box::from(history))
} }
} }
} }
/// Total time in seconds tracked on this task by the current user. /// Total time in seconds tracked on this task by the current user.
pub(crate) fn time_tracked(&self, id: EventId) -> u64 { pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
TimesTracked::from(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![&id]).sum::<Duration>().as_secs() Durations::from(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![&id]).sum::<Duration>().as_secs()
} }
@ -234,7 +236,7 @@ impl Tasks {
let children = self.get_task_tree(&id).get_all(); let children = self.get_task_tree(&id).get_all();
for user in self.history.values() { for user in self.history.values() {
total += TimesTracked::from(user, &children).into_iter().sum::<Duration>().as_secs(); total += Durations::from(user, &children).sum::<Duration>().as_secs();
} }
total total
} }
@ -263,8 +265,8 @@ impl Tasks {
// Parents // Parents
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())
} }
@ -295,7 +297,7 @@ impl Tasks {
fn relative_path(&self, id: EventId) -> String { fn relative_path(&self, id: EventId) -> String {
join_tasks( join_tasks(
self.traverse_up_from(Some(id)) self.traverse_up_from(Some(id))
.take_while(|t| Some(t.event.id) != self.position), .take_while(|t| Some(&t.event.id) != self.get_position_ref()),
false, false,
).unwrap_or(id.to_string()) ).unwrap_or(id.to_string())
} }
@ -352,20 +354,20 @@ impl Tasks {
#[inline] #[inline]
pub(crate) fn get_current_task(&self) -> Option<&Task> { pub(crate) fn get_current_task(&self) -> Option<&Task> {
self.position.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(&self, id: Option<EventId>) -> impl Iterator<Item=&EventId> + '_ { pub(crate) fn children_of<'a>(&'a self, id: Option<&'a EventId>) -> impl Iterator<Item=&EventId> + 'a {
self.tasks self.tasks
.values() .values()
.filter(move |t| t.parent_id() == id.as_ref()) .filter(move |t| t.parent_id() == id)
.map(|t| t.get_id()) .map(|t| t.get_id())
} }
pub(crate) fn filtered_tasks(&self, position: Option<EventId>) -> impl Iterator<Item=&Task> { pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a {
// TODO use ChildIterator // TODO use ChildIterator
self.resolve_tasks(self.children_of(position)) self.resolve_tasks(self.children_of(position))
.filter(|t| { .filter(move |t| {
// TODO apply filters in transit // TODO apply filters in transit
self.state.matches(t) && self.state.matches(t) &&
t.tags.as_ref().map_or(true, |tags| { t.tags.as_ref().map_or(true, |tags| {
@ -381,12 +383,12 @@ impl Tasks {
pub(crate) fn visible_tasks(&self) -> Vec<&Task> { pub(crate) fn visible_tasks(&self) -> Vec<&Task> {
if self.depth == 0 { if self.depth == 0 {
return self.get_current_task().into_iter().collect(); return vec![];
} }
if self.view.len() > 0 { if self.view.len() > 0 {
return self.resolve_tasks(self.view.iter()).collect(); return self.resolve_tasks(self.view.iter()).collect();
} }
self.filtered_tasks(self.position).collect() self.filtered_tasks(self.get_position_ref()).collect()
} }
pub(crate) fn print_tasks(&self) -> Result<(), Error> { pub(crate) fn print_tasks(&self) -> Result<(), Error> {
@ -413,11 +415,21 @@ impl Tasks {
)?; )?;
writeln!(lock, "{}", t.descriptions().join("\n"))?; writeln!(lock, "{}", t.descriptions().join("\n"))?;
} }
let mut tasks = self.visible_tasks();
if tasks.is_empty() {
let (label, times) = self.times_tracked();
let mut times_recent = times.rev().take(6).collect_vec();
times_recent.reverse();
// TODO Add recent prefix
writeln!(lock, "{}\n{}", label.italic(), times_recent.join("\n"))?;
return Ok(());
}
// TODO proper column alignment // TODO proper column alignment
// TODO hide empty columns // TODO hide empty columns
writeln!(lock, "{}", self.properties.join("\t").bold())?; writeln!(lock, "{}", self.properties.join("\t").bold())?;
let mut total_time = 0; let mut total_time = 0;
let mut tasks = self.visible_tasks();
let count = tasks.len(); let count = tasks.len();
tasks.sort_by_cached_key(|task| { tasks.sort_by_cached_key(|task| {
self.sorting self.sorting
@ -433,7 +445,7 @@ impl Tasks {
.map(|p| self.get_property(task, p.as_str())) .map(|p| self.get_property(task, p.as_str()))
.join(" \t") .join(" \t")
)?; )?;
if self.depth < 2 || task.parent_id() == self.position.as_ref() { if self.depth < 2 || task.parent_id() == self.get_position_ref() {
total_time += self.total_time_tracked(task.event.id) total_time += self.total_time_tracked(task.event.id)
} }
} }
@ -550,14 +562,14 @@ impl Tasks {
} }
/// Returns ids of tasks starting with the given string. /// Returns ids of tasks starting with the given string.
pub(crate) fn get_filtered(&self, arg: &str) -> Vec<EventId> { pub(crate) fn get_filtered(&self, position: Option<&EventId>, arg: &str) -> Vec<EventId> {
if let Ok(id) = EventId::parse(arg) { if let Ok(id) = EventId::parse(arg) {
return vec![id]; return vec![id];
} }
let mut filtered: Vec<EventId> = Vec::with_capacity(32); let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let lowercase_arg = arg.to_ascii_lowercase(); let lowercase_arg = arg.to_ascii_lowercase();
let mut filtered_more: Vec<EventId> = Vec::with_capacity(32); let mut filtered_more: Vec<EventId> = Vec::with_capacity(32);
for task in self.filtered_tasks(self.position) { for task in self.filtered_tasks(position) {
let lowercase = task.event.content.to_ascii_lowercase(); let lowercase = task.event.content.to_ascii_lowercase();
if lowercase == lowercase_arg { if lowercase == lowercase_arg {
return vec![task.event.id]; return vec![task.event.id];
@ -575,8 +587,8 @@ impl Tasks {
/// Finds out what to do with the given string. /// Finds out what to do with the given string.
/// Returns an EventId if a new Task was created. /// Returns an EventId if a new Task was created.
pub(crate) fn filter_or_create(&mut self, arg: &str) -> Option<EventId> { pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
let filtered = self.get_filtered(arg); let filtered = self.get_filtered(position, arg);
match filtered.len() { match filtered.len() {
0 => { 0 => {
// No match, new task // No match, new task
@ -595,25 +607,45 @@ impl Tasks {
} }
_ => { _ => {
// Multiple match, filter // Multiple match, filter
self.move_to(position.cloned());
self.set_filter(filtered); self.set_filter(filtered);
None None
} }
} }
} }
pub(crate) fn move_to(&mut self, id: Option<EventId>) { /// Returns all recent events from history until the first event at or before the given timestamp.
fn history_until(&self, stamp: Timestamp) -> impl Iterator<Item=&Event> {
self.history.get(&self.sender.pubkey()).map(|hist| {
hist.iter().rev().take_while_inclusive(move |e| e.created_at > stamp)
}).into_iter().flatten()
}
const MAX_OFFSET: u64 = 9;
pub(crate) fn move_to(&mut self, target: Option<EventId>) {
self.view.clear(); self.view.clear();
if id == self.position { let pos = self.get_position_ref();
if target.as_ref() == pos {
debug!("Flushing Tasks because of move in place"); debug!("Flushing Tasks because of move in place");
self.flush(); self.flush();
return; return;
} }
self.submit(build_tracking(id));
if !id.and_then(|id| self.tasks.get(&id)).is_some_and(|t| t.parent_id() == self.position.as_ref()) { if !target.and_then(|id| self.tasks.get(&id)).is_some_and(|t| t.parent_id() == pos) {
debug!("Flushing Tasks because of move beyond child"); debug!("Flushing Tasks because of move beyond child");
self.flush(); self.flush();
} }
self.position = id;
let now = Timestamp::now();
let offset: u64 = self.history_until(Timestamp::now()).skip_while(|e| e.created_at.as_u64() > now.as_u64() + Self::MAX_OFFSET).count() as u64;
if offset >= Self::MAX_OFFSET {
warn!("Whoa you are moving around quickly! Give me a few seconds to process.")
}
self.submit(
build_tracking(target)
.custom_created_at(Timestamp::from(now.as_u64() + offset))
);
} }
// Updates // Updates
@ -637,7 +669,7 @@ impl Tasks {
} }
pub(crate) fn parent_tag(&self) -> Option<Tag> { pub(crate) fn parent_tag(&self) -> Option<Tag> {
self.position.map(|p| self.make_event_tag_from_id(p, MARKER_PARENT)) self.get_position_ref().map(|p| self.make_event_tag_from_id(*p, MARKER_PARENT))
} }
pub(crate) fn position_tags(&self) -> Vec<Tag> { pub(crate) fn position_tags(&self) -> Vec<Tag> {
@ -661,10 +693,11 @@ impl Tasks {
self.make_task_with(input, empty(), true) self.make_task_with(input, empty(), true)
} }
pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) { pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) -> EventId {
let id = self.make_task_with(input, empty(), false); let id = self.make_task_with(input, empty(), false);
self.set_state_for(id, "", state); self.set_state_for(id, "", state);
self.move_to(Some(id)); self.move_to(Some(id));
id
} }
/// Creates a task with tags from filter and position /// Creates a task with tags from filter and position
@ -690,51 +723,17 @@ impl Tasks {
/// Parse string and set tracking /// Parse string and set tracking
/// Returns false and prints a message if parsing failed /// Returns false and prints a message if parsing failed
pub(crate) fn track_from(&mut self, str: &str) -> bool { pub(crate) fn track_from(&mut self, str: &str) -> bool {
// Using two libraries for better exhaustiveness, see https://github.com/uutils/parse_datetime/issues/84 parse_tracking_stamp(str)
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in "); .map(|stamp| self.track_at(stamp, self.get_position()))
if let Ok(num) = stripped.parse::<i64>() { .is_some()
self.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num * 60)));
return true;
}
match interim::parse_date_string(stripped, Local::now(), interim::Dialect::Us) {
Ok(date) => Some(date.to_utc()),
Err(e) => {
match parse_datetime::parse_datetime_at_date(Local::now(), stripped) {
Ok(date) => Some(date.to_utc()),
Err(_) => {
warn!("Could not parse time from {str}: {e}");
None
}
}
}
}.filter(|time| {
if time.timestamp() > 0 {
self.track_at(Timestamp::from(time.timestamp() as u64));
true
} else {
warn!("Can only track times after 1970!");
false
}
}).is_some()
} }
pub(crate) fn track_at(&mut self, time: Timestamp) -> EventId { pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId {
info!("{} from {}", self.position.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time)); info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time));
let pos = self.get_position(); self.submit(
let tracking = build_tracking(pos); build_tracking(task)
// TODO this can lead to funny deletions .custom_created_at(time)
self.get_own_history().map(|events| { )
if let Some(event) = events.pop_last() {
if event.kind.as_u16() == TRACKING_KIND &&
(pos == None && event.tags.is_empty()) ||
event.tags.iter().all(|t| t.content().map(|str| str.to_string()) == pos.map(|id| id.to_string())) {
// Replace last for easier calculation
} else {
events.insert(event);
}
}
});
self.submit(tracking.custom_created_at(time))
} }
/// Sign and queue the event to the relay, returning its id /// Sign and queue the event to the relay, returning its id
@ -802,13 +801,10 @@ impl Tasks {
} }
fn remove(&mut self, event: &Event) { fn remove(&mut self, event: &Event) {
if let Some(pos) = self.position {
if pos == event.id {
self.move_up()
}
}
self.tasks.remove(&event.id); self.tasks.remove(&event.id);
self.get_own_history().map(|t| t.remove(event)); self.get_own_history().map(
|t| t.retain(|e| e != event &&
!referenced_events(e).is_some_and(|id| id == &event.id)));
self.referenced_tasks(event, |t| { t.props.remove(event); }); self.referenced_tasks(event, |t| { t.props.remove(event); });
} }
@ -826,15 +822,15 @@ impl Tasks {
self.submit(prop) self.submit(prop)
} }
pub(crate) fn update_state(&mut self, comment: &str, state: State) { pub(crate) fn update_state(&mut self, comment: &str, state: State) -> Option<EventId> {
self.position let id = self.get_position_ref()?;
.map(|id| self.set_state_for(id, comment, state)); Some(self.set_state_for(id.clone(), comment, state))
} }
pub(crate) fn make_note(&mut self, note: &str) { pub(crate) fn make_note(&mut self, note: &str) {
if let Some(id) = self.position { if let Some(id) = self.get_position_ref() {
if self.get_by_id(&id).is_some_and(|t| t.is_task()) { if self.get_by_id(id).is_some_and(|t| t.is_task()) {
let prop = build_prop(Kind::TextNote, note.trim(), id); let prop = build_prop(Kind::TextNote, note.trim(), id.clone());
self.submit(prop); self.submit(prop);
return; return;
} }
@ -944,6 +940,13 @@ pub(crate) fn join_tasks<'a>(
}) })
} }
fn referenced_events(event: &Event) -> Option<&EventId> {
event.tags.iter().find_map(|tag| match tag.as_standardized() {
Some(TagStandard::Event { event_id, .. }) => Some(event_id),
_ => None
})
}
fn matching_tag_id<'a>(event: &'a Event, ids: &'a Vec<&'a EventId>) -> Option<&'a EventId> { fn matching_tag_id<'a>(event: &'a Event, ids: &'a Vec<&'a EventId>) -> Option<&'a EventId> {
event.tags.iter().find_map(|tag| match tag.as_standardized() { event.tags.iter().find_map(|tag| match tag.as_standardized() {
Some(TagStandard::Event { event_id, .. }) if ids.contains(&event_id) => Some(event_id), Some(TagStandard::Event { event_id, .. }) if ids.contains(&event_id) => Some(event_id),
@ -960,22 +963,21 @@ fn timestamps<'a>(events: impl Iterator<Item=&'a Event>, ids: &'a Vec<&'a EventI
/// Iterates Events to accumulate times tracked /// Iterates Events to accumulate times tracked
/// Expects a sorted iterator /// Expects a sorted iterator
struct TimesTracked<'a> { struct Durations<'a> {
events: Box<dyn Iterator<Item=&'a Event> + 'a>, events: Box<dyn Iterator<Item=&'a Event> + 'a>,
ids: &'a Vec<&'a EventId>, ids: &'a Vec<&'a EventId>,
threshold: Option<Timestamp>, threshold: Option<Timestamp>,
} }
impl TimesTracked<'_> { impl Durations<'_> {
fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<&EventId>) -> TimesTracked<'b> { fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<&EventId>) -> Durations<'b> {
TimesTracked { Durations {
events: Box::new(events.into_iter()), events: Box::new(events.into_iter()),
ids, ids,
threshold: Some(Timestamp::now()), threshold: Some(Timestamp::now()),
} }
} }
} }
impl Iterator for Durations<'_> {
impl Iterator for TimesTracked<'_> {
type Item = Duration; type Item = Duration;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
@ -1000,20 +1002,35 @@ impl Iterator for TimesTracked<'_> {
/// Breadth-First Iterator over Tasks and recursive children /// Breadth-First Iterator over Tasks and recursive children
struct ChildIterator<'a> { struct ChildIterator<'a> {
tasks: &'a TaskMap, tasks: &'a TaskMap,
/// Found Events
queue: Vec<&'a EventId>, queue: Vec<&'a EventId>,
/// Index of the next element in the queue
index: usize, index: usize,
/// Depth of the next element
depth: usize,
/// Element with the next depth boundary
next_depth_at: usize,
} }
impl<'a> ChildIterator<'a> { impl<'a> ChildIterator<'a> {
fn from(tasks: &'a TaskMap, id: &'a EventId) -> Self { fn from(tasks: &'a Tasks, 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 {
tasks, tasks: &tasks.tasks,
queue, queue,
index: 0, index: 0,
depth: 0,
next_depth_at: 1,
} }
} }
fn get_depth(mut self, depth: usize) -> Vec<&'a EventId> {
while self.depth < depth {
self.next();
}
self.queue
}
fn get_all(mut self) -> Vec<&'a EventId> { fn get_all(mut self) -> Vec<&'a EventId> {
while self.next().is_some() {} while self.next().is_some() {}
self.queue self.queue
@ -1031,7 +1048,7 @@ impl<'a> Iterator for ChildIterator<'a> {
self.queue.reserve(task.children.len()); self.queue.reserve(task.children.len());
self.queue.extend(task.children.iter()); self.queue.extend(task.children.iter());
} else { } else {
// Unknown task, can still find children // Unknown task, might still find children, just slower
for task in self.tasks.values() { for task in self.tasks.values() {
if task.parent_id().is_some_and(|i| i == id) { if task.parent_id().is_some_and(|i| i == id) {
self.queue.push(task.get_id()); self.queue.push(task.get_id());
@ -1039,6 +1056,10 @@ impl<'a> Iterator for ChildIterator<'a> {
} }
} }
self.index += 1; self.index += 1;
if self.next_depth_at == self.index {
self.depth += 1;
self.next_depth_at = self.queue.len();
}
Some(id) Some(id)
} }
} }
@ -1082,10 +1103,20 @@ mod tasks_test {
}) })
} }
macro_rules! assert_position {
($left:expr, $right:expr $(,)?) => {
assert_eq!($left.get_position_ref(), Some(&$right))
//assert_eq!($left, $right)
};
}
#[test] #[test]
fn test_procedures() { fn test_procedures() {
let mut tasks = stub_tasks(); let mut tasks = stub_tasks();
tasks.make_task_and_enter("proc: tags", State::Procedure); let id = tasks.make_task_and_enter("proc: tags", State::Procedure);
assert_position!(tasks, id);
assert_eq!(tasks.get_own_history().unwrap().len(), 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.get_current_task().unwrap().children, HashSet::<EventId>::new());
let sub_id = tasks.make_task("sub"); let sub_id = tasks.make_task("sub");
@ -1100,18 +1131,17 @@ mod tasks_test {
let mut tasks = stub_tasks(); let mut tasks = stub_tasks();
let zero = EventId::all_zeros(); let zero = EventId::all_zeros();
tasks.track_at(Timestamp::from(0)); tasks.track_at(Timestamp::from(0), None);
assert_eq!(tasks.history.len(), 1); assert_eq!(tasks.history.len(), 1);
tasks.move_to(Some(zero)); let almost_now: Timestamp = Timestamp::now() - 12u64;
let now: Timestamp = Timestamp::now() - 2u64; tasks.track_at(Timestamp::from(11), Some(zero));
tasks.track_at(Timestamp::from(1)); tasks.track_at(Timestamp::from(13), Some(zero));
assert!(tasks.time_tracked(zero) > now.as_u64()); assert!(tasks.time_tracked(zero) > almost_now.as_u64());
tasks.move_to(None); tasks.track_at(Timestamp::from(22), None);
tasks.track_at(Timestamp::from(2)); assert_eq!(tasks.get_own_history().unwrap().len(), 4);
assert_eq!(tasks.get_own_history().unwrap().len(), 3); assert_eq!(tasks.time_tracked(zero), 11);
assert_eq!(tasks.time_tracked(zero), 1);
// TODO test received events // TODO test received events
} }
@ -1122,8 +1152,7 @@ mod tasks_test {
let mut tasks = stub_tasks(); let mut tasks = stub_tasks();
let zero = EventId::all_zeros(); let zero = EventId::all_zeros();
tasks.move_to(Some(zero)); tasks.track_at(Timestamp::from(Timestamp::now().as_u64() + 100), Some(zero));
tasks.track_at(Timestamp::from(Timestamp::now().as_u64() + 100));
assert_eq!(timestamps(tasks.history.values().nth(0).unwrap().into_iter(), &vec![&zero]).collect_vec().len(), 2) assert_eq!(timestamps(tasks.history.values().nth(0).unwrap().into_iter(), &vec![&zero]).collect_vec().len(), 2)
// TODO Does not show both future and current tracking properly, need to split by current time // TODO Does not show both future and current tracking properly, need to split by current time
} }
@ -1143,6 +1172,7 @@ mod tasks_test {
assert_eq!(tasks.visible_tasks().len(), 0); assert_eq!(tasks.visible_tasks().len(), 0);
tasks.move_to(Some(t1)); tasks.move_to(Some(t1));
assert_position!(tasks, t1);
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.visible_tasks().len(), 0); assert_eq!(tasks.visible_tasks().len(), 0);
let t2 = tasks.make_task("t2"); let t2 = tasks.make_task("t2");
@ -1153,6 +1183,7 @@ mod tasks_test {
assert_eq!(tasks.visible_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
tasks.move_to(Some(t2)); tasks.move_to(Some(t2));
assert_position!(tasks, t2);
assert_eq!(tasks.visible_tasks().len(), 0); assert_eq!(tasks.visible_tasks().len(), 0);
let t4 = tasks.make_task("t4"); let t4 = tasks.make_task("t4");
assert_eq!(tasks.visible_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
@ -1163,7 +1194,16 @@ mod tasks_test {
tasks.depth = -1; tasks.depth = -1;
assert_eq!(tasks.visible_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
assert_eq!(ChildIterator::from(&tasks, &EventId::all_zeros()).get_all().len(), 1);
assert_eq!(ChildIterator::from(&tasks, &EventId::all_zeros()).get_depth(0).len(), 1);
assert_eq!(ChildIterator::from(&tasks, &t1).get_depth(0).len(), 1);
assert_eq!(ChildIterator::from(&tasks, &t1).get_depth(1).len(), 3);
assert_eq!(ChildIterator::from(&tasks, &t1).get_depth(2).len(), 4);
assert_eq!(ChildIterator::from(&tasks, &t1).get_all().len(), 4);
tasks.move_to(Some(t1)); tasks.move_to(Some(t1));
assert_position!(tasks, t1);
assert_eq!(tasks.get_own_history().unwrap().len(), 3);
assert_eq!(tasks.relative_path(t4), "t2>t4"); assert_eq!(tasks.relative_path(t4), "t2>t4");
assert_eq!(tasks.visible_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 2; tasks.depth = 2;