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?
|
||||
- Task markdown support? - colored
|
||||
- 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)
|
||||
+ Fetch most recent tasks first
|
||||
+ 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 log::info;
|
||||
use nostr_sdk::TagStandard::Hashtag;
|
||||
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 PROCEDURE_KIND_ID: u16 = 1639;
|
||||
|
@ -83,33 +82,21 @@ pub(crate) fn build_prop(
|
|||
)
|
||||
}
|
||||
|
||||
/// Return Hashtags embedded in the string.
|
||||
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.
|
||||
/// Expects sanitized input
|
||||
pub(crate) fn extract_tags(input: &str) -> (&str, Vec<Tag>) {
|
||||
match input.split_once(": ") {
|
||||
None => (input, extract_hashtags(input).collect_vec()),
|
||||
Some((name, tags)) => {
|
||||
let tags = extract_hashtags(name)
|
||||
.chain(tags.split_ascii_whitespace().map(to_hashtag))
|
||||
None => (input, vec![]),
|
||||
Some(s) => {
|
||||
let tags = s
|
||||
.1
|
||||
.split_ascii_whitespace()
|
||||
.map(|t| Hashtag(t.to_string()).into())
|
||||
.collect();
|
||||
(name, tags)
|
||||
(s.0, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_hashtag(tag: &str) -> Tag {
|
||||
Hashtag(tag.to_string()).into()
|
||||
}
|
||||
|
||||
fn format_tag(tag: &Tag) -> String {
|
||||
match tag.as_standardized() {
|
||||
Some(TagStandard::Event {
|
||||
|
@ -136,9 +123,3 @@ pub(crate) fn is_hashtag(tag: &Tag) -> bool {
|
|||
.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 {
|
||||
info!("Received {count} Updates");
|
||||
} else {
|
||||
relays.values_mut().for_each(|tasks| tasks.process_overflow());
|
||||
}
|
||||
|
||||
let mut iter = input.chars();
|
||||
|
|
|
@ -78,14 +78,11 @@ impl Task {
|
|||
|
||||
/// Trimmed event content or stringified id
|
||||
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())
|
||||
}
|
||||
|
||||
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> + '_ {
|
||||
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::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 itertools::{Either, Itertools};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::prelude::Marker;
|
||||
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 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;
|
||||
fn now() -> Timestamp {
|
||||
Timestamp::now() + MAX_OFFSET
|
||||
|
@ -54,7 +54,6 @@ pub(crate) struct Tasks {
|
|||
state: StateFilter,
|
||||
|
||||
sender: EventSender,
|
||||
overflow: VecDeque<Event>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -106,7 +105,7 @@ impl Display for StateFilter {
|
|||
impl Tasks {
|
||||
pub(crate) fn from(
|
||||
url: Option<Url>,
|
||||
tx: &Sender<MostrMessage>,
|
||||
tx: &tokio::sync::mpsc::Sender<MostrMessage>,
|
||||
keys: &Keys,
|
||||
metadata: Option<Metadata>,
|
||||
) -> Self {
|
||||
|
@ -143,25 +142,10 @@ impl Tasks {
|
|||
tags_excluded: Default::default(),
|
||||
state: Default::default(),
|
||||
depth: 1,
|
||||
|
||||
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
|
||||
|
||||
#[inline]
|
||||
|
@ -217,10 +201,9 @@ impl Tasks {
|
|||
last = new;
|
||||
}
|
||||
}
|
||||
// TODO show history for active tags
|
||||
("Your Time-Tracking History:".to_string(), Box::from(full.into_iter()))
|
||||
} else {
|
||||
("You have nothing time-tracked yet".to_string(), Box::from(empty()))
|
||||
("You have nothing tracked yet".to_string(), Box::from(empty()))
|
||||
}
|
||||
}
|
||||
Some(id) => {
|
||||
|
@ -687,21 +670,20 @@ impl Tasks {
|
|||
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
||||
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
||||
for task in self.filtered_tasks(position, false) {
|
||||
let content = task.get_filter_title();
|
||||
let lowercase = content.to_ascii_lowercase();
|
||||
let lowercase = task.event.content.to_ascii_lowercase();
|
||||
if lowercase == lowercase_arg {
|
||||
return vec![task.event.id];
|
||||
} else if content.starts_with(arg) {
|
||||
} else if task.event.content.starts_with(arg) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
// Find global exact match
|
||||
for task in self.tasks.values() {
|
||||
if task.get_filter_title().to_ascii_lowercase() == lowercase_arg &&
|
||||
// exclude closed tasks and their subtasks
|
||||
// Find global exact match
|
||||
if task.event.content.to_ascii_lowercase() == lowercase_arg &&
|
||||
!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];
|
||||
}
|
||||
}
|
||||
|
@ -718,10 +700,7 @@ impl Tasks {
|
|||
filtered
|
||||
}
|
||||
|
||||
/// Finds out what to do with the given string, one of:
|
||||
/// - filtering the visible tasks
|
||||
/// - entering the only matching task
|
||||
/// - creating a new task
|
||||
/// Finds out what to do with the given string.
|
||||
/// Returns an EventId if a new Task was created.
|
||||
pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
|
||||
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)])); }
|
||||
}
|
||||
} else {
|
||||
if let Some(event) = self.add_prop(event) {
|
||||
debug!("Requeueing unknown Event {:?}", event);
|
||||
self.overflow.push_back(event);
|
||||
}
|
||||
self.add_prop(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -951,19 +927,17 @@ impl Tasks {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add event as prop, returning it if not processable
|
||||
fn add_prop(&mut self, event: Event) -> Option<Event> {
|
||||
fn add_prop(&mut self, event: Event) {
|
||||
let found = self.referenced_tasks(&event, |t| {
|
||||
t.props.insert(event.clone());
|
||||
});
|
||||
if !found {
|
||||
if event.kind.as_u16() == 1 {
|
||||
self.add_task(event);
|
||||
} else {
|
||||
return Some(event)
|
||||
return;
|
||||
}
|
||||
warn!("Unknown event {:?}", event)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {
|
||||
|
|
Loading…
Reference in New Issue