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
- `.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
- `|[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.

View File

@ -4,6 +4,7 @@ use std::env::{args, var};
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader, stdin, stdout, Write};
use std::iter::once;
use std::ops::Sub;
use std::path::PathBuf;
use std::str::FromStr;
@ -20,7 +21,7 @@ use xdg::BaseDirectories;
use crate::helpers::*;
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};
mod helpers;
@ -339,15 +340,26 @@ async fn main() {
.map(|t| t.event.id)
.collect()
);
info!("Filtering for procedures");
}
Some(id) => {
tasks.set_state_for(id, "", State::Procedure);
}
},
Some(arg) => {
let id = tasks.make_task(arg);
tasks.set_state_for(id, "", State::Procedure);
tasks.move_to(Some(id));
Some(arg) => 'arm: {
if arg.chars().next() != Some('|') {
if let Some(pos) = tasks.get_position() {
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
}
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))
}

View File

@ -19,7 +19,7 @@ use TagStandard::Hashtag;
use crate::{Events, EventSender};
use crate::helpers::some_non_empty;
use crate::kinds::*;
use crate::task::{State, Task, TaskState};
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
type TaskMap = HashMap<EventId, Task>;
#[derive(Debug, Clone)]
@ -151,28 +151,8 @@ impl Tasks {
pub(crate) fn len(&self) -> usize { self.tasks.len() }
/// Ids of all subtasks recursively found for id, including itself
fn get_task_tree<'a>(&'a self, id: &'a EventId) -> Vec<&'a EventId> {
let mut children = Vec::with_capacity(32);
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 get_task_tree<'a>(&'a self, id: &'a EventId) -> ChildIterator {
ChildIterator::from(&self.tasks, id)
}
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
@ -237,7 +217,7 @@ impl Tasks {
fn total_time_tracked(&self, id: EventId) -> u64 {
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() {
total += TimesTracked::from(user, &children).into_iter().sum::<Duration>().as_secs();
}
@ -613,7 +593,6 @@ impl Tasks {
/// Expects sanitized input
pub(crate) fn parse_task(&self, input: &str) -> EventBuilder {
let mut tags: Vec<Tag> = self.tags.iter().cloned().collect();
self.position.inspect(|p| tags.push(Tag::event(*p)));
match input.split_once(": ") {
None => build_task(input, tags),
Some(s) => {
@ -629,34 +608,65 @@ 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| {
pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
Tag::from(TagStandard::Event {
event_id: id,
relay_url: self.sender.url.as_ref().map(|url| UncheckedUrl::new(url.as_str())),
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 {
t.children.iter()
.filter_map(|id| self.get_by_id(id))
.max()
.map(|t| {
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
.map(|t| tags.push(self.make_event_tag(&t.event, MARKER_DEPENDS)));
}
});
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(
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
}
@ -730,13 +740,15 @@ impl Tasks {
}
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) {
debug!("Did not insert duplicate event {}", event.id); // TODO warn in next sdk version
} 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> {
tasks: &'a TaskMap,
@ -968,6 +1026,8 @@ impl<'a> Iterator for ParentIterator<'a> {
#[cfg(test)]
mod tasks_test {
use std::collections::HashSet;
use super::*;
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]
fn test_tracking() {
let mut tasks = stub_tasks();