forked from janek/mostr
		
	feat: properly handle commands without argument
This commit is contained in:
		
					parent
					
						
							
								4180533844
							
						
					
				
			
			
				commit
				
					
						08b0ba48a3
					
				
			
		
					 4 changed files with 157 additions and 118 deletions
				
			
		
							
								
								
									
										16
									
								
								src/kinds.rs
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								src/kinds.rs
									
										
									
									
									
								
							| 
						 | 
					@ -6,6 +6,22 @@ pub const TASK_KIND: u16 = 1621;
 | 
				
			||||||
pub const TRACKING_KIND: u16 = 1650;
 | 
					pub const TRACKING_KIND: u16 = 1650;
 | 
				
			||||||
pub const KINDS: [u16; 7] = [1, TASK_KIND, TRACKING_KIND, 1630, 1631, 1632, 1633];
 | 
					pub const KINDS: [u16; 7] = [1, TASK_KIND, TRACKING_KIND, 1630, 1631, 1632, 1633];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const PROPERTY_COLUMNS: &str = "Available properties:
 | 
				
			||||||
 | 
					- `id`
 | 
				
			||||||
 | 
					- `parentid`
 | 
				
			||||||
 | 
					- `name`
 | 
				
			||||||
 | 
					- `state`
 | 
				
			||||||
 | 
					- `hashtags`
 | 
				
			||||||
 | 
					- `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
 | 
				
			||||||
 | 
					- `path` - name including parent tasks
 | 
				
			||||||
 | 
					- `rpath` - name including parent tasks up to active task
 | 
				
			||||||
 | 
					- `time` - time tracked on this task by you
 | 
				
			||||||
 | 
					- `rtime` - time tracked on this tasks and its subtree by everyone
 | 
				
			||||||
 | 
					- `progress` - recursive subtask completion in percent
 | 
				
			||||||
 | 
					- `subtasks` - how many direct subtasks are complete";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub(crate) fn build_tracking<I>(id: I) -> EventBuilder
 | 
					pub(crate) fn build_tracking<I>(id: I) -> EventBuilder
 | 
				
			||||||
where
 | 
					where
 | 
				
			||||||
    I: IntoIterator<Item=EventId>,
 | 
					    I: IntoIterator<Item=EventId>,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										112
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										112
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
					@ -8,17 +8,17 @@ use std::ops::Sub;
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
use std::str::FromStr;
 | 
					use std::str::FromStr;
 | 
				
			||||||
use std::sync::mpsc;
 | 
					use std::sync::mpsc;
 | 
				
			||||||
use std::sync::mpsc::Sender;
 | 
					use std::sync::mpsc::{Sender};
 | 
				
			||||||
 | 
					use chrono::{DateTime};
 | 
				
			||||||
use chrono::DateTime;
 | 
					 | 
				
			||||||
use colored::Colorize;
 | 
					use colored::Colorize;
 | 
				
			||||||
 | 
					use itertools::Itertools;
 | 
				
			||||||
use log::{debug, error, info, trace, warn};
 | 
					use log::{debug, error, info, trace, warn};
 | 
				
			||||||
use nostr_sdk::prelude::*;
 | 
					use nostr_sdk::prelude::*;
 | 
				
			||||||
use regex::Regex;
 | 
					use regex::Regex;
 | 
				
			||||||
use xdg::BaseDirectories;
 | 
					use xdg::BaseDirectories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::helpers::*;
 | 
					use crate::helpers::*;
 | 
				
			||||||
use crate::kinds::{KINDS, TRACKING_KIND};
 | 
					use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
 | 
				
			||||||
use crate::task::State;
 | 
					use crate::task::State;
 | 
				
			||||||
use crate::tasks::Tasks;
 | 
					use crate::tasks::Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -224,12 +224,10 @@ async fn main() {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    println!();
 | 
					 | 
				
			||||||
    let mut lines = stdin().lines();
 | 
					    let mut lines = stdin().lines();
 | 
				
			||||||
    loop {
 | 
					    loop {
 | 
				
			||||||
 | 
					        println!();
 | 
				
			||||||
        selected_relay.as_ref().and_then(|url| relays.get(url)).inspect(|tasks| {
 | 
					        selected_relay.as_ref().and_then(|url| relays.get(url)).inspect(|tasks| {
 | 
				
			||||||
            or_print(tasks.print_tasks());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            print!(
 | 
					            print!(
 | 
				
			||||||
                "{}",
 | 
					                "{}",
 | 
				
			||||||
                format!(
 | 
					                format!(
 | 
				
			||||||
| 
						 | 
					@ -266,10 +264,11 @@ async fn main() {
 | 
				
			||||||
                let mut iter = input.chars();
 | 
					                let mut iter = input.chars();
 | 
				
			||||||
                let op = iter.next();
 | 
					                let op = iter.next();
 | 
				
			||||||
                let arg = if input.len() > 1 {
 | 
					                let arg = if input.len() > 1 {
 | 
				
			||||||
                    input[1..].trim()
 | 
					                    Some(input[1..].trim())
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    ""
 | 
					                    None
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					                let arg_default = arg.unwrap_or("");
 | 
				
			||||||
                let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
 | 
					                let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
 | 
				
			||||||
                match op {
 | 
					                match op {
 | 
				
			||||||
                    None => {
 | 
					                    None => {
 | 
				
			||||||
| 
						 | 
					@ -277,49 +276,43 @@ async fn main() {
 | 
				
			||||||
                        tasks.flush()
 | 
					                        tasks.flush()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some(':') => match iter.next().and_then(|s| s.to_digit(10)) {
 | 
					                    Some(':') =>
 | 
				
			||||||
                        Some(digit) => {
 | 
					                        if let Some(digit) = iter.next().and_then(|s| s.to_digit(10)) {
 | 
				
			||||||
                            let index = (digit as usize).saturating_sub(1);
 | 
					                            let index = (digit as usize).saturating_sub(1);
 | 
				
			||||||
                            let remaining = iter.collect::<String>().trim().to_string();
 | 
					                            let remaining = iter.collect::<String>().trim().to_string();
 | 
				
			||||||
                            if remaining.is_empty() {
 | 
					                            if remaining.is_empty() {
 | 
				
			||||||
                                tasks.remove_column(index);
 | 
					                                tasks.remove_column(index);
 | 
				
			||||||
                                continue;
 | 
					                            } else {
 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                                let value = input[2..].trim().to_string();
 | 
					                                let value = input[2..].trim().to_string();
 | 
				
			||||||
                                tasks.add_or_remove_property_column_at_index(value, index);
 | 
					                                tasks.add_or_remove_property_column_at_index(value, index);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        None => {
 | 
					                        } else if let Some(arg) = arg {
 | 
				
			||||||
                            if arg.is_empty() {
 | 
					 | 
				
			||||||
                                println!("Available properties:
 | 
					 | 
				
			||||||
- `id`
 | 
					 | 
				
			||||||
- `parentid`
 | 
					 | 
				
			||||||
- `name`
 | 
					 | 
				
			||||||
- `state`
 | 
					 | 
				
			||||||
- `hashtags`
 | 
					 | 
				
			||||||
- `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
 | 
					 | 
				
			||||||
- `path` - name including parent tasks
 | 
					 | 
				
			||||||
- `rpath` - name including parent tasks up to active task
 | 
					 | 
				
			||||||
- `time` - time tracked on this task
 | 
					 | 
				
			||||||
- `rtime` - time tracked on this tasks and all recursive subtasks
 | 
					 | 
				
			||||||
- `progress` - recursive subtask completion in percent
 | 
					 | 
				
			||||||
- `subtasks` - how many direct subtasks are complete");
 | 
					 | 
				
			||||||
                                continue;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            tasks.add_or_remove_property_column(arg);
 | 
					                            tasks.add_or_remove_property_column(arg);
 | 
				
			||||||
                        }
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            println!("{}", PROPERTY_COLUMNS);
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some(',') => tasks.make_note(arg),
 | 
					                    Some(',') => {
 | 
				
			||||||
 | 
					                        match arg {
 | 
				
			||||||
 | 
					                            None => {
 | 
				
			||||||
 | 
					                                tasks.get_current_task().map_or_else(
 | 
				
			||||||
 | 
					                                    || info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
 | 
				
			||||||
 | 
					                                    |task| println!("{}", task.description_events().map(|e| format!("{} {}", e.created_at.to_human_datetime(), e.content)).join("\n")),
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                                continue
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                            Some(arg) => tasks.make_note(arg),
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('>') => {
 | 
					                    Some('>') => {
 | 
				
			||||||
                        tasks.update_state(arg, State::Done);
 | 
					                        tasks.update_state(&arg_default, State::Done);
 | 
				
			||||||
                        tasks.move_up();
 | 
					                        tasks.move_up();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('<') => {
 | 
					                    Some('<') => {
 | 
				
			||||||
                        tasks.update_state(arg, State::Closed);
 | 
					                        tasks.update_state(&arg_default, State::Closed);
 | 
				
			||||||
                        tasks.move_up();
 | 
					                        tasks.move_up();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -328,33 +321,32 @@ async fn main() {
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('?') => {
 | 
					                    Some('?') => {
 | 
				
			||||||
                        tasks.set_state_filter(some_non_empty(arg).filter(|s| !s.is_empty()));
 | 
					                        tasks.set_state_filter(arg.map(|s| s.to_string()));
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('!') => match tasks.get_position() {
 | 
					                    Some('!') => match tasks.get_position() {
 | 
				
			||||||
                        None => {
 | 
					                        None => warn!("First select a task to set its state!"),
 | 
				
			||||||
                            warn!("First select a task to set its state!");
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Some(id) => {
 | 
					                        Some(id) => {
 | 
				
			||||||
                            tasks.set_state_for(id, arg, match arg {
 | 
					                            tasks.set_state_for_with(id, arg_default);
 | 
				
			||||||
                                "Closed" => State::Closed,
 | 
					 | 
				
			||||||
                                "Done" => State::Done,
 | 
					 | 
				
			||||||
                                _ => State::Open,
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('#') | Some('+') => {
 | 
					                    Some('#') | Some('+') => {
 | 
				
			||||||
                        tasks.add_tag(arg.to_string());
 | 
					                        match arg {
 | 
				
			||||||
                        info!("Added tag filter for #{arg}")
 | 
					                            Some(arg) => tasks.add_tag(arg.to_string()),
 | 
				
			||||||
 | 
					                            None => tasks.clear_filter()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('-') => {
 | 
					                    Some('-') => {
 | 
				
			||||||
                        tasks.remove_tag(arg.to_string());
 | 
					                        match arg {
 | 
				
			||||||
                        info!("Removed tag filter for #{arg}")
 | 
					                            Some(arg) => tasks.remove_tag(arg),
 | 
				
			||||||
 | 
					                            None => tasks.clear_filter()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('*') => {
 | 
					                    Some('*') => match arg {
 | 
				
			||||||
 | 
					                        Some(arg) => {
 | 
				
			||||||
                            if let Ok(num) = arg.parse::<i64>() {
 | 
					                            if let Ok(num) = arg.parse::<i64>() {
 | 
				
			||||||
                                tasks.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num)));
 | 
					                                tasks.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num)));
 | 
				
			||||||
                            } else if let Ok(date) = DateTime::parse_from_rfc3339(arg) {
 | 
					                            } else if let Ok(date) = DateTime::parse_from_rfc3339(arg) {
 | 
				
			||||||
| 
						 | 
					@ -363,6 +355,11 @@ async fn main() {
 | 
				
			||||||
                                warn!("Cannot parse {arg}");
 | 
					                                warn!("Cannot parse {arg}");
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        None => {
 | 
				
			||||||
 | 
					                            // TODO time tracked list
 | 
				
			||||||
 | 
					                            // continue
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Some('.') => {
 | 
					                    Some('.') => {
 | 
				
			||||||
                        let mut dots = 1;
 | 
					                        let mut dots = 1;
 | 
				
			||||||
| 
						 | 
					@ -372,12 +369,12 @@ async fn main() {
 | 
				
			||||||
                            pos = tasks.get_parent(pos).cloned();
 | 
					                            pos = tasks.get_parent(pos).cloned();
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        let slice = &input[dots..];
 | 
					                        let slice = &input[dots..];
 | 
				
			||||||
 | 
					                        tasks.move_to(pos);
 | 
				
			||||||
                        if slice.is_empty() {
 | 
					                        if slice.is_empty() {
 | 
				
			||||||
                            tasks.move_to(pos);
 | 
					                            if dots > 1 { 
 | 
				
			||||||
                            continue;
 | 
					                                info!("Moving up {} tasks", dots - 1)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        if let Ok(depth) = slice.parse::<i8>() {
 | 
					                        } else if let Ok(depth) = slice.parse::<i8>() {
 | 
				
			||||||
                            tasks.move_to(pos);
 | 
					 | 
				
			||||||
                            tasks.set_depth(depth);
 | 
					                            tasks.set_depth(depth);
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id)));
 | 
					                            tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id)));
 | 
				
			||||||
| 
						 | 
					@ -394,9 +391,7 @@ async fn main() {
 | 
				
			||||||
                        let slice = &input[dots..].to_ascii_lowercase();
 | 
					                        let slice = &input[dots..].to_ascii_lowercase();
 | 
				
			||||||
                        if slice.is_empty() {
 | 
					                        if slice.is_empty() {
 | 
				
			||||||
                            tasks.move_to(pos);
 | 
					                            tasks.move_to(pos);
 | 
				
			||||||
                            continue;
 | 
					                        } else if let Ok(depth) = slice.parse::<i8>() {
 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        if let Ok(depth) = slice.parse::<i8>() {
 | 
					 | 
				
			||||||
                            tasks.move_to(pos);
 | 
					                            tasks.move_to(pos);
 | 
				
			||||||
                            tasks.set_depth(depth);
 | 
					                            tasks.set_depth(depth);
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
| 
						 | 
					@ -434,11 +429,14 @@ async fn main() {
 | 
				
			||||||
                            if new_relay.is_some() {
 | 
					                            if new_relay.is_some() {
 | 
				
			||||||
                                selected_relay = new_relay;
 | 
					                                selected_relay = new_relay;
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					                            //or_print(tasks.print_tasks());
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            tasks.filter_or_create(&input);
 | 
					                            tasks.filter_or_create(&input);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                or_print(tasks.print_tasks());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            Some(Err(e)) => warn!("{}", e),
 | 
					            Some(Err(e)) => warn!("{}", e),
 | 
				
			||||||
            None => break,
 | 
					            None => break,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/task.rs
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								src/task.rs
									
										
									
									
									
								
							| 
						 | 
					@ -49,16 +49,20 @@ impl Task {
 | 
				
			||||||
            .unwrap_or_else(|| self.get_id().to_string())
 | 
					            .unwrap_or_else(|| self.get_id().to_string())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn descriptions(&self) -> impl Iterator<Item=&String> + '_ {
 | 
					    pub(crate) fn description_events(&self) -> impl Iterator<Item=&Event> + '_ {
 | 
				
			||||||
        self.props.iter().filter_map(|event| {
 | 
					        self.props.iter().filter_map(|event| {
 | 
				
			||||||
            if event.kind == Kind::TextNote {
 | 
					            if event.kind == Kind::TextNote {
 | 
				
			||||||
                Some(&event.content)
 | 
					                Some(event)
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                None
 | 
					                None
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    pub(crate) fn descriptions(&self) -> impl Iterator<Item=&String> + '_ {
 | 
				
			||||||
 | 
					        self.description_events().map(|e| &e.content)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
 | 
					    fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
 | 
				
			||||||
        self.props.iter().filter_map(|event| {
 | 
					        self.props.iter().filter_map(|event| {
 | 
				
			||||||
            event.kind.try_into().ok().map(|s| TaskState {
 | 
					            event.kind.try_into().ok().map(|s| TaskState {
 | 
				
			||||||
| 
						 | 
					@ -118,11 +122,11 @@ impl Task {
 | 
				
			||||||
                self.props
 | 
					                self.props
 | 
				
			||||||
                    .iter()
 | 
					                    .iter()
 | 
				
			||||||
                    .map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
 | 
					                    .map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
 | 
				
			||||||
                    .collect::<Vec<String>>()
 | 
					                    .collect_vec()
 | 
				
			||||||
            )),
 | 
					            )),
 | 
				
			||||||
            "descriptions" => Some(format!(
 | 
					            "descriptions" => Some(format!(
 | 
				
			||||||
                "{:?}",
 | 
					                "{:?}",
 | 
				
			||||||
                self.descriptions().collect::<Vec<&String>>()
 | 
					                self.descriptions().collect_vec()
 | 
				
			||||||
            )),
 | 
					            )),
 | 
				
			||||||
            _ => {
 | 
					            _ => {
 | 
				
			||||||
                warn!("Unknown task property {}", property);
 | 
					                warn!("Unknown task property {}", property);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										107
									
								
								src/tasks.rs
									
										
									
									
									
								
							
							
						
						
									
										107
									
								
								src/tasks.rs
									
										
									
									
									
								
							| 
						 | 
					@ -1,10 +1,10 @@
 | 
				
			||||||
use std::collections::{BTreeSet, HashMap};
 | 
					use std::collections::{BTreeSet, HashMap};
 | 
				
			||||||
use std::io::{Error, stdout, Write};
 | 
					use std::io::{Error, stdout, Write};
 | 
				
			||||||
use std::iter::once;
 | 
					use std::iter::{once, Sum};
 | 
				
			||||||
use std::ops::{Div, Rem};
 | 
					use std::ops::{Div, Rem};
 | 
				
			||||||
use std::sync::mpsc::Sender;
 | 
					use std::sync::mpsc::Sender;
 | 
				
			||||||
 | 
					use std::time::Duration;
 | 
				
			||||||
use chrono::{Local, TimeZone};
 | 
					use chrono::{DateTime, Local, TimeZone};
 | 
				
			||||||
use chrono::LocalResult::Single;
 | 
					use chrono::LocalResult::Single;
 | 
				
			||||||
use colored::Colorize;
 | 
					use colored::Colorize;
 | 
				
			||||||
use itertools::Itertools;
 | 
					use itertools::Itertools;
 | 
				
			||||||
| 
						 | 
					@ -103,45 +103,18 @@ impl Tasks {
 | 
				
			||||||
        children
 | 
					        children
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Total time tracked on this task by the current user.
 | 
					    /// Total time in seconds tracked on this task by the current user.
 | 
				
			||||||
    pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
 | 
					    pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
 | 
				
			||||||
        Self::time_tracked_for(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![id])
 | 
					        TimesTracked::from(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![id]).sum::<Duration>().as_secs()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Total time tracked on this task and its subtasks by all users.
 | 
					    /// Total time in seconds tracked on this task and its subtasks by all users.
 | 
				
			||||||
    /// TODO needs testing!
 | 
					 | 
				
			||||||
    fn total_time_tracked(&self, id: EventId) -> u64 {
 | 
					    fn total_time_tracked(&self, id: EventId) -> u64 {
 | 
				
			||||||
        let mut total = 0;
 | 
					        let mut total = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let children = self.get_subtasks(id);
 | 
					        let children = self.get_subtasks(id);
 | 
				
			||||||
        for user in self.history.values() {
 | 
					        for user in self.history.values() {
 | 
				
			||||||
            total += Self::time_tracked_for(user, &children);
 | 
					            total += TimesTracked::from(user, &children).into_iter().sum::<Duration>().as_secs();
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        total
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn time_tracked_for<'a, E>(events: E, ids: &Vec<EventId>) -> u64
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        E: IntoIterator<Item=&'a Event>,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let mut total = 0;
 | 
					 | 
				
			||||||
        let mut start: Option<Timestamp> = None;
 | 
					 | 
				
			||||||
        for event in events {
 | 
					 | 
				
			||||||
            match event.tags.first().and_then(|tag| tag.as_standardized()) {
 | 
					 | 
				
			||||||
                Some(TagStandard::Event {
 | 
					 | 
				
			||||||
                         event_id,
 | 
					 | 
				
			||||||
                         ..
 | 
					 | 
				
			||||||
                     }) if ids.contains(event_id) => {
 | 
					 | 
				
			||||||
                    start = start.or(Some(event.created_at))
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                _ => if let Some(stamp) = start {
 | 
					 | 
				
			||||||
                    total += (event.created_at - stamp).as_u64();
 | 
					 | 
				
			||||||
                    start = None;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if let Some(start) = start {
 | 
					 | 
				
			||||||
            total += (Timestamp::now() - start).as_u64();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        total
 | 
					        total
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -253,7 +226,7 @@ impl Tasks {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[inline]
 | 
					    #[inline]
 | 
				
			||||||
    fn current_task(&self) -> Option<&Task> {
 | 
					    pub(crate) fn get_current_task(&self) -> Option<&Task> {
 | 
				
			||||||
        self.position.and_then(|id| self.get_by_id(&id))
 | 
					        self.position.and_then(|id| self.get_by_id(&id))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -266,7 +239,7 @@ impl Tasks {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn current_tasks(&self) -> Vec<&Task> {
 | 
					    pub(crate) fn current_tasks(&self) -> Vec<&Task> {
 | 
				
			||||||
        if self.depth == 0 {
 | 
					        if self.depth == 0 {
 | 
				
			||||||
            return self.current_task().into_iter().collect();
 | 
					            return self.get_current_task().into_iter().collect();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
 | 
					        let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
 | 
				
			||||||
        if res.len() > 0 {
 | 
					        if res.len() > 0 {
 | 
				
			||||||
| 
						 | 
					@ -295,7 +268,7 @@ impl Tasks {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn print_tasks(&self) -> Result<(), Error> {
 | 
					    pub(crate) fn print_tasks(&self) -> Result<(), Error> {
 | 
				
			||||||
        let mut lock = stdout().lock();
 | 
					        let mut lock = stdout().lock();
 | 
				
			||||||
        if let Some(t) = self.current_task() {
 | 
					        if let Some(t) = self.get_current_task() {
 | 
				
			||||||
            let state = t.state_or_default();
 | 
					            let state = t.state_or_default();
 | 
				
			||||||
            writeln!(
 | 
					            writeln!(
 | 
				
			||||||
                lock,
 | 
					                lock,
 | 
				
			||||||
| 
						 | 
					@ -362,7 +335,6 @@ impl Tasks {
 | 
				
			||||||
                    .join(" \t")
 | 
					                    .join(" \t")
 | 
				
			||||||
            )?;
 | 
					            )?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        writeln!(lock)?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -372,23 +344,37 @@ impl Tasks {
 | 
				
			||||||
        self.view = view;
 | 
					        self.view = view;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) fn clear_filter(&mut self) {
 | 
				
			||||||
 | 
					        self.view.clear();
 | 
				
			||||||
 | 
					        self.tags.clear();
 | 
				
			||||||
 | 
					        info!("Removed all filters");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn add_tag(&mut self, tag: String) {
 | 
					    pub(crate) fn add_tag(&mut self, tag: String) {
 | 
				
			||||||
        self.view.clear();
 | 
					        self.view.clear();
 | 
				
			||||||
 | 
					        info!("Added tag filter for #{tag}");
 | 
				
			||||||
        self.tags.insert(Hashtag(tag).into());
 | 
					        self.tags.insert(Hashtag(tag).into());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn remove_tag(&mut self, tag: String) {
 | 
					    pub(crate) fn remove_tag(&mut self, tag: &str) {
 | 
				
			||||||
        self.view.clear();
 | 
					        self.view.clear();
 | 
				
			||||||
        self.tags.retain(|t| !t.content().is_some_and(|value| value.to_string().starts_with(&tag)));
 | 
					        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 {
 | 
				
			||||||
 | 
					            info!("Found no tag filters starting with {tag} to remove");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
 | 
					    pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
 | 
				
			||||||
        self.view.clear();
 | 
					        self.view.clear();
 | 
				
			||||||
 | 
					        info!("Filtering for {}", state.as_ref().map_or("open tasks".to_string(), |s| format!("state {s}")));
 | 
				
			||||||
        self.state = state;
 | 
					        self.state = state;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn move_up(&mut self) {
 | 
					    pub(crate) fn move_up(&mut self) {
 | 
				
			||||||
        self.move_to(self.current_task().and_then(|t| t.parent_id()).cloned());
 | 
					        self.move_to(self.get_current_task().and_then(|t| t.parent_id()).cloned());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub(crate) fn flush(&self) {
 | 
					    pub(crate) fn flush(&self) {
 | 
				
			||||||
| 
						 | 
					@ -646,7 +632,6 @@ impl Tasks {
 | 
				
			||||||
            self.properties.insert(index, property);
 | 
					            self.properties.insert(index, property);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Formats the given seconds according to the given format.
 | 
					/// Formats the given seconds according to the given format.
 | 
				
			||||||
| 
						 | 
					@ -686,6 +671,43 @@ pub(crate) fn join_tasks<'a>(
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TimesTracked<'a> {
 | 
				
			||||||
 | 
					    events: Box<dyn Iterator<Item=&'a Event> + 'a>,
 | 
				
			||||||
 | 
					    ids: &'a Vec<EventId>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl TimesTracked<'_> {
 | 
				
			||||||
 | 
					    fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<EventId>) -> TimesTracked<'b> {
 | 
				
			||||||
 | 
					        TimesTracked {
 | 
				
			||||||
 | 
					            events: Box::new(events.into_iter()),
 | 
				
			||||||
 | 
					            ids,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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() {
 | 
				
			||||||
 | 
					            match event.tags.first().and_then(|tag| tag.as_standardized()) {
 | 
				
			||||||
 | 
					                Some(TagStandard::Event {
 | 
				
			||||||
 | 
					                         event_id,
 | 
				
			||||||
 | 
					                         ..
 | 
				
			||||||
 | 
					                     }) if self.ids.contains(event_id) => {
 | 
				
			||||||
 | 
					                    start = start.or(Some(event.created_at.as_u64()))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => if let Some(stamp) = start {
 | 
				
			||||||
 | 
					                    return Some(Duration::from_secs(event.created_at.as_u64() - stamp))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return start.map(|stamp| Duration::from_secs(Timestamp::now().as_u64() - stamp))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct ParentIterator<'a> {
 | 
					struct ParentIterator<'a> {
 | 
				
			||||||
    tasks: &'a TaskMap,
 | 
					    tasks: &'a TaskMap,
 | 
				
			||||||
    current: Option<EventId>,
 | 
					    current: Option<EventId>,
 | 
				
			||||||
| 
						 | 
					@ -832,7 +854,6 @@ mod tasks_test {
 | 
				
			||||||
            "0000000000000000000000000000000000000000000000000000000000000000>test"
 | 
					            "0000000000000000000000000000000000000000000000000000000000000000>test"
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        assert_eq!(tasks.relative_path(dangling), "test");
 | 
					        assert_eq!(tasks.relative_path(dangling), "test");
 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[allow(dead_code)]
 | 
					    #[allow(dead_code)]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue