Compare commits
7 commits
20fc8f9a3a
...
bd32e61212
Author | SHA1 | Date | |
---|---|---|---|
|
bd32e61212 | ||
|
5cd82e8581 | ||
|
eea8511a6e | ||
|
5032b4db93 | ||
|
fc97b513c4 | ||
|
9c92a19cde | ||
|
0a7685d907 |
7 changed files with 317 additions and 173 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1488,7 +1488,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mostr"
|
name = "mostr"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-english",
|
"chrono-english",
|
||||||
|
|
|
@ -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.6.1"
|
version = "0.6.2"
|
||||||
rust-version = "1.82"
|
rust-version = "1.82"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "mostr"
|
default-run = "mostr"
|
||||||
|
|
96
src/hashtag.rs
Normal file
96
src/hashtag.rs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
use nostr_sdk::{Alphabet, Tag};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
pub fn is_hashtag(tag: &Tag) -> bool {
|
||||||
|
tag.single_letter_tag()
|
||||||
|
.is_some_and(|letter| letter.character == Alphabet::T)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This exists so that Hashtags can easily be matched without caring about case
|
||||||
|
/// but displayed in their original case
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Hashtag {
|
||||||
|
value: String,
|
||||||
|
lowercased: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashtag {
|
||||||
|
pub fn matches(&self, token: &str) -> bool {
|
||||||
|
self.lowercased.contains(&token.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Hashtag {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Hashtag {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
state.write(self.lowercased.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Hashtag {}
|
||||||
|
impl PartialEq<Self> for Hashtag {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.lowercased == other.lowercased
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<&Tag> for Hashtag {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: &Tag) -> Result<Self, Self::Error> {
|
||||||
|
value.content().take_if(|_| is_hashtag(value))
|
||||||
|
.map(|s| Hashtag::from(s))
|
||||||
|
.ok_or_else(|| "Tag is not a Hashtag".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&str> for Hashtag {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
let val = value.trim().to_string();
|
||||||
|
Hashtag {
|
||||||
|
lowercased: val.to_ascii_lowercase(),
|
||||||
|
value: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&Hashtag> for Tag {
|
||||||
|
fn from(value: &Hashtag) -> Self {
|
||||||
|
Tag::hashtag(&value.lowercased)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Hashtag {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.lowercased.cmp(&other.lowercased)
|
||||||
|
// Wanted to do this so lowercase tags are preferred,
|
||||||
|
// but is technically undefined behaviour
|
||||||
|
// because it deviates from Eq implementation
|
||||||
|
//match {
|
||||||
|
// Ordering::Equal => self.0.cmp(&other.0),
|
||||||
|
// other => other,
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialOrd for Hashtag {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.lowercased.cmp(&other.lowercased))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hashtag() {
|
||||||
|
assert_eq!("yeah", "YeaH".to_ascii_lowercase());
|
||||||
|
assert_eq!("yeah".to_ascii_lowercase().cmp(&"YeaH".to_ascii_lowercase()), Ordering::Equal);
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
let strings = vec!["yeah", "YeaH"];
|
||||||
|
let mut tags = strings.iter().cloned().map(Hashtag::from).sorted_unstable().collect_vec();
|
||||||
|
assert_eq!(strings, tags.iter().map(ToString::to_string).collect_vec());
|
||||||
|
tags.sort_unstable();
|
||||||
|
assert_eq!(strings, tags.iter().map(ToString::to_string).collect_vec());
|
||||||
|
}
|
13
src/kinds.rs
13
src/kinds.rs
|
@ -92,7 +92,7 @@ pub(crate) fn extract_hashtags(input: &str) -> impl Iterator<Item=Tag> + '_ {
|
||||||
input.split_ascii_whitespace()
|
input.split_ascii_whitespace()
|
||||||
.filter(|s| s.starts_with('#'))
|
.filter(|s| s.starts_with('#'))
|
||||||
.map(|s| s.trim_start_matches('#'))
|
.map(|s| s.trim_start_matches('#'))
|
||||||
.map(to_hashtag)
|
.map(to_hashtag_tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts everything after a " # " as a list of tags
|
/// Extracts everything after a " # " as a list of tags
|
||||||
|
@ -121,7 +121,7 @@ pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
||||||
let mut split = result.split(|e| { e == &"#" });
|
let mut split = result.split(|e| { e == &"#" });
|
||||||
let main = split.next().unwrap().join(" ");
|
let main = split.next().unwrap().join(" ");
|
||||||
let mut tags = extract_hashtags(&main)
|
let mut tags = extract_hashtags(&main)
|
||||||
.chain(split.flatten().map(|s| to_hashtag(&s)))
|
.chain(split.flatten().map(|s| to_hashtag_tag(&s)))
|
||||||
.chain(prio.map(|p| to_prio_tag(p)))
|
.chain(prio.map(|p| to_prio_tag(p)))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
tags.sort();
|
tags.sort();
|
||||||
|
@ -129,7 +129,7 @@ pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
||||||
(main, tags)
|
(main, tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_hashtag(tag: &str) -> Tag {
|
pub fn to_hashtag_tag(tag: &str) -> Tag {
|
||||||
TagStandard::Hashtag(tag.to_string()).into()
|
TagStandard::Hashtag(tag.to_string()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +155,6 @@ pub fn format_tag_basic(tag: &Tag) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_hashtag(tag: &Tag) -> bool {
|
|
||||||
tag.single_letter_tag()
|
|
||||||
.is_some_and(|letter| letter.character == Alphabet::T)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_prio_tag(value: Prio) -> Tag {
|
pub fn to_prio_tag(value: Prio) -> Tag {
|
||||||
Tag::custom(TagKind::Custom(Cow::from(PRIO)), [value.to_string()])
|
Tag::custom(TagKind::Custom(Cow::from(PRIO)), [value.to_string()])
|
||||||
}
|
}
|
||||||
|
@ -169,7 +164,7 @@ fn test_extract_tags() {
|
||||||
assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"),
|
assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"),
|
||||||
("Hello from #mars with #greetings #yeah".to_string(),
|
("Hello from #mars with #greetings #yeah".to_string(),
|
||||||
std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))
|
std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))
|
||||||
.chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag)).collect()));
|
.chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag_tag)).collect()));
|
||||||
assert_eq!(extract_tags("So tagless #"),
|
assert_eq!(extract_tags("So tagless #"),
|
||||||
("So tagless".to_string(), vec![]));
|
("So tagless".to_string(), vec![]));
|
||||||
}
|
}
|
22
src/main.rs
22
src/main.rs
|
@ -21,7 +21,6 @@ use itertools::Itertools;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use log::{debug, error, info, trace, warn, LevelFilter};
|
use log::{debug, error, info, trace, warn, LevelFilter};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use nostr_sdk::TagStandard::Hashtag;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustyline::config::Configurer;
|
use rustyline::config::Configurer;
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
|
@ -29,12 +28,14 @@ use rustyline::DefaultEditor;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::time::error::Elapsed;
|
use tokio::time::error::Elapsed;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
use crate::hashtag::Hashtag;
|
||||||
|
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod task;
|
mod task;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
mod kinds;
|
mod kinds;
|
||||||
mod event_sender;
|
mod event_sender;
|
||||||
|
mod hashtag;
|
||||||
|
|
||||||
const INACTVITY_DELAY: u64 = 200;
|
const INACTVITY_DELAY: u64 = 200;
|
||||||
const LOCAL_RELAY_NAME: &str = "TEMP";
|
const LOCAL_RELAY_NAME: &str = "TEMP";
|
||||||
|
@ -385,7 +386,7 @@ async fn main() -> Result<()> {
|
||||||
match arg {
|
match arg {
|
||||||
None => {
|
None => {
|
||||||
if let Some(task) = tasks.get_current_task() {
|
if let Some(task) = tasks.get_current_task() {
|
||||||
println!("Change History:");
|
println!("Change History for {}:", task.get_id());
|
||||||
for e in once(&task.event).chain(task.props.iter().rev()) {
|
for e in once(&task.event).chain(task.props.iter().rev()) {
|
||||||
println!("{} {} [{}]",
|
println!("{} {} [{}]",
|
||||||
format_timestamp_full(&e.created_at),
|
format_timestamp_full(&e.created_at),
|
||||||
|
@ -464,12 +465,9 @@ async fn main() -> Result<()> {
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if arg == "@" {
|
if arg == "@" {
|
||||||
tasks.reset_key_filter()
|
tasks.reset_key_filter()
|
||||||
} else if let Ok(key) = PublicKey::from_str(arg) {
|
} else if let Some((key, name)) = tasks.find_user_with_displayname(arg) {
|
||||||
info!("Showing {}'s tasks", tasks.get_username(&key));
|
info!("Showing {}'s tasks", name);
|
||||||
tasks.set_key_filter(key)
|
tasks.set_key_filter(key)
|
||||||
} else if let Some((key, meta)) = tasks.find_user(arg) {
|
|
||||||
info!("Showing {}'s tasks", meta.display_name.as_ref().unwrap_or(meta.name.as_ref().unwrap_or(&key.to_string())));
|
|
||||||
tasks.set_key_filter(key.clone())
|
|
||||||
} else {
|
} else {
|
||||||
if parse_hour(arg, 1)
|
if 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)))
|
||||||
|
@ -576,14 +574,14 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('#') => {
|
Some('#') => {
|
||||||
if !tasks.update_tags(arg_default.split_whitespace().map(|s| Hashtag(s.to_string()).into())) {
|
if !tasks.update_tags(arg_default.split_whitespace().map(Hashtag::from)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('+') =>
|
Some('+') =>
|
||||||
match arg {
|
match arg {
|
||||||
Some(arg) => tasks.add_tag(arg.to_string()),
|
Some(arg) => tasks.add_tag(arg),
|
||||||
None => {
|
None => {
|
||||||
tasks.print_hashtags();
|
tasks.print_hashtags();
|
||||||
if tasks.has_tag_filter() {
|
if tasks.has_tag_filter() {
|
||||||
|
@ -621,7 +619,7 @@ async fn main() -> Result<()> {
|
||||||
label)
|
label)
|
||||||
},
|
},
|
||||||
vec.iter().rev().join("\n"));
|
vec.iter().rev().join("\n"));
|
||||||
} else if let Ok(key) = PublicKey::parse(arg) { // TODO also match name
|
} else if let Some((key, _)) = tasks.find_user(arg) {
|
||||||
let (label, mut times) = tasks.times_tracked_for(&key);
|
let (label, mut times) = tasks.times_tracked_for(&key);
|
||||||
println!("{}\n{}", label.italic(),
|
println!("{}\n{}", label.italic(),
|
||||||
times.join("\n"));
|
times.join("\n"));
|
||||||
|
@ -708,8 +706,8 @@ async fn main() -> Result<()> {
|
||||||
let filtered =
|
let filtered =
|
||||||
tasks.get_filtered(pos, |t| {
|
tasks.get_filtered(pos, |t| {
|
||||||
transform(&t.event.content).contains(&remaining) ||
|
transform(&t.event.content).contains(&remaining) ||
|
||||||
t.get_hashtags().any(
|
t.list_hashtags().any(
|
||||||
|tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
|
|tag| tag.matches(&remaining))
|
||||||
});
|
});
|
||||||
if filtered.len() == 1 {
|
if filtered.len() == 1 {
|
||||||
tasks.move_to(filtered.into_iter().next());
|
tasks.move_to(filtered.into_iter().next());
|
||||||
|
|
20
src/task.rs
20
src/task.rs
|
@ -11,9 +11,9 @@ use itertools::Either::{Left, Right};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::{Alphabet, Event, EventId, Kind, Tag, Timestamp};
|
use nostr_sdk::{Alphabet, Event, EventId, Kind, Tag, Timestamp};
|
||||||
|
use crate::hashtag::{is_hashtag, Hashtag};
|
||||||
use crate::helpers::{format_timestamp_local, some_non_empty};
|
use crate::helpers::{format_timestamp_local, some_non_empty};
|
||||||
use crate::kinds::{is_hashtag, match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
use crate::kinds::{match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
||||||
use crate::tasks::now;
|
use crate::tasks::now;
|
||||||
|
|
||||||
pub static MARKER_PARENT: &str = "parent";
|
pub static MARKER_PARENT: &str = "parent";
|
||||||
|
@ -98,7 +98,8 @@ impl Task {
|
||||||
|
|
||||||
/// Description items, ordered newest to oldest
|
/// Description items, ordered newest to oldest
|
||||||
pub(crate) fn descriptions(&self) -> impl DoubleEndedIterator<Item=&String> + '_ {
|
pub(crate) fn descriptions(&self) -> impl DoubleEndedIterator<Item=&String> + '_ {
|
||||||
self.description_events().map(|e| &e.content)
|
self.description_events()
|
||||||
|
.filter_map(|e| Some(&e.content).take_if(|s| !s.trim().is_empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_task_kind(&self) -> bool {
|
pub(crate) fn is_task_kind(&self) -> bool {
|
||||||
|
@ -174,8 +175,8 @@ impl Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_hashtags(&self) -> impl Iterator<Item=&Tag> {
|
pub(crate) fn list_hashtags(&self) -> impl Iterator<Item=Hashtag> + use<'_> {
|
||||||
self.tags().filter(|t| is_hashtag(t))
|
self.tags().filter_map(|t| Hashtag::try_from(t).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
||||||
|
@ -222,10 +223,7 @@ impl Task {
|
||||||
.map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
|
.map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
|
||||||
.collect_vec()
|
.collect_vec()
|
||||||
)),
|
)),
|
||||||
"descriptions" => Some(format!(
|
"descriptions" => Some(format!("{:?}", self.descriptions().collect_vec())),
|
||||||
"{:?}",
|
|
||||||
self.descriptions().collect_vec()
|
|
||||||
)),
|
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Unknown task property {}", property);
|
warn!("Unknown task property {}", property);
|
||||||
None
|
None
|
||||||
|
@ -357,7 +355,7 @@ mod tasks_test {
|
||||||
EventBuilder::new(TASK_KIND, "task").tags([Tag::hashtag("tag1")])
|
EventBuilder::new(TASK_KIND, "task").tags([Tag::hashtag("tag1")])
|
||||||
.sign_with_keys(&keys).unwrap());
|
.sign_with_keys(&keys).unwrap());
|
||||||
assert_eq!(task.pure_state(), State::Open);
|
assert_eq!(task.pure_state(), State::Open);
|
||||||
assert_eq!(task.get_hashtags().count(), 1);
|
assert_eq!(task.list_hashtags().count(), 1);
|
||||||
task.props.insert(
|
task.props.insert(
|
||||||
EventBuilder::new(State::Done.into(), "")
|
EventBuilder::new(State::Done.into(), "")
|
||||||
.sign_with_keys(&keys).unwrap());
|
.sign_with_keys(&keys).unwrap());
|
||||||
|
@ -367,7 +365,7 @@ mod tasks_test {
|
||||||
.custom_created_at(Timestamp::from(Timestamp::now() - 2))
|
.custom_created_at(Timestamp::from(Timestamp::now() - 2))
|
||||||
.sign_with_keys(&keys).unwrap());
|
.sign_with_keys(&keys).unwrap());
|
||||||
assert_eq!(task.pure_state(), State::Done);
|
assert_eq!(task.pure_state(), State::Done);
|
||||||
assert_eq!(task.get_hashtags().count(), 2);
|
assert_eq!(task.list_hashtags().count(), 2);
|
||||||
task.props.insert(
|
task.props.insert(
|
||||||
EventBuilder::new(State::Closed.into(), "")
|
EventBuilder::new(State::Closed.into(), "")
|
||||||
.custom_created_at(Timestamp::from(Timestamp::now() + 1))
|
.custom_created_at(Timestamp::from(Timestamp::now() + 1))
|
||||||
|
|
335
src/tasks.rs
335
src/tasks.rs
|
@ -1,11 +1,11 @@
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::io::Write;
|
|
||||||
use std::iter::{empty, once, FusedIterator};
|
use std::iter::{empty, once, FusedIterator};
|
||||||
use std::ops::{Div, Rem};
|
use std::ops::{Div, Rem};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::hashtag::Hashtag;
|
||||||
use crate::event_sender::{EventSender, MostrMessage};
|
use crate::event_sender::{EventSender, MostrMessage};
|
||||||
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, to_string_or_default, CHARACTER_THRESHOLD};
|
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, to_string_or_default, CHARACTER_THRESHOLD};
|
||||||
use crate::kinds::*;
|
use crate::kinds::*;
|
||||||
|
@ -16,7 +16,6 @@ use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, SingleLetterTag, Tag, TagKind, TagStandard, Timestamp, Url};
|
use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, SingleLetterTag, Tag, TagKind, TagStandard, Timestamp, Url};
|
||||||
use regex::bytes::Regex;
|
use regex::bytes::Regex;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use TagStandard::Hashtag;
|
|
||||||
|
|
||||||
const DEFAULT_PRIO: Prio = 25;
|
const DEFAULT_PRIO: Prio = 25;
|
||||||
pub const HIGH_PRIO: Prio = 85;
|
pub const HIGH_PRIO: Prio = 85;
|
||||||
|
@ -72,9 +71,9 @@ pub(crate) struct TasksRelay {
|
||||||
pub(crate) recurse_activities: bool,
|
pub(crate) recurse_activities: bool,
|
||||||
|
|
||||||
/// Currently active tags
|
/// Currently active tags
|
||||||
tags: BTreeSet<Tag>,
|
tags: BTreeSet<Hashtag>,
|
||||||
/// Tags filtered out from view
|
/// Tags filtered out from view
|
||||||
tags_excluded: BTreeSet<Tag>,
|
tags_excluded: BTreeSet<Hashtag>,
|
||||||
/// Current active state
|
/// Current active state
|
||||||
state: StateFilter,
|
state: StateFilter,
|
||||||
/// Current priority for filtering and new tasks
|
/// Current priority for filtering and new tasks
|
||||||
|
@ -244,12 +243,8 @@ impl TasksRelay {
|
||||||
.filter(|t| t.pure_state() != State::Closed)
|
.filter(|t| t.pure_state() != State::Closed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
|
pub(crate) fn all_hashtags(&self) -> BTreeSet<Hashtag> {
|
||||||
self.nonclosed_tasks()
|
self.nonclosed_tasks().flat_map(|t| t.list_hashtags()).collect()
|
||||||
.flat_map(|t| t.get_hashtags())
|
|
||||||
.filter_map(|tag| tag.content().map(|s| s.trim()))
|
|
||||||
.sorted_unstable()
|
|
||||||
.dedup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dynamic time tracking overview for current task or current user.
|
/// Dynamic time tracking overview for current task or current user.
|
||||||
|
@ -284,12 +279,12 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
// TODO show history for active tags
|
// TODO show history for active tags
|
||||||
(
|
(
|
||||||
"Your Time-Tracking History:".to_string(),
|
format!("Time-Tracking History for {}:", self.get_displayname(&key)),
|
||||||
Box::from(full.into_iter()),
|
Box::from(full.into_iter()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
"You have nothing time-tracked yet".to_string(),
|
"Nothing time-tracked yet".to_string(),
|
||||||
Box::from(empty()),
|
Box::from(empty()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -397,10 +392,10 @@ impl TasksRelay {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for tag in self.tags.iter() {
|
for tag in self.tags.iter() {
|
||||||
prompt.push_str(&format!(" #{}", tag.content().unwrap()));
|
prompt.push_str(&format!(" #{}", tag));
|
||||||
}
|
}
|
||||||
for tag in self.tags_excluded.iter() {
|
for tag in self.tags_excluded.iter() {
|
||||||
prompt.push_str(&format!(" -#{}", tag.content().unwrap()));
|
prompt.push_str(&format!(" -#{}", tag));
|
||||||
}
|
}
|
||||||
prompt.push_str(&self.state.indicator());
|
prompt.push_str(&self.state.indicator());
|
||||||
self.priority.map(|p|
|
self.priority.map(|p|
|
||||||
|
@ -498,10 +493,10 @@ impl TasksRelay {
|
||||||
self.priority.is_none_or(|prio| {
|
self.priority.is_none_or(|prio| {
|
||||||
task.priority().unwrap_or(DEFAULT_PRIO) >= prio
|
task.priority().unwrap_or(DEFAULT_PRIO) >= prio
|
||||||
}) &&
|
}) &&
|
||||||
!task.get_hashtags().any(|tag| self.tags_excluded.contains(tag)) &&
|
!task.list_hashtags().any(|tag| self.tags_excluded.contains(&tag)) &&
|
||||||
(self.tags.is_empty() || {
|
(self.tags.is_empty() || {
|
||||||
let mut iter = task.get_hashtags().sorted_unstable();
|
let mut iter = task.list_hashtags().sorted_unstable();
|
||||||
self.tags.iter().all(|tag| iter.any(|t| t == tag))
|
self.tags.iter().all(|tag| iter.any(|t| &t == tag))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -547,16 +542,55 @@ impl TasksRelay {
|
||||||
current
|
current
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO this is a relict for tests
|
|
||||||
#[deprecated]
|
|
||||||
fn visible_tasks(&self) -> Vec<&Task> {
|
fn visible_tasks(&self) -> Vec<&Task> {
|
||||||
if self.search_depth == 0 {
|
let tasks = self.viewed_tasks();
|
||||||
return vec![];
|
if self.view.is_empty() && !tasks.is_empty() {
|
||||||
|
let bookmarks = self.bookmarked_tasks_deduped(&tasks);
|
||||||
|
return bookmarks.chain(tasks.into_iter()).collect_vec();
|
||||||
}
|
}
|
||||||
if !self.view.is_empty() {
|
tasks
|
||||||
return self.view.iter().flat_map(|id| self.get_by_id(id)).collect();
|
}
|
||||||
|
|
||||||
|
fn viewed_tasks(&self) -> Vec<&Task> {
|
||||||
|
let view = self.view.iter()
|
||||||
|
.flat_map(|id| self.get_by_id(id))
|
||||||
|
.collect_vec();
|
||||||
|
if self.search_depth > 0 && view.is_empty() {
|
||||||
|
self.resolve_tasks_rec(
|
||||||
|
self.tasks.children_for(self.get_position()),
|
||||||
|
true,
|
||||||
|
self.search_depth + self.view_depth,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.resolve_tasks_rec(view.into_iter(), true, self.view_depth)
|
||||||
}
|
}
|
||||||
self.filtered_tasks(self.get_position(), true)
|
}
|
||||||
|
|
||||||
|
fn bookmarked_tasks_deduped(&self, visible: &[&Task]) -> impl Iterator<Item=&Task> {
|
||||||
|
let tree = visible.iter()
|
||||||
|
.flat_map(|task| self.traverse_up_from(Some(task.event.id)))
|
||||||
|
.unique();
|
||||||
|
let pos = self.get_position();
|
||||||
|
let ids: HashSet<&EventId> = tree.map(|t| t.get_id()).chain(pos.as_ref()).collect();
|
||||||
|
// TODO add recent tasks (most time tracked + recently created)
|
||||||
|
self.bookmarks.iter()
|
||||||
|
.chain(
|
||||||
|
// Latest
|
||||||
|
self.tasks.values()
|
||||||
|
.sorted_unstable().rev()
|
||||||
|
.take(3).map(|t| t.get_id()))
|
||||||
|
.chain(
|
||||||
|
// Highest Prio
|
||||||
|
self.tasks.values()
|
||||||
|
.filter_map(|t| t.priority().filter(|p| *p > 35).map(|p| (p, t)))
|
||||||
|
.sorted_unstable()
|
||||||
|
.take(3).map(|(_, t)| t.get_id())
|
||||||
|
)
|
||||||
|
.filter(|id| !ids.contains(id))
|
||||||
|
.filter_map(|id| self.get_by_id(id))
|
||||||
|
.filter(|t| self.filter(t))
|
||||||
|
.sorted_by_cached_key(|t| self.sorting_key(t))
|
||||||
|
.dedup()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_property(&self, task: &Task, str: &str) -> String {
|
fn get_property(&self, task: &Task, str: &str) -> String {
|
||||||
|
@ -623,11 +657,29 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_user_with_displayname(&self, term: &str) -> Option<(PublicKey, String)> {
|
||||||
|
match PublicKey::from_str(term) {
|
||||||
|
Ok(key) => Some((key, self.get_displayname(&key))),
|
||||||
|
Err(_) => self.find_user(term).map(|(k, _)| (*k, self.get_displayname(k))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find username or key starting with the given term.
|
||||||
pub(crate) fn find_user(&self, term: &str) -> Option<(&PublicKey, &Metadata)> {
|
pub(crate) fn find_user(&self, term: &str) -> Option<(&PublicKey, &Metadata)> {
|
||||||
self.users.iter().find(|(_, v)|
|
if let Ok(key) = PublicKey::from_str(term) {
|
||||||
|
return self.users.get_key_value(&key);
|
||||||
|
}
|
||||||
|
self.users.iter().find(|(k, v)|
|
||||||
// TODO regex word boundary
|
// TODO regex word boundary
|
||||||
v.name.as_ref().is_some_and(|n| n.starts_with(term)) ||
|
v.name.as_ref().is_some_and(|n| n.starts_with(term)) ||
|
||||||
v.display_name.as_ref().is_some_and(|n| n.starts_with(term)))
|
v.display_name.as_ref().is_some_and(|n| n.starts_with(term)) ||
|
||||||
|
(term.len() > 4 && k.to_string().starts_with(term)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_displayname(&self, pubkey: &PublicKey) -> String {
|
||||||
|
self.users.get(pubkey)
|
||||||
|
.and_then(|m| m.display_name.clone().or(m.name.clone()))
|
||||||
|
.unwrap_or_else(|| pubkey.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_username(&self, pubkey: &PublicKey) -> String {
|
pub(crate) fn get_username(&self, pubkey: &PublicKey) -> String {
|
||||||
|
@ -728,12 +780,12 @@ impl TasksRelay {
|
||||||
pub(crate) fn print_hashtags(&self) {
|
pub(crate) fn print_hashtags(&self) {
|
||||||
println!(
|
println!(
|
||||||
"Hashtags of all known tasks:\n{}",
|
"Hashtags of all known tasks:\n{}",
|
||||||
self.all_hashtags().join(" ").italic()
|
self.all_hashtags().into_iter().join(" ").italic()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if tags have been updated, false if it printed something
|
/// Returns true if tags have been updated, false if it printed something
|
||||||
pub(crate) fn update_tags(&mut self, tags: impl IntoIterator<Item=Tag>) -> bool {
|
pub(crate) fn update_tags(&mut self, tags: impl IntoIterator<Item=Hashtag>) -> bool {
|
||||||
let mut peekable = tags.into_iter().peekable();
|
let mut peekable = tags.into_iter().peekable();
|
||||||
if self.tags.is_empty() && peekable.peek().is_none() {
|
if self.tags.is_empty() && peekable.peek().is_none() {
|
||||||
if !self.tags_excluded.is_empty() {
|
if !self.tags_excluded.is_empty() {
|
||||||
|
@ -747,15 +799,15 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_tags(&mut self, tags: impl IntoIterator<Item=Tag>) {
|
fn set_tags(&mut self, tags: impl IntoIterator<Item=Hashtag>) {
|
||||||
self.tags.clear();
|
self.tags.clear();
|
||||||
self.tags.extend(tags);
|
self.tags.extend(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_tag(&mut self, tag: String) {
|
pub(crate) fn add_tag(&mut self, tag: &str) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
info!("Added tag filter for #{tag}");
|
info!("Added tag filter for #{tag}");
|
||||||
let tag: Tag = Hashtag(tag).into();
|
let tag = Hashtag::from(tag);
|
||||||
self.tags_excluded.remove(&tag);
|
self.tags_excluded.remove(&tag);
|
||||||
self.tags.insert(tag);
|
self.tags.insert(tag);
|
||||||
}
|
}
|
||||||
|
@ -763,13 +815,11 @@ impl TasksRelay {
|
||||||
pub(crate) fn remove_tag(&mut self, tag: &str) {
|
pub(crate) fn remove_tag(&mut self, tag: &str) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
let len = self.tags.len();
|
let len = self.tags.len();
|
||||||
self.tags.retain(|t| {
|
self.tags.retain(|t| !t.matches(tag));
|
||||||
!t.content().is_some_and(|value| value.to_string().starts_with(tag))
|
|
||||||
});
|
|
||||||
if self.tags.len() < len {
|
if self.tags.len() < len {
|
||||||
info!("Removed tag filters starting with {tag}");
|
info!("Removed tag filters containing {tag}");
|
||||||
} else {
|
} else {
|
||||||
self.tags_excluded.insert(Hashtag(tag.to_string()).into());
|
self.tags_excluded.insert(Hashtag::from(tag).into());
|
||||||
info!("Excluding #{tag} from view");
|
info!("Excluding #{tag} from view");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -964,6 +1014,10 @@ impl TasksRelay {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn context_hashtags(&self) -> impl Iterator<Item=Tag> + use<'_> {
|
||||||
|
self.tags.iter().map(Tag::from)
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a task following the current state
|
/// Creates a task following the current state
|
||||||
///
|
///
|
||||||
/// Sanitizes input
|
/// Sanitizes input
|
||||||
|
@ -1007,7 +1061,7 @@ impl TasksRelay {
|
||||||
let id = self.submit(
|
let id = self.submit(
|
||||||
EventBuilder::new(TASK_KIND, &input)
|
EventBuilder::new(TASK_KIND, &input)
|
||||||
.tags(input_tags)
|
.tags(input_tags)
|
||||||
.tags(self.tags.iter().cloned())
|
.tags(self.context_hashtags())
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.tags(prio)
|
.tags(prio)
|
||||||
);
|
);
|
||||||
|
@ -1090,6 +1144,10 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add(&mut self, event: Event) {
|
pub(crate) fn add(&mut self, event: Event) {
|
||||||
|
let author = event.pubkey;
|
||||||
|
if !self.users.contains_key(&author) {
|
||||||
|
self.users.insert(author, Metadata::new());
|
||||||
|
}
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::GitIssue => self.add_task(event),
|
Kind::GitIssue => self.add_task(event),
|
||||||
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
||||||
|
@ -1255,12 +1313,12 @@ impl TasksRelay {
|
||||||
MARKER_PROPERTY
|
MARKER_PROPERTY
|
||||||
} else {
|
} else {
|
||||||
// Activity if parent is not a task
|
// Activity if parent is not a task
|
||||||
prop = prop.add_tags(self.tags.iter().cloned());
|
prop = prop.tags(self.context_hashtags());
|
||||||
MARKER_PARENT
|
MARKER_PARENT
|
||||||
};
|
};
|
||||||
info!("Created {} {format}", if marker == MARKER_PROPERTY { "note" } else { "activity" } );
|
info!("Created {} {format}", if marker == MARKER_PROPERTY { "note" } else { "activity" } );
|
||||||
self.submit(
|
self.submit(
|
||||||
prop.add_tags(
|
prop.tags(
|
||||||
self.get_position().map(|pos| self.make_event_tag_from_id(pos, marker))))
|
self.get_position().map(|pos| self.make_event_tag_from_id(pos, marker))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1324,35 +1382,10 @@ impl Display for TasksRelay {
|
||||||
writeln!(lock)?;
|
writeln!(lock)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let position = self.get_position();
|
let visible = self.viewed_tasks();
|
||||||
let mut current: Vec<&Task>;
|
|
||||||
let view = self.view.iter()
|
|
||||||
.flat_map(|id| self.get_by_id(id))
|
|
||||||
.collect_vec();
|
|
||||||
if self.search_depth > 0 && view.is_empty() {
|
|
||||||
current = self.resolve_tasks_rec(
|
|
||||||
self.tasks.children_for(position),
|
|
||||||
true,
|
|
||||||
self.search_depth + self.view_depth,
|
|
||||||
);
|
|
||||||
if current.is_empty() {
|
|
||||||
if !self.tags.is_empty() {
|
|
||||||
let mut children = self.tasks.children_for(position).peekable();
|
|
||||||
if children.peek().is_some() {
|
|
||||||
current = self.resolve_tasks_rec(children, true, 9);
|
|
||||||
if current.is_empty() {
|
|
||||||
writeln!(lock, "No tasks here matching{}", self.get_prompt_suffix())?;
|
|
||||||
} else {
|
|
||||||
writeln!(lock, "Found matching tasks beyond specified search depth:")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current = self.resolve_tasks_rec(view.into_iter(), true, self.view_depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
if current.is_empty() {
|
if visible.is_empty() {
|
||||||
|
writeln!(lock, "No tasks here matching{}", self.get_prompt_suffix())?;
|
||||||
let (label, times) = self.times_tracked();
|
let (label, times) = self.times_tracked();
|
||||||
let mut times_recent = times.rev().take(6).collect_vec();
|
let mut times_recent = times.rev().take(6).collect_vec();
|
||||||
times_recent.reverse();
|
times_recent.reverse();
|
||||||
|
@ -1361,32 +1394,8 @@ impl Display for TasksRelay {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let tree = current.iter()
|
|
||||||
.flat_map(|task| self.traverse_up_from(Some(task.event.id)))
|
|
||||||
.unique();
|
|
||||||
let ids: HashSet<&EventId> = tree.map(|t| t.get_id()).chain(position.as_ref()).collect();
|
|
||||||
if self.view.is_empty() {
|
if self.view.is_empty() {
|
||||||
let mut bookmarks =
|
let mut bookmarks = self.bookmarked_tasks_deduped(&visible).peekable();
|
||||||
// TODO add recent tasks (most time tracked + recently created)
|
|
||||||
self.bookmarks.iter()
|
|
||||||
.chain(
|
|
||||||
// Latest
|
|
||||||
self.tasks.values()
|
|
||||||
.sorted_unstable().rev()
|
|
||||||
.take(3).map(|t| t.get_id()))
|
|
||||||
.chain(
|
|
||||||
// Highest Prio
|
|
||||||
self.tasks.values()
|
|
||||||
.filter_map(|t| t.priority().filter(|p| *p > 35).map(|p| (p, t)))
|
|
||||||
.sorted_unstable()
|
|
||||||
.take(3).map(|(_, t)| t.get_id())
|
|
||||||
)
|
|
||||||
.filter(|id| !ids.contains(id))
|
|
||||||
.filter_map(|id| self.get_by_id(id))
|
|
||||||
.filter(|t| self.filter(t))
|
|
||||||
.sorted_by_cached_key(|t| self.sorting_key(t))
|
|
||||||
.dedup()
|
|
||||||
.peekable();
|
|
||||||
if bookmarks.peek().is_some() {
|
if bookmarks.peek().is_some() {
|
||||||
writeln!(lock, "{}", Colorize::bold("Quick Access"))?;
|
writeln!(lock, "{}", Colorize::bold("Quick Access"))?;
|
||||||
for task in bookmarks {
|
for task in bookmarks {
|
||||||
|
@ -1405,9 +1414,9 @@ impl Display for TasksRelay {
|
||||||
// TODO hide empty columns
|
// TODO hide empty columns
|
||||||
writeln!(lock, "{}", self.properties.join(" \t").bold())?;
|
writeln!(lock, "{}", self.properties.join(" \t").bold())?;
|
||||||
|
|
||||||
let count = current.len();
|
let count = visible.len();
|
||||||
let mut total_time = 0;
|
let mut total_time = 0;
|
||||||
for task in current {
|
for task in visible {
|
||||||
writeln!(
|
writeln!(
|
||||||
lock,
|
lock,
|
||||||
"{}",
|
"{}",
|
||||||
|
@ -1762,7 +1771,7 @@ mod tasks_test {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! assert_tasks {
|
macro_rules! assert_tasks_visible {
|
||||||
($left:expr, $right:expr $(,)?) => {
|
($left:expr, $right:expr $(,)?) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
$left
|
$left
|
||||||
|
@ -1775,6 +1784,22 @@ mod tasks_test {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_tasks_view {
|
||||||
|
($left:expr, $right:expr $(,)?) => {
|
||||||
|
let tasks = $left.viewed_tasks();
|
||||||
|
assert_eq!(
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.event.id)
|
||||||
|
.collect::<HashSet<EventId>>(),
|
||||||
|
HashSet::from($right),
|
||||||
|
"Tasks Visible: {:?}\nExpected: {:?}",
|
||||||
|
tasks,
|
||||||
|
$right.map(|id| $left.get_relative_path(id))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_recursive_closing() {
|
fn test_recursive_closing() {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
|
@ -1783,21 +1808,48 @@ mod tasks_test {
|
||||||
let parent = tasks.make_task("parent #tag1");
|
let parent = tasks.make_task("parent #tag1");
|
||||||
tasks.move_to(Some(parent));
|
tasks.move_to(Some(parent));
|
||||||
let sub = tasks.make_task("sub #oi # tag2");
|
let sub = tasks.make_task("sub #oi # tag2");
|
||||||
assert_eq!(tasks.all_hashtags().collect_vec(), vec!["oi", "tag1", "tag2"]);
|
assert_eq!(tasks.all_hashtags(), ["oi", "tag1", "tag2"].into_iter().map(Hashtag::from).collect());
|
||||||
tasks.make_note("note with #tag3 # yeah");
|
tasks.make_note("note with #tag3 # yeah");
|
||||||
assert_eq!(tasks.all_hashtags().collect_vec(), vec!["oi", "tag1", "tag2", "tag3", "yeah"]);
|
let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect();
|
||||||
|
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||||
|
|
||||||
tasks.custom_time = Some(Timestamp::now());
|
tasks.custom_time = Some(Timestamp::now());
|
||||||
tasks.update_state("Finished #yeah # oi", State::Done);
|
tasks.update_state("Finished #YeaH # oi", State::Done);
|
||||||
assert_eq!(tasks.get_by_id(&parent).unwrap().get_hashtags().cloned().collect_vec(), ["tag1", "oi", "yeah", "tag3", "yeah"].map(to_hashtag));
|
assert_eq!(tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), ["tag1", "YeaH", "oi", "tag3", "yeah"].map(Hashtag::from));
|
||||||
assert_eq!(tasks.all_hashtags().collect_vec(), vec!["oi", "tag1", "tag2", "tag3", "yeah"]);
|
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||||
|
|
||||||
tasks.custom_time = Some(now());
|
tasks.custom_time = Some(now());
|
||||||
tasks.update_state("Closing Down", State::Closed);
|
tasks.update_state("Closing Down", State::Closed);
|
||||||
assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed);
|
assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed);
|
||||||
assert_eq!(tasks.get_by_id(&parent).unwrap().pure_state(), State::Closed);
|
assert_eq!(tasks.get_by_id(&parent).unwrap().pure_state(), State::Closed);
|
||||||
assert_eq!(tasks.nonclosed_tasks().next(), None);
|
assert_eq!(tasks.nonclosed_tasks().next(), None);
|
||||||
assert_eq!(tasks.all_hashtags().next(), None);
|
assert_eq!(tasks.all_hashtags(), Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_context() {
|
||||||
|
let mut tasks = stub_tasks();
|
||||||
|
tasks.update_tags(["dp", "yeah"].into_iter().map(Hashtag::from));
|
||||||
|
assert_eq!(tasks.get_prompt_suffix(), " #dp #yeah");
|
||||||
|
tasks.remove_tag("Y");
|
||||||
|
assert_eq!(tasks.tags, ["dp"].into_iter().map(Hashtag::from).collect());
|
||||||
|
tasks.set_priority(Some(HIGH_PRIO));
|
||||||
|
assert_eq!(tasks.get_prompt_suffix(), " #dp *85");
|
||||||
|
let id = tasks.make_task("test # tag");
|
||||||
|
let task1 = tasks.get_by_id(&id).unwrap();
|
||||||
|
assert_eq!(task1.priority(), Some(HIGH_PRIO));
|
||||||
|
assert_eq!(
|
||||||
|
task1.list_hashtags().collect_vec(),
|
||||||
|
vec!["DP", "tag"].into_iter().map(Hashtag::from).collect_vec()
|
||||||
|
);
|
||||||
|
tasks.make_task_and_enter("another *4", State::Pending);
|
||||||
|
assert_eq!(tasks.get_current_task().unwrap().priority(), Some(40));
|
||||||
|
tasks.make_note("*3");
|
||||||
|
let task2 = tasks.get_current_task().unwrap();
|
||||||
|
assert_eq!(task2.descriptions().next(), None);
|
||||||
|
assert_eq!(task2.priority(), Some(30));
|
||||||
|
|
||||||
|
tasks.pubkey = Some(Keys::generate().public_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1808,13 +1860,13 @@ mod tasks_test {
|
||||||
EventBuilder::new(TASK_KIND, "sub")
|
EventBuilder::new(TASK_KIND, "sub")
|
||||||
.tags([tasks.make_event_tag_from_id(parent, MARKER_PARENT)])
|
.tags([tasks.make_event_tag_from_id(parent, MARKER_PARENT)])
|
||||||
);
|
);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||||
tasks.track_at(Timestamp::now(), Some(sub));
|
tasks.track_at(Timestamp::now(), Some(sub));
|
||||||
assert_eq!(tasks.get_own_events_history().count(), 1);
|
assert_eq!(tasks.get_own_events_history().count(), 1);
|
||||||
|
|
||||||
tasks.make_dependent_sibling("sibling");
|
tasks.make_dependent_sibling("sibling");
|
||||||
assert_eq!(tasks.len(), 3);
|
assert_eq!(tasks.len(), 3);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1823,7 +1875,7 @@ mod tasks_test {
|
||||||
let zero = EventId::all_zeros();
|
let zero = EventId::all_zeros();
|
||||||
let test = tasks.make_task("test # tag");
|
let test = tasks.make_task("test # tag");
|
||||||
let parent = tasks.make_task("parent");
|
let parent = tasks.make_task("parent");
|
||||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||||
tasks.move_to(Some(parent));
|
tasks.move_to(Some(parent));
|
||||||
let pin = tasks.make_task("pin");
|
let pin = tasks.make_task("pin");
|
||||||
|
|
||||||
|
@ -1839,7 +1891,7 @@ mod tasks_test {
|
||||||
EventBuilder::new(Kind::Bookmarks, "")
|
EventBuilder::new(Kind::Bookmarks, "")
|
||||||
.tags([Tag::event(pin), Tag::event(zero)])
|
.tags([Tag::event(pin), Tag::event(zero)])
|
||||||
);
|
);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||||
assert_eq!(tasks.filtered_tasks(Some(pin), true).len(), 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(pin), false).len(), 0);
|
||||||
assert_eq!(tasks.filtered_tasks(Some(zero), true).len(), 0);
|
assert_eq!(tasks.filtered_tasks(Some(zero), true).len(), 0);
|
||||||
|
@ -1850,11 +1902,11 @@ mod tasks_test {
|
||||||
|
|
||||||
tasks.move_to(None);
|
tasks.move_to(None);
|
||||||
assert_eq!(tasks.view_depth, 0);
|
assert_eq!(tasks.view_depth, 0);
|
||||||
assert_tasks!(tasks, [pin, test, parent]);
|
assert_tasks_visible!(tasks, [pin, test, parent]);
|
||||||
tasks.set_view_depth(1);
|
tasks.set_view_depth(1);
|
||||||
assert_tasks!(tasks, [pin, test]);
|
assert_tasks_visible!(tasks, [pin, test]);
|
||||||
tasks.add_tag("tag".to_string());
|
tasks.add_tag("tag");
|
||||||
assert_tasks!(tasks, [test]);
|
assert_tasks_visible!(tasks, [test]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tasks.filtered_tasks(None, true),
|
tasks.filtered_tasks(None, true),
|
||||||
vec![tasks.get_by_id(&test).unwrap()]
|
vec![tasks.get_by_id(&test).unwrap()]
|
||||||
|
@ -1862,9 +1914,9 @@ mod tasks_test {
|
||||||
|
|
||||||
tasks.submit(EventBuilder::new(Kind::Bookmarks, ""));
|
tasks.submit(EventBuilder::new(Kind::Bookmarks, ""));
|
||||||
tasks.clear_filters();
|
tasks.clear_filters();
|
||||||
assert_tasks!(tasks, [pin, test]);
|
assert_tasks_visible!(tasks, [pin, test]);
|
||||||
tasks.set_view_depth(0);
|
tasks.set_view_depth(0);
|
||||||
assert_tasks!(tasks, [test, parent]);
|
assert_tasks_visible!(tasks, [test, parent]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1876,9 +1928,9 @@ mod tasks_test {
|
||||||
EventBuilder::new(TASK_KIND, "side")
|
EventBuilder::new(TASK_KIND, "side")
|
||||||
.tags([tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)])
|
.tags([tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)])
|
||||||
);
|
);
|
||||||
assert_eq!(tasks.visible_tasks(), Vec::<&Task>::new());
|
assert_eq!(tasks.viewed_tasks(), Vec::<&Task>::new());
|
||||||
let sub_id = tasks.make_task("sub");
|
let sub_id = tasks.make_task("sub");
|
||||||
assert_tasks!(tasks, [sub_id]);
|
assert_tasks_view!(tasks, [sub_id]);
|
||||||
assert_eq!(tasks.len(), 3);
|
assert_eq!(tasks.len(), 3);
|
||||||
let sub = tasks.get_by_id(&sub_id).unwrap();
|
let sub = tasks.get_by_id(&sub_id).unwrap();
|
||||||
assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new());
|
assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new());
|
||||||
|
@ -1892,20 +1944,20 @@ mod tasks_test {
|
||||||
|
|
||||||
let id1 = tasks.filter_or_create(zero, "newer");
|
let id1 = tasks.filter_or_create(zero, "newer");
|
||||||
assert_eq!(tasks.len(), 1);
|
assert_eq!(tasks.len(), 1);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 0);
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||||
assert_eq!(tasks.get_by_id(&id1.unwrap()).unwrap().parent_id(), zero.as_ref());
|
assert_eq!(tasks.get_by_id(&id1.unwrap()).unwrap().parent_id(), zero.as_ref());
|
||||||
|
|
||||||
tasks.move_to(zero);
|
tasks.move_to(zero);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||||
let sub = tasks.make_task("test");
|
let sub = tasks.make_task("test");
|
||||||
assert_eq!(tasks.len(), 2);
|
assert_eq!(tasks.len(), 2);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||||
assert_eq!(tasks.get_by_id(&sub).unwrap().parent_id(), zero.as_ref());
|
assert_eq!(tasks.get_by_id(&sub).unwrap().parent_id(), zero.as_ref());
|
||||||
|
|
||||||
// Do not substring match invisible subtask
|
// Do not substring match invisible subtask
|
||||||
let id2 = tasks.filter_or_create(None, "#new-is gold wrapped").unwrap();
|
let id2 = tasks.filter_or_create(None, "#new-is gold wrapped").unwrap();
|
||||||
assert_eq!(tasks.len(), 3);
|
assert_eq!(tasks.len(), 3);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||||
let new2 = tasks.get_by_id(&id2).unwrap();
|
let new2 = tasks.get_by_id(&id2).unwrap();
|
||||||
assert_eq!(new2.props, Default::default());
|
assert_eq!(new2.props, Default::default());
|
||||||
|
|
||||||
|
@ -1981,32 +2033,32 @@ mod tasks_test {
|
||||||
assert_eq!(tasks.view_depth, 0);
|
assert_eq!(tasks.view_depth, 0);
|
||||||
assert_eq!(activity_t1.pure_state(), State::Open);
|
assert_eq!(activity_t1.pure_state(), State::Open);
|
||||||
debug!("{:?}", tasks);
|
debug!("{:?}", tasks);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||||
tasks.search_depth = 0;
|
tasks.search_depth = 0;
|
||||||
assert_eq!(tasks.visible_tasks().len(), 0);
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||||
tasks.recurse_activities = false;
|
tasks.recurse_activities = false;
|
||||||
assert_eq!(tasks.filtered_tasks(None, false).len(), 1);
|
assert_eq!(tasks.filtered_tasks(None, false).len(), 1);
|
||||||
|
|
||||||
tasks.move_to(Some(t1));
|
tasks.move_to(Some(t1));
|
||||||
assert_position!(tasks, t1);
|
assert_position!(tasks, t1);
|
||||||
tasks.search_depth = 2;
|
tasks.search_depth = 2;
|
||||||
assert_eq!(tasks.visible_tasks().len(), 0);
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||||
let t11 = tasks.make_task("t11 # tag");
|
let t11 = tasks.make_task("t11 # tag");
|
||||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||||
assert_eq!(tasks.get_task_path(Some(t11)), "t1>t11");
|
assert_eq!(tasks.get_task_path(Some(t11)), "t1>t11");
|
||||||
assert_eq!(tasks.get_relative_path(t11), "t11");
|
assert_eq!(tasks.get_relative_path(t11), "t11");
|
||||||
let t12 = tasks.make_task("t12");
|
let t12 = tasks.make_task("t12");
|
||||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||||
|
|
||||||
tasks.move_to(Some(t11));
|
tasks.move_to(Some(t11));
|
||||||
assert_position!(tasks, t11);
|
assert_position!(tasks, t11);
|
||||||
assert_eq!(tasks.visible_tasks().len(), 0);
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||||
let t111 = tasks.make_task("t111");
|
let t111 = tasks.make_task("t111");
|
||||||
assert_tasks!(tasks, [t111]);
|
assert_tasks_view!(tasks, [t111]);
|
||||||
assert_eq!(tasks.get_task_path(Some(t111)), "t1>t11>t111");
|
assert_eq!(tasks.get_task_path(Some(t111)), "t1>t11>t111");
|
||||||
assert_eq!(tasks.get_relative_path(t111), "t111");
|
assert_eq!(tasks.get_relative_path(t111), "t111");
|
||||||
tasks.view_depth = 2;
|
tasks.view_depth = 2;
|
||||||
assert_tasks!(tasks, [t111]);
|
assert_tasks_view!(tasks, [t111]);
|
||||||
|
|
||||||
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);
|
||||||
|
@ -2021,32 +2073,37 @@ mod tasks_test {
|
||||||
assert_eq!(tasks.get_own_events_history().count(), 3);
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
||||||
assert_eq!(tasks.get_relative_path(t111), "t11>t111");
|
assert_eq!(tasks.get_relative_path(t111), "t11>t111");
|
||||||
assert_eq!(tasks.view_depth, 2);
|
assert_eq!(tasks.view_depth, 2);
|
||||||
assert_tasks!(tasks, [t111, t12]);
|
tasks.set_search_depth(1);
|
||||||
tasks.set_view(vec![t11]);
|
assert_tasks_view!(tasks, [t111, t12]);
|
||||||
assert_tasks!(tasks, [t11]); // No more depth applied to view
|
|
||||||
tasks.set_search_depth(1); // resets view
|
|
||||||
assert_tasks!(tasks, [t111, t12]);
|
|
||||||
tasks.set_view_depth(0);
|
tasks.set_view_depth(0);
|
||||||
assert_tasks!(tasks, [t11, t12]);
|
assert_tasks_view!(tasks, [t11, t12]);
|
||||||
|
tasks.set_view(vec![t11]);
|
||||||
|
assert_tasks_view!(tasks, [t11]);
|
||||||
|
tasks.set_view_depth(1);
|
||||||
|
assert_tasks_view!(tasks, [t111]);
|
||||||
|
tasks.set_search_depth(1); // resets view
|
||||||
|
assert_tasks_view!(tasks, [t111, t12]);
|
||||||
|
tasks.set_view_depth(0);
|
||||||
|
assert_tasks_view!(tasks, [t11, t12]);
|
||||||
|
|
||||||
tasks.move_to(None);
|
tasks.move_to(None);
|
||||||
tasks.recurse_activities = true;
|
tasks.recurse_activities = true;
|
||||||
assert_tasks!(tasks, [t11, t12]);
|
assert_tasks_view!(tasks, [t11, t12]);
|
||||||
tasks.recurse_activities = false;
|
tasks.recurse_activities = false;
|
||||||
assert_tasks!(tasks, [t1]);
|
assert_tasks_view!(tasks, [t1]);
|
||||||
tasks.view_depth = 1;
|
tasks.view_depth = 1;
|
||||||
assert_tasks!(tasks, [t11, t12]);
|
assert_tasks_view!(tasks, [t11, t12]);
|
||||||
tasks.view_depth = 2;
|
tasks.view_depth = 2;
|
||||||
assert_tasks!(tasks, [t111, t12]);
|
assert_tasks_view!(tasks, [t111, t12]);
|
||||||
tasks.view_depth = 9;
|
tasks.view_depth = 9;
|
||||||
assert_tasks!(tasks, [t111, t12]);
|
assert_tasks_view!(tasks, [t111, t12]);
|
||||||
|
|
||||||
tasks.add_tag("tag".to_string());
|
tasks.add_tag("tag");
|
||||||
tasks.view_depth = 0;
|
tasks.view_depth = 0;
|
||||||
assert_tasks!(tasks, [t11]);
|
assert_tasks_view!(tasks, [t11]);
|
||||||
tasks.search_depth = 0;
|
tasks.search_depth = 0;
|
||||||
assert_eq!(tasks.view, []);
|
assert_eq!(tasks.view, []);
|
||||||
assert_tasks!(tasks, []);
|
assert_tasks_view!(tasks, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Add table
Reference in a new issue