Compare commits
No commits in common. "5e6b274fe313aff6c29f53482296c672f2673282" and "4b59b273f53cb51af5d1a2264c6eeb801164fecb" have entirely different histories.
5e6b274fe3
...
4b59b273f5
|
@ -166,6 +166,9 @@ Considering to use Calendar: https://github.com/nostr-protocol/nips/blob/master/
|
||||||
- Remove status filter when moving up?
|
- Remove status filter when moving up?
|
||||||
- Task markdown support? - colored
|
- Task markdown support? - colored
|
||||||
- Time tracking: Ability to postpone task and add planned timestamps (calendar entry)
|
- Time tracking: Ability to postpone task and add planned timestamps (calendar entry)
|
||||||
|
- Parse Hashtag tags from task name
|
||||||
|
- Unified Filter object
|
||||||
|
-> include subtasks of matched tasks
|
||||||
- Speedup: Offline caching & Expiry (no need to fetch potential years of history)
|
- Speedup: Offline caching & Expiry (no need to fetch potential years of history)
|
||||||
+ Fetch most recent tasks first
|
+ Fetch most recent tasks first
|
||||||
+ Relay: compress tracked time for old tasks, filter closed tasks
|
+ Relay: compress tracked time for old tasks, filter closed tasks
|
||||||
|
|
39
src/kinds.rs
39
src/kinds.rs
|
@ -1,10 +1,9 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::info;
|
use log::info;
|
||||||
use nostr_sdk::TagStandard::Hashtag;
|
|
||||||
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
|
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
|
||||||
use std::collections::HashSet;
|
use nostr_sdk::TagStandard::Hashtag;
|
||||||
|
|
||||||
use crate::task::{State, MARKER_PARENT};
|
use crate::task::{MARKER_PARENT, State};
|
||||||
|
|
||||||
pub const TASK_KIND: Kind = Kind::GitIssue;
|
pub const TASK_KIND: Kind = Kind::GitIssue;
|
||||||
pub const PROCEDURE_KIND_ID: u16 = 1639;
|
pub const PROCEDURE_KIND_ID: u16 = 1639;
|
||||||
|
@ -83,33 +82,21 @@ pub(crate) fn build_prop(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return Hashtags embedded in the string.
|
/// Expects sanitized input
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts everything after a ": " as a list of tags.
|
|
||||||
///
|
|
||||||
/// Expects sanitized input.
|
|
||||||
pub(crate) fn extract_tags(input: &str) -> (&str, Vec<Tag>) {
|
pub(crate) fn extract_tags(input: &str) -> (&str, Vec<Tag>) {
|
||||||
match input.split_once(": ") {
|
match input.split_once(": ") {
|
||||||
None => (input, extract_hashtags(input).collect_vec()),
|
None => (input, vec![]),
|
||||||
Some((name, tags)) => {
|
Some(s) => {
|
||||||
let tags = extract_hashtags(name)
|
let tags = s
|
||||||
.chain(tags.split_ascii_whitespace().map(to_hashtag))
|
.1
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.map(|t| Hashtag(t.to_string()).into())
|
||||||
.collect();
|
.collect();
|
||||||
(name, tags)
|
(s.0, tags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_hashtag(tag: &str) -> Tag {
|
|
||||||
Hashtag(tag.to_string()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_tag(tag: &Tag) -> String {
|
fn format_tag(tag: &Tag) -> String {
|
||||||
match tag.as_standardized() {
|
match tag.as_standardized() {
|
||||||
Some(TagStandard::Event {
|
Some(TagStandard::Event {
|
||||||
|
@ -136,9 +123,3 @@ pub(crate) fn is_hashtag(tag: &Tag) -> bool {
|
||||||
.is_some_and(|letter| letter.character == Alphabet::T)
|
.is_some_and(|letter| letter.character == Alphabet::T)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tags() {
|
|
||||||
assert_eq!(extract_tags("Hello from #mars with #greetings: yeah done-it"),
|
|
||||||
("Hello from #mars with #greetings", ["mars", "greetings", "yeah", "done-it"].into_iter().map(to_hashtag).collect()))
|
|
||||||
}
|
|
|
@ -361,8 +361,6 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
info!("Received {count} Updates");
|
info!("Received {count} Updates");
|
||||||
} else {
|
|
||||||
relays.values_mut().for_each(|tasks| tasks.process_overflow());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut iter = input.chars();
|
let mut iter = input.chars();
|
||||||
|
|
|
@ -78,14 +78,11 @@ impl Task {
|
||||||
|
|
||||||
/// Trimmed event content or stringified id
|
/// Trimmed event content or stringified id
|
||||||
pub(crate) fn get_title(&self) -> String {
|
pub(crate) fn get_title(&self) -> String {
|
||||||
some_non_empty(self.event.content.trim())
|
Some(self.event.content.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
.unwrap_or_else(|| self.get_id().to_string())
|
.unwrap_or_else(|| self.get_id().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_filter_title(&self) -> String {
|
|
||||||
self.event.content.trim().trim_start_matches('#').to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn description_events(&self) -> impl Iterator<Item=&Event> + '_ {
|
pub(crate) fn description_events(&self) -> impl Iterator<Item=&Event> + '_ {
|
||||||
self.props.iter().filter(|event| event.kind == Kind::TextNote)
|
self.props.iter().filter(|event| event.kind == Kind::TextNote)
|
||||||
}
|
}
|
||||||
|
|
62
src/tasks.rs
62
src/tasks.rs
|
@ -6,18 +6,18 @@ use std::ops::{Div, Rem};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
|
|
||||||
use crate::kinds::*;
|
|
||||||
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
|
|
||||||
use crate::{EventSender, MostrMessage};
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use itertools::{Either, Itertools};
|
use itertools::{Either, Itertools};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::prelude::Marker;
|
use nostr_sdk::prelude::Marker;
|
||||||
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
|
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
|
||||||
use tokio::sync::mpsc::Sender;
|
|
||||||
use TagStandard::Hashtag;
|
use TagStandard::Hashtag;
|
||||||
|
|
||||||
|
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
|
||||||
|
use crate::kinds::*;
|
||||||
|
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
|
||||||
|
use crate::{EventSender, MostrMessage};
|
||||||
|
|
||||||
const MAX_OFFSET: u64 = 9;
|
const MAX_OFFSET: u64 = 9;
|
||||||
fn now() -> Timestamp {
|
fn now() -> Timestamp {
|
||||||
Timestamp::now() + MAX_OFFSET
|
Timestamp::now() + MAX_OFFSET
|
||||||
|
@ -54,7 +54,6 @@ pub(crate) struct Tasks {
|
||||||
state: StateFilter,
|
state: StateFilter,
|
||||||
|
|
||||||
sender: EventSender,
|
sender: EventSender,
|
||||||
overflow: VecDeque<Event>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
@ -106,7 +105,7 @@ impl Display for StateFilter {
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
pub(crate) fn from(
|
pub(crate) fn from(
|
||||||
url: Option<Url>,
|
url: Option<Url>,
|
||||||
tx: &Sender<MostrMessage>,
|
tx: &tokio::sync::mpsc::Sender<MostrMessage>,
|
||||||
keys: &Keys,
|
keys: &Keys,
|
||||||
metadata: Option<Metadata>,
|
metadata: Option<Metadata>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -143,25 +142,10 @@ impl Tasks {
|
||||||
tags_excluded: Default::default(),
|
tags_excluded: Default::default(),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
|
||||||
sender,
|
sender,
|
||||||
overflow: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process_overflow(&mut self) {
|
|
||||||
let elements = self.overflow.len();
|
|
||||||
for _ in 0..elements {
|
|
||||||
if let Some(event) = self.overflow.pop_front() {
|
|
||||||
if let Some(event) = self.add_prop(event) {
|
|
||||||
warn!("Unable to sort Event {:?}", event);
|
|
||||||
//self.overflow.push_back(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("Reprocessed {elements} Updates {}", self.sender.url.clone().map(|url| format!(" from {url}")).unwrap_or_default());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -217,10 +201,9 @@ impl Tasks {
|
||||||
last = new;
|
last = new;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO show history for active tags
|
|
||||||
("Your Time-Tracking History:".to_string(), Box::from(full.into_iter()))
|
("Your Time-Tracking History:".to_string(), Box::from(full.into_iter()))
|
||||||
} else {
|
} else {
|
||||||
("You have nothing time-tracked yet".to_string(), Box::from(empty()))
|
("You have nothing tracked yet".to_string(), Box::from(empty()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
|
@ -687,21 +670,20 @@ impl Tasks {
|
||||||
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
||||||
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
||||||
for task in self.filtered_tasks(position, false) {
|
for task in self.filtered_tasks(position, false) {
|
||||||
let content = task.get_filter_title();
|
let lowercase = task.event.content.to_ascii_lowercase();
|
||||||
let lowercase = content.to_ascii_lowercase();
|
|
||||||
if lowercase == lowercase_arg {
|
if lowercase == lowercase_arg {
|
||||||
return vec![task.event.id];
|
return vec![task.event.id];
|
||||||
} else if content.starts_with(arg) {
|
} else if task.event.content.starts_with(arg) {
|
||||||
filtered.push(task.event.id)
|
filtered.push(task.event.id)
|
||||||
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.trim_start_matches('#').starts_with(&lowercase_arg)) } {
|
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.starts_with(&lowercase_arg)) } {
|
||||||
filtered_fuzzy.push(task.event.id)
|
filtered_fuzzy.push(task.event.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Find global exact match
|
|
||||||
for task in self.tasks.values() {
|
for task in self.tasks.values() {
|
||||||
if task.get_filter_title().to_ascii_lowercase() == lowercase_arg &&
|
// Find global exact match
|
||||||
// exclude closed tasks and their subtasks
|
if task.event.content.to_ascii_lowercase() == lowercase_arg &&
|
||||||
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
|
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
|
||||||
|
// exclude closed tasks and their subtasks
|
||||||
return vec![task.event.id];
|
return vec![task.event.id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -718,10 +700,7 @@ impl Tasks {
|
||||||
filtered
|
filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds out what to do with the given string, one of:
|
/// Finds out what to do with the given string.
|
||||||
/// - filtering the visible tasks
|
|
||||||
/// - entering the only matching task
|
|
||||||
/// - creating a new task
|
|
||||||
/// Returns an EventId if a new Task was created.
|
/// Returns an EventId if a new Task was created.
|
||||||
pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
|
pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
|
||||||
let filtered = self.get_matching(position, arg);
|
let filtered = self.get_matching(position, arg);
|
||||||
|
@ -929,10 +908,7 @@ impl Tasks {
|
||||||
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
|
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(event) = self.add_prop(event) {
|
self.add_prop(event)
|
||||||
debug!("Requeueing unknown Event {:?}", event);
|
|
||||||
self.overflow.push_back(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -951,19 +927,17 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add event as prop, returning it if not processable
|
fn add_prop(&mut self, event: Event) {
|
||||||
fn add_prop(&mut self, event: Event) -> Option<Event> {
|
|
||||||
let found = self.referenced_tasks(&event, |t| {
|
let found = self.referenced_tasks(&event, |t| {
|
||||||
t.props.insert(event.clone());
|
t.props.insert(event.clone());
|
||||||
});
|
});
|
||||||
if !found {
|
if !found {
|
||||||
if event.kind.as_u16() == 1 {
|
if event.kind.as_u16() == 1 {
|
||||||
self.add_task(event);
|
self.add_task(event);
|
||||||
} else {
|
return;
|
||||||
return Some(event)
|
|
||||||
}
|
}
|
||||||
|
warn!("Unknown event {:?}", event)
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {
|
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {
|
||||||
|
|
Loading…
Reference in New Issue