Compare commits
7 Commits
b03ad00b6a
...
dda969e08b
Author | SHA1 | Date |
---|---|---|
xeruf | dda969e08b | |
xeruf | dcf333353b | |
xeruf | 15bd21059d | |
xeruf | 9fbe3e27cb | |
xeruf | 06bfe8e18a | |
xeruf | b74ac18e39 | |
xeruf | ae2172c8f2 |
36
README.md
36
README.md
|
@ -54,10 +54,16 @@ Using subtasks has two main advantages:
|
|||
- ability to accumulate time tracked
|
||||
- swiftly navigate between related tasks
|
||||
|
||||
Managing a project with subtasks makes it continuously visible,
|
||||
which is helpful if you want to be able to track time on the project itself
|
||||
without a specific task,
|
||||
Thus subtasks can be very useful for specific contexts,
|
||||
for example a project or a specific place.
|
||||
|
||||
On the other hand, related tasks like chores
|
||||
should be grouped with a tag instead.
|
||||
Similarly for projects which are only sporadically worked on
|
||||
when a specific task comes up, so they do not clutter the list.
|
||||
|
||||
### Collaboration
|
||||
|
||||
|
@ -86,7 +92,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
|||
|
||||
`TASK` creation syntax: `NAME: TAG1 TAG2 ...`
|
||||
|
||||
- `TASK` - create task
|
||||
- `TASK` - create task (prefix with space if you want a task to start with a command character)
|
||||
- `.` - clear filters and reload
|
||||
- `.TASK`
|
||||
+ activate task by id
|
||||
|
@ -96,34 +102,38 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
|||
- `/[TEXT]` - like `.`, but never creates a task
|
||||
- `|[TASK]` - (un)mark current task as procedure or create and activate a new task procedure (where subtasks automatically depend on the previously created task)
|
||||
|
||||
Dots can be repeated to move to parent tasks.
|
||||
Dots and slashes can be repeated to move to parent tasks.
|
||||
|
||||
- `:[IND][COL]` - add property column COL at IND or end, if it already exists remove property column COL or IND (1-indexed)
|
||||
- `*[TIME]` - add timetracking with the specified offset (empty: list tracked times)
|
||||
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (1-indexed)
|
||||
- `::[PROP]` - Sort by property PROP
|
||||
- `([TIME]` - insert timetracking with the specified offset in minutes (empty: list tracked times)
|
||||
- `)[TIME]` - stop timetracking with the specified offset in minutes - convenience helper to move to root (empty: stop now)
|
||||
- `>[TEXT]` - complete active task and move to parent, with optional state description
|
||||
- `<[TEXT]` - close active task and move to parent, with optional state description
|
||||
- `!TEXT` - set state for current task from text
|
||||
- `,TEXT` - add text note (comment / description)
|
||||
- TBI: `*[INT]` - set priority - can also be used in task, with any digit
|
||||
- `@` - undoes last action (moving in place or upwards or waiting a minute confirms pending actions)
|
||||
- `wss://...` - switch or subscribe to relay
|
||||
- `wss://...` - switch or subscribe to relay (prefix with space to forcibly add a new one)
|
||||
|
||||
Property Filters:
|
||||
|
||||
- `#TAG` - set tag filter (empty: list all used tags)
|
||||
- `+TAG` - add tag filter
|
||||
- `-TAG` - remove tag filters
|
||||
- `?STATE` - filter by state (type or description) - plain `?` to reset, `??` to show all
|
||||
- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all
|
||||
|
||||
State descriptions can be used for example for Kanban columns or review flows.
|
||||
An active tag or state filter will also set that attribute for newly created tasks.
|
||||
Status descriptions can be used for example for Kanban columns or review flows.
|
||||
An active tag or status filter will also set that attribute for newly created tasks.
|
||||
|
||||
### Available Columns
|
||||
|
||||
- `id`
|
||||
- `parentid`
|
||||
- `name`
|
||||
- `state`
|
||||
- `hashtags`
|
||||
- `state` - indicator of current progress
|
||||
- `status` - pure task status
|
||||
- `hashtags` - list of hashtags set for the task
|
||||
- `tags` - values of all nostr tags associated with the event, except event tags
|
||||
- `desc` - last note on the task
|
||||
- `description` - accumulated notes on the task
|
||||
|
@ -171,6 +181,7 @@ The following features are not ready to be implemented
|
|||
because they need conceptualization.
|
||||
Suggestions welcome!
|
||||
|
||||
- What if I want to postpone a procedure, i.e. make it pending, or move it across kanban, does this make sense?
|
||||
- Priorities
|
||||
- Dependencies (change from tags to properties so they can be added later? or maybe as a state?)
|
||||
- Templates
|
||||
|
@ -185,3 +196,8 @@ Suggestions welcome!
|
|||
- TUI: Clear terminal? Refresh on empty prompt after timeout?
|
||||
- Kanban, GANTT, Calendar
|
||||
- Web Interface, Messenger integrations
|
||||
|
||||
## Notes
|
||||
|
||||
- TBI = To Be Implemented
|
||||
- `. TASK` - create and enter a new task even if the name matches an existing one
|
34
src/main.rs
34
src/main.rs
|
@ -284,8 +284,7 @@ async fn main() {
|
|||
if remaining.is_empty() {
|
||||
tasks.remove_column(index);
|
||||
} else {
|
||||
let value = input[2..].trim().to_string();
|
||||
tasks.add_or_remove_property_column_at_index(value, index);
|
||||
tasks.add_or_remove_property_column_at_index(remaining, index);
|
||||
}
|
||||
} else if let Some(arg) = arg {
|
||||
tasks.add_or_remove_property_column(arg);
|
||||
|
@ -379,22 +378,27 @@ async fn main() {
|
|||
None => tasks.clear_filter()
|
||||
}
|
||||
|
||||
Some('*') =>
|
||||
Some('(') =>
|
||||
match arg {
|
||||
Some(arg) => {
|
||||
if let Ok(num) = arg.parse::<i64>() {
|
||||
tasks.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num)));
|
||||
} else if let Ok(date) = DateTime::parse_from_rfc3339(arg) {
|
||||
tasks.track_at(Timestamp::from(date.to_utc().timestamp() as u64));
|
||||
} else {
|
||||
warn!("Cannot parse {arg}");
|
||||
Some(arg) =>
|
||||
if !tasks.track_from(arg) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("{}", tasks.times_tracked());
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some(')') => {
|
||||
tasks.move_to(None);
|
||||
if let Some(arg) = arg {
|
||||
if !tasks.track_from(arg) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some('.') => {
|
||||
let mut dots = 1;
|
||||
|
@ -403,7 +407,8 @@ async fn main() {
|
|||
dots += 1;
|
||||
pos = tasks.get_parent(pos).cloned();
|
||||
}
|
||||
let slice = &input[dots..];
|
||||
let slice = input[dots..].trim();
|
||||
|
||||
tasks.move_to(pos);
|
||||
if slice.is_empty() {
|
||||
if dots > 1 {
|
||||
|
@ -423,7 +428,8 @@ async fn main() {
|
|||
dots += 1;
|
||||
pos = tasks.get_parent(pos).cloned();
|
||||
}
|
||||
let slice = &input[dots..].to_ascii_lowercase();
|
||||
let slice = &input[dots..].trim().to_ascii_lowercase();
|
||||
|
||||
if slice.is_empty() {
|
||||
tasks.move_to(pos);
|
||||
} else if let Ok(depth) = slice.parse::<i8>() {
|
||||
|
@ -447,7 +453,7 @@ async fn main() {
|
|||
}
|
||||
|
||||
_ =>
|
||||
if Regex::new("^wss?://").unwrap().is_match(&input) {
|
||||
if Regex::new("^wss?://").unwrap().is_match(&input.trim()) {
|
||||
tasks.move_to(None);
|
||||
let mut new_relay = relays.keys().find(|key| key.as_str().starts_with(&input)).cloned();
|
||||
if new_relay.is_none() {
|
||||
|
|
50
src/task.rs
50
src/task.rs
|
@ -4,11 +4,11 @@ use std::collections::{BTreeSet, HashSet};
|
|||
use std::fmt;
|
||||
use std::string::ToString;
|
||||
|
||||
use colored::Colorize;
|
||||
use colored::{ColoredString, Colorize};
|
||||
use itertools::Either::{Left, Right};
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, TagStandard, Timestamp};
|
||||
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
|
||||
|
||||
use crate::helpers::some_non_empty;
|
||||
use crate::kinds::{is_hashtag, PROCEDURE_KIND};
|
||||
|
@ -72,7 +72,6 @@ impl Task {
|
|||
}
|
||||
|
||||
pub(crate) fn get_dependendees(&self) -> Vec<&EventId> {
|
||||
// TODO honor properly
|
||||
self.find_refs(MARKER_DEPENDS).collect()
|
||||
}
|
||||
|
||||
|
@ -134,7 +133,6 @@ impl Task {
|
|||
tags.into_iter()
|
||||
.filter(predicate)
|
||||
.map(|t| format!("{}", t.content().unwrap()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
})
|
||||
}
|
||||
|
@ -143,24 +141,14 @@ impl Task {
|
|||
match property {
|
||||
"id" => Some(self.event.id.to_string()),
|
||||
"parentid" => self.parent_id().map(|i| i.to_string()),
|
||||
"state" => Some({
|
||||
let state = self.state_or_default();
|
||||
let label = state.get_label();
|
||||
match state.state {
|
||||
State::Open => label.green(),
|
||||
State::Done => label.bright_black(),
|
||||
State::Closed => label.magenta(),
|
||||
State::Pending => label.yellow(),
|
||||
State::Procedure => label.blue(),
|
||||
}.to_string()
|
||||
}),
|
||||
"status" => Some(self.state_or_default().get_colored_label().to_string()),
|
||||
"name" => Some(self.event.content.clone()),
|
||||
"desc" => self.descriptions().last().cloned(),
|
||||
"description" => Some(self.descriptions().join(" ")),
|
||||
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
|
||||
"tags" => self.filter_tags(|_| true),
|
||||
"alltags" => Some(format!("{:?}", self.tags)),
|
||||
"refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())),
|
||||
"refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())),
|
||||
"props" => Some(format!(
|
||||
"{:?}",
|
||||
self.props
|
||||
|
@ -181,7 +169,7 @@ impl Task {
|
|||
}
|
||||
|
||||
pub(crate) struct TaskState {
|
||||
state: State,
|
||||
pub(crate) state: State,
|
||||
name: Option<String>,
|
||||
pub(crate) time: Timestamp,
|
||||
}
|
||||
|
@ -192,6 +180,9 @@ impl TaskState {
|
|||
pub(crate) fn get_label(&self) -> String {
|
||||
self.name.clone().unwrap_or_else(|| self.state.to_string())
|
||||
}
|
||||
pub(crate) fn get_colored_label(&self) -> ColoredString {
|
||||
self.state.colorize(&self.get_label())
|
||||
}
|
||||
pub(crate) fn matches_label(&self, label: &str) -> bool {
|
||||
self.name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(label))
|
||||
|| self.state.to_string().eq_ignore_ascii_case(label)
|
||||
|
@ -212,7 +203,7 @@ impl Display for TaskState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq)]
|
||||
pub(crate) enum State {
|
||||
Open,
|
||||
Done,
|
||||
|
@ -220,6 +211,17 @@ pub(crate) enum State {
|
|||
Pending,
|
||||
Procedure,
|
||||
}
|
||||
impl From<&str> for State {
|
||||
fn from(value: &str) -> Self {
|
||||
match value {
|
||||
"Closed" => State::Closed,
|
||||
"Done" => State::Done,
|
||||
"Pending" => State::Pending,
|
||||
"Proc" | "Procedure" | "List" => State::Procedure,
|
||||
_ => State::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryFrom<Kind> for State {
|
||||
type Error = ();
|
||||
|
||||
|
@ -237,7 +239,7 @@ impl TryFrom<Kind> for State {
|
|||
impl State {
|
||||
pub(crate) fn is_open(&self) -> bool {
|
||||
match self {
|
||||
State::Open | State::Procedure => true,
|
||||
State::Open | State::Pending | State::Procedure => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -251,6 +253,16 @@ impl State {
|
|||
State::Procedure => PROCEDURE_KIND,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn colorize(&self, str: &str) -> ColoredString {
|
||||
match self {
|
||||
State::Open => str.green(),
|
||||
State::Done => str.bright_black(),
|
||||
State::Closed => str.magenta(),
|
||||
State::Pending => str.yellow(),
|
||||
State::Procedure => str.blue(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<State> for Kind {
|
||||
fn from(value: State) -> Self {
|
||||
|
|
115
src/tasks.rs
115
src/tasks.rs
|
@ -7,13 +7,12 @@ use std::str::FromStr;
|
|||
use std::sync::mpsc::Sender;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{Local, TimeZone};
|
||||
use chrono::{DateTime, Local, TimeZone};
|
||||
use chrono::LocalResult::Single;
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
|
||||
use nostr_sdk::base64::write::StrConsumer;
|
||||
use nostr_sdk::prelude::Marker;
|
||||
use TagStandard::Hashtag;
|
||||
|
||||
|
@ -68,12 +67,12 @@ impl StateFilter {
|
|||
StateFilter::Default => {
|
||||
let state = task.pure_state();
|
||||
state.is_open() || (state == State::Done && task.parent_id() != None)
|
||||
},
|
||||
}
|
||||
StateFilter::All => true,
|
||||
StateFilter::State(filter) => task.state().is_some_and(|t| t.matches_label(filter)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn as_option(&self) -> Option<String> {
|
||||
if let StateFilter::State(str) = self {
|
||||
Some(str.to_string())
|
||||
|
@ -117,7 +116,6 @@ impl Tasks {
|
|||
history: Default::default(),
|
||||
properties: vec![
|
||||
"state".into(),
|
||||
"progress".into(),
|
||||
"rtime".into(),
|
||||
"hashtags".into(),
|
||||
"rpath".into(),
|
||||
|
@ -172,11 +170,7 @@ impl Tasks {
|
|||
.dedup()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Dynamic time tracking overview for current task or current user.
|
||||
pub(crate) fn times_tracked(&self) -> String {
|
||||
match self.get_position() {
|
||||
None => {
|
||||
|
@ -201,23 +195,29 @@ impl Tasks {
|
|||
}
|
||||
}
|
||||
Some(id) => {
|
||||
let vec = vec![id];
|
||||
let res =
|
||||
once(format!("Times tracked on {}", self.get_task_title(&id))).chain(
|
||||
self.history.iter().flat_map(|(key, set)|
|
||||
timestamps(set.iter(), &vec)
|
||||
.tuples::<(_, _)>()
|
||||
.map(move |((start, _), (end, _))| {
|
||||
format!("{} - {} by {}", start.to_human_datetime(), end.to_human_datetime(), key)
|
||||
})
|
||||
).sorted_unstable()
|
||||
).join("\n");
|
||||
drop(vec);
|
||||
res
|
||||
let ids = vec![id];
|
||||
once(format!("Times tracked on {}", self.get_task_title(&id))).chain(
|
||||
self.history.iter().flat_map(|(key, set)| {
|
||||
let mut vec = Vec::with_capacity(set.len() / 2);
|
||||
let mut iter = timestamps(set.iter(), &ids).tuples();
|
||||
while let Some(((start, _), (end, _))) = iter.next() {
|
||||
vec.push(format!("{} - {} by {}", start.to_human_datetime(), end.to_human_datetime(), key))
|
||||
}
|
||||
iter.into_buffer().for_each(|(stamp, _)|
|
||||
vec.push(format!("{} started by {}", stamp.to_human_datetime(), key)));
|
||||
vec
|
||||
}).sorted_unstable()
|
||||
).join("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
|
||||
/// Total time in seconds tracked on this task and its subtasks by all users.
|
||||
fn total_time_tracked(&self, id: EventId) -> u64 {
|
||||
let mut total = 0;
|
||||
|
@ -399,9 +399,15 @@ impl Tasks {
|
|||
writeln!(lock, "{}", t.descriptions().join("\n"))?;
|
||||
}
|
||||
// TODO proper column alignment
|
||||
// TODO hide empty columns and sorting
|
||||
writeln!(lock, "{}", self.properties.join("\t").bold())?;
|
||||
let mut total_time = 0;
|
||||
for task in self.current_tasks() {
|
||||
let progress =
|
||||
self
|
||||
.total_progress(task.get_id())
|
||||
.filter(|_| task.children.len() > 0);
|
||||
let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0));
|
||||
writeln!(
|
||||
lock,
|
||||
"{}",
|
||||
|
@ -423,10 +429,18 @@ impl Tasks {
|
|||
"".to_string()
|
||||
}
|
||||
}
|
||||
"progress" => self
|
||||
.total_progress(task.get_id())
|
||||
.filter(|_| task.children.len() > 0)
|
||||
.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)),
|
||||
"state" => {
|
||||
if let Some(task) = task.get_dependendees().iter().filter_map(|id| self.get_by_id(id)).find(|t| t.pure_state().is_open()) {
|
||||
return format!("Blocked by \"{}\"", task.get_title()).bright_red().to_string()
|
||||
}
|
||||
let state = task.state_or_default();
|
||||
if state.state.is_open() && progress.is_some_and(|p| p > 0.1) {
|
||||
state.state.colorize(&prog_string)
|
||||
} else {
|
||||
state.get_colored_label()
|
||||
}.to_string()
|
||||
}
|
||||
"progress" => prog_string.clone(),
|
||||
"path" => self.get_task_path(Some(task.event.id)),
|
||||
"rpath" => self.relative_path(task.event.id),
|
||||
// TODO format strings configurable
|
||||
|
@ -585,11 +599,11 @@ impl Tasks {
|
|||
}
|
||||
}
|
||||
|
||||
/// Creates a task following the current state
|
||||
/// Sanitizes input
|
||||
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
||||
let tag: Option<Tag> = self.get_current_task()
|
||||
.and_then(|t| {
|
||||
println!("{:?}", t);
|
||||
if t.pure_state() == State::Procedure {
|
||||
t.children.iter()
|
||||
.filter_map(|id| self.get_by_id(id))
|
||||
|
@ -633,10 +647,25 @@ impl Tasks {
|
|||
self.tasks.get(id).map_or(id.to_string(), |t| t.get_title())
|
||||
}
|
||||
|
||||
/// Parse string and set tracking
|
||||
/// Returns false if parsing failed
|
||||
pub(crate) fn track_from(&mut self, str: &str) -> bool {
|
||||
if let Ok(num) = str.parse::<i64>() {
|
||||
self.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num * 60)));
|
||||
} else if let Ok(date) = DateTime::parse_from_rfc3339(str) {
|
||||
self.track_at(Timestamp::from(date.to_utc().timestamp() as u64));
|
||||
} else {
|
||||
warn!("Cannot parse {str}");
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn track_at(&mut self, time: Timestamp) -> EventId {
|
||||
info!("Tracking \"{:?}\" from {}", self.position.map(|id| self.get_task_title(&id)), time.to_human_datetime());
|
||||
info!("{} from {}", self.position.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), time.to_human_datetime()); // TODO omit seconds
|
||||
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 &&
|
||||
|
@ -712,11 +741,7 @@ impl Tasks {
|
|||
}
|
||||
|
||||
pub(crate) fn set_state_for_with(&mut self, id: EventId, comment: &str) {
|
||||
self.set_state_for(id, comment, match comment {
|
||||
"Closed" => State::Closed,
|
||||
"Done" => State::Done,
|
||||
_ => State::Open,
|
||||
});
|
||||
self.set_state_for(id, comment, comment.into());
|
||||
}
|
||||
|
||||
pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId {
|
||||
|
@ -828,15 +853,19 @@ fn timestamps<'a>(events: impl Iterator<Item=&'a Event>, ids: &'a Vec<EventId>)
|
|||
.skip_while(|element| element.1 == None)
|
||||
}
|
||||
|
||||
/// Iterates Events to accumulate times tracked
|
||||
/// Expects a sorted iterator
|
||||
struct TimesTracked<'a> {
|
||||
events: Box<dyn Iterator<Item=&'a Event> + 'a>,
|
||||
ids: &'a Vec<EventId>,
|
||||
threshold: Option<Timestamp>,
|
||||
}
|
||||
impl TimesTracked<'_> {
|
||||
fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<EventId>) -> TimesTracked<'b> {
|
||||
TimesTracked {
|
||||
events: Box::new(events.into_iter()),
|
||||
ids,
|
||||
threshold: Some(Timestamp::now()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -848,6 +877,9 @@ impl Iterator for TimesTracked<'_> {
|
|||
let mut start: Option<u64> = None;
|
||||
while let Some(event) = self.events.next() {
|
||||
if matching_tag_id(event, self.ids).is_some() {
|
||||
if self.threshold.is_some_and(|th| event.created_at > th) {
|
||||
continue;
|
||||
}
|
||||
start = start.or(Some(event.created_at.as_u64()))
|
||||
} else {
|
||||
if let Some(stamp) = start {
|
||||
|
@ -855,7 +887,8 @@ impl Iterator for TimesTracked<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
return start.map(|stamp| Duration::from_secs(Timestamp::now().as_u64() - stamp));
|
||||
let now = self.threshold.unwrap_or(Timestamp::now()).as_u64();
|
||||
return start.filter(|t| t < &now).map(|stamp| Duration::from_secs(now.saturating_sub(stamp)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -918,6 +951,18 @@ mod tasks_test {
|
|||
// TODO test received events
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_timestamps() {
|
||||
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));
|
||||
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 now
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_depth() {
|
||||
let mut tasks = stub_tasks();
|
||||
|
|
Loading…
Reference in New Issue