Compare commits
8 commits
ca263b50d2
...
cdf75cda24
Author | SHA1 | Date | |
---|---|---|---|
|
cdf75cda24 | ||
|
e1c1b1d4f6 | ||
|
6fc8b42bcc | ||
|
0dba23bcc6 | ||
|
db11b54220 | ||
|
df598efdc3 | ||
|
d159004340 | ||
|
591adafd6e |
9 changed files with 3299 additions and 161 deletions
3085
Cargo.lock
generated
3085
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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.7.0"
|
version = "0.7.1"
|
||||||
rust-version = "1.82"
|
rust-version = "1.82"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "mostr"
|
default-run = "mostr"
|
||||||
|
@ -26,6 +26,7 @@ rustyline = { git = "https://github.com/xeruf/rustyline", rev = "465b14d" }
|
||||||
keyring = { version = "3", features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] }
|
keyring = { version = "3", features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] }
|
||||||
directories = "5.0"
|
directories = "5.0"
|
||||||
whoami = "1.5"
|
whoami = "1.5"
|
||||||
|
slint = "1.8"
|
||||||
# Application Utils
|
# Application Utils
|
||||||
itertools = "0.12"
|
itertools = "0.12"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
|
|
@ -17,9 +17,12 @@ pub struct Hashtag {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hashtag {
|
impl Hashtag {
|
||||||
pub fn matches(&self, token: &str) -> bool {
|
pub fn contains(&self, token: &str) -> bool {
|
||||||
self.lowercased.contains(&token.to_ascii_lowercase())
|
self.lowercased.contains(&token.to_ascii_lowercase())
|
||||||
}
|
}
|
||||||
|
pub fn matches(&self, token: &str) -> bool {
|
||||||
|
token.contains(&self.lowercased)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Hashtag {
|
impl Display for Hashtag {
|
||||||
|
|
|
@ -36,7 +36,7 @@ impl<T: TimeZone> ToTimestamp for DateTime<T> {
|
||||||
|
|
||||||
/// Parses the hour from a plain number in the String,
|
/// Parses the hour from a plain number in the String,
|
||||||
/// with max of max_future hours into the future.
|
/// with max of max_future hours into the future.
|
||||||
/// TODO parse HHMM as well
|
// TODO parse HHMM as well
|
||||||
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
||||||
str.parse::<u32>().ok().and_then(|hour| {
|
str.parse::<u32>().ok().and_then(|hour| {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
|
|
|
@ -118,8 +118,8 @@ pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec<Tag>
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Ok(num) = s[1..].parse::<Prio>() {
|
if let Ok(num) = s[1..].parse::<Prio>() {
|
||||||
tags.push(to_prio_tag(num * (if s.len() > 2 { 1 } else { 10 })));
|
tags.push(to_prio_tag(num * (if s.len() > 2 { 1 } else { 10 })));
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
@ -154,7 +154,7 @@ pub fn format_tag_basic(tag: &Tag) -> String {
|
||||||
public_key,
|
public_key,
|
||||||
alias,
|
alias,
|
||||||
..
|
..
|
||||||
}) => format!("Key{}: {:.8}", public_key, alias.as_ref().map(|s| format!(" {s}")).unwrap_or_default()),
|
}) => format!("Key{}: {:.8}", alias.as_ref().map(|s| format!(" {s}")).unwrap_or_default(), public_key),
|
||||||
Some(TagStandard::Hashtag(content)) =>
|
Some(TagStandard::Hashtag(content)) =>
|
||||||
format!("#{content}"),
|
format!("#{content}"),
|
||||||
_ => tag.as_slice().join(" ")
|
_ => tag.as_slice().join(" ")
|
||||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -9,6 +9,7 @@ use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::event_sender::MostrMessage;
|
use crate::event_sender::MostrMessage;
|
||||||
|
use crate::hashtag::Hashtag;
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
||||||
use crate::task::{State, Task, TaskState, MARKER_PROPERTY};
|
use crate::task::{State, Task, TaskState, MARKER_PROPERTY};
|
||||||
|
@ -28,7 +29,6 @@ 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;
|
||||||
|
@ -282,8 +282,9 @@ async fn main() -> Result<()> {
|
||||||
println!();
|
println!();
|
||||||
let tasks = relays.get(&selected_relay).unwrap();
|
let tasks = relays.get(&selected_relay).unwrap();
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
"{} {}{}{}",
|
"{}{} {}{}{}",
|
||||||
selected_relay.as_ref().map_or(LOCAL_RELAY_NAME.to_string(), |url| url.to_string()).dimmed(),
|
selected_relay.as_ref().map_or(LOCAL_RELAY_NAME.to_string(), |url| url.to_string()).dimmed(),
|
||||||
|
tasks.pubkey_str().map_or(String::new(), |s| format!(" @{s}")),
|
||||||
tasks.get_task_path(tasks.get_position()).bold(),
|
tasks.get_task_path(tasks.get_position()).bold(),
|
||||||
tasks.get_prompt_suffix().italic(),
|
tasks.get_prompt_suffix().italic(),
|
||||||
"❯ ".dimmed()
|
"❯ ".dimmed()
|
||||||
|
@ -403,7 +404,7 @@ async fn main() -> Result<()> {
|
||||||
Some(et) =>
|
Some(et) =>
|
||||||
Some(et).take_if(|et| et.marker.as_ref().is_some_and(|m| m != MARKER_PROPERTY))
|
Some(et).take_if(|et| et.marker.as_ref().is_some_and(|m| m != MARKER_PROPERTY))
|
||||||
.map(|et| format!("{}: {}", et.marker.as_ref().unwrap(), tasks.get_relative_path(et.id))),
|
.map(|et| format!("{}: {}", et.marker.as_ref().unwrap(), tasks.get_relative_path(et.id))),
|
||||||
None =>
|
None =>
|
||||||
Some(format_tag_basic(t)),
|
Some(format_tag_basic(t)),
|
||||||
}
|
}
|
||||||
}).join(", ")
|
}).join(", ")
|
||||||
|
@ -491,7 +492,8 @@ async fn main() -> Result<()> {
|
||||||
tasks.set_key_filter(key)
|
tasks.set_key_filter(key)
|
||||||
} 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)))
|
||||||
.map(|time| {
|
.map(|time| {
|
||||||
info!("Filtering for tasks from {}", format_datetime_relative(time));
|
info!("Filtering for tasks from {}", format_datetime_relative(time));
|
||||||
tasks.set_filter_since(time.to_timestamp())
|
tasks.set_filter_since(time.to_timestamp())
|
||||||
|
@ -638,7 +640,7 @@ async fn main() -> Result<()> {
|
||||||
format!("{} {}",
|
format!("{} {}",
|
||||||
if max == usize::MAX { "All".to_string() } else { format!("Latest {max} entries of") },
|
if max == usize::MAX { "All".to_string() } else { format!("Latest {max} entries of") },
|
||||||
label)
|
label)
|
||||||
},
|
}.italic(),
|
||||||
vec.iter().rev().join("\n"));
|
vec.iter().rev().join("\n"));
|
||||||
} else if let Some((key, _)) = tasks.find_user(arg) {
|
} 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);
|
||||||
|
@ -728,7 +730,7 @@ async fn main() -> Result<()> {
|
||||||
tasks.get_filtered(pos, |t| {
|
tasks.get_filtered(pos, |t| {
|
||||||
transform(&t.event.content).contains(&remaining) ||
|
transform(&t.event.content).contains(&remaining) ||
|
||||||
t.list_hashtags().any(
|
t.list_hashtags().any(
|
||||||
|tag| tag.matches(&remaining))
|
|tag| tag.contains(&remaining))
|
||||||
});
|
});
|
||||||
if filtered.len() == 1 {
|
if filtered.len() == 1 {
|
||||||
tasks.move_to(filtered.into_iter().next());
|
tasks.move_to(filtered.into_iter().next());
|
||||||
|
|
26
src/task.rs
26
src/task.rs
|
@ -7,15 +7,16 @@ use std::iter::once;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
|
use crate::hashtag::{is_hashtag, Hashtag};
|
||||||
|
use crate::helpers::{format_timestamp_local, some_non_empty};
|
||||||
|
use crate::kinds::{match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
||||||
|
use crate::tasks::now;
|
||||||
|
|
||||||
use colored::{ColoredString, Colorize};
|
use colored::{ColoredString, Colorize};
|
||||||
use itertools::Either::{Left, Right};
|
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, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp};
|
use nostr_sdk::{Alphabet, Event, EventId, Kind, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp};
|
||||||
use crate::hashtag::{is_hashtag, Hashtag};
|
|
||||||
use crate::helpers::{format_timestamp_local, some_non_empty};
|
|
||||||
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";
|
pub static MARKER_PARENT: &str = "parent";
|
||||||
pub static MARKER_DEPENDS: &str = "depends";
|
pub static MARKER_DEPENDS: &str = "depends";
|
||||||
|
@ -71,14 +72,17 @@ impl Task {
|
||||||
&self.event.id
|
&self.event.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_owner(&self) -> PublicKey {
|
pub(crate) fn get_participants(&self) -> impl Iterator<Item=PublicKey> + '_ {
|
||||||
self.tags()
|
self.tags()
|
||||||
.find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
|
.filter(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
|
||||||
.and_then(|t| t.content()
|
.filter_map(|t| t.content()
|
||||||
.and_then(|c| PublicKey::from_str(c).inspect_err(|e| warn!("Unparseable pubkey in {:?}", t)).ok()))
|
.and_then(|c| PublicKey::from_str(c).inspect_err(|e| warn!("Unparseable pubkey in {:?}", t)).ok()))
|
||||||
.unwrap_or_else(|| self.event.pubkey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_owner(&self) -> PublicKey {
|
||||||
|
self.get_participants().next()
|
||||||
|
.unwrap_or_else(|| self.event.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> {
|
pub(crate) fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> {
|
||||||
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
|
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
|
||||||
|
@ -148,7 +152,7 @@ impl Task {
|
||||||
pub fn last_state_update(&self) -> Timestamp {
|
pub fn last_state_update(&self) -> Timestamp {
|
||||||
self.state().map(|s| s.time).unwrap_or(self.event.created_at)
|
self.state().map(|s| s.time).unwrap_or(self.event.created_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state_at(&self, time: Timestamp) -> Option<TaskState> {
|
pub fn state_at(&self, time: Timestamp) -> Option<TaskState> {
|
||||||
// TODO do not iterate constructed state objects
|
// TODO do not iterate constructed state objects
|
||||||
let state = self.states().take_while_inclusive(|ts| ts.time > time);
|
let state = self.states().take_while_inclusive(|ts| ts.time > time);
|
||||||
|
@ -198,7 +202,7 @@ impl Task {
|
||||||
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
||||||
self.props.iter()
|
self.props.iter()
|
||||||
.flat_map(|e| e.tags.iter()
|
.flat_map(|e| e.tags.iter()
|
||||||
.filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E)))
|
.filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E)))
|
||||||
.chain(self.tags.iter().flatten())
|
.chain(self.tags.iter().flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,7 +377,7 @@ mod tasks_test {
|
||||||
.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.list_hashtags().count(), 1);
|
assert_eq!(task.list_hashtags().count(), 1);
|
||||||
|
|
||||||
let now = Timestamp::now();
|
let now = Timestamp::now();
|
||||||
task.props.insert(
|
task.props.insert(
|
||||||
EventBuilder::new(State::Done.into(), "")
|
EventBuilder::new(State::Done.into(), "")
|
||||||
|
|
313
src/tasks.rs
313
src/tasks.rs
|
@ -9,14 +9,20 @@ use std::time::Duration;
|
||||||
|
|
||||||
use crate::event_sender::{EventSender, MostrMessage};
|
use crate::event_sender::{EventSender, MostrMessage};
|
||||||
use crate::hashtag::Hashtag;
|
use crate::hashtag::Hashtag;
|
||||||
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::*;
|
||||||
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY};
|
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY};
|
||||||
use crate::tasks::nostr_users::NostrUsers;
|
use crate::tasks::nostr_users::NostrUsers;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
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, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp, Url};
|
use nostr_sdk::{
|
||||||
|
Alphabet, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey,
|
||||||
|
SingleLetterTag, Tag, TagKind, Timestamp, Url,
|
||||||
|
};
|
||||||
use regex::bytes::Regex;
|
use regex::bytes::Regex;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
@ -64,7 +70,7 @@ pub(crate) struct TasksRelay {
|
||||||
/// The task properties currently visible
|
/// The task properties currently visible
|
||||||
properties: Vec<String>,
|
properties: Vec<String>,
|
||||||
/// The task properties sorted by
|
/// The task properties sorted by
|
||||||
sorting: VecDeque<String>, // TODO track boolean for reversal?
|
sorting: VecDeque<String>, // TODO prefix +/- for asc/desc, no prefix for default
|
||||||
|
|
||||||
/// A filtered view of the current tasks.
|
/// A filtered view of the current tasks.
|
||||||
/// Would like this to be Task references
|
/// Would like this to be Task references
|
||||||
|
@ -257,7 +263,7 @@ impl TasksRelay {
|
||||||
|
|
||||||
/// Dynamic time tracking overview for current task or current user.
|
/// Dynamic time tracking overview for current task or current user.
|
||||||
pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
||||||
self.times_tracked_for(&self.sender.pubkey())
|
self.times_tracked_with(&self.sender.pubkey())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn history_for(
|
pub(crate) fn history_for(
|
||||||
|
@ -270,7 +276,9 @@ impl TasksRelay {
|
||||||
hist.values().filter_map(move |event| {
|
hist.values().filter_map(move |event| {
|
||||||
let new = some_non_empty(&event.tags.iter()
|
let new = some_non_empty(&event.tags.iter()
|
||||||
.filter_map(|t| t.content())
|
.filter_map(|t| t.content())
|
||||||
.map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_path(Some(id))))
|
.map(|str|
|
||||||
|
EventId::from_str(str).ok()
|
||||||
|
.map_or(str.to_string(), |id| self.get_task_path(Some(id))))
|
||||||
.join(" "));
|
.join(" "));
|
||||||
if new != last {
|
if new != last {
|
||||||
// TODO omit intervals <2min - but I think I need threeway variable tracking for that
|
// TODO omit intervals <2min - but I think I need threeway variable tracking for that
|
||||||
|
@ -290,24 +298,27 @@ impl TasksRelay {
|
||||||
pub(crate) fn times_tracked_for(
|
pub(crate) fn times_tracked_for(
|
||||||
&self,
|
&self,
|
||||||
key: &PublicKey,
|
key: &PublicKey,
|
||||||
|
) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
||||||
|
match self.history_for(key) {
|
||||||
|
Some(hist) => (
|
||||||
|
format!(
|
||||||
|
"Time-Tracking History for {}:",
|
||||||
|
self.users.get_displayname(&key)
|
||||||
|
),
|
||||||
|
Box::from(hist),
|
||||||
|
),
|
||||||
|
None => ("Nothing time-tracked yet".to_string(), Box::from(empty())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn times_tracked_with(
|
||||||
|
&self,
|
||||||
|
key: &PublicKey,
|
||||||
) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
||||||
match self.get_position() {
|
match self.get_position() {
|
||||||
None => {
|
None => self.times_tracked_for(key),
|
||||||
match self.history_for(key) {
|
|
||||||
Some(hist) =>
|
|
||||||
(
|
|
||||||
format!("Time-Tracking History for {}:", self.users.get_displayname(&key)),
|
|
||||||
Box::from(hist),
|
|
||||||
),
|
|
||||||
None =>
|
|
||||||
(
|
|
||||||
"Nothing time-tracked yet".to_string(),
|
|
||||||
Box::from(empty()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
// TODO show current recursive with pubkey
|
// TODO show current recursive if there is a pubkey
|
||||||
let ids = [id];
|
let ids = [id];
|
||||||
let mut history =
|
let mut history =
|
||||||
self.history.iter().flat_map(|(key, set)| {
|
self.history.iter().flat_map(|(key, set)| {
|
||||||
|
@ -332,8 +343,7 @@ impl TasksRelay {
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
vec
|
vec
|
||||||
})
|
}).collect_vec();
|
||||||
.collect_vec();
|
|
||||||
// TODO sorting depends on timestamp format - needed to interleave different people
|
// TODO sorting depends on timestamp format - needed to interleave different people
|
||||||
history.sort_unstable();
|
history.sort_unstable();
|
||||||
(
|
(
|
||||||
|
@ -399,18 +409,23 @@ impl TasksRelay {
|
||||||
.and_then(|t| t.parent_id())
|
.and_then(|t| t.parent_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pubkey_str(&self) -> Option<String> {
|
||||||
|
match self.pubkey {
|
||||||
|
None => Some("ALL".to_string()),
|
||||||
|
Some(key) => {
|
||||||
|
if key != self.sender.pubkey() {
|
||||||
|
Some(self.users.get_username(&key))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO test with context elements
|
// TODO test with context elements
|
||||||
/// Visual representation of current context
|
/// Visual representation of current context
|
||||||
pub(crate) fn get_prompt_suffix(&self) -> String {
|
pub(crate) fn get_prompt_suffix(&self) -> String {
|
||||||
let mut prompt = String::with_capacity(128);
|
let mut prompt = String::with_capacity(128);
|
||||||
match self.pubkey {
|
|
||||||
None => { prompt.push_str(" @ALL"); }
|
|
||||||
Some(key) =>
|
|
||||||
if key != self.sender.pubkey() {
|
|
||||||
prompt.push_str(" @");
|
|
||||||
prompt.push_str(&self.users.get_username(&key))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for tag in self.tags.iter() {
|
for tag in self.tags.iter() {
|
||||||
prompt.push_str(&format!(" #{}", tag));
|
prompt.push_str(&format!(" #{}", tag));
|
||||||
}
|
}
|
||||||
|
@ -418,8 +433,8 @@ impl TasksRelay {
|
||||||
prompt.push_str(&format!(" -#{}", tag));
|
prompt.push_str(&format!(" -#{}", tag));
|
||||||
}
|
}
|
||||||
prompt.push_str(&self.state.indicator());
|
prompt.push_str(&self.state.indicator());
|
||||||
self.priority.map(|p|
|
self.priority
|
||||||
prompt.push_str(&format!(" *{:02}", p)));
|
.map(|p| prompt.push_str(&format!(" *{:02}", p)));
|
||||||
prompt
|
prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,7 +461,6 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
fn resolve_tasks_rec<'a>(
|
fn resolve_tasks_rec<'a>(
|
||||||
|
@ -488,8 +502,7 @@ impl TasksRelay {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for tag in event.tags.iter() {
|
for tag in event.tags.iter() {
|
||||||
if let Some(event_tag) = match_event_tag(tag) {
|
if let Some(event_tag) = match_event_tag(tag) {
|
||||||
if event_tag.marker
|
if event_tag.marker.as_ref()
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|m| m.to_string() == MARKER_PROPERTY)
|
.is_none_or(|m| m.to_string() == MARKER_PROPERTY)
|
||||||
{
|
{
|
||||||
self.tasks.get_mut(&event_tag.id).map(|t| {
|
self.tasks.get_mut(&event_tag.id).map(|t| {
|
||||||
|
@ -509,7 +522,8 @@ impl TasksRelay {
|
||||||
|
|
||||||
fn filter(&self, task: &Task) -> bool {
|
fn filter(&self, task: &Task) -> bool {
|
||||||
self.state.matches(task) &&
|
self.state.matches(task) &&
|
||||||
(!task.is_task() || self.pubkey.is_none_or(|p| p == task.get_owner())) &&
|
(!task.is_task() || self.pubkey.is_none_or(|p| p == task.get_owner() ||
|
||||||
|
task.list_hashtags().any(|t| t.matches(&self.users.get_username(&p))))) &&
|
||||||
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
|
||||||
}) &&
|
}) &&
|
||||||
|
@ -521,11 +535,7 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO sparse is deprecated and only left for tests
|
// TODO sparse is deprecated and only left for tests
|
||||||
pub(crate) fn filtered_tasks(
|
pub(crate) fn filtered_tasks(&self, position: Option<EventId>, sparse: bool) -> Vec<&Task> {
|
||||||
&self,
|
|
||||||
position: Option<EventId>,
|
|
||||||
sparse: bool,
|
|
||||||
) -> Vec<&Task> {
|
|
||||||
let roots = self.tasks.children_for(position);
|
let roots = self.tasks.children_for(position);
|
||||||
let mut current =
|
let mut current =
|
||||||
self.resolve_tasks_rec(roots, sparse, self.search_depth + self.view_depth);
|
self.resolve_tasks_rec(roots, sparse, self.search_depth + self.view_depth);
|
||||||
|
@ -704,7 +714,7 @@ impl TasksRelay {
|
||||||
};
|
};
|
||||||
self.sender.submit(
|
self.sender.submit(
|
||||||
EventBuilder::new(Kind::Bookmarks, "mostr pins")
|
EventBuilder::new(Kind::Bookmarks, "mostr pins")
|
||||||
.tags(self.bookmarks.iter().map(|id| Tag::event(*id)))
|
.tags(self.bookmarks.iter().map(|id| Tag::event(*id))),
|
||||||
)?;
|
)?;
|
||||||
Ok(added)
|
Ok(added)
|
||||||
}
|
}
|
||||||
|
@ -816,7 +826,7 @@ 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| !t.matches(tag));
|
self.tags.retain(|t| !t.contains(tag));
|
||||||
if self.tags.len() < len {
|
if self.tags.len() < len {
|
||||||
info!("Removed tag filters containing {tag}");
|
info!("Removed tag filters containing {tag}");
|
||||||
} else {
|
} else {
|
||||||
|
@ -918,7 +928,7 @@ impl TasksRelay {
|
||||||
warn!("New task name needs at least {CHARACTER_THRESHOLD} characters");
|
warn!("New task name needs at least {CHARACTER_THRESHOLD} characters");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(self.make_task_with(arg, self.position_tags_for(position), true))
|
self.make_task_with(arg, self.position_tags_for(position), true)
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
// One match, activate
|
// One match, activate
|
||||||
|
@ -947,7 +957,7 @@ impl TasksRelay {
|
||||||
self.track_at(time, target);
|
self.track_at(time, target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
let pos = self.get_position();
|
let pos = self.get_position();
|
||||||
if target == pos {
|
if target == pos {
|
||||||
|
@ -981,20 +991,26 @@ impl TasksRelay {
|
||||||
// Updates
|
// Updates
|
||||||
|
|
||||||
pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
|
pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
|
||||||
Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)), [
|
Tag::custom(
|
||||||
id.to_string(),
|
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)),
|
||||||
to_string_or_default(self.sender.url.as_ref()),
|
[
|
||||||
marker.to_string(),
|
id.to_string(),
|
||||||
])
|
to_string_or_default(self.sender.url.as_ref()),
|
||||||
|
marker.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn make_event_tag(&self, event: &Event, marker: &str) -> Tag {
|
pub(crate) fn make_event_tag(&self, event: &Event, marker: &str) -> Tag {
|
||||||
Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)), [
|
Tag::custom(
|
||||||
event.id.to_string(),
|
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)),
|
||||||
to_string_or_default(self.sender.url.as_ref()),
|
[
|
||||||
marker.to_string(),
|
event.id.to_string(),
|
||||||
event.pubkey.to_string(),
|
to_string_or_default(self.sender.url.as_ref()),
|
||||||
])
|
marker.to_string(),
|
||||||
|
event.pubkey.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parent_tag(&self) -> Option<Tag> {
|
pub(crate) fn parent_tag(&self) -> Option<Tag> {
|
||||||
|
@ -1026,19 +1042,6 @@ impl TasksRelay {
|
||||||
self.tags.iter().map(Tag::from)
|
self.tags.iter().map(Tag::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a task following the current state
|
|
||||||
///
|
|
||||||
/// Sanitizes input
|
|
||||||
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
|
||||||
self.make_task_with(input, self.position_tags(), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) {
|
|
||||||
let id = self.make_task_with(input, self.position_tags(), false);
|
|
||||||
self.set_state_for(id, "", state);
|
|
||||||
self.move_to(Some(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves up and creates a sibling task dependent on the current one
|
/// Moves up and creates a sibling task dependent on the current one
|
||||||
///
|
///
|
||||||
/// Returns true if successful, false if there is no current task
|
/// Returns true if successful, false if there is no current task
|
||||||
|
@ -1058,29 +1061,75 @@ impl TasksRelay {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a task including current tag filters
|
/// Creates a task including current tag filters if content is not empty.
|
||||||
|
///
|
||||||
|
/// @param set_state: whether to set context state - use false if setting state manually
|
||||||
///
|
///
|
||||||
/// Sanitizes input
|
/// Sanitizes input
|
||||||
pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId {
|
pub(crate) fn make_task_with(
|
||||||
let (input, input_tags) = extract_tags(input.trim(), &self.users);
|
&mut self,
|
||||||
let prio =
|
input: &str,
|
||||||
if input_tags.iter().any(|t| t.kind().to_string() == PRIO) { None } else { self.priority.map(|p| to_prio_tag(p)) };
|
tags: impl IntoIterator<Item=Tag>,
|
||||||
info!("Created task \"{input}\" with tags [{}]", join_tags(&input_tags));
|
set_state: bool,
|
||||||
let id = self.submit(
|
) -> Option<EventId> {
|
||||||
EventBuilder::new(TASK_KIND, &input)
|
let (input, mut input_tags) = extract_tags(input.trim(), &self.users);
|
||||||
.tags(input_tags)
|
if input.is_empty() {
|
||||||
.tags(self.context_hashtags())
|
warn!("Task content must not be empty!");
|
||||||
.tags(tags)
|
return None;
|
||||||
.tags(prio)
|
}
|
||||||
|
info!(
|
||||||
|
"Created task \"{input}\" with tags [{}]",
|
||||||
|
join_tags(&input_tags)
|
||||||
);
|
);
|
||||||
|
input_tags.extend(tags);
|
||||||
|
let id = self.make_task_unchecked(&input, input_tags);
|
||||||
if set_state {
|
if set_state {
|
||||||
self.state
|
self.state
|
||||||
.as_option()
|
.as_option()
|
||||||
.inspect(|s| self.set_state_for_with(id, s));
|
.inspect(|s| self.set_state_for_with(id, s));
|
||||||
}
|
}
|
||||||
id
|
Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_task_unchecked(
|
||||||
|
&mut self,
|
||||||
|
input: &str,
|
||||||
|
tags: Vec<Tag>,
|
||||||
|
) -> EventId {
|
||||||
|
let prio =
|
||||||
|
if tags.iter().any(|t| t.kind().to_string() == PRIO) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.priority.map(|p| to_prio_tag(p))
|
||||||
|
};
|
||||||
|
self.submit(
|
||||||
|
EventBuilder::new(TASK_KIND, input)
|
||||||
|
.tags(self.context_hashtags())
|
||||||
|
.tags(tags)
|
||||||
|
.tags(prio),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Primarily for tests
|
||||||
|
fn make_task_unwrapped(&mut self, input: &str) -> EventId {
|
||||||
|
self.make_task(input).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a task following the current state if input is sufficient.
|
||||||
|
///
|
||||||
|
/// Sanitizes input
|
||||||
|
pub(crate) fn make_task(&mut self, input: &str) -> Option<EventId> {
|
||||||
|
self.make_task_with(input, self.position_tags(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) {
|
||||||
|
if let Some(id) = self.make_task_with(input, self.position_tags(), false) {
|
||||||
|
self.set_state_for(id, "", state);
|
||||||
|
self.move_to(Some(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn get_task_title(&self, id: &EventId) -> String {
|
pub(crate) fn get_task_title(&self, id: &EventId) -> String {
|
||||||
self.get_by_id(id).map_or(id.to_string(), |t| t.get_title())
|
self.get_by_id(id).map_or(id.to_string(), |t| t.get_title())
|
||||||
}
|
}
|
||||||
|
@ -1216,9 +1265,7 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_own_events_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
fn get_own_events_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
||||||
self.get_own_history()
|
self.get_own_history().into_iter().flat_map(|t| t.values())
|
||||||
.into_iter()
|
|
||||||
.flat_map(|t| t.values())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
pub(super) fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
||||||
|
@ -1302,7 +1349,8 @@ impl TasksRelay {
|
||||||
self.get_by_id(&id)
|
self.get_by_id(&id)
|
||||||
.and_then(|task| task.state_at(self.custom_time.unwrap_or_default()))
|
.and_then(|task| task.state_at(self.custom_time.unwrap_or_default()))
|
||||||
.map(|ts| format!(" from {}", ts))
|
.map(|ts| format!(" from {}", ts))
|
||||||
.unwrap_or_default());
|
.unwrap_or_default()
|
||||||
|
);
|
||||||
self.submit(prop)
|
self.submit(prop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1330,7 +1378,8 @@ impl TasksRelay {
|
||||||
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.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))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
|
@ -1373,9 +1422,7 @@ impl Display for TasksRelay {
|
||||||
let state = t.state_or_default();
|
let state = t.state_or_default();
|
||||||
let now = &now();
|
let now = &now();
|
||||||
let mut tracking_stamp: Option<Timestamp> = None;
|
let mut tracking_stamp: Option<Timestamp> = None;
|
||||||
for elem in
|
for elem in timestamps(self.get_own_events_history(), &[t.event.id]).map(|(e, _)| e) {
|
||||||
timestamps(self.get_own_events_history(), &[t.event.id])
|
|
||||||
.map(|(e, _)| e) {
|
|
||||||
if tracking_stamp.is_some() && elem > now {
|
if tracking_stamp.is_some() && elem > now {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1429,11 +1476,10 @@ impl Display for TasksRelay {
|
||||||
let count = visible.len();
|
let count = visible.len();
|
||||||
let mut total_time = 0;
|
let mut total_time = 0;
|
||||||
for task in visible {
|
for task in visible {
|
||||||
writeln!(
|
writeln!(lock, "{}",
|
||||||
lock,
|
self.properties.iter()
|
||||||
"{}", self.properties.iter()
|
.map(|p| self.get_property(task, p.as_str()))
|
||||||
.map(|p| self.get_property(task, p.as_str()))
|
.join(" \t")
|
||||||
.join(" \t")
|
|
||||||
)?;
|
)?;
|
||||||
total_time += self.total_time_tracked(task.event.id) // TODO include parent if it matches
|
total_time += self.total_time_tracked(task.event.id) // TODO include parent if it matches
|
||||||
}
|
}
|
||||||
|
@ -1491,11 +1537,12 @@ where
|
||||||
fn display_time(format: &str, secs: u64) -> String {
|
fn display_time(format: &str, secs: u64) -> String {
|
||||||
Some(secs / 60)
|
Some(secs / 60)
|
||||||
.filter(|t| t > &0)
|
.filter(|t| t > &0)
|
||||||
.map_or(String::new(), |mins| format
|
.map_or(String::new(), |mins| {
|
||||||
.replace("MMM", &format!("{:3}", mins))
|
format
|
||||||
.replace("HH", &format!("{:02}", mins.div(60)))
|
.replace("MMM", &format!("{:3}", mins))
|
||||||
.replace("MM", &format!("{:02}", mins.rem(60))),
|
.replace("HH", &format!("{:02}", mins.div(60)))
|
||||||
)
|
.replace("MM", &format!("{:02}", mins.rem(60)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Joins the tasks of this upwards iterator.
|
/// Joins the tasks of this upwards iterator.
|
||||||
|
@ -1513,7 +1560,8 @@ pub(crate) fn join_tasks<'a>(
|
||||||
.take_if(|_| include_last_id)
|
.take_if(|_| include_last_id)
|
||||||
.and_then(|t| t.parent_id())
|
.and_then(|t| t.parent_id())
|
||||||
.map(|id| id.to_string())
|
.map(|id| id.to_string())
|
||||||
.into_iter())
|
.into_iter(),
|
||||||
|
)
|
||||||
.fold(None, |acc, val| {
|
.fold(None, |acc, val| {
|
||||||
Some(acc.map_or_else(
|
Some(acc.map_or_else(
|
||||||
|| val.clone(),
|
|| val.clone(),
|
||||||
|
@ -1785,7 +1833,8 @@ mod tasks_test {
|
||||||
($left:expr, $right:expr $(,)?) => {
|
($left:expr, $right:expr $(,)?) => {
|
||||||
let tasks = $left.visible_tasks();
|
let tasks = $left.visible_tasks();
|
||||||
assert_tasks!($left, tasks, $right,
|
assert_tasks!($left, tasks, $right,
|
||||||
"\nQuick Access: {:?}", $left.quick_access_raw().map(|id| $left.get_relative_path(*id)).collect_vec());
|
"\nQuick Access: {:?}",
|
||||||
|
$left.quick_access_raw().map(|id| $left.get_relative_path(*id)).collect_vec());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1817,17 +1866,23 @@ mod tasks_test {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
|
|
||||||
tasks.custom_time = Some(Timestamp::zero());
|
tasks.custom_time = Some(Timestamp::zero());
|
||||||
let parent = tasks.make_task("parent #tag1");
|
let parent = tasks.make_task_unwrapped("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_unwrapped("sub #oi # tag2");
|
||||||
assert_eq!(tasks.all_hashtags(), ["oi", "tag1", "tag2"].into_iter().map(Hashtag::from).collect());
|
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");
|
||||||
let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect();
|
let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect();
|
||||||
assert_eq!(tasks.all_hashtags(), all_tags);
|
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().list_hashtags().collect_vec(), ["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from));
|
assert_eq!(
|
||||||
|
tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(),
|
||||||
|
["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from)
|
||||||
|
);
|
||||||
assert_eq!(tasks.all_hashtags(), all_tags);
|
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||||
|
|
||||||
tasks.custom_time = Some(now());
|
tasks.custom_time = Some(now());
|
||||||
|
@ -1848,7 +1903,7 @@ mod tasks_test {
|
||||||
|
|
||||||
tasks.set_priority(Some(HIGH_PRIO));
|
tasks.set_priority(Some(HIGH_PRIO));
|
||||||
assert_eq!(tasks.get_prompt_suffix(), " #dp *85");
|
assert_eq!(tasks.get_prompt_suffix(), " #dp *85");
|
||||||
let id_hp = tasks.make_task("high prio tagged # tag");
|
let id_hp = tasks.make_task_unwrapped("high prio tagged # tag");
|
||||||
let hp = tasks.get_by_id(&id_hp).unwrap();
|
let hp = tasks.get_by_id(&id_hp).unwrap();
|
||||||
assert_eq!(hp.priority(), Some(HIGH_PRIO));
|
assert_eq!(hp.priority(), Some(HIGH_PRIO));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -1871,11 +1926,11 @@ mod tasks_test {
|
||||||
let anid = task2.event.id;
|
let anid = task2.event.id;
|
||||||
|
|
||||||
tasks.custom_time = Some(Timestamp::now() + 1);
|
tasks.custom_time = Some(Timestamp::now() + 1);
|
||||||
let s1 = tasks.make_task("sub1");
|
let s1 = tasks.make_task_unwrapped("sub1");
|
||||||
tasks.custom_time = Some(Timestamp::now() + 2);
|
tasks.custom_time = Some(Timestamp::now() + 2);
|
||||||
tasks.set_priority(Some(QUICK_PRIO + 1));
|
tasks.set_priority(Some(QUICK_PRIO + 1));
|
||||||
let s2 = tasks.make_task("sub2");
|
let s2 = tasks.make_task_unwrapped("sub2");
|
||||||
let s3 = tasks.make_task("sub3");
|
let s3 = tasks.make_task_unwrapped("sub3");
|
||||||
tasks.set_priority(Some(QUICK_PRIO));
|
tasks.set_priority(Some(QUICK_PRIO));
|
||||||
|
|
||||||
assert_tasks_visible!(tasks, [s1, s2, s3]);
|
assert_tasks_visible!(tasks, [s1, s2, s3]);
|
||||||
|
@ -1888,7 +1943,7 @@ mod tasks_test {
|
||||||
assert_tasks_visible!(tasks, [s1, s2, s3, id_hp]);
|
assert_tasks_visible!(tasks, [s1, s2, s3, id_hp]);
|
||||||
|
|
||||||
tasks.set_priority(None);
|
tasks.set_priority(None);
|
||||||
let s4 = tasks.make_task_with("sub4", [tasks.make_event_tag_from_id(anid, MARKER_PARENT)], true);
|
let s4 = tasks.make_task_with("sub4", [tasks.make_event_tag_from_id(anid, MARKER_PARENT)], true).unwrap();
|
||||||
assert_eq!(tasks.get_parent(Some(&s4)), Some(&anid));
|
assert_eq!(tasks.get_parent(Some(&s4)), Some(&anid));
|
||||||
assert_tasks_view!(tasks, [anid, id_hp]);
|
assert_tasks_view!(tasks, [anid, id_hp]);
|
||||||
// s2-4 are newest while s2,s3,hp are highest prio
|
// s2-4 are newest while s2,s3,hp are highest prio
|
||||||
|
@ -1900,10 +1955,10 @@ mod tasks_test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sibling_dependency() {
|
fn test_sibling_dependency() {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
let parent = tasks.make_task("parent");
|
let parent = tasks.make_task_unwrapped("parent");
|
||||||
let sub = tasks.submit(
|
let sub = tasks.submit(
|
||||||
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_tasks_view!(tasks, [parent]);
|
assert_tasks_view!(tasks, [parent]);
|
||||||
tasks.track_at(Timestamp::now(), Some(sub));
|
tasks.track_at(Timestamp::now(), Some(sub));
|
||||||
|
@ -1919,11 +1974,11 @@ mod tasks_test {
|
||||||
fn test_bookmarks() {
|
fn test_bookmarks() {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
let zero = EventId::all_zeros();
|
let zero = EventId::all_zeros();
|
||||||
let test = tasks.make_task("test # tag");
|
let test = tasks.make_task_unwrapped("test # tag");
|
||||||
let parent = tasks.make_task("parent");
|
let parent = tasks.make_task_unwrapped("parent");
|
||||||
assert_eq!(tasks.viewed_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_unwrapped("pin");
|
||||||
|
|
||||||
tasks.search_depth = 1;
|
tasks.search_depth = 1;
|
||||||
assert_eq!(tasks.filtered_tasks(None, true).len(), 2);
|
assert_eq!(tasks.filtered_tasks(None, true).len(), 2);
|
||||||
|
@ -1964,7 +2019,7 @@ mod tasks_test {
|
||||||
assert_tasks_visible!(tasks, [pin, test]);
|
assert_tasks_visible!(tasks, [pin, test]);
|
||||||
tasks.set_view_depth(0);
|
tasks.set_view_depth(0);
|
||||||
tasks.custom_time = Some(now());
|
tasks.custom_time = Some(now());
|
||||||
let mut new = (0..3).map(|t| tasks.make_task(t.to_string().as_str())).collect_vec();
|
let mut new = (0..3).map(|t| tasks.make_task_unwrapped(t.to_string().as_str())).collect_vec();
|
||||||
// Show the newest tasks in quick access and remove old pin
|
// Show the newest tasks in quick access and remove old pin
|
||||||
new.extend([test, parent]);
|
new.extend([test, parent]);
|
||||||
assert_tasks_visible!(tasks, new);
|
assert_tasks_visible!(tasks, new);
|
||||||
|
@ -1980,7 +2035,7 @@ mod tasks_test {
|
||||||
.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.viewed_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_unwrapped("sub");
|
||||||
assert_tasks_view!(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();
|
||||||
|
@ -2000,7 +2055,7 @@ mod tasks_test {
|
||||||
|
|
||||||
tasks.move_to(zero);
|
tasks.move_to(zero);
|
||||||
assert_eq!(tasks.viewed_tasks().len(), 1);
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
||||||
let sub = tasks.make_task("test");
|
let sub = tasks.make_task_unwrapped("test");
|
||||||
assert_eq!(tasks.len(), 2);
|
assert_eq!(tasks.len(), 2);
|
||||||
assert_eq!(tasks.viewed_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());
|
||||||
|
@ -2049,7 +2104,7 @@ mod tasks_test {
|
||||||
assert_eq!(tasks.get_own_events_history().count(), 3);
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
||||||
assert!(tasks.time_tracked(zero) > 999);
|
assert!(tasks.time_tracked(zero) > 999);
|
||||||
|
|
||||||
let some = tasks.make_task("some");
|
let some = tasks.make_task_unwrapped("some");
|
||||||
tasks.track_at(Timestamp::from(22 + 1), Some(some));
|
tasks.track_at(Timestamp::from(22 + 1), Some(some));
|
||||||
assert_eq!(tasks.get_own_events_history().count(), 4);
|
assert_eq!(tasks.get_own_events_history().count(), 4);
|
||||||
assert_eq!(tasks.time_tracked(zero), 12);
|
assert_eq!(tasks.time_tracked(zero), 12);
|
||||||
|
@ -2094,17 +2149,17 @@ mod tasks_test {
|
||||||
assert_position!(tasks, t1);
|
assert_position!(tasks, t1);
|
||||||
tasks.search_depth = 2;
|
tasks.search_depth = 2;
|
||||||
assert_eq!(tasks.viewed_tasks().len(), 0);
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||||
let t11 = tasks.make_task("t11 # tag");
|
let t11 = tasks.make_task_unwrapped("t11 # tag");
|
||||||
assert_eq!(tasks.viewed_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_unwrapped("t12");
|
||||||
assert_eq!(tasks.viewed_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.viewed_tasks().len(), 0);
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
||||||
let t111 = tasks.make_task("t111");
|
let t111 = tasks.make_task_unwrapped("t111");
|
||||||
assert_tasks_view!(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");
|
||||||
|
@ -2161,13 +2216,21 @@ mod tasks_test {
|
||||||
fn test_empty_task_title_fallback_to_id() {
|
fn test_empty_task_title_fallback_to_id() {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
|
|
||||||
let empty = tasks.make_task("");
|
let empty = tasks.make_task_unchecked("", vec![]);
|
||||||
let empty_task = tasks.get_by_id(&empty).unwrap();
|
let empty_task = tasks.get_by_id(&empty).unwrap();
|
||||||
let empty_id = empty_task.event.id.to_string();
|
let empty_id = empty_task.event.id.to_string();
|
||||||
assert_eq!(empty_task.get_title(), empty_id);
|
assert_eq!(empty_task.get_title(), empty_id);
|
||||||
assert_eq!(tasks.get_task_path(Some(empty)), empty_id);
|
assert_eq!(tasks.get_task_path(Some(empty)), empty_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_short_task() {
|
||||||
|
let mut tasks = stub_tasks();
|
||||||
|
let str = " # one";
|
||||||
|
assert_eq!(extract_tags(str, &tasks.users), ("".to_string(), vec![to_hashtag_tag("one")]));
|
||||||
|
assert_eq!(tasks.make_task(str), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unknown_task() {
|
fn test_unknown_task() {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
|
@ -2175,7 +2238,7 @@ mod tasks_test {
|
||||||
let zero = EventId::all_zeros();
|
let zero = EventId::all_zeros();
|
||||||
assert_eq!(tasks.get_task_path(Some(zero)), zero.to_string());
|
assert_eq!(tasks.get_task_path(Some(zero)), zero.to_string());
|
||||||
tasks.move_to(Some(zero));
|
tasks.move_to(Some(zero));
|
||||||
let dangling = tasks.make_task("test");
|
let dangling = tasks.make_task_unwrapped("test");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tasks.get_task_path(Some(dangling)),
|
tasks.get_task_path(Some(dangling)),
|
||||||
"0000000000000000000000000000000000000000000000000000000000000000>test"
|
"0000000000000000000000000000000000000000000000000000000000000000>test"
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
use nostr_sdk::{Keys, Metadata, PublicKey, Tag};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use nostr_sdk::{Keys, Metadata, PublicKey, Tag};
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct NostrUsers {
|
pub struct NostrUsers {
|
||||||
users: HashMap<PublicKey, Metadata>
|
users: HashMap<PublicKey, Metadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrUsers {
|
impl NostrUsers {
|
||||||
|
@ -18,7 +18,7 @@ impl NostrUsers {
|
||||||
let lowered = term.trim().to_ascii_lowercase();
|
let lowered = term.trim().to_ascii_lowercase();
|
||||||
let term = lowered.as_str();
|
let term = lowered.as_str();
|
||||||
if term.is_empty() {
|
if term.is_empty() {
|
||||||
return None
|
return None;
|
||||||
}
|
}
|
||||||
if let Ok(key) = PublicKey::from_str(term) {
|
if let Ok(key) = PublicKey::from_str(term) {
|
||||||
return self.users.get_key_value(&key);
|
return self.users.get_key_value(&key);
|
||||||
|
|
Loading…
Add table
Reference in a new issue