2024-08-11 10:58:34 +03:00
use std ::collections ::{ BTreeSet , HashMap , VecDeque } ;
2024-08-10 15:44:52 +03:00
use std ::fmt ::{ Display , Formatter } ;
2024-07-30 08:23:32 +03:00
use std ::io ::{ Error , stdout , Write } ;
2024-08-18 22:24:14 +03:00
use std ::iter ::{ empty , once } ;
2024-07-31 20:05:52 +03:00
use std ::ops ::{ Div , Rem } ;
2024-08-08 15:09:39 +03:00
use std ::str ::FromStr ;
2024-08-07 15:03:29 +03:00
use std ::sync ::mpsc ::Sender ;
2024-08-08 00:18:34 +03:00
use std ::time ::Duration ;
2024-08-08 13:52:02 +03:00
2024-08-15 12:21:21 +03:00
use chrono ::Local ;
2024-07-30 09:00:39 +03:00
use colored ::Colorize ;
2024-07-30 08:23:32 +03:00
use itertools ::Itertools ;
2024-07-29 21:06:23 +03:00
use log ::{ debug , error , info , trace , warn } ;
2024-08-18 21:33:04 +03:00
use nostr_sdk ::{ Event , EventBuilder , EventId , JsonUtil , Keys , Kind , Metadata , PublicKey , Tag , TagStandard , Timestamp , UncheckedUrl , Url } ;
2024-08-08 21:10:17 +03:00
use nostr_sdk ::prelude ::Marker ;
2024-08-06 11:34:18 +03:00
use TagStandard ::Hashtag ;
2024-07-19 21:06:03 +03:00
2024-08-15 12:21:21 +03:00
use crate ::{ EventSender , MostrMessage } ;
2024-08-14 21:49:36 +03:00
use crate ::helpers ::{ format_stamp , local_datetimestamp , relative_datetimestamp , some_non_empty } ;
2024-08-02 14:43:39 +03:00
use crate ::kinds ::* ;
2024-08-12 23:02:50 +03:00
use crate ::task ::{ MARKER_DEPENDS , MARKER_PARENT , State , Task , TaskState } ;
2024-07-19 21:06:03 +03:00
2024-07-19 01:15:11 +03:00
type TaskMap = HashMap < EventId , Task > ;
2024-07-26 21:45:29 +03:00
#[ derive(Debug, Clone) ]
2024-07-19 01:15:11 +03:00
pub ( crate ) struct Tasks {
2024-07-19 09:35:03 +03:00
/// The Tasks
2024-07-24 21:11:36 +03:00
tasks : TaskMap ,
2024-07-31 20:05:52 +03:00
/// History of active tasks by PubKey
history : HashMap < PublicKey , BTreeSet < Event > > ,
2024-08-18 21:33:04 +03:00
/// Index of found users with metadata
users : HashMap < PublicKey , Metadata > ,
2024-08-14 15:59:43 +03:00
2024-07-19 09:35:03 +03:00
/// The task properties currently visible
2024-08-07 00:06:09 +03:00
properties : Vec < String > ,
2024-08-11 10:01:46 +03:00
/// The task properties sorted by
2024-08-11 10:58:34 +03:00
sorting : VecDeque < String > ,
2024-08-18 21:37:39 +03:00
/// Currently active task
position : Option < EventId > ,
/// A filtered view of the current tasks
view : Vec < EventId > ,
2024-07-25 00:26:29 +03:00
/// Negative: Only Leaf nodes
/// Zero: Only Active node
/// Positive: Go down the respective level
2024-08-07 00:06:09 +03:00
depth : i8 ,
2024-07-24 21:11:36 +03:00
2024-07-25 22:40:35 +03:00
/// Currently active tags
tags : BTreeSet < Tag > ,
2024-08-11 12:28:08 +03:00
/// Tags filtered out
tags_excluded : BTreeSet < Tag > ,
2024-07-26 21:45:29 +03:00
/// Current active state
2024-08-10 15:44:52 +03:00
state : StateFilter ,
2024-07-24 21:11:36 +03:00
2024-07-25 10:55:29 +03:00
sender : EventSender ,
2024-07-19 01:15:11 +03:00
}
2024-08-10 15:44:52 +03:00
#[ derive(Clone, Debug) ]
pub ( crate ) enum StateFilter {
Default ,
All ,
State ( String ) ,
}
impl StateFilter {
fn indicator ( & self ) -> String {
match self {
StateFilter ::Default = > " " . to_string ( ) ,
StateFilter ::All = > " ?ALL " . to_string ( ) ,
StateFilter ::State ( str ) = > format! ( " ? {str} " ) ,
}
}
fn matches ( & self , task : & Task ) -> bool {
match self {
StateFilter ::Default = > {
let state = task . pure_state ( ) ;
state . is_open ( ) | | ( state = = State ::Done & & task . parent_id ( ) ! = None )
2024-08-10 18:12:31 +03:00
}
2024-08-10 15:44:52 +03:00
StateFilter ::All = > true ,
StateFilter ::State ( filter ) = > task . state ( ) . is_some_and ( | t | t . matches_label ( filter ) ) ,
}
}
2024-08-10 18:12:31 +03:00
2024-08-10 15:44:52 +03:00
fn as_option ( & self ) -> Option < String > {
if let StateFilter ::State ( str ) = self {
Some ( str . to_string ( ) )
} else {
None
}
}
}
impl Default for StateFilter {
fn default ( ) -> Self {
StateFilter ::Default
}
}
impl Display for StateFilter {
fn fmt ( & self , f : & mut Formatter < '_ > ) -> std ::fmt ::Result {
write! (
f ,
" {} " ,
match self {
StateFilter ::Default = > " relevant tasks " . to_string ( ) ,
StateFilter ::All = > " all tasks " . to_string ( ) ,
StateFilter ::State ( s ) = > format! ( " state {s} " ) ,
}
)
}
}
2024-07-24 16:03:34 +03:00
impl Tasks {
2024-08-13 21:35:35 +03:00
pub ( crate ) fn from ( url : Option < Url > , tx : & Sender < MostrMessage > , keys : & Keys ) -> Self {
2024-08-07 15:03:29 +03:00
Self ::with_sender ( EventSender {
url ,
tx : tx . clone ( ) ,
keys : keys . clone ( ) ,
queue : Default ::default ( ) ,
} )
}
pub ( crate ) fn with_sender ( sender : EventSender ) -> Self {
2024-07-19 01:15:11 +03:00
Tasks {
tasks : Default ::default ( ) ,
2024-07-31 20:05:52 +03:00
history : Default ::default ( ) ,
2024-08-18 21:33:04 +03:00
users : Default ::default ( ) ,
2024-07-26 21:50:55 +03:00
properties : vec ! [
" state " . into ( ) ,
2024-07-28 11:37:36 +03:00
" rtime " . into ( ) ,
2024-07-30 09:37:12 +03:00
" hashtags " . into ( ) ,
2024-07-26 21:50:55 +03:00
" rpath " . into ( ) ,
" desc " . into ( ) ,
] ,
2024-08-11 10:58:34 +03:00
sorting : VecDeque ::from ( [
2024-08-11 10:01:46 +03:00
" state " . into ( ) ,
2024-08-14 19:42:58 +03:00
" hashtags " . into ( ) ,
2024-08-13 21:32:45 +03:00
" rtime " . into ( ) ,
2024-08-11 10:01:46 +03:00
" name " . into ( ) ,
2024-08-11 10:58:34 +03:00
] ) ,
2024-08-07 15:04:18 +03:00
position : None , // TODO persist position
2024-07-19 09:35:03 +03:00
view : Default ::default ( ) ,
2024-07-25 22:40:35 +03:00
tags : Default ::default ( ) ,
2024-08-11 12:28:08 +03:00
tags_excluded : Default ::default ( ) ,
2024-08-10 15:44:52 +03:00
state : Default ::default ( ) ,
2024-07-25 00:26:29 +03:00
depth : 1 ,
2024-07-25 10:55:29 +03:00
sender ,
2024-07-19 01:15:11 +03:00
}
}
2024-07-25 22:10:01 +03:00
// Accessors
2024-07-30 17:13:29 +03:00
#[ inline ]
2024-08-01 14:07:40 +03:00
pub ( crate ) fn get_by_id ( & self , id : & EventId ) -> Option < & Task > { self . tasks . get ( id ) }
2024-07-26 21:45:29 +03:00
2024-07-30 17:13:29 +03:00
#[ inline ]
2024-08-01 14:07:40 +03:00
pub ( crate ) fn get_position ( & self ) -> Option < EventId > { self . position }
#[ inline ]
pub ( crate ) fn len ( & self ) -> usize { self . tasks . len ( ) }
2024-07-19 21:06:03 +03:00
2024-08-12 12:17:03 +03:00
/// Ids of all subtasks recursively found for id, including itself
2024-08-12 23:07:39 +03:00
pub ( crate ) fn get_task_tree < ' a > ( & ' a self , id : & ' a EventId ) -> ChildIterator {
ChildIterator ::from ( & self . tasks , id )
2024-07-31 20:05:52 +03:00
}
2024-08-08 13:52:02 +03:00
2024-08-08 13:04:22 +03:00
pub ( crate ) fn all_hashtags ( & self ) -> impl Iterator < Item = & str > {
self . tasks . values ( )
. filter ( | t | t . pure_state ( ) ! = State ::Closed )
. filter_map ( | t | t . tags . as_ref ( ) ) . flatten ( )
. filter ( | tag | is_hashtag ( tag ) )
. filter_map ( | tag | tag . content ( ) . map ( | s | s . trim ( ) ) )
. sorted_unstable ( )
. dedup ( )
}
2024-07-31 20:05:52 +03:00
2024-08-10 21:04:13 +03:00
/// Dynamic time tracking overview for current task or current user.
2024-08-19 11:27:12 +03:00
pub ( crate ) fn times_tracked ( & self ) -> ( String , Box < dyn DoubleEndedIterator < Item = String > > ) {
2024-08-08 15:09:39 +03:00
match self . get_position ( ) {
None = > {
2024-08-19 11:27:12 +03:00
if let Some ( set ) = self . history . get ( & self . sender . pubkey ( ) ) {
let mut full = Vec ::with_capacity ( set . len ( ) ) ;
2024-08-08 15:09:39 +03:00
for event in set {
let new = some_non_empty ( & event . tags . iter ( )
. filter_map ( | t | t . content ( ) )
. map ( | str | EventId ::from_str ( str ) . ok ( ) . map_or ( str . to_string ( ) , | id | self . get_task_title ( & id ) ) )
. join ( " " ) ) ;
2024-08-19 11:27:12 +03:00
if new . as_ref ( ) ! = full . last ( ) {
full . push ( format! ( " {:>15} {} " , relative_datetimestamp ( & event . created_at ) , new . as_ref ( ) . unwrap_or ( & " --- " . to_string ( ) ) ) ) ;
2024-08-08 15:09:39 +03:00
}
}
2024-08-19 11:27:12 +03:00
( " Your Time Tracking History: " . to_string ( ) , Box ::from ( full . into_iter ( ) ) )
2024-08-08 15:09:39 +03:00
} else {
2024-08-19 11:27:12 +03:00
( " You have nothing tracked yet " . to_string ( ) , Box ::from ( empty ( ) ) )
2024-08-08 15:09:39 +03:00
}
}
Some ( id ) = > {
2024-08-12 12:17:03 +03:00
let ids = vec! [ & id ] ;
2024-08-19 11:27:12 +03:00
let history =
2024-08-10 21:04:13 +03:00
self . history . iter ( ) . flat_map ( | ( key , set ) | {
let mut vec = Vec ::with_capacity ( set . len ( ) / 2 ) ;
let mut iter = timestamps ( set . iter ( ) , & ids ) . tuples ( ) ;
while let Some ( ( ( start , _ ) , ( end , _ ) ) ) = iter . next ( ) {
2024-08-14 21:49:36 +03:00
vec . push ( format! ( " {} - {} by {} " ,
local_datetimestamp ( start ) ,
// Only use full stamp when ambiguous (>1day)
2024-08-19 11:27:12 +03:00
if end . as_u64 ( ) - start . as_u64 ( ) > 80_000 {
2024-08-14 21:49:36 +03:00
local_datetimestamp ( end )
} else {
format_stamp ( end , " %H:%M " )
} ,
key ) )
2024-08-10 21:04:13 +03:00
}
2024-08-14 21:49:36 +03:00
iter . into_buffer ( )
. for_each ( | ( stamp , _ ) |
vec . push ( format! ( " {} started by {} " , local_datetimestamp ( stamp ) , key ) ) ) ;
2024-08-10 21:04:13 +03:00
vec
2024-08-19 11:27:12 +03:00
} ) . sorted_unstable ( ) ; // TODO sorting depends on timestamp format - needed to interleave different people
( format! ( " Times tracked on {} " , self . get_task_title ( & id ) ) , Box ::from ( history ) )
2024-08-08 15:09:39 +03:00
}
}
}
2024-08-10 21:04:13 +03:00
/// Total time in seconds tracked on this task by the current user.
pub ( crate ) fn time_tracked ( & self , id : EventId ) -> u64 {
2024-08-12 12:17:03 +03:00
TimesTracked ::from ( self . history . get ( & self . sender . pubkey ( ) ) . into_iter ( ) . flatten ( ) , & vec! [ & id ] ) . sum ::< Duration > ( ) . as_secs ( )
2024-08-10 21:04:13 +03:00
}
2024-08-08 00:18:34 +03:00
/// Total time in seconds tracked on this task and its subtasks by all users.
2024-08-06 11:34:18 +03:00
fn total_time_tracked ( & self , id : EventId ) -> u64 {
let mut total = 0 ;
2024-08-12 23:07:39 +03:00
let children = self . get_task_tree ( & id ) . get_all ( ) ;
2024-08-06 11:34:18 +03:00
for user in self . history . values ( ) {
2024-08-08 00:18:34 +03:00
total + = TimesTracked ::from ( user , & children ) . into_iter ( ) . sum ::< Duration > ( ) . as_secs ( ) ;
2024-07-31 20:05:52 +03:00
}
total
}
2024-07-30 17:11:43 +03:00
fn total_progress ( & self , id : & EventId ) -> Option < f32 > {
2024-07-30 17:13:29 +03:00
self . get_by_id ( id ) . and_then ( | t | match t . pure_state ( ) {
2024-07-30 17:11:43 +03:00
State ::Closed = > None ,
State ::Done = > Some ( 1.0 ) ,
_ = > {
2024-08-01 20:40:55 +03:00
let mut sum = 0 f32 ;
let mut count = 0 ;
for prog in t . children . iter ( ) . filter_map ( | e | self . total_progress ( e ) ) {
sum + = prog ;
count + = 1 ;
}
2024-07-30 17:11:43 +03:00
Some (
2024-08-01 20:40:55 +03:00
if count > 0 {
sum / ( count as f32 )
} else {
0.0
}
2024-07-30 17:11:43 +03:00
)
}
} )
}
2024-07-25 22:10:01 +03:00
// Parents
2024-08-01 14:07:40 +03:00
pub ( crate ) fn get_parent ( & self , id : Option < EventId > ) -> Option < & EventId > {
2024-07-30 17:13:29 +03:00
id . and_then ( | id | self . get_by_id ( & id ) )
2024-07-25 22:10:01 +03:00
. and_then ( | t | t . parent_id ( ) )
}
2024-07-26 21:45:29 +03:00
pub ( crate ) fn get_prompt_suffix ( & self ) -> String {
2024-08-11 12:28:08 +03:00
self . tags . iter ( )
2024-07-26 21:45:29 +03:00
. map ( | t | format! ( " # {} " , t . content ( ) . unwrap ( ) ) )
2024-08-11 12:28:08 +03:00
. chain ( self . tags_excluded . iter ( )
. map ( | t | format! ( " -# {} " , t . content ( ) . unwrap ( ) ) ) )
2024-08-10 15:44:52 +03:00
. chain ( once ( self . state . indicator ( ) ) )
2024-07-26 21:45:29 +03:00
. join ( " " )
}
pub ( crate ) fn get_task_path ( & self , id : Option < EventId > ) -> String {
2024-07-29 21:27:50 +03:00
join_tasks ( self . traverse_up_from ( id ) , true )
2024-07-29 13:32:47 +03:00
. filter ( | s | ! s . is_empty ( ) )
. or_else ( | | id . map ( | id | id . to_string ( ) ) )
. unwrap_or ( String ::new ( ) )
2024-07-25 22:10:01 +03:00
}
2024-08-07 00:06:09 +03:00
fn traverse_up_from ( & self , id : Option < EventId > ) -> ParentIterator {
2024-07-25 22:10:01 +03:00
ParentIterator {
tasks : & self . tasks ,
current : id ,
prev : None ,
}
2024-07-19 09:35:03 +03:00
}
2024-07-25 10:55:29 +03:00
2024-07-29 21:27:50 +03:00
fn relative_path ( & self , id : EventId ) -> String {
join_tasks (
self . traverse_up_from ( Some ( id ) )
. take_while ( | t | Some ( t . event . id ) ! = self . position ) ,
false ,
2024-07-31 20:08:33 +03:00
) . unwrap_or ( id . to_string ( ) )
2024-07-29 21:27:50 +03:00
}
2024-07-25 22:10:01 +03:00
// Helpers
2024-08-16 09:45:35 +03:00
fn resolve_tasks < ' a > ( & ' a self , iter : impl Iterator < Item = & ' a EventId > ) -> impl Iterator < Item = & ' a Task > {
2024-07-25 00:26:29 +03:00
self . resolve_tasks_rec ( iter , self . depth )
}
2024-07-25 10:55:29 +03:00
fn resolve_tasks_rec < ' a > (
2024-08-16 09:45:35 +03:00
& ' a self ,
iter : impl Iterator < Item = & ' a EventId > ,
2024-07-25 10:55:29 +03:00
depth : i8 ,
2024-08-16 09:45:35 +03:00
) -> Box < impl Iterator < Item = & ' a Task > > {
iter . filter_map ( | id | self . get_by_id ( & id ) )
. flat_map ( move | task | {
2024-07-25 10:55:29 +03:00
let new_depth = depth - 1 ;
2024-08-16 09:45:35 +03:00
if new_depth = = 0 {
vec! [ task ]
} else {
let tasks_iter = self . resolve_tasks_rec ( task . children . iter ( ) , new_depth ) ;
if new_depth < 0 {
let tasks : Vec < & Task > = tasks_iter . collect ( ) ;
if tasks . is_empty ( ) {
vec! [ task ]
} else {
tasks
}
2024-07-25 10:55:29 +03:00
} else {
2024-08-16 09:45:35 +03:00
tasks_iter . chain ( once ( task ) ) . collect ( )
2024-07-25 10:55:29 +03:00
}
2024-07-25 00:26:29 +03:00
}
2024-07-25 10:55:29 +03:00
} )
2024-08-16 09:45:35 +03:00
. into ( )
2024-07-24 21:11:36 +03:00
}
2024-07-25 00:26:29 +03:00
2024-08-18 22:24:14 +03:00
/// Executes the given function with each task referenced by this event without marker.
2024-08-18 21:33:04 +03:00
/// Returns true if any task was found.
pub ( crate ) fn referenced_tasks < F : Fn ( & mut Task ) > ( & mut self , event : & Event , f : F ) -> bool {
let mut found = false ;
2024-07-25 22:10:01 +03:00
for tag in event . tags . iter ( ) {
2024-08-18 22:24:14 +03:00
if let Some ( TagStandard ::Event { event_id , marker , .. } ) = tag . as_standardized ( ) {
if marker . is_none ( ) {
self . tasks . get_mut ( event_id ) . map ( | t | {
found = true ;
f ( t )
} ) ;
}
2024-07-25 22:10:01 +03:00
}
}
2024-08-18 21:33:04 +03:00
found
2024-07-25 22:10:01 +03:00
}
2024-07-30 17:13:29 +03:00
#[ inline ]
2024-08-08 00:18:34 +03:00
pub ( crate ) fn get_current_task ( & self ) -> Option < & Task > {
2024-07-30 17:13:29 +03:00
self . position . and_then ( | id | self . get_by_id ( & id ) )
2024-07-30 08:23:32 +03:00
}
2024-08-06 11:34:18 +03:00
2024-08-14 15:56:40 +03:00
pub ( crate ) fn children_of ( & self , id : Option < EventId > ) -> impl Iterator < Item = & EventId > + '_ {
2024-08-01 21:40:15 +03:00
self . tasks
. values ( )
. filter ( move | t | t . parent_id ( ) = = id . as_ref ( ) )
. map ( | t | t . get_id ( ) )
}
2024-07-30 08:23:32 +03:00
2024-08-15 10:16:40 +03:00
pub ( crate ) fn filtered_tasks ( & self , position : Option < EventId > ) -> impl Iterator < Item = & Task > {
2024-08-16 09:45:35 +03:00
// TODO use ChildIterator
self . resolve_tasks ( self . children_of ( position ) )
2024-07-31 20:08:33 +03:00
. filter ( | t | {
2024-08-01 21:40:15 +03:00
// TODO apply filters in transit
2024-08-11 12:28:08 +03:00
self . state . matches ( t ) & &
t . tags . as_ref ( ) . map_or ( true , | tags | {
tags . iter ( ) . find ( | tag | self . tags_excluded . contains ( tag ) ) . is_none ( )
} ) & &
( self . tags . is_empty ( ) | |
t . tags . as_ref ( ) . map_or ( false , | tags | {
let mut iter = tags . iter ( ) ;
self . tags . iter ( ) . all ( | tag | iter . any ( | t | t = = tag ) )
} ) )
2024-07-31 20:08:33 +03:00
} )
2024-08-15 10:16:40 +03:00
}
pub ( crate ) fn visible_tasks ( & self ) -> Vec < & Task > {
if self . depth = = 0 {
return self . get_current_task ( ) . into_iter ( ) . collect ( ) ;
}
if self . view . len ( ) > 0 {
2024-08-16 09:45:35 +03:00
return self . resolve_tasks ( self . view . iter ( ) ) . collect ( ) ;
2024-08-15 10:16:40 +03:00
}
self . filtered_tasks ( self . position ) . collect ( )
2024-07-19 09:35:03 +03:00
}
2024-07-19 01:15:11 +03:00
2024-07-30 08:23:32 +03:00
pub ( crate ) fn print_tasks ( & self ) -> Result < ( ) , Error > {
let mut lock = stdout ( ) . lock ( ) ;
2024-08-08 00:18:34 +03:00
if let Some ( t ) = self . get_current_task ( ) {
2024-08-01 14:07:40 +03:00
let state = t . state_or_default ( ) ;
2024-08-14 22:12:43 +03:00
let now = & Timestamp ::now ( ) ;
let mut tracking_stamp : Option < Timestamp > = None ;
for elem in
timestamps ( self . history . get ( & self . sender . pubkey ( ) ) . into_iter ( ) . flatten ( ) , & vec! [ t . get_id ( ) ] )
. map ( | ( e , o ) | e ) {
if tracking_stamp . is_some ( ) & & elem > now {
break ;
}
tracking_stamp = Some ( elem . clone ( ) )
}
2024-08-01 14:07:40 +03:00
writeln! (
lock ,
2024-08-14 22:12:43 +03:00
" Tracking since {} (total tracked time {}m) - {} since {} " ,
tracking_stamp . map_or ( " ? " . to_string ( ) , | t | relative_datetimestamp ( & t ) ) ,
self . time_tracked ( * t . get_id ( ) ) / 60 ,
2024-08-01 14:07:40 +03:00
state . get_label ( ) ,
2024-08-14 22:12:43 +03:00
relative_datetimestamp ( & state . time )
2024-08-01 14:07:40 +03:00
) ? ;
2024-07-30 08:23:32 +03:00
writeln! ( lock , " {} " , t . descriptions ( ) . join ( " \n " ) ) ? ;
}
2024-08-07 15:04:18 +03:00
// TODO proper column alignment
2024-08-11 10:01:46 +03:00
// TODO hide empty columns
2024-07-30 09:00:39 +03:00
writeln! ( lock , " {} " , self . properties . join ( " \t " ) . bold ( ) ) ? ;
2024-08-08 15:14:04 +03:00
let mut total_time = 0 ;
2024-08-15 10:16:40 +03:00
let mut tasks = self . visible_tasks ( ) ;
2024-08-13 11:59:57 +03:00
let count = tasks . len ( ) ;
2024-08-11 10:01:46 +03:00
tasks . sort_by_cached_key ( | task | {
self . sorting
. iter ( )
. map ( | p | self . get_property ( task , p . as_str ( ) ) )
. collect_vec ( )
} ) ;
for task in tasks {
2024-07-30 08:23:32 +03:00
writeln! (
lock ,
2024-07-19 09:35:03 +03:00
" {} " ,
2024-08-13 11:59:57 +03:00
self . properties . iter ( )
2024-08-11 10:01:46 +03:00
. map ( | p | self . get_property ( task , p . as_str ( ) ) )
2024-07-29 16:20:43 +03:00
. join ( " \t " )
2024-07-30 08:23:32 +03:00
) ? ;
2024-08-13 11:59:57 +03:00
if self . depth < 2 | | task . parent_id ( ) = = self . position . as_ref ( ) {
total_time + = self . total_time_tracked ( task . event . id )
}
2024-07-19 09:35:03 +03:00
}
2024-08-08 15:14:04 +03:00
if total_time > 0 {
2024-08-13 11:59:57 +03:00
writeln! ( lock , " {} visible tasks{} " , count , display_time ( " tracked a total of HHhMMm " , total_time ) ) ? ;
2024-08-08 15:14:04 +03:00
}
2024-07-30 08:23:32 +03:00
Ok ( ( ) )
2024-07-19 09:35:03 +03:00
}
2024-07-19 21:06:03 +03:00
2024-08-11 10:01:46 +03:00
fn get_property ( & self , task : & Task , str : & str ) -> String {
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 ) ) ;
match str {
" subtasks " = > {
let mut total = 0 ;
let mut done = 0 ;
for subtask in task . children . iter ( ) . filter_map ( | id | self . get_by_id ( id ) ) {
let state = subtask . pure_state ( ) ;
total + = & ( state ! = State ::Closed ) . into ( ) ;
done + = & ( state = = State ::Done ) . into ( ) ;
}
if total > 0 {
format! ( " {done} / {total} " )
} else {
" " . to_string ( )
}
}
" 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 ( ) ;
}
2024-08-18 22:43:14 +03:00
let state = task . pure_state ( ) ;
if state . is_open ( ) & & progress . is_some_and ( | p | p > 0.1 ) {
state . colorize ( & prog_string )
2024-08-11 10:01:46 +03:00
} else {
2024-08-18 22:43:14 +03:00
task . state_label ( ) . unwrap_or_default ( )
2024-08-11 10:01:46 +03:00
} . to_string ( )
}
" progress " = > prog_string . clone ( ) ,
2024-08-18 21:54:05 +03:00
" author " = > self . get_author ( & task . event . pubkey ) ,
2024-08-11 10:01:46 +03:00
" path " = > self . get_task_path ( Some ( task . event . id ) ) ,
" rpath " = > self . relative_path ( task . event . id ) ,
// TODO format strings configurable
" time " = > display_time ( " MMMm " , self . time_tracked ( * task . get_id ( ) ) ) ,
" rtime " = > display_time ( " HH:MM " , self . total_time_tracked ( * task . get_id ( ) ) ) ,
prop = > task . get ( prop ) . unwrap_or ( String ::new ( ) ) ,
}
}
2024-08-18 21:33:04 +03:00
pub ( crate ) fn get_author ( & self , pubkey : & PublicKey ) -> String {
self . users . get ( pubkey )
. and_then ( | m | m . name . clone ( ) )
. unwrap_or_else ( | | pubkey . to_string ( ) )
}
2024-07-25 22:10:01 +03:00
// Movement and Selection
pub ( crate ) fn set_filter ( & mut self , view : Vec < EventId > ) {
2024-08-15 10:33:52 +03:00
if view . is_empty ( ) {
warn! ( " No match for filter! " )
}
2024-07-26 21:45:29 +03:00
self . view = view ;
2024-07-25 22:10:01 +03:00
}
2024-08-08 00:18:34 +03:00
pub ( crate ) fn clear_filter ( & mut self ) {
self . view . clear ( ) ;
self . tags . clear ( ) ;
2024-08-11 12:28:08 +03:00
self . tags_excluded . clear ( ) ;
2024-08-08 00:18:34 +03:00
info! ( " Removed all filters " ) ;
}
2024-08-14 19:42:58 +03:00
pub ( crate ) fn set_tags ( & mut self , tags : impl IntoIterator < Item = Tag > ) {
2024-08-11 12:28:08 +03:00
self . tags_excluded . clear ( ) ;
2024-08-08 13:04:22 +03:00
self . tags . clear ( ) ;
2024-08-14 19:42:58 +03:00
self . tags . extend ( tags ) ;
2024-08-08 13:04:22 +03:00
}
2024-07-25 22:40:35 +03:00
pub ( crate ) fn add_tag ( & mut self , tag : String ) {
self . view . clear ( ) ;
2024-08-08 00:18:34 +03:00
info! ( " Added tag filter for #{tag} " ) ;
2024-08-11 12:28:08 +03:00
let tag : Tag = Hashtag ( tag ) . into ( ) ;
self . tags_excluded . remove ( & tag ) ;
self . tags . insert ( tag ) ;
2024-07-25 22:40:35 +03:00
}
2024-08-08 00:18:34 +03:00
pub ( crate ) fn remove_tag ( & mut self , tag : & str ) {
2024-08-01 20:12:04 +03:00
self . view . clear ( ) ;
2024-08-08 00:18:34 +03:00
let len = self . tags . len ( ) ;
self . tags . retain ( | t | ! t . content ( ) . is_some_and ( | value | value . to_string ( ) . starts_with ( tag ) ) ) ;
if self . tags . len ( ) < len {
info! ( " Removed tag filters starting with {tag} " ) ;
} else {
2024-08-11 12:28:08 +03:00
self . tags_excluded . insert ( Hashtag ( tag . to_string ( ) ) . into ( ) ) ;
info! ( " Excluding #{tag} from view " ) ;
2024-08-08 00:18:34 +03:00
}
2024-08-01 20:12:04 +03:00
}
2024-08-10 15:44:52 +03:00
pub ( crate ) fn set_state_filter ( & mut self , state : StateFilter ) {
2024-07-26 21:45:29 +03:00
self . view . clear ( ) ;
2024-08-10 15:44:52 +03:00
info! ( " Filtering for {} " , state ) ;
2024-07-26 21:45:29 +03:00
self . state = state ;
}
2024-07-25 22:10:01 +03:00
pub ( crate ) fn move_up ( & mut self ) {
2024-08-08 00:18:34 +03:00
self . move_to ( self . get_current_task ( ) . and_then ( | t | t . parent_id ( ) ) . cloned ( ) ) ;
2024-08-01 14:07:40 +03:00
}
pub ( crate ) fn flush ( & self ) {
self . sender . flush ( ) ;
2024-07-24 16:03:34 +03:00
}
2024-08-06 11:34:18 +03:00
2024-08-18 21:37:39 +03:00
/// Returns ids of tasks starting with the given string.
2024-08-01 21:40:15 +03:00
pub ( crate ) fn get_filtered ( & self , arg : & str ) -> Vec < EventId > {
2024-08-01 21:11:33 +03:00
if let Ok ( id ) = EventId ::parse ( arg ) {
2024-08-01 21:40:15 +03:00
return vec! [ id ] ;
2024-08-01 21:11:33 +03:00
}
2024-08-15 10:16:40 +03:00
let mut filtered : Vec < EventId > = Vec ::with_capacity ( 32 ) ;
2024-08-01 21:11:33 +03:00
let lowercase_arg = arg . to_ascii_lowercase ( ) ;
2024-08-15 10:16:40 +03:00
let mut filtered_more : Vec < EventId > = Vec ::with_capacity ( 32 ) ;
for task in self . filtered_tasks ( self . position ) {
2024-08-01 21:11:33 +03:00
let lowercase = task . event . content . to_ascii_lowercase ( ) ;
if lowercase = = lowercase_arg {
2024-08-06 11:34:18 +03:00
return vec! [ task . event . id ] ;
2024-08-01 21:11:33 +03:00
} else if task . event . content . starts_with ( arg ) {
filtered . push ( task . event . id )
} else if lowercase . starts_with ( & lowercase_arg ) {
filtered_more . push ( task . event . id )
}
}
2024-08-01 21:40:15 +03:00
if filtered . len ( ) = = 0 {
2024-08-06 11:34:18 +03:00
return filtered_more ;
2024-08-01 21:11:33 +03:00
}
2024-08-06 11:34:18 +03:00
return filtered ;
2024-08-01 21:40:15 +03:00
}
/// 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 , arg : & str ) -> Option < EventId > {
let filtered = self . get_filtered ( arg ) ;
2024-08-01 21:11:33 +03:00
match filtered . len ( ) {
0 = > {
// No match, new task
self . view . clear ( ) ;
2024-08-08 13:04:52 +03:00
if arg . len ( ) > 2 {
Some ( self . make_task ( arg ) )
} else {
2024-08-15 10:33:52 +03:00
warn! ( " Name of a task needs to have at least 3 characters " ) ;
2024-08-08 13:04:52 +03:00
None
}
2024-08-01 21:11:33 +03:00
}
1 = > {
// One match, activate
self . move_to ( filtered . into_iter ( ) . nth ( 0 ) ) ;
None
}
_ = > {
// Multiple match, filter
self . set_filter ( filtered ) ;
None
}
}
}
2024-07-25 22:10:01 +03:00
pub ( crate ) fn move_to ( & mut self , id : Option < EventId > ) {
self . view . clear ( ) ;
if id = = self . position {
2024-08-01 20:00:45 +03:00
debug! ( " Flushing Tasks because of move in place " ) ;
2024-08-01 14:07:40 +03:00
self . flush ( ) ;
2024-07-25 22:10:01 +03:00
return ;
}
2024-08-02 14:43:39 +03:00
self . submit ( build_tracking ( id ) ) ;
2024-08-01 14:07:40 +03:00
if ! id . and_then ( | id | self . tasks . get ( & id ) ) . is_some_and ( | t | t . parent_id ( ) = = self . position . as_ref ( ) ) {
2024-08-06 11:34:18 +03:00
debug! ( " Flushing Tasks because of move beyond child " ) ;
2024-08-01 14:07:40 +03:00
self . flush ( ) ;
}
2024-08-01 20:00:45 +03:00
self . position = id ;
2024-07-25 22:10:01 +03:00
}
// Updates
2024-08-12 23:02:50 +03:00
pub ( crate ) fn make_event_tag_from_id ( & self , id : EventId , marker : & str ) -> Tag {
Tag ::from ( TagStandard ::Event {
event_id : id ,
relay_url : self . sender . url . as_ref ( ) . map ( | url | UncheckedUrl ::new ( url . as_str ( ) ) ) ,
marker : Some ( Marker ::Custom ( marker . to_string ( ) ) ) ,
public_key : self . get_by_id ( & id ) . map ( | e | e . event . pubkey ) ,
} )
}
pub ( crate ) fn make_event_tag ( & self , event : & Event , marker : & str ) -> Tag {
Tag ::from ( TagStandard ::Event {
event_id : event . id ,
relay_url : self . sender . url . as_ref ( ) . map ( | url | UncheckedUrl ::new ( url . as_str ( ) ) ) ,
marker : Some ( Marker ::Custom ( marker . to_string ( ) ) ) ,
public_key : Some ( event . pubkey ) ,
} )
}
pub ( crate ) fn parent_tag ( & self ) -> Option < Tag > {
self . position . map ( | p | self . make_event_tag_from_id ( p , MARKER_PARENT ) )
}
pub ( crate ) fn position_tags ( & self ) -> Vec < Tag > {
let mut tags = Vec ::with_capacity ( 2 ) ;
self . parent_tag ( ) . map ( | t | tags . push ( t ) ) ;
self . get_current_task ( )
. map ( | t | {
2024-08-08 21:10:17 +03:00
if t . pure_state ( ) = = State ::Procedure {
t . children . iter ( )
. filter_map ( | id | self . get_by_id ( id ) )
. max ( )
2024-08-12 23:02:50 +03:00
. map ( | t | tags . push ( self . make_event_tag ( & t . event , MARKER_DEPENDS ) ) ) ;
2024-08-08 21:10:17 +03:00
}
} ) ;
2024-08-12 23:02:50 +03:00
tags
}
/// Creates a task following the current state
/// Sanitizes input
pub ( crate ) fn make_task ( & mut self , input : & str ) -> EventId {
2024-08-18 22:24:14 +03:00
self . make_task_with ( input , empty ( ) , true )
2024-08-12 23:02:50 +03:00
}
pub ( crate ) fn make_task_and_enter ( & mut self , input : & str , state : State ) {
2024-08-18 22:24:14 +03:00
let id = self . make_task_with ( input , empty ( ) , false ) ;
2024-08-12 23:02:50 +03:00
self . set_state_for ( id , " " , state ) ;
self . move_to ( Some ( id ) ) ;
}
2024-08-18 22:24:14 +03:00
/// Creates a task with tags from filter and position
2024-08-12 23:02:50 +03:00
/// Sanitizes input
pub ( crate ) fn make_task_with ( & mut self , input : & str , tags : impl IntoIterator < Item = Tag > , set_state : bool ) -> EventId {
2024-08-18 22:24:14 +03:00
let ( input , input_tags ) = extract_tags ( input . trim ( ) ) ;
2024-08-08 21:10:17 +03:00
let id = self . submit (
2024-08-18 22:24:14 +03:00
build_task ( input , input_tags , None )
. add_tags ( self . tags . iter ( ) . cloned ( ) )
. add_tags ( self . position_tags ( ) )
2024-08-12 23:02:50 +03:00
. add_tags ( tags . into_iter ( ) )
2024-08-08 21:10:17 +03:00
) ;
2024-08-12 23:02:50 +03:00
if set_state {
self . state . as_option ( ) . inspect ( | s | self . set_state_for_with ( id , s ) ) ;
}
2024-08-07 23:59:05 +03:00
id
2024-08-01 14:07:40 +03:00
}
2024-08-14 15:56:40 +03:00
pub ( crate ) fn get_task_title ( & self , id : & EventId ) -> String {
2024-08-06 17:52:20 +03:00
self . tasks . get ( id ) . map_or ( id . to_string ( ) , | t | t . get_title ( ) )
}
2024-08-10 20:48:57 +03:00
/// Parse string and set tracking
2024-08-15 12:21:21 +03:00
/// Returns false and prints a message if parsing failed
2024-08-10 20:48:57 +03:00
pub ( crate ) fn track_from ( & mut self , str : & str ) -> bool {
2024-08-15 12:21:21 +03:00
// Using two libraries for better exhaustiveness, see https://github.com/uutils/parse_datetime/issues/84
2024-08-15 13:16:14 +03:00
let stripped = str . trim ( ) . trim_start_matches ( '+' ) . trim_start_matches ( " in " ) ;
if let Ok ( num ) = stripped . parse ::< i64 > ( ) {
self . track_at ( Timestamp ::from ( Timestamp ::now ( ) . as_u64 ( ) . saturating_add_signed ( num * 60 ) ) ) ;
2024-08-18 21:54:05 +03:00
return true ;
2024-08-15 13:16:14 +03:00
}
match interim ::parse_date_string ( stripped , Local ::now ( ) , interim ::Dialect ::Us ) {
2024-08-15 12:21:21 +03:00
Ok ( date ) = > Some ( date . to_utc ( ) ) ,
Err ( e ) = > {
2024-08-15 13:16:14 +03:00
match parse_datetime ::parse_datetime_at_date ( Local ::now ( ) , stripped ) {
2024-08-15 12:21:21 +03:00
Ok ( date ) = > Some ( date . to_utc ( ) ) ,
Err ( _ ) = > {
warn! ( " Could not parse time from {str}: {e} " ) ;
None
}
}
}
2024-08-15 13:12:42 +03:00
} . filter ( | time | {
if time . timestamp ( ) > 0 {
self . track_at ( Timestamp ::from ( time . timestamp ( ) as u64 ) ) ;
true
} else {
warn! ( " Can only track times after 1970! " ) ;
false
}
} ) . is_some ( )
2024-08-10 20:48:57 +03:00
}
2024-08-02 20:40:42 +03:00
pub ( crate ) fn track_at ( & mut self , time : Timestamp ) -> EventId {
2024-08-14 21:49:36 +03:00
info! ( " {} from {} " , self . position . map_or ( String ::from ( " Stopping time-tracking " ) , | id | format! ( " Tracking \" {} \" " , self . get_task_title ( & id ) ) ) , relative_datetimestamp ( & time ) ) ;
2024-08-06 17:52:20 +03:00
let pos = self . get_position ( ) ;
let tracking = build_tracking ( pos ) ;
2024-08-19 11:27:12 +03:00
// TODO this can lead to funny omittals
2024-08-06 17:52:20 +03:00
self . get_own_history ( ) . map ( | events | {
if let Some ( event ) = events . pop_last ( ) {
if event . kind . as_u16 ( ) = = TRACKING_KIND & &
( pos = = None & & event . tags . is_empty ( ) ) | |
event . tags . iter ( ) . all ( | t | t . content ( ) . map ( | str | str . to_string ( ) ) = = pos . map ( | id | id . to_string ( ) ) ) {
// Replace last for easier calculation
} else {
events . insert ( event ) ;
}
}
} ) ;
self . submit ( tracking . custom_created_at ( time ) )
2024-08-02 20:40:42 +03:00
}
2024-08-14 15:59:43 +03:00
/// Sign and queue the event to the relay, returning its id
2024-08-01 14:07:40 +03:00
fn submit ( & mut self , builder : EventBuilder ) -> EventId {
2024-08-01 20:00:45 +03:00
let event = self . sender . submit ( builder ) . unwrap ( ) ;
2024-08-01 14:07:40 +03:00
let id = event . id ;
self . add ( event ) ;
id
2024-07-19 16:49:23 +03:00
}
2024-07-19 21:04:21 +03:00
pub ( crate ) fn add ( & mut self , event : Event ) {
2024-08-06 11:34:18 +03:00
match event . kind . as_u16 ( ) {
2024-07-31 20:05:52 +03:00
TASK_KIND = > self . add_task ( event ) ,
TRACKING_KIND = >
match self . history . get_mut ( & event . pubkey ) {
Some ( c ) = > { c . insert ( event ) ; }
None = > { self . history . insert ( event . pubkey , BTreeSet ::from ( [ event ] ) ) ; }
} ,
2024-08-18 21:33:04 +03:00
METADATA_KIND = >
match Metadata ::from_json ( event . content ( ) ) {
Ok ( metadata ) = > { self . users . insert ( event . pubkey , metadata ) ; }
Err ( e ) = > warn! ( " Cannot parse metadata: {} from {:?} " , e , event )
}
2024-08-18 22:24:14 +03:00
_ = > self . add_prop ( event ) ,
2024-07-19 21:04:21 +03:00
}
}
2024-07-19 16:49:23 +03:00
pub ( crate ) fn add_task ( & mut self , event : Event ) {
2024-07-24 16:03:34 +03:00
if self . tasks . contains_key ( & event . id ) {
2024-08-08 21:10:17 +03:00
debug! ( " Did not insert duplicate event {} " , event . id ) ; // TODO warn in next sdk version
2024-07-24 16:03:34 +03:00
} else {
2024-08-12 23:02:50 +03:00
let id = event . id ;
let task = Task ::new ( event ) ;
task . find_refs ( MARKER_PARENT ) . for_each ( | parent | {
self . tasks . get_mut ( parent ) . map ( | t | { t . children . insert ( id ) ; } ) ;
} ) ;
self . tasks . insert ( id , task ) ;
2024-07-24 16:03:34 +03:00
}
2024-07-19 01:15:11 +03:00
}
2024-07-25 10:55:29 +03:00
2024-08-18 22:24:14 +03:00
fn add_prop ( & mut self , event : Event ) {
let found = self . referenced_tasks ( & event , | t | {
2024-07-25 10:55:29 +03:00
t . props . insert ( event . clone ( ) ) ;
} ) ;
2024-08-18 22:24:14 +03:00
if ! found {
if event . kind . as_u16 ( ) = = NOTE_KIND {
self . add_task ( event ) ;
return ;
}
warn! ( " Unknown event {:?} " , event )
}
2024-07-19 21:04:21 +03:00
}
2024-07-19 01:15:11 +03:00
2024-08-06 17:52:20 +03:00
fn get_own_history ( & mut self ) -> Option < & mut BTreeSet < Event > > {
self . history . get_mut ( & self . sender . pubkey ( ) )
}
2024-08-01 14:07:40 +03:00
pub ( crate ) fn undo ( & mut self ) {
2024-08-06 22:11:34 +03:00
let mut count = 0 ;
2024-08-01 14:07:40 +03:00
self . sender . clear ( ) . into_iter ( ) . rev ( ) . for_each ( | event | {
2024-08-06 22:11:34 +03:00
count + = 1 ;
2024-08-01 14:07:40 +03:00
self . remove ( & event )
} ) ;
2024-08-06 22:11:34 +03:00
info! ( " Reverted last {count} actions! " )
2024-07-26 21:45:29 +03:00
}
2024-08-01 14:07:40 +03:00
fn remove ( & mut self , event : & Event ) {
2024-08-01 20:00:45 +03:00
if let Some ( pos ) = self . position {
if pos = = event . id {
self . move_up ( )
}
}
2024-08-01 14:07:40 +03:00
self . tasks . remove ( & event . id ) ;
2024-08-06 17:52:20 +03:00
self . get_own_history ( ) . map ( | t | t . remove ( event ) ) ;
2024-08-01 14:07:40 +03:00
self . referenced_tasks ( event , | t | { t . props . remove ( event ) ; } ) ;
}
2024-08-07 23:59:05 +03:00
pub ( crate ) fn set_state_for_with ( & mut self , id : EventId , comment : & str ) {
2024-08-10 18:16:21 +03:00
self . set_state_for ( id , comment , comment . into ( ) ) ;
2024-08-07 23:59:05 +03:00
}
2024-08-08 13:52:02 +03:00
2024-08-01 14:07:40 +03:00
pub ( crate ) fn set_state_for ( & mut self , id : EventId , comment : & str , state : State ) -> EventId {
2024-08-18 22:24:14 +03:00
let prop = build_prop (
2024-08-01 14:07:40 +03:00
state . into ( ) ,
comment ,
id ,
) ;
2024-08-06 22:11:34 +03:00
info! ( " Task status {} set for \" {} \" " , TaskState ::get_label_for ( & state , comment ) , self . get_task_title ( & id ) ) ;
2024-08-01 14:07:40 +03:00
self . submit ( prop )
2024-07-19 01:15:11 +03:00
}
2024-08-02 14:31:00 +03:00
pub ( crate ) fn update_state ( & mut self , comment : & str , state : State ) {
2024-07-25 10:50:53 +03:00
self . position
2024-08-01 14:07:40 +03:00
. map ( | id | self . set_state_for ( id , comment , state ) ) ;
2024-07-25 10:50:53 +03:00
}
2024-08-02 14:43:39 +03:00
pub ( crate ) fn make_note ( & mut self , note : & str ) {
2024-08-18 22:24:14 +03:00
if let Some ( id ) = self . position {
if self . get_by_id ( & id ) . is_some_and ( | t | t . is_task ( ) ) {
let prop = build_prop ( Kind ::TextNote , note . trim ( ) , id ) ;
2024-08-01 19:18:46 +03:00
self . submit ( prop ) ;
2024-08-18 22:24:14 +03:00
return ;
2024-07-25 10:50:53 +03:00
}
}
2024-08-18 22:24:14 +03:00
let ( input , tags ) = extract_tags ( note . trim ( ) ) ;
self . submit (
build_task ( input , tags , Some ( ( " stateless " , Kind ::TextNote ) ) )
. add_tags ( self . parent_tag ( ) )
. add_tags ( self . tags . iter ( ) . cloned ( ) )
) ;
2024-07-19 01:15:11 +03:00
}
2024-08-08 00:18:34 +03:00
2024-08-07 00:06:09 +03:00
// Properties
pub ( crate ) fn set_depth ( & mut self , depth : i8 ) {
self . depth = depth ;
info! ( " Changed view depth to {depth} " ) ;
}
2024-08-08 00:18:34 +03:00
2024-08-11 10:58:34 +03:00
pub ( crate ) fn get_columns ( & mut self ) -> & mut Vec < String > {
& mut self . properties
}
2024-08-11 12:05:29 +03:00
pub ( crate ) fn set_sorting ( & mut self , vec : VecDeque < String > ) {
self . sorting = vec ;
info! ( " Now sorting by {:?} " , self . sorting ) ;
}
2024-08-14 15:59:43 +03:00
2024-08-11 10:58:34 +03:00
pub ( crate ) fn add_sorting_property ( & mut self , property : String ) {
2024-08-11 12:05:29 +03:00
// TODO reverse order if already present
2024-08-11 10:58:34 +03:00
self . sorting . push_front ( property ) ;
self . sorting . truncate ( 4 ) ;
info! ( " Now sorting by {:?} " , self . sorting ) ;
}
}
pub trait PropertyCollection < T > {
fn remove_at ( & mut self , index : usize ) ;
fn add_or_remove ( & mut self , value : T ) ;
fn add_or_remove_at ( & mut self , value : T , index : usize ) ;
}
2024-08-14 15:59:43 +03:00
impl < T > PropertyCollection < T > for Vec < T >
where
T : Display + Eq + Clone ,
{
fn remove_at ( & mut self , index : usize ) {
2024-08-11 10:58:34 +03:00
let col = self . remove ( index ) ;
2024-08-07 00:06:09 +03:00
info! ( " Removed property column \" {col} \" " ) ;
}
2024-08-08 00:18:34 +03:00
2024-08-11 10:58:34 +03:00
fn add_or_remove ( & mut self , property : T ) {
match self . iter ( ) . position ( | s | s = = & property ) {
2024-08-07 00:06:09 +03:00
None = > {
info! ( " Added property column \" {property} \" " ) ;
2024-08-11 10:58:34 +03:00
self . push ( property ) ;
2024-08-07 00:06:09 +03:00
}
Some ( index ) = > {
2024-08-11 10:58:34 +03:00
self . remove_at ( index ) ;
2024-08-07 00:06:09 +03:00
}
}
}
2024-08-11 10:58:34 +03:00
fn add_or_remove_at ( & mut self , property : T , index : usize ) {
if self . get ( index ) = = Some ( & property ) {
self . remove_at ( index ) ;
2024-08-07 00:06:09 +03:00
} else {
info! ( " Added property column \" {property} \" at position {} " , index + 1 ) ;
2024-08-11 10:58:34 +03:00
self . insert ( index , property ) ;
2024-08-07 00:06:09 +03:00
}
}
2024-07-19 01:15:11 +03:00
}
2024-08-02 11:13:36 +03:00
/// Formats the given seconds according to the given format.
/// MMM - minutes
/// MM - minutes of the hour
/// HH - hours
/// Returns an empty string if under a minute.
2024-07-31 20:05:52 +03:00
fn display_time ( format : & str , secs : u64 ) -> String {
Some ( secs / 60 )
. filter ( | t | t > & 0 )
. map_or ( String ::new ( ) , | mins | format
2024-08-02 11:13:36 +03:00
. replace ( " MMM " , & format! ( " {:3} " , mins ) )
2024-07-31 20:05:52 +03:00
. replace ( " HH " , & format! ( " {:02} " , mins . div ( 60 ) ) )
2024-08-06 11:34:18 +03:00
. replace ( " MM " , & format! ( " {:02} " , mins . rem ( 60 ) ) ) ,
2024-07-31 20:05:52 +03:00
)
}
2024-07-30 09:02:56 +03:00
pub ( crate ) fn join_tasks < ' a > (
2024-07-31 20:05:52 +03:00
iter : impl Iterator < Item = & ' a Task > ,
2024-07-30 09:02:56 +03:00
include_last_id : bool ,
) -> Option < String > {
2024-07-29 13:32:47 +03:00
let tasks : Vec < & Task > = iter . collect ( ) ;
tasks
. iter ( )
. map ( | t | t . get_title ( ) )
2024-07-30 09:02:56 +03:00
. chain ( if include_last_id {
tasks
. last ( )
. and_then ( | t | t . parent_id ( ) )
. map ( | id | id . to_string ( ) )
. into_iter ( )
} else {
None . into_iter ( )
} )
2024-07-25 10:55:29 +03:00
. fold ( None , | acc , val | {
Some ( acc . map_or_else ( | | val . clone ( ) , | cur | format! ( " {} > {} " , val , cur ) ) )
} )
2024-07-25 00:52:03 +03:00
}
2024-08-12 12:17:03 +03:00
fn matching_tag_id < ' a > ( event : & ' a Event , ids : & ' a Vec < & ' a EventId > ) -> Option < & ' a EventId > {
2024-08-08 15:09:39 +03:00
event . tags . iter ( ) . find_map ( | tag | match tag . as_standardized ( ) {
2024-08-12 12:17:03 +03:00
Some ( TagStandard ::Event { event_id , .. } ) if ids . contains ( & event_id ) = > Some ( event_id ) ,
2024-08-08 15:09:39 +03:00
_ = > None
} )
}
2024-08-14 22:12:43 +03:00
/// Filters out event timestamps to those that start or stop one of the given events
2024-08-12 12:17:03 +03:00
fn timestamps < ' a > ( events : impl Iterator < Item = & ' a Event > , ids : & ' a Vec < & ' a EventId > ) -> impl Iterator < Item = ( & Timestamp , Option < & EventId > ) > {
2024-08-08 15:09:39 +03:00
events . map ( | event | ( & event . created_at , matching_tag_id ( event , ids ) ) )
. dedup_by ( | ( _ , e1 ) , ( _ , e2 ) | e1 = = e2 )
. skip_while ( | element | element . 1 = = None )
}
2024-08-08 00:18:34 +03:00
2024-08-10 21:17:07 +03:00
/// Iterates Events to accumulate times tracked
/// Expects a sorted iterator
2024-08-08 00:18:34 +03:00
struct TimesTracked < ' a > {
events : Box < dyn Iterator < Item = & ' a Event > + ' a > ,
2024-08-12 12:17:03 +03:00
ids : & ' a Vec < & ' a EventId > ,
2024-08-10 21:17:07 +03:00
threshold : Option < Timestamp > ,
2024-08-08 00:18:34 +03:00
}
impl TimesTracked < '_ > {
2024-08-12 12:17:03 +03:00
fn from < ' b > ( events : impl IntoIterator < Item = & ' b Event > + ' b , ids : & ' b Vec < & EventId > ) -> TimesTracked < ' b > {
2024-08-08 00:18:34 +03:00
TimesTracked {
events : Box ::new ( events . into_iter ( ) ) ,
ids ,
2024-08-10 21:17:07 +03:00
threshold : Some ( Timestamp ::now ( ) ) ,
2024-08-08 00:18:34 +03:00
}
}
}
impl Iterator for TimesTracked < '_ > {
type Item = Duration ;
fn next ( & mut self ) -> Option < Self ::Item > {
let mut start : Option < u64 > = None ;
while let Some ( event ) = self . events . next ( ) {
2024-08-08 15:09:39 +03:00
if matching_tag_id ( event , self . ids ) . is_some ( ) {
2024-08-10 21:17:07 +03:00
if self . threshold . is_some_and ( | th | event . created_at > th ) {
continue ;
}
2024-08-08 15:09:39 +03:00
start = start . or ( Some ( event . created_at . as_u64 ( ) ) )
} else {
if let Some ( stamp ) = start {
return Some ( Duration ::from_secs ( event . created_at . as_u64 ( ) - stamp ) ) ;
2024-08-08 00:18:34 +03:00
}
}
}
2024-08-10 21:17:07 +03:00
let now = self . threshold . unwrap_or ( Timestamp ::now ( ) ) . as_u64 ( ) ;
return start . filter ( | t | t < & now ) . map ( | stamp | Duration ::from_secs ( now . saturating_sub ( stamp ) ) ) ;
2024-08-08 00:18:34 +03:00
}
}
2024-08-12 23:07:39 +03:00
/// Breadth-First Iterator over Tasks and recursive children
struct ChildIterator < ' a > {
tasks : & ' a TaskMap ,
queue : Vec < & ' a EventId > ,
index : usize ,
}
impl < ' a > ChildIterator < ' a > {
fn from ( tasks : & ' a TaskMap , id : & ' a EventId ) -> Self {
let mut queue = Vec ::with_capacity ( 30 ) ;
queue . push ( id ) ;
ChildIterator {
tasks ,
queue ,
index : 0 ,
}
}
fn get_all ( mut self ) -> Vec < & ' a EventId > {
while self . next ( ) . is_some ( ) { }
self . queue
}
}
impl < ' a > Iterator for ChildIterator < ' a > {
type Item = & ' a EventId ;
fn next ( & mut self ) -> Option < Self ::Item > {
if self . index > = self . queue . len ( ) {
return None ;
}
let id = self . queue [ self . index ] ;
if let Some ( task ) = self . tasks . get ( & id ) {
self . queue . reserve ( task . children . len ( ) ) ;
self . queue . extend ( task . children . iter ( ) ) ;
} else {
// Unknown task, can still find children
for task in self . tasks . values ( ) {
if task . parent_id ( ) . is_some_and ( | i | i = = id ) {
self . queue . push ( task . get_id ( ) ) ;
}
}
}
self . index + = 1 ;
Some ( id )
}
}
2024-08-08 00:18:34 +03:00
2024-07-19 01:15:11 +03:00
struct ParentIterator < ' a > {
tasks : & ' a TaskMap ,
current : Option < EventId > ,
2024-07-19 16:49:23 +03:00
/// Inexpensive helper to assert correctness
prev : Option < EventId > ,
2024-07-19 01:15:11 +03:00
}
impl < ' a > Iterator for ParentIterator < ' a > {
type Item = & ' a Task ;
fn next ( & mut self ) -> Option < Self ::Item > {
self . current . and_then ( | id | self . tasks . get ( & id ) ) . map ( | t | {
2024-07-19 16:49:23 +03:00
self . prev . map ( | id | assert! ( t . children . contains ( & id ) ) ) ;
self . prev = self . current ;
2024-08-01 14:07:40 +03:00
self . current = t . parent_id ( ) . cloned ( ) ;
2024-07-19 01:15:11 +03:00
t
} )
}
}
2024-07-25 00:26:29 +03:00
2024-08-06 17:52:20 +03:00
#[ cfg(test) ]
mod tasks_test {
2024-08-12 23:02:50 +03:00
use std ::collections ::HashSet ;
2024-08-06 17:52:20 +03:00
use super ::* ;
2024-08-06 11:34:18 +03:00
2024-08-06 17:52:20 +03:00
fn stub_tasks ( ) -> Tasks {
use std ::sync ::mpsc ;
use nostr_sdk ::Keys ;
2024-08-02 14:31:28 +03:00
2024-08-06 17:52:20 +03:00
let ( tx , _rx ) = mpsc ::channel ( ) ;
2024-08-07 15:03:29 +03:00
Tasks ::with_sender ( EventSender {
url : None ,
2024-08-06 17:52:20 +03:00
tx ,
keys : Keys ::generate ( ) ,
queue : Default ::default ( ) ,
} )
}
2024-07-29 13:32:47 +03:00
2024-08-12 23:02:50 +03:00
#[ test ]
fn test_procedures ( ) {
let mut tasks = stub_tasks ( ) ;
tasks . make_task_and_enter ( " proc: tags " , State ::Procedure ) ;
2024-08-18 22:24:14 +03:00
let side = tasks . submit ( build_task ( " side " , vec! [ tasks . make_event_tag ( & tasks . get_current_task ( ) . unwrap ( ) . event , MARKER_DEPENDS ) ] , None ) ) ;
2024-08-12 23:02:50 +03:00
assert_eq! ( tasks . get_current_task ( ) . unwrap ( ) . children , HashSet ::< EventId > ::new ( ) ) ;
2024-08-14 16:00:03 +03:00
let sub_id = tasks . make_task ( " sub " ) ;
assert_eq! ( tasks . get_current_task ( ) . unwrap ( ) . children , HashSet ::from ( [ sub_id ] ) ) ;
assert_eq! ( tasks . len ( ) , 3 ) ;
let sub = tasks . get_by_id ( & sub_id ) . unwrap ( ) ;
2024-08-12 23:02:50 +03:00
assert_eq! ( sub . get_dependendees ( ) , Vec ::< & EventId > ::new ( ) ) ;
}
2024-08-06 17:52:20 +03:00
#[ test ]
fn test_tracking ( ) {
let mut tasks = stub_tasks ( ) ;
2024-08-14 16:00:03 +03:00
let zero = EventId ::all_zeros ( ) ;
2024-08-06 17:52:20 +03:00
tasks . track_at ( Timestamp ::from ( 0 ) ) ;
assert_eq! ( tasks . history . len ( ) , 1 ) ;
tasks . move_to ( Some ( zero ) ) ;
let now : Timestamp = Timestamp ::now ( ) - 2 u64 ;
tasks . track_at ( Timestamp ::from ( 1 ) ) ;
assert! ( tasks . time_tracked ( zero ) > now . as_u64 ( ) ) ;
tasks . move_to ( None ) ;
tasks . track_at ( Timestamp ::from ( 2 ) ) ;
assert_eq! ( tasks . get_own_history ( ) . unwrap ( ) . len ( ) , 3 ) ;
assert_eq! ( tasks . time_tracked ( zero ) , 1 ) ;
2024-08-08 00:18:34 +03:00
2024-08-07 15:04:18 +03:00
// TODO test received events
2024-08-06 17:52:20 +03:00
}
2024-08-10 20:48:57 +03:00
#[ test ]
#[ ignore ]
fn test_timestamps ( ) {
let mut tasks = stub_tasks ( ) ;
let zero = EventId ::all_zeros ( ) ;
2024-08-14 16:00:03 +03:00
2024-08-10 20:48:57 +03:00
tasks . move_to ( Some ( zero ) ) ;
tasks . track_at ( Timestamp ::from ( Timestamp ::now ( ) . as_u64 ( ) + 100 ) ) ;
2024-08-12 12:17:03 +03:00
assert_eq! ( timestamps ( tasks . history . values ( ) . nth ( 0 ) . unwrap ( ) . into_iter ( ) , & vec! [ & zero ] ) . collect_vec ( ) . len ( ) , 2 )
2024-08-14 16:00:03 +03:00
// TODO Does not show both future and current tracking properly, need to split by current time
2024-08-10 20:48:57 +03:00
}
2024-08-06 17:52:20 +03:00
#[ test ]
fn test_depth ( ) {
let mut tasks = stub_tasks ( ) ;
let t1 = tasks . make_task ( " t1 " ) ;
let task1 = tasks . get_by_id ( & t1 ) . unwrap ( ) ;
assert_eq! ( tasks . depth , 1 ) ;
assert_eq! ( task1 . pure_state ( ) , State ::Open ) ;
debug! ( " {:?} " , tasks ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 0 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 0 ) ;
2024-08-06 17:52:20 +03:00
tasks . move_to ( Some ( t1 ) ) ;
tasks . depth = 2 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 0 ) ;
2024-08-06 17:52:20 +03:00
let t2 = tasks . make_task ( " t2 " ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
assert_eq! ( tasks . get_task_path ( Some ( t2 ) ) , " t1>t2 " ) ;
assert_eq! ( tasks . relative_path ( t2 ) , " t2 " ) ;
let t3 = tasks . make_task ( " t3 " ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 2 ) ;
2024-08-06 17:52:20 +03:00
tasks . move_to ( Some ( t2 ) ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 0 ) ;
2024-08-06 17:52:20 +03:00
let t4 = tasks . make_task ( " t4 " ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
assert_eq! ( tasks . get_task_path ( Some ( t4 ) ) , " t1>t2>t4 " ) ;
assert_eq! ( tasks . relative_path ( t4 ) , " t4 " ) ;
tasks . depth = 2 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = - 1 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
tasks . move_to ( Some ( t1 ) ) ;
assert_eq! ( tasks . relative_path ( t4 ) , " t2>t4 " ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 2 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 2 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 3 ) ;
2024-08-06 17:52:20 +03:00
tasks . set_filter ( vec! [ t2 ] ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 2 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 1 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = - 1 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
tasks . set_filter ( vec! [ t2 , t3 ] ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 2 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 2 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 3 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 1 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 2 ) ;
2024-08-06 17:52:20 +03:00
tasks . move_to ( None ) ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 1 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 2 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 3 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 3 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 4 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = 9 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 4 ) ;
2024-08-06 17:52:20 +03:00
tasks . depth = - 1 ;
2024-08-15 10:16:40 +03:00
assert_eq! ( tasks . visible_tasks ( ) . len ( ) , 2 ) ;
2024-08-06 17:52:20 +03:00
}
#[ test ]
fn test_empty_task_title_fallback_to_id ( ) {
let mut tasks = stub_tasks ( ) ;
let empty = tasks . make_task ( " " ) ;
let empty_task = tasks . get_by_id ( & empty ) . unwrap ( ) ;
let empty_id = empty_task . event . id . to_string ( ) ;
assert_eq! ( empty_task . get_title ( ) , empty_id ) ;
assert_eq! ( tasks . get_task_path ( Some ( empty ) ) , empty_id ) ;
}
#[ test ]
fn test_unknown_task ( ) {
let mut tasks = stub_tasks ( ) ;
let zero = EventId ::all_zeros ( ) ;
assert_eq! ( tasks . get_task_path ( Some ( zero ) ) , zero . to_string ( ) ) ;
tasks . move_to ( Some ( zero ) ) ;
let dangling = tasks . make_task ( " test " ) ;
assert_eq! (
tasks . get_task_path ( Some ( dangling ) ) ,
" 0000000000000000000000000000000000000000000000000000000000000000>test "
) ;
assert_eq! ( tasks . relative_path ( dangling ) , " test " ) ;
2024-08-07 15:04:18 +03:00
}
#[ allow(dead_code) ]
fn test_itertools ( ) {
2024-08-06 17:52:20 +03:00
use itertools ::Itertools ;
2024-08-07 15:04:18 +03:00
assert_eq! (
" test toast " . split ( ' ' ) . collect_vec ( ) . len ( ) ,
3
) ;
2024-08-06 17:52:20 +03:00
assert_eq! (
" test toast " . split_ascii_whitespace ( ) . collect_vec ( ) . len ( ) ,
2
) ;
}
}