2024-11-25 02:15:18 +01:00
|
|
|
pub(crate) mod nostr_users;
|
2024-11-24 23:42:47 +01:00
|
|
|
|
2024-09-22 16:24:07 +02:00
|
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
2024-08-10 15:44:52 +03:00
|
|
|
use std::fmt::{Display, Formatter};
|
2024-09-05 13:54:23 +03:00
|
|
|
use std::iter::{empty, once, FusedIterator};
|
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-08 00:18:34 +03:00
|
|
|
use std::time::Duration;
|
2024-08-08 13:52:02 +03:00
|
|
|
|
2024-11-11 13:13:15 +01:00
|
|
|
use crate::event_sender::{EventSender, MostrMessage};
|
2024-11-25 02:15:18 +01:00
|
|
|
use crate::hashtag::Hashtag;
|
2024-11-15 17:18:30 +01:00
|
|
|
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, to_string_or_default, CHARACTER_THRESHOLD};
|
2024-09-14 17:14:51 +03:00
|
|
|
use crate::kinds::*;
|
2024-11-09 18:00:17 +01:00
|
|
|
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY};
|
2024-11-25 02:15:18 +01:00
|
|
|
use crate::tasks::nostr_users::NostrUsers;
|
2024-07-30 09:00:39 +03:00
|
|
|
use colored::Colorize;
|
2024-11-11 14:59:25 +01:00
|
|
|
use itertools::Itertools;
|
2024-07-29 21:06:23 +03:00
|
|
|
use log::{debug, error, info, trace, warn};
|
2024-11-22 11:22:28 +01:00
|
|
|
use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp, Url};
|
2024-10-01 23:20:08 +02:00
|
|
|
use regex::bytes::Regex;
|
2024-09-14 17:14:51 +03:00
|
|
|
use tokio::sync::mpsc::Sender;
|
2024-07-19 21:06:03 +03:00
|
|
|
|
2024-11-09 19:18:42 +01:00
|
|
|
const DEFAULT_PRIO: Prio = 25;
|
2024-11-22 11:20:13 +01:00
|
|
|
const QUICK_PRIO: Prio = 35;
|
2024-11-09 19:18:42 +01:00
|
|
|
pub const HIGH_PRIO: Prio = 85;
|
2024-11-09 14:40:18 +01:00
|
|
|
|
|
|
|
/// Amount of seconds to treat as "now"
|
2024-08-25 16:37:55 +03:00
|
|
|
const MAX_OFFSET: u64 = 9;
|
2024-11-09 20:00:06 +01:00
|
|
|
pub(crate) fn now() -> Timestamp {
|
2024-08-25 16:37:55 +03:00
|
|
|
Timestamp::now() + MAX_OFFSET
|
|
|
|
}
|
|
|
|
|
2024-07-19 01:15:11 +03:00
|
|
|
type TaskMap = HashMap<EventId, Task>;
|
2024-09-22 16:47:26 +02:00
|
|
|
trait TaskMapMethods {
|
|
|
|
fn children_of<'a>(&'a self, task: &'a Task) -> impl Iterator<Item=&Task> + 'a;
|
2024-11-18 14:40:50 +01:00
|
|
|
fn children_for<'a>(&'a self, id: Option<EventId>) -> impl Iterator<Item=&Task> + 'a;
|
|
|
|
fn children_ids_for<'a>(&'a self, id: EventId) -> impl Iterator<Item=&EventId> + 'a;
|
2024-09-22 16:47:26 +02:00
|
|
|
}
|
|
|
|
impl TaskMapMethods for TaskMap {
|
|
|
|
fn children_of<'a>(&'a self, task: &'a Task) -> impl Iterator<Item=&Task> + 'a {
|
2024-11-18 14:40:50 +01:00
|
|
|
self.children_for(Some(task.event.id))
|
2024-09-22 16:47:26 +02:00
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
fn children_for<'a>(&'a self, id: Option<EventId>) -> impl Iterator<Item=&Task> + 'a {
|
|
|
|
self.values().filter(move |t| t.parent_id() == id.as_ref())
|
2024-09-22 16:47:26 +02:00
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
fn children_ids_for<'a>(&'a self, id: EventId) -> impl Iterator<Item=&EventId> + 'a {
|
|
|
|
self.children_for(Some(id)).map(|t| t.get_id())
|
2024-09-22 16:47:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-26 21:45:29 +03:00
|
|
|
#[derive(Debug, Clone)]
|
2024-09-22 16:48:15 +02:00
|
|
|
pub(crate) struct TasksRelay {
|
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
|
2024-08-27 15:00:53 +03:00
|
|
|
history: HashMap<PublicKey, BTreeMap<Timestamp, Event>>,
|
2024-10-03 13:39:52 +02:00
|
|
|
/// Index of known users with metadata
|
2024-11-24 23:42:47 +01:00
|
|
|
users: NostrUsers,
|
2024-08-29 22:28:25 +03:00
|
|
|
/// Own pinned tasks
|
|
|
|
bookmarks: Vec<EventId>,
|
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-10-15 03:01:57 +02:00
|
|
|
sorting: VecDeque<String>, // TODO track boolean for reversal?
|
2024-08-18 21:37:39 +03:00
|
|
|
|
2024-10-03 13:39:52 +02:00
|
|
|
/// A filtered view of the current tasks.
|
|
|
|
/// Would like this to be Task references
|
|
|
|
/// but that doesn't work unless I start meddling with Rc everywhere.
|
2024-08-18 21:37:39 +03:00
|
|
|
view: Vec<EventId>,
|
2024-10-11 01:10:17 +02:00
|
|
|
search_depth: usize,
|
|
|
|
view_depth: usize,
|
2024-10-11 22:06:18 +02:00
|
|
|
pub(crate) recurse_activities: bool,
|
2024-07-24 21:11:36 +03:00
|
|
|
|
2024-07-25 22:40:35 +03:00
|
|
|
/// Currently active tags
|
2024-11-21 09:17:56 +01:00
|
|
|
tags: BTreeSet<Hashtag>,
|
2024-10-03 13:39:52 +02:00
|
|
|
/// Tags filtered out from view
|
2024-11-21 09:17:56 +01:00
|
|
|
tags_excluded: BTreeSet<Hashtag>,
|
2024-07-26 21:45:29 +03:00
|
|
|
/// Current active state
|
2024-08-10 15:44:52 +03:00
|
|
|
state: StateFilter,
|
2024-11-09 19:18:42 +01:00
|
|
|
/// Current priority for filtering and new tasks
|
|
|
|
priority: Option<Prio>,
|
2024-11-20 23:16:57 +01:00
|
|
|
pubkey: Option<PublicKey>,
|
2024-07-24 21:11:36 +03:00
|
|
|
|
2024-07-25 10:55:29 +03:00
|
|
|
sender: EventSender,
|
2024-09-14 17:14:51 +03:00
|
|
|
overflow: VecDeque<Event>,
|
2024-09-22 20:05:05 +02:00
|
|
|
pub(crate) custom_time: Option<Timestamp>,
|
2024-07-19 01:15:11 +03:00
|
|
|
}
|
|
|
|
|
2024-08-25 14:46:07 +03:00
|
|
|
#[derive(Clone, Debug, Default)]
|
2024-08-10 15:44:52 +03:00
|
|
|
pub(crate) enum StateFilter {
|
2024-08-25 14:46:07 +03:00
|
|
|
#[default]
|
2024-08-10 15:44:52 +03:00
|
|
|
Default,
|
|
|
|
All,
|
|
|
|
State(String),
|
|
|
|
}
|
|
|
|
impl StateFilter {
|
2024-11-22 11:20:13 +01:00
|
|
|
fn from(str: &str) -> Self {
|
|
|
|
StateFilter::State(str.to_string())
|
|
|
|
}
|
|
|
|
|
2024-08-10 15:44:52 +03:00
|
|
|
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 {
|
2024-09-07 13:03:30 +03:00
|
|
|
StateFilter::Default => task.pure_state().is_open(),
|
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 Display for StateFilter {
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{}",
|
|
|
|
match self {
|
2024-09-07 13:03:30 +03:00
|
|
|
StateFilter::Default => "open tasks".to_string(),
|
2024-08-10 15:44:52 +03:00
|
|
|
StateFilter::All => "all tasks".to_string(),
|
|
|
|
StateFilter::State(s) => format!("state {s}"),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-22 16:48:15 +02:00
|
|
|
impl TasksRelay {
|
2024-08-29 22:28:25 +03:00
|
|
|
pub(crate) fn from(
|
|
|
|
url: Option<Url>,
|
2024-09-14 17:14:51 +03:00
|
|
|
tx: &Sender<MostrMessage>,
|
2024-08-29 22:28:25 +03:00
|
|
|
keys: &Keys,
|
|
|
|
metadata: Option<Metadata>,
|
|
|
|
) -> Self {
|
2024-08-25 14:46:07 +03:00
|
|
|
let mut new = Self::with_sender(EventSender::from(url, tx, keys));
|
2024-08-21 12:30:13 +03:00
|
|
|
metadata.map(|m| new.users.insert(keys.public_key(), m));
|
|
|
|
new
|
2024-08-07 15:03:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn with_sender(sender: EventSender) -> Self {
|
2024-09-22 16:48:15 +02:00
|
|
|
TasksRelay {
|
2024-07-19 01:15:11 +03:00
|
|
|
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-08-29 22:28:25 +03:00
|
|
|
bookmarks: Default::default(),
|
|
|
|
|
2024-08-19 21:18:11 +03:00
|
|
|
properties: [
|
2024-11-25 02:29:23 +01:00
|
|
|
"owner",
|
2024-11-09 20:33:29 +01:00
|
|
|
"prio",
|
2024-08-19 21:18:11 +03:00
|
|
|
"state",
|
|
|
|
"rtime",
|
|
|
|
"hashtags",
|
|
|
|
"rpath",
|
|
|
|
"desc",
|
|
|
|
].into_iter().map(|s| s.to_string()).collect(),
|
|
|
|
sorting: [
|
2024-11-09 20:33:29 +01:00
|
|
|
"priority",
|
|
|
|
"status",
|
2024-11-25 02:29:23 +01:00
|
|
|
"owner",
|
2024-08-19 21:18:11 +03:00
|
|
|
"hashtags",
|
|
|
|
"rtime",
|
|
|
|
"name",
|
|
|
|
].into_iter().map(|s| s.to_string()).collect(),
|
2024-08-29 22:28:25 +03:00
|
|
|
|
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-11-09 19:18:42 +01:00
|
|
|
priority: None,
|
2024-11-20 23:16:57 +01:00
|
|
|
pubkey: Some(sender.pubkey()),
|
2024-11-09 19:18:42 +01:00
|
|
|
|
2024-10-11 01:10:17 +02:00
|
|
|
search_depth: 4,
|
|
|
|
view_depth: 0,
|
2024-11-15 17:12:52 +01:00
|
|
|
recurse_activities: false,
|
2024-09-14 17:14:51 +03:00
|
|
|
|
2024-07-25 10:55:29 +03:00
|
|
|
sender,
|
2024-09-14 17:14:51 +03:00
|
|
|
overflow: Default::default(),
|
2024-09-22 20:05:05 +02:00
|
|
|
custom_time: None,
|
2024-09-14 17:14:51 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn process_overflow(&mut self) {
|
|
|
|
let elements = self.overflow.len();
|
2024-09-23 13:59:29 +02:00
|
|
|
let mut issues = 0;
|
2024-09-14 17:14:51 +03:00
|
|
|
for _ in 0..elements {
|
2024-09-23 13:59:29 +02:00
|
|
|
if let Some(event) = self.overflow.pop_back() {
|
2024-09-14 17:14:51 +03:00
|
|
|
if let Some(event) = self.add_prop(event) {
|
|
|
|
warn!("Unable to sort Event {:?}", event);
|
2024-09-23 13:59:29 +02:00
|
|
|
issues += 1;
|
2024-09-14 17:14:51 +03:00
|
|
|
//self.overflow.push_back(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if elements > 0 {
|
2024-11-18 14:43:49 +01:00
|
|
|
info!(
|
|
|
|
"Reprocessed {elements} updates with {issues} issues{}",
|
|
|
|
self.sender.url.as_ref().map(|url| format!(" from {url}")).unwrap_or_default()
|
|
|
|
);
|
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-11-18 14:40:50 +01: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-08-01 14:07:40 +03:00
|
|
|
#[inline]
|
|
|
|
pub(crate) fn len(&self) -> usize { self.tasks.len() }
|
2024-07-19 21:06:03 +03:00
|
|
|
|
2024-08-19 13:06:20 +03:00
|
|
|
pub(crate) fn get_position(&self) -> Option<EventId> {
|
2024-08-25 16:37:55 +03:00
|
|
|
self.get_position_at(now()).1
|
2024-08-25 10:48:59 +03:00
|
|
|
}
|
|
|
|
|
2024-10-14 16:10:56 +02:00
|
|
|
fn sorting_key(&self, task: &Task) -> impl Ord {
|
|
|
|
self.sorting
|
|
|
|
.iter()
|
|
|
|
.map(|p| self.get_property(task, p.as_str()))
|
|
|
|
.collect_vec()
|
|
|
|
}
|
|
|
|
|
2024-08-29 22:15:30 +03:00
|
|
|
// TODO binary search
|
|
|
|
/// Gets last position change before the given timestamp
|
2024-11-18 14:40:50 +01:00
|
|
|
fn get_position_at(&self, timestamp: Timestamp) -> (Timestamp, Option<EventId>) {
|
2024-08-25 16:37:55 +03:00
|
|
|
self.history_from(timestamp)
|
2024-08-19 13:59:37 +03:00
|
|
|
.last()
|
2024-08-25 16:37:55 +03:00
|
|
|
.filter(|e| e.created_at <= timestamp)
|
|
|
|
.map_or_else(
|
|
|
|
|| (Timestamp::now(), None),
|
2024-11-18 14:40:50 +01:00
|
|
|
|e| (e.created_at, referenced_event(e)),
|
|
|
|
)
|
2024-08-19 13:06:20 +03:00
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
fn nonclosed_tasks(&self) -> impl Iterator<Item=&Task> {
|
2024-08-08 13:04:22 +03:00
|
|
|
self.tasks.values()
|
|
|
|
.filter(|t| t.pure_state() != State::Closed)
|
2024-11-18 14:40:50 +01:00
|
|
|
}
|
|
|
|
|
2024-11-21 09:47:14 +01:00
|
|
|
pub(crate) fn all_hashtags(&self) -> BTreeSet<Hashtag> {
|
|
|
|
self.nonclosed_tasks().flat_map(|t| t.list_hashtags()).collect()
|
2024-08-08 13:04:22 +03:00
|
|
|
}
|
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-11-25 01:45:18 +01:00
|
|
|
pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
2024-09-23 13:51:16 +02:00
|
|
|
self.times_tracked_for(&self.sender.pubkey())
|
|
|
|
}
|
|
|
|
|
2024-11-25 01:45:18 +01:00
|
|
|
pub(crate) fn history_for(
|
|
|
|
&self,
|
|
|
|
key: &PublicKey,
|
|
|
|
) -> Option<impl DoubleEndedIterator<Item=String> + '_> {
|
|
|
|
self.history.get(key).map(|hist| {
|
|
|
|
let mut last = None;
|
|
|
|
// TODO limit history to active tags
|
|
|
|
hist.values().filter_map(move |event| {
|
|
|
|
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_path(Some(id))))
|
|
|
|
.join(" "));
|
|
|
|
if new != last {
|
|
|
|
// TODO omit intervals <2min - but I think I need threeway variable tracking for that
|
|
|
|
// TODO alternate color with grey between days
|
|
|
|
last = new;
|
|
|
|
return Some(format!(
|
|
|
|
"{} {}",
|
|
|
|
format_timestamp_local(&event.created_at),
|
|
|
|
last.as_ref().unwrap_or(&"---".to_string())
|
|
|
|
));
|
|
|
|
}
|
|
|
|
None
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn times_tracked_for(
|
|
|
|
&self,
|
|
|
|
key: &PublicKey,
|
2024-11-25 01:45:18 +01:00
|
|
|
) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
2024-11-18 14:40:50 +01:00
|
|
|
match self.get_position() {
|
2024-08-08 15:09:39 +03:00
|
|
|
None => {
|
2024-11-25 01:45:18 +01:00
|
|
|
match self.history_for(key) {
|
|
|
|
Some(hist) =>
|
|
|
|
(
|
|
|
|
format!("Time-Tracking History for {}:", self.users.get_displayname(&key)),
|
|
|
|
Box::from(hist),
|
|
|
|
),
|
|
|
|
None =>
|
|
|
|
(
|
|
|
|
"Nothing time-tracked yet".to_string(),
|
|
|
|
Box::from(empty()),
|
|
|
|
)
|
2024-08-08 15:09:39 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(id) => {
|
2024-11-11 01:08:05 +01:00
|
|
|
// TODO show current recursive with pubkey
|
2024-11-18 14:40:50 +01:00
|
|
|
let ids = [id];
|
2024-11-25 01:45:18 +01:00
|
|
|
let mut 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);
|
2024-08-27 15:00:53 +03:00
|
|
|
let mut iter = timestamps(set.values(), &ids).tuples();
|
2024-08-10 21:04:13 +03:00
|
|
|
while let Some(((start, _), (end, _))) = iter.next() {
|
2024-10-18 18:13:35 +02:00
|
|
|
// Filter out intervals <2 mins
|
|
|
|
if start.as_u64() + 120 < end.as_u64() {
|
2024-11-18 14:43:49 +01:00
|
|
|
vec.push(format!(
|
|
|
|
"{} - {} by {}",
|
|
|
|
format_timestamp_local(start),
|
|
|
|
format_timestamp_relative_to(end, start),
|
2024-11-24 23:42:47 +01:00
|
|
|
self.users.get_displayname(key)
|
2024-11-18 14:43:49 +01:00
|
|
|
))
|
2024-10-18 18:13:35 +02:00
|
|
|
}
|
2024-08-10 21:04:13 +03:00
|
|
|
}
|
2024-11-18 14:43:49 +01:00
|
|
|
iter.into_buffer().for_each(|(stamp, _)| {
|
|
|
|
vec.push(format!(
|
|
|
|
"{} started by {}",
|
|
|
|
format_timestamp_local(stamp),
|
2024-11-24 23:42:47 +01:00
|
|
|
self.users.get_displayname(key)
|
2024-11-18 14:43:49 +01:00
|
|
|
))
|
|
|
|
});
|
2024-08-10 21:04:13 +03:00
|
|
|
vec
|
2024-11-25 01:45:18 +01:00
|
|
|
})
|
|
|
|
.collect_vec();
|
|
|
|
// TODO sorting depends on timestamp format - needed to interleave different people
|
|
|
|
history.sort_unstable();
|
2024-11-18 14:40:50 +01:00
|
|
|
(
|
|
|
|
format!("Times Tracked on {:?}", self.get_task_title(&id)),
|
2024-11-25 01:45:18 +01:00
|
|
|
Box::from(history.into_iter()),
|
2024-11-18 14:40:50 +01:00
|
|
|
)
|
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-11-18 14:40:50 +01:00
|
|
|
Durations::from(self.get_own_events_history(), &[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-11-18 14:40:50 +01:00
|
|
|
let children = ChildIterator::from(&self, id).get_all();
|
2024-08-06 11:34:18 +03:00
|
|
|
for user in self.history.values() {
|
2024-11-18 14:43:49 +01:00
|
|
|
total += Durations::from(user.values(), &children)
|
|
|
|
.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-09-22 16:24:07 +02:00
|
|
|
self.get_by_id(id).and_then(|task| match task.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 = 0f32;
|
|
|
|
let mut count = 0;
|
2024-11-18 14:40:50 +01:00
|
|
|
for prog in self.tasks
|
|
|
|
.children_ids_for(task.event.id)
|
|
|
|
.filter_map(|e| self.total_progress(e))
|
|
|
|
{
|
2024-08-01 20:40:55 +03:00
|
|
|
sum += prog;
|
|
|
|
count += 1;
|
|
|
|
}
|
2024-11-18 14:43:49 +01:00
|
|
|
Some(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-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn up_by(&self, count: usize) -> Option<EventId> {
|
|
|
|
let pos = self.get_position();
|
|
|
|
let mut result = pos.as_ref();
|
2024-09-22 20:05:05 +02:00
|
|
|
for _ in 0..count {
|
2024-11-18 14:40:50 +01:00
|
|
|
result = self.get_parent(result);
|
2024-09-22 20:05:05 +02:00
|
|
|
}
|
2024-11-18 14:40:50 +01:00
|
|
|
result.cloned()
|
2024-09-22 20:05:05 +02:00
|
|
|
}
|
|
|
|
|
2024-08-19 16:36:06 +03:00
|
|
|
pub(crate) fn get_parent(&self, id: Option<&EventId>) -> Option<&EventId> {
|
|
|
|
id.and_then(|id| self.get_by_id(id))
|
2024-07-25 22:10:01 +03:00
|
|
|
.and_then(|t| t.parent_id())
|
|
|
|
}
|
|
|
|
|
2024-11-20 23:16:57 +01:00
|
|
|
// TODO test with context elements
|
|
|
|
/// Visual representation of current context
|
2024-07-26 21:45:29 +03:00
|
|
|
pub(crate) fn get_prompt_suffix(&self) -> String {
|
2024-11-20 23:16:57 +01:00
|
|
|
let mut prompt = String::with_capacity(128);
|
|
|
|
match self.pubkey {
|
|
|
|
None => { prompt.push_str(" @ALL"); }
|
|
|
|
Some(key) =>
|
|
|
|
if key != self.sender.pubkey() {
|
2024-11-24 08:47:57 +01:00
|
|
|
prompt.push_str(" @");
|
2024-11-24 23:42:47 +01:00
|
|
|
prompt.push_str(&self.users.get_username(&key))
|
2024-11-20 23:16:57 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
for tag in self.tags.iter() {
|
2024-11-21 09:17:56 +01:00
|
|
|
prompt.push_str(&format!(" #{}", tag));
|
2024-11-20 23:16:57 +01:00
|
|
|
}
|
|
|
|
for tag in self.tags_excluded.iter() {
|
2024-11-21 09:17:56 +01:00
|
|
|
prompt.push_str(&format!(" -#{}", tag));
|
2024-11-20 23:16:57 +01:00
|
|
|
}
|
|
|
|
prompt.push_str(&self.state.indicator());
|
2024-11-22 11:22:28 +01:00
|
|
|
self.priority.map(|p|
|
2024-11-20 23:16:57 +01:00
|
|
|
prompt.push_str(&format!(" *{:02}", p)));
|
|
|
|
prompt
|
2024-07-26 21:45:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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()))
|
2024-08-27 11:54:08 +03:00
|
|
|
.unwrap_or_default()
|
2024-07-25 22:10:01 +03:00
|
|
|
}
|
|
|
|
|
2024-11-20 19:05:33 +01:00
|
|
|
pub(crate) fn get_relative_path(&self, id: EventId) -> String {
|
|
|
|
join_tasks(
|
|
|
|
self.traverse_up_from(Some(id))
|
|
|
|
.take_while(|t| Some(t.event.id) != self.get_position()),
|
|
|
|
false,
|
|
|
|
).unwrap_or(id.to_string())
|
|
|
|
}
|
|
|
|
|
2024-08-25 16:46:21 +03:00
|
|
|
/// Iterate over the task referenced by the given id and all its available parents.
|
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,
|
|
|
|
}
|
2024-07-19 09:35:03 +03:00
|
|
|
}
|
2024-07-25 10:55:29 +03:00
|
|
|
|
2024-07-29 21:27:50 +03:00
|
|
|
|
2024-07-25 22:10:01 +03:00
|
|
|
// Helpers
|
|
|
|
|
2024-07-25 10:55:29 +03:00
|
|
|
fn resolve_tasks_rec<'a>(
|
2024-08-16 09:45:35 +03:00
|
|
|
&'a self,
|
2024-09-22 16:24:07 +02:00
|
|
|
iter: impl Iterator<Item=&'a Task>,
|
2024-09-07 13:03:30 +03:00
|
|
|
sparse: bool,
|
|
|
|
depth: usize,
|
|
|
|
) -> Vec<&'a Task> {
|
2024-10-14 16:10:56 +02:00
|
|
|
iter.sorted_by_cached_key(|task| self.sorting_key(task))
|
|
|
|
.flat_map(move |task| {
|
2024-11-09 18:01:40 +01:00
|
|
|
if !self.state.matches(task) {
|
|
|
|
return vec![];
|
2024-10-12 14:17:46 +02:00
|
|
|
}
|
2024-11-09 18:01:40 +01:00
|
|
|
let mut new_depth = depth;
|
|
|
|
if depth > 0 && (!self.recurse_activities || task.is_task()) {
|
|
|
|
new_depth = depth - 1;
|
|
|
|
if sparse && new_depth > self.view_depth && self.filter(task) {
|
|
|
|
new_depth = self.view_depth;
|
2024-07-25 10:55:29 +03:00
|
|
|
}
|
2024-07-25 00:26:29 +03:00
|
|
|
}
|
2024-11-09 18:01:40 +01:00
|
|
|
if new_depth > 0 {
|
2024-11-18 14:43:49 +01:00
|
|
|
let mut children =
|
|
|
|
self.resolve_tasks_rec(self.tasks.children_of(&task), sparse, new_depth);
|
2024-11-09 18:01:40 +01:00
|
|
|
if !children.is_empty() {
|
|
|
|
if !sparse {
|
|
|
|
children.push(task);
|
|
|
|
}
|
|
|
|
return children;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return if self.filter(task) { vec![task] } else { vec![] };
|
2024-11-18 14:43:49 +01:00
|
|
|
})
|
|
|
|
.collect_vec()
|
2024-07-24 21:11:36 +03:00
|
|
|
}
|
2024-07-25 00:26:29 +03:00
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
/// Executes the given function with each task referenced by this event with no or property 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-11-18 14:40:50 +01:00
|
|
|
if let Some(event_tag) = match_event_tag(tag) {
|
|
|
|
if event_tag.marker
|
|
|
|
.as_ref()
|
|
|
|
.is_none_or(|m| m.to_string() == MARKER_PROPERTY)
|
|
|
|
{
|
|
|
|
self.tasks.get_mut(&event_tag.id).map(|t| {
|
2024-08-18 22:24:14 +03:00
|
|
|
found = true;
|
2024-11-18 14:40:50 +01:00
|
|
|
f(t);
|
2024-08-18 22:24:14 +03:00
|
|
|
});
|
|
|
|
}
|
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-11-18 14:40:50 +01:00
|
|
|
self.get_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-09-07 13:03:30 +03:00
|
|
|
fn filter(&self, task: &Task) -> bool {
|
|
|
|
self.state.matches(task) &&
|
2024-11-24 23:14:35 +01:00
|
|
|
(!task.is_task() || self.pubkey.is_none_or(|p| p == task.event.pubkey)) &&
|
2024-11-09 20:41:22 +01:00
|
|
|
self.priority.is_none_or(|prio| {
|
|
|
|
task.priority().unwrap_or(DEFAULT_PRIO) >= prio
|
|
|
|
}) &&
|
2024-11-21 09:17:56 +01:00
|
|
|
!task.list_hashtags().any(|tag| self.tags_excluded.contains(&tag)) &&
|
2024-11-12 23:03:53 +01:00
|
|
|
(self.tags.is_empty() || {
|
2024-11-21 09:17:56 +01:00
|
|
|
let mut iter = task.list_hashtags().sorted_unstable();
|
|
|
|
self.tags.iter().all(|tag| iter.any(|t| &t == tag))
|
2024-11-12 23:03:53 +01:00
|
|
|
})
|
2024-09-07 13:03:30 +03:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:22:28 +01:00
|
|
|
// TODO sparse is deprecated and only left for tests
|
2024-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn filtered_tasks(
|
|
|
|
&self,
|
|
|
|
position: Option<EventId>,
|
|
|
|
sparse: bool,
|
|
|
|
) -> Vec<&Task> {
|
2024-10-14 16:10:56 +02:00
|
|
|
let roots = self.tasks.children_for(position);
|
2024-11-18 14:43:49 +01:00
|
|
|
let mut current =
|
|
|
|
self.resolve_tasks_rec(roots, sparse, self.search_depth + self.view_depth);
|
2024-09-07 16:26:55 +03:00
|
|
|
if current.is_empty() {
|
|
|
|
if !self.tags.is_empty() {
|
2024-11-18 14:40:50 +01:00
|
|
|
let mut children = self.tasks.children_for(position).peekable();
|
2024-09-07 16:26:55 +03:00
|
|
|
if children.peek().is_some() {
|
|
|
|
current = self.resolve_tasks_rec(children, true, 9);
|
|
|
|
if sparse {
|
|
|
|
if current.is_empty() {
|
|
|
|
println!("No tasks here matching{}", self.get_prompt_suffix());
|
|
|
|
} else {
|
2024-10-12 11:54:29 +02:00
|
|
|
println!("Found matching tasks beyond specified search depth:");
|
2024-09-07 16:26:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
let ids = current.iter().map(|t| t.get_id()).collect_vec();
|
|
|
|
let mut bookmarks =
|
|
|
|
if sparse && current.is_empty() {
|
2024-08-29 23:28:40 +03:00
|
|
|
vec![]
|
|
|
|
} else {
|
2024-09-07 16:25:44 +03:00
|
|
|
// TODO highlight bookmarks
|
2024-08-29 23:28:40 +03:00
|
|
|
self.bookmarks.iter()
|
2024-11-18 14:40:50 +01:00
|
|
|
.filter(|id| !position.is_some_and(|p| &&p == id) && !ids.contains(id))
|
2024-08-29 23:28:40 +03:00
|
|
|
.filter_map(|id| self.get_by_id(id))
|
2024-09-07 13:03:30 +03:00
|
|
|
.filter(|t| self.filter(t))
|
2024-08-29 23:28:40 +03:00
|
|
|
.collect_vec()
|
|
|
|
};
|
2024-09-07 13:03:30 +03:00
|
|
|
current.append(&mut bookmarks);
|
2024-09-07 16:25:44 +03:00
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
current
|
2024-08-15 10:16:40 +03:00
|
|
|
}
|
|
|
|
|
2024-11-09 19:36:06 +01:00
|
|
|
fn visible_tasks(&self) -> Vec<&Task> {
|
2024-11-21 23:56:26 +01:00
|
|
|
let tasks = self.viewed_tasks();
|
|
|
|
if self.view.is_empty() && !tasks.is_empty() {
|
|
|
|
let bookmarks = self.bookmarked_tasks_deduped(&tasks);
|
|
|
|
return bookmarks.chain(tasks.into_iter()).collect_vec();
|
2024-08-15 10:16:40 +03:00
|
|
|
}
|
2024-11-21 23:56:26 +01:00
|
|
|
tasks
|
|
|
|
}
|
|
|
|
|
|
|
|
fn viewed_tasks(&self) -> Vec<&Task> {
|
|
|
|
let view = self.view.iter()
|
|
|
|
.flat_map(|id| self.get_by_id(id))
|
|
|
|
.collect_vec();
|
|
|
|
if self.search_depth > 0 && view.is_empty() {
|
|
|
|
self.resolve_tasks_rec(
|
|
|
|
self.tasks.children_for(self.get_position()),
|
|
|
|
true,
|
|
|
|
self.search_depth + self.view_depth,
|
|
|
|
)
|
|
|
|
} else {
|
2024-11-22 10:06:50 +01:00
|
|
|
self.resolve_tasks_rec(view.into_iter(), true, self.view_depth + 1)
|
2024-08-15 10:16:40 +03:00
|
|
|
}
|
2024-11-21 23:56:26 +01:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:20:13 +01:00
|
|
|
fn quick_access_raw(&self) -> impl Iterator<Item=&EventId> {
|
2024-11-21 23:56:26 +01:00
|
|
|
// TODO add recent tasks (most time tracked + recently created)
|
|
|
|
self.bookmarks.iter()
|
|
|
|
.chain(
|
|
|
|
// Latest
|
|
|
|
self.tasks.values()
|
2024-11-22 10:06:50 +01:00
|
|
|
.sorted_unstable()
|
2024-11-21 23:56:26 +01:00
|
|
|
.take(3).map(|t| t.get_id()))
|
|
|
|
.chain(
|
|
|
|
// Highest Prio
|
|
|
|
self.tasks.values()
|
2024-11-22 11:20:13 +01:00
|
|
|
.filter_map(|t| t.priority().filter(|p| *p >= QUICK_PRIO)
|
|
|
|
.map(|p| (p, t)))
|
|
|
|
.sorted_unstable().rev()
|
2024-11-21 23:56:26 +01:00
|
|
|
.take(3).map(|(_, t)| t.get_id())
|
|
|
|
)
|
2024-11-22 11:20:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn bookmarked_tasks_deduped(&self, visible: &[&Task]) -> impl Iterator<Item=&Task> {
|
|
|
|
let tree = visible.iter()
|
|
|
|
.flat_map(|task| self.traverse_up_from(Some(task.event.id)))
|
|
|
|
.unique();
|
|
|
|
let pos = self.get_position();
|
|
|
|
let ids: HashSet<&EventId> = tree.map(|t| t.get_id()).chain(pos.as_ref()).collect();
|
|
|
|
self.quick_access_raw()
|
2024-11-21 23:56:26 +01:00
|
|
|
.filter(|id| !ids.contains(id))
|
|
|
|
.filter_map(|id| self.get_by_id(id))
|
|
|
|
.filter(|t| self.filter(t))
|
|
|
|
.sorted_by_cached_key(|t| self.sorting_key(t))
|
|
|
|
.dedup()
|
2024-07-19 09:35:03 +03:00
|
|
|
}
|
2024-07-19 01:15:11 +03:00
|
|
|
|
2024-08-11 10:01:46 +03:00
|
|
|
fn get_property(&self, task: &Task, str: &str) -> String {
|
2024-09-22 16:47:26 +02:00
|
|
|
let mut children = self.tasks.children_of(task).peekable();
|
2024-10-13 16:00:35 +02:00
|
|
|
// Only show progress for non-activities with children
|
2024-08-11 10:01:46 +03:00
|
|
|
let progress =
|
2024-10-13 16:00:35 +02:00
|
|
|
children.peek()
|
|
|
|
.filter(|_| task.is_task())
|
|
|
|
.and_then(|_| self.total_progress(task.get_id()));
|
2024-08-11 10:01:46 +03:00
|
|
|
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;
|
2024-09-22 16:24:07 +02:00
|
|
|
for subtask in children {
|
2024-08-11 10:01:46 +03:00
|
|
|
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" => {
|
2024-11-18 14:43:49 +01:00
|
|
|
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-11 10:01:46 +03:00
|
|
|
}
|
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
|
|
|
|
2024-11-25 02:29:23 +01:00
|
|
|
"owner" => format!("{:.6}", self.users.get_username(&task.get_owner())),
|
2024-11-24 23:42:47 +01:00
|
|
|
"author" | "creator" => format!("{:.6}", self.users.get_username(&task.event.pubkey)), // FIXME temporary until proper column alignment
|
2024-11-18 14:43:49 +01:00
|
|
|
"prio" => self
|
|
|
|
.traverse_up_from(Some(task.event.id))
|
|
|
|
.find_map(Task::priority_raw)
|
|
|
|
.map(|p| p.to_string())
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
if self.priority.is_some() {
|
|
|
|
DEFAULT_PRIO.to_string().dimmed().to_string()
|
|
|
|
} else {
|
|
|
|
"".to_string()
|
|
|
|
}
|
2024-11-09 20:41:22 +01:00
|
|
|
}),
|
2024-08-11 10:01:46 +03:00
|
|
|
"path" => self.get_task_path(Some(task.event.id)),
|
2024-11-20 19:05:33 +01:00
|
|
|
"rpath" => self.get_relative_path(task.event.id),
|
2024-08-11 10:01:46 +03:00
|
|
|
// 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())),
|
2024-08-25 14:46:07 +03:00
|
|
|
prop => task.get(prop).unwrap_or_default(),
|
2024-08-11 10:01:46 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-24 23:42:47 +01:00
|
|
|
pub(super) fn find_user(&self, name: &str) -> Option<(PublicKey, String)> {
|
|
|
|
self.users.find_user_with_displayname(name)
|
2024-08-18 21:33:04 +03:00
|
|
|
}
|
|
|
|
|
2024-07-25 22:10:01 +03:00
|
|
|
// Movement and Selection
|
|
|
|
|
2024-09-07 16:25:44 +03:00
|
|
|
/// Toggle bookmark on the given id.
|
|
|
|
/// Returns whether it was added (true) or removed (false).
|
|
|
|
pub(crate) fn toggle_bookmark(&mut self, id: EventId) -> nostr_sdk::Result<bool> {
|
|
|
|
let added = match self.bookmarks.iter().position(|b| b == &id) {
|
|
|
|
None => {
|
|
|
|
self.bookmarks.push(id);
|
|
|
|
true
|
|
|
|
}
|
|
|
|
Some(pos) => {
|
|
|
|
self.bookmarks.remove(pos);
|
|
|
|
false
|
|
|
|
}
|
|
|
|
};
|
2024-11-18 14:52:52 +01:00
|
|
|
self.sender.submit(
|
|
|
|
EventBuilder::new(Kind::Bookmarks, "mostr pins")
|
|
|
|
.tags(self.bookmarks.iter().map(|id| Tag::event(*id)))
|
|
|
|
)?;
|
2024-09-07 16:25:44 +03:00
|
|
|
Ok(added)
|
2024-08-29 23:20:31 +03:00
|
|
|
}
|
|
|
|
|
2024-11-20 23:22:28 +01:00
|
|
|
pub(crate) fn reset_key_filter(&mut self) {
|
|
|
|
let own = self.sender.pubkey();
|
|
|
|
if self.pubkey.is_some_and(|k| k == own) {
|
|
|
|
info!("Showing everybody's tasks");
|
|
|
|
self.pubkey = None
|
|
|
|
} else {
|
|
|
|
info!("Showing own tasks");
|
|
|
|
self.pubkey = Some(own)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn set_key_filter(&mut self, key: PublicKey) {
|
|
|
|
self.pubkey = Some(key)
|
2024-09-07 13:03:30 +03:00
|
|
|
}
|
|
|
|
|
2024-11-20 23:23:15 +01:00
|
|
|
pub(crate) fn set_filter_since(&mut self, time: Timestamp) -> bool {
|
2024-09-22 20:05:05 +02:00
|
|
|
// TODO filter at both ends
|
2024-09-07 13:03:30 +03:00
|
|
|
self.set_filter(|t| t.last_state_update() > time)
|
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn get_filtered<P>(&self, position: Option<EventId>, predicate: P) -> Vec<EventId>
|
2024-09-07 13:03:30 +03:00
|
|
|
where
|
|
|
|
P: Fn(&&Task) -> bool,
|
|
|
|
{
|
2024-11-09 19:36:06 +01:00
|
|
|
self.filtered_tasks(position, false)
|
2024-09-07 13:03:30 +03:00
|
|
|
.into_iter()
|
|
|
|
.filter(predicate)
|
|
|
|
.map(|t| t.event.id)
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn set_filter<P>(&mut self, predicate: P) -> bool
|
|
|
|
where
|
|
|
|
P: Fn(&&Task) -> bool,
|
|
|
|
{
|
2024-11-18 14:40:50 +01:00
|
|
|
self.set_view(self.get_filtered(self.get_position(), predicate))
|
2024-08-29 23:20:31 +03:00
|
|
|
}
|
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
pub(crate) fn set_view_bookmarks(&mut self) -> bool {
|
|
|
|
self.set_view(self.bookmarks.clone())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set currently visible tasks.
|
|
|
|
/// Returns whether there are any.
|
|
|
|
pub(crate) fn set_view(&mut self, view: Vec<EventId>) -> bool {
|
2024-08-15 10:33:52 +03:00
|
|
|
if view.is_empty() {
|
2024-09-07 13:03:30 +03:00
|
|
|
warn!("No match for filter!");
|
|
|
|
self.view = view;
|
|
|
|
return false;
|
2024-08-15 10:33:52 +03:00
|
|
|
}
|
2024-07-26 21:45:29 +03:00
|
|
|
self.view = view;
|
2024-09-07 13:03:30 +03:00
|
|
|
true
|
2024-07-25 22:10:01 +03:00
|
|
|
}
|
|
|
|
|
2024-08-25 15:37:05 +03:00
|
|
|
pub(crate) fn clear_filters(&mut self) {
|
2024-09-07 13:03:30 +03:00
|
|
|
self.state = StateFilter::Default;
|
2024-11-20 23:22:28 +01:00
|
|
|
self.pubkey = Some(self.sender.pubkey());
|
2024-11-22 11:20:13 +01:00
|
|
|
self.priority = None;
|
2024-08-08 00:18:34 +03:00
|
|
|
self.view.clear();
|
|
|
|
self.tags.clear();
|
2024-08-11 12:28:08 +03:00
|
|
|
self.tags_excluded.clear();
|
2024-11-20 23:22:28 +01:00
|
|
|
info!("Reset all filters");
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
|
|
|
|
2024-08-25 15:37:05 +03:00
|
|
|
pub(crate) fn has_tag_filter(&self) -> bool {
|
|
|
|
!self.tags.is_empty() || !self.tags_excluded.is_empty()
|
|
|
|
}
|
|
|
|
|
2024-11-10 20:41:13 +01:00
|
|
|
pub(crate) fn print_hashtags(&self) {
|
2024-11-18 14:43:49 +01:00
|
|
|
println!(
|
|
|
|
"Hashtags of all known tasks:\n{}",
|
2024-11-21 09:47:14 +01:00
|
|
|
self.all_hashtags().into_iter().join(" ").italic()
|
2024-11-18 14:43:49 +01:00
|
|
|
);
|
2024-11-10 20:41:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if tags have been updated, false if it printed something
|
2024-11-21 09:17:56 +01:00
|
|
|
pub(crate) fn update_tags(&mut self, tags: impl IntoIterator<Item=Hashtag>) -> bool {
|
2024-11-10 20:41:13 +01:00
|
|
|
let mut peekable = tags.into_iter().peekable();
|
|
|
|
if self.tags.is_empty() && peekable.peek().is_none() {
|
|
|
|
if !self.tags_excluded.is_empty() {
|
|
|
|
self.tags_excluded.clear();
|
|
|
|
}
|
|
|
|
self.print_hashtags();
|
|
|
|
false
|
|
|
|
} else {
|
|
|
|
self.set_tags(peekable);
|
|
|
|
true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-21 09:17:56 +01:00
|
|
|
fn set_tags(&mut self, tags: impl IntoIterator<Item=Hashtag>) {
|
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-11-21 09:47:14 +01:00
|
|
|
pub(crate) fn add_tag(&mut self, tag: &str) {
|
2024-07-25 22:40:35 +03:00
|
|
|
self.view.clear();
|
2024-08-08 00:18:34 +03:00
|
|
|
info!("Added tag filter for #{tag}");
|
2024-11-21 09:47:14 +01:00
|
|
|
let tag = Hashtag::from(tag);
|
2024-08-11 12:28:08 +03:00
|
|
|
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();
|
2024-11-21 09:47:14 +01:00
|
|
|
self.tags.retain(|t| !t.matches(tag));
|
2024-08-08 00:18:34 +03:00
|
|
|
if self.tags.len() < len {
|
2024-11-21 09:47:14 +01:00
|
|
|
info!("Removed tag filters containing {tag}");
|
2024-08-08 00:18:34 +03:00
|
|
|
} else {
|
2024-11-21 09:47:14 +01:00
|
|
|
self.tags_excluded.insert(Hashtag::from(tag).into());
|
2024-08-11 12:28:08 +03:00
|
|
|
info!("Excluding #{tag} from view");
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
2024-08-01 20:12:04 +03:00
|
|
|
}
|
|
|
|
|
2024-11-09 19:18:42 +01:00
|
|
|
pub(crate) fn set_priority(&mut self, priority: Option<Prio>) {
|
|
|
|
self.view.clear();
|
|
|
|
match priority {
|
|
|
|
None => info!("Removing priority filter"),
|
|
|
|
Some(prio) => info!("Filtering for priority {}", prio),
|
|
|
|
}
|
|
|
|
self.priority = priority;
|
|
|
|
}
|
|
|
|
|
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) {
|
2024-11-22 11:21:04 +01:00
|
|
|
self.sender.force_flush();
|
2024-07-24 16:03:34 +03:00
|
|
|
}
|
2024-08-06 11:34:18 +03:00
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
/// Returns ids of tasks matching the given string.
|
2024-08-25 14:30:08 +03:00
|
|
|
///
|
|
|
|
/// Tries, in order:
|
|
|
|
/// - single case-insensitive exact name match in visible tasks
|
|
|
|
/// - single case-insensitive exact name match in all tasks
|
|
|
|
/// - visible tasks starting with given arg case-sensitive
|
|
|
|
/// - visible tasks where any word starts with given arg case-insensitive
|
2024-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn get_matching(&self, position: Option<EventId>, 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
|
|
|
}
|
|
|
|
let lowercase_arg = arg.to_ascii_lowercase();
|
2024-10-01 23:20:08 +02:00
|
|
|
// TODO apply regex to all matching
|
|
|
|
let regex = Regex::new(&format!(r"\b{}", lowercase_arg)).unwrap();
|
2024-08-25 14:30:08 +03:00
|
|
|
|
|
|
|
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
2024-08-29 22:15:30 +03:00
|
|
|
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
2024-09-07 13:03:30 +03:00
|
|
|
for task in self.filtered_tasks(position, false) {
|
2024-09-14 16:06:47 +03:00
|
|
|
let content = task.get_filter_title();
|
|
|
|
let lowercase = content.to_ascii_lowercase();
|
2024-08-01 21:11:33 +03:00
|
|
|
if lowercase == lowercase_arg {
|
2024-08-06 11:34:18 +03:00
|
|
|
return vec![task.event.id];
|
2024-09-14 16:06:47 +03:00
|
|
|
} else if content.starts_with(arg) {
|
2024-08-01 21:11:33 +03:00
|
|
|
filtered.push(task.event.id)
|
2024-10-01 23:20:08 +02:00
|
|
|
} else if regex.is_match(lowercase.as_bytes()) {
|
2024-08-29 22:15:30 +03:00
|
|
|
filtered_fuzzy.push(task.event.id)
|
2024-08-01 21:11:33 +03:00
|
|
|
}
|
|
|
|
}
|
2024-09-14 16:17:30 +03:00
|
|
|
// Find global exact match
|
2024-08-25 14:30:08 +03:00
|
|
|
for task in self.tasks.values() {
|
2024-09-14 16:06:47 +03:00
|
|
|
if task.get_filter_title().to_ascii_lowercase() == lowercase_arg &&
|
2024-08-25 16:46:21 +03:00
|
|
|
// exclude closed tasks and their subtasks
|
2024-11-18 14:43:49 +01:00
|
|
|
!self.traverse_up_from(Some(*task.get_id())).any(|t| !t.pure_state().is_open())
|
|
|
|
{
|
2024-08-25 14:30:08 +03:00
|
|
|
return vec![task.event.id];
|
|
|
|
}
|
|
|
|
}
|
2024-08-29 12:11:43 +03:00
|
|
|
|
2024-08-25 14:30:08 +03:00
|
|
|
if filtered.is_empty() {
|
2024-08-29 22:15:30 +03:00
|
|
|
filtered = filtered_fuzzy;
|
2024-08-29 12:11:43 +03:00
|
|
|
}
|
|
|
|
let immediate = filtered.iter().filter(
|
2024-11-18 14:40:50 +01:00
|
|
|
|t| self.get_by_id(t).is_some_and(|t| t.parent_id() == position.as_ref())).collect_vec();
|
2024-08-29 12:11:43 +03:00
|
|
|
if immediate.len() == 1 {
|
|
|
|
return immediate.into_iter().cloned().collect_vec();
|
2024-08-01 21:11:33 +03:00
|
|
|
}
|
2024-08-25 14:46:07 +03:00
|
|
|
filtered
|
2024-08-01 21:40:15 +03:00
|
|
|
}
|
|
|
|
|
2024-09-14 16:17:30 +03:00
|
|
|
/// Finds out what to do with the given string, one of:
|
|
|
|
/// - filtering the visible tasks
|
|
|
|
/// - entering the only matching task
|
|
|
|
/// - creating a new task
|
2024-08-01 21:40:15 +03:00
|
|
|
/// Returns an EventId if a new Task was created.
|
2024-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn filter_or_create(
|
|
|
|
&mut self,
|
|
|
|
position: Option<EventId>,
|
|
|
|
arg: &str,
|
|
|
|
) -> Option<EventId> {
|
2024-09-07 13:03:30 +03:00
|
|
|
let filtered = self.get_matching(position, arg);
|
2024-08-01 21:11:33 +03:00
|
|
|
match filtered.len() {
|
|
|
|
0 => {
|
|
|
|
// No match, new task
|
|
|
|
self.view.clear();
|
2024-08-25 11:16:05 +03:00
|
|
|
if arg.len() < CHARACTER_THRESHOLD {
|
|
|
|
warn!("New task name needs at least {CHARACTER_THRESHOLD} characters");
|
2024-08-25 14:46:07 +03:00
|
|
|
return None;
|
2024-08-08 13:04:52 +03:00
|
|
|
}
|
2024-08-25 11:16:05 +03:00
|
|
|
Some(self.make_task_with(arg, self.position_tags_for(position), true))
|
2024-08-01 21:11:33 +03:00
|
|
|
}
|
|
|
|
1 => {
|
|
|
|
// One match, activate
|
|
|
|
self.move_to(filtered.into_iter().nth(0));
|
|
|
|
None
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
// Multiple match, filter
|
2024-11-18 14:40:50 +01:00
|
|
|
self.move_to(position);
|
2024-09-07 13:03:30 +03:00
|
|
|
self.set_view(filtered);
|
2024-08-01 21:11:33 +03:00
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-19 13:47:27 +03:00
|
|
|
/// Returns all recent events from history until the first event at or before the given timestamp.
|
2024-08-25 10:48:59 +03:00
|
|
|
fn history_from(&self, stamp: Timestamp) -> impl Iterator<Item=&Event> {
|
2024-11-18 14:43:49 +01:00
|
|
|
self.history.get(&self.sender.pubkey())
|
|
|
|
.map(|hist| {
|
|
|
|
hist.values().rev().take_while_inclusive(move |e| e.created_at > stamp)
|
|
|
|
}).into_iter().flatten()
|
2024-08-19 13:47:27 +03:00
|
|
|
}
|
|
|
|
|
2024-08-19 16:36:06 +03:00
|
|
|
pub(crate) fn move_to(&mut self, target: Option<EventId>) {
|
2024-11-24 23:11:19 +01:00
|
|
|
if let Some(time) = self.custom_time {
|
|
|
|
self.track_at(time, target);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-25 22:10:01 +03:00
|
|
|
self.view.clear();
|
2024-11-18 14:40:50 +01:00
|
|
|
let pos = self.get_position();
|
|
|
|
if target == pos {
|
2024-08-01 20:00:45 +03:00
|
|
|
debug!("Flushing Tasks because of move in place");
|
2024-11-22 11:21:04 +01:00
|
|
|
self.sender.flush();
|
2024-07-25 22:10:01 +03:00
|
|
|
return;
|
|
|
|
}
|
2024-08-19 13:06:20 +03:00
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
if !target
|
|
|
|
.and_then(|id| self.get_by_id(&id))
|
|
|
|
.is_some_and(|t| t.parent_id() == pos.as_ref())
|
|
|
|
{
|
2024-08-19 13:48:24 +03:00
|
|
|
debug!("Flushing Tasks because of move beyond child");
|
2024-11-22 11:21:04 +01:00
|
|
|
self.sender.flush();
|
2024-08-19 13:48:24 +03:00
|
|
|
}
|
|
|
|
|
2024-08-19 13:06:20 +03:00
|
|
|
let now = Timestamp::now();
|
2024-11-18 14:43:49 +01:00
|
|
|
let offset: u64 = self
|
|
|
|
.history_from(now)
|
|
|
|
.skip_while(|e| e.created_at.as_u64() > now.as_u64() + MAX_OFFSET)
|
|
|
|
.count() as u64;
|
2024-08-25 16:37:55 +03:00
|
|
|
if offset >= MAX_OFFSET {
|
2024-08-19 13:47:27 +03:00
|
|
|
warn!("Whoa you are moving around quickly! Give me a few seconds to process.")
|
|
|
|
}
|
2024-08-19 13:06:20 +03:00
|
|
|
self.submit(
|
2024-08-19 16:36:06 +03:00
|
|
|
build_tracking(target)
|
2024-11-22 11:22:28 +01:00
|
|
|
.custom_created_at(now + offset),
|
2024-08-19 13:06:20 +03:00
|
|
|
);
|
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 {
|
2024-11-15 17:18:30 +01:00
|
|
|
Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)), [
|
|
|
|
id.to_string(),
|
|
|
|
to_string_or_default(self.sender.url.as_ref()),
|
|
|
|
marker.to_string(),
|
|
|
|
])
|
2024-08-12 23:02:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn make_event_tag(&self, event: &Event, marker: &str) -> Tag {
|
2024-11-15 17:18:30 +01:00
|
|
|
Tag::custom(TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)), [
|
|
|
|
event.id.to_string(),
|
|
|
|
to_string_or_default(self.sender.url.as_ref()),
|
|
|
|
marker.to_string(),
|
|
|
|
event.pubkey.to_string(),
|
|
|
|
])
|
2024-08-12 23:02:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn parent_tag(&self) -> Option<Tag> {
|
2024-11-18 14:40:50 +01:00
|
|
|
self.get_position()
|
|
|
|
.map(|p| self.make_event_tag_from_id(p, MARKER_PARENT))
|
2024-08-12 23:02:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn position_tags(&self) -> Vec<Tag> {
|
2024-11-18 14:40:50 +01:00
|
|
|
self.position_tags_for(self.get_position())
|
2024-08-20 21:42:05 +03:00
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
pub(crate) fn position_tags_for(&self, position: Option<EventId>) -> Vec<Tag> {
|
2024-08-20 21:42:05 +03:00
|
|
|
position.map_or(vec![], |pos| {
|
|
|
|
let mut tags = Vec::with_capacity(2);
|
2024-11-18 14:40:50 +01:00
|
|
|
tags.push(self.make_event_tag_from_id(pos, MARKER_PARENT));
|
|
|
|
self.get_by_id(&pos).map(|task| {
|
|
|
|
if task.pure_state() == State::Procedure {
|
|
|
|
self.tasks
|
|
|
|
.children_of(task)
|
|
|
|
.max()
|
|
|
|
.map(|t| tags.push(self.make_event_tag(&t.event, MARKER_DEPENDS)));
|
|
|
|
}
|
|
|
|
});
|
2024-08-20 21:42:05 +03:00
|
|
|
tags
|
|
|
|
})
|
2024-08-12 23:02:50 +03:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:22:28 +01:00
|
|
|
fn context_hashtags(&self) -> impl Iterator<Item=Tag> + '_ {
|
2024-11-21 09:17:56 +01:00
|
|
|
self.tags.iter().map(Tag::from)
|
|
|
|
}
|
2024-11-21 23:56:26 +01:00
|
|
|
|
2024-08-12 23:02:50 +03:00
|
|
|
/// Creates a task following the current state
|
2024-08-20 21:42:05 +03:00
|
|
|
///
|
2024-08-12 23:02:50 +03:00
|
|
|
/// Sanitizes input
|
|
|
|
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
2024-08-20 21:42:05 +03:00
|
|
|
self.make_task_with(input, self.position_tags(), true)
|
2024-08-12 23:02:50 +03:00
|
|
|
}
|
|
|
|
|
2024-08-20 21:42:05 +03:00
|
|
|
pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) {
|
|
|
|
let id = self.make_task_with(input, self.position_tags(), false);
|
2024-08-12 23:02:50 +03:00
|
|
|
self.set_state_for(id, "", state);
|
|
|
|
self.move_to(Some(id));
|
|
|
|
}
|
|
|
|
|
2024-11-08 11:49:49 +01:00
|
|
|
/// Moves up and creates a sibling task dependent on the current one
|
|
|
|
///
|
|
|
|
/// Returns true if successful, false if there is no current task
|
|
|
|
pub(crate) fn make_dependent_sibling(&mut self, input: &str) -> bool {
|
|
|
|
if let Some(pos) = self.get_position() {
|
|
|
|
self.move_up();
|
|
|
|
self.make_task_with(
|
|
|
|
input,
|
2024-11-18 14:43:49 +01:00
|
|
|
self.get_position()
|
|
|
|
.map(|par| self.make_event_tag_from_id(par, MARKER_PARENT))
|
|
|
|
.into_iter()
|
|
|
|
.chain(once(self.make_event_tag_from_id(pos, MARKER_DEPENDS))),
|
|
|
|
true,
|
|
|
|
);
|
2024-11-08 11:49:49 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
2024-08-20 21:42:05 +03:00
|
|
|
/// Creates a task including current tag filters
|
|
|
|
///
|
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-11-25 02:15:18 +01:00
|
|
|
let (input, input_tags) = extract_tags(input.trim(), &self.users);
|
2024-11-09 19:18:42 +01:00
|
|
|
let prio =
|
2024-11-09 20:33:29 +01:00
|
|
|
if input_tags.iter().any(|t| t.kind().to_string() == PRIO) { None } else { self.priority.map(|p| to_prio_tag(p)) };
|
2024-11-20 19:05:33 +01:00
|
|
|
info!("Created task \"{input}\" with tags [{}]", join_tags(&input_tags));
|
2024-08-08 21:10:17 +03:00
|
|
|
let id = self.submit(
|
2024-11-18 14:52:52 +01:00
|
|
|
EventBuilder::new(TASK_KIND, &input)
|
|
|
|
.tags(input_tags)
|
2024-11-21 09:17:56 +01:00
|
|
|
.tags(self.context_hashtags())
|
2024-11-18 14:52:52 +01:00
|
|
|
.tags(tags)
|
|
|
|
.tags(prio)
|
2024-08-08 21:10:17 +03:00
|
|
|
);
|
2024-08-12 23:02:50 +03:00
|
|
|
if set_state {
|
2024-11-11 22:56:42 +01:00
|
|
|
self.state
|
|
|
|
.as_option()
|
|
|
|
.inspect(|s| self.set_state_for_with(id, s));
|
2024-08-12 23:02:50 +03:00
|
|
|
}
|
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-11-18 14:43:49 +01:00
|
|
|
self.get_by_id(id).map_or(id.to_string(), |t| t.get_title())
|
2024-08-06 17:52:20 +03:00
|
|
|
}
|
|
|
|
|
2024-08-25 16:37:55 +03:00
|
|
|
/// Parse relative time string and track for current position
|
|
|
|
///
|
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-19 11:45:12 +03:00
|
|
|
parse_tracking_stamp(str)
|
2024-08-25 16:37:55 +03:00
|
|
|
.and_then(|stamp| self.track_at(stamp, self.get_position()))
|
2024-08-19 11:45:12 +03:00
|
|
|
.is_some()
|
|
|
|
}
|
|
|
|
|
2024-11-18 14:43:49 +01:00
|
|
|
pub(crate) fn track_at(
|
|
|
|
&mut self,
|
|
|
|
mut time: Timestamp,
|
|
|
|
target: Option<EventId>,
|
|
|
|
) -> Option<EventId> {
|
2024-08-27 15:00:53 +03:00
|
|
|
if target.is_none() {
|
2024-11-08 11:30:08 +01:00
|
|
|
// Prevent random overlap with tracking started in the same second
|
2024-08-27 15:00:53 +03:00
|
|
|
time = time - 1;
|
|
|
|
} else if let Some(hist) = self.history.get(&self.sender.pubkey()) {
|
|
|
|
while hist.get(&time).is_some() {
|
|
|
|
time = time + 1;
|
|
|
|
}
|
|
|
|
}
|
2024-08-25 16:37:55 +03:00
|
|
|
let current_pos = self.get_position_at(time);
|
2024-11-18 14:40:50 +01:00
|
|
|
if (time < Timestamp::now() || target.is_none()) && current_pos.1 == target {
|
|
|
|
warn!(
|
|
|
|
"Already {} from {}",
|
|
|
|
target.map_or("stopped time-tracking".to_string(), |id| format!(
|
|
|
|
"tracking \"{}\"",
|
|
|
|
self.get_task_title(&id)
|
|
|
|
)),
|
2024-08-25 16:37:55 +03:00
|
|
|
format_timestamp_relative(¤t_pos.0),
|
|
|
|
);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
info!("{}", match target {
|
2024-11-18 14:40:50 +01:00
|
|
|
None => format!(
|
|
|
|
"Stopping time-tracking of \"{}\" at {}",
|
|
|
|
current_pos.1.map_or("???".to_string(), |id| self.get_task_title(&id)),
|
|
|
|
format_timestamp_relative(&time)
|
|
|
|
),
|
|
|
|
Some(new_id) => format!(
|
|
|
|
"Tracking \"{}\" from {}{}",
|
|
|
|
self.get_task_title(&new_id),
|
|
|
|
format_timestamp_relative(&time),
|
|
|
|
current_pos.1.filter(|id| id != &new_id).map(|id|
|
|
|
|
format!(" replacing \"{}\"", self.get_task_title(&id)))
|
|
|
|
.unwrap_or_default()
|
|
|
|
)
|
2024-08-25 16:37:55 +03:00
|
|
|
});
|
2024-08-19 11:45:12 +03:00
|
|
|
self.submit(
|
2024-08-25 16:37:55 +03:00
|
|
|
build_tracking(target)
|
2024-08-19 11:45:12 +03:00
|
|
|
.custom_created_at(time)
|
2024-08-25 16:37:55 +03:00
|
|
|
).into()
|
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-09-22 20:05:05 +02:00
|
|
|
fn submit(&mut self, mut builder: EventBuilder) -> EventId {
|
|
|
|
if let Some(stamp) = self.custom_time {
|
|
|
|
builder = builder.custom_created_at(stamp);
|
|
|
|
}
|
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-11-21 21:18:26 +01:00
|
|
|
let author = event.pubkey;
|
2024-11-24 23:42:47 +01:00
|
|
|
self.users.create(author);
|
2024-08-29 22:12:55 +03:00
|
|
|
match event.kind {
|
|
|
|
Kind::GitIssue => self.add_task(event),
|
2024-11-18 14:43:49 +01:00
|
|
|
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
|
|
|
Ok(metadata) => { self.users.insert(event.pubkey, metadata); }
|
|
|
|
Err(e) => warn!("Cannot parse metadata: {} from {:?}", e, event),
|
|
|
|
},
|
2024-08-29 22:12:55 +03:00
|
|
|
Kind::Bookmarks => {
|
2024-08-29 22:28:25 +03:00
|
|
|
if event.pubkey == self.sender.pubkey() {
|
2024-11-18 14:40:50 +01:00
|
|
|
self.bookmarks = referenced_events(&event).collect_vec()
|
2024-08-29 22:28:25 +03:00
|
|
|
}
|
2024-08-29 22:12:55 +03:00
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
if event.kind == TRACKING_KIND {
|
|
|
|
match self.history.get_mut(&event.pubkey) {
|
2024-11-18 14:40:50 +01:00
|
|
|
Some(c) => {
|
|
|
|
c.insert(event.created_at, event);
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
self.history
|
|
|
|
.insert(event.pubkey, BTreeMap::from([(event.created_at, event)]));
|
|
|
|
}
|
2024-08-29 22:12:55 +03:00
|
|
|
}
|
|
|
|
} else {
|
2024-09-14 17:14:51 +03:00
|
|
|
if let Some(event) = self.add_prop(event) {
|
|
|
|
debug!("Requeueing unknown Event {:?}", event);
|
|
|
|
self.overflow.push_back(event);
|
|
|
|
}
|
2024-08-29 22:12:55 +03:00
|
|
|
}
|
|
|
|
}
|
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-20 13:00:36 +03:00
|
|
|
warn!("Did not insert duplicate event {}", event.id);
|
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);
|
|
|
|
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-09-14 17:14:51 +03:00
|
|
|
/// Add event as prop, returning it if not processable
|
|
|
|
fn add_prop(&mut self, event: Event) -> Option<Event> {
|
2024-08-18 22:24:14 +03:00
|
|
|
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 {
|
2024-10-03 13:39:52 +02:00
|
|
|
if event.kind == Kind::TextNote {
|
2024-08-18 22:24:14 +03:00
|
|
|
self.add_task(event);
|
2024-09-14 17:14:51 +03:00
|
|
|
} else {
|
2024-10-03 13:39:52 +02:00
|
|
|
return Some(event);
|
2024-08-18 22:24:14 +03:00
|
|
|
}
|
|
|
|
}
|
2024-09-14 17:14:51 +03:00
|
|
|
None
|
2024-07-19 21:04:21 +03:00
|
|
|
}
|
2024-07-19 01:15:11 +03:00
|
|
|
|
2024-08-29 11:50:34 +03:00
|
|
|
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {
|
|
|
|
self.history.get(&self.sender.pubkey())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_own_events_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
2024-11-25 01:45:18 +01:00
|
|
|
self.get_own_history()
|
2024-11-18 14:43:49 +01:00
|
|
|
.into_iter()
|
|
|
|
.flat_map(|t| t.values())
|
2024-08-06 17:52:20 +03:00
|
|
|
}
|
|
|
|
|
2024-11-25 01:45:18 +01:00
|
|
|
pub(super) fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
2024-08-29 11:50:34 +03:00
|
|
|
self.get_own_history().into_iter().flat_map(|hist| {
|
|
|
|
let now = now();
|
2024-11-25 01:45:18 +01:00
|
|
|
hist.values().rev()
|
|
|
|
.skip_while(move |e| e.created_at > now)
|
|
|
|
.dedup_by(|e1, e2| e1.id == e2.id)
|
2024-08-29 11:50:34 +03:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn move_back_to(&mut self, str: &str) -> bool {
|
|
|
|
let lower = str.to_ascii_lowercase();
|
2024-11-25 01:45:18 +01:00
|
|
|
let found =
|
|
|
|
self.history_before_now()
|
|
|
|
.find(|e| {
|
|
|
|
referenced_event(e)
|
|
|
|
.and_then(|id| self.get_by_id(&id))
|
|
|
|
.is_some_and(|t| t.event.content.to_ascii_lowercase().contains(&lower))
|
|
|
|
});
|
2024-08-29 11:50:34 +03:00
|
|
|
if let Some(event) = found {
|
2024-11-18 14:40:50 +01:00
|
|
|
self.move_to(referenced_event(event));
|
2024-08-29 22:15:30 +03:00
|
|
|
return true;
|
2024-08-29 11:50:34 +03:00
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn move_back_by(&mut self, steps: usize) {
|
2024-11-18 14:43:49 +01:00
|
|
|
let id = self
|
|
|
|
.history_before_now()
|
|
|
|
.nth(steps)
|
2024-08-29 22:17:46 +03:00
|
|
|
.and_then(|e| referenced_event(e));
|
2024-11-18 14:40:50 +01:00
|
|
|
self.move_to(id)
|
2024-08-29 11:50:34 +03:00
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
self.tasks.remove(&event.id);
|
2024-08-27 15:00:53 +03:00
|
|
|
self.history.get_mut(&self.sender.pubkey())
|
2024-11-18 14:40:50 +01:00
|
|
|
.map(|t| {
|
|
|
|
t.retain(|_, e| e != event && !referenced_event(e).is_some_and(|id| id == event.id))
|
|
|
|
});
|
|
|
|
self.referenced_tasks(event, |t| {
|
|
|
|
t.props.remove(event);
|
|
|
|
});
|
2024-08-01 14:07:40 +03:00
|
|
|
}
|
|
|
|
|
2024-08-07 23:59:05 +03:00
|
|
|
pub(crate) fn set_state_for_with(&mut self, id: EventId, comment: &str) {
|
2024-08-29 11:06:56 +03:00
|
|
|
self.set_state_for(id, comment, comment.try_into().unwrap_or(State::Open));
|
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-11-09 18:00:17 +01:00
|
|
|
let ids =
|
|
|
|
if state == State::Closed {
|
|
|
|
// Close whole subtree
|
2024-11-18 14:40:50 +01:00
|
|
|
ChildIterator::from(self, id).get_all()
|
2024-11-09 18:00:17 +01:00
|
|
|
} else {
|
2024-11-18 14:40:50 +01:00
|
|
|
vec![id]
|
2024-11-09 18:00:17 +01:00
|
|
|
};
|
2024-11-25 02:15:18 +01:00
|
|
|
let (desc, tags) = extract_tags(comment, &self.users);
|
2024-11-18 14:52:52 +01:00
|
|
|
let prop =
|
|
|
|
EventBuilder::new(state.into(), desc)
|
|
|
|
.tags(ids.into_iter()
|
|
|
|
.map(|e| self.make_event_tag_from_id(e, MARKER_PROPERTY)))
|
|
|
|
.tags(tags);
|
2024-11-18 14:40:50 +01:00
|
|
|
info!(
|
2024-11-24 08:51:54 +01:00
|
|
|
"Task status {} set for \"{}\"{}{}",
|
2024-09-23 08:50:12 +02:00
|
|
|
TaskState::get_label_for(&state, comment),
|
|
|
|
self.get_task_title(&id),
|
2024-11-18 14:40:50 +01:00
|
|
|
self.custom_time
|
|
|
|
.map(|ts| format!(" at {}", format_timestamp_relative(&ts)))
|
2024-11-24 08:51:54 +01:00
|
|
|
.unwrap_or_default(),
|
|
|
|
self.get_by_id(&id)
|
|
|
|
.and_then(|task| task.state_at(self.custom_time.unwrap_or_default()))
|
|
|
|
.map(|ts| format!(" from {}", ts))
|
|
|
|
.unwrap_or_default());
|
2024-08-01 14:07:40 +03:00
|
|
|
self.submit(prop)
|
2024-07-19 01:15:11 +03:00
|
|
|
}
|
|
|
|
|
2024-08-19 16:36:06 +03:00
|
|
|
pub(crate) fn update_state(&mut self, comment: &str, state: State) -> Option<EventId> {
|
2024-11-18 14:40:50 +01:00
|
|
|
let id = self.get_position()?;
|
|
|
|
Some(self.set_state_for(id, comment, state))
|
2024-07-25 10:50:53 +03:00
|
|
|
}
|
|
|
|
|
2024-11-11 22:56:42 +01:00
|
|
|
/// Creates a note or activity, depending on whether the parent is a task.
|
|
|
|
/// Sanitizes Input.
|
2024-10-12 14:17:46 +02:00
|
|
|
pub(crate) fn make_note(&mut self, note: &str) -> EventId {
|
2024-11-25 02:15:18 +01:00
|
|
|
let (name, tags) = extract_tags(note.trim(), &self.users);
|
2024-11-20 19:05:33 +01:00
|
|
|
let format = format!("\"{name}\" with tags [{}]", join_tags(&tags));
|
2024-11-11 22:56:42 +01:00
|
|
|
let mut prop =
|
2024-11-23 12:11:31 +01:00
|
|
|
EventBuilder::new(Kind::TextNote, name)
|
|
|
|
.tags(tags);
|
2024-11-11 22:56:42 +01:00
|
|
|
let marker =
|
|
|
|
if self.get_current_task().is_some_and(|t| t.is_task()) {
|
|
|
|
MARKER_PROPERTY
|
|
|
|
} else {
|
|
|
|
// Activity if parent is not a task
|
2024-11-21 23:56:26 +01:00
|
|
|
prop = prop.tags(self.context_hashtags());
|
2024-11-11 22:56:42 +01:00
|
|
|
MARKER_PARENT
|
|
|
|
};
|
|
|
|
info!("Created {} {format}", if marker == MARKER_PROPERTY { "note" } else { "activity" } );
|
2024-08-18 22:24:14 +03:00
|
|
|
self.submit(
|
2024-11-21 23:56:26 +01:00
|
|
|
prop.tags(
|
2024-11-11 22:56:42 +01:00
|
|
|
self.get_position().map(|pos| self.make_event_tag_from_id(pos, marker))))
|
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
|
|
|
|
|
2024-10-11 01:10:17 +02:00
|
|
|
pub(crate) fn set_view_depth(&mut self, depth: usize) {
|
2024-10-12 11:35:43 +02:00
|
|
|
info!("Showing {depth} subtask levels");
|
2024-10-11 01:10:17 +02:00
|
|
|
self.view_depth = depth;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn set_search_depth(&mut self, depth: usize) {
|
2024-09-07 13:03:30 +03:00
|
|
|
if !self.view.is_empty() {
|
2024-08-29 12:02:13 +03:00
|
|
|
self.view.clear();
|
2024-10-11 01:10:17 +02:00
|
|
|
info!("Cleared search and changed search depth to {depth}");
|
2024-08-29 12:02:13 +03:00
|
|
|
} else {
|
2024-10-11 01:10:17 +02:00
|
|
|
info!("Changed search depth to {depth}");
|
2024-08-29 12:02:13 +03:00
|
|
|
}
|
2024-10-12 11:54:29 +02:00
|
|
|
self.search_depth = depth;
|
2024-08-07 00:06:09 +03:00
|
|
|
}
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-14 16:10:56 +02:00
|
|
|
impl Display for TasksRelay {
|
|
|
|
fn fmt(&self, lock: &mut Formatter<'_>) -> std::fmt::Result {
|
|
|
|
if let Some(t) = self.get_current_task() {
|
|
|
|
let state = t.state_or_default();
|
|
|
|
let now = &now();
|
|
|
|
let mut tracking_stamp: Option<Timestamp> = None;
|
|
|
|
for elem in
|
2024-11-18 14:40:50 +01:00
|
|
|
timestamps(self.get_own_events_history(), &[t.event.id])
|
2024-10-14 16:10:56 +02:00
|
|
|
.map(|(e, _)| e) {
|
|
|
|
if tracking_stamp.is_some() && elem > now {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
tracking_stamp = Some(*elem)
|
|
|
|
}
|
|
|
|
writeln!(
|
|
|
|
lock,
|
2024-11-24 08:51:54 +01:00
|
|
|
"Active from {} (total tracked time {}m) - {} since {}",
|
2024-10-14 16:10:56 +02:00
|
|
|
tracking_stamp.map_or("?".to_string(), |t| format_timestamp_relative(&t)),
|
|
|
|
self.time_tracked(*t.get_id()) / 60,
|
2024-11-24 08:51:54 +01:00
|
|
|
state,
|
2024-10-14 16:10:56 +02:00
|
|
|
format_timestamp_relative(&state.time)
|
|
|
|
)?;
|
2024-11-20 23:10:28 +01:00
|
|
|
for d in t.descriptions().rev() { writeln!(lock, "{}", d)?; }
|
2024-11-20 19:06:32 +01:00
|
|
|
writeln!(lock)?;
|
2024-10-14 16:10:56 +02:00
|
|
|
}
|
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
let visible = self.viewed_tasks();
|
2024-10-14 16:10:56 +02:00
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
if visible.is_empty() {
|
2024-11-22 09:28:46 +01:00
|
|
|
if self.tasks.children_for(self.get_position()).next().is_some() {
|
|
|
|
writeln!(lock, "No tasks here matching{}", self.get_prompt_suffix())?;
|
|
|
|
}
|
2024-10-14 16:10:56 +02:00
|
|
|
let (label, times) = self.times_tracked();
|
|
|
|
let mut times_recent = times.rev().take(6).collect_vec();
|
|
|
|
times_recent.reverse();
|
2024-11-22 11:20:13 +01:00
|
|
|
writeln!(lock, "{}\n{}", format!("Recent {}", label).italic(), times_recent.join("\n"))?;
|
2024-10-14 16:10:56 +02:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2024-11-09 19:36:52 +01:00
|
|
|
if self.view.is_empty() {
|
2024-11-21 23:56:26 +01:00
|
|
|
let mut bookmarks = self.bookmarked_tasks_deduped(&visible).peekable();
|
2024-11-09 19:36:52 +01:00
|
|
|
if bookmarks.peek().is_some() {
|
|
|
|
writeln!(lock, "{}", Colorize::bold("Quick Access"))?;
|
|
|
|
for task in bookmarks {
|
|
|
|
writeln!(
|
|
|
|
lock,
|
|
|
|
"{}",
|
|
|
|
self.properties.iter()
|
|
|
|
.map(|p| self.get_property(task, p.as_str()))
|
|
|
|
.join(" \t")
|
|
|
|
)?;
|
|
|
|
}
|
2024-10-14 16:43:59 +02:00
|
|
|
}
|
|
|
|
}
|
2024-10-14 16:10:56 +02:00
|
|
|
|
|
|
|
// TODO proper column alignment
|
|
|
|
// TODO hide empty columns
|
|
|
|
writeln!(lock, "{}", self.properties.join(" \t").bold())?;
|
2024-10-14 16:43:59 +02:00
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
let count = visible.len();
|
2024-10-14 16:43:59 +02:00
|
|
|
let mut total_time = 0;
|
2024-11-21 23:56:26 +01:00
|
|
|
for task in visible {
|
2024-10-14 16:10:56 +02:00
|
|
|
writeln!(
|
|
|
|
lock,
|
2024-11-22 11:22:28 +01:00
|
|
|
"{}", self.properties.iter()
|
2024-10-14 16:10:56 +02:00
|
|
|
.map(|p| self.get_property(task, p.as_str()))
|
|
|
|
.join(" \t")
|
|
|
|
)?;
|
2024-10-14 16:43:59 +02:00
|
|
|
total_time += self.total_time_tracked(task.event.id) // TODO include parent if it matches
|
2024-10-14 16:10:56 +02:00
|
|
|
}
|
|
|
|
|
2024-11-18 14:43:49 +01:00
|
|
|
writeln!(lock,
|
|
|
|
"{count} visible tasks{}",
|
|
|
|
display_time(" tracked a total of HHhMMm", total_time)
|
|
|
|
)?;
|
2024-10-14 16:10:56 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-11 10:58:34 +03:00
|
|
|
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 {
|
2024-11-18 14:40:50 +01:00
|
|
|
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.
|
2024-11-09 18:01:40 +01:00
|
|
|
/// - MMM - minutes
|
|
|
|
/// - MM - minutes of the hour
|
|
|
|
/// - HH - hours
|
|
|
|
///
|
|
|
|
/// Returns an empty string if under one 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-11-18 14:52:52 +01:00
|
|
|
.replace("MM", &format!("{:02}", mins.rem(60))),
|
2024-07-31 20:05:52 +03:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-11-20 19:05:33 +01:00
|
|
|
/// Joins the tasks of this upwards iterator.
|
|
|
|
/// * `include_last_id` whether to add the id of an unknown parent at the top
|
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-11-20 19:05:33 +01:00
|
|
|
.chain(
|
2024-11-18 14:43:49 +01:00
|
|
|
tasks.last()
|
2024-11-20 19:05:33 +01:00
|
|
|
.take_if(|_| include_last_id)
|
2024-07-30 09:02:56 +03:00
|
|
|
.and_then(|t| t.parent_id())
|
|
|
|
.map(|id| id.to_string())
|
2024-11-20 19:05:33 +01:00
|
|
|
.into_iter())
|
2024-07-25 10:55:29 +03:00
|
|
|
.fold(None, |acc, val| {
|
2024-11-18 14:43:49 +01:00
|
|
|
Some(acc.map_or_else(
|
|
|
|
|| val.clone(),
|
|
|
|
|cur| format!("{}{}{}", val, ">".dimmed(), cur),
|
|
|
|
))
|
2024-07-25 10:55:29 +03:00
|
|
|
})
|
2024-07-25 00:52:03 +03:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:22:28 +01:00
|
|
|
fn referenced_events(event: &Event) -> impl Iterator<Item=EventId> + '_ {
|
|
|
|
event.tags.iter().filter_map(|tag| match_event_tag(tag).map(|t| t.id))
|
2024-08-19 13:06:20 +03:00
|
|
|
}
|
|
|
|
|
2024-11-25 01:45:18 +01:00
|
|
|
pub fn referenced_event(event: &Event) -> Option<EventId> {
|
2024-08-29 22:28:25 +03:00
|
|
|
referenced_events(event).next()
|
|
|
|
}
|
|
|
|
|
2024-10-18 18:13:35 +02:00
|
|
|
/// Returns the id of a referenced event if it is contained in the provided ids list.
|
2024-11-18 14:40:50 +01:00
|
|
|
fn matching_tag_id<'a>(event: &'a Event, ids: &'a [EventId]) -> Option<EventId> {
|
2024-08-29 22:28:25 +03:00
|
|
|
referenced_events(event).find(|id| ids.contains(id))
|
2024-08-08 15:09:39 +03:00
|
|
|
}
|
|
|
|
|
2024-08-14 22:12:43 +03:00
|
|
|
/// Filters out event timestamps to those that start or stop one of the given events
|
2024-11-18 14:40:50 +01:00
|
|
|
fn timestamps<'a>(
|
|
|
|
events: impl Iterator<Item=&'a Event>,
|
|
|
|
ids: &'a [EventId],
|
|
|
|
) -> impl Iterator<Item=(&Timestamp, Option<EventId>)> {
|
|
|
|
events
|
|
|
|
.map(|event| (&event.created_at, matching_tag_id(event, ids)))
|
2024-08-08 15:09:39 +03:00
|
|
|
.dedup_by(|(_, e1), (_, e2)| e1 == e2)
|
2024-08-25 14:46:07 +03:00
|
|
|
.skip_while(|element| element.1.is_none())
|
2024-08-08 15:09:39 +03:00
|
|
|
}
|
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-19 17:30:05 +03:00
|
|
|
struct Durations<'a> {
|
2024-08-08 00:18:34 +03:00
|
|
|
events: Box<dyn Iterator<Item=&'a Event> + 'a>,
|
2024-11-18 14:40:50 +01:00
|
|
|
ids: &'a [EventId],
|
2024-08-10 21:17:07 +03:00
|
|
|
threshold: Option<Timestamp>,
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
2024-08-19 17:30:05 +03:00
|
|
|
impl Durations<'_> {
|
2024-11-18 14:40:50 +01:00
|
|
|
fn from<'b>(
|
|
|
|
events: impl IntoIterator<Item=&'b Event> + 'b,
|
|
|
|
ids: &'b [EventId],
|
|
|
|
) -> Durations<'b> {
|
2024-08-19 17:30:05 +03:00
|
|
|
Durations {
|
2024-08-08 00:18:34 +03:00
|
|
|
events: Box::new(events.into_iter()),
|
|
|
|
ids,
|
2024-08-25 10:48:59 +03:00
|
|
|
threshold: Some(Timestamp::now()), // TODO consider offset?
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-08-19 17:30:05 +03:00
|
|
|
impl Iterator for Durations<'_> {
|
2024-08-08 00:18:34 +03:00
|
|
|
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();
|
2024-11-18 14:43:49 +01:00
|
|
|
start.filter(|t| t < &now)
|
|
|
|
.map(|stamp| Duration::from_secs(now.saturating_sub(stamp)))
|
2024-08-08 00:18:34 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
enum ChildIteratorFilter {
|
|
|
|
Reject = 0b00,
|
|
|
|
TakeSelf = 0b01,
|
|
|
|
TakeChildren = 0b10,
|
|
|
|
Take = 0b11,
|
|
|
|
}
|
|
|
|
impl ChildIteratorFilter {
|
|
|
|
fn takes_children(&self) -> bool {
|
|
|
|
self == &ChildIteratorFilter::Take ||
|
|
|
|
self == &ChildIteratorFilter::TakeChildren
|
|
|
|
}
|
|
|
|
fn takes_self(&self) -> bool {
|
|
|
|
self == &ChildIteratorFilter::Take ||
|
|
|
|
self == &ChildIteratorFilter::TakeSelf
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-12 23:07:39 +03:00
|
|
|
/// Breadth-First Iterator over Tasks and recursive children
|
|
|
|
struct ChildIterator<'a> {
|
|
|
|
tasks: &'a TaskMap,
|
2024-08-19 16:47:09 +03:00
|
|
|
/// Found Events
|
2024-11-18 14:40:50 +01:00
|
|
|
queue: Vec<EventId>,
|
2024-08-19 16:47:09 +03:00
|
|
|
/// Index of the next element in the queue
|
2024-08-12 23:07:39 +03:00
|
|
|
index: usize,
|
2024-08-19 16:47:09 +03:00
|
|
|
/// Depth of the next element
|
|
|
|
depth: usize,
|
|
|
|
/// Element with the next depth boundary
|
|
|
|
next_depth_at: usize,
|
2024-08-12 23:07:39 +03:00
|
|
|
}
|
|
|
|
impl<'a> ChildIterator<'a> {
|
2024-09-07 13:03:30 +03:00
|
|
|
fn rooted(tasks: &'a TaskMap, id: Option<&EventId>) -> Self {
|
|
|
|
let mut queue = Vec::with_capacity(tasks.len());
|
|
|
|
queue.append(
|
|
|
|
&mut tasks
|
|
|
|
.values()
|
|
|
|
.filter(move |t| t.parent_id() == id)
|
2024-11-18 14:40:50 +01:00
|
|
|
.map(|t| t.event.id)
|
2024-09-07 13:03:30 +03:00
|
|
|
.collect_vec()
|
|
|
|
);
|
|
|
|
Self::with_queue(tasks, queue)
|
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
fn with_queue(tasks: &'a TaskMap, queue: Vec<EventId>) -> Self {
|
2024-09-07 13:03:30 +03:00
|
|
|
ChildIterator {
|
|
|
|
tasks: &tasks,
|
|
|
|
next_depth_at: queue.len(),
|
|
|
|
index: 0,
|
|
|
|
depth: 1,
|
|
|
|
queue,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
fn from(tasks: &'a TasksRelay, id: EventId) -> Self {
|
|
|
|
let mut queue = Vec::with_capacity(64);
|
2024-08-12 23:07:39 +03:00
|
|
|
queue.push(id);
|
|
|
|
ChildIterator {
|
2024-08-19 16:47:09 +03:00
|
|
|
tasks: &tasks.tasks,
|
2024-08-12 23:07:39 +03:00
|
|
|
queue,
|
|
|
|
index: 0,
|
2024-08-19 16:47:09 +03:00
|
|
|
depth: 0,
|
|
|
|
next_depth_at: 1,
|
2024-08-12 23:07:39 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-03 21:15:48 +03:00
|
|
|
/// Process until the given depth
|
|
|
|
/// Returns true if that depth was reached
|
|
|
|
fn process_depth(&mut self, depth: usize) -> bool {
|
2024-08-19 16:47:09 +03:00
|
|
|
while self.depth < depth {
|
2024-09-03 21:15:48 +03:00
|
|
|
if self.next().is_none() {
|
2024-09-07 13:03:30 +03:00
|
|
|
return false;
|
2024-09-03 21:15:48 +03:00
|
|
|
}
|
2024-08-19 16:47:09 +03:00
|
|
|
}
|
2024-09-03 21:15:48 +03:00
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2024-09-05 13:54:23 +03:00
|
|
|
/// Get all children
|
2024-11-18 14:40:50 +01:00
|
|
|
fn get_all(mut self) -> Vec<EventId> {
|
2024-08-12 23:07:39 +03:00
|
|
|
while self.next().is_some() {}
|
|
|
|
self.queue
|
|
|
|
}
|
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
/// Get all tasks until the specified depth
|
2024-11-18 14:40:50 +01:00
|
|
|
fn get_depth(mut self, depth: usize) -> Vec<EventId> {
|
2024-09-07 13:03:30 +03:00
|
|
|
self.process_depth(depth);
|
|
|
|
self.queue
|
|
|
|
}
|
|
|
|
|
|
|
|
fn check_depth(&mut self) {
|
2024-08-19 16:47:09 +03:00
|
|
|
if self.next_depth_at == self.index {
|
|
|
|
self.depth += 1;
|
|
|
|
self.next_depth_at = self.queue.len();
|
|
|
|
}
|
2024-09-07 13:03:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Get next id and advance, without adding children
|
2024-11-18 14:40:50 +01:00
|
|
|
fn next_task(&mut self) -> Option<EventId> {
|
2024-09-07 13:03:30 +03:00
|
|
|
if self.index >= self.queue.len() {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let id = self.queue[self.index];
|
|
|
|
self.index += 1;
|
2024-08-12 23:07:39 +03:00
|
|
|
Some(id)
|
|
|
|
}
|
2024-09-07 13:03:30 +03:00
|
|
|
|
|
|
|
/// Get the next known task and run it through the filter
|
|
|
|
fn next_filtered<F>(&mut self, filter: &F) -> Option<&'a Task>
|
|
|
|
where
|
|
|
|
F: Fn(&Task) -> ChildIteratorFilter,
|
|
|
|
{
|
|
|
|
self.next_task().and_then(|id| {
|
2024-11-18 14:40:50 +01:00
|
|
|
if let Some(task) = self.tasks.get(&id) {
|
2024-09-07 13:03:30 +03:00
|
|
|
let take = filter(task);
|
|
|
|
if take.takes_children() {
|
2024-09-22 16:47:26 +02:00
|
|
|
self.queue_children_of(&task);
|
2024-09-07 13:03:30 +03:00
|
|
|
}
|
|
|
|
if take.takes_self() {
|
|
|
|
self.check_depth();
|
|
|
|
return Some(task);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.check_depth();
|
|
|
|
self.next_filtered(filter)
|
|
|
|
})
|
|
|
|
}
|
2024-09-22 16:47:26 +02:00
|
|
|
|
|
|
|
fn queue_children_of(&mut self, task: &'a Task) {
|
2024-11-18 14:40:50 +01:00
|
|
|
self.queue.extend(self.tasks.children_ids_for(task.event.id));
|
2024-09-22 16:47:26 +02:00
|
|
|
}
|
2024-08-12 23:07:39 +03:00
|
|
|
}
|
2024-09-05 13:54:23 +03:00
|
|
|
impl FusedIterator for ChildIterator<'_> {}
|
2024-09-07 13:03:30 +03:00
|
|
|
impl<'a> Iterator for ChildIterator<'a> {
|
2024-11-18 14:40:50 +01:00
|
|
|
type Item = EventId;
|
2024-09-07 13:03:30 +03:00
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
self.next_task().inspect(|id| {
|
|
|
|
match self.tasks.get(id) {
|
|
|
|
None => {
|
|
|
|
// Unknown task, might still find children, just slower
|
|
|
|
for task in self.tasks.values() {
|
2024-11-18 14:40:50 +01:00
|
|
|
if task.parent_id().is_some_and(|i| i == id) {
|
|
|
|
self.queue.push(task.event.id);
|
2024-09-07 13:03:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(task) => {
|
2024-09-22 16:47:26 +02:00
|
|
|
self.queue_children_of(&task);
|
2024-09-07 13:03:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
self.check_depth();
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-08-12 23:07:39 +03:00
|
|
|
|
2024-07-19 01:15:11 +03:00
|
|
|
struct ParentIterator<'a> {
|
|
|
|
tasks: &'a TaskMap,
|
|
|
|
current: Option<EventId>,
|
|
|
|
}
|
|
|
|
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-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 {
|
|
|
|
use super::*;
|
2024-11-18 14:43:49 +01:00
|
|
|
use std::collections::HashSet;
|
2024-08-06 11:34:18 +03:00
|
|
|
|
2024-09-22 16:48:15 +02:00
|
|
|
fn stub_tasks() -> TasksRelay {
|
2024-08-06 17:52:20 +03:00
|
|
|
use nostr_sdk::Keys;
|
2024-11-14 14:23:42 +01:00
|
|
|
use tokio::sync::mpsc;
|
2024-08-02 14:31:28 +03:00
|
|
|
|
2024-08-20 14:27:16 +03:00
|
|
|
let (tx, _rx) = mpsc::channel(16);
|
2024-09-22 16:48:15 +02:00
|
|
|
TasksRelay::with_sender(EventSender {
|
2024-08-07 15:03:29 +03:00
|
|
|
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-19 16:36:06 +03:00
|
|
|
macro_rules! assert_position {
|
|
|
|
($left:expr, $right:expr $(,)?) => {
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!($left.get_position(), Some($right))
|
2024-08-19 16:36:06 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
macro_rules! assert_tasks_visible {
|
2024-09-07 13:03:30 +03:00
|
|
|
($left:expr, $right:expr $(,)?) => {
|
2024-11-22 10:06:50 +01:00
|
|
|
let tasks = $left.visible_tasks();
|
2024-11-22 11:20:13 +01:00
|
|
|
assert_tasks!($left, tasks, $right,
|
|
|
|
"\nQuick Access: {:?}", $left.quick_access_raw().map(|id| $left.get_relative_path(*id)).collect_vec());
|
2024-09-07 13:03:30 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
macro_rules! assert_tasks_view {
|
|
|
|
($left:expr, $right:expr $(,)?) => {
|
|
|
|
let tasks = $left.viewed_tasks();
|
2024-11-22 11:20:13 +01:00
|
|
|
assert_tasks!($left, tasks, $right, "");
|
2024-11-22 10:06:50 +01:00
|
|
|
};
|
|
|
|
}
|
2024-11-22 11:20:13 +01:00
|
|
|
|
2024-11-22 10:06:50 +01:00
|
|
|
macro_rules! assert_tasks {
|
2024-11-22 11:20:13 +01:00
|
|
|
($left:expr, $tasks:expr, $right:expr $(, $($arg:tt)*)?) => {
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(
|
2024-11-22 10:06:50 +01:00
|
|
|
$tasks
|
2024-11-21 23:56:26 +01:00
|
|
|
.iter()
|
|
|
|
.map(|t| t.event.id)
|
|
|
|
.collect::<HashSet<EventId>>(),
|
2024-11-22 10:06:50 +01:00
|
|
|
HashSet::from_iter($right.clone()),
|
2024-11-22 11:20:13 +01:00
|
|
|
"Tasks Visible: {:?}\nExpected: {:?}{}",
|
2024-11-22 10:06:50 +01:00
|
|
|
$tasks.iter().map(|t| t.event.id).map(|id| $left.get_relative_path(id)).collect_vec(),
|
|
|
|
$right.into_iter().map(|id| $left.get_relative_path(id)).collect_vec(),
|
2024-11-22 11:20:13 +01:00
|
|
|
format!($($($arg)*)?)
|
2024-11-22 10:06:50 +01:00
|
|
|
);
|
2024-11-21 23:56:26 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-11-09 18:00:17 +01:00
|
|
|
#[test]
|
|
|
|
fn test_recursive_closing() {
|
|
|
|
let mut tasks = stub_tasks();
|
2024-11-13 11:12:08 +01:00
|
|
|
|
|
|
|
tasks.custom_time = Some(Timestamp::zero());
|
2024-11-09 18:00:17 +01:00
|
|
|
let parent = tasks.make_task("parent #tag1");
|
|
|
|
tasks.move_to(Some(parent));
|
2024-11-13 11:12:08 +01:00
|
|
|
let sub = tasks.make_task("sub #oi # tag2");
|
2024-11-21 09:47:14 +01:00
|
|
|
assert_eq!(tasks.all_hashtags(), ["oi", "tag1", "tag2"].into_iter().map(Hashtag::from).collect());
|
2024-11-12 23:03:53 +01:00
|
|
|
tasks.make_note("note with #tag3 # yeah");
|
2024-11-21 09:47:14 +01:00
|
|
|
let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect();
|
|
|
|
assert_eq!(tasks.all_hashtags(), all_tags);
|
2024-11-13 11:12:08 +01:00
|
|
|
|
|
|
|
tasks.custom_time = Some(Timestamp::now());
|
2024-11-21 09:17:56 +01:00
|
|
|
tasks.update_state("Finished #YeaH # oi", State::Done);
|
2024-11-25 02:29:23 +01:00
|
|
|
assert_eq!(tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), ["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from));
|
2024-11-21 09:47:14 +01:00
|
|
|
assert_eq!(tasks.all_hashtags(), all_tags);
|
2024-11-13 11:12:08 +01:00
|
|
|
|
|
|
|
tasks.custom_time = Some(now());
|
2024-11-09 18:00:17 +01:00
|
|
|
tasks.update_state("Closing Down", State::Closed);
|
|
|
|
assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed);
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(tasks.get_by_id(&parent).unwrap().pure_state(), State::Closed);
|
|
|
|
assert_eq!(tasks.nonclosed_tasks().next(), None);
|
2024-11-21 09:47:14 +01:00
|
|
|
assert_eq!(tasks.all_hashtags(), Default::default());
|
|
|
|
}
|
2024-11-21 23:56:26 +01:00
|
|
|
|
2024-11-21 09:47:14 +01:00
|
|
|
#[test]
|
2024-11-21 23:56:26 +01:00
|
|
|
fn test_context() {
|
2024-11-21 09:47:14 +01:00
|
|
|
let mut tasks = stub_tasks();
|
|
|
|
tasks.update_tags(["dp", "yeah"].into_iter().map(Hashtag::from));
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.get_prompt_suffix(), " #dp #yeah");
|
2024-11-21 09:47:14 +01:00
|
|
|
tasks.remove_tag("Y");
|
|
|
|
assert_eq!(tasks.tags, ["dp"].into_iter().map(Hashtag::from).collect());
|
2024-11-22 11:20:13 +01:00
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
tasks.set_priority(Some(HIGH_PRIO));
|
|
|
|
assert_eq!(tasks.get_prompt_suffix(), " #dp *85");
|
2024-11-22 11:20:13 +01:00
|
|
|
let id_hp = tasks.make_task("high prio tagged # tag");
|
|
|
|
let hp = tasks.get_by_id(&id_hp).unwrap();
|
|
|
|
assert_eq!(hp.priority(), Some(HIGH_PRIO));
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(
|
2024-11-22 11:20:13 +01:00
|
|
|
hp.list_hashtags().collect_vec(),
|
2024-11-21 23:56:26 +01:00
|
|
|
vec!["DP", "tag"].into_iter().map(Hashtag::from).collect_vec()
|
|
|
|
);
|
2024-11-22 11:20:13 +01:00
|
|
|
|
|
|
|
tasks.state = StateFilter::from("WIP");
|
|
|
|
tasks.set_priority(Some(QUICK_PRIO));
|
|
|
|
|
2024-11-21 23:56:26 +01:00
|
|
|
tasks.make_task_and_enter("another *4", State::Pending);
|
2024-11-22 11:20:13 +01:00
|
|
|
let task2 = tasks.get_current_task().unwrap();
|
|
|
|
assert_eq!(task2.priority(), Some(40));
|
|
|
|
assert_eq!(task2.pure_state(), State::Pending);
|
|
|
|
assert_eq!(task2.state().unwrap().get_label(), "Pending");
|
2024-11-21 23:56:26 +01:00
|
|
|
tasks.make_note("*3");
|
|
|
|
let task2 = tasks.get_current_task().unwrap();
|
|
|
|
assert_eq!(task2.descriptions().next(), None);
|
|
|
|
assert_eq!(task2.priority(), Some(30));
|
2024-11-22 11:20:13 +01:00
|
|
|
let anid = task2.event.id;
|
|
|
|
|
|
|
|
tasks.custom_time = Some(Timestamp::now() + 1);
|
|
|
|
let s1 = tasks.make_task("sub1");
|
|
|
|
tasks.custom_time = Some(Timestamp::now() + 2);
|
|
|
|
tasks.set_priority(Some(QUICK_PRIO + 1));
|
|
|
|
let s2 = tasks.make_task("sub2");
|
|
|
|
let s3 = tasks.make_task("sub3");
|
|
|
|
tasks.set_priority(Some(QUICK_PRIO));
|
|
|
|
|
|
|
|
assert_tasks_visible!(tasks, [s1, s2, s3]);
|
|
|
|
tasks.state = StateFilter::Default;
|
|
|
|
assert_tasks_view!(tasks, [s1, s2, s3]);
|
|
|
|
assert_tasks_visible!(tasks, [id_hp, s1, s2, s3]);
|
|
|
|
tasks.move_up();
|
|
|
|
tasks.set_search_depth(1);
|
|
|
|
assert_tasks_view!(tasks, [id_hp]);
|
|
|
|
assert_tasks_visible!(tasks, [s1, s2, s3, id_hp]);
|
|
|
|
|
|
|
|
tasks.set_priority(None);
|
|
|
|
let s4 = tasks.make_task_with("sub4", [tasks.make_event_tag_from_id(anid, MARKER_PARENT)], true);
|
|
|
|
assert_eq!(tasks.get_parent(Some(&s4)), Some(&anid));
|
|
|
|
assert_tasks_view!(tasks, [anid, id_hp]);
|
|
|
|
// s2-4 are newest while s2,s3,hp are highest prio
|
|
|
|
assert_tasks_visible!(tasks, [s4, s2, s3, anid, id_hp]);
|
2024-11-21 23:56:26 +01:00
|
|
|
|
|
|
|
tasks.pubkey = Some(Keys::generate().public_key);
|
2024-11-09 18:00:17 +01:00
|
|
|
}
|
|
|
|
|
2024-11-08 11:49:49 +01:00
|
|
|
#[test]
|
|
|
|
fn test_sibling_dependency() {
|
|
|
|
let mut tasks = stub_tasks();
|
|
|
|
let parent = tasks.make_task("parent");
|
2024-11-18 14:52:52 +01:00
|
|
|
let sub = tasks.submit(
|
|
|
|
EventBuilder::new(TASK_KIND, "sub")
|
|
|
|
.tags([tasks.make_event_tag_from_id(parent, MARKER_PARENT)])
|
|
|
|
);
|
2024-11-22 11:22:28 +01:00
|
|
|
assert_tasks_view!(tasks, [parent]);
|
2024-11-08 11:49:49 +01:00
|
|
|
tasks.track_at(Timestamp::now(), Some(sub));
|
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 1);
|
2024-11-22 11:22:28 +01:00
|
|
|
assert_tasks_view!(tasks, []);
|
2024-11-08 11:49:49 +01:00
|
|
|
|
|
|
|
tasks.make_dependent_sibling("sibling");
|
|
|
|
assert_eq!(tasks.len(), 3);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
2024-11-08 11:49:49 +01:00
|
|
|
}
|
|
|
|
|
2024-08-29 22:58:17 +03:00
|
|
|
#[test]
|
|
|
|
fn test_bookmarks() {
|
|
|
|
let mut tasks = stub_tasks();
|
|
|
|
let zero = EventId::all_zeros();
|
2024-11-08 12:15:32 +01:00
|
|
|
let test = tasks.make_task("test # tag");
|
2024-08-29 22:58:17 +03:00
|
|
|
let parent = tasks.make_task("parent");
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
2024-08-29 22:58:17 +03:00
|
|
|
tasks.move_to(Some(parent));
|
|
|
|
let pin = tasks.make_task("pin");
|
|
|
|
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.search_depth = 1;
|
2024-09-07 13:03:30 +03:00
|
|
|
assert_eq!(tasks.filtered_tasks(None, true).len(), 2);
|
|
|
|
assert_eq!(tasks.filtered_tasks(None, false).len(), 2);
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(tasks.filtered_tasks(Some(zero), false).len(), 0);
|
|
|
|
assert_eq!(tasks.filtered_tasks(Some(parent), false).len(), 1);
|
|
|
|
assert_eq!(tasks.filtered_tasks(Some(pin), false).len(), 0);
|
|
|
|
assert_eq!(tasks.filtered_tasks(Some(zero), false).len(), 0);
|
2024-09-07 13:03:30 +03:00
|
|
|
|
2024-11-18 14:52:52 +01:00
|
|
|
tasks.submit(
|
|
|
|
EventBuilder::new(Kind::Bookmarks, "")
|
|
|
|
.tags([Tag::event(pin), Tag::event(zero)])
|
|
|
|
);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(tasks.filtered_tasks(Some(pin), true).len(), 0);
|
|
|
|
assert_eq!(tasks.filtered_tasks(Some(pin), false).len(), 0);
|
|
|
|
assert_eq!(tasks.filtered_tasks(Some(zero), true).len(), 0);
|
2024-11-14 14:23:42 +01:00
|
|
|
assert_eq!(
|
2024-11-18 14:40:50 +01:00
|
|
|
tasks.filtered_tasks(Some(zero), false),
|
2024-11-14 14:23:42 +01:00
|
|
|
vec![tasks.get_by_id(&pin).unwrap()]
|
|
|
|
);
|
2024-08-29 22:58:17 +03:00
|
|
|
|
|
|
|
tasks.move_to(None);
|
2024-10-12 11:35:43 +02:00
|
|
|
assert_eq!(tasks.view_depth, 0);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_visible!(tasks, [pin, test, parent]);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.set_view_depth(1);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_visible!(tasks, [pin, test]);
|
2024-11-21 09:47:14 +01:00
|
|
|
tasks.add_tag("tag");
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_visible!(tasks, [test]);
|
2024-11-14 14:23:42 +01:00
|
|
|
assert_eq!(
|
|
|
|
tasks.filtered_tasks(None, true),
|
|
|
|
vec![tasks.get_by_id(&test).unwrap()]
|
|
|
|
);
|
2024-10-12 11:54:29 +02:00
|
|
|
|
2024-11-18 14:52:52 +01:00
|
|
|
tasks.submit(EventBuilder::new(Kind::Bookmarks, ""));
|
2024-11-22 10:06:50 +01:00
|
|
|
assert!(tasks.bookmarks.is_empty());
|
2024-08-29 22:58:17 +03:00
|
|
|
tasks.clear_filters();
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_visible!(tasks, [pin, test]);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.set_view_depth(0);
|
2024-11-22 10:06:50 +01:00
|
|
|
tasks.custom_time = Some(now());
|
|
|
|
let mut new = (0..3).map(|t| tasks.make_task(t.to_string().as_str())).collect_vec();
|
|
|
|
// Show the newest tasks in quick access and remove old pin
|
|
|
|
new.extend([test, parent]);
|
|
|
|
assert_tasks_visible!(tasks, new);
|
2024-08-29 22:58:17 +03:00
|
|
|
}
|
|
|
|
|
2024-08-12 23:02:50 +03:00
|
|
|
#[test]
|
|
|
|
fn test_procedures() {
|
|
|
|
let mut tasks = stub_tasks();
|
2024-11-08 12:15:32 +01:00
|
|
|
tasks.make_task_and_enter("proc # tags", State::Procedure);
|
2024-08-29 11:50:34 +03:00
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 1);
|
2024-11-18 14:52:52 +01:00
|
|
|
let side = tasks.submit(
|
|
|
|
EventBuilder::new(TASK_KIND, "side")
|
|
|
|
.tags([tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)])
|
|
|
|
);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks(), Vec::<&Task>::new());
|
2024-08-14 16:00:03 +03:00
|
|
|
let sub_id = tasks.make_task("sub");
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [sub_id]);
|
2024-08-14 16:00:03 +03:00
|
|
|
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-20 21:42:05 +03:00
|
|
|
#[test]
|
|
|
|
fn test_filter_or_create() {
|
|
|
|
let mut tasks = stub_tasks();
|
|
|
|
let zeros = EventId::all_zeros();
|
2024-11-18 14:40:50 +01:00
|
|
|
let zero = Some(zeros);
|
2024-08-20 21:42:05 +03:00
|
|
|
|
2024-08-25 14:30:08 +03:00
|
|
|
let id1 = tasks.filter_or_create(zero, "newer");
|
2024-08-20 21:42:05 +03:00
|
|
|
assert_eq!(tasks.len(), 1);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(tasks.get_by_id(&id1.unwrap()).unwrap().parent_id(), zero.as_ref());
|
2024-08-20 21:42:05 +03:00
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
tasks.move_to(zero);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
2024-08-20 21:42:05 +03:00
|
|
|
let sub = tasks.make_task("test");
|
|
|
|
assert_eq!(tasks.len(), 2);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(tasks.get_by_id(&sub).unwrap().parent_id(), zero.as_ref());
|
2024-08-20 21:42:05 +03:00
|
|
|
|
2024-10-01 23:20:08 +02:00
|
|
|
// Do not substring match invisible subtask
|
|
|
|
let id2 = tasks.filter_or_create(None, "#new-is gold wrapped").unwrap();
|
2024-08-20 21:42:05 +03:00
|
|
|
assert_eq!(tasks.len(), 3);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
2024-10-01 23:20:08 +02:00
|
|
|
let new2 = tasks.get_by_id(&id2).unwrap();
|
2024-08-20 21:42:05 +03:00
|
|
|
assert_eq!(new2.props, Default::default());
|
2024-08-25 14:30:08 +03:00
|
|
|
|
2024-10-01 23:20:08 +02:00
|
|
|
tasks.move_up();
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(tasks.get_matching(tasks.get_position(), "wrapped").len(), 1);
|
|
|
|
assert_eq!(tasks.get_matching(tasks.get_position(), "new-i").len(), 1);
|
2024-10-01 23:20:08 +02:00
|
|
|
tasks.filter_or_create(None, "is gold");
|
|
|
|
assert_position!(tasks, id2);
|
|
|
|
|
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
|
|
|
// Global match
|
2024-08-25 14:30:08 +03:00
|
|
|
let idagain = tasks.filter_or_create(None, "newer");
|
|
|
|
assert_eq!(idagain, None);
|
|
|
|
assert_position!(tasks, id1.unwrap());
|
2024-10-01 23:20:08 +02:00
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 4);
|
2024-08-25 14:30:08 +03:00
|
|
|
assert_eq!(tasks.len(), 3);
|
2024-08-20 21:42:05 +03:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2024-08-19 11:45:12 +03:00
|
|
|
tasks.track_at(Timestamp::from(0), None);
|
2024-08-25 16:37:55 +03:00
|
|
|
assert_eq!(tasks.history.len(), 0);
|
2024-08-06 17:52:20 +03:00
|
|
|
|
2024-08-19 13:06:20 +03:00
|
|
|
let almost_now: Timestamp = Timestamp::now() - 12u64;
|
|
|
|
tasks.track_at(Timestamp::from(11), Some(zero));
|
|
|
|
tasks.track_at(Timestamp::from(13), Some(zero));
|
2024-08-20 21:42:05 +03:00
|
|
|
assert_position!(tasks, zero);
|
2024-08-19 13:06:20 +03:00
|
|
|
assert!(tasks.time_tracked(zero) > almost_now.as_u64());
|
2024-08-06 17:52:20 +03:00
|
|
|
|
2024-08-27 15:00:53 +03:00
|
|
|
// Because None is backtracked by one to avoid conflicts
|
|
|
|
tasks.track_at(Timestamp::from(22 + 1), None);
|
2024-08-29 11:50:34 +03:00
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 2);
|
2024-08-19 13:06:20 +03:00
|
|
|
assert_eq!(tasks.time_tracked(zero), 11);
|
2024-08-27 15:00:53 +03:00
|
|
|
tasks.track_at(Timestamp::from(22 + 1), Some(zero));
|
2024-08-29 11:50:34 +03:00
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
2024-08-27 15:00:53 +03:00
|
|
|
assert!(tasks.time_tracked(zero) > 999);
|
|
|
|
|
|
|
|
let some = tasks.make_task("some");
|
|
|
|
tasks.track_at(Timestamp::from(22 + 1), Some(some));
|
2024-08-29 11:50:34 +03:00
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 4);
|
2024-08-27 15:00:53 +03:00
|
|
|
assert_eq!(tasks.time_tracked(zero), 12);
|
|
|
|
assert!(tasks.time_tracked(some) > 999);
|
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-11-22 11:22:28 +01:00
|
|
|
tasks.track_at(Timestamp::now() + 100, Some(zero));
|
2024-11-14 14:23:42 +01:00
|
|
|
assert_eq!(
|
2024-11-18 14:40:50 +01:00
|
|
|
timestamps(tasks.get_own_events_history(), &[zero])
|
2024-11-14 14:23:42 +01:00
|
|
|
.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();
|
|
|
|
|
2024-10-12 14:17:46 +02:00
|
|
|
let t1 = tasks.make_note("t1");
|
|
|
|
let activity_t1 = tasks.get_by_id(&t1).unwrap();
|
|
|
|
assert!(!activity_t1.is_task());
|
2024-10-12 11:35:43 +02:00
|
|
|
assert_eq!(tasks.view_depth, 0);
|
2024-10-12 14:17:46 +02:00
|
|
|
assert_eq!(activity_t1.pure_state(), State::Open);
|
2024-08-06 17:52:20 +03:00
|
|
|
debug!("{:?}", tasks);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.search_depth = 0;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
2024-10-12 21:55:32 +02:00
|
|
|
tasks.recurse_activities = false;
|
|
|
|
assert_eq!(tasks.filtered_tasks(None, false).len(), 1);
|
2024-08-06 17:52:20 +03:00
|
|
|
|
|
|
|
tasks.move_to(Some(t1));
|
2024-08-19 16:36:06 +03:00
|
|
|
assert_position!(tasks, t1);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.search_depth = 2;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
2024-11-08 12:15:32 +01:00
|
|
|
let t11 = tasks.make_task("t11 # tag");
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 1);
|
2024-09-07 13:03:30 +03:00
|
|
|
assert_eq!(tasks.get_task_path(Some(t11)), "t1>t11");
|
2024-11-20 19:05:33 +01:00
|
|
|
assert_eq!(tasks.get_relative_path(t11), "t11");
|
2024-09-07 13:03:30 +03:00
|
|
|
let t12 = tasks.make_task("t12");
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 2);
|
2024-08-06 17:52:20 +03:00
|
|
|
|
2024-09-07 13:03:30 +03:00
|
|
|
tasks.move_to(Some(t11));
|
|
|
|
assert_position!(tasks, t11);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_eq!(tasks.viewed_tasks().len(), 0);
|
2024-09-07 13:03:30 +03:00
|
|
|
let t111 = tasks.make_task("t111");
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t111]);
|
2024-09-07 13:03:30 +03:00
|
|
|
assert_eq!(tasks.get_task_path(Some(t111)), "t1>t11>t111");
|
2024-11-20 19:05:33 +01:00
|
|
|
assert_eq!(tasks.get_relative_path(t111), "t111");
|
2024-10-11 01:10:17 +02:00
|
|
|
tasks.view_depth = 2;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t111]);
|
2024-08-06 17:52:20 +03:00
|
|
|
|
2024-11-18 14:40:50 +01:00
|
|
|
assert_eq!(ChildIterator::from(&tasks, EventId::all_zeros()).get_all().len(), 1);
|
|
|
|
assert_eq!(ChildIterator::from(&tasks, EventId::all_zeros()).get_depth(0).len(), 1);
|
|
|
|
assert_eq!(ChildIterator::from(&tasks, t1).get_depth(0).len(), 1);
|
|
|
|
assert_eq!(ChildIterator::from(&tasks, t1).get_depth(1).len(), 3);
|
|
|
|
assert_eq!(ChildIterator::from(&tasks, t1).get_depth(2).len(), 4);
|
|
|
|
assert_eq!(ChildIterator::from(&tasks, t1).get_depth(9).len(), 4);
|
|
|
|
assert_eq!(ChildIterator::from(&tasks, t1).get_all().len(), 4);
|
2024-08-19 16:47:09 +03:00
|
|
|
|
2024-08-06 17:52:20 +03:00
|
|
|
tasks.move_to(Some(t1));
|
2024-08-19 16:36:06 +03:00
|
|
|
assert_position!(tasks, t1);
|
2024-08-29 11:50:34 +03:00
|
|
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
2024-11-20 19:05:33 +01:00
|
|
|
assert_eq!(tasks.get_relative_path(t111), "t11>t111");
|
2024-10-11 01:10:17 +02:00
|
|
|
assert_eq!(tasks.view_depth, 2);
|
2024-11-21 23:56:26 +01:00
|
|
|
tasks.set_search_depth(1);
|
|
|
|
assert_tasks_view!(tasks, [t111, t12]);
|
|
|
|
tasks.set_view_depth(0);
|
|
|
|
assert_tasks_view!(tasks, [t11, t12]);
|
2024-09-07 13:03:30 +03:00
|
|
|
tasks.set_view(vec![t11]);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t11]);
|
|
|
|
tasks.set_view_depth(1);
|
|
|
|
assert_tasks_view!(tasks, [t111]);
|
2024-11-22 10:06:50 +01:00
|
|
|
tasks.set_search_depth(2); // resets view
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t111, t12]);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.set_view_depth(0);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t11, t12]);
|
2024-08-06 17:52:20 +03:00
|
|
|
|
|
|
|
tasks.move_to(None);
|
2024-10-12 21:55:32 +02:00
|
|
|
tasks.recurse_activities = true;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t11, t12]);
|
2024-10-12 14:17:46 +02:00
|
|
|
tasks.recurse_activities = false;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t1]);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.view_depth = 1;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t11, t12]);
|
2024-10-12 11:35:43 +02:00
|
|
|
tasks.view_depth = 2;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t111, t12]);
|
2024-10-11 01:10:17 +02:00
|
|
|
tasks.view_depth = 9;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t111, t12]);
|
2024-10-12 11:54:29 +02:00
|
|
|
|
2024-11-21 09:47:14 +01:00
|
|
|
tasks.add_tag("tag");
|
2024-10-12 11:54:29 +02:00
|
|
|
tasks.view_depth = 0;
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, [t11]);
|
2024-10-12 11:54:29 +02:00
|
|
|
tasks.search_depth = 0;
|
|
|
|
assert_eq!(tasks.view, []);
|
2024-11-21 23:56:26 +01:00
|
|
|
assert_tasks_view!(tasks, []);
|
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"
|
|
|
|
);
|
2024-11-20 19:05:33 +01:00
|
|
|
assert_eq!(tasks.get_relative_path(dangling), "test");
|
2024-08-07 15:04:18 +03:00
|
|
|
}
|
|
|
|
|
2024-08-20 21:42:05 +03:00
|
|
|
#[allow(dead_code)] // #[test]
|
2024-08-07 15:04:18 +03:00
|
|
|
fn test_itertools() {
|
2024-08-06 17:52:20 +03:00
|
|
|
use itertools::Itertools;
|
2024-11-14 14:23:42 +01:00
|
|
|
assert_eq!("test toast".split(' ').collect_vec().len(), 3);
|
|
|
|
assert_eq!("test toast".split_ascii_whitespace().collect_vec().len(), 2);
|
2024-08-06 17:52:20 +03:00
|
|
|
}
|
2024-11-09 18:01:40 +01:00
|
|
|
}
|