Compare commits
14 Commits
9eaf10006b
...
3942105764
Author | SHA1 | Date |
---|---|---|
xeruf | 3942105764 | |
xeruf | 945e29b5ed | |
xeruf | 1297be43bc | |
xeruf | 5a62e8f99e | |
xeruf | 018357b21e | |
xeruf | d1ae0f5458 | |
xeruf | d4f544173a | |
xeruf | c2b106ea69 | |
xeruf | 003d1d6120 | |
xeruf | 039c390c66 | |
xeruf | b974957bc9 | |
xeruf | dd5aaf71d2 | |
xeruf | c5a2872534 | |
xeruf | 1a1f23007b |
23
README.md
23
README.md
|
@ -107,20 +107,23 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
||||||
|
|
||||||
Dot or slash can be repeated to move to parent tasks before acting.
|
Dot or slash can be repeated to move to parent tasks before acting.
|
||||||
|
|
||||||
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (
|
- `:[IND][PROP]` - add property column PROP at IND or end,
|
||||||
1-indexed), empty: list properties
|
if it already exists remove property column PROP or IND; empty: list properties
|
||||||
- `::[PROP]` - sort by property PROP (multiple space-separated values allowed)
|
- `::[PROP]` - sort by property PROP (multiple space-separated values allowed)
|
||||||
- `([TIME]` - list tracked times or insert timetracking with the specified offset
|
- `([TIME]` - list tracked times or insert timetracking with the specified offset
|
||||||
such as `-1d`, `-15 minutes`, `yesterday 17:20`, `in 2 fortnights`
|
such as `-1d`, `-15 minutes`, `yesterday 17:20`, `in 2 fortnights`
|
||||||
- `)[TIME]` - stop timetracking with optional offset - also convenience helper to move to root
|
- `)[TIME]` - stop timetracking with optional offset - also convenience helper to move to root
|
||||||
- `>[TEXT]` - complete active task and move up, with optional status description
|
- `>[TEXT]` - complete active task and move up, with optional status description
|
||||||
- `<[TEXT]` - close active task and move up, with optional status description
|
- `<[TEXT]` - close active task and move up, with optional status description
|
||||||
- `!TEXT` - set status for current task from text and move up (empty: Open)
|
- `!TEXT` - set status for current task from text and move up; empty: Open
|
||||||
- `,[TEXT]` - list notes or add text note (comment / description)
|
|
||||||
- TBI: `*[INT]` - set priority - can also be used in task creation, with any digit
|
- TBI: `*[INT]` - set priority - can also be used in task creation, with any digit
|
||||||
|
- `,[TEXT]` - list notes or add text note (stateless task / task description)
|
||||||
- TBI: `;[TEXT]` - list comments or comment on task
|
- TBI: `;[TEXT]` - list comments or comment on task
|
||||||
- TBI: show status history and creation with attribution
|
- TBI: show status history and creation with attribution
|
||||||
- `&` - undo last action (moving in place or upwards confirms pending actions)
|
- `&` - revert
|
||||||
|
- with string argument, find first matching task in history
|
||||||
|
- with int argument, jump back X tasks in history
|
||||||
|
- undo last action (moving in place or upwards confirms pending actions)
|
||||||
- `wss://...` - switch or subscribe to relay (prefix with space to forcibly add a new one)
|
- `wss://...` - switch or subscribe to relay (prefix with space to forcibly add a new one)
|
||||||
|
|
||||||
Property Filters:
|
Property Filters:
|
||||||
|
@ -129,9 +132,8 @@ Property Filters:
|
||||||
- `+TAG` - add tag filter (empty: list all used tags)
|
- `+TAG` - add tag filter (empty: list all used tags)
|
||||||
- `-TAG` - remove tag filters (by prefix)
|
- `-TAG` - remove tag filters (by prefix)
|
||||||
- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all
|
- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all
|
||||||
- `@AUTHOR` - filter by time or author (pubkey, or `@` for self, TBI: id prefix, name prefix)
|
- `@[AUTHOR|TIME]` - filter by time or author (pubkey, or `@` for self, TBI: id prefix, name prefix)
|
||||||
- TBI: `**INT` - filter by priority
|
- TBI: `**INT` - filter by priority
|
||||||
- TBI: Filter by time
|
|
||||||
|
|
||||||
Status descriptions can be used for example for Kanban columns or review flows.
|
Status descriptions can be used for example for Kanban columns or review flows.
|
||||||
An active tag or status filter will also set that attribute for newly created tasks.
|
An active tag or status filter will also set that attribute for newly created tasks.
|
||||||
|
@ -206,12 +208,15 @@ Suggestions welcome!
|
||||||
|
|
||||||
- TUI: Clear Terminal? Refresh on empty prompt after timeout?
|
- TUI: Clear Terminal? Refresh on empty prompt after timeout?
|
||||||
- Kanban, GANTT, Calendar
|
- Kanban, GANTT, Calendar
|
||||||
- Web Interface, Messenger integrations
|
- Web Interface
|
||||||
|
- Messenger Integrations (Telegram Bot)
|
||||||
|
- n8n node
|
||||||
|
- Caldav Feed: Scheduled (planning) / Tracked (events, timetracking) with args for how far back/forward
|
||||||
|
|
||||||
## Exemplary Workflows
|
## Exemplary Workflows
|
||||||
|
|
||||||
- Freelancer
|
- Freelancer
|
||||||
- Family Chore management
|
- Family Chore Management
|
||||||
- Inter-Disciplinary Project Team -> Company with multiple projects and multiple relays
|
- Inter-Disciplinary Project Team -> Company with multiple projects and multiple relays
|
||||||
+ Permissions via status or assignment (reassignment?)
|
+ Permissions via status or assignment (reassignment?)
|
||||||
+ Tasks can be blocked while having a status (e.g. kanban column)
|
+ Tasks can be blocked while having a status (e.g. kanban column)
|
||||||
|
|
31
src/kinds.rs
31
src/kinds.rs
|
@ -5,26 +5,27 @@ use nostr_sdk::TagStandard::Hashtag;
|
||||||
|
|
||||||
use crate::task::{MARKER_PARENT, State};
|
use crate::task::{MARKER_PARENT, State};
|
||||||
|
|
||||||
pub const METADATA_KIND: u16 = 0;
|
pub const TASK_KIND: Kind = Kind::GitIssue;
|
||||||
pub const NOTE_KIND: u16 = 1;
|
pub const PROCEDURE_KIND_ID: u16 = 1639;
|
||||||
pub const TASK_KIND: u16 = 1621;
|
pub const PROCEDURE_KIND: Kind = Kind::Regular(PROCEDURE_KIND_ID);
|
||||||
pub const TRACKING_KIND: u16 = 1650;
|
pub const TRACKING_KIND: Kind = Kind::Regular(1650);
|
||||||
pub const KINDS: [u16; 3] = [
|
pub const BASIC_KINDS: [Kind; 4] = [
|
||||||
METADATA_KIND,
|
Kind::Metadata,
|
||||||
NOTE_KIND,
|
Kind::TextNote,
|
||||||
TASK_KIND,
|
TASK_KIND,
|
||||||
|
Kind::Bookmarks,
|
||||||
];
|
];
|
||||||
pub const PROP_KINDS: [u16; 6] = [
|
pub const PROP_KINDS: [Kind; 6] = [
|
||||||
TRACKING_KIND,
|
TRACKING_KIND,
|
||||||
State::Open as u16,
|
Kind::GitStatusOpen,
|
||||||
State::Done as u16,
|
Kind::GitStatusApplied,
|
||||||
State::Closed as u16,
|
Kind::GitStatusClosed,
|
||||||
State::Pending as u16,
|
Kind::GitStatusDraft,
|
||||||
State::Procedure as u16
|
PROCEDURE_KIND,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// TODO: use formatting - bold / heading / italics - and generate from code
|
||||||
/// Helper for available properties.
|
/// Helper for available properties.
|
||||||
/// TODO: use formatting - bold / heading / italics - and generate from code
|
|
||||||
pub const PROPERTY_COLUMNS: &str =
|
pub const PROPERTY_COLUMNS: &str =
|
||||||
"# Available Properties
|
"# Available Properties
|
||||||
Immutable:
|
Immutable:
|
||||||
|
@ -66,7 +67,7 @@ pub(crate) fn build_task(name: &str, tags: Vec<Tag>, kind: Option<(&str, Kind)>)
|
||||||
info!("Created {}task \"{name}\" with tags [{}]",
|
info!("Created {}task \"{name}\" with tags [{}]",
|
||||||
kind.map(|k| k.0).unwrap_or_default(),
|
kind.map(|k| k.0).unwrap_or_default(),
|
||||||
tags.iter().map(format_tag).join(", "));
|
tags.iter().map(format_tag).join(", "));
|
||||||
EventBuilder::new(kind.map(|k| k.1).unwrap_or(Kind::from(TASK_KIND)), name, tags)
|
EventBuilder::new(kind.map(|k| k.1).unwrap_or(TASK_KIND), name, tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_prop(
|
pub(crate) fn build_prop(
|
||||||
|
|
73
src/main.rs
73
src/main.rs
|
@ -28,7 +28,7 @@ use tokio::time::timeout;
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{KINDS, PROP_KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
use crate::kinds::{BASIC_KINDS, PROP_KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
||||||
use crate::task::{MARKER_DEPENDS, State};
|
use crate::task::{MARKER_DEPENDS, State};
|
||||||
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
||||||
|
|
||||||
|
@ -95,9 +95,9 @@ impl EventSender {
|
||||||
}
|
}
|
||||||
let mut queue = self.queue.borrow_mut();
|
let mut queue = self.queue.borrow_mut();
|
||||||
Ok(event_builder.to_event(&self.keys).inspect(|event| {
|
Ok(event_builder.to_event(&self.keys).inspect(|event| {
|
||||||
if event.kind.as_u16() == TRACKING_KIND {
|
if event.kind == TRACKING_KIND {
|
||||||
queue.retain(|e| {
|
queue.retain(|e| {
|
||||||
e.kind.as_u16() != TRACKING_KIND
|
e.kind != TRACKING_KIND
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
queue.push(event.clone());
|
queue.push(event.clone());
|
||||||
|
@ -115,7 +115,7 @@ impl EventSender {
|
||||||
}
|
}
|
||||||
/// Sends all pending events if there is a non-tracking event
|
/// Sends all pending events if there is a non-tracking event
|
||||||
fn flush(&self) {
|
fn flush(&self) {
|
||||||
if self.queue.borrow().iter().any(|event| event.kind.as_u16() != TRACKING_KIND) {
|
if self.queue.borrow().iter().any(|event| event.kind != TRACKING_KIND) {
|
||||||
self.force_flush()
|
self.force_flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,14 +240,10 @@ async fn main() -> Result<()> {
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
|
||||||
let sub1 = client.subscribe(vec![
|
let sub1 = client.subscribe(vec![Filter::new().kinds(BASIC_KINDS)], None).await;
|
||||||
Filter::new().kinds(KINDS.into_iter().map(Kind::from))
|
|
||||||
], None).await;
|
|
||||||
info!("Subscribed to tasks with {:?}", sub1);
|
info!("Subscribed to tasks with {:?}", sub1);
|
||||||
|
|
||||||
let sub2 = client.subscribe(vec![
|
let sub2 = client.subscribe(vec![Filter::new().kinds(PROP_KINDS)], None).await;
|
||||||
Filter::new().kinds(PROP_KINDS.into_iter().map(Kind::from))
|
|
||||||
], None).await;
|
|
||||||
info!("Subscribed to updates with {:?}", sub2);
|
info!("Subscribed to updates with {:?}", sub2);
|
||||||
|
|
||||||
let metadata = var("USER").ok().map(
|
let metadata = var("USER").ok().map(
|
||||||
|
@ -379,7 +375,7 @@ async fn main() -> Result<()> {
|
||||||
match op {
|
match op {
|
||||||
None => {
|
None => {
|
||||||
debug!("Flushing Tasks because of empty command");
|
debug!("Flushing Tasks because of empty command");
|
||||||
tasks.flush()
|
tasks.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(':') => {
|
Some(':') => {
|
||||||
|
@ -437,17 +433,30 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('&') => {
|
Some('&') => {
|
||||||
tasks.undo();
|
match arg {
|
||||||
|
None => tasks.undo(),
|
||||||
|
Some(text) => match text.parse::<u8>() {
|
||||||
|
Ok(int) => {
|
||||||
|
tasks.move_back_by(int as usize);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if !tasks.move_back_to(text) {
|
||||||
|
warn!("Did not find a match in history for \"{text}\"");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('@') => {
|
Some('@') => {
|
||||||
match arg {
|
match arg {
|
||||||
None => {
|
None => {
|
||||||
let today = Timestamp::now() - 80_000;
|
let today = Timestamp::now() - 80_000;
|
||||||
info!("Filtering for tasks created in the last 22 hours");
|
info!("Filtering for tasks opened in the last 22 hours");
|
||||||
tasks.set_filter(
|
tasks.set_filter(
|
||||||
tasks.filtered_tasks(tasks.get_position_ref())
|
tasks.filtered_tasks(tasks.get_position_ref())
|
||||||
.filter(|t| t.event.created_at > today)
|
.filter(|t| t.last_state_update() > today)
|
||||||
.map(|t| t.event.id)
|
.map(|t| t.event.id)
|
||||||
.collect()
|
.collect()
|
||||||
);
|
);
|
||||||
|
@ -475,11 +484,11 @@ async fn main() -> Result<()> {
|
||||||
parse_hour(arg, 1)
|
parse_hour(arg, 1)
|
||||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
||||||
.map(|time| {
|
.map(|time| {
|
||||||
info!("Filtering for tasks created after {}", format_datetime_relative(time));
|
info!("Filtering for tasks opened after {}", format_datetime_relative(time));
|
||||||
let threshold = time.to_utc().timestamp();
|
let threshold = time.to_utc().timestamp();
|
||||||
tasks.set_filter(
|
tasks.set_filter(
|
||||||
tasks.filtered_tasks(tasks.get_position_ref())
|
tasks.filtered_tasks(tasks.get_position_ref())
|
||||||
.filter(|t| t.event.created_at.as_u64() as i64 > threshold)
|
.filter(|t| t.last_state_update().as_u64() as i64 > threshold)
|
||||||
.map(|t| t.event.id)
|
.map(|t| t.event.id)
|
||||||
.collect()
|
.collect()
|
||||||
);
|
);
|
||||||
|
@ -490,7 +499,19 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('*') => {
|
Some('*') => {
|
||||||
info!("Setting priority not yet implemented")
|
match arg {
|
||||||
|
None => match tasks.get_position_ref() {
|
||||||
|
None => {
|
||||||
|
info!("Filtering for bookmarked tasks");
|
||||||
|
tasks.set_filter_bookmarks()
|
||||||
|
},
|
||||||
|
Some(pos) => {
|
||||||
|
info!("Toggling bookmark");
|
||||||
|
or_warn!(tasks.toggle_bookmark(*pos));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(arg) => info!("Setting priority not yet implemented"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('|') =>
|
Some('|') =>
|
||||||
|
@ -530,9 +551,20 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
Some('!') =>
|
Some('!') =>
|
||||||
match tasks.get_position() {
|
match tasks.get_position() {
|
||||||
None => warn!("First select a task to set its state!"),
|
None => {
|
||||||
|
warn!("First select a task to set its state!");
|
||||||
|
info!("Usage: ![(Open|Procedure|Pending|Done|Closed): ][Statename]");
|
||||||
|
}
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
|
'block: {
|
||||||
|
if let Some((left, right)) = arg_default.split_once(": ") {
|
||||||
|
if let Ok(state) = left.try_into() {
|
||||||
|
tasks.set_state_for(id, right, state);
|
||||||
|
break 'block;
|
||||||
|
}
|
||||||
|
}
|
||||||
tasks.set_state_for_with(id, arg_default);
|
tasks.set_state_for_with(id, arg_default);
|
||||||
|
}
|
||||||
tasks.move_up();
|
tasks.move_up();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -628,11 +660,6 @@ async fn main() -> Result<()> {
|
||||||
if dots > 1 {
|
if dots > 1 {
|
||||||
info!("Moving up {} tasks", dots - 1)
|
info!("Moving up {} tasks", dots - 1)
|
||||||
}
|
}
|
||||||
} else if let Ok(depth) = slice.parse::<i8>() {
|
|
||||||
if pos != tasks.get_position_ref() {
|
|
||||||
tasks.move_to(pos.cloned());
|
|
||||||
}
|
|
||||||
tasks.set_depth(depth);
|
|
||||||
} else {
|
} else {
|
||||||
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
|
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
|
||||||
if !slice.chars().any(|c| c.is_ascii_uppercase()) {
|
if !slice.chars().any(|c| c.is_ascii_uppercase()) {
|
||||||
|
|
52
src/task.rs
52
src/task.rs
|
@ -11,7 +11,7 @@ use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
|
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
|
||||||
|
|
||||||
use crate::helpers::{format_timestamp_local, some_non_empty};
|
use crate::helpers::{format_timestamp_local, some_non_empty};
|
||||||
use crate::kinds::{is_hashtag, TASK_KIND};
|
use crate::kinds::{is_hashtag, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
||||||
|
|
||||||
pub static MARKER_PARENT: &str = "parent";
|
pub static MARKER_PARENT: &str = "parent";
|
||||||
pub static MARKER_DEPENDS: &str = "depends";
|
pub static MARKER_DEPENDS: &str = "depends";
|
||||||
|
@ -76,6 +76,7 @@ impl Task {
|
||||||
self.find_refs(MARKER_DEPENDS).collect()
|
self.find_refs(MARKER_DEPENDS).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trimmed event content or stringified id
|
||||||
pub(crate) fn get_title(&self) -> String {
|
pub(crate) fn get_title(&self) -> String {
|
||||||
Some(self.event.content.trim().to_string())
|
Some(self.event.content.trim().to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
|
@ -91,7 +92,7 @@ impl Task {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_task(&self) -> bool {
|
pub(crate) fn is_task(&self) -> bool {
|
||||||
self.event.kind.as_u16() == TASK_KIND ||
|
self.event.kind == TASK_KIND ||
|
||||||
self.states().next().is_some()
|
self.states().next().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,8 +106,12 @@ impl Task {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn last_state_update(&self) -> Timestamp {
|
||||||
|
self.state().map(|s| s.time).unwrap_or(self.event.created_at)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn state(&self) -> Option<TaskState> {
|
pub(crate) fn state(&self) -> Option<TaskState> {
|
||||||
self.states().max_by_key(|t| t.time)
|
self.states().last()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pure_state(&self) -> State {
|
pub(crate) fn pure_state(&self) -> State {
|
||||||
|
@ -215,7 +220,6 @@ impl Display for TaskState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const PROCEDURE_KIND: u16 = 1639;
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq)]
|
||||||
pub(crate) enum State {
|
pub(crate) enum State {
|
||||||
/// Actionable
|
/// Actionable
|
||||||
|
@ -227,16 +231,19 @@ pub(crate) enum State {
|
||||||
/// Temporarily not actionable
|
/// Temporarily not actionable
|
||||||
Pending,
|
Pending,
|
||||||
/// Actionable ordered task list
|
/// Actionable ordered task list
|
||||||
Procedure = PROCEDURE_KIND as isize,
|
Procedure = PROCEDURE_KIND_ID as isize,
|
||||||
}
|
}
|
||||||
impl From<&str> for State {
|
impl TryFrom<&str> for State {
|
||||||
fn from(value: &str) -> Self {
|
type Error = ();
|
||||||
match value {
|
|
||||||
"Closed" => State::Closed,
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
"Done" => State::Done,
|
match value.to_ascii_lowercase().as_str() {
|
||||||
"Pending" => State::Pending,
|
"closed" => Ok(State::Closed),
|
||||||
"Proc" | "Procedure" | "List" => State::Procedure,
|
"done" => Ok(State::Done),
|
||||||
_ => State::Open,
|
"pending" => Ok(State::Pending),
|
||||||
|
"proc" | "procedure" | "list" => Ok(State::Procedure),
|
||||||
|
"open" => Ok(State::Open),
|
||||||
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,13 +251,18 @@ impl TryFrom<Kind> for State {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn try_from(value: Kind) -> Result<Self, Self::Error> {
|
fn try_from(value: Kind) -> Result<Self, Self::Error> {
|
||||||
match value.as_u16() {
|
match value {
|
||||||
1630 => Ok(State::Open),
|
Kind::GitStatusOpen => Ok(State::Open),
|
||||||
1631 => Ok(State::Done),
|
Kind::GitStatusApplied => Ok(State::Done),
|
||||||
1632 => Ok(State::Closed),
|
Kind::GitStatusClosed => Ok(State::Closed),
|
||||||
1633 => Ok(State::Pending),
|
Kind::GitStatusDraft => Ok(State::Pending),
|
||||||
PROCEDURE_KIND => Ok(State::Procedure),
|
_ => {
|
||||||
_ => Err(()),
|
if value == PROCEDURE_KIND {
|
||||||
|
Ok(State::Procedure)
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
199
src/tasks.rs
199
src/tasks.rs
|
@ -32,6 +32,8 @@ pub(crate) struct Tasks {
|
||||||
history: HashMap<PublicKey, BTreeMap<Timestamp, Event>>,
|
history: HashMap<PublicKey, BTreeMap<Timestamp, Event>>,
|
||||||
/// Index of found users with metadata
|
/// Index of found users with metadata
|
||||||
users: HashMap<PublicKey, Metadata>,
|
users: HashMap<PublicKey, Metadata>,
|
||||||
|
/// Own pinned tasks
|
||||||
|
bookmarks: Vec<EventId>,
|
||||||
|
|
||||||
/// The task properties currently visible
|
/// The task properties currently visible
|
||||||
properties: Vec<String>,
|
properties: Vec<String>,
|
||||||
|
@ -104,7 +106,12 @@ impl Display for StateFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
pub(crate) fn from(url: Option<Url>, tx: &tokio::sync::mpsc::Sender<MostrMessage>, keys: &Keys, metadata: Option<Metadata>) -> Self {
|
pub(crate) fn from(
|
||||||
|
url: Option<Url>,
|
||||||
|
tx: &tokio::sync::mpsc::Sender<MostrMessage>,
|
||||||
|
keys: &Keys,
|
||||||
|
metadata: Option<Metadata>,
|
||||||
|
) -> Self {
|
||||||
let mut new = Self::with_sender(EventSender::from(url, tx, keys));
|
let mut new = Self::with_sender(EventSender::from(url, tx, keys));
|
||||||
metadata.map(|m| new.users.insert(keys.public_key(), m));
|
metadata.map(|m| new.users.insert(keys.public_key(), m));
|
||||||
new
|
new
|
||||||
|
@ -115,6 +122,8 @@ impl Tasks {
|
||||||
tasks: Default::default(),
|
tasks: Default::default(),
|
||||||
history: Default::default(),
|
history: Default::default(),
|
||||||
users: Default::default(),
|
users: Default::default(),
|
||||||
|
bookmarks: Default::default(),
|
||||||
|
|
||||||
properties: [
|
properties: [
|
||||||
"author",
|
"author",
|
||||||
"state",
|
"state",
|
||||||
|
@ -130,6 +139,7 @@ impl Tasks {
|
||||||
"rtime",
|
"rtime",
|
||||||
"name",
|
"name",
|
||||||
].into_iter().map(|s| s.to_string()).collect(),
|
].into_iter().map(|s| s.to_string()).collect(),
|
||||||
|
|
||||||
view: Default::default(),
|
view: Default::default(),
|
||||||
tags: Default::default(),
|
tags: Default::default(),
|
||||||
tags_excluded: Default::default(),
|
tags_excluded: Default::default(),
|
||||||
|
@ -155,13 +165,15 @@ impl Tasks {
|
||||||
self.get_position_at(now()).1
|
self.get_position_at(now()).1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO binary search
|
||||||
|
/// Gets last position change before the given timestamp
|
||||||
fn get_position_at(&self, timestamp: Timestamp) -> (Timestamp, Option<&EventId>) {
|
fn get_position_at(&self, timestamp: Timestamp) -> (Timestamp, Option<&EventId>) {
|
||||||
self.history_from(timestamp)
|
self.history_from(timestamp)
|
||||||
.last()
|
.last()
|
||||||
.filter(|e| e.created_at <= timestamp)
|
.filter(|e| e.created_at <= timestamp)
|
||||||
.map_or_else(
|
.map_or_else(
|
||||||
|| (Timestamp::now(), None),
|
|| (Timestamp::now(), None),
|
||||||
|e| (e.created_at, referenced_events(e)))
|
|e| (e.created_at, referenced_event(e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ids of all subtasks recursively found for id, including itself
|
/// Ids of all subtasks recursively found for id, including itself
|
||||||
|
@ -226,7 +238,7 @@ impl Tasks {
|
||||||
|
|
||||||
/// Total time in seconds tracked on this task by the current user.
|
/// Total time in seconds tracked on this task by the current user.
|
||||||
pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
|
pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
|
||||||
Durations::from(self.get_own_history(), &vec![&id]).sum::<Duration>().as_secs()
|
Durations::from(self.get_own_events_history(), &vec![&id]).sum::<Duration>().as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -366,9 +378,20 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a {
|
pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>) -> impl Iterator<Item=&Task> + 'a {
|
||||||
|
let current: HashMap<&EventId, &Task> = self.resolve_tasks(self.children_of(position)).map(|t| (t.get_id(), t)).collect();
|
||||||
|
let bookmarks =
|
||||||
|
if current.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
self.bookmarks.iter()
|
||||||
|
.filter(|id| !position.is_some_and(|p| &p == id) && !current.contains_key(id))
|
||||||
|
.filter_map(|id| self.get_by_id(id))
|
||||||
|
.collect_vec()
|
||||||
|
};
|
||||||
// TODO use ChildIterator
|
// TODO use ChildIterator
|
||||||
self.resolve_tasks(self.children_of(position))
|
current.into_values().chain(
|
||||||
.filter(move |t| {
|
bookmarks
|
||||||
|
).filter(move |t| {
|
||||||
// TODO apply filters in transit
|
// TODO apply filters in transit
|
||||||
self.state.matches(t) &&
|
self.state.matches(t) &&
|
||||||
t.tags.as_ref().map_or(true, |tags| {
|
t.tags.as_ref().map_or(true, |tags| {
|
||||||
|
@ -399,7 +422,7 @@ impl Tasks {
|
||||||
let now = &now();
|
let now = &now();
|
||||||
let mut tracking_stamp: Option<Timestamp> = None;
|
let mut tracking_stamp: Option<Timestamp> = None;
|
||||||
for elem in
|
for elem in
|
||||||
timestamps(self.get_own_history(), &[t.get_id()])
|
timestamps(self.get_own_events_history(), &[t.get_id()])
|
||||||
.map(|(e, _)| e) {
|
.map(|(e, _)| e) {
|
||||||
if tracking_stamp.is_some() && elem > now {
|
if tracking_stamp.is_some() && elem > now {
|
||||||
break;
|
break;
|
||||||
|
@ -507,6 +530,20 @@ impl Tasks {
|
||||||
|
|
||||||
// Movement and Selection
|
// Movement and Selection
|
||||||
|
|
||||||
|
pub(crate) fn toggle_bookmark(&mut self, id: EventId) -> nostr_sdk::Result<Event> {
|
||||||
|
match self.bookmarks.iter().position(|b| b == &id) {
|
||||||
|
None => self.bookmarks.push(id),
|
||||||
|
Some(pos) => { self.bookmarks.remove(pos); }
|
||||||
|
}
|
||||||
|
self.sender.submit(
|
||||||
|
EventBuilder::new(Kind::Bookmarks, "mostr pins",
|
||||||
|
self.bookmarks.iter().map(|id| Tag::event(*id))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_filter_bookmarks(&mut self) {
|
||||||
|
self.set_filter(self.bookmarks.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
|
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
|
||||||
if view.is_empty() {
|
if view.is_empty() {
|
||||||
warn!("No match for filter!")
|
warn!("No match for filter!")
|
||||||
|
@ -579,7 +616,7 @@ impl Tasks {
|
||||||
let has_space = lowercase_arg.split_ascii_whitespace().count() > 1;
|
let has_space = lowercase_arg.split_ascii_whitespace().count() > 1;
|
||||||
|
|
||||||
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
||||||
let mut filtered_more: Vec<EventId> = Vec::with_capacity(32);
|
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
||||||
for task in self.filtered_tasks(position) {
|
for task in self.filtered_tasks(position) {
|
||||||
let lowercase = task.event.content.to_ascii_lowercase();
|
let lowercase = task.event.content.to_ascii_lowercase();
|
||||||
if lowercase == lowercase_arg {
|
if lowercase == lowercase_arg {
|
||||||
|
@ -587,18 +624,26 @@ impl Tasks {
|
||||||
} else if task.event.content.starts_with(arg) {
|
} else if task.event.content.starts_with(arg) {
|
||||||
filtered.push(task.event.id)
|
filtered.push(task.event.id)
|
||||||
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.starts_with(&lowercase_arg)) } {
|
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.starts_with(&lowercase_arg)) } {
|
||||||
filtered_more.push(task.event.id)
|
filtered_fuzzy.push(task.event.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for task in self.tasks.values() {
|
for task in self.tasks.values() {
|
||||||
|
// Find global exact match
|
||||||
if task.event.content.to_ascii_lowercase() == lowercase_arg &&
|
if task.event.content.to_ascii_lowercase() == lowercase_arg &&
|
||||||
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
|
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
|
||||||
// exclude closed tasks and their subtasks
|
// exclude closed tasks and their subtasks
|
||||||
return vec![task.event.id];
|
return vec![task.event.id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filtered.is_empty() {
|
if filtered.is_empty() {
|
||||||
return filtered_more;
|
filtered = filtered_fuzzy;
|
||||||
|
}
|
||||||
|
let pos = self.get_position_ref();
|
||||||
|
let immediate = filtered.iter().filter(
|
||||||
|
|t| self.get_by_id(t).is_some_and(|t| t.parent_id() == pos)).collect_vec();
|
||||||
|
if immediate.len() == 1 {
|
||||||
|
return immediate.into_iter().cloned().collect_vec();
|
||||||
}
|
}
|
||||||
filtered
|
filtered
|
||||||
}
|
}
|
||||||
|
@ -792,19 +837,28 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add(&mut self, event: Event) {
|
pub(crate) fn add(&mut self, event: Event) {
|
||||||
match event.kind.as_u16() {
|
match event.kind {
|
||||||
TASK_KIND => self.add_task(event),
|
Kind::GitIssue => self.add_task(event),
|
||||||
TRACKING_KIND =>
|
Kind::Metadata =>
|
||||||
match self.history.get_mut(&event.pubkey) {
|
|
||||||
Some(c) => { c.insert(event.created_at, event); }
|
|
||||||
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
|
|
||||||
},
|
|
||||||
METADATA_KIND =>
|
|
||||||
match Metadata::from_json(event.content()) {
|
match Metadata::from_json(event.content()) {
|
||||||
Ok(metadata) => { self.users.insert(event.pubkey, metadata); }
|
Ok(metadata) => { self.users.insert(event.pubkey, metadata); }
|
||||||
Err(e) => warn!("Cannot parse metadata: {} from {:?}", e, event)
|
Err(e) => warn!("Cannot parse metadata: {} from {:?}", e, event)
|
||||||
}
|
}
|
||||||
_ => self.add_prop(event),
|
Kind::Bookmarks => {
|
||||||
|
if event.pubkey == self.sender.pubkey() {
|
||||||
|
self.bookmarks = referenced_events(&event).cloned().collect_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if event.kind == TRACKING_KIND {
|
||||||
|
match self.history.get_mut(&event.pubkey) {
|
||||||
|
Some(c) => { c.insert(event.created_at, event); }
|
||||||
|
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.add_prop(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -826,7 +880,7 @@ impl Tasks {
|
||||||
t.props.insert(event.clone());
|
t.props.insert(event.clone());
|
||||||
});
|
});
|
||||||
if !found {
|
if !found {
|
||||||
if event.kind.as_u16() == NOTE_KIND {
|
if event.kind.as_u16() == 1 {
|
||||||
self.add_task(event);
|
self.add_task(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -834,10 +888,40 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_own_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
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> + '_ {
|
||||||
self.history.get(&self.sender.pubkey()).into_iter().flat_map(|t| t.values())
|
self.history.get(&self.sender.pubkey()).into_iter().flat_map(|t| t.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
||||||
|
self.get_own_history().into_iter().flat_map(|hist| {
|
||||||
|
let now = now();
|
||||||
|
hist.values().rev().skip_while(move |e| e.created_at > now)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_back_to(&mut self, str: &str) -> bool {
|
||||||
|
let lower = str.to_ascii_lowercase();
|
||||||
|
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)));
|
||||||
|
if let Some(event) = found {
|
||||||
|
self.move_to(referenced_event(event).cloned());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_back_by(&mut self, steps: usize) {
|
||||||
|
let id = self.history_before_now().nth(steps)
|
||||||
|
.and_then(|e| referenced_event(e));
|
||||||
|
self.move_to(id.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn undo(&mut self) {
|
pub(crate) fn undo(&mut self) {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
self.sender.clear().into_iter().rev().for_each(|event| {
|
self.sender.clear().into_iter().rev().for_each(|event| {
|
||||||
|
@ -851,12 +935,12 @@ impl Tasks {
|
||||||
self.tasks.remove(&event.id);
|
self.tasks.remove(&event.id);
|
||||||
self.history.get_mut(&self.sender.pubkey())
|
self.history.get_mut(&self.sender.pubkey())
|
||||||
.map(|t| t.retain(|t, e| e != event &&
|
.map(|t| t.retain(|t, e| e != event &&
|
||||||
!referenced_events(e).is_some_and(|id| id == &event.id)));
|
!referenced_event(e).is_some_and(|id| id == &event.id)));
|
||||||
self.referenced_tasks(event, |t| { t.props.remove(event); });
|
self.referenced_tasks(event, |t| { t.props.remove(event); });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_state_for_with(&mut self, id: EventId, comment: &str) {
|
pub(crate) fn set_state_for_with(&mut self, id: EventId, comment: &str) {
|
||||||
self.set_state_for(id, comment, comment.into());
|
self.set_state_for(id, comment, comment.try_into().unwrap_or(State::Open));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId {
|
pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId {
|
||||||
|
@ -893,9 +977,14 @@ impl Tasks {
|
||||||
// Properties
|
// Properties
|
||||||
|
|
||||||
pub(crate) fn set_depth(&mut self, depth: i8) {
|
pub(crate) fn set_depth(&mut self, depth: i8) {
|
||||||
self.depth = depth;
|
if depth < self.depth && !self.view.is_empty() {
|
||||||
|
self.view.clear();
|
||||||
|
info!("Cleared search and reduced view depth to {depth}");
|
||||||
|
} else {
|
||||||
info!("Changed view depth to {depth}");
|
info!("Changed view depth to {depth}");
|
||||||
}
|
}
|
||||||
|
self.depth = depth;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_columns(&mut self) -> &mut Vec<String> {
|
pub(crate) fn get_columns(&mut self) -> &mut Vec<String> {
|
||||||
&mut self.properties
|
&mut self.properties
|
||||||
|
@ -987,18 +1076,19 @@ pub(crate) fn join_tasks<'a>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn referenced_events(event: &Event) -> Option<&EventId> {
|
fn referenced_events(event: &Event) -> impl Iterator<Item=&EventId> {
|
||||||
event.tags.iter().find_map(|tag| match tag.as_standardized() {
|
event.tags.iter().filter_map(|tag| match tag.as_standardized() {
|
||||||
Some(TagStandard::Event { event_id, .. }) => Some(event_id),
|
Some(TagStandard::Event { event_id, .. }) => Some(event_id),
|
||||||
_ => None
|
_ => None
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn referenced_event(event: &Event) -> Option<&EventId> {
|
||||||
|
referenced_events(event).next()
|
||||||
|
}
|
||||||
|
|
||||||
fn matching_tag_id<'a>(event: &'a Event, ids: &'a [&'a EventId]) -> Option<&'a EventId> {
|
fn matching_tag_id<'a>(event: &'a Event, ids: &'a [&'a EventId]) -> Option<&'a EventId> {
|
||||||
event.tags.iter().find_map(|tag| match tag.as_standardized() {
|
referenced_events(event).find(|id| ids.contains(id))
|
||||||
Some(TagStandard::Event { event_id, .. }) if ids.contains(&event_id) => Some(event_id),
|
|
||||||
_ => None
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filters out event timestamps to those that start or stop one of the given events
|
/// Filters out event timestamps to those that start or stop one of the given events
|
||||||
|
@ -1156,11 +1246,46 @@ mod tasks_test {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bookmarks() {
|
||||||
|
let mut tasks = stub_tasks();
|
||||||
|
let zero = EventId::all_zeros();
|
||||||
|
let test = tasks.make_task("test: tag");
|
||||||
|
let parent = tasks.make_task("parent");
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 2);
|
||||||
|
tasks.move_to(Some(parent));
|
||||||
|
let pin = tasks.make_task("pin");
|
||||||
|
|
||||||
|
assert_eq!(tasks.filtered_tasks(None).count(), 2);
|
||||||
|
assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0);
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 1);
|
||||||
|
assert_eq!(tasks.filtered_tasks(Some(&pin)).count(), 0);
|
||||||
|
assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0);
|
||||||
|
|
||||||
|
tasks.submit(EventBuilder::new(Kind::Bookmarks, "", [Tag::event(pin), Tag::event(zero)]));
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 1);
|
||||||
|
assert_eq!(tasks.filtered_tasks(Some(&pin)).count(), 0);
|
||||||
|
assert_eq!(tasks.filtered_tasks(Some(&zero)).count(), 0);
|
||||||
|
|
||||||
|
tasks.move_to(None);
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 3);
|
||||||
|
tasks.set_depth(2);
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 3);
|
||||||
|
tasks.add_tag("tag".to_string());
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 1);
|
||||||
|
assert_eq!(tasks.filtered_tasks(None).collect_vec(), vec![tasks.get_by_id(&test).unwrap()]);
|
||||||
|
tasks.submit(EventBuilder::new(Kind::Bookmarks, "", []));
|
||||||
|
tasks.clear_filters();
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 3);
|
||||||
|
tasks.set_depth(1);
|
||||||
|
assert_eq!(tasks.visible_tasks().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_procedures() {
|
fn test_procedures() {
|
||||||
let mut tasks = stub_tasks();
|
let mut tasks = stub_tasks();
|
||||||
tasks.make_task_and_enter("proc: tags", State::Procedure);
|
tasks.make_task_and_enter("proc: tags", State::Procedure);
|
||||||
assert_eq!(tasks.get_own_history().count(), 1);
|
assert_eq!(tasks.get_own_events_history().count(), 1);
|
||||||
let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)], None));
|
let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)], None));
|
||||||
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::<EventId>::new());
|
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::<EventId>::new());
|
||||||
let sub_id = tasks.make_task("sub");
|
let sub_id = tasks.make_task("sub");
|
||||||
|
@ -1194,11 +1319,11 @@ mod tasks_test {
|
||||||
let new2 = tasks.get_by_id(&id2.unwrap()).unwrap();
|
let new2 = tasks.get_by_id(&id2.unwrap()).unwrap();
|
||||||
assert_eq!(new2.props, Default::default());
|
assert_eq!(new2.props, Default::default());
|
||||||
|
|
||||||
assert_eq!(tasks.get_own_history().count(), 1);
|
assert_eq!(tasks.get_own_events_history().count(), 1);
|
||||||
let idagain = tasks.filter_or_create(None, "newer");
|
let idagain = tasks.filter_or_create(None, "newer");
|
||||||
assert_eq!(idagain, None);
|
assert_eq!(idagain, None);
|
||||||
assert_position!(tasks, id1.unwrap());
|
assert_position!(tasks, id1.unwrap());
|
||||||
assert_eq!(tasks.get_own_history().count(), 2);
|
assert_eq!(tasks.get_own_events_history().count(), 2);
|
||||||
assert_eq!(tasks.len(), 3);
|
assert_eq!(tasks.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1218,15 +1343,15 @@ mod tasks_test {
|
||||||
|
|
||||||
// Because None is backtracked by one to avoid conflicts
|
// Because None is backtracked by one to avoid conflicts
|
||||||
tasks.track_at(Timestamp::from(22 + 1), None);
|
tasks.track_at(Timestamp::from(22 + 1), None);
|
||||||
assert_eq!(tasks.get_own_history().count(), 2);
|
assert_eq!(tasks.get_own_events_history().count(), 2);
|
||||||
assert_eq!(tasks.time_tracked(zero), 11);
|
assert_eq!(tasks.time_tracked(zero), 11);
|
||||||
tasks.track_at(Timestamp::from(22 + 1), Some(zero));
|
tasks.track_at(Timestamp::from(22 + 1), Some(zero));
|
||||||
assert_eq!(tasks.get_own_history().count(), 3);
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
||||||
assert!(tasks.time_tracked(zero) > 999);
|
assert!(tasks.time_tracked(zero) > 999);
|
||||||
|
|
||||||
let some = tasks.make_task("some");
|
let some = tasks.make_task("some");
|
||||||
tasks.track_at(Timestamp::from(22 + 1), Some(some));
|
tasks.track_at(Timestamp::from(22 + 1), Some(some));
|
||||||
assert_eq!(tasks.get_own_history().count(), 4);
|
assert_eq!(tasks.get_own_events_history().count(), 4);
|
||||||
assert_eq!(tasks.time_tracked(zero), 12);
|
assert_eq!(tasks.time_tracked(zero), 12);
|
||||||
assert!(tasks.time_tracked(some) > 999);
|
assert!(tasks.time_tracked(some) > 999);
|
||||||
|
|
||||||
|
@ -1240,7 +1365,7 @@ mod tasks_test {
|
||||||
let zero = EventId::all_zeros();
|
let zero = EventId::all_zeros();
|
||||||
|
|
||||||
tasks.track_at(Timestamp::from(Timestamp::now().as_u64() + 100), Some(zero));
|
tasks.track_at(Timestamp::from(Timestamp::now().as_u64() + 100), Some(zero));
|
||||||
assert_eq!(timestamps(tasks.get_own_history(), &vec![&zero]).collect_vec().len(), 2)
|
assert_eq!(timestamps(tasks.get_own_events_history(), &vec![&zero]).collect_vec().len(), 2)
|
||||||
// TODO Does not show both future and current tracking properly, need to split by current time
|
// TODO Does not show both future and current tracking properly, need to split by current time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1290,7 +1415,7 @@ mod tasks_test {
|
||||||
|
|
||||||
tasks.move_to(Some(t1));
|
tasks.move_to(Some(t1));
|
||||||
assert_position!(tasks, t1);
|
assert_position!(tasks, t1);
|
||||||
assert_eq!(tasks.get_own_history().count(), 3);
|
assert_eq!(tasks.get_own_events_history().count(), 3);
|
||||||
assert_eq!(tasks.relative_path(t4), "t2>t4");
|
assert_eq!(tasks.relative_path(t4), "t2>t4");
|
||||||
assert_eq!(tasks.visible_tasks().len(), 2);
|
assert_eq!(tasks.visible_tasks().len(), 2);
|
||||||
tasks.depth = 2;
|
tasks.depth = 2;
|
||||||
|
|
Loading…
Reference in New Issue