Compare commits

...

3 Commits

Author SHA1 Message Date
xeruf 960a5210c6 docs: document tags property column 2024-07-25 22:51:12 +03:00
xeruf fd94de7149 feat: tags property column 2024-07-25 22:50:57 +03:00
xeruf ce7e015b02 feat: activating tags 2024-07-25 22:40:35 +03:00
4 changed files with 57 additions and 16 deletions

View File

@ -22,7 +22,9 @@ Recommendation: Flat hierarchy, using tags for filtering (TBI)
## Reference ## Reference
TASK add syntax: `NAME: TAG1 TAG2` ### Command Syntax
TASK add syntax: `NAME: TAG1 TAG2 ...`
- `TASK` - create task - `TASK` - create task
- `.` - clear filters and reload - `.` - clear filters and reload
@ -35,26 +37,29 @@ Dots can be repeated to move to parent tasks
- `>[TEXT]` - Complete active task and move to parent, with optional state description - `>[TEXT]` - Complete active task and move to parent, with optional state description
- `<[TEXT]` - Close active task and move to parent, with optional state description - `<[TEXT]` - Close active task and move to parent, with optional state description
- `-TEXT` - add text note (comment / description) - `-TEXT` - add text note (comment / description)
- `#TAG` - filter by tag
State descriptions can be used for example for Kanban columns. State descriptions can be used for example for Kanban columns.
### Columns ### Available Columns
- `id` - `id`
- `parentid` - `parentid`
- `name` - `name`
- `state` - `state`
- `tags`
- `desc` - accumulated notes of the task - `desc` - accumulated notes of the task
- `path` - name including parent tasks - `path` - name including parent tasks
- `rpath` - name including parent tasks up to active task - `rpath` - name including parent tasks up to active task
- `time` - time tracked - `time` - time tracked
- `ttime` - time tracked including subtasks - `ttime` - time tracked including subtasks
- TBI: `progress` - how many subtasks are complete - TBI: `progress` - how many subtasks are complete
- TBI: `progressp` - subtask completion in percent
For debugging: `props` - Task Property Events For debugging: `props` - Task Property Events
## Plans ## Plans
- Expiry (no need to fetch potential years of history) - Expiry (no need to fetch potential years of history)
- Web Interface, Messenger bots - Web Interface, Messenger integrations
- TUI - Clear terminal? - TUI - Clear terminal?

View File

@ -201,6 +201,10 @@ async fn main() {
tasks.move_up() tasks.move_up()
} }
Some('#') => {
tasks.add_tag(input[1..].to_string());
}
Some('.') => { Some('.') => {
let mut dots = 1; let mut dots = 1;
let mut pos = tasks.get_position(); let mut pos = tasks.get_position();

View File

@ -7,13 +7,16 @@ pub(crate) struct Task {
pub(crate) event: Event, pub(crate) event: Event,
pub(crate) children: HashSet<EventId>, pub(crate) children: HashSet<EventId>,
pub(crate) props: BTreeSet<Event>, pub(crate) props: BTreeSet<Event>,
/// Cached sorted tags of the event
pub(crate) tags: Option<BTreeSet<Tag>>,
} }
impl Task { impl Task {
pub(crate) fn new(event: Event) -> Task { pub(crate) fn new(event: Event) -> Task {
Task { Task {
event,
children: Default::default(), children: Default::default(),
props: Default::default(), props: Default::default(),
tags: if event.tags.is_empty() { None } else { Some(event.tags.iter().cloned().collect()) },
event,
} }
} }
@ -100,6 +103,12 @@ impl Task {
"state" => self.state().map(|s| s.to_string()), "state" => self.state().map(|s| s.to_string()),
"name" => Some(self.event.content.clone()), "name" => Some(self.event.content.clone()),
"time" => Some(self.time_tracked().to_string()), // TODO: format properly "time" => Some(self.time_tracked().to_string()), // TODO: format properly
"tags" => self.tags.as_ref().map(|tags| {
tags.iter()
.map(|t| format!("{}", t.content().unwrap()))
.collect::<Vec<String>>()
.join(" ")
}),
"props" => Some(format!( "props" => Some(format!(
"{:?}", "{:?}",
self.props self.props

View File

@ -1,7 +1,8 @@
use std::collections::HashMap; use std::collections::{BTreeSet, HashMap};
use std::iter::once; use std::iter::once;
use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, Tag}; use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, Tag};
use nostr_sdk::Tag::Hashtag;
use crate::{EventSender, TASK_KIND}; use crate::{EventSender, TASK_KIND};
use crate::task::{State, Task}; use crate::task::{State, Task};
@ -17,8 +18,10 @@ pub(crate) struct Tasks {
/// Positive: Go down the respective level /// Positive: Go down the respective level
pub(crate) depth: i8, pub(crate) depth: i8,
/// The task currently selected. /// Currently active task
position: Option<EventId>, position: Option<EventId>,
/// Currently active tags
tags: BTreeSet<Tag>,
/// A filtered view of the current tasks /// A filtered view of the current tasks
view: Vec<EventId>, view: Vec<EventId>,
@ -32,6 +35,7 @@ impl Tasks {
properties: vec!["id".into(), "name".into(), "state".into(), "ttime".into()], properties: vec!["id".into(), "name".into(), "state".into(), "ttime".into()],
position: None, position: None,
view: Default::default(), view: Default::default(),
tags: Default::default(),
depth: 1, depth: 1,
sender, sender,
} }
@ -65,6 +69,12 @@ impl Tasks {
pub(crate) fn taskpath(&self, id: Option<EventId>) -> String { pub(crate) fn taskpath(&self, id: Option<EventId>) -> String {
join_tasks(self.traverse_up_from(id)) join_tasks(self.traverse_up_from(id))
+ &self
.tags
.iter()
.map(|t| format!(" #{}", t.content().unwrap()))
.collect::<Vec<String>>()
.join("")
} }
pub(crate) fn traverse_up_from(&self, id: Option<EventId>) -> ParentIterator { pub(crate) fn traverse_up_from(&self, id: Option<EventId>) -> ParentIterator {
@ -132,7 +142,7 @@ impl Tasks {
if res.len() > 0 { if res.len() > 0 {
return res; return res;
} }
self.position.map_or_else( let tasks = self.position.map_or_else(
|| { || {
if self.depth > 8 { if self.depth > 8 {
self.tasks.values().collect() self.tasks.values().collect()
@ -155,7 +165,20 @@ impl Tasks {
.get(&p) .get(&p)
.map_or(Vec::new(), |t| self.resolve_tasks(t.children.iter())) .map_or(Vec::new(), |t| self.resolve_tasks(t.children.iter()))
}, },
) );
if self.tags.is_empty() {
tasks
} else {
tasks
.into_iter()
.filter(|t| {
t.tags.as_ref().map_or(false, |tags| {
let mut iter = tags.iter();
self.tags.iter().all(|tag| iter.any(|t| t == tag))
})
})
.collect()
}
} }
pub(crate) fn print_current_tasks(&self) { pub(crate) fn print_current_tasks(&self) {
@ -187,6 +210,11 @@ impl Tasks {
self.view = view self.view = view
} }
pub(crate) fn add_tag(&mut self, tag: String) {
self.view.clear();
self.tags.insert(Hashtag(tag));
}
pub(crate) fn move_up(&mut self) { pub(crate) fn move_up(&mut self) {
self.move_to( self.move_to(
self.position self.position
@ -197,6 +225,7 @@ impl Tasks {
pub(crate) fn move_to(&mut self, id: Option<EventId>) { pub(crate) fn move_to(&mut self, id: Option<EventId>) {
self.view.clear(); self.view.clear();
self.tags.clear();
if id == self.position { if id == self.position {
return; return;
} }
@ -220,18 +249,12 @@ impl Tasks {
// Updates // Updates
pub(crate) fn build_task(&self, input: &str) -> EventBuilder { pub(crate) fn build_task(&self, input: &str) -> EventBuilder {
let mut tags: Vec<Tag> = Vec::new(); let mut tags: Vec<Tag> = self.tags.iter().cloned().collect();
self.position.inspect(|p| tags.push(Tag::event(*p))); self.position.inspect(|p| tags.push(Tag::event(*p)));
return match input.split_once(": ") { return match input.split_once(": ") {
None => EventBuilder::new(Kind::from(TASK_KIND), input, tags), None => EventBuilder::new(Kind::from(TASK_KIND), input, tags),
Some(s) => { Some(s) => {
tags.append( tags.append(&mut s.1.split(" ").map(|t| Hashtag(t.to_string())).collect());
&mut s
.1
.split(" ")
.map(|t| Tag::Hashtag(t.to_string()))
.collect(),
);
EventBuilder::new(Kind::from(TASK_KIND), s.0, tags) EventBuilder::new(Kind::from(TASK_KIND), s.0, tags)
} }
}; };