diff --git a/Cargo.lock b/Cargo.lock index fe938f5..8dc24a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,9 +143,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", @@ -166,9 +166,9 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5725a0615e4eb98e82e9cb963529398114e3fccfbf0e8b9111d605e2ac443e46" +checksum = "1eee6fcc818b89848df37050215603de0e2e072734e4730c03060feb2d0abebb" dependencies = [ "async-utility", "futures", @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.13" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "shlex", ] @@ -956,9 +956,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -1188,7 +1188,7 @@ dependencies = [ [[package]] name = "mostr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "chrono-english", @@ -1257,9 +1257,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5897e4142fcc33c4f1d58ad17f665e87dcba70de7e370c0bda1aa0fb73212c2a" +checksum = "a1c3c32439eef3ea4d9079b2a8f557992d27259c26527e43d4228dd321e43a77" dependencies = [ "aes", "base64 0.21.7", @@ -1301,9 +1301,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.34.0" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6480cf60564957a2a64bd050d047ee0717e08dced7a389e22ef4e9fc104edd2" +checksum = "d0e37c5ea991802a91728d4c09d5a7276938104ead8bf140a63a60acabc5c756" dependencies = [ "async-utility", "async-wsocket", @@ -1386,9 +1386,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -1543,9 +1543,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +checksum = "a2d2fb862b7ba45e615c1429def928f2e15f815bdf933b27a2d3824e224c1f46" dependencies = [ "bytes", "pin-project-lite", @@ -1561,9 +1561,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +checksum = "ea0a9b3a42929fad8a7c3de7f86ce0814cfa893328157672680e9fb1145549c5" dependencies = [ "bytes", "rand", @@ -1779,9 +1779,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", @@ -1822,9 +1822,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" dependencies = [ "ring", "rustls-pki-types", @@ -1908,18 +1908,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1928,9 +1928,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "indexmap", "itoa", @@ -2033,9 +2033,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.75" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2101,9 +2101,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -2445,9 +2445,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index b109220..dbea489 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr" readme = "README.md" license = "GPL 3.0" authors = ["melonion"] -version = "0.4.0" +version = "0.5.0" edition = "2021" default-run = "mostr" @@ -22,10 +22,10 @@ colored = "2.1" parse_datetime = "0.5.0" interim = { version = "0.1", features = ["chrono"] } nostr-sdk = "0.34" # { git = "https://github.com/rust-nostr/nostr" } -tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "macros"] } -regex = "1.10.5" +tokio = { version = "1.40", features = ["rt", "rt-multi-thread", "macros"] } +regex = "1.10.6" rustyline = { git = "https://github.com/xeruf/rustyline", rev = "465b14d" } [dev-dependencies] chrono-english = "0.1" -linefeed = "0.6" \ No newline at end of file +linefeed = "0.6" diff --git a/src/main.rs b/src/main.rs index 55e3959..aeda770 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,13 +14,13 @@ use chrono::Local; use colored::Colorize; use env_logger::{Builder, Target, WriteStyle}; use itertools::Itertools; -use log::{debug, error, info, LevelFilter, trace, warn}; +use log::{debug, error, info, trace, warn, LevelFilter}; use nostr_sdk::prelude::*; use nostr_sdk::TagStandard::Hashtag; use regex::Regex; use rustyline::config::Configurer; -use rustyline::DefaultEditor; use rustyline::error::ReadlineError; +use rustyline::DefaultEditor; use tokio::sync::mpsc; use tokio::sync::mpsc::Sender; use tokio::time::error::Elapsed; @@ -28,8 +28,8 @@ use tokio::time::timeout; use xdg::BaseDirectories; use crate::helpers::*; -use crate::kinds::{BASIC_KINDS, PROP_KINDS, PROPERTY_COLUMNS, TRACKING_KIND}; -use crate::task::{MARKER_DEPENDS, State}; +use crate::kinds::{BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND}; +use crate::task::{State, MARKER_DEPENDS}; use crate::tasks::{PropertyCollection, StateFilter, Tasks}; mod helpers; @@ -450,51 +450,37 @@ async fn main() -> Result<()> { } Some('@') => { - match arg { + let success = match arg { None => { let today = Timestamp::now() - 80_000; - info!("Filtering for tasks opened in the last 22 hours"); - tasks.set_filter( - tasks.filtered_tasks(tasks.get_position_ref()) - .filter(|t| t.last_state_update() > today) - .map(|t| t.event.id) - .collect() - ); + info!("Filtering for tasks from the last 22 hours"); + tasks.set_filter_from(today) } Some(arg) => { if arg == "@" { - let key = keys.public_key(); info!("Filtering for own tasks"); - tasks.set_filter( - tasks.filtered_tasks(tasks.get_position_ref()) - .filter(|t| t.event.pubkey == key) - .map(|t| t.event.id) - .collect() - ) + tasks.set_filter_author(keys.public_key()) } else if let Ok(key) = PublicKey::from_str(arg) { let author = tasks.get_author(&key); info!("Filtering for tasks by {author}"); - tasks.set_filter( - tasks.filtered_tasks(tasks.get_position_ref()) - .filter(|t| t.event.pubkey == key) - .map(|t| t.event.id) - .collect() - ) + tasks.set_filter_author(key) } else { parse_hour(arg, 1) .or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) .map(|time| { - info!("Filtering for tasks opened after {}", format_datetime_relative(time)); + info!("Filtering for tasks from {}", format_datetime_relative(time)); let threshold = time.to_utc().timestamp(); - tasks.set_filter( - tasks.filtered_tasks(tasks.get_position_ref()) - .filter(|t| t.last_state_update().as_u64() as i64 > threshold) - .map(|t| t.event.id) - .collect() - ); - }); + tasks.set_filter_from( + if let Some(t) = 0u64.checked_add_signed(threshold) { + Timestamp::from(t) + } else { Timestamp::zero() }) + }) + .unwrap_or(false) } } + }; + if !success { + continue; } } @@ -503,8 +489,8 @@ async fn main() -> Result<()> { None => match tasks.get_position_ref() { None => { info!("Filtering for bookmarked tasks"); - tasks.set_filter_bookmarks() - }, + tasks.set_view_bookmarks(); + } Some(pos) => { info!("Toggling bookmark"); or_warn!(tasks.toggle_bookmark(*pos)); @@ -636,7 +622,7 @@ async fn main() -> Result<()> { } else { tasks.clear_filters(); } - } else if let Ok(depth) = slice.parse::() { + } else if let Ok(depth) = slice.parse::() { if pos != tasks.get_position_ref() { tasks.move_to(pos.cloned()); } @@ -667,19 +653,17 @@ async fn main() -> Result<()> { transform = Box::new(|s| s.to_ascii_lowercase()); } - let filtered = tasks.filtered_tasks(pos) - .filter(|t| { + let filtered = + tasks.get_filtered(|t| { transform(&t.event.content).contains(slice) || t.tags.iter().flatten().any( |tag| tag.content().is_some_and(|s| transform(s).contains(slice))) - }) - .map(|t| t.event.id) - .collect_vec(); + }); if filtered.len() == 1 { tasks.move_to(filtered.into_iter().next()); } else { tasks.move_to(pos.cloned()); - tasks.set_filter(filtered); + tasks.set_view(filtered); } } } diff --git a/src/tasks.rs b/src/tasks.rs index fc1ea89..fa8519f 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -7,16 +7,16 @@ use std::str::FromStr; use std::time::Duration; use colored::Colorize; -use itertools::Itertools; +use itertools::{Either, Itertools}; use log::{debug, error, info, trace, warn}; -use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url}; use nostr_sdk::prelude::Marker; +use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url}; use TagStandard::Hashtag; -use crate::{EventSender, MostrMessage}; -use crate::helpers::{CHARACTER_THRESHOLD, format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty}; +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::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState}; +use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT}; +use crate::{EventSender, MostrMessage}; const MAX_OFFSET: u64 = 9; fn now() -> Timestamp { @@ -41,10 +41,10 @@ pub(crate) struct Tasks { sorting: VecDeque, /// A filtered view of the current tasks + /// Would like this to be Task references but that doesn't work + /// unless I start meddling with Rc everywhere. view: Vec, - /// Zero: Only Active node - /// Positive: Go down the respective level - depth: i8, + depth: usize, /// Currently active tags tags: BTreeSet, @@ -74,10 +74,7 @@ impl StateFilter { fn matches(&self, task: &Task) -> bool { match self { - StateFilter::Default => { - let state = task.pure_state(); - state.is_open() || (state == State::Done && task.parent_id().is_some()) - } + StateFilter::Default => task.pure_state().is_open(), StateFilter::All => true, StateFilter::State(filter) => task.state().is_some_and(|t| t.matches_label(filter)), } @@ -97,7 +94,7 @@ impl Display for StateFilter { f, "{}", match self { - StateFilter::Default => "relevant tasks".to_string(), + StateFilter::Default => "open tasks".to_string(), StateFilter::All => "all tasks".to_string(), StateFilter::State(s) => format!("state {s}"), } @@ -176,11 +173,6 @@ impl Tasks { |e| (e.created_at, referenced_event(e))) } - /// Ids of all subtasks recursively found for id, including itself - fn get_task_tree<'a>(&'a self, id: &'a EventId) -> ChildIterator { - ChildIterator::from(self, id) - } - pub(crate) fn all_hashtags(&self) -> impl Iterator { self.tasks.values() .filter(|t| t.pure_state() != State::Closed) @@ -246,7 +238,7 @@ impl Tasks { fn total_time_tracked(&self, id: EventId) -> u64 { let mut total = 0; - let children = self.get_task_tree(&id).get_all(); + let children = ChildIterator::from(&self, &id).get_all(); for user in self.history.values() { total += Durations::from(user.values(), &children).sum::().as_secs(); } @@ -317,35 +309,35 @@ impl Tasks { // Helpers - fn resolve_tasks<'a>(&'a self, iter: impl Iterator) -> impl Iterator { - self.resolve_tasks_rec(iter, self.depth) + fn resolve_tasks<'a>( + &'a self, + iter: impl Iterator, + sparse: bool, + ) -> Vec<&'a Task> { + self.resolve_tasks_rec(iter, sparse, self.depth) } fn resolve_tasks_rec<'a>( &'a self, iter: impl Iterator, - depth: i8, - ) -> Box> { + sparse: bool, + depth: usize, + ) -> Vec<&'a Task> { iter.filter_map(|id| self.get_by_id(id)) .flat_map(move |task| { let new_depth = depth - 1; - if new_depth == 0 { - vec![task] - } else { - let tasks_iter = self.resolve_tasks_rec(task.children.iter(), new_depth); - if new_depth < 0 { - let tasks: Vec<&Task> = tasks_iter.collect(); - if tasks.is_empty() { - vec![task] - } else { - tasks + if new_depth > 0 { + let mut children = self.resolve_tasks_rec(task.children.iter(), sparse, new_depth); + if !children.is_empty() { + if !sparse { + children.push(task); } - } else { - tasks_iter.chain(once(task)).collect() + return children; } } + return if self.filter(task) { vec![task] } else { vec![] }; }) - .into() + .collect_vec() } /// Executes the given function with each task referenced by this event without marker. @@ -377,32 +369,33 @@ impl Tasks { .map(|t| t.get_id()) } - pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>) -> impl Iterator + 'a { - let current: HashMap<&EventId, &Task> = self.resolve_tasks(self.children_of(position)).map(|t| (t.get_id(), t)).collect(); - let bookmarks = - if current.is_empty() { + fn filter(&self, task: &Task) -> bool { + self.state.matches(task) && + task.tags.as_ref().map_or(true, |tags| { + !tags.iter().any(|tag| self.tags_excluded.contains(tag)) + }) && + (self.tags.is_empty() || + task.tags.as_ref().map_or(false, |tags| { + let mut iter = tags.iter(); + self.tags.iter().all(|tag| iter.any(|t| t == tag)) + })) + } + + 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 ids = current.iter().map(|t| t.get_id()).collect_vec(); + let mut bookmarks = + if sparse && current.is_empty() { vec![] } else { self.bookmarks.iter() - .filter(|id| !position.is_some_and(|p| &p == id) && !current.contains_key(id)) + .filter(|id| !position.is_some_and(|p| &p == id) && !ids.contains(id)) .filter_map(|id| self.get_by_id(id)) + .filter(|t| self.filter(t)) .collect_vec() }; - // TODO use ChildIterator - current.into_values().chain( - bookmarks - ).filter(move |t| { - // TODO apply filters in transit - self.state.matches(t) && - t.tags.as_ref().map_or(true, |tags| { - !tags.iter().any(|tag| self.tags_excluded.contains(tag)) - }) && - (self.tags.is_empty() || - t.tags.as_ref().map_or(false, |tags| { - let mut iter = tags.iter(); - self.tags.iter().all(|tag| iter.any(|t| t == tag)) - })) - }) + current.append(&mut bookmarks); + current } pub(crate) fn visible_tasks(&self) -> Vec<&Task> { @@ -410,9 +403,9 @@ impl Tasks { return vec![]; } if !self.view.is_empty() { - return self.resolve_tasks(self.view.iter()).collect(); + return self.view.iter().flat_map(|id| self.get_by_id(id)).collect(); } - self.filtered_tasks(self.get_position_ref()).collect() + self.filtered_tasks(self.get_position_ref(), true) } pub(crate) fn print_tasks(&self) -> Result<(), Error> { @@ -540,18 +533,50 @@ impl Tasks { self.bookmarks.iter().map(|id| Tag::event(*id)))) } - pub(crate) fn set_filter_bookmarks(&mut self) { - self.set_filter(self.bookmarks.clone()) + pub(crate) fn set_filter_author(&mut self, key: PublicKey) -> bool { + self.set_filter(|t| t.event.pubkey == key) } - pub(crate) fn set_filter(&mut self, view: Vec) { + pub(crate) fn set_filter_from(&mut self, time: Timestamp) -> bool { + self.set_filter(|t| t.last_state_update() > time) + } + + pub(crate) fn get_filtered

(&self, predicate: P) -> Vec + where + P: Fn(&&Task) -> bool, + { + self.filtered_tasks(self.get_position_ref(), false) + .into_iter() + .filter(predicate) + .map(|t| t.event.id) + .collect() + } + + pub(crate) fn set_filter

(&mut self, predicate: P) -> bool + where + P: Fn(&&Task) -> bool, + { + self.set_view(self.get_filtered(predicate)) + } + + pub(crate) fn set_view_bookmarks(&mut self) -> bool { + self.set_view(self.bookmarks.clone()) + } + + /// Set currently visible tasks. + /// Returns whether there are any. + pub(crate) fn set_view(&mut self, view: Vec) -> bool { if view.is_empty() { - warn!("No match for filter!") + warn!("No match for filter!"); + self.view = view; + return false; } self.view = view; + true } pub(crate) fn clear_filters(&mut self) { + self.state = StateFilter::Default; self.view.clear(); self.tags.clear(); self.tags_excluded.clear(); @@ -601,14 +626,14 @@ impl Tasks { self.sender.flush(); } - /// Returns ids of tasks starting with the given string. + /// Returns ids of tasks matching 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 { + pub(crate) fn get_matching(&self, position: Option<&EventId>, arg: &str) -> Vec { if let Ok(id) = EventId::parse(arg) { return vec![id]; } @@ -617,7 +642,7 @@ impl Tasks { let mut filtered: Vec = Vec::with_capacity(32); let mut filtered_fuzzy: Vec = Vec::with_capacity(32); - for task in self.filtered_tasks(position) { + for task in self.filtered_tasks(position, false) { let lowercase = task.event.content.to_ascii_lowercase(); if lowercase == lowercase_arg { return vec![task.event.id]; @@ -651,7 +676,7 @@ impl Tasks { /// Finds out what to do with the given string. /// Returns an EventId if a new Task was created. pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option { - let filtered = self.get_filtered(position, arg); + let filtered = self.get_matching(position, arg); match filtered.len() { 0 => { // No match, new task @@ -670,7 +695,7 @@ impl Tasks { _ => { // Multiple match, filter self.move_to(position.cloned()); - self.set_filter(filtered); + self.set_view(filtered); None } } @@ -976,10 +1001,10 @@ impl Tasks { // Properties - pub(crate) fn set_depth(&mut self, depth: i8) { - if depth < self.depth && !self.view.is_empty() { + pub(crate) fn set_depth(&mut self, depth: usize) { + if !self.view.is_empty() { self.view.clear(); - info!("Cleared search and reduced view depth to {depth}"); + info!("Cleared search and changed view depth to {depth}"); } else { info!("Changed view depth to {depth}"); } @@ -1136,6 +1161,24 @@ impl Iterator for Durations<'_> { } } +#[derive(Clone, Debug, PartialEq)] +enum ChildIteratorFilter { + Reject = 0b00, + TakeSelf = 0b01, + TakeChildren = 0b10, + Take = 0b11, +} +impl ChildIteratorFilter { + fn takes_children(&self) -> bool { + self == &ChildIteratorFilter::Take || + self == &ChildIteratorFilter::TakeChildren + } + fn takes_self(&self) -> bool { + self == &ChildIteratorFilter::Take || + self == &ChildIteratorFilter::TakeSelf + } +} + /// Breadth-First Iterator over Tasks and recursive children struct ChildIterator<'a> { tasks: &'a TaskMap, @@ -1149,6 +1192,28 @@ struct ChildIterator<'a> { next_depth_at: usize, } impl<'a> ChildIterator<'a> { + fn rooted(tasks: &'a TaskMap, id: Option<&EventId>) -> Self { + let mut queue = Vec::with_capacity(tasks.len()); + queue.append( + &mut tasks + .values() + .filter(move |t| t.parent_id() == id) + .map(|t| t.get_id()) + .collect_vec() + ); + Self::with_queue(tasks, queue) + } + + fn with_queue(tasks: &'a TaskMap, queue: Vec<&'a EventId>) -> Self { + ChildIterator { + tasks: &tasks, + next_depth_at: queue.len(), + index: 0, + depth: 1, + queue, + } + } + fn from(tasks: &'a Tasks, id: &'a EventId) -> Self { let mut queue = Vec::with_capacity(30); queue.push(id); @@ -1166,52 +1231,110 @@ impl<'a> ChildIterator<'a> { fn process_depth(&mut self, depth: usize) -> bool { while self.depth < depth { if self.next().is_none() { - return false + return false; } } true } + /// Get all children + fn get_all(mut self) -> Vec<&'a EventId> { + while self.next().is_some() {} + self.queue + } + /// Get all tasks until the specified depth fn get_depth(mut self, depth: usize) -> Vec<&'a EventId> { self.process_depth(depth); self.queue } - /// Get all children - 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 { - 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, might still find children, just slower - for task in self.tasks.values() { - if task.parent_id().is_some_and(|i| i == id) { - self.queue.push(task.get_id()); - } + /// Get all tasks until the specified depth matching the filter + fn get_depth_filtered(mut self, depth: usize, filter: F) -> Vec<&'a EventId> + where + F: Fn(&Task) -> ChildIteratorFilter, + { + while self.depth < depth { + if self.next_filtered(&filter).is_none() { + // TODO this can easily recurse beyond the intended depth + break; } } - self.index += 1; + while self.index < self.queue.len() { + if let Some(task) = self.tasks.get(self.queue[self.index]) { + if !filter(task).takes_self() { + self.queue.remove(self.index); + continue; + } + } + self.index += 1; + } + self.queue + } + + fn check_depth(&mut self) { if self.next_depth_at == self.index { self.depth += 1; self.next_depth_at = self.queue.len(); } + } + + /// Get next id and advance, without adding children + fn next_task(&mut self) -> Option<&'a EventId> { + if self.index >= self.queue.len() { + return None; + } + let id = self.queue[self.index]; + self.index += 1; Some(id) } + + /// Get the next known task and run it through the filter + fn next_filtered(&mut self, filter: &F) -> Option<&'a Task> + where + F: Fn(&Task) -> ChildIteratorFilter, + { + self.next_task().and_then(|id| { + 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()); + } + if take.takes_self() { + self.check_depth(); + return Some(task); + } + } + self.check_depth(); + self.next_filtered(filter) + }) + } } impl FusedIterator for ChildIterator<'_> {} +impl<'a> Iterator for ChildIterator<'a> { + type Item = &'a EventId; + + fn next(&mut self) -> Option { + self.next_task().inspect(|id| { + match self.tasks.get(id) { + None => { + // Unknown task, might still find children, just slower + for task in self.tasks.values() { + if task.parent_id().is_some_and(|i| i == *id) { + self.queue.push(task.get_id()); + } + } + } + Some(task) => { + self.queue.reserve(task.children.len()); + self.queue.extend(task.children.iter()); + } + } + self.check_depth(); + }) + } +} struct ParentIterator<'a> { @@ -1265,6 +1388,13 @@ mod tasks_test { }; } + macro_rules! assert_tasks { + ($left:expr, $right:expr $(,)?) => { + assert_eq!($left.visible_tasks().iter().map(|t| t.event.id).collect::>(), + HashSet::from($right)) + }; + } + #[test] fn test_bookmarks() { let mut tasks = stub_tasks(); @@ -1275,29 +1405,33 @@ mod tasks_test { tasks.move_to(Some(parent)); let pin = tasks.make_task("pin"); - assert_eq!(tasks.filtered_tasks(None).count(), 2); - assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0); + assert_eq!(tasks.filtered_tasks(None, true).len(), 2); + assert_eq!(tasks.filtered_tasks(None, false).len(), 2); + assert_eq!(tasks.filtered_tasks(Some(&zero), false).len(), 0); assert_eq!(tasks.visible_tasks().len(), 1); - assert_eq!(tasks.filtered_tasks(Some(&pin)).count(), 0); - assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0); - + assert_eq!(tasks.filtered_tasks(Some(&pin), false).len(), 0); + assert_eq!(tasks.filtered_tasks(Some(&zero), false).len(), 0); + tasks.submit(EventBuilder::new(Kind::Bookmarks, "", [Tag::event(pin), Tag::event(zero)])); assert_eq!(tasks.visible_tasks().len(), 1); - assert_eq!(tasks.filtered_tasks(Some(&pin)).count(), 0); - assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0); + assert_eq!(tasks.filtered_tasks(Some(&pin), true).len(), 0); + assert_eq!(tasks.filtered_tasks(Some(&pin), false).len(), 0); + assert_eq!(tasks.filtered_tasks(Some(&zero), true).len(), 0); + assert_eq!(tasks.filtered_tasks(Some(&zero), false), vec![tasks.get_by_id(&pin).unwrap()]); tasks.move_to(None); - assert_eq!(tasks.visible_tasks().len(), 3); + assert_eq!(tasks.depth, 1); + assert_tasks!(tasks, [pin, test, parent]); tasks.set_depth(2); - assert_eq!(tasks.visible_tasks().len(), 3); + assert_tasks!(tasks, [pin, test]); tasks.add_tag("tag".to_string()); - assert_eq!(tasks.visible_tasks().len(), 1); - assert_eq!(tasks.filtered_tasks(None).collect_vec(), vec![tasks.get_by_id(&test).unwrap()]); + assert_tasks!(tasks, [test]); + assert_eq!(tasks.filtered_tasks(None, true), vec![tasks.get_by_id(&test).unwrap()]); tasks.submit(EventBuilder::new(Kind::Bookmarks, "", [])); tasks.clear_filters(); - assert_eq!(tasks.visible_tasks().len(), 3); + assert_tasks!(tasks, [pin, test]); tasks.set_depth(1); - assert_eq!(tasks.visible_tasks().len(), 2); + assert_tasks!(tasks, [test, parent]); } #[test] @@ -1406,24 +1540,22 @@ mod tasks_test { assert_position!(tasks, t1); tasks.depth = 2; assert_eq!(tasks.visible_tasks().len(), 0); - let t2 = tasks.make_task("t2"); + let t11 = tasks.make_task("t11: tag"); assert_eq!(tasks.visible_tasks().len(), 1); - assert_eq!(tasks.get_task_path(Some(t2)), "t1>t2"); - assert_eq!(tasks.relative_path(t2), "t2"); - let t3 = tasks.make_task("t3"); + assert_eq!(tasks.get_task_path(Some(t11)), "t1>t11"); + assert_eq!(tasks.relative_path(t11), "t11"); + let t12 = tasks.make_task("t12"); assert_eq!(tasks.visible_tasks().len(), 2); - tasks.move_to(Some(t2)); - assert_position!(tasks, t2); + tasks.move_to(Some(t11)); + assert_position!(tasks, t11); assert_eq!(tasks.visible_tasks().len(), 0); - let t4 = tasks.make_task("t4"); - assert_eq!(tasks.visible_tasks().len(), 1); - assert_eq!(tasks.get_task_path(Some(t4)), "t1>t2>t4"); - assert_eq!(tasks.relative_path(t4), "t4"); + let t111 = tasks.make_task("t111"); + assert_tasks!(tasks, [t111]); + assert_eq!(tasks.get_task_path(Some(t111)), "t1>t11>t111"); + assert_eq!(tasks.relative_path(t111), "t111"); tasks.depth = 2; - assert_eq!(tasks.visible_tasks().len(), 1); - tasks.depth = -1; - assert_eq!(tasks.visible_tasks().len(), 1); + assert_tasks!(tasks, [t111]); assert_eq!(ChildIterator::from(&tasks, &EventId::all_zeros()).get_all().len(), 1); assert_eq!(ChildIterator::from(&tasks, &EventId::all_zeros()).get_depth(0).len(), 1); @@ -1436,33 +1568,22 @@ mod tasks_test { tasks.move_to(Some(t1)); assert_position!(tasks, t1); assert_eq!(tasks.get_own_events_history().count(), 3); - assert_eq!(tasks.relative_path(t4), "t2>t4"); - assert_eq!(tasks.visible_tasks().len(), 2); - tasks.depth = 2; - assert_eq!(tasks.visible_tasks().len(), 3); - tasks.set_filter(vec![t2]); - assert_eq!(tasks.visible_tasks().len(), 2); - tasks.depth = 1; - assert_eq!(tasks.visible_tasks().len(), 1); - tasks.depth = -1; - assert_eq!(tasks.visible_tasks().len(), 1); - tasks.set_filter(vec![t2, t3]); - assert_eq!(tasks.visible_tasks().len(), 2); - tasks.depth = 2; - assert_eq!(tasks.visible_tasks().len(), 3); - tasks.depth = 1; - assert_eq!(tasks.visible_tasks().len(), 2); + assert_eq!(tasks.relative_path(t111), "t11>t111"); + assert_eq!(tasks.depth, 2); + assert_tasks!(tasks, [t111, t12]); + tasks.set_view(vec![t11]); + assert_tasks!(tasks, [t11]); // No more depth applied to view + tasks.set_depth(1); + assert_tasks!(tasks, [t11, t12]); tasks.move_to(None); - assert_eq!(tasks.visible_tasks().len(), 1); + assert_tasks!(tasks, [t1]); tasks.depth = 2; - assert_eq!(tasks.visible_tasks().len(), 3); + assert_tasks!(tasks, [t11, t12]); tasks.depth = 3; - assert_eq!(tasks.visible_tasks().len(), 4); + assert_tasks!(tasks, [t111, t12]); tasks.depth = 9; - assert_eq!(tasks.visible_tasks().len(), 4); - tasks.depth = -1; - assert_eq!(tasks.visible_tasks().len(), 2); + assert_tasks!(tasks, [t111, t12]); } #[test]