Compare commits

...

6 commits

Author SHA1 Message Date
xeruf
01ece3b2af fix(tasks): only enter a perfect global match that is not closed 2024-08-25 16:46:21 +03:00
xeruf
298c1eceeb feat(main): automatic readline history 2024-08-25 16:38:55 +03:00
xeruf
a801e12b57 feat(tasks): prevent accidental redundant time-tracking 2024-08-25 16:37:55 +03:00
xeruf
f381608b4c feat: more adaptive tag filtering
Make tag exclusions more persistent
2024-08-25 15:37:05 +03:00
xeruf
dedda147c0 refactor: code cleanup with clippy 2024-08-25 15:30:55 +03:00
xeruf
38fd00b150 feat(tasks): allow jumping to task anywhere by exact name match 2024-08-25 14:30:08 +03:00
5 changed files with 132 additions and 104 deletions

View file

@ -95,7 +95,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
- `TASK` - create task
+ prefix with space if you want a task to start with a command character
+ copy in text with newlines to create one task per line
- `.` - clear filters
- `.` - clear all filters
- `.TASK`
+ activate task by id
+ match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive)
@ -125,9 +125,9 @@ Dot or slash can be repeated to move to parent tasks before acting.
Property Filters:
- `#TAG1 TAG2` - set tag filter (empty: list all used tags)
- `+TAG` - add tag filter
- `-TAG` - remove tag filters by prefix
- `#TAG1 TAG2` - set tag filter
- `+TAG` - add tag filter (empty: list all used tags)
- `-TAG` - remove tag filters (by prefix)
- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all
- `@AUTHOR` - filter by time or author (pubkey, or `@` for self, TBI: id prefix, name prefix)
- TBI: `**INT` - filter by priority

View file

@ -57,7 +57,7 @@ where
EventBuilder::new(
Kind::from(TRACKING_KIND),
"",
id.into_iter().map(|id| Tag::event(id)),
id.into_iter().map(Tag::event),
)
}
@ -65,7 +65,7 @@ where
pub(crate) fn build_task(name: &str, tags: Vec<Tag>, kind: Option<(&str, Kind)>) -> EventBuilder {
info!("Created {}task \"{name}\" with tags [{}]",
kind.map(|k| k.0).unwrap_or_default(),
tags.iter().map(|tag| format_tag(tag)).join(", "));
tags.iter().map(format_tag).join(", "));
EventBuilder::new(kind.map(|k| k.1).unwrap_or(Kind::from(TASK_KIND)), name, tags)
}
@ -107,7 +107,7 @@ fn format_tag(tag: &Tag) -> String {
public_key,
alias,
..
}) => format!("Key{}: {:.8}", public_key.to_string(), alias.as_ref().map(|s| format!(" {s}")).unwrap_or_default()),
}) => format!("Key{}: {:.8}", public_key, alias.as_ref().map(|s| format!(" {s}")).unwrap_or_default()),
Some(TagStandard::Hashtag(content)) =>
format!("#{content}"),
_ => tag.content().map_or_else(

View file

@ -18,7 +18,8 @@ use log::{debug, error, info, LevelFilter, trace, warn};
use nostr_sdk::prelude::*;
use nostr_sdk::TagStandard::Hashtag;
use regex::Regex;
use rustyline::{DefaultEditor, Editor};
use rustyline::config::Configurer;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender;
@ -143,6 +144,7 @@ pub(crate) enum MostrMessage {
#[tokio::main]
async fn main() -> Result<()> {
let mut rl = DefaultEditor::new()?;
rl.set_auto_add_history(true);
let mut args = args().skip(1).peekable();
let mut builder = if args.peek().is_some_and(|arg| arg == "--debug") {
@ -239,12 +241,12 @@ async fn main() -> Result<()> {
client.connect().await;
let sub1 = client.subscribe(vec![
Filter::new().kinds(KINDS.into_iter().map(|k| Kind::from(k)))
Filter::new().kinds(KINDS.into_iter().map(Kind::from))
], None).await;
info!("Subscribed to tasks with {:?}", sub1);
let sub2 = client.subscribe(vec![
Filter::new().kinds(PROP_KINDS.into_iter().map(|k| Kind::from(k)))
Filter::new().kinds(PROP_KINDS.into_iter().map(Kind::from))
], None).await;
info!("Subscribed to updates with {:?}", sub2);
@ -418,19 +420,19 @@ async fn main() -> Result<()> {
Some(arg) => {
if arg.len() < CHARACTER_THRESHOLD {
warn!("Note needs at least {CHARACTER_THRESHOLD} characters!");
continue
continue;
}
tasks.make_note(arg)
},
}
}
Some('>') => {
tasks.update_state(&arg_default, State::Done);
tasks.update_state(arg_default, State::Done);
tasks.move_up();
}
Some('<') => {
tasks.update_state(&arg_default, State::Closed);
tasks.update_state(arg_default, State::Closed);
tasks.move_up();
}
@ -441,7 +443,7 @@ async fn main() -> Result<()> {
Some('@') => {
match arg {
None => {
let today = Timestamp::from(Timestamp::now() - 80_000);
let today = Timestamp::now() - 80_000;
info!("Filtering for tasks created in the last 22 hours");
tasks.set_filter(
tasks.filtered_tasks(tasks.get_position_ref())
@ -503,7 +505,7 @@ async fn main() -> Result<()> {
}
},
Some(arg) => 'arm: {
if arg.chars().next() != Some('|') {
if !arg.starts_with('|') {
if let Some(pos) = tasks.get_position() {
tasks.move_up();
tasks.make_task_with(
@ -536,24 +538,24 @@ async fn main() -> Result<()> {
}
Some('#') =>
match arg {
Some(arg) => tasks.set_tags(arg.split_whitespace().map(|s| Hashtag(s.to_string()).into())),
None => {
println!("Hashtags of all known tasks:\n{}", tasks.all_hashtags().join(" "));
continue;
}
}
tasks.set_tags(arg_default.split_whitespace().map(|s| Hashtag(s.to_string()).into())),
Some('+') =>
match arg {
Some(arg) => tasks.add_tag(arg.to_string()),
None => tasks.clear_filter()
None => {
println!("Hashtags of all known tasks:\n{}", tasks.all_hashtags().join(" ").italic());
if tasks.has_tag_filter() {
println!("Use # to remove tag filters and . to remove all filters.")
}
continue;
}
}
Some('-') =>
match arg {
Some(arg) => tasks.remove_tag(arg),
None => tasks.clear_filter()
None => tasks.clear_filters()
}
Some('(') => {
@ -562,7 +564,7 @@ async fn main() -> Result<()> {
let (label, times) = tasks.times_tracked();
println!("{}\n{}", label.italic(), times.rev().take(15).join("\n"));
}
// TODO show history from author / pubkey
// TODO show history of author / pubkey
} else {
let (label, mut times) = tasks.times_tracked();
println!("{}\n{}", label.italic(), times.join("\n"));
@ -574,7 +576,7 @@ async fn main() -> Result<()> {
match arg {
None => tasks.move_to(None),
Some(arg) => {
if parse_tracking_stamp(arg).map(|stamp| tasks.track_at(stamp, None)).is_some() {
if parse_tracking_stamp(arg).and_then(|stamp| tasks.track_at(stamp, None)).is_some() {
let (label, times) = tasks.times_tracked();
println!("{}\n{}", label.italic(), times.rev().take(15).join("\n"));
}
@ -597,6 +599,8 @@ async fn main() -> Result<()> {
tasks.move_to(pos.cloned());
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
} else {
tasks.clear_filters();
}
} else if let Ok(depth) = slice.parse::<i8>() {
if pos != tasks.get_position_ref() {
@ -629,7 +633,7 @@ async fn main() -> Result<()> {
tasks.set_depth(depth);
} else {
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
if slice.chars().find(|c| c.is_ascii_uppercase()).is_none() {
if !slice.chars().any(|c| c.is_ascii_uppercase()) {
// Smart-case - case-sensitive if any uppercase char is entered
transform = Box::new(|s| s.to_ascii_lowercase());
}
@ -643,7 +647,7 @@ async fn main() -> Result<()> {
.map(|t| t.event.id)
.collect_vec();
if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().nth(0));
tasks.move_to(filtered.into_iter().next());
} else {
tasks.move_to(pos.cloned());
tasks.set_filter(filtered);
@ -652,10 +656,10 @@ async fn main() -> Result<()> {
}
_ =>
if Regex::new("^wss?://").unwrap().is_match(&input.trim()) {
if Regex::new("^wss?://").unwrap().is_match(input.trim()) {
tasks.move_to(None);
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&input))) {
selected_relay = url.clone();
selected_relay.clone_from(url);
or_warn!(tasks.print_tasks());
continue;
}

View file

@ -46,7 +46,8 @@ impl Ord for Task {
impl Task {
pub(crate) fn new(event: Event) -> Task {
let (refs, tags) = event.tags.iter().partition_map(|tag| match tag.as_standardized() {
Some(TagStandard::Event { event_id, marker, .. }) => Left((marker.as_ref().map_or(MARKER_PARENT.to_string(), |m| m.to_string()), event_id.clone())),
Some(TagStandard::Event { event_id, marker, .. }) =>
Left((marker.as_ref().map_or(MARKER_PARENT.to_string(), |m| m.to_string()), *event_id)),
_ => Right(tag.clone()),
});
// Separate refs for dependencies
@ -82,13 +83,7 @@ impl Task {
}
pub(crate) fn description_events(&self) -> impl Iterator<Item=&Event> + '_ {
self.props.iter().filter_map(|event| {
if event.kind == Kind::TextNote {
Some(event)
} else {
None
}
})
self.props.iter().filter(|event| event.kind == Kind::TextNote)
}
pub(crate) fn descriptions(&self) -> impl Iterator<Item=&String> + '_ {
@ -105,7 +100,7 @@ impl Task {
event.kind.try_into().ok().map(|s| TaskState {
name: some_non_empty(&event.content),
state: s,
time: event.created_at.clone(),
time: event.created_at,
})
})
}
@ -142,9 +137,9 @@ impl Task {
P: FnMut(&&Tag) -> bool,
{
self.tags.as_ref().map(|tags| {
tags.into_iter()
tags.iter()
.filter(predicate)
.map(|t| format!("{}", t.content().unwrap()))
.map(|t| t.content().unwrap().to_string())
.join(" ")
})
}
@ -261,10 +256,7 @@ impl TryFrom<Kind> for State {
}
impl State {
pub(crate) fn is_open(&self) -> bool {
match self {
State::Open | State::Pending | State::Procedure => true,
_ => false,
}
matches!(self, State::Open | State::Pending | State::Procedure)
}
pub(crate) fn kind(self) -> u16 {

View file

@ -18,6 +18,11 @@ use crate::helpers::{CHARACTER_THRESHOLD, format_timestamp_local, format_timesta
use crate::kinds::*;
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
const MAX_OFFSET: u64 = 9;
fn now() -> Timestamp {
Timestamp::now() + MAX_OFFSET
}
type TaskMap = HashMap<EventId, Task>;
#[derive(Debug, Clone)]
pub(crate) struct Tasks {
@ -49,8 +54,9 @@ pub(crate) struct Tasks {
sender: EventSender,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub(crate) enum StateFilter {
#[default]
Default,
All,
State(String),
@ -68,7 +74,7 @@ impl StateFilter {
match self {
StateFilter::Default => {
let state = task.pure_state();
state.is_open() || (state == State::Done && task.parent_id() != None)
state.is_open() || (state == State::Done && task.parent_id().is_some())
}
StateFilter::All => true,
StateFilter::State(filter) => task.state().is_some_and(|t| t.matches_label(filter)),
@ -83,11 +89,6 @@ impl StateFilter {
}
}
}
impl Default for StateFilter {
fn default() -> Self {
StateFilter::Default
}
}
impl Display for StateFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
@ -104,12 +105,7 @@ impl Display for StateFilter {
impl Tasks {
pub(crate) fn from(url: Option<Url>, tx: &tokio::sync::mpsc::Sender<MostrMessage>, keys: &Keys, metadata: Option<Metadata>) -> Self {
let mut new = Self::with_sender(EventSender {
url,
tx: tx.clone(),
keys: keys.clone(),
queue: Default::default(),
});
let mut new = Self::with_sender(EventSender::from(url, tx, keys));
metadata.map(|m| new.users.insert(keys.public_key(), m));
new
}
@ -155,18 +151,21 @@ impl Tasks {
self.get_position_ref().cloned()
}
fn now() -> Timestamp {
Timestamp::from(Timestamp::now() + Self::MAX_OFFSET)
pub(crate) fn get_position_ref(&self) -> Option<&EventId> {
self.get_position_at(now()).1
}
pub(crate) fn get_position_ref(&self) -> Option<&EventId> {
self.history_from(Self::now())
fn get_position_at(&self, timestamp: Timestamp) -> (Timestamp, Option<&EventId>) {
self.history_from(timestamp)
.last()
.and_then(|e| referenced_events(e))
.filter(|e| e.created_at <= timestamp)
.map_or_else(
|| (Timestamp::now(), None),
|e| (e.created_at, 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 {
fn get_task_tree<'a>(&'a self, id: &'a EventId) -> ChildIterator {
ChildIterator::from(self, id)
}
@ -220,7 +219,7 @@ impl Tasks {
vec.push(format!("{} started by {}", format_timestamp_local(stamp), self.get_author(key))));
vec
}).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))
(format!("Times Tracked on {:?}", self.get_task_title(id)), Box::from(history))
}
}
}
@ -287,6 +286,7 @@ impl Tasks {
.unwrap_or(String::new())
}
/// Iterate over the task referenced by the given id and all its available parents.
fn traverse_up_from(&self, id: Option<EventId>) -> ParentIterator {
ParentIterator {
tasks: &self.tasks,
@ -314,7 +314,7 @@ impl Tasks {
iter: impl Iterator<Item=&'a EventId>,
depth: i8,
) -> Box<impl Iterator<Item=&'a Task>> {
iter.filter_map(|id| self.get_by_id(&id))
iter.filter_map(|id| self.get_by_id(id))
.flat_map(move |task| {
let new_depth = depth - 1;
if new_depth == 0 {
@ -372,7 +372,7 @@ impl Tasks {
// TODO apply filters in transit
self.state.matches(t) &&
t.tags.as_ref().map_or(true, |tags| {
tags.iter().find(|tag| self.tags_excluded.contains(tag)).is_none()
!tags.iter().any(|tag| self.tags_excluded.contains(tag))
}) &&
(self.tags.is_empty() ||
t.tags.as_ref().map_or(false, |tags| {
@ -386,7 +386,7 @@ impl Tasks {
if self.depth == 0 {
return vec![];
}
if self.view.len() > 0 {
if !self.view.is_empty() {
return self.resolve_tasks(self.view.iter()).collect();
}
self.filtered_tasks(self.get_position_ref()).collect()
@ -396,15 +396,15 @@ impl Tasks {
let mut lock = stdout().lock();
if let Some(t) = self.get_current_task() {
let state = t.state_or_default();
let now = &Self::now();
let now = &now();
let mut tracking_stamp: Option<Timestamp> = None;
for elem in
timestamps(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![t.get_id()])
timestamps(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &[t.get_id()])
.map(|(e, _)| e) {
if tracking_stamp.is_some() && elem > now {
break;
}
tracking_stamp = Some(elem.clone())
tracking_stamp = Some(*elem)
}
writeln!(
lock,
@ -458,9 +458,8 @@ impl Tasks {
fn get_property(&self, task: &Task, str: &str) -> String {
let progress =
self
.total_progress(task.get_id())
.filter(|_| task.children.len() > 0);
self.total_progress(task.get_id())
.filter(|_| !task.children.is_empty());
let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0));
match str {
"subtasks" => {
@ -496,7 +495,7 @@ impl Tasks {
// TODO format strings configurable
"time" => display_time("MMMm", self.time_tracked(*task.get_id())),
"rtime" => display_time("HH:MM", self.total_time_tracked(*task.get_id())),
prop => task.get(prop).unwrap_or(String::new()),
prop => task.get(prop).unwrap_or_default(),
}
}
@ -515,15 +514,18 @@ impl Tasks {
self.view = view;
}
pub(crate) fn clear_filter(&mut self) {
pub(crate) fn clear_filters(&mut self) {
self.view.clear();
self.tags.clear();
self.tags_excluded.clear();
info!("Removed all filters");
}
pub(crate) fn has_tag_filter(&self) -> bool {
!self.tags.is_empty() || !self.tags_excluded.is_empty()
}
pub(crate) fn set_tags(&mut self, tags: impl IntoIterator<Item=Tag>) {
self.tags_excluded.clear();
self.tags.clear();
self.tags.extend(tags);
}
@ -563,12 +565,20 @@ impl Tasks {
}
/// Returns ids of tasks starting with the given string.
///
/// Tries, in order:
/// - single case-insensitive exact name match in visible tasks
/// - single case-insensitive exact name match in all tasks
/// - visible tasks starting with given arg case-sensitive
/// - visible tasks where any word starts with given arg case-insensitive
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 has_space = lowercase_arg.split_ascii_whitespace().count() > 1;
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let mut filtered_more: Vec<EventId> = Vec::with_capacity(32);
for task in self.filtered_tasks(position) {
let lowercase = task.event.content.to_ascii_lowercase();
@ -576,14 +586,21 @@ impl Tasks {
return vec![task.event.id];
} else if task.event.content.starts_with(arg) {
filtered.push(task.event.id)
} else if lowercase.starts_with(&lowercase_arg) {
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.starts_with(&lowercase_arg)) } {
filtered_more.push(task.event.id)
}
}
if filtered.len() == 0 {
for task in self.tasks.values() {
if task.event.content.to_ascii_lowercase() == lowercase_arg &&
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
// exclude closed tasks and their subtasks
return vec![task.event.id];
}
}
if filtered.is_empty() {
return filtered_more;
}
return filtered;
filtered
}
/// Finds out what to do with the given string.
@ -596,7 +613,7 @@ impl Tasks {
self.view.clear();
if arg.len() < CHARACTER_THRESHOLD {
warn!("New task name needs at least {CHARACTER_THRESHOLD} characters");
return None
return None;
}
Some(self.make_task_with(arg, self.position_tags_for(position), true))
}
@ -621,8 +638,6 @@ impl Tasks {
}).into_iter().flatten()
}
const MAX_OFFSET: u64 = 9;
pub(crate) fn move_to(&mut self, target: Option<EventId>) {
self.view.clear();
let pos = self.get_position_ref();
@ -638,8 +653,8 @@ impl Tasks {
}
let now = Timestamp::now();
let offset: u64 = self.history_from(now).skip_while(|e| e.created_at.as_u64() > now.as_u64() + Self::MAX_OFFSET).count() as u64;
if offset >= Self::MAX_OFFSET {
let offset: u64 = self.history_from(now).skip_while(|e| e.created_at.as_u64() > now.as_u64() + MAX_OFFSET).count() as u64;
if offset >= MAX_OFFSET {
warn!("Whoa you are moving around quickly! Give me a few seconds to process.")
}
self.submit(
@ -714,7 +729,7 @@ impl Tasks {
let id = self.submit(
build_task(input, input_tags, None)
.add_tags(self.tags.iter().cloned())
.add_tags(tags.into_iter())
.add_tags(tags)
);
if set_state {
self.state.as_option().inspect(|s| self.set_state_for_with(id, s));
@ -726,22 +741,39 @@ impl Tasks {
self.tasks.get(id).map_or(id.to_string(), |t| t.get_title())
}
/// Parse string and set tracking
/// Parse relative time string and track for current position
///
/// Returns false and prints a message if parsing failed
pub(crate) fn track_from(&mut self, str: &str) -> bool {
parse_tracking_stamp(str)
.map(|stamp| self.track_at(stamp, self.get_position()))
.and_then(|stamp| self.track_at(stamp, self.get_position()))
.is_some()
}
pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId {
info!("{} {}", task.map_or(
String::from("Stopping time-tracking at"),
|id| format!("Tracking \"{}\" from", self.get_task_title(&id))), format_timestamp_relative(&time));
pub(crate) fn track_at(&mut self, time: Timestamp, target: Option<EventId>) -> Option<EventId> {
let current_pos = self.get_position_at(time);
if (time < Timestamp::now() || target.is_none()) && current_pos.1 == target.as_ref() {
warn!("Already {} from {}",
target.map_or("stopped time-tracking".to_string(),
|id| format!("tracking \"{}\"", self.get_task_title(&id))),
format_timestamp_relative(&current_pos.0),
);
return None;
}
info!("{}", match target {
None => format!("Stopping time-tracking of \"{}\" at {}",
current_pos.1.map_or("???".to_string(), |id| self.get_task_title(id)),
format_timestamp_relative(&time)),
Some(new_id) => format!("Tracking \"{}\" from {}{}",
self.get_task_title(&new_id),
format_timestamp_relative(&time),
current_pos.1.filter(|id| id != &&new_id).map(
|id| format!(" replacing \"{}\"", self.get_task_title(id))).unwrap_or_default()),
});
self.submit(
build_tracking(task)
build_tracking(target)
.custom_created_at(time)
)
).into()
}
/// Sign and queue the event to the relay, returning its id
@ -832,13 +864,13 @@ impl Tasks {
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))
Some(self.set_state_for(*id, comment, state))
}
pub(crate) fn make_note(&mut self, note: &str) {
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());
let prop = build_prop(Kind::TextNote, note.trim(), *id);
self.submit(prop);
return;
}
@ -955,7 +987,7 @@ fn referenced_events(event: &Event) -> Option<&EventId> {
})
}
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 [&'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),
_ => None
@ -963,10 +995,10 @@ fn matching_tag_id<'a>(event: &'a Event, ids: &'a Vec<&'a EventId>) -> Option<&'
}
/// Filters out event timestamps to those that start or stop one of the given events
fn timestamps<'a>(events: impl Iterator<Item=&'a Event>, ids: &'a Vec<&'a EventId>) -> impl Iterator<Item=(&Timestamp, Option<&EventId>)> {
fn timestamps<'a>(events: impl Iterator<Item=&'a Event>, ids: &'a [&'a EventId]) -> impl Iterator<Item=(&Timestamp, Option<&EventId>)> {
events.map(|event| (&event.created_at, matching_tag_id(event, ids)))
.dedup_by(|(_, e1), (_, e2)| e1 == e2)
.skip_while(|element| element.1 == None)
.skip_while(|element| element.1.is_none())
}
/// Iterates Events to accumulate times tracked
@ -1003,7 +1035,7 @@ impl Iterator for Durations<'_> {
}
}
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)));
start.filter(|t| t < &now).map(|stamp| Duration::from_secs(now.saturating_sub(stamp)))
}
}
@ -1052,7 +1084,7 @@ impl<'a> Iterator for ChildIterator<'a> {
return None;
}
let id = self.queue[self.index];
if let Some(task) = self.tasks.get(&id) {
if let Some(task) = self.tasks.get(id) {
self.queue.reserve(task.children.len());
self.queue.extend(task.children.iter());
} else {
@ -1084,7 +1116,7 @@ impl<'a> Iterator for ParentIterator<'a> {
fn next(&mut self) -> Option<Self::Item> {
self.current.and_then(|id| self.tasks.get(&id)).map(|t| {
self.prev.map(|id| assert!(t.children.contains(&id)));
self.prev.inspect(|id| assert!(t.children.contains(id)));
self.prev = self.current;
self.current = t.parent_id().cloned();
t