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
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
/// 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());
info!("Filtering for events by {author}");
tasks.set_filter(
tasks.filtered_tasks(tasks.get_position())
tasks.filtered_tasks(tasks.get_position_ref())
.filter(|t| t.event.pubkey == author)
.map(|t| t.event.id)
.collect()
@ -455,11 +455,14 @@ async fn main() {
Some('(') => {
if let Some(arg) = arg {
if !tasks.track_from(arg) {
continue;
if tasks.track_from(arg) {
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;
}
@ -474,42 +477,46 @@ async fn main() {
Some('.') => {
let mut dots = 1;
let mut pos = tasks.get_position();
let mut pos = tasks.get_position_ref();
for _ in iter.take_while(|c| c == &'.') {
dots += 1;
pos = tasks.get_parent(pos).cloned();
pos = tasks.get_parent(pos);
}
let slice = input[dots..].trim();
if pos != tasks.get_position() || slice.is_empty() {
tasks.move_to(pos);
}
if slice.is_empty() {
tasks.move_to(pos.cloned());
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
}
} else if let Ok(depth) = slice.parse::<i8>() {
if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned());
}
tasks.set_depth(depth);
} 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('/') => {
let mut dots = 1;
let mut pos = tasks.get_position();
let mut pos = tasks.get_position_ref();
for _ in iter.take_while(|c| c == &'/') {
dots += 1;
pos = tasks.get_parent(pos).cloned();
pos = tasks.get_parent(pos);
}
let slice = input[dots..].trim();
if slice.is_empty() {
tasks.move_to(pos);
tasks.move_to(pos.cloned());
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
}
} else if let Ok(depth) = slice.parse::<i8>() {
if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned());
}
tasks.set_depth(depth);
} else {
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 {
tasks.move_to(filtered.into_iter().nth(0));
} else {
tasks.move_to(pos);
tasks.move_to(pos.cloned());
tasks.set_filter(filtered);
}
}
@ -556,7 +563,7 @@ async fn main() {
}
continue;
} else {
tasks.filter_or_create(&input);
tasks.filter_or_create(tasks.get_position().as_ref(), &input);
}
}
or_print(tasks.print_tasks());

View File

