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 crate::task::{State, MARKER_PARENT}; pub const TASK_KIND: Kind = Kind::GitIssue; pub const PROCEDURE_KIND_ID: u16 = 1639; pub const PROCEDURE_KIND: Kind = Kind::Regular(PROCEDURE_KIND_ID); pub const TRACKING_KIND: Kind = Kind::Regular(1650); pub const BASIC_KINDS: [Kind; 4] = [ Kind::Metadata, Kind::TextNote, TASK_KIND, Kind::Bookmarks, ]; pub const PROP_KINDS: [Kind; 6] = [ TRACKING_KIND, Kind::GitStatusOpen, Kind::GitStatusApplied, Kind::GitStatusClosed, Kind::GitStatusDraft, PROCEDURE_KIND, ]; // TODO: use formatting - bold / heading / italics - and generate from code /// Helper for available properties. pub const PROPERTY_COLUMNS: &str = "# Available Properties Immutable: - `id` - unique task id - `parentid` - unique task id of the parent, if any - `name` - initial name of the task - `created` - task creation timestamp - `author` - name or abbreviated key of the task creator Task: - `status` - pure task status - `hashtags` - list of hashtags set for the task - `tags` - values of all nostr tags associated with the event, except event tags - `desc` - last note on the task - `description` - accumulated notes on the task - `time` - time tracked on this task by you Utilities: - `state` - indicator of current progress - `rtime` - time tracked on this tasks and its subtree by everyone - `progress` - recursive subtask completion in percent - `subtasks` - how many direct subtasks are complete - `path` - name including parent tasks - `rpath` - name including parent tasks up to active task - TBI `depends` - list all tasks this task depends on before it becomes actionable Debugging: `kind`, `pubkey`, `props`, `alltags`, `descriptions`"; pub(crate) fn build_tracking(id: I) -> EventBuilder where I: IntoIterator, { EventBuilder::new( Kind::from(TRACKING_KIND), "", id.into_iter().map(Tag::event), ) } /// Build a task with informational output and optional labeled kind pub(crate) fn build_task(name: &str, tags: Vec, kind: Option<(&str, Kind)>) -> EventBuilder { info!("Created {}task \"{name}\" with tags [{}]", kind.map(|k| k.0).unwrap_or_default(), tags.iter().map(format_tag).join(", ")); EventBuilder::new(kind.map(|k| k.1).unwrap_or(TASK_KIND), name, tags) } pub(crate) fn build_prop( kind: Kind, comment: &str, id: EventId, ) -> EventBuilder { EventBuilder::new( kind, comment, vec![Tag::event(id)], ) } /// Return Hashtags embedded in the string. pub(crate) fn extract_hashtags(input: &str) -> impl Iterator + '_ { 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) { 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)) .collect(); (name, 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 { event_id, marker, .. }) => format!("{}: {:.8}", marker.as_ref().map(|m| m.to_string()).unwrap_or(MARKER_PARENT.to_string()), event_id), Some(TagStandard::PublicKey { public_key, alias, .. }) => format!("Key{}: {:.8}", public_key, alias.as_ref().map(|s| format!(" {s}")).unwrap_or_default()), Some(TagStandard::Hashtag(content)) => format!("#{content}"), _ => tag.content().map_or_else( || format!("Kind {}", tag.kind()), |content| content.to_string(), ) } } pub(crate) fn is_hashtag(tag: &Tag) -> bool { tag.single_letter_tag() .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())) }