feat: greatly revamp filtering

This commit is contained in:
xeruf 2024-09-07 13:03:30 +03:00
parent ddc57dc36a
commit 132ea048a5
4 changed files with 329 additions and 224 deletions

70
Cargo.lock generated
View File

@ -143,9 +143,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.81" version = "0.1.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -166,9 +166,9 @@ dependencies = [
[[package]] [[package]]
name = "async-wsocket" name = "async-wsocket"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5725a0615e4eb98e82e9cb963529398114e3fccfbf0e8b9111d605e2ac443e46" checksum = "1eee6fcc818b89848df37050215603de0e2e072734e4730c03060feb2d0abebb"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"futures", "futures",
@ -372,9 +372,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.13" version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -956,9 +956,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -1188,7 +1188,7 @@ dependencies = [
[[package]] [[package]]
name = "mostr" name = "mostr"
version = "0.4.0" version = "0.5.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-english", "chrono-english",
@ -1257,9 +1257,9 @@ dependencies = [
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.34.0" version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5897e4142fcc33c4f1d58ad17f665e87dcba70de7e370c0bda1aa0fb73212c2a" checksum = "a1c3c32439eef3ea4d9079b2a8f557992d27259c26527e43d4228dd321e43a77"
dependencies = [ dependencies = [
"aes", "aes",
"base64 0.21.7", "base64 0.21.7",
@ -1301,9 +1301,9 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.34.0" version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6480cf60564957a2a64bd050d047ee0717e08dced7a389e22ef4e9fc104edd2" checksum = "d0e37c5ea991802a91728d4c09d5a7276938104ead8bf140a63a60acabc5c756"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@ -1386,9 +1386,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.3" version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -1543,9 +1543,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.3" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" checksum = "a2d2fb862b7ba45e615c1429def928f2e15f815bdf933b27a2d3824e224c1f46"
dependencies = [ dependencies = [
"bytes", "bytes",
"pin-project-lite", "pin-project-lite",
@ -1561,9 +1561,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.6" version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" checksum = "ea0a9b3a42929fad8a7c3de7f86ce0814cfa893328157672680e9fb1145549c5"
dependencies = [ dependencies = [
"bytes", "bytes",
"rand", "rand",
@ -1779,9 +1779,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
@ -1822,9 +1822,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.102.6" version = "0.102.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -1908,18 +1908,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.208" version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.208" version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1928,9 +1928,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.125" version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
@ -2033,9 +2033,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.75" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2101,9 +2101,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.39.3" version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -2445,9 +2445,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.26.3" version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]

View File

@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
readme = "README.md" readme = "README.md"
license = "GPL 3.0" license = "GPL 3.0"
authors = ["melonion"] authors = ["melonion"]
version = "0.4.0" version = "0.5.0"
edition = "2021" edition = "2021"
default-run = "mostr" default-run = "mostr"
@ -22,10 +22,10 @@ colored = "2.1"
parse_datetime = "0.5.0" parse_datetime = "0.5.0"
interim = { version = "0.1", features = ["chrono"] } interim = { version = "0.1", features = ["chrono"] }
nostr-sdk = "0.34" # { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = "0.34" # { git = "https://github.com/rust-nostr/nostr" }
tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.40", features = ["rt", "rt-multi-thread", "macros"] }
regex = "1.10.5" regex = "1.10.6"
rustyline = { git = "https://github.com/xeruf/rustyline", rev = "465b14d" } rustyline = { git = "https://github.com/xeruf/rustyline", rev = "465b14d" }
[dev-dependencies] [dev-dependencies]
chrono-english = "0.1" chrono-english = "0.1"
linefeed = "0.6" linefeed = "0.6"

View File

@ -14,13 +14,13 @@ use chrono::Local;
use colored::Colorize; use colored::Colorize;
use env_logger::{Builder, Target, WriteStyle}; use env_logger::{Builder, Target, WriteStyle};
use itertools::Itertools; 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::prelude::*;
use nostr_sdk::TagStandard::Hashtag; use nostr_sdk::TagStandard::Hashtag;
use regex::Regex; use regex::Regex;
use rustyline::config::Configurer; use rustyline::config::Configurer;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::time::error::Elapsed; use tokio::time::error::Elapsed;
@ -28,8 +28,8 @@ use tokio::time::timeout;
use xdg::BaseDirectories; use xdg::BaseDirectories;
use crate::helpers::*; use crate::helpers::*;
use crate::kinds::{BASIC_KINDS, PROP_KINDS, PROPERTY_COLUMNS, TRACKING_KIND}; use crate::kinds::{BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND};
use crate::task::{MARKER_DEPENDS, State}; use crate::task::{State, MARKER_DEPENDS};
use crate::tasks::{PropertyCollection, StateFilter, Tasks}; use crate::tasks::{PropertyCollection, StateFilter, Tasks};
mod helpers; mod helpers;
@ -450,51 +450,37 @@ async fn main() -> Result<()> {
} }
Some('@') => { Some('@') => {
match arg { let success = match arg {
None => { None => {
let today = Timestamp::now() - 80_000; let today = Timestamp::now() - 80_000;
info!("Filtering for tasks opened in the last 22 hours"); info!("Filtering for tasks from the last 22 hours");
tasks.set_filter( tasks.set_filter_from(today)
tasks.filtered_tasks(tasks.get_position_ref())
.filter(|t| t.last_state_update() > today)
.map(|t| t.event.id)
.collect()
);
} }
Some(arg) => { Some(arg) => {
if arg == "@" { if arg == "@" {
let key = keys.public_key();
info!("Filtering for own tasks"); info!("Filtering for own tasks");
tasks.set_filter( tasks.set_filter_author(keys.public_key())
tasks.filtered_tasks(tasks.get_position_ref())
.filter(|t| t.event.pubkey == key)
.map(|t| t.event.id)
.collect()
)
} else if let Ok(key) = PublicKey::from_str(arg) { } else if let Ok(key) = PublicKey::from_str(arg) {
let author = tasks.get_author(&key); let author = tasks.get_author(&key);
info!("Filtering for tasks by {author}"); info!("Filtering for tasks by {author}");
tasks.set_filter( tasks.set_filter_author(key)
tasks.filtered_tasks(tasks.get_position_ref())
.filter(|t| t.event.pubkey == key)
.map(|t| t.event.id)
.collect()
)
} else { } else {
parse_hour(arg, 1) parse_hour(arg, 1)
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) .or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
.map(|time| { .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(); let threshold = time.to_utc().timestamp();
tasks.set_filter( tasks.set_filter_from(
tasks.filtered_tasks(tasks.get_position_ref()) if let Some(t) = 0u64.checked_add_signed(threshold) {
.filter(|t| t.last_state_update().as_u64() as i64 > threshold) Timestamp::from(t)
.map(|t| t.event.id) } else { Timestamp::zero() })
.collect() })
); .unwrap_or(false)
});
} }
} }
};
if !success {
continue;
} }
} }
@ -503,8 +489,8 @@ async fn main() -> Result<()> {
None => match tasks.get_position_ref() { None => match tasks.get_position_ref() {
None => { None => {
info!("Filtering for bookmarked tasks"); info!("Filtering for bookmarked tasks");
tasks.set_filter_bookmarks() tasks.set_view_bookmarks();
}, }
Some(pos) => { Some(pos) => {
info!("Toggling bookmark"); info!("Toggling bookmark");
or_warn!(tasks.toggle_bookmark(*pos)); or_warn!(tasks.toggle_bookmark(*pos));
@ -636,7 +622,7 @@ async fn main() -> Result<()> {
} else { } else {
tasks.clear_filters(); tasks.clear_filters();
} }
} else if let Ok(depth) = slice.parse::<i8>() { } else if let Ok(depth) = slice.parse::<usize>() {
if pos != tasks.get_position_ref() { if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
} }
@ -667,19 +653,17 @@ async fn main() -> Result<()> {
transform = Box::new(|s| s.to_ascii_lowercase()); transform = Box::new(|s| s.to_ascii_lowercase());
} }
let filtered = tasks.filtered_tasks(pos) let filtered =
.filter(|t| { tasks.get_filtered(|t| {
transform(&t.event.content).contains(slice) || transform(&t.event.content).contains(slice) ||
t.tags.iter().flatten().any( 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(slice)))
}) });
.map(|t| t.event.id)
.collect_vec();
if filtered.len() == 1 { if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().next()); tasks.move_to(filtered.into_iter().next());
} else { } else {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
tasks.set_filter(filtered); tasks.set_view(filtered);
} }
} }
} }

View File

@ -7,16 +7,16 @@ use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use colored::Colorize; use colored::Colorize;
use itertools::Itertools; use itertools::{Either, Itertools};
use log::{debug, error, info, trace, warn}; 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::prelude::Marker;
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
use TagStandard::Hashtag; use TagStandard::Hashtag;
use crate::{EventSender, MostrMessage}; use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
use crate::helpers::{CHARACTER_THRESHOLD, format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty};
use crate::kinds::*; 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; const MAX_OFFSET: u64 = 9;
fn now() -> Timestamp { fn now() -> Timestamp {
@ -41,10 +41,10 @@ pub(crate) struct Tasks {
sorting: VecDeque<String>, sorting: VecDeque<String>,
/// A filtered view of the current tasks /// 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<EventId>, view: Vec<EventId>,
/// Zero: Only Active node depth: usize,
/// Positive: Go down the respective level
depth: i8,
/// Currently active tags /// Currently active tags
tags: BTreeSet<Tag>, tags: BTreeSet<Tag>,
@ -74,10 +74,7 @@ impl StateFilter {
fn matches(&self, task: &Task) -> bool { fn matches(&self, task: &Task) -> bool {
match self { match self {
StateFilter::Default => { StateFilter::Default => task.pure_state().is_open(),
let state = task.pure_state();
state.is_open() || (state == State::Done && task.parent_id().is_some())
}
StateFilter::All => true, StateFilter::All => true,
StateFilter::State(filter) => task.state().is_some_and(|t| t.matches_label(filter)), StateFilter::State(filter) => task.state().is_some_and(|t| t.matches_label(filter)),
} }
@ -97,7 +94,7 @@ impl Display for StateFilter {
f, f,
"{}", "{}",
match self { match self {
StateFilter::Default => "relevant tasks".to_string(), StateFilter::Default => "open tasks".to_string(),
StateFilter::All => "all tasks".to_string(), StateFilter::All => "all tasks".to_string(),
StateFilter::State(s) => format!("state {s}"), StateFilter::State(s) => format!("state {s}"),
} }
@ -176,11 +173,6 @@ impl Tasks {
|e| (e.created_at, referenced_event(e))) |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<Item=&str> { pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
self.tasks.values() self.tasks.values()
.filter(|t| t.pure_state() != State::Closed) .filter(|t| t.pure_state() != State::Closed)
@ -246,7 +238,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).get_all(); let children = ChildIterator::from(&self, &id).get_all();
for user in self.history.values() { for user in self.history.values() {
total += Durations::from(user.values(), &children).sum::<Duration>().as_secs(); total += Durations::from(user.values(), &children).sum::<Duration>().as_secs();
} }
@ -317,35 +309,35 @@ impl Tasks {
// Helpers // Helpers
fn resolve_tasks<'a>(&'a self, iter: impl Iterator<Item=&'a EventId>) -> impl Iterator<Item=&'a Task> { fn resolve_tasks<'a>(
self.resolve_tasks_rec(iter, self.depth) &'a self,
iter: impl Iterator<Item=&'a EventId>,
sparse: bool,
) -> Vec<&'a Task> {
self.resolve_tasks_rec(iter, sparse, self.depth)
} }
fn resolve_tasks_rec<'a>( fn resolve_tasks_rec<'a>(
&'a self, &'a self,
iter: impl Iterator<Item=&'a EventId>, iter: impl Iterator<Item=&'a EventId>,
depth: i8, sparse: bool,
) -> Box<impl Iterator<Item=&'a Task>> { depth: usize,
) -> Vec<&'a Task> {
iter.filter_map(|id| self.get_by_id(id)) iter.filter_map(|id| self.get_by_id(id))
.flat_map(move |task| { .flat_map(move |task| {
let new_depth = depth - 1; let new_depth = depth - 1;
if new_depth == 0 { if new_depth > 0 {
vec![task] let mut children = self.resolve_tasks_rec(task.children.iter(), sparse, new_depth);
} else { if !children.is_empty() {
let tasks_iter = self.resolve_tasks_rec(task.children.iter(), new_depth); if !sparse {
if new_depth < 0 { children.push(task);
let tasks: Vec<&Task> = tasks_iter.collect();
if tasks.is_empty() {
vec![task]
} else {
tasks
} }
} else { return children;
tasks_iter.chain(once(task)).collect()
} }
} }
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. /// Executes the given function with each task referenced by this event without marker.
@ -377,32 +369,33 @@ impl Tasks {
.map(|t| t.get_id()) .map(|t| t.get_id())
} }
pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a { fn filter(&self, task: &Task) -> bool {
let current: HashMap<&EventId, &Task> = self.resolve_tasks(self.children_of(position)).map(|t| (t.get_id(), t)).collect(); self.state.matches(task) &&
let bookmarks = task.tags.as_ref().map_or(true, |tags| {
if current.is_empty() { !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![] vec![]
} else { } else {
self.bookmarks.iter() 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_map(|id| self.get_by_id(id))
.filter(|t| self.filter(t))
.collect_vec() .collect_vec()
}; };
// TODO use ChildIterator current.append(&mut bookmarks);
current.into_values().chain( current
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))
}))
})
} }
pub(crate) fn visible_tasks(&self) -> Vec<&Task> { pub(crate) fn visible_tasks(&self) -> Vec<&Task> {
@ -410,9 +403,9 @@ impl Tasks {
return vec![]; return vec![];
} }
if !self.view.is_empty() { 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> { pub(crate) fn print_tasks(&self) -> Result<(), Error> {
@ -540,18 +533,50 @@ impl Tasks {
self.bookmarks.iter().map(|id| Tag::event(*id)))) self.bookmarks.iter().map(|id| Tag::event(*id))))
} }
pub(crate) fn set_filter_bookmarks(&mut self) { pub(crate) fn set_filter_author(&mut self, key: PublicKey) -> bool {
self.set_filter(self.bookmarks.clone()) self.set_filter(|t| t.event.pubkey == key)
} }
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) { 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<P>(&self, predicate: P) -> Vec<EventId>
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<P>(&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<EventId>) -> bool {
if view.is_empty() { if view.is_empty() {
warn!("No match for filter!") warn!("No match for filter!");
self.view = view;
return false;
} }
self.view = view; self.view = view;
true
} }
pub(crate) fn clear_filters(&mut self) { pub(crate) fn clear_filters(&mut self) {
self.state = StateFilter::Default;
self.view.clear(); self.view.clear();
self.tags.clear(); self.tags.clear();
self.tags_excluded.clear(); self.tags_excluded.clear();
@ -601,14 +626,14 @@ impl Tasks {
self.sender.flush(); self.sender.flush();
} }
/// Returns ids of tasks starting with the given string. /// Returns ids of tasks matching the given string.
/// ///
/// Tries, in order: /// Tries, in order:
/// - single case-insensitive exact name match in visible tasks /// - single case-insensitive exact name match in visible tasks
/// - single case-insensitive exact name match in all tasks /// - single case-insensitive exact name match in all tasks
/// - visible tasks starting with given arg case-sensitive /// - visible tasks starting with given arg case-sensitive
/// - visible tasks where any word starts with given arg case-insensitive /// - visible tasks where any word starts with given arg case-insensitive
pub(crate) fn get_filtered(&self, position: Option<&EventId>, arg: &str) -> Vec<EventId> { pub(crate) fn get_matching(&self, position: Option<&EventId>, arg: &str) -> Vec<EventId> {
if let Ok(id) = EventId::parse(arg) { if let Ok(id) = EventId::parse(arg) {
return vec![id]; return vec![id];
} }
@ -617,7 +642,7 @@ impl Tasks {
let mut filtered: Vec<EventId> = Vec::with_capacity(32); let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32); let mut filtered_fuzzy: Vec<EventId> = 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(); let lowercase = task.event.content.to_ascii_lowercase();
if lowercase == lowercase_arg { if lowercase == lowercase_arg {
return vec![task.event.id]; return vec![task.event.id];
@ -651,7 +676,7 @@ impl Tasks {
/// Finds out what to do with the given string. /// Finds out what to do with the given string.
/// Returns an EventId if a new Task was created. /// Returns an EventId if a new Task was created.
pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> { pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
let filtered = self.get_filtered(position, arg); let filtered = self.get_matching(position, arg);
match filtered.len() { match filtered.len() {
0 => { 0 => {
// No match, new task // No match, new task
@ -670,7 +695,7 @@ impl Tasks {
_ => { _ => {
// Multiple match, filter // Multiple match, filter
self.move_to(position.cloned()); self.move_to(position.cloned());
self.set_filter(filtered); self.set_view(filtered);
None None
} }
} }
@ -976,10 +1001,10 @@ impl Tasks {
// Properties // Properties
pub(crate) fn set_depth(&mut self, depth: i8) { pub(crate) fn set_depth(&mut self, depth: usize) {
if depth < self.depth && !self.view.is_empty() { if !self.view.is_empty() {
self.view.clear(); self.view.clear();
info!("Cleared search and reduced view depth to {depth}"); info!("Cleared search and changed view depth to {depth}");
} else { } else {
info!("Changed view depth to {depth}"); 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 /// Breadth-First Iterator over Tasks and recursive children
struct ChildIterator<'a> { struct ChildIterator<'a> {
tasks: &'a TaskMap, tasks: &'a TaskMap,
@ -1149,6 +1192,28 @@ struct ChildIterator<'a> {
next_depth_at: usize, next_depth_at: usize,
} }
impl<'a> ChildIterator<'a> { 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 { fn from(tasks: &'a Tasks, id: &'a EventId) -> Self {
let mut queue = Vec::with_capacity(30); let mut queue = Vec::with_capacity(30);
queue.push(id); queue.push(id);
@ -1166,52 +1231,110 @@ impl<'a> ChildIterator<'a> {
fn process_depth(&mut self, depth: usize) -> bool { fn process_depth(&mut self, depth: usize) -> bool {
while self.depth < depth { while self.depth < depth {
if self.next().is_none() { if self.next().is_none() {
return false return false;
} }
} }
true 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 /// Get all tasks until the specified depth
fn get_depth(mut self, depth: usize) -> Vec<&'a EventId> { fn get_depth(mut self, depth: usize) -> Vec<&'a EventId> {
self.process_depth(depth); self.process_depth(depth);
self.queue self.queue
} }
/// Get all children /// Get all tasks until the specified depth matching the filter
fn get_all(mut self) -> Vec<&'a EventId> { fn get_depth_filtered<F>(mut self, depth: usize, filter: F) -> Vec<&'a EventId>
while self.next().is_some() {} where
self.queue F: Fn(&Task) -> ChildIteratorFilter,
} {
} while self.depth < depth {
impl<'a> Iterator for ChildIterator<'a> { if self.next_filtered(&filter).is_none() {
type Item = &'a EventId; // TODO this can easily recurse beyond the intended depth
break;
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, 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());
}
} }
} }
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 { if self.next_depth_at == self.index {
self.depth += 1; self.depth += 1;
self.next_depth_at = self.queue.len(); 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) Some(id)
} }
/// Get the next known task and run it through the filter
fn next_filtered<F>(&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 FusedIterator for ChildIterator<'_> {}
impl<'a> Iterator for ChildIterator<'a> {
type Item = &'a EventId;
fn next(&mut self) -> Option<Self::Item> {
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> { 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<EventId>>(),
HashSet::from($right))
};
}
#[test] #[test]
fn test_bookmarks() { fn test_bookmarks() {
let mut tasks = stub_tasks(); let mut tasks = stub_tasks();
@ -1275,29 +1405,33 @@ mod tasks_test {
tasks.move_to(Some(parent)); tasks.move_to(Some(parent));
let pin = tasks.make_task("pin"); let pin = tasks.make_task("pin");
assert_eq!(tasks.filtered_tasks(None).count(), 2); assert_eq!(tasks.filtered_tasks(None, true).len(), 2);
assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0); 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.visible_tasks().len(), 1);
assert_eq!(tasks.filtered_tasks(Some(&pin)).count(), 0); assert_eq!(tasks.filtered_tasks(Some(&pin), false).len(), 0);
assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0); assert_eq!(tasks.filtered_tasks(Some(&zero), false).len(), 0);
tasks.submit(EventBuilder::new(Kind::Bookmarks, "", [Tag::event(pin), Tag::event(zero)])); tasks.submit(EventBuilder::new(Kind::Bookmarks, "", [Tag::event(pin), Tag::event(zero)]));
assert_eq!(tasks.visible_tasks().len(), 1); assert_eq!(tasks.visible_tasks().len(), 1);
assert_eq!(tasks.filtered_tasks(Some(&pin)).count(), 0); assert_eq!(tasks.filtered_tasks(Some(&pin), true).len(), 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), true).len(), 0);
assert_eq!(tasks.filtered_tasks(Some(&zero), false), vec![tasks.get_by_id(&pin).unwrap()]);
tasks.move_to(None); 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); tasks.set_depth(2);
assert_eq!(tasks.visible_tasks().len(), 3); assert_tasks!(tasks, [pin, test]);
tasks.add_tag("tag".to_string()); tasks.add_tag("tag".to_string());
assert_eq!(tasks.visible_tasks().len(), 1); assert_tasks!(tasks, [test]);
assert_eq!(tasks.filtered_tasks(None).collect_vec(), vec![tasks.get_by_id(&test).unwrap()]); assert_eq!(tasks.filtered_tasks(None, true), vec![tasks.get_by_id(&test).unwrap()]);
tasks.submit(EventBuilder::new(Kind::Bookmarks, "", [])); tasks.submit(EventBuilder::new(Kind::Bookmarks, "", []));
tasks.clear_filters(); tasks.clear_filters();
assert_eq!(tasks.visible_tasks().len(), 3); assert_tasks!(tasks, [pin, test]);
tasks.set_depth(1); tasks.set_depth(1);
assert_eq!(tasks.visible_tasks().len(), 2); assert_tasks!(tasks, [test, parent]);
} }
#[test] #[test]
@ -1406,24 +1540,22 @@ mod tasks_test {
assert_position!(tasks, t1); assert_position!(tasks, t1);
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.visible_tasks().len(), 0); 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.visible_tasks().len(), 1);
assert_eq!(tasks.get_task_path(Some(t2)), "t1>t2"); assert_eq!(tasks.get_task_path(Some(t11)), "t1>t11");
assert_eq!(tasks.relative_path(t2), "t2"); assert_eq!(tasks.relative_path(t11), "t11");
let t3 = tasks.make_task("t3"); let t12 = tasks.make_task("t12");
assert_eq!(tasks.visible_tasks().len(), 2); assert_eq!(tasks.visible_tasks().len(), 2);
tasks.move_to(Some(t2)); tasks.move_to(Some(t11));
assert_position!(tasks, t2); assert_position!(tasks, t11);
assert_eq!(tasks.visible_tasks().len(), 0); assert_eq!(tasks.visible_tasks().len(), 0);
let t4 = tasks.make_task("t4"); let t111 = tasks.make_task("t111");
assert_eq!(tasks.visible_tasks().len(), 1); assert_tasks!(tasks, [t111]);
assert_eq!(tasks.get_task_path(Some(t4)), "t1>t2>t4"); assert_eq!(tasks.get_task_path(Some(t111)), "t1>t11>t111");
assert_eq!(tasks.relative_path(t4), "t4"); assert_eq!(tasks.relative_path(t111), "t111");
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.visible_tasks().len(), 1); assert_tasks!(tasks, [t111]);
tasks.depth = -1;
assert_eq!(tasks.visible_tasks().len(), 1);
assert_eq!(ChildIterator::from(&tasks, &EventId::all_zeros()).get_all().len(), 1); 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); 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)); tasks.move_to(Some(t1));
assert_position!(tasks, t1); assert_position!(tasks, t1);
assert_eq!(tasks.get_own_events_history().count(), 3); assert_eq!(tasks.get_own_events_history().count(), 3);
assert_eq!(tasks.relative_path(t4), "t2>t4"); assert_eq!(tasks.relative_path(t111), "t11>t111");
assert_eq!(tasks.visible_tasks().len(), 2); assert_eq!(tasks.depth, 2);
tasks.depth = 2; assert_tasks!(tasks, [t111, t12]);
assert_eq!(tasks.visible_tasks().len(), 3); tasks.set_view(vec![t11]);
tasks.set_filter(vec![t2]); assert_tasks!(tasks, [t11]); // No more depth applied to view
assert_eq!(tasks.visible_tasks().len(), 2); tasks.set_depth(1);
tasks.depth = 1; assert_tasks!(tasks, [t11, t12]);
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);
tasks.move_to(None); tasks.move_to(None);
assert_eq!(tasks.visible_tasks().len(), 1); assert_tasks!(tasks, [t1]);
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.visible_tasks().len(), 3); assert_tasks!(tasks, [t11, t12]);
tasks.depth = 3; tasks.depth = 3;
assert_eq!(tasks.visible_tasks().len(), 4); assert_tasks!(tasks, [t111, t12]);
tasks.depth = 9; tasks.depth = 9;
assert_eq!(tasks.visible_tasks().len(), 4); assert_tasks!(tasks, [t111, t12]);
tasks.depth = -1;
assert_eq!(tasks.visible_tasks().len(), 2);
} }
#[test] #[test]