Compare commits
6 Commits
5e6b274fe3
...
516acadd4a
Author | SHA1 | Date |
---|---|---|
xeruf | 516acadd4a | |
xeruf | 945eb6906a | |
xeruf | 34657540de | |
xeruf | afe3fa8670 | |
xeruf | 753afad2fd | |
xeruf | 7755967a7a |
|
@ -106,6 +106,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
|||
- `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up
|
||||
|
||||
Dot or slash can be repeated to move to parent tasks before acting.
|
||||
Append `@TIME` to any task creation or change command to record the action with the given time.
|
||||
|
||||
- `:[IND][PROP]` - add property column PROP at IND or end,
|
||||
if it already exists remove property column PROP or IND; empty: list properties
|
||||
|
|
|
@ -11,6 +11,25 @@ pub fn some_non_empty(str: &str) -> Option<String> {
|
|||
if str.is_empty() { None } else { Some(str.to_string()) }
|
||||
}
|
||||
|
||||
pub fn trim_start_count(str: &str, char: char) -> (&str, usize) {
|
||||
let len = str.len();
|
||||
let result = str.trim_start_matches(char);
|
||||
let dots = len - result.len();
|
||||
(result, dots)
|
||||
}
|
||||
|
||||
pub trait ToTimestamp {
|
||||
fn to_timestamp(&self) -> Timestamp;
|
||||
}
|
||||
impl<T: TimeZone> ToTimestamp for DateTime<T> {
|
||||
fn to_timestamp(&self) -> Timestamp {
|
||||
let stamp = self.to_utc().timestamp();
|
||||
if let Some(t) = 0u64.checked_add_signed(stamp) {
|
||||
Timestamp::from(t)
|
||||
} else { Timestamp::zero() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the hour from a plain number in the String,
|
||||
/// with max of max_future hours into the future.
|
||||
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
||||
|
@ -57,7 +76,7 @@ pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
|
|||
/// - Otherwise try to parse a relative date
|
||||
pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
|
||||
if let Some(num) = parse_hour(str, 6) {
|
||||
return Some(Timestamp::from(num.to_utc().timestamp() as u64));
|
||||
return Some(num.to_timestamp());
|
||||
}
|
||||
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
|
||||
if let Ok(num) = stripped.parse::<i64>() {
|
||||
|
|
112
src/main.rs
112
src/main.rs
|
@ -30,7 +30,7 @@ use xdg::BaseDirectories;
|
|||
use crate::helpers::*;
|
||||
use crate::kinds::{BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND};
|
||||
use crate::task::{State, MARKER_DEPENDS};
|
||||
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
||||
use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
|
||||
|
||||
mod helpers;
|
||||
mod task;
|
||||
|
@ -251,8 +251,8 @@ async fn main() -> Result<()> {
|
|||
let moved_metadata = metadata.clone();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<MostrMessage>(64);
|
||||
let tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys, metadata.clone());
|
||||
let mut relays: HashMap<Option<Url>, Tasks> =
|
||||
let tasks_for_url = |url: Option<Url>| TasksRelay::from(url, &tx, &keys, metadata.clone());
|
||||
let mut relays: HashMap<Option<Url>, TasksRelay> =
|
||||
client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect();
|
||||
|
||||
let sender = tokio::spawn(async move {
|
||||
|
@ -326,10 +326,6 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
loop {
|
||||
trace!("All Root Tasks:\n{}", relays.iter().map(|(url, tasks)|
|
||||
format!("{}: [{}]",
|
||||
url.as_ref().map(ToString::to_string).unwrap_or(LOCAL_RELAY_NAME.to_string()),
|
||||
tasks.children_of(None).map(|id| tasks.get_task_title(id)).join("; "))).join("\n"));
|
||||
println!();
|
||||
let tasks = relays.get(&selected_relay).unwrap();
|
||||
let prompt = format!(
|
||||
|
@ -361,36 +357,53 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
if count > 0 {
|
||||
info!("Received {count} Updates");
|
||||
} else {
|
||||
relays.values_mut().for_each(|tasks| tasks.process_overflow());
|
||||
}
|
||||
|
||||
let mut iter = input.chars();
|
||||
let op = iter.next();
|
||||
let arg = if input.len() > 1 {
|
||||
Some(input[1..].trim())
|
||||
let tasks = relays.get_mut(&selected_relay).unwrap();
|
||||
|
||||
let operator = input.chars().next();
|
||||
let mut command = input;
|
||||
match operator {
|
||||
None => {
|
||||
debug!("Flushing Tasks because of empty command");
|
||||
tasks.flush();
|
||||
or_warn!(tasks.print_tasks());
|
||||
continue;
|
||||
}
|
||||
Some('@') => {}
|
||||
Some(_) => {
|
||||
if let Some((left, arg)) = command.split_once("@") {
|
||||
if let Some(time) = parse_hour(arg, 20)
|
||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) {
|
||||
command = left.to_string();
|
||||
tasks.custom_time = Some(time.to_timestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arg = if command.len() > 1 {
|
||||
Some(command[1..].trim())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let arg_default = arg.unwrap_or("");
|
||||
let tasks = relays.get_mut(&selected_relay).unwrap();
|
||||
match op {
|
||||
None => {
|
||||
debug!("Flushing Tasks because of empty command");
|
||||
tasks.flush();
|
||||
}
|
||||
|
||||
match operator {
|
||||
Some(':') => {
|
||||
let mut iter = arg_default.chars();
|
||||
let next = iter.next();
|
||||
let remaining = iter.collect::<String>().trim().to_string();
|
||||
if let Some(':') = next {
|
||||
let str: String = iter.collect();
|
||||
let result = str.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>();
|
||||
if result.len() == 1 {
|
||||
tasks.add_sorting_property(str.trim().to_string())
|
||||
let props = remaining.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>();
|
||||
if props.len() == 1 {
|
||||
tasks.add_sorting_property(remaining)
|
||||
} else {
|
||||
tasks.set_sorting(result)
|
||||
tasks.set_sorting(props)
|
||||
}
|
||||
} else if let Some(digit) = next.and_then(|s| s.to_digit(10)) {
|
||||
let index = (digit as usize).saturating_sub(1);
|
||||
let remaining = iter.collect::<String>().trim().to_string();
|
||||
if remaining.is_empty() {
|
||||
tasks.get_columns().remove_at(index);
|
||||
} else {
|
||||
|
@ -469,11 +482,7 @@ async fn main() -> Result<()> {
|
|||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
||||
.map(|time| {
|
||||
info!("Filtering for tasks from {}", format_datetime_relative(time));
|
||||
let threshold = time.to_utc().timestamp();
|
||||
tasks.set_filter_from(
|
||||
if let Some(t) = 0u64.checked_add_signed(threshold) {
|
||||
Timestamp::from(t)
|
||||
} else { Timestamp::zero() })
|
||||
tasks.set_filter_from(time.to_timestamp())
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
@ -609,59 +618,49 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Some('.') => {
|
||||
let mut dots = 1;
|
||||
let mut pos = tasks.get_position_ref();
|
||||
for _ in iter.take_while(|c| c == &'.') {
|
||||
dots += 1;
|
||||
pos = tasks.get_parent(pos);
|
||||
}
|
||||
let (remaining, dots) = trim_start_count(&command, '.');
|
||||
let pos = tasks.up_by(dots - 1);
|
||||
|
||||
let slice = input[dots..].trim();
|
||||
if slice.is_empty() {
|
||||
if remaining.is_empty() {
|
||||
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::<usize>() {
|
||||
} else if let Ok(depth) = remaining.parse::<usize>() {
|
||||
if pos != tasks.get_position_ref() {
|
||||
tasks.move_to(pos.cloned());
|
||||
}
|
||||
tasks.set_depth(depth);
|
||||
} else {
|
||||
tasks.filter_or_create(pos.cloned().as_ref(), slice).map(|id| tasks.move_to(Some(id)));
|
||||
tasks.filter_or_create(pos.cloned().as_ref(), &remaining).map(|id| tasks.move_to(Some(id)));
|
||||
}
|
||||
}
|
||||
|
||||
Some('/') => if arg.is_none() {
|
||||
tasks.move_to(None);
|
||||
} else {
|
||||
let mut dots = 1;
|
||||
let mut pos = tasks.get_position_ref();
|
||||
for _ in iter.take_while(|c| c == &'/') {
|
||||
dots += 1;
|
||||
pos = tasks.get_parent(pos);
|
||||
}
|
||||
let (remaining, dots) = trim_start_count(&command, '/');
|
||||
let pos = tasks.up_by(dots - 1);
|
||||
|
||||
let slice = input[dots..].trim();
|
||||
if slice.is_empty() {
|
||||
if remaining.is_empty() {
|
||||
tasks.move_to(pos.cloned());
|
||||
if dots > 1 {
|
||||
info!("Moving up {} tasks", dots - 1)
|
||||
}
|
||||
} else {
|
||||
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
|
||||
if !slice.chars().any(|c| c.is_ascii_uppercase()) {
|
||||
if !remaining.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());
|
||||
}
|
||||
|
||||
let filtered =
|
||||
tasks.get_filtered(|t| {
|
||||
transform(&t.event.content).contains(slice) ||
|
||||
transform(&t.event.content).contains(&remaining) ||
|
||||
t.tags.iter().flatten().any(
|
||||
|tag| tag.content().is_some_and(|s| transform(s).contains(slice)))
|
||||
|tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
|
||||
});
|
||||
if filtered.len() == 1 {
|
||||
tasks.move_to(filtered.into_iter().next());
|
||||
|
@ -673,14 +672,14 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
_ =>
|
||||
if Regex::new("^wss?://").unwrap().is_match(input.trim()) {
|
||||
if Regex::new("^wss?://").unwrap().is_match(command.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))) {
|
||||
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&command))) {
|
||||
selected_relay.clone_from(url);
|
||||
or_warn!(tasks.print_tasks());
|
||||
continue;
|
||||
}
|
||||
or_warn!(Url::parse(&input), "Failed to parse url {}", input).map(|url| {
|
||||
or_warn!(Url::parse(&command), "Failed to parse url {}", command).map(|url| {
|
||||
match tx.try_send(MostrMessage::NewRelay(url.clone())) {
|
||||
Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"),
|
||||
Ok(_) => {
|
||||
|
@ -691,16 +690,17 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
});
|
||||
continue;
|
||||
} else if input.contains('\n') {
|
||||
input.split('\n').for_each(|line| {
|
||||
} else if command.contains('\n') {
|
||||
command.split('\n').for_each(|line| {
|
||||
if !line.trim().is_empty() {
|
||||
tasks.make_task(line);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tasks.filter_or_create(tasks.get_position().as_ref(), &input);
|
||||
tasks.filter_or_create(tasks.get_position().as_ref(), &command);
|
||||
}
|
||||
}
|
||||
tasks.custom_time = None;
|
||||
or_warn!(tasks.print_tasks());
|
||||
}
|
||||
Err(ReadlineError::Eof) => break,
|
||||
|
|
23
src/task.rs
23
src/task.rs
|
@ -24,9 +24,6 @@ pub(crate) struct Task {
|
|||
pub(crate) tags: Option<BTreeSet<Tag>>,
|
||||
/// Task references derived from the event tags
|
||||
refs: Vec<(String, EventId)>,
|
||||
|
||||
/// Reference to children, populated dynamically
|
||||
pub(crate) children: HashSet<EventId>,
|
||||
/// Events belonging to this task, such as state updates and notes
|
||||
pub(crate) props: BTreeSet<Event>,
|
||||
}
|
||||
|
@ -52,7 +49,6 @@ impl Task {
|
|||
});
|
||||
// Separate refs for dependencies
|
||||
Task {
|
||||
children: Default::default(),
|
||||
props: Default::default(),
|
||||
tags: Some(tags).filter(|t: &BTreeSet<Tag>| !t.is_empty()),
|
||||
refs,
|
||||
|
@ -94,12 +90,16 @@ impl Task {
|
|||
self.description_events().map(|e| &e.content)
|
||||
}
|
||||
|
||||
pub(crate) fn is_task_kind(&self) -> bool {
|
||||
self.event.kind == TASK_KIND
|
||||
}
|
||||
|
||||
pub(crate) fn is_task(&self) -> bool {
|
||||
self.event.kind == TASK_KIND ||
|
||||
self.is_task_kind() ||
|
||||
self.states().next().is_some()
|
||||
}
|
||||
|
||||
fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
|
||||
fn states(&self) -> impl DoubleEndedIterator<Item=TaskState> + '_ {
|
||||
self.props.iter().filter_map(|event| {
|
||||
event.kind.try_into().ok().map(|s| TaskState {
|
||||
name: some_non_empty(&event.content),
|
||||
|
@ -114,7 +114,16 @@ impl Task {
|
|||
}
|
||||
|
||||
pub(crate) fn state(&self) -> Option<TaskState> {
|
||||
self.states().last()
|
||||
let now = Timestamp::now();
|
||||
// TODO do not iterate constructed state objects
|
||||
let state = self.states().rev().take_while_inclusive(|ts| ts.time > now);
|
||||
state.last().map(|ts| {
|
||||
if ts.time <= now {
|
||||
ts
|
||||
} else {
|
||||
self.default_state()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn pure_state(&self) -> State {
|
||||
|
|
161
src/tasks.rs
161
src/tasks.rs
|
@ -1,4 +1,4 @@
|
|||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::{stdout, Error, Write};
|
||||
use std::iter::{empty, once, FusedIterator};
|
||||
|
@ -6,26 +6,47 @@ use std::ops::{Div, Rem};
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
|
||||
use crate::kinds::*;
|
||||
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
|
||||
use crate::{EventSender, MostrMessage};
|
||||
use colored::Colorize;
|
||||
use itertools::{Either, Itertools};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::prelude::Marker;
|
||||
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use TagStandard::Hashtag;
|
||||
|
||||
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
|
||||
use crate::kinds::*;
|
||||
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
|
||||
use crate::{EventSender, MostrMessage};
|
||||
|
||||
const MAX_OFFSET: u64 = 9;
|
||||
fn now() -> Timestamp {
|
||||
Timestamp::now() + MAX_OFFSET
|
||||
}
|
||||
|
||||
type TaskMap = HashMap<EventId, Task>;
|
||||
trait TaskMapMethods {
|
||||
fn children_of<'a>(&'a self, task: &'a Task) -> impl Iterator<Item=&Task> + 'a;
|
||||
fn children_for<'a>(&'a self, id: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a;
|
||||
fn children_ids_for<'a>(&'a self, id: &'a EventId) -> impl Iterator<Item=&EventId> + 'a;
|
||||
}
|
||||
impl TaskMapMethods for TaskMap {
|
||||
fn children_of<'a>(&'a self, task: &'a Task) -> impl Iterator<Item=&Task> + 'a {
|
||||
self.children_for(Some(task.get_id()))
|
||||
}
|
||||
|
||||
fn children_for<'a>(&'a self, id: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a {
|
||||
self.values()
|
||||
.filter(move |t| t.parent_id() == id)
|
||||
}
|
||||
|
||||
fn children_ids_for<'a>(&'a self, id: &'a EventId) -> impl Iterator<Item=&EventId> + 'a {
|
||||
self.children_for(Some(id))
|
||||
.map(|t| t.get_id())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Tasks {
|
||||
pub(crate) struct TasksRelay {
|
||||
/// The Tasks
|
||||
tasks: TaskMap,
|
||||
/// History of active tasks by PubKey
|
||||
|
@ -54,6 +75,8 @@ pub(crate) struct Tasks {
|
|||
state: StateFilter,
|
||||
|
||||
sender: EventSender,
|
||||
overflow: VecDeque<Event>,
|
||||
pub(crate) custom_time: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -102,10 +125,10 @@ impl Display for StateFilter {
|
|||
}
|
||||
}
|
||||
|
||||
impl Tasks {
|
||||
impl TasksRelay {
|
||||
pub(crate) fn from(
|
||||
url: Option<Url>,
|
||||
tx: &tokio::sync::mpsc::Sender<MostrMessage>,
|
||||
tx: &Sender<MostrMessage>,
|
||||
keys: &Keys,
|
||||
metadata: Option<Metadata>,
|
||||
) -> Self {
|
||||
|
@ -115,7 +138,7 @@ impl Tasks {
|
|||
}
|
||||
|
||||
pub(crate) fn with_sender(sender: EventSender) -> Self {
|
||||
Tasks {
|
||||
TasksRelay {
|
||||
tasks: Default::default(),
|
||||
history: Default::default(),
|
||||
users: Default::default(),
|
||||
|
@ -142,7 +165,25 @@ impl Tasks {
|
|||
tags_excluded: Default::default(),
|
||||
state: Default::default(),
|
||||
depth: 1,
|
||||
|
||||
sender,
|
||||
overflow: Default::default(),
|
||||
custom_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_overflow(&mut self) {
|
||||
let elements = self.overflow.len();
|
||||
for _ in 0..elements {
|
||||
if let Some(event) = self.overflow.pop_front() {
|
||||
if let Some(event) = self.add_prop(event) {
|
||||
warn!("Unable to sort Event {:?}", event);
|
||||
//self.overflow.push_back(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
if elements > 0 {
|
||||
info!("Reprocessed {elements} updates{}", self.sender.url.clone().map(|url| format!(" from {url}")).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,13 +288,13 @@ impl Tasks {
|
|||
}
|
||||
|
||||
fn total_progress(&self, id: &EventId) -> Option<f32> {
|
||||
self.get_by_id(id).and_then(|t| match t.pure_state() {
|
||||
self.get_by_id(id).and_then(|task| match task.pure_state() {
|
||||
State::Closed => None,
|
||||
State::Done => Some(1.0),
|
||||
_ => {
|
||||
let mut sum = 0f32;
|
||||
let mut count = 0;
|
||||
for prog in t.children.iter().filter_map(|e| self.total_progress(e)) {
|
||||
for prog in self.tasks.children_ids_for(task.get_id()).filter_map(|e| self.total_progress(e)) {
|
||||
sum += prog;
|
||||
count += 1;
|
||||
}
|
||||
|
@ -270,6 +311,14 @@ impl Tasks {
|
|||
|
||||
// Parents
|
||||
|
||||
pub(crate) fn up_by(&self, count: usize) -> Option<&EventId> {
|
||||
let mut pos = self.get_position_ref();
|
||||
for _ in 0..count {
|
||||
pos = self.get_parent(pos);
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
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())
|
||||
|
@ -296,7 +345,6 @@ impl Tasks {
|
|||
ParentIterator {
|
||||
tasks: &self.tasks,
|
||||
current: id,
|
||||
prev: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,7 +360,7 @@ impl Tasks {
|
|||
|
||||
fn resolve_tasks<'a>(
|
||||
&'a self,
|
||||
iter: impl Iterator<Item=&'a EventId>,
|
||||
iter: impl Iterator<Item=&'a Task>,
|
||||
sparse: bool,
|
||||
) -> Vec<&'a Task> {
|
||||
self.resolve_tasks_rec(iter, sparse, self.depth)
|
||||
|
@ -320,15 +368,14 @@ impl Tasks {
|
|||
|
||||
fn resolve_tasks_rec<'a>(
|
||||
&'a self,
|
||||
iter: impl Iterator<Item=&'a EventId>,
|
||||
iter: impl Iterator<Item=&'a Task>,
|
||||
sparse: bool,
|
||||
depth: usize,
|
||||
) -> Vec<&'a Task> {
|
||||
iter.filter_map(|id| self.get_by_id(id))
|
||||
.flat_map(move |task| {
|
||||
iter.flat_map(move |task| {
|
||||
let new_depth = depth - 1;
|
||||
if new_depth > 0 {
|
||||
let mut children = self.resolve_tasks_rec(task.children.iter(), sparse, new_depth);
|
||||
let mut children = self.resolve_tasks_rec(self.tasks.children_of(&task), sparse, new_depth);
|
||||
if !children.is_empty() {
|
||||
if !sparse {
|
||||
children.push(task);
|
||||
|
@ -363,13 +410,6 @@ impl Tasks {
|
|||
self.get_position_ref().and_then(|id| self.get_by_id(id))
|
||||
}
|
||||
|
||||
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)
|
||||
.map(|t| t.get_id())
|
||||
}
|
||||
|
||||
fn filter(&self, task: &Task) -> bool {
|
||||
self.state.matches(task) &&
|
||||
task.tags.as_ref().map_or(true, |tags| {
|
||||
|
@ -383,10 +423,10 @@ impl Tasks {
|
|||
}
|
||||
|
||||
pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>, sparse: bool) -> Vec<&'a Task> {
|
||||
let mut current = self.resolve_tasks(self.children_of(position), sparse);
|
||||
let mut current = self.resolve_tasks(self.tasks.children_for(position), sparse);
|
||||
if current.is_empty() {
|
||||
if !self.tags.is_empty() {
|
||||
let mut children = self.children_of(self.get_position_ref()).peekable();
|
||||
let mut children = self.tasks.children_for(self.get_position_ref()).peekable();
|
||||
if children.peek().is_some() {
|
||||
current = self.resolve_tasks_rec(children, true, 9);
|
||||
if sparse {
|
||||
|
@ -492,15 +532,16 @@ impl Tasks {
|
|||
}
|
||||
|
||||
fn get_property(&self, task: &Task, str: &str) -> String {
|
||||
let mut children = self.tasks.children_of(task).peekable();
|
||||
let progress =
|
||||
self.total_progress(task.get_id())
|
||||
.filter(|_| !task.children.is_empty());
|
||||
.filter(|_| children.peek().is_some());
|
||||
let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0));
|
||||
match str {
|
||||
"subtasks" => {
|
||||
let mut total = 0;
|
||||
let mut done = 0;
|
||||
for subtask in task.children.iter().filter_map(|id| self.get_by_id(id)) {
|
||||
for subtask in children {
|
||||
let state = subtask.pure_state();
|
||||
total += &(state != State::Closed).into();
|
||||
done += &(state == State::Done).into();
|
||||
|
@ -566,6 +607,7 @@ impl Tasks {
|
|||
}
|
||||
|
||||
pub(crate) fn set_filter_from(&mut self, time: Timestamp) -> bool {
|
||||
// TODO filter at both ends
|
||||
self.set_filter(|t| t.last_state_update() > time)
|
||||
}
|
||||
|
||||
|
@ -798,10 +840,9 @@ impl Tasks {
|
|||
let mut tags = Vec::with_capacity(2);
|
||||
tags.push(self.make_event_tag_from_id(*pos, MARKER_PARENT));
|
||||
self.get_by_id(pos)
|
||||
.map(|t| {
|
||||
if t.pure_state() == State::Procedure {
|
||||
t.children.iter()
|
||||
.filter_map(|id| self.get_by_id(id))
|
||||
.map(|task| {
|
||||
if task.pure_state() == State::Procedure {
|
||||
self.tasks.children_of(task)
|
||||
.max()
|
||||
.map(|t| tags.push(self.make_event_tag(&t.event, MARKER_DEPENDS)));
|
||||
}
|
||||
|
@ -886,7 +927,10 @@ impl Tasks {
|
|||
}
|
||||
|
||||
/// Sign and queue the event to the relay, returning its id
|
||||
fn submit(&mut self, builder: EventBuilder) -> EventId {
|
||||
fn submit(&mut self, mut builder: EventBuilder) -> EventId {
|
||||
if let Some(stamp) = self.custom_time {
|
||||
builder = builder.custom_created_at(stamp);
|
||||
}
|
||||
let event = self.sender.submit(builder).unwrap();
|
||||
let id = event.id;
|
||||
self.add(event);
|
||||
|
@ -913,7 +957,10 @@ impl Tasks {
|
|||
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
|
||||
}
|
||||
} else {
|
||||
self.add_prop(event)
|
||||
if let Some(event) = self.add_prop(event) {
|
||||
debug!("Requeueing unknown Event {:?}", event);
|
||||
self.overflow.push_back(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -925,24 +972,23 @@ impl Tasks {
|
|||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_prop(&mut self, event: Event) {
|
||||
/// Add event as prop, returning it if not processable
|
||||
fn add_prop(&mut self, event: Event) -> Option<Event> {
|
||||
let found = self.referenced_tasks(&event, |t| {
|
||||
t.props.insert(event.clone());
|
||||
});
|
||||
if !found {
|
||||
if event.kind.as_u16() == 1 {
|
||||
self.add_task(event);
|
||||
return;
|
||||
} else {
|
||||
return Some(event)
|
||||
}
|
||||
warn!("Unknown event {:?}", event)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {
|
||||
|
@ -1246,7 +1292,7 @@ impl<'a> ChildIterator<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn from(tasks: &'a Tasks, id: &'a EventId) -> Self {
|
||||
fn from(tasks: &'a TasksRelay, id: &'a EventId) -> Self {
|
||||
let mut queue = Vec::with_capacity(30);
|
||||
queue.push(id);
|
||||
ChildIterator {
|
||||
|
@ -1330,8 +1376,7 @@ impl<'a> ChildIterator<'a> {
|
|||
if let Some(task) = self.tasks.get(id) {
|
||||
let take = filter(task);
|
||||
if take.takes_children() {
|
||||
self.queue.reserve(task.children.len());
|
||||
self.queue.extend(task.children.iter());
|
||||
self.queue_children_of(&task);
|
||||
}
|
||||
if take.takes_self() {
|
||||
self.check_depth();
|
||||
|
@ -1342,6 +1387,10 @@ impl<'a> ChildIterator<'a> {
|
|||
self.next_filtered(filter)
|
||||
})
|
||||
}
|
||||
|
||||
fn queue_children_of(&mut self, task: &'a Task) {
|
||||
self.queue.extend(self.tasks.children_ids_for(task.get_id()));
|
||||
}
|
||||
}
|
||||
impl FusedIterator for ChildIterator<'_> {}
|
||||
impl<'a> Iterator for ChildIterator<'a> {
|
||||
|
@ -1359,8 +1408,7 @@ impl<'a> Iterator for ChildIterator<'a> {
|
|||
}
|
||||
}
|
||||
Some(task) => {
|
||||
self.queue.reserve(task.children.len());
|
||||
self.queue.extend(task.children.iter());
|
||||
self.queue_children_of(&task);
|
||||
}
|
||||
}
|
||||
self.check_depth();
|
||||
|
@ -1372,23 +1420,12 @@ impl<'a> Iterator for ChildIterator<'a> {
|
|||
struct ParentIterator<'a> {
|
||||
tasks: &'a TaskMap,
|
||||
current: Option<EventId>,
|
||||
/// Inexpensive helper to assert correctness
|
||||
prev: Option<EventId>,
|
||||
}
|
||||
impl<'a> Iterator for ParentIterator<'a> {
|
||||
type Item = &'a Task;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.current.and_then(|id| self.tasks.get(&id)).map(|t| {
|
||||
self.prev.inspect(|id| {
|
||||
// Fails if child is discovered before parent
|
||||
// Need to reverse add as well
|
||||
//assert!(t.children.contains(id))
|
||||
if !t.children.contains(id) {
|
||||
warn!("\"{}\" is missing child \"{}\"", t.get_title(), self.tasks.get(id).map_or(id.to_string(), |cht| cht.get_title()))
|
||||
}
|
||||
});
|
||||
self.prev = self.current;
|
||||
self.current = t.parent_id().cloned();
|
||||
t
|
||||
})
|
||||
|
@ -1401,12 +1438,12 @@ mod tasks_test {
|
|||
|
||||
use super::*;
|
||||
|
||||
fn stub_tasks() -> Tasks {
|
||||
fn stub_tasks() -> TasksRelay {
|
||||
use tokio::sync::mpsc;
|
||||
use nostr_sdk::Keys;
|
||||
|
||||
let (tx, _rx) = mpsc::channel(16);
|
||||
Tasks::with_sender(EventSender {
|
||||
TasksRelay::with_sender(EventSender {
|
||||
url: None,
|
||||
tx,
|
||||
keys: Keys::generate(),
|
||||
|
@ -1472,9 +1509,11 @@ mod tasks_test {
|
|||
tasks.make_task_and_enter("proc: tags", State::Procedure);
|
||||
assert_eq!(tasks.get_own_events_history().count(), 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());
|
||||
assert_eq!(tasks.visible_tasks(),
|
||||
Vec::<&Task>::new());
|
||||
let sub_id = tasks.make_task("sub");
|
||||
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([sub_id]));
|
||||
assert_eq!(tasks.visible_tasks().iter().map(|t| t.event.id).collect_vec(),
|
||||
Vec::from([sub_id]));
|
||||
assert_eq!(tasks.len(), 3);
|
||||
let sub = tasks.get_by_id(&sub_id).unwrap();
|
||||
assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new());
|
||||
|
|
Loading…
Reference in New Issue