feat: greatly revamp filtering
This commit is contained in:
parent
ddc57dc36a
commit
132ea048a5
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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,8 +22,8 @@ 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]
|
||||
|
|
68
src/main.rs
68
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::<i8>() {
|
||||
} else if let Ok(depth) = slice.parse::<usize>() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
399
src/tasks.rs
399
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<String>,
|
||||
|
||||
/// 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>,
|
||||
/// Zero: Only Active node
|
||||
/// Positive: Go down the respective level
|
||||
depth: i8,
|
||||
depth: usize,
|
||||
|
||||
/// Currently active tags
|
||||
tags: BTreeSet<Tag>,
|
||||
|
@ -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<Item=&str> {
|
||||
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::<Duration>().as_secs();
|
||||
}
|
||||
|
@ -317,35 +309,35 @@ impl Tasks {
|
|||
|
||||
// Helpers
|
||||
|
||||
fn resolve_tasks<'a>(&'a self, iter: impl Iterator<Item=&'a EventId>) -> impl Iterator<Item=&'a Task> {
|
||||
self.resolve_tasks_rec(iter, self.depth)
|
||||
fn resolve_tasks<'a>(
|
||||
&'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>(
|
||||
&'a self,
|
||||
iter: impl Iterator<Item=&'a EventId>,
|
||||
depth: i8,
|
||||
) -> Box<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| {
|
||||
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<Item=&Task> + '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() {
|
||||
vec![]
|
||||
} else {
|
||||
self.bookmarks.iter()
|
||||
.filter(|id| !position.is_some_and(|p| &p == id) && !current.contains_key(id))
|
||||
.filter_map(|id| self.get_by_id(id))
|
||||
.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| {
|
||||
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() ||
|
||||
t.tags.as_ref().map_or(false, |tags| {
|
||||
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) && !ids.contains(id))
|
||||
.filter_map(|id| self.get_by_id(id))
|
||||
.filter(|t| self.filter(t))
|
||||
.collect_vec()
|
||||
};
|
||||
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<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() {
|
||||
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<EventId> {
|
||||
pub(crate) fn get_matching(&self, position: Option<&EventId>, arg: &str) -> Vec<EventId> {
|
||||
if let Ok(id) = EventId::parse(arg) {
|
||||
return vec![id];
|
||||
}
|
||||
|
@ -617,7 +642,7 @@ impl Tasks {
|
|||
|
||||
let mut filtered: 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();
|
||||
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<EventId> {
|
||||
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
|
||||
/// Get all tasks until the specified depth matching the filter
|
||||
fn get_depth_filtered<F>(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;
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for ChildIterator<'a> {
|
||||
type Item = &'a EventId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index >= self.queue.len() {
|
||||
return None;
|
||||
}
|
||||
let id = self.queue[self.index];
|
||||
if let Some(task) = self.tasks.get(id) {
|
||||
self.queue.reserve(task.children.len());
|
||||
self.queue.extend(task.children.iter());
|
||||
} else {
|
||||
// Unknown task, 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());
|
||||
}
|
||||
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<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<'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> {
|
||||
|
@ -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]
|
||||
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]
|
||||
|
|
Loading…
Reference in New Issue