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
|
||||
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
|
||||
/// 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());
|
||||
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());
|
||||
|
|
290
src/tasks.rs
290
src/tasks.rs
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue