diff --git a/README.md b/README.md index 9e17acf..c9c7a03 100644 --- a/README.md +++ b/README.md @@ -121,18 +121,19 @@ Property Filters: - `#TAG` - set tag filter (empty: list all used tags) - `+TAG` - add tag filter - `-TAG` - remove tag filters -- `?STATE` - filter by state (type or description) - plain `?` to reset, `??` to show all +- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all -State descriptions can be used for example for Kanban columns or review flows. -An active tag or state filter will also set that attribute for newly created tasks. +Status descriptions can be used for example for Kanban columns or review flows. +An active tag or status filter will also set that attribute for newly created tasks. ### Available Columns - `id` - `parentid` - `name` -- `state` -- `hashtags` +- `state` - indicator of current progress +- `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 @@ -180,6 +181,7 @@ The following features are not ready to be implemented because they need conceptualization. Suggestions welcome! +- What if I want to postpone a procedure, i.e. make it pending, or move it across kanban, does this make sense? - Priorities - Dependencies (change from tags to properties so they can be added later? or maybe as a state?) - Templates @@ -194,3 +196,8 @@ Suggestions welcome! - TUI: Clear terminal? Refresh on empty prompt after timeout? - Kanban, GANTT, Calendar - Web Interface, Messenger integrations + +## Notes + +- TBI = To Be Implemented +- `. TASK` - create and enter a new task even if the name matches an existing one \ No newline at end of file diff --git a/src/task.rs b/src/task.rs index aa078cf..d152640 100644 --- a/src/task.rs +++ b/src/task.rs @@ -4,11 +4,11 @@ use std::collections::{BTreeSet, HashSet}; use std::fmt; use std::string::ToString; -use colored::Colorize; +use colored::{ColoredString, Colorize}; use itertools::Either::{Left, Right}; use itertools::Itertools; use log::{debug, error, info, trace, warn}; -use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, TagStandard, Timestamp}; +use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp}; use crate::helpers::some_non_empty; use crate::kinds::{is_hashtag, PROCEDURE_KIND}; @@ -72,7 +72,6 @@ impl Task { } pub(crate) fn get_dependendees(&self) -> Vec<&EventId> { - // TODO honor properly self.find_refs(MARKER_DEPENDS).collect() } @@ -142,17 +141,7 @@ impl Task { match property { "id" => Some(self.event.id.to_string()), "parentid" => self.parent_id().map(|i| i.to_string()), - "state" => Some({ - let state = self.state_or_default(); - let label = state.get_label(); - match state.state { - State::Open => label.green(), - State::Done => label.bright_black(), - State::Closed => label.magenta(), - State::Pending => label.yellow(), - State::Procedure => label.blue(), - }.to_string() - }), + "status" => Some(self.state_or_default().get_colored_label().to_string()), "name" => Some(self.event.content.clone()), "desc" => self.descriptions().last().cloned(), "description" => Some(self.descriptions().join(" ")), @@ -180,7 +169,7 @@ impl Task { } pub(crate) struct TaskState { - state: State, + pub(crate) state: State, name: Option, pub(crate) time: Timestamp, } @@ -191,6 +180,9 @@ impl TaskState { pub(crate) fn get_label(&self) -> String { self.name.clone().unwrap_or_else(|| self.state.to_string()) } + pub(crate) fn get_colored_label(&self) -> ColoredString { + self.state.colorize(&self.get_label()) + } pub(crate) fn matches_label(&self, label: &str) -> bool { self.name.as_ref().is_some_and(|n| n.eq_ignore_ascii_case(label)) || self.state.to_string().eq_ignore_ascii_case(label) @@ -211,7 +203,7 @@ impl Display for TaskState { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq)] pub(crate) enum State { Open, Done, @@ -261,6 +253,16 @@ impl State { State::Procedure => PROCEDURE_KIND, } } + + pub(crate) fn colorize(&self, str: &str) -> ColoredString { + match self { + State::Open => str.green(), + State::Done => str.bright_black(), + State::Closed => str.magenta(), + State::Pending => str.yellow(), + State::Procedure => str.blue(), + } + } } impl From for Kind { fn from(value: State) -> Self { diff --git a/src/tasks.rs b/src/tasks.rs index 64ac3c9..25164f2 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -116,7 +116,6 @@ impl Tasks { history: Default::default(), properties: vec![ "state".into(), - "progress".into(), "rtime".into(), "hashtags".into(), "rpath".into(), @@ -400,9 +399,15 @@ impl Tasks { writeln!(lock, "{}", t.descriptions().join("\n"))?; } // TODO proper column alignment + // TODO hide empty columns and sorting writeln!(lock, "{}", self.properties.join("\t").bold())?; let mut total_time = 0; for task in self.current_tasks() { + let progress = + self + .total_progress(task.get_id()) + .filter(|_| task.children.len() > 0); + let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)); writeln!( lock, "{}", @@ -424,10 +429,18 @@ impl Tasks { "".to_string() } } - "progress" => self - .total_progress(task.get_id()) - .filter(|_| task.children.len() > 0) - .map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)), + "state" => { + if let Some(task) = task.get_dependendees().iter().filter_map(|id| self.get_by_id(id)).find(|t| t.pure_state().is_open()) { + return format!("Blocked by \"{}\"", task.get_title()).bright_red().to_string() + } + let state = task.state_or_default(); + if state.state.is_open() && progress.is_some_and(|p| p > 0.1) { + state.state.colorize(&prog_string) + } else { + state.get_colored_label() + }.to_string() + } + "progress" => prog_string.clone(), "path" => self.get_task_path(Some(task.event.id)), "rpath" => self.relative_path(task.event.id), // TODO format strings configurable