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]]
|
||||
name = "mostr"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-english",
|
||||
|
|
|
@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
|
|||
readme = "README.md"
|
||||
license = "GPL 3.0"
|
||||
authors = ["melonion"]
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
rust-version = "1.82"
|
||||
edition = "2021"
|
||||
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()
|
||||
.filter(|s| s.starts_with('#'))
|
||||
.map(|s| s.trim_start_matches('#'))
|
||||
.map(to_hashtag)
|
||||
.map(to_hashtag_tag)
|
||||
}
|
||||
|
||||
/// 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 main = split.next().unwrap().join(" ");
|
||||
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)))
|
||||
.collect_vec();
|
||||
tags.sort();
|
||||
|
@ -129,7 +129,7 @@ pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
|||
(main, tags)
|
||||
}
|
||||
|
||||
pub fn to_hashtag(tag: &str) -> Tag {
|
||||
pub fn to_hashtag_tag(tag: &str) -> Tag {
|
||||
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 {
|
||||
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"),
|
||||
("Hello from #mars with #greetings #yeah".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 #"),
|
||||
("So tagless".to_string(), vec![]));
|
||||
}
|
22
src/main.rs
22
src/main.rs
|
@ -21,7 +21,6 @@ use itertools::Itertools;
|
|||
use keyring::Entry;
|
||||
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::error::ReadlineError;
|
||||
|
@ -29,12 +28,14 @@ use rustyline::DefaultEditor;
|
|||
use tokio::sync::mpsc;
|
||||
use tokio::time::error::Elapsed;
|
||||
use tokio::time::timeout;
|
||||
use crate::hashtag::Hashtag;
|
||||
|
||||
mod helpers;
|
||||
mod task;
|
||||
mod tasks;
|
||||
mod kinds;
|
||||
mod event_sender;
|
||||
mod hashtag;
|
||||
|
||||
const INACTVITY_DELAY: u64 = 200;
|
||||
const LOCAL_RELAY_NAME: &str = "TEMP";
|
||||
|
@ -385,7 +386,7 @@ async fn main() -> Result<()> {
|
|||
match arg {
|
||||
None => {
|
||||
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()) {
|
||||
println!("{} {} [{}]",
|
||||
format_timestamp_full(&e.created_at),
|
||||
|
@ -464,12 +465,9 @@ async fn main() -> Result<()> {
|
|||
Some(arg) => {
|
||||
if arg == "@" {
|
||||
tasks.reset_key_filter()
|
||||
} else if let Ok(key) = PublicKey::from_str(arg) {
|
||||
info!("Showing {}'s tasks", tasks.get_username(&key));
|
||||
} else if let Some((key, name)) = tasks.find_user_with_displayname(arg) {
|
||||
info!("Showing {}'s tasks", name);
|
||||
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 {
|
||||
if parse_hour(arg, 1)
|
||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
||||
|
@ -576,14 +574,14 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Some('+') =>
|
||||
match arg {
|
||||
Some(arg) => tasks.add_tag(arg.to_string()),
|
||||
Some(arg) => tasks.add_tag(arg),
|
||||
None => {
|
||||
tasks.print_hashtags();
|
||||
if tasks.has_tag_filter() {
|
||||
|
@ -621,7 +619,7 @@ async fn main() -> Result<()> {
|
|||
label)
|
||||
},
|
||||
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);
|
||||
println!("{}\n{}", label.italic(),
|
||||
times.join("\n"));
|
||||
|
@ -708,8 +706,8 @@ async fn main() -> Result<()> {
|
|||
let filtered =
|
||||
tasks.get_filtered(pos, |t| {
|
||||
transform(&t.event.content).contains(&remaining) ||
|
||||
t.get_hashtags().any(
|
||||
|tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
|
||||
t.list_hashtags().any(
|
||||
|tag| tag.matches(&remaining))
|
||||
});
|
||||
if filtered.len() == 1 {
|
||||
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 log::{debug, error, info, trace, warn};
|
||||
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::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;
|
||||
|
||||
pub static MARKER_PARENT: &str = "parent";
|
||||
|
@ -98,7 +98,8 @@ impl Task {
|
|||
|
||||
/// Description items, ordered newest to oldest
|
||||
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 {
|
||||
|
@ -174,8 +175,8 @@ impl Task {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_hashtags(&self) -> impl Iterator<Item=&Tag> {
|
||||
self.tags().filter(|t| is_hashtag(t))
|
||||
pub(crate) fn list_hashtags(&self) -> impl Iterator<Item=Hashtag> + use<'_> {
|
||||
self.tags().filter_map(|t| Hashtag::try_from(t).ok())
|
||||
}
|
||||
|
||||
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
||||
|
@ -222,10 +223,7 @@ impl Task {
|
|||
.map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
|
||||
.collect_vec()
|
||||
)),
|
||||
"descriptions" => Some(format!(
|
||||
"{:?}",
|
||||
self.descriptions().collect_vec()
|
||||
)),
|
||||
"descriptions" => Some(format!("{:?}", self.descriptions().collect_vec())),
|
||||
_ => {
|
||||
warn!("Unknown task property {}", property);
|
||||
None
|
||||
|
@ -357,7 +355,7 @@ mod tasks_test {
|
|||
EventBuilder::new(TASK_KIND, "task").tags([Tag::hashtag("tag1")])
|
||||
.sign_with_keys(&keys).unwrap());
|
||||
assert_eq!(task.pure_state(), State::Open);
|
||||
assert_eq!(task.get_hashtags().count(), 1);
|
||||
assert_eq!(task.list_hashtags().count(), 1);
|
||||
task.props.insert(
|
||||
EventBuilder::new(State::Done.into(), "")
|
||||
.sign_with_keys(&keys).unwrap());
|
||||
|
@ -367,7 +365,7 @@ mod tasks_test {
|
|||
.custom_created_at(Timestamp::from(Timestamp::now() - 2))
|
||||
.sign_with_keys(&keys).unwrap());
|
||||
assert_eq!(task.pure_state(), State::Done);
|
||||
assert_eq!(task.get_hashtags().count(), 2);
|
||||
assert_eq!(task.list_hashtags().count(), 2);
|
||||
task.props.insert(
|
||||
EventBuilder::new(State::Closed.into(), "")
|
||||
.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::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
use std::iter::{empty, once, FusedIterator};
|
||||
use std::ops::{Div, Rem};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::hashtag::Hashtag;
|
||||
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::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 regex::bytes::Regex;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use TagStandard::Hashtag;
|
||||
|
||||
const DEFAULT_PRIO: Prio = 25;
|
||||
pub const HIGH_PRIO: Prio = 85;
|
||||
|
@ -72,9 +71,9 @@ pub(crate) struct TasksRelay {
|
|||
pub(crate) recurse_activities: bool,
|
||||
|
||||
/// Currently active tags
|
||||
tags: BTreeSet<Tag>,
|
||||
tags: BTreeSet<Hashtag>,
|
||||
/// Tags filtered out from view
|
||||
tags_excluded: BTreeSet<Tag>,
|
||||
tags_excluded: BTreeSet<Hashtag>,
|
||||
/// Current active state
|
||||
state: StateFilter,
|
||||
/// Current priority for filtering and new tasks
|
||||
|
@ -244,12 +243,8 @@ impl TasksRelay {
|
|||
.filter(|t| t.pure_state() != State::Closed)
|
||||
}
|
||||
|
||||
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
|
||||
self.nonclosed_tasks()
|
||||
.flat_map(|t| t.get_hashtags())
|
||||
.filter_map(|tag| tag.content().map(|s| s.trim()))
|
||||
.sorted_unstable()
|
||||
.dedup()
|
||||
pub(crate) fn all_hashtags(&self) -> BTreeSet<Hashtag> {
|
||||
self.nonclosed_tasks().flat_map(|t| t.list_hashtags()).collect()
|
||||
}
|
||||
|
||||
/// Dynamic time tracking overview for current task or current user.
|
||||
|
@ -284,12 +279,12 @@ impl TasksRelay {
|
|||
}
|
||||
// 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()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"You have nothing time-tracked yet".to_string(),
|
||||
"Nothing time-tracked yet".to_string(),
|
||||
Box::from(empty()),
|
||||
)
|
||||
}
|
||||
|
@ -397,10 +392,10 @@ impl TasksRelay {
|
|||
},
|
||||
}
|
||||
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() {
|
||||
prompt.push_str(&format!(" -#{}", tag.content().unwrap()));
|
||||
prompt.push_str(&format!(" -#{}", tag));
|
||||
}
|
||||
prompt.push_str(&self.state.indicator());
|
||||
self.priority.map(|p|
|
||||
|
@ -498,10 +493,10 @@ impl TasksRelay {
|
|||
self.priority.is_none_or(|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() || {
|
||||
let mut iter = task.get_hashtags().sorted_unstable();
|
||||
self.tags.iter().all(|tag| iter.any(|t| t == tag))
|
||||
let mut iter = task.list_hashtags().sorted_unstable();
|
||||
self.tags.iter().all(|tag| iter.any(|t| &t == tag))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -547,16 +542,55 @@ impl TasksRelay {
|
|||
current
|
||||
}
|
||||
|
||||
// TODO this is a relict for tests
|
||||
#[deprecated]
|
||||
fn visible_tasks(&self) -> Vec<&Task> {
|
||||
if self.search_depth == 0 {
|
||||
return vec![];
|
||||
let tasks = self.viewed_tasks();
|
||||
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() {
|
||||
return self.view.iter().flat_map(|id| self.get_by_id(id)).collect();
|
||||
tasks
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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)> {
|
||||
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
|
||||
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 {
|
||||
|
@ -728,12 +780,12 @@ impl TasksRelay {
|
|||
pub(crate) fn print_hashtags(&self) {
|
||||
println!(
|
||||
"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
|
||||
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();
|
||||
if self.tags.is_empty() && peekable.peek().is_none() {
|
||||
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.extend(tags);
|
||||
}
|
||||
|
||||
pub(crate) fn add_tag(&mut self, tag: String) {
|
||||
pub(crate) fn add_tag(&mut self, tag: &str) {
|
||||
self.view.clear();
|
||||
info!("Added tag filter for #{tag}");
|
||||
let tag: Tag = Hashtag(tag).into();
|
||||
let tag = Hashtag::from(tag);
|
||||
self.tags_excluded.remove(&tag);
|
||||
self.tags.insert(tag);
|
||||
}
|
||||
|
@ -763,13 +815,11 @@ impl TasksRelay {
|
|||
pub(crate) fn remove_tag(&mut self, tag: &str) {
|
||||
self.view.clear();
|
||||
let len = self.tags.len();
|
||||
self.tags.retain(|t| {
|
||||
!t.content().is_some_and(|value| value.to_string().starts_with(tag))
|
||||
});
|
||||
self.tags.retain(|t| !t.matches(tag));
|
||||
if self.tags.len() < len {
|
||||
info!("Removed tag filters starting with {tag}");
|
||||
info!("Removed tag filters containing {tag}");
|
||||
} else {
|
||||
self.tags_excluded.insert(Hashtag(tag.to_string()).into());
|
||||
self.tags_excluded.insert(Hashtag::from(tag).into());
|
||||
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
|
||||
///
|
||||
/// Sanitizes input
|
||||
|
@ -1007,7 +1061,7 @@ impl TasksRelay {
|
|||
let id = self.submit(
|
||||
EventBuilder::new(TASK_KIND, &input)
|
||||
.tags(input_tags)
|
||||
.tags(self.tags.iter().cloned())
|
||||
.tags(self.context_hashtags())
|
||||
.tags(tags)
|
||||
.tags(prio)
|
||||
);
|
||||
|
@ -1090,6 +1144,10 @@ impl TasksRelay {
|
|||
}
|
||||
|
||||
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 {
|
||||
Kind::GitIssue => self.add_task(event),
|
||||
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
||||
|
@ -1255,12 +1313,12 @@ impl TasksRelay {
|
|||
MARKER_PROPERTY
|
||||
} else {
|
||||
// Activity if parent is not a task
|
||||
prop = prop.add_tags(self.tags.iter().cloned());
|
||||
prop = prop.tags(self.context_hashtags());
|
||||
MARKER_PARENT
|
||||
};
|
||||
info!("Created {} {format}", if marker == MARKER_PROPERTY { "note" } else { "activity" } );
|
||||
self.submit(
|
||||
prop.add_tags(
|
||||
prop.tags(
|
||||
self.get_position().map(|pos| self.make_event_tag_from_id(pos, marker))))
|
||||
}
|
||||
|
||||
|
@ -1324,35 +1382,10 @@ impl Display for TasksRelay {
|
|||
writeln!(lock)?;
|
||||
}
|
||||
|
||||
let position = self.get_position();
|
||||
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);
|
||||
}
|
||||
let visible = self.viewed_tasks();
|
||||
|
||||
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 mut times_recent = times.rev().take(6).collect_vec();
|
||||
times_recent.reverse();
|
||||
|
@ -1361,32 +1394,8 @@ impl Display for TasksRelay {
|
|||
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() {
|
||||
let mut bookmarks =
|
||||
// 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();
|
||||
let mut bookmarks = self.bookmarked_tasks_deduped(&visible).peekable();
|
||||
if bookmarks.peek().is_some() {
|
||||
writeln!(lock, "{}", Colorize::bold("Quick Access"))?;
|
||||
for task in bookmarks {
|
||||
|
@ -1405,9 +1414,9 @@ impl Display for TasksRelay {
|
|||
// TODO hide empty columns
|
||||
writeln!(lock, "{}", self.properties.join(" \t").bold())?;
|
||||
|
||||
let count = current.len();
|
||||
let count = visible.len();
|
||||
let mut total_time = 0;
|
||||
for task in current {
|
||||
for task in visible {
|
||||
writeln!(
|
||||
lock,
|
||||
"{}",
|
||||
|
@ -1762,7 +1771,7 @@ mod tasks_test {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_tasks {
|
||||
macro_rules! assert_tasks_visible {
|
||||
($left:expr, $right:expr $(,)?) => {
|
||||
assert_eq!(
|
||||
$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]
|
||||
fn test_recursive_closing() {
|
||||
let mut tasks = stub_tasks();
|
||||
|
@ -1783,21 +1808,48 @@ mod tasks_test {
|
|||
let parent = tasks.make_task("parent #tag1");
|
||||
tasks.move_to(Some(parent));
|
||||
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");
|
||||
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.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.all_hashtags().collect_vec(), vec!["oi", "tag1", "tag2", "tag3", "yeah"]);
|
||||
tasks.update_state("Finished #YeaH # oi", State::Done);
|
||||
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(), all_tags);
|
||||
|
||||
tasks.custom_time = Some(now());
|
||||
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(&parent).unwrap().pure_state(), State::Closed);
|
||||
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]
|
||||
|
@ -1808,13 +1860,13 @@ mod tasks_test {
|
|||
EventBuilder::new(TASK_KIND, "sub")
|
||||
.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));
|
||||
assert_eq!(tasks.get_own_events_history().count(), 1);
|
||||
|
||||
tasks.make_dependent_sibling("sibling");
|
||||
assert_eq!(tasks.len(), 3);
|
||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
||||
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1823,7 +1875,7 @@ mod tasks_test {
|
|||
let zero = EventId::all_zeros();
|
||||
let test = tasks.make_task("test # tag");
|
||||
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));
|
||||
let pin = tasks.make_task("pin");
|
||||
|
||||
|
@ -1839,7 +1891,7 @@ mod tasks_test {
|
|||
EventBuilder::new(Kind::Bookmarks, "")
|
||||
.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), false).len(), 0);
|
||||
assert_eq!(tasks.filtered_tasks(Some(zero), true).len(), 0);
|
||||
|
@ -1850,11 +1902,11 @@ mod tasks_test {
|
|||
|
||||
tasks.move_to(None);
|
||||
assert_eq!(tasks.view_depth, 0);
|
||||
assert_tasks!(tasks, [pin, test, parent]);
|
||||
assert_tasks_visible!(tasks, [pin, test, parent]);
|
||||
tasks.set_view_depth(1);
|
||||
assert_tasks!(tasks, [pin, test]);
|
||||
tasks.add_tag("tag".to_string());
|
||||
assert_tasks!(tasks, [test]);
|
||||
assert_tasks_visible!(tasks, [pin, test]);
|
||||
tasks.add_tag("tag");
|
||||
assert_tasks_visible!(tasks, [test]);
|
||||
assert_eq!(
|
||||
tasks.filtered_tasks(None, true),
|
||||
vec![tasks.get_by_id(&test).unwrap()]
|
||||
|
@ -1862,9 +1914,9 @@ mod tasks_test {
|
|||
|
||||
tasks.submit(EventBuilder::new(Kind::Bookmarks, ""));
|
||||
tasks.clear_filters();
|
||||
assert_tasks!(tasks, [pin, test]);
|
||||
assert_tasks_visible!(tasks, [pin, test]);
|
||||
tasks.set_view_depth(0);
|
||||
assert_tasks!(tasks, [test, parent]);
|
||||
assert_tasks_visible!(tasks, [test, parent]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1876,9 +1928,9 @@ mod tasks_test {
|
|||
EventBuilder::new(TASK_KIND, "side")
|
||||
.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");
|
||||
assert_tasks!(tasks, [sub_id]);
|
||||
assert_tasks_view!(tasks, [sub_id]);
|
||||
assert_eq!(tasks.len(), 3);
|
||||
let sub = tasks.get_by_id(&sub_id).unwrap();
|
||||
assert_eq!(sub.get_dependendees(), Vec::<&EventId>::new());
|
||||
|
@ -1892,20 +1944,20 @@ mod tasks_test {
|
|||
|
||||
let id1 = tasks.filter_or_create(zero, "newer");
|
||||
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());
|
||||
|
||||
tasks.move_to(zero);
|
||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
||||
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||
let sub = tasks.make_task("test");
|
||||
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());
|
||||
|
||||
// Do not substring match invisible subtask
|
||||
let id2 = tasks.filter_or_create(None, "#new-is gold wrapped").unwrap();
|
||||
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();
|
||||
assert_eq!(new2.props, Default::default());
|
||||
|
||||
|
@ -1981,32 +2033,32 @@ mod tasks_test {
|
|||
assert_eq!(tasks.view_depth, 0);
|
||||
assert_eq!(activity_t1.pure_state(), State::Open);
|
||||
debug!("{:?}", tasks);
|
||||
assert_eq!(tasks.visible_tasks().len(), 1);
|
||||
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||
tasks.search_depth = 0;
|
||||
assert_eq!(tasks.visible_tasks().len(), 0);
|
||||
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||
tasks.recurse_activities = false;
|
||||
assert_eq!(tasks.filtered_tasks(None, false).len(), 1);
|
||||
|
||||
tasks.move_to(Some(t1));
|
||||
assert_position!(tasks, t1);
|
||||
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");
|
||||
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_relative_path(t11), "t11");
|
||||
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));
|
||||
assert_position!(tasks, t11);
|
||||
assert_eq!(tasks.visible_tasks().len(), 0);
|
||||
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||
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_relative_path(t111), "t111");
|
||||
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_depth(0).len(), 1);
|
||||
|
@ -2021,32 +2073,37 @@ mod tasks_test {
|
|||
assert_eq!(tasks.get_own_events_history().count(), 3);
|
||||
assert_eq!(tasks.get_relative_path(t111), "t11>t111");
|
||||
assert_eq!(tasks.view_depth, 2);
|
||||
assert_tasks!(tasks, [t111, t12]);
|
||||
tasks.set_view(vec![t11]);
|
||||
assert_tasks!(tasks, [t11]); // No more depth applied to view
|
||||
tasks.set_search_depth(1); // resets view
|
||||
assert_tasks!(tasks, [t111, t12]);
|
||||
tasks.set_search_depth(1);
|
||||
assert_tasks_view!(tasks, [t111, t12]);
|
||||
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.recurse_activities = true;
|
||||
assert_tasks!(tasks, [t11, t12]);
|
||||
assert_tasks_view!(tasks, [t11, t12]);
|
||||
tasks.recurse_activities = false;
|
||||
assert_tasks!(tasks, [t1]);
|
||||
assert_tasks_view!(tasks, [t1]);
|
||||
tasks.view_depth = 1;
|
||||
assert_tasks!(tasks, [t11, t12]);
|
||||
assert_tasks_view!(tasks, [t11, t12]);
|
||||
tasks.view_depth = 2;
|
||||
assert_tasks!(tasks, [t111, t12]);
|
||||
assert_tasks_view!(tasks, [t111, t12]);
|
||||
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;
|
||||
assert_tasks!(tasks, [t11]);
|
||||
assert_tasks_view!(tasks, [t11]);
|
||||
tasks.search_depth = 0;
|
||||
assert_eq!(tasks.view, []);
|
||||
assert_tasks!(tasks, []);
|
||||
assert_tasks_view!(tasks, []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Add table
Reference in a new issue