Compare commits

...

6 Commits

Author SHA1 Message Date
xeruf 4b59b273f5 feat(tasks): extend search depth by tags if appropriate 2024-09-07 16:26:55 +03:00
xeruf bb3bb1fd56 feat: better feedback on bookmarking 2024-09-07 16:25:44 +03:00
xeruf 593ebcddca feat(main): make empty / go to root 2024-09-07 16:06:59 +03:00
xeruf 132ea048a5 feat: greatly revamp filtering 2024-09-07 16:06:28 +03:00
xeruf ddc57dc36a fix(tasks): do not panic on missing children 2024-09-05 13:56:48 +03:00
xeruf 77bfc4cb7a fix: add weekday to relative date formatting 2024-09-05 13:50:50 +03:00
6 changed files with 388 additions and 243 deletions

70
Cargo.lock generated
View File

@ -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",
]

View File

@ -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]

View File

@ -101,7 +101,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
+ match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive)
+ no match: create & activate task
- `.2` - set view depth to the given number (how many subtask levels to show, default is 1)
- `/[TEXT]` - activate task or filter by smart-case substring match
- `/[TEXT]` - activate task or filter by smart-case substring match (empty: move to root)
- `||TASK` - create and activate a new task procedure (where subtasks automatically depend on the previously created task)
- `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up

View File

@ -1,7 +1,7 @@
use std::ops::Sub;
use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc};
use chrono::LocalResult::Single;
use chrono::{DateTime, Local, NaiveTime, TimeDelta, TimeZone, Utc};
use log::{debug, error, info, trace, warn};
use nostr_sdk::Timestamp;
@ -86,10 +86,10 @@ pub fn format_datetime_relative(time: DateTime<Local>) -> String {
-1 => "tomorrow ".into(),
0 => "".into(),
1 => "yesterday ".into(),
-3..=3 => date.format("%a ").to_string(),
//-10..=10 => date.format("%d. %a ").to_string(),
-100..=100 => date.format("%b %d ").to_string(),
_ => date.format("%y-%m-%d ").to_string(),
//-3..=3 => date.format("%a ").to_string(),
-10..=10 => date.format("%d. %a ").to_string(),
-100..=100 => date.format("%a %b %d ").to_string(),
_ => date.format("%y-%m-%d %a ").to_string(),
};
format!("{}{}", prefix, time.format("%H:%M"))
}

View File

@ -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,65 +450,53 @@ 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;
}
}
Some('*') => {
match arg {
None => match tasks.get_position_ref() {
None => match tasks.get_position() {
None => {
info!("Filtering for bookmarked tasks");
tasks.set_filter_bookmarks()
},
Some(pos) => {
info!("Toggling bookmark");
or_warn!(tasks.toggle_bookmark(*pos));
tasks.set_view_bookmarks();
}
Some(pos) =>
match or_warn!(tasks.toggle_bookmark(pos)) {
Some(true) => info!("Bookmarking \"{}\"", tasks.get_task_title(&pos)),
Some(false) => info!("Removing bookmark for \"{}\"", tasks.get_task_title(&pos)),
None => {}
}
},
Some(arg) => info!("Setting priority not yet implemented"),
}
@ -636,7 +624,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());
}
@ -646,7 +634,9 @@ async fn main() -> Result<()> {
}
}
Some('/') => {
Some('/') => if arg.is_none() {
tasks.move_to(None);
} else {
let mut dots = 1;
let mut pos = tasks.get_position_ref();
for _ in iter.take_while(|c| c == &'/') {
@ -667,19 +657,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);
}
}
}

View File

@ -1,22 +1,22 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::fmt::{Display, Formatter};
use std::io::{Error, stdout, Write};
use std::iter::{empty, once};
use std::io::{stdout, Error, Write};
use std::iter::{empty, once, FusedIterator};
use std::ops::{Div, Rem};
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,51 @@ 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() {
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);
if current.is_empty() {
if !self.tags.is_empty() {
let mut children = self.children_of(self.get_position_ref()).peekable();
if children.peek().is_some() {
current = self.resolve_tasks_rec(children, true, 9);
if sparse {
if current.is_empty() {
println!("No tasks here matching{}", self.get_prompt_suffix());
} else {
println!("Found some matching tasks beyond specified view depth:");
}
}
}
}
}
let ids = current.iter().map(|t| t.get_id()).collect_vec();
let mut bookmarks =
if sparse && current.is_empty() {
vec![]
} else {
// TODO highlight bookmarks
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 +421,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> {
@ -530,28 +541,69 @@ impl Tasks {
// Movement and Selection
pub(crate) fn toggle_bookmark(&mut self, id: EventId) -> nostr_sdk::Result<Event> {
match self.bookmarks.iter().position(|b| b == &id) {
None => self.bookmarks.push(id),
Some(pos) => { self.bookmarks.remove(pos); }
}
/// Toggle bookmark on the given id.
/// Returns whether it was added (true) or removed (false).
pub(crate) fn toggle_bookmark(&mut self, id: EventId) -> nostr_sdk::Result<bool> {
let added = match self.bookmarks.iter().position(|b| b == &id) {
None => {
self.bookmarks.push(id);
true
}
Some(pos) => {
self.bookmarks.remove(pos);
false
}
};
self.sender.submit(
EventBuilder::new(Kind::Bookmarks, "mostr pins",
self.bookmarks.iter().map(|id| Tag::event(*id))))
self.bookmarks.iter().map(|id| Tag::event(*id))))?;
Ok(added)
}
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 +653,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 +669,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 +703,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 +722,7 @@ impl Tasks {
_ => {
// Multiple match, filter
self.move_to(position.cloned());
self.set_filter(filtered);
self.set_view(filtered);
None
}
}
@ -976,10 +1028,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 +1188,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 +1219,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,49 +1258,109 @@ 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
}
fn get_all(mut self) -> Vec<&'a EventId> {
while self.next().is_some() {}
self.queue
}
}
impl<'a> Iterator for ChildIterator<'a> {
type Item = &'a EventId;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.queue.len() {
return None;
}
let id = self.queue[self.index];
if let Some(task) = self.tasks.get(id) {
self.queue.reserve(task.children.len());
self.queue.extend(task.children.iter());
} else {
// Unknown task, 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<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;
}
}
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<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();
})
}
}
@ -1223,7 +1375,14 @@ impl<'a> Iterator for ParentIterator<'a> {
fn next(&mut self) -> Option<Self::Item> {
self.current.and_then(|id| self.tasks.get(&id)).map(|t| {
self.prev.inspect(|id| assert!(t.children.contains(id)));
self.prev.inspect(|id| {
// Fails if child is discovered before parent
// Need to reverse add as well
//assert!(t.children.contains(id))
if !t.children.contains(id) {
warn!("\"{}\" is missing child \"{}\"", t.get_title(), self.tasks.get(id).map_or(id.to_string(), |cht| cht.get_title()))
}
});
self.prev = self.current;
self.current = t.parent_id().cloned();
t
@ -1256,6 +1415,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();
@ -1266,29 +1432,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]
@ -1397,24 +1567,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);
@ -1427,33 +1595,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]