@ -16,7 +16,7 @@ use nostr_sdk::prelude::Marker;
use TagStandard::Hashtag;
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::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
@ -35,11 +35,8 @@ pub(crate) struct Tasks {
/// The task properties sorted by
sorting: VecDeque<String>,
/// Currently active task
position: Option<EventId>,
/// A filtered view of the current tasks
view: Vec<EventId>,
/// Negative: Only Leaf nodes
/// Zero: Only Active node
/// Positive: Go down the respective level
depth: i8,
@ -135,7 +132,6 @@ impl Tasks {
"rtime".into(),
"name".into(),
]),
position: None, // TODO persist position
view: Default::default(),
tags: Default::default(),
tags_excluded: Default::default(),
@ -150,15 +146,22 @@ impl Tasks {
#[inline]
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]
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
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> {
@ -172,32 +175,31 @@ impl Tasks {
}
/// Dynamic time tracking overview for current task or current user.
pub(crate) fn times_tracked(&self) -> String {
match self.get_position() {
pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String>>) {
match self.get_position_ref() {
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");
if let Some(set) = self.history.get(&self.sender.pubkey()) {
let mut last = None;
let mut full = Vec::with_capacity(set.len());
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!("{:>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;
}
}
full
("Your Time-Tracking History:".to_string(), Box::from(full.into_iter()))
} else {
String::from("You have nothing tracked yet")
("You have nothing tracked yet".to_string(), Box::from(empty()))
}
}
Some(id) => {
let ids = vec![&id];
once(format!("Times tracked on {}", self.get_task_title(&id))).chain(
let ids = vec![id];
let history =
self.history.iter().flat_map(|(key, set)| {
let mut vec = Vec::with_capacity(set.len() / 2);
let mut iter = timestamps(set.iter(), &ids).tuples();
@ -205,7 +207,7 @@ impl Tasks {
vec.push(format!("{} - {} by {}",
local_datetimestamp(start),
// 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)
} else {
format_stamp(end, "%H:%M")
@ -216,15 +218,15 @@ impl Tasks {
.for_each(|(stamp, _)|
vec.push(format!("{} started by {}", local_datetimestamp(stamp), key)));
vec
}).sorted_unstable() // TODO sorting depends on timestamp format - needed to interleave different people
).join("\n")
}).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people
(format!("Times Tracked on {:?}", self.get_task_title(&id)), Box::from(history))
}
}
}
/// Total time in seconds tracked on this task by the current user.
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();
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
}
@ -263,8 +265,8 @@ impl Tasks {
// Parents
pub(crate) fn get_parent(&self, id: Option<EventId>) -> Option<&EventId> {
id.and_then(|id| self.get_by_id(&id))
pub(crate) fn get_parent(&self, id: Option<&EventId>) -> Option<&EventId> {
id.and_then(|id| self.get_by_id(id))
.and_then(|t| t.parent_id())
}
@ -295,7 +297,7 @@ impl Tasks {
fn relative_path(&self, id: EventId) -> String {
join_tasks(
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,
).unwrap_or(id.to_string())
}
@ -352,20 +354,20 @@ impl Tasks {
#[inline]
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
.values()
.filter(move |t| t.parent_id() == id.as_ref())
.filter(move |t| t.parent_id() == 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
self.resolve_tasks(self.children_of(position))
.filter(|t| {
.filter(move |t| {
// TODO apply filters in transit
self.state.matches(t) &&
t.tags.as_ref().map_or(true, |tags| {
@ -381,12 +383,12 @@ impl Tasks {
pub(crate) fn visible_tasks(&self) -> Vec<&Task> {
if self.depth == 0 {
return self.get_current_task().into_iter().collect();
return vec![];
}
if self.view.len() > 0 {
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> {
@ -413,11 +415,21 @@ impl Tasks {
)?;
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 hide empty columns
writeln!(lock, "{}", self.properties.join("\t").bold())?;
let mut total_time = 0;
let mut tasks = self.visible_tasks();
let count = tasks.len();
tasks.sort_by_cached_key(|task| {
self.sorting
@ -433,7 +445,7 @@ impl Tasks {
.map(|p| self.get_property(task, p.as_str()))
.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)
}
}
@ -550,14 +562,14 @@ impl Tasks {
}
/// 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) {
return vec![id];
}
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let lowercase_arg = arg.to_ascii_lowercase();
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();
if lowercase == lowercase_arg {
return vec![task.event.id];
@ -575,8 +587,8 @@ impl Tasks {
/// Finds out what to do with the given string.
/// Returns an EventId if a new Task was created.
pub(crate) fn filter_or_create(&mut self, arg: &str) -> Option<EventId> {
let filtered = self.get_filtered(arg);
pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
let filtered = self.get_filtered(position, arg);
match filtered.len() {
0 => {
// No match, new task
@ -595,25 +607,45 @@ impl Tasks {
}
_ => {
// Multiple match, filter
self.move_to(position.cloned());
self.set_filter(filtered);
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();
if id == self.position {
let pos = self.get_position_ref();
if target.as_ref() == pos {
debug!("Flushing Tasks because of move in place");
self.flush();
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");
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
@ -637,7 +669,7 @@ impl Tasks {
}
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> {
@ -661,10 +693,11 @@ impl Tasks {
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);
self.set_state_for(id, "", state);
self.move_to(Some(id));
id
}
/// Creates a task with tags from filter and position
@ -690,51 +723,17 @@ impl Tasks {
/// Parse string and set tracking
/// Returns false and prints a message if parsing failed
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
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
if let Ok(num) = stripped.parse::<i64>() {
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()
parse_tracking_stamp(str)
.map(|stamp| self.track_at(stamp, self.get_position()))
.is_some()
}
pub(crate) fn track_at(&mut self, time: Timestamp) -> EventId {
info!("{} from {}", self.position.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time));
let pos = self.get_position();
let tracking = build_tracking(pos);
// TODO this can lead to funny deletions
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))
pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId {
info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time));
self.submit(
build_tracking(task)
.custom_created_at(time)
)
}
/// Sign and queue the event to the relay, returning its id
@ -802,13 +801,10 @@ impl Tasks {
}
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.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); });
}
@ -826,15 +822,15 @@ impl Tasks {
self.submit(prop)
}
pub(crate) fn update_state(&mut self, comment: &str, state: State) {
self.position
.map(|id| self.set_state_for(id, comment, state));
pub(crate) fn update_state(&mut self, comment: &str, state: State) -> Option<EventId> {
let id = self.get_position_ref()?;
Some(self.set_state_for(id.clone(), comment, state))
}
pub(crate) fn make_note(&mut self, note: &str) {
if let Some(id) = self.position {
if self.get_by_id(&id).is_some_and(|t| t.is_task()) {
let prop = build_prop(Kind::TextNote, note.trim(), id);
if let Some(id) = self.get_position_ref() {
if self.get_by_id(id).is_some_and(|t| t.is_task()) {
let prop = build_prop(Kind::TextNote, note.trim(), id.clone());
self.submit(prop);
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> {
event.tags.iter().find_map(|tag| match tag.as_standardized() {
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
/// Expects a sorted iterator
struct TimesTracked<'a> {
struct Durations<'a> {
events: Box<dyn Iterator<Item=&'a Event> + 'a>,
ids: &'a Vec<&'a EventId>,
threshold: Option<Timestamp>,
}
impl TimesTracked<'_> {
fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<&EventId>) -> TimesTracked<'b> {
TimesTracked {
impl Durations<'_> {
fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<&EventId>) -> Durations<'b> {
Durations {
events: Box::new(events.into_iter()),
ids,
threshold: Some(Timestamp::now()),
}
}
}
impl Iterator for TimesTracked<'_> {
impl Iterator for Durations<'_> {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
@ -1000,20 +1002,35 @@ impl Iterator for TimesTracked<'_> {
/// Breadth-First Iterator over Tasks and recursive children
struct ChildIterator<'a> {
tasks: &'a TaskMap,
/// Found Events
queue: Vec<&'a EventId>,
/// Index of the next element in the queue
index: usize,
/// Depth of the next element
depth: usize,
/// Element with the next depth boundary
next_depth_at: usize,
}
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);
queue.push(id);
ChildIterator {
tasks,
tasks: &tasks.tasks,
queue,
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> {
while self.next().is_some() {}
self.queue
@ -1031,7 +1048,7 @@ impl<'a> Iterator for ChildIterator<'a> {
self.queue.reserve(task.children.len());
self.queue.extend(task.children.iter());
} else {
// Unknown task, can still find children
// Unknown task, might still find children, just slower
for task in self.tasks.values() {
if task.parent_id().is_some_and(|i| i == id) {
self.queue.push(task.get_id());
@ -1039,6 +1056,10 @@ impl<'a> Iterator for ChildIterator<'a> {
}
}
self.index += 1;
if self.next_depth_at == self.index {
self.depth += 1;
self.next_depth_at = self.queue.len();
}
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]
fn test_procedures() {
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));
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::<EventId>::new());
let sub_id = tasks.make_task("sub");
@ -1100,18 +1131,17 @@ mod tasks_test {
let mut tasks = stub_tasks();
let zero = EventId::all_zeros();
tasks.track_at(Timestamp::from(0));
tasks.track_at(Timestamp::from(0), None);
assert_eq!(tasks.history.len(), 1);
tasks.move_to(Some(zero));
let now: Timestamp = Timestamp::now() - 2u64;
tasks.track_at(Timestamp::from(1));
assert!(tasks.time_tracked(zero) > now.as_u64());
let almost_now: Timestamp = Timestamp::now() - 12u64;
tasks.track_at(Timestamp::from(11), Some(zero));
tasks.track_at(Timestamp::from(13), Some(zero));
assert!(tasks.time_tracked(zero) > almost_now.as_u64());
tasks.move_to(None);
tasks.track_at(Timestamp::from(2));
assert_eq!(tasks.get_own_history().unwrap().len(), 3);
assert_eq!(tasks.time_tracked(zero), 1);
tasks.track_at(Timestamp::from(22), None);
assert_eq!(tasks.get_own_history().unwrap().len(), 4);
assert_eq!(tasks.time_tracked(zero), 11);
// TODO test received events
}
@ -1122,8 +1152,7 @@ mod tasks_test {
let mut tasks = stub_tasks();
let zero = EventId::all_zeros();
tasks.move_to(Some(zero));
tasks.track_at(Timestamp::from(Timestamp::now().as_u64() + 100));
tasks.track_at(Timestamp::from(Timestamp::now().as_u64() + 100), Some(zero));
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
}
@ -1143,6 +1172,7 @@ mod tasks_test {
assert_eq!(tasks.visible_tasks().len(), 0);
tasks.move_to(Some(t1));
assert_position!(tasks, t1);
tasks.depth = 2;
assert_eq!(tasks.visible_tasks().len(), 0);
let t2 = tasks.make_task("t2");
@ -1153,6 +1183,7 @@ mod tasks_test {
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.move_to(Some(t2));
assert_position!(tasks, t2);
assert_eq!(tasks.visible_tasks().len(), 0);
let t4 = tasks.make_task("t4");
assert_eq!(tasks.visible_tasks().len(), 1);
@ -1163,7 +1194,16 @@ mod tasks_test {
tasks.depth = -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));
assert_position!(tasks, t1);
assert_eq!(tasks.get_own_history().unwrap().len(), 3);
assert_eq!(tasks.relative_path(t4), "t2>t4");
assert_eq!(tasks.visible_tasks().len(), 2);
tasks.depth = 2;