Compare commits
10 Commits
f8375cf879
...
721c200b97
Author | SHA1 | Date |
---|---|---|
xeruf | 721c200b97 | |
xeruf | 3b9fedd9a3 | |
xeruf | 12b7c909ab | |
xeruf | 5bc3509930 | |
xeruf | 629db66018 | |
xeruf | e0d241ec5a | |
xeruf | 506a73060a | |
xeruf | ebfe632497 | |
xeruf | a4f9398846 | |
xeruf | 416a7f195d |
|
@ -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
|
||||||
|
|
37
src/main.rs
37
src/main.rs
|
@ -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());
|
||||||
|
|
290
src/tasks.rs
290
src/tasks.rs
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue