Compare commits

...

2 Commits

Author SHA1 Message Date
xeruf 619bcfbbad feat(tasks): generate tree from iterator 2024-08-12 23:08:06 +03:00
xeruf 85b923edc8 feat: enable creating dependent sibling task 2024-08-12 23:06:49 +03:00
4 changed files with 140 additions and 55 deletions

View File

@ -100,7 +100,8 @@ To stop time-tracking completely, simply move to the root of all tasks.
+ no match: create & activate task + no match: create & activate task
- `.2` - set view depth to `2`, which can be substituted for any number (how many subtask levels to show, default 1) - `.2` - set view depth to `2`, which can be substituted for any number (how many subtask levels to show, default 1)
- `/[TEXT]` - like `.`, but never creates a task - `/[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) - `||TASK` - create and activate a new task procedure (where subtasks automatically depend on the previously created task)
- `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up
Dots and slashes can be repeated to move to parent tasks. Dots and slashes can be repeated to move to parent tasks.

View File

@ -4,6 +4,7 @@ use std::env::{args, var};
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::{BufRead, BufReader, stdin, stdout, Write}; use std::io::{BufRead, BufReader, stdin, stdout, Write};
use std::iter::once;
use std::ops::Sub; use std::ops::Sub;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
@ -20,7 +21,7 @@ use xdg::BaseDirectories;
use crate::helpers::*; use crate::helpers::*;
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND}; use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
use crate::task::State; use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State};
use crate::tasks::{PropertyCollection, StateFilter, Tasks}; use crate::tasks::{PropertyCollection, StateFilter, Tasks};
mod helpers; mod helpers;
@ -339,15 +340,26 @@ async fn main() {
.map(|t| t.event.id) .map(|t| t.event.id)
.collect() .collect()
); );
info!("Filtering for procedures");
} }
Some(id) => { Some(id) => {
tasks.set_state_for(id, "", State::Procedure); tasks.set_state_for(id, "", State::Procedure);
} }
}, },
Some(arg) => { Some(arg) => 'arm: {
let id = tasks.make_task(arg); if arg.chars().next() != Some('|') {
tasks.set_state_for(id, "", State::Procedure); if let Some(pos) = tasks.get_position() {
tasks.move_to(Some(id)); tasks.move_up();
tasks.make_task_with(
arg,
once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS))
.chain(tasks.parent_tag()),
true);
break 'arm;
}
}
let arg: String = arg.chars().skip_while(|c| c == &'|').collect();
tasks.make_task_and_enter(&arg, State::Procedure);
} }
} }

View File

@ -63,7 +63,7 @@ impl Task {
&self.event.id &self.event.id
} }
fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> { pub(crate) fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> {
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker)) self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
} }

View File

