Compare commits
No commits in common. "e95a14ae89f13604482f8927f9b72ddc8c4fe517" and "b87970d4e2309f4c215caa2ede6d9f5542b8f1a5" have entirely different histories.
e95a14ae89
...
b87970d4e2
10 changed files with 360 additions and 711 deletions
65
Cargo.lock
generated
65
Cargo.lock
generated
|
@ -340,7 +340,7 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
|
||||
dependencies = [
|
||||
"bitcoin-internals",
|
||||
"bitcoin-internals 0.3.0",
|
||||
"bitcoin_hashes 0.14.0",
|
||||
]
|
||||
|
||||
|
@ -393,16 +393,22 @@ checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb"
|
|||
dependencies = [
|
||||
"base58ck",
|
||||
"bech32",
|
||||
"bitcoin-internals",
|
||||
"bitcoin-internals 0.3.0",
|
||||
"bitcoin-io",
|
||||
"bitcoin-units",
|
||||
"bitcoin_hashes 0.14.0",
|
||||
"hex-conservative",
|
||||
"hex-conservative 0.2.1",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-internals"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-internals"
|
||||
version = "0.3.0"
|
||||
|
@ -424,7 +430,7 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
|
||||
dependencies = [
|
||||
"bitcoin-internals",
|
||||
"bitcoin-internals 0.3.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -434,6 +440,16 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
||||
dependencies = [
|
||||
"bitcoin-internals 0.2.0",
|
||||
"hex-conservative 0.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.14.0"
|
||||
|
@ -441,7 +457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16"
|
||||
dependencies = [
|
||||
"bitcoin-io",
|
||||
"hex-conservative",
|
||||
"hex-conservative 0.2.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -1114,6 +1130,12 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.2.1"
|
||||
|
@ -1466,7 +1488,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mostr"
|
||||
version = "0.7.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-english",
|
||||
|
@ -1484,7 +1506,6 @@ dependencies = [
|
|||
"regex",
|
||||
"rustyline",
|
||||
"tokio",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1941,15 +1962,6 @@ version = "0.1.57"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.3.5"
|
||||
|
@ -1957,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"redox_syscall 0.1.57",
|
||||
"redox_syscall",
|
||||
"rust-argon2",
|
||||
]
|
||||
|
||||
|
@ -2138,7 +2150,7 @@ version = "0.29.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.14.0",
|
||||
"bitcoin_hashes 0.13.0",
|
||||
"rand",
|
||||
"secp256k1-sys",
|
||||
"serde",
|
||||
|
@ -2672,12 +2684,6 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
|
@ -2764,17 +2770,6 @@ dependencies = [
|
|||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
|
||||
dependencies = [
|
||||
"redox_syscall 0.5.7",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
|
|||
readme = "README.md"
|
||||
license = "GPL 3.0"
|
||||
authors = ["melonion"]
|
||||
version = "0.7.0"
|
||||
version = "0.6.1"
|
||||
rust-version = "1.82"
|
||||
edition = "2021"
|
||||
default-run = "mostr"
|
||||
|
@ -25,7 +25,6 @@ rustyline = { git = "https://github.com/xeruf/rustyline", rev = "465b14d" }
|
|||
# OS-Specific Abstractions
|
||||
keyring = { version = "3", features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] }
|
||||
directories = "5.0"
|
||||
whoami = "1.5"
|
||||
# Application Utils
|
||||
itertools = "0.12"
|
||||
chrono = "0.4"
|
||||
|
|
|
@ -65,7 +65,7 @@ impl EventSender {
|
|||
})?)
|
||||
}
|
||||
/// Sends all pending events
|
||||
pub(crate) fn force_flush(&self) {
|
||||
fn force_flush(&self) {
|
||||
debug!("Flushing {} events from queue", self.queue.borrow().len());
|
||||
let values = self.clear();
|
||||
self.url.as_ref().map(|url| {
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
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());
|
||||
}
|
|
@ -36,7 +36,6 @@ impl<T: TimeZone> ToTimestamp for DateTime<T> {
|
|||
|
||||
/// Parses the hour from a plain number in the String,
|
||||
/// with max of max_future hours into the future.
|
||||
/// TODO parse HHMM as well
|
||||
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
||||
str.parse::<u32>().ok().and_then(|hour| {
|
||||
let now = Local::now();
|
||||
|
|
53
src/kinds.rs
53
src/kinds.rs
|
@ -1,8 +1,7 @@
|
|||
use crate::task::MARKER_PARENT;
|
||||
use crate::tasks::nostr_users::NostrUsers;
|
||||
use crate::tasks::HIGH_PRIO;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::{EventBuilder, EventId, Kind, PublicKey, Tag, TagKind, TagStandard};
|
||||
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub const TASK_KIND: Kind = Kind::GitIssue;
|
||||
|
@ -46,7 +45,6 @@ Task:
|
|||
- `time` - time tracked on this task by you
|
||||
Utilities:
|
||||
- `state` - indicator of current progress
|
||||
- `owner` - author or task assignee
|
||||
- `rtime` - time tracked on this tasks and its subtree by everyone
|
||||
- `progress` - recursive subtask completion in percent
|
||||
- `subtasks` - how many direct subtasks are complete
|
||||
|
@ -93,49 +91,44 @@ 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_tag)
|
||||
.map(to_hashtag)
|
||||
}
|
||||
|
||||
/// Extracts everything after a " # " as a list of tags
|
||||
/// as well as various embedded tags.
|
||||
///
|
||||
/// Expects sanitized input.
|
||||
pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec<Tag>) {
|
||||
pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
||||
let words = input.split_ascii_whitespace();
|
||||
let mut tags = Vec::with_capacity(4);
|
||||
let mut prio = None;
|
||||
let result = words.filter(|s| {
|
||||
if s.starts_with('@') {
|
||||
if let Ok(key) = PublicKey::parse(&s[1..]) {
|
||||
tags.push(Tag::public_key(key));
|
||||
return false;
|
||||
} else if let Some((key, _)) = users.find_user(&s[1..]) {
|
||||
tags.push(Tag::public_key(*key));
|
||||
return false;
|
||||
}
|
||||
} else if s.starts_with('*') {
|
||||
if s.starts_with('*') {
|
||||
if s.len() == 1 {
|
||||
tags.push(to_prio_tag(HIGH_PRIO));
|
||||
prio = Some(HIGH_PRIO);
|
||||
return false;
|
||||
}
|
||||
if let Ok(num) = s[1..].parse::<Prio>() {
|
||||
tags.push(to_prio_tag(num * (if s.len() > 2 { 1 } else { 10 })));
|
||||
return false
|
||||
}
|
||||
return match s[1..].parse::<Prio>() {
|
||||
Ok(num) => {
|
||||
prio = Some(num * (if s.len() > 2 { 1 } else { 10 }));
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
true
|
||||
}).collect_vec();
|
||||
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_tag(&s)))
|
||||
.chain(tags)
|
||||
.chain(split.flatten().map(|s| to_hashtag(&s)))
|
||||
.chain(prio.map(|p| to_prio_tag(p)))
|
||||
.collect_vec();
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
(main, tags)
|
||||
}
|
||||
|
||||
pub fn to_hashtag_tag(tag: &str) -> Tag {
|
||||
pub fn to_hashtag(tag: &str) -> Tag {
|
||||
TagStandard::Hashtag(tag.to_string()).into()
|
||||
}
|
||||
|
||||
|
@ -161,17 +154,21 @@ 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()])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_tags() {
|
||||
assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it", &Default::default()),
|
||||
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_tag))
|
||||
.collect()));
|
||||
assert_eq!(extract_tags("So tagless @hewo #", &Default::default()),
|
||||
("So tagless @hewo".to_string(), vec![]));
|
||||
.chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag)).collect()));
|
||||
assert_eq!(extract_tags("So tagless #"),
|
||||
("So tagless".to_string(), vec![]));
|
||||
}
|
100
src/main.rs
100
src/main.rs
|
@ -12,7 +12,7 @@ use crate::event_sender::MostrMessage;
|
|||
use crate::helpers::*;
|
||||
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::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
|
||||
use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
|
||||
use chrono::Local;
|
||||
use colored::Colorize;
|
||||
use directories::ProjectDirs;
|
||||
|
@ -21,6 +21,7 @@ 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;
|
||||
|
@ -28,14 +29,12 @@ 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";
|
||||
|
@ -200,20 +199,21 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
let metadata = Metadata::new()
|
||||
.name(whoami::username())
|
||||
.display_name(whoami::realname());
|
||||
let metadata_clone = metadata.clone();
|
||||
let metadata = var("USER").ok().map(
|
||||
|user| Metadata::new().name(user));
|
||||
let moved_metadata = metadata.clone();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<MostrMessage>(64);
|
||||
let tasks_for_url = |url: Option<Url>| TasksRelay::from(url, &tx, &keys, Some(metadata.clone()));
|
||||
let tasks_for_url = |url: Option<Url>| TasksRelay::from(url, &tx, &keys, metadata.clone());
|
||||
let mut relays: HashMap<Option<Url>, TasksRelay> =
|
||||
client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect();
|
||||
|
||||
let sender = tokio::spawn(async move {
|
||||
let mut queue: Option<(Url, Vec<Event>)> = None;
|
||||
|
||||
or_warn!(client.set_metadata(&metadata_clone).await, "Unable to set metadata");
|
||||
if let Some(meta) = moved_metadata.as_ref() {
|
||||
or_warn!(client.set_metadata(meta).await, "Unable to set metadata");
|
||||
}
|
||||
|
||||
'repl: loop {
|
||||
let result_received = timeout(Duration::from_secs(INACTVITY_DELAY), rx.recv()).await;
|
||||
|
@ -385,7 +385,7 @@ async fn main() -> Result<()> {
|
|||
match arg {
|
||||
None => {
|
||||
if let Some(task) = tasks.get_current_task() {
|
||||
println!("Change History for {}:", task.get_id());
|
||||
println!("Change History:");
|
||||
for e in once(&task.event).chain(task.props.iter().rev()) {
|
||||
println!("{} {} [{}]",
|
||||
format_timestamp_full(&e.created_at),
|
||||
|
@ -417,8 +417,8 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
Some(arg) => {
|
||||
if arg.trim().len() < 2 {
|
||||
warn!("Needs at least 2 characters!");
|
||||
if arg.len() < CHARACTER_THRESHOLD {
|
||||
warn!("Note needs at least {CHARACTER_THRESHOLD} characters!");
|
||||
continue 'repl;
|
||||
}
|
||||
tasks.make_note(arg);
|
||||
|
@ -438,36 +438,14 @@ async fn main() -> Result<()> {
|
|||
Some('&') => {
|
||||
match arg {
|
||||
None => tasks.undo(),
|
||||
Some(text) => {
|
||||
if text == "&" {
|
||||
println!(
|
||||
"My History:\n{}",
|
||||
tasks.history_before_now()
|
||||
.take(9)
|
||||
.enumerate()
|
||||
.dropping(1)
|
||||
.map(|(c, e)| {
|
||||
format!("({}) {}",
|
||||
c,
|
||||
match referenced_event(e) {
|
||||
Some(target) => tasks.get_task_path(Some(target)),
|
||||
None => "---".to_string(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
);
|
||||
continue 'repl;
|
||||
Some(text) => match text.parse::<u8>() {
|
||||
Ok(int) => {
|
||||
tasks.move_back_by(int as usize);
|
||||
}
|
||||
match text.parse::<u8>() {
|
||||
Ok(int) => {
|
||||
tasks.move_back_by(int as usize);
|
||||
}
|
||||
_ => {
|
||||
if !tasks.move_back_to(text) {
|
||||
warn!("Did not find a match in history for \"{text}\"");
|
||||
continue 'repl;
|
||||
}
|
||||
_ => {
|
||||
if !tasks.move_back_to(text) {
|
||||
warn!("Did not find a match in history for \"{text}\"");
|
||||
continue 'repl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -475,33 +453,37 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Some('@') => {
|
||||
match arg {
|
||||
let success = match arg {
|
||||
None => {
|
||||
let today = Timestamp::now() - 80_000;
|
||||
info!("Filtering for tasks from the last 22 hours");
|
||||
if !tasks.set_filter_since(today) {
|
||||
continue 'repl;
|
||||
}
|
||||
tasks.set_filter_from(today)
|
||||
}
|
||||
Some(arg) => {
|
||||
if arg == "@" {
|
||||
tasks.reset_key_filter()
|
||||
} else if let Some((key, name)) = tasks.find_user(arg) {
|
||||
info!("Showing {}'s tasks", name);
|
||||
tasks.set_key_filter(key)
|
||||
info!("Filtering for own tasks");
|
||||
tasks.set_filter_author(keys.public_key())
|
||||
} else if let Ok(key) = PublicKey::from_str(arg) {
|
||||
let author = tasks.get_username(&key);
|
||||
info!("Filtering for tasks by {author}");
|
||||
tasks.set_filter_author(key)
|
||||
} else if let Some((key, meta)) = tasks.find_user(arg) {
|
||||
info!("Filtering for tasks by {}", meta.display_name.as_ref().unwrap_or(meta.name.as_ref().unwrap_or(&key.to_string())));
|
||||
tasks.set_filter_author(key.clone())
|
||||
} else {
|
||||
if parse_hour(arg, 1)
|
||||
parse_hour(arg, 1)
|
||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
||||
.map(|time| {
|
||||
info!("Filtering for tasks from {}", format_datetime_relative(time));
|
||||
tasks.set_filter_since(time.to_timestamp())
|
||||
tasks.set_filter_from(time.to_timestamp())
|
||||
})
|
||||
.is_none_or(|b| !b) {
|
||||
continue 'repl;
|
||||
}
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
if !success {
|
||||
continue 'repl;
|
||||
}
|
||||
}
|
||||
|
||||
Some('*') => {
|
||||
|
@ -595,14 +577,14 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Some('#') => {
|
||||
if !tasks.update_tags(arg_default.split_whitespace().map(Hashtag::from)) {
|
||||
if !tasks.update_tags(arg_default.split_whitespace().map(|s| Hashtag(s.to_string()).into())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Some('+') =>
|
||||
match arg {
|
||||
Some(arg) => tasks.add_tag(arg),
|
||||
Some(arg) => tasks.add_tag(arg.to_string()),
|
||||
None => {
|
||||
tasks.print_hashtags();
|
||||
if tasks.has_tag_filter() {
|
||||
|
@ -640,7 +622,7 @@ async fn main() -> Result<()> {
|
|||
label)
|
||||
},
|
||||
vec.iter().rev().join("\n"));
|
||||
} else if let Some((key, _)) = tasks.find_user(arg) {
|
||||
} else if let Ok(key) = PublicKey::parse(arg) { // TODO also match name
|
||||
let (label, mut times) = tasks.times_tracked_for(&key);
|
||||
println!("{}\n{}", label.italic(),
|
||||
times.join("\n"));
|
||||
|
@ -727,8 +709,8 @@ async fn main() -> Result<()> {
|
|||
let filtered =
|
||||
tasks.get_filtered(pos, |t| {
|
||||
transform(&t.event.content).contains(&remaining) ||
|
||||
t.list_hashtags().any(
|
||||
|tag| tag.matches(&remaining))
|
||||
t.get_hashtags().any(
|
||||
|tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
|
||||
});
|
||||
if filtered.len() == 1 {
|
||||
tasks.move_to(filtered.into_iter().next());
|
||||
|
|
80
src/task.rs
80
src/task.rs
|
@ -4,17 +4,16 @@ use std::collections::BTreeSet;
|
|||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::once;
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use colored::{ColoredString, Colorize};
|
||||
use itertools::Either::{Left, Right};
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::{Alphabet, Event, EventId, Kind, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp};
|
||||
use crate::hashtag::{is_hashtag, Hashtag};
|
||||
use nostr_sdk::{Alphabet, Event, EventId, Kind, Tag, Timestamp};
|
||||
|
||||
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::kinds::{is_hashtag, match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
||||
use crate::tasks::now;
|
||||
|
||||
pub static MARKER_PARENT: &str = "parent";
|
||||
|
@ -71,15 +70,6 @@ impl Task {
|
|||
&self.event.id
|
||||
}
|
||||
|
||||
pub(crate) fn get_owner(&self) -> PublicKey {
|
||||
self.tags()
|
||||
.find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
|
||||
.and_then(|t| t.content()
|
||||
.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 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))
|
||||
}
|
||||
|
@ -108,8 +98,7 @@ impl Task {
|
|||
|
||||
/// Description items, ordered newest to oldest
|
||||
pub(crate) fn descriptions(&self) -> impl DoubleEndedIterator<Item=&String> + '_ {
|
||||
self.description_events()
|
||||
.filter_map(|e| Some(&e.content).take_if(|s| !s.trim().is_empty()))
|
||||
self.description_events().map(|e| &e.content)
|
||||
}
|
||||
|
||||
pub(crate) fn is_task_kind(&self) -> bool {
|
||||
|
@ -145,15 +134,16 @@ impl Task {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn last_state_update(&self) -> Timestamp {
|
||||
pub(crate) fn last_state_update(&self) -> Timestamp {
|
||||
self.state().map(|s| s.time).unwrap_or(self.event.created_at)
|
||||
}
|
||||
|
||||
pub fn state_at(&self, time: Timestamp) -> Option<TaskState> {
|
||||
|
||||
pub(crate) fn state(&self) -> Option<TaskState> {
|
||||
let now = now();
|
||||
// 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 > now);
|
||||
state.last().map(|ts| {
|
||||
if ts.time <= time {
|
||||
if ts.time <= now {
|
||||
ts
|
||||
} else {
|
||||
self.default_state()
|
||||
|
@ -161,12 +151,6 @@ impl Task {
|
|||
})
|
||||
}
|
||||
|
||||
/// Returns the current state if this is a task rather than an activity
|
||||
pub fn state(&self) -> Option<TaskState> {
|
||||
let now = now();
|
||||
self.state_at(now)
|
||||
}
|
||||
|
||||
pub(crate) fn pure_state(&self) -> State {
|
||||
self.state().map_or(State::Open, |s| s.state)
|
||||
}
|
||||
|
@ -190,16 +174,15 @@ impl Task {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_hashtags(&self) -> impl Iterator<Item=Hashtag> + '_ {
|
||||
self.tags().filter_map(|t| Hashtag::try_from(t).ok())
|
||||
pub(crate) fn get_hashtags(&self) -> impl Iterator<Item=&Tag> {
|
||||
self.tags().filter(|t| is_hashtag(t))
|
||||
}
|
||||
|
||||
/// Tags of this task that are not event references, newest to oldest
|
||||
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
||||
self.props.iter()
|
||||
.flat_map(|e| e.tags.iter()
|
||||
.filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E)))
|
||||
.chain(self.tags.iter().flatten())
|
||||
self.tags.iter().flatten().chain(
|
||||
self.props.iter().flat_map(|e| e.tags.iter()
|
||||
.filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E)))
|
||||
)
|
||||
}
|
||||
|
||||
fn join_tags<P>(&self, predicate: P) -> String
|
||||
|
@ -239,7 +222,10 @@ 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
|
||||
|
@ -248,7 +234,6 @@ impl Task {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub(crate) struct TaskState {
|
||||
pub(crate) state: State,
|
||||
name: Option<String>,
|
||||
|
@ -284,7 +269,7 @@ impl Display for TaskState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq)]
|
||||
pub(crate) enum State {
|
||||
/// Actionable
|
||||
Open = 1630,
|
||||
|
@ -372,34 +357,21 @@ 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.list_hashtags().count(), 1);
|
||||
|
||||
let now = Timestamp::now();
|
||||
assert_eq!(task.get_hashtags().count(), 1);
|
||||
task.props.insert(
|
||||
EventBuilder::new(State::Done.into(), "")
|
||||
.custom_created_at(now)
|
||||
.sign_with_keys(&keys).unwrap());
|
||||
assert_eq!(task.pure_state(), State::Done);
|
||||
task.props.insert(
|
||||
EventBuilder::new(State::Open.into(), "Ready").tags([Tag::hashtag("tag2")])
|
||||
.custom_created_at(now - 2)
|
||||
EventBuilder::new(State::Open.into(), "").tags([Tag::hashtag("tag2")])
|
||||
.custom_created_at(Timestamp::from(Timestamp::now() - 2))
|
||||
.sign_with_keys(&keys).unwrap());
|
||||
assert_eq!(task.pure_state(), State::Done);
|
||||
assert_eq!(task.list_hashtags().count(), 2);
|
||||
assert_eq!(task.get_hashtags().count(), 2);
|
||||
task.props.insert(
|
||||
EventBuilder::new(State::Closed.into(), "")
|
||||
.custom_created_at(now + 9)
|
||||
.custom_created_at(Timestamp::from(Timestamp::now() + 1))
|
||||
.sign_with_keys(&keys).unwrap());
|
||||
assert_eq!(task.pure_state(), State::Closed);
|
||||
assert_eq!(task.state_at(now), Some(TaskState {
|
||||
state: State::Done,
|
||||
name: None,
|
||||
time: now,
|
||||
}));
|
||||
assert_eq!(task.state_at(now - 1), Some(TaskState {
|
||||
state: State::Open,
|
||||
name: Some("Ready".to_string()),
|
||||
time: now - 2,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
608
src/tasks.rs
608
src/tasks.rs
File diff suppressed because it is too large
Load diff
|
@ -1,63 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use nostr_sdk::{Keys, Metadata, PublicKey, Tag};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NostrUsers {
|
||||
users: HashMap<PublicKey, Metadata>
|
||||
}
|
||||
|
||||
impl NostrUsers {
|
||||
pub(crate) fn find_user_with_displayname(&self, term: &str) -> Option<(PublicKey, String)> {
|
||||
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)> {
|
||||
let lowered = term.trim().to_ascii_lowercase();
|
||||
let term = lowered.as_str();
|
||||
if term.is_empty() {
|
||||
return None
|
||||
}
|
||||
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.to_ascii_lowercase().starts_with(term)) ||
|
||||
v.display_name.as_ref().is_some_and(|n| n.to_ascii_lowercase().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 {
|
||||
self.users.get(pubkey)
|
||||
.and_then(|m| m.name.clone())
|
||||
.unwrap_or_else(|| format!("{:.6}", pubkey.to_string()))
|
||||
}
|
||||
|
||||
pub(super) fn insert(&mut self, pubkey: PublicKey, metadata: Metadata) {
|
||||
self.users.insert(pubkey, metadata);
|
||||
}
|
||||
|
||||
pub(super) fn create(&mut self, pubkey: PublicKey) {
|
||||
if !self.users.contains_key(&pubkey) {
|
||||
self.users.insert(pubkey, Default::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_extract() {
|
||||
let keys = Keys::generate();
|
||||
let mut users = NostrUsers::default();
|
||||
users.insert(keys.public_key, Metadata::new().display_name("Tester Jo"));
|
||||
assert_eq!(crate::kinds::extract_tags("Hello @test", &users),
|
||||
("Hello".to_string(), vec![Tag::public_key(keys.public_key)]));
|
||||
}
|
Loading…
Add table
Reference in a new issue