@ -19,7 +19,7 @@ use TagStandard::Hashtag;
use crate::{Events, EventSender}; use crate::{Events, EventSender};
use crate::helpers::some_non_empty; use crate::helpers::some_non_empty;
use crate::kinds::*; use crate::kinds::*;
use crate::task::{State, Task, TaskState}; use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
type TaskMap = HashMap<EventId, Task>; type TaskMap = HashMap<EventId, Task>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -151,28 +151,8 @@ impl Tasks {
pub(crate) fn len(&self) -> usize { self.tasks.len() } pub(crate) fn len(&self) -> usize { self.tasks.len() }
/// Ids of all subtasks recursively found for id, including itself /// Ids of all subtasks recursively found for id, including itself
fn get_task_tree<'a>(&'a self, id: &'a EventId) -> Vec<&'a EventId> { pub(crate) fn get_task_tree<'a>(&'a self, id: &'a EventId) -> ChildIterator {
let mut children = Vec::with_capacity(32); ChildIterator::from(&self.tasks, id)
let mut index = 0;
children.push(id);
while index < children.len() {
let id = children[index];
if let Some(task) = self.tasks.get(&id) {
children.reserve(task.children.len());
children.extend(task.children.iter());
} else {
// Unknown task, can still find children
for task in self.tasks.values() {
if task.parent_id().is_some_and(|i| i == id) {
children.push(task.get_id());
}
}
}
index += 1;
}
children
} }
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> { pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
@ -237,7 +217,7 @@ impl Tasks {
fn total_time_tracked(&self, id: EventId) -> u64 { fn total_time_tracked(&self, id: EventId) -> u64 {
let mut total = 0; let mut total = 0;
let children = self.get_task_tree(&id); 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 += TimesTracked::from(user, &children).into_iter().sum::<Duration>().as_secs();
} }
@ -613,7 +593,6 @@ impl Tasks {
/// Expects sanitized input /// Expects sanitized input
pub(crate) fn parse_task(&self, input: &str) -> EventBuilder { pub(crate) fn parse_task(&self, input: &str) -> EventBuilder {
let mut tags: Vec<Tag> = self.tags.iter().cloned().collect(); let mut tags: Vec<Tag> = self.tags.iter().cloned().collect();
self.position.inspect(|p| tags.push(Tag::event(*p)));
match input.split_once(": ") { match input.split_once(": ") {
None => build_task(input, tags), None => build_task(input, tags),
Some(s) => { Some(s) => {
@ -629,34 +608,65 @@ impl Tasks {
} }
} }
/// Creates a task following the current state pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
/// Sanitizes input Tag::from(TagStandard::Event {
pub(crate) fn make_task(&mut self, input: &str) -> EventId { event_id: id,
let tag: Option<Tag> = self.get_current_task() relay_url: self.sender.url.as_ref().map(|url| UncheckedUrl::new(url.as_str())),
.and_then(|t| { marker: Some(Marker::Custom(marker.to_string())),
public_key: self.get_by_id(&id).map(|e| e.event.pubkey),
})
}
pub(crate) fn make_event_tag(&self, event: &Event, marker: &str) -> Tag {
Tag::from(TagStandard::Event {
event_id: event.id,
relay_url: self.sender.url.as_ref().map(|url| UncheckedUrl::new(url.as_str())),
marker: Some(Marker::Custom(marker.to_string())),
public_key: Some(event.pubkey),
})
}
pub(crate) fn parent_tag(&self) -> Option<Tag> {
self.position.map(|p| self.make_event_tag_from_id(p, MARKER_PARENT))
}
pub(crate) fn position_tags(&self) -> Vec<Tag> {
let mut tags = Vec::with_capacity(2);
self.parent_tag().map(|t| tags.push(t));
self.get_current_task()
.map(|t| {
if t.pure_state() == State::Procedure { if t.pure_state() == State::Procedure {
t.children.iter() t.children.iter()
.filter_map(|id| self.get_by_id(id)) .filter_map(|id| self.get_by_id(id))
.max() .max()
.map(|t| { .map(|t| tags.push(self.make_event_tag(&t.event, MARKER_DEPENDS)));
Tag::from(
TagStandard::Event {
event_id: t.event.id,
relay_url: self.sender.url.as_ref().map(|url| UncheckedUrl::new(url.as_str())),
marker: Some(Marker::Custom("depends".to_string())),
public_key: Some(t.event.pubkey),
}
)
})
} else {
None
} }
}); });
tags
}
/// Creates a task following the current state
/// Sanitizes input
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
self.make_task_with(input, self.position_tags(), true)
}
pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) {
let id = self.make_task_with(input, self.position_tags(), false);
self.set_state_for(id, "", state);
self.move_to(Some(id));
}
/// Creates a task
/// Sanitizes input
pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId {
let id = self.submit( let id = self.submit(
self.parse_task(input.trim()) self.parse_task(input.trim())
.add_tags(tag.into_iter()) .add_tags(tags.into_iter())
); );
self.state.as_option().inspect(|s| self.set_state_for_with(id, s)); if set_state {
self.state.as_option().inspect(|s| self.set_state_for_with(id, s));
}
id id
} }
@ -730,13 +740,15 @@ impl Tasks {
} }
pub(crate) fn add_task(&mut self, event: Event) { pub(crate) fn add_task(&mut self, event: Event) {
self.referenced_tasks(&event, |t| {
t.children.insert(event.id);
});
if self.tasks.contains_key(&event.id) { if self.tasks.contains_key(&event.id) {
debug!("Did not insert duplicate event {}", event.id); // TODO warn in next sdk version debug!("Did not insert duplicate event {}", event.id); // TODO warn in next sdk version
} else { } else {
self.tasks.insert(event.id, Task::new(event)); let id = event.id;
let task = Task::new(event);
task.find_refs(MARKER_PARENT).for_each(|parent| {
self.tasks.get_mut(parent).map(|t| { t.children.insert(id); });
});
self.tasks.insert(id, task);
} }
} }
@ -946,6 +958,52 @@ impl Iterator for TimesTracked<'_> {
} }
} }
/// Breadth-First Iterator over Tasks and recursive children
struct ChildIterator<'a> {
tasks: &'a TaskMap,
queue: Vec<&'a EventId>,
index: usize,
}
impl<'a> ChildIterator<'a> {
fn from(tasks: &'a TaskMap, id: &'a EventId) -> Self {
let mut queue = Vec::with_capacity(30);
queue.push(id);
ChildIterator {
tasks,
queue,
index: 0,
}
}
fn get_all(mut self) -> Vec<&'a EventId> {
while self.next().is_some() {}
self.queue
}
}
impl<'a> Iterator for ChildIterator<'a> {
type Item = &'a EventId;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.queue.len() {
return None;
}
let id = self.queue[self.index];
if let Some(task) = self.tasks.get(&id) {
self.queue.reserve(task.children.len());
self.queue.extend(task.children.iter());
} else {
// Unknown task, can still find children
for task in self.tasks.values() {
if task.parent_id().is_some_and(|i| i == id) {
self.queue.push(task.get_id());
}
}
}
self.index += 1;
Some(id)
}
}
struct ParentIterator<'a> { struct ParentIterator<'a> {
tasks: &'a TaskMap, tasks: &'a TaskMap,
@ -968,6 +1026,8 @@ impl<'a> Iterator for ParentIterator<'a> {
#[cfg(test)] #[cfg(test)]
mod tasks_test { mod tasks_test {
use std::collections::HashSet;
use super::*; use super::*;
fn stub_tasks() -> Tasks { fn stub_tasks() -> Tasks {
@ -983,6 +1043,18 @@ mod tasks_test {
}) })
} }
#[test]
fn test_procedures() {
let mut tasks = stub_tasks();
tasks.make_task_and_enter("proc: tags", State::Procedure);
let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)]));
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::<EventId>::new());
let subid = tasks.make_task("sub");
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([subid]));
let sub = tasks.get_by_id(&subid).unwrap();
assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new());
}
#[test] #[test]
fn test_tracking() { fn test_tracking() {
let mut tasks = stub_tasks(); let mut tasks = stub_tasks();