Compare commits
5 Commits
fca9b1492b
...
dea8a46318
Author | SHA1 | Date |
---|---|---|
xeruf | dea8a46318 | |
xeruf | a3a732879f | |
xeruf | 6b7b6b91a8 | |
xeruf | 08b0ba48a3 | |
xeruf | 4180533844 |
|
@ -80,7 +80,7 @@ as you work.
|
||||||
The currently active task is automatically time-tracked.
|
The currently active task is automatically time-tracked.
|
||||||
To stop time-tracking completely, simply move to the root of all tasks.
|
To stop time-tracking completely, simply move to the root of all tasks.
|
||||||
Time-tracking is currently also stopped
|
Time-tracking is currently also stopped
|
||||||
when the application is terminated regularly.
|
when the application is terminated regularly with Ctrl-D.
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
|
@ -110,8 +110,9 @@ Dots can be repeated to move to parent tasks.
|
||||||
|
|
||||||
Property Filters:
|
Property Filters:
|
||||||
|
|
||||||
- `+TAG` - filter by tag
|
- `#TAG` - set tag filter (empty: list all used tags)
|
||||||
- `-TAG` - remove filter by tag
|
- `+TAG` - add tag filter
|
||||||
|
- `-TAG` - remove tag filters
|
||||||
- `?STATE` - filter by state (type or description) - plain `?` to reset
|
- `?STATE` - filter by state (type or description) - plain `?` to reset
|
||||||
|
|
||||||
State descriptions can be used for example for Kanban columns or review flows.
|
State descriptions can be used for example for Kanban columns or review flows.
|
||||||
|
|
18
src/kinds.rs
18
src/kinds.rs
|
@ -6,6 +6,22 @@ pub const TASK_KIND: u16 = 1621;
|
||||||
pub const TRACKING_KIND: u16 = 1650;
|
pub const TRACKING_KIND: u16 = 1650;
|
||||||
pub const KINDS: [u16; 7] = [1, TASK_KIND, TRACKING_KIND, 1630, 1631, 1632, 1633];
|
pub const KINDS: [u16; 7] = [1, TASK_KIND, TRACKING_KIND, 1630, 1631, 1632, 1633];
|
||||||
|
|
||||||
|
pub const PROPERTY_COLUMNS: &str = "Available properties:
|
||||||
|
- `id`
|
||||||
|
- `parentid`
|
||||||
|
- `name`
|
||||||
|
- `state`
|
||||||
|
- `hashtags`
|
||||||
|
- `tags` - values of all nostr tags associated with the event, except event tags
|
||||||
|
- `desc` - last note on the task
|
||||||
|
- `description` - accumulated notes on the task
|
||||||
|
- `path` - name including parent tasks
|
||||||
|
- `rpath` - name including parent tasks up to active task
|
||||||
|
- `time` - time tracked on this task by you
|
||||||
|
- `rtime` - time tracked on this tasks and its subtree by everyone
|
||||||
|
- `progress` - recursive subtask completion in percent
|
||||||
|
- `subtasks` - how many direct subtasks are complete";
|
||||||
|
|
||||||
pub(crate) fn build_tracking<I>(id: I) -> EventBuilder
|
pub(crate) fn build_tracking<I>(id: I) -> EventBuilder
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item=EventId>,
|
I: IntoIterator<Item=EventId>,
|
||||||
|
@ -42,6 +58,6 @@ fn format_tag(tag: &Tag) -> String {
|
||||||
|
|
||||||
pub(crate) fn is_hashtag(tag: &Tag) -> bool {
|
pub(crate) fn is_hashtag(tag: &Tag) -> bool {
|
||||||
tag.single_letter_tag()
|
tag.single_letter_tag()
|
||||||
.is_some_and(|sltag| sltag.character == Alphabet::T)
|
.is_some_and(|letter| letter.character == Alphabet::T)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
119
src/main.rs
119
src/main.rs
|
@ -12,13 +12,14 @@ use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{KINDS, TRACKING_KIND};
|
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
||||||
use crate::task::State;
|
use crate::task::State;
|
||||||
use crate::tasks::Tasks;
|
use crate::tasks::Tasks;
|
||||||
|
|
||||||
|
@ -224,12 +225,10 @@ async fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
|
||||||
let mut lines = stdin().lines();
|
let mut lines = stdin().lines();
|
||||||
loop {
|
loop {
|
||||||
|
println!();
|
||||||
selected_relay.as_ref().and_then(|url| relays.get(url)).inspect(|tasks| {
|
selected_relay.as_ref().and_then(|url| relays.get(url)).inspect(|tasks| {
|
||||||
or_print(tasks.print_tasks());
|
|
||||||
|
|
||||||
print!(
|
print!(
|
||||||
"{}",
|
"{}",
|
||||||
format!(
|
format!(
|
||||||
|
@ -266,10 +265,11 @@ async fn main() {
|
||||||
let mut iter = input.chars();
|
let mut iter = input.chars();
|
||||||
let op = iter.next();
|
let op = iter.next();
|
||||||
let arg = if input.len() > 1 {
|
let arg = if input.len() > 1 {
|
||||||
input[1..].trim()
|
Some(input[1..].trim())
|
||||||
} else {
|
} else {
|
||||||
""
|
None
|
||||||
};
|
};
|
||||||
|
let arg_default = arg.unwrap_or("");
|
||||||
let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
|
||||||
match op {
|
match op {
|
||||||
None => {
|
None => {
|
||||||
|
@ -277,49 +277,43 @@ async fn main() {
|
||||||
tasks.flush()
|
tasks.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(':') => match iter.next().and_then(|s| s.to_digit(10)) {
|
Some(':') =>
|
||||||
Some(digit) => {
|
if let Some(digit) = iter.next().and_then(|s| s.to_digit(10)) {
|
||||||
let index = (digit as usize).saturating_sub(1);
|
let index = (digit as usize).saturating_sub(1);
|
||||||
let remaining = iter.collect::<String>().trim().to_string();
|
let remaining = iter.collect::<String>().trim().to_string();
|
||||||
if remaining.is_empty() {
|
if remaining.is_empty() {
|
||||||
tasks.remove_column(index);
|
tasks.remove_column(index);
|
||||||
continue;
|
} else {
|
||||||
}
|
|
||||||
let value = input[2..].trim().to_string();
|
let value = input[2..].trim().to_string();
|
||||||
tasks.add_or_remove_property_column_at_index(value, index);
|
tasks.add_or_remove_property_column_at_index(value, index);
|
||||||
}
|
}
|
||||||
None => {
|
} else if let Some(arg) = arg {
|
||||||
if arg.is_empty() {
|
|
||||||
println!("Available properties:
|
|
||||||
- `id`
|
|
||||||
- `parentid`
|
|
||||||
- `name`
|
|
||||||
- `state`
|
|
||||||
- `hashtags`
|
|
||||||
- `tags` - values of all nostr tags associated with the event, except event tags
|
|
||||||
- `desc` - last note on the task
|
|
||||||
- `description` - accumulated notes on the task
|
|
||||||
- `path` - name including parent tasks
|
|
||||||
- `rpath` - name including parent tasks up to active task
|
|
||||||
- `time` - time tracked on this task
|
|
||||||
- `rtime` - time tracked on this tasks and all recursive subtasks
|
|
||||||
- `progress` - recursive subtask completion in percent
|
|
||||||
- `subtasks` - how many direct subtasks are complete");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tasks.add_or_remove_property_column(arg);
|
tasks.add_or_remove_property_column(arg);
|
||||||
}
|
} else {
|
||||||
|
println!("{}", PROPERTY_COLUMNS);
|
||||||
|
continue
|
||||||
},
|
},
|
||||||
|
|
||||||
Some(',') => tasks.make_note(arg),
|
Some(',') => {
|
||||||
|
match arg {
|
||||||
|
None => {
|
||||||
|
tasks.get_current_task().map_or_else(
|
||||||
|
|| info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
|
||||||
|
|task| println!("{}", task.description_events().map(|e| format!("{} {}", e.created_at.to_human_datetime(), e.content)).join("\n")),
|
||||||
|
);
|
||||||
|
continue
|
||||||
|
},
|
||||||
|
Some(arg) => tasks.make_note(arg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some('>') => {
|
Some('>') => {
|
||||||
tasks.update_state(arg, State::Done);
|
tasks.update_state(&arg_default, State::Done);
|
||||||
tasks.move_up();
|
tasks.move_up();
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('<') => {
|
Some('<') => {
|
||||||
tasks.update_state(arg, State::Closed);
|
tasks.update_state(&arg_default, State::Closed);
|
||||||
tasks.move_up();
|
tasks.move_up();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,33 +322,42 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('?') => {
|
Some('?') => {
|
||||||
tasks.set_state_filter(some_non_empty(arg).filter(|s| !s.is_empty()));
|
tasks.set_state_filter(arg.map(|s| s.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('!') => match tasks.get_position() {
|
Some('!') => match tasks.get_position() {
|
||||||
None => {
|
None => warn!("First select a task to set its state!"),
|
||||||
warn!("First select a task to set its state!");
|
|
||||||
}
|
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
tasks.set_state_for(id, arg, match arg {
|
tasks.set_state_for_with(id, arg_default);
|
||||||
"Closed" => State::Closed,
|
|
||||||
"Done" => State::Done,
|
|
||||||
_ => State::Open,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Some('#') | Some('+') => {
|
Some('#') => {
|
||||||
tasks.add_tag(arg.to_string());
|
match arg {
|
||||||
info!("Added tag filter for #{arg}")
|
Some(arg) => tasks.set_tag(arg.to_string()),
|
||||||
|
None => {
|
||||||
|
println!("Hashtags of all known tasks:\n{}", tasks.all_hashtags().join(" "));
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some('+') => {
|
||||||
|
match arg {
|
||||||
|
Some(arg) => tasks.add_tag(arg.to_string()),
|
||||||
|
None => tasks.clear_filter()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('-') => {
|
Some('-') => {
|
||||||
tasks.remove_tag(arg.to_string());
|
match arg {
|
||||||
info!("Removed tag filter for #{arg}")
|
Some(arg) => tasks.remove_tag(arg),
|
||||||
|
None => tasks.clear_filter()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('*') => {
|
Some('*') => match arg {
|
||||||
|
Some(arg) => {
|
||||||
if let Ok(num) = arg.parse::<i64>() {
|
if let Ok(num) = arg.parse::<i64>() {
|
||||||
tasks.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num)));
|
tasks.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num)));
|
||||||
} else if let Ok(date) = DateTime::parse_from_rfc3339(arg) {
|
} else if let Ok(date) = DateTime::parse_from_rfc3339(arg) {
|
||||||
|
@ -363,6 +366,11 @@ async fn main() {
|
||||||
warn!("Cannot parse {arg}");
|
warn!("Cannot parse {arg}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
// TODO time tracked list
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some('.') => {
|
Some('.') => {
|
||||||
let mut dots = 1;
|
let mut dots = 1;
|
||||||
|
@ -372,12 +380,12 @@ async fn main() {
|
||||||
pos = tasks.get_parent(pos).cloned();
|
pos = tasks.get_parent(pos).cloned();
|
||||||
}
|
}
|
||||||
let slice = &input[dots..];
|
let slice = &input[dots..];
|
||||||
|
tasks.move_to(pos);
|
||||||
if slice.is_empty() {
|
if slice.is_empty() {
|
||||||
tasks.move_to(pos);
|
if dots > 1 {
|
||||||
continue;
|
info!("Moving up {} tasks", dots - 1)
|
||||||
}
|
}
|
||||||
if let Ok(depth) = slice.parse::<i8>() {
|
} else if let Ok(depth) = slice.parse::<i8>() {
|
||||||
tasks.move_to(pos);
|
|
||||||
tasks.set_depth(depth);
|
tasks.set_depth(depth);
|
||||||
} else {
|
} else {
|
||||||
tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id)));
|
tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id)));
|
||||||
|
@ -394,9 +402,7 @@ async fn main() {
|
||||||
let slice = &input[dots..].to_ascii_lowercase();
|
let slice = &input[dots..].to_ascii_lowercase();
|
||||||
if slice.is_empty() {
|
if slice.is_empty() {
|
||||||
tasks.move_to(pos);
|
tasks.move_to(pos);
|
||||||
continue;
|
} else if let Ok(depth) = slice.parse::<i8>() {
|
||||||
}
|
|
||||||
if let Ok(depth) = slice.parse::<i8>() {
|
|
||||||
tasks.move_to(pos);
|
tasks.move_to(pos);
|
||||||
tasks.set_depth(depth);
|
tasks.set_depth(depth);
|
||||||
} else {
|
} else {
|
||||||
|
@ -434,11 +440,14 @@ async fn main() {
|
||||||
if new_relay.is_some() {
|
if new_relay.is_some() {
|
||||||
selected_relay = new_relay;
|
selected_relay = new_relay;
|
||||||
}
|
}
|
||||||
|
//or_print(tasks.print_tasks());
|
||||||
|
continue
|
||||||
} else {
|
} else {
|
||||||
tasks.filter_or_create(&input);
|
tasks.filter_or_create(&input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
or_print(tasks.print_tasks());
|
||||||
}
|
}
|
||||||
Some(Err(e)) => warn!("{}", e),
|
Some(Err(e)) => warn!("{}", e),
|
||||||
None => break,
|
None => break,
|
||||||
|
|
12
src/task.rs
12
src/task.rs
|
@ -49,16 +49,20 @@ impl Task {
|
||||||
.unwrap_or_else(|| self.get_id().to_string())
|
.unwrap_or_else(|| self.get_id().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn descriptions(&self) -> impl Iterator<Item=&String> + '_ {
|
pub(crate) fn description_events(&self) -> impl Iterator<Item=&Event> + '_ {
|
||||||
self.props.iter().filter_map(|event| {
|
self.props.iter().filter_map(|event| {
|
||||||
if event.kind == Kind::TextNote {
|
if event.kind == Kind::TextNote {
|
||||||
Some(&event.content)
|
Some(event)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn descriptions(&self) -> impl Iterator<Item=&String> + '_ {
|
||||||
|
self.description_events().map(|e| &e.content)
|
||||||
|
}
|
||||||
|
|
||||||
fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
|
fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
|
||||||
self.props.iter().filter_map(|event| {
|
self.props.iter().filter_map(|event| {
|
||||||
event.kind.try_into().ok().map(|s| TaskState {
|
event.kind.try_into().ok().map(|s| TaskState {
|
||||||
|
@ -118,11 +122,11 @@ impl Task {
|
||||||
self.props
|
self.props
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
|
.map(|e| format!("{} kind {} \"{}\"", e.created_at, e.kind, e.content))
|
||||||
.collect::<Vec<String>>()
|
.collect_vec()
|
||||||
)),
|
)),
|
||||||
"descriptions" => Some(format!(
|
"descriptions" => Some(format!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
self.descriptions().collect::<Vec<&String>>()
|
self.descriptions().collect_vec()
|
||||||
)),
|
)),
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Unknown task property {}", property);
|
warn!("Unknown task property {}", property);
|
||||||
|
|
140
src/tasks.rs
140
src/tasks.rs
|
@ -1,10 +1,11 @@
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::io::{Error, stdout, Write};
|
use std::io::{Error, stdout, Write};
|
||||||
use std::iter::once;
|
use std::iter::{once, Sum};
|
||||||
use std::ops::{Div, Rem};
|
use std::ops::{Div, Rem};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{DateTime, Local, TimeZone};
|
||||||
use chrono::LocalResult::Single;
|
use chrono::LocalResult::Single;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -103,45 +104,28 @@ impl Tasks {
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total time tracked on this task by the current user.
|
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
|
||||||
pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
|
self.tasks.values()
|
||||||
Self::time_tracked_for(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![id])
|
.filter(|t| t.pure_state() != State::Closed)
|
||||||
|
.filter_map(|t| t.tags.as_ref()).flatten()
|
||||||
|
.filter(|tag| is_hashtag(tag))
|
||||||
|
.filter_map(|tag| tag.content().map(|s| s.trim()))
|
||||||
|
.sorted_unstable()
|
||||||
|
.dedup()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total time tracked on this task and its subtasks by all users.
|
/// Total time in seconds tracked on this task by the current user.
|
||||||
/// TODO needs testing!
|
pub(crate) fn time_tracked(&self, id: EventId) -> u64 {
|
||||||
|
TimesTracked::from(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![id]).sum::<Duration>().as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total time in seconds tracked on this task and its subtasks by all users.
|
||||||
fn total_time_tracked(&self, id: EventId) -> u64 {
|
fn total_time_tracked(&self, id: EventId) -> u64 {
|
||||||
let mut total = 0;
|
let mut total = 0;
|
||||||
|
|
||||||
let children = self.get_subtasks(id);
|
let children = self.get_subtasks(id);
|
||||||
for user in self.history.values() {
|
for user in self.history.values() {
|
||||||
total += Self::time_tracked_for(user, &children);
|
total += TimesTracked::from(user, &children).into_iter().sum::<Duration>().as_secs();
|
||||||
}
|
|
||||||
total
|
|
||||||
}
|
|
||||||
|
|
||||||
fn time_tracked_for<'a, E>(events: E, ids: &Vec<EventId>) -> u64
|
|
||||||
where
|
|
||||||
E: IntoIterator<Item=&'a Event>,
|
|
||||||
{
|
|
||||||
let mut total = 0;
|
|
||||||
let mut start: Option<Timestamp> = None;
|
|
||||||
for event in events {
|
|
||||||
match event.tags.first().and_then(|tag| tag.as_standardized()) {
|
|
||||||
Some(TagStandard::Event {
|
|
||||||
event_id,
|
|
||||||
..
|
|
||||||
}) if ids.contains(event_id) => {
|
|
||||||
start = start.or(Some(event.created_at))
|
|
||||||
}
|
|
||||||
_ => if let Some(stamp) = start {
|
|
||||||
total += (event.created_at - stamp).as_u64();
|
|
||||||
start = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(start) = start {
|
|
||||||
total += (Timestamp::now() - start).as_u64();
|
|
||||||
}
|
}
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
@ -253,7 +237,7 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn current_task(&self) -> Option<&Task> {
|
pub(crate) fn get_current_task(&self) -> Option<&Task> {
|
||||||
self.position.and_then(|id| self.get_by_id(&id))
|
self.position.and_then(|id| self.get_by_id(&id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +250,7 @@ impl Tasks {
|
||||||
|
|
||||||
pub(crate) fn current_tasks(&self) -> Vec<&Task> {
|
pub(crate) fn current_tasks(&self) -> Vec<&Task> {
|
||||||
if self.depth == 0 {
|
if self.depth == 0 {
|
||||||
return self.current_task().into_iter().collect();
|
return self.get_current_task().into_iter().collect();
|
||||||
}
|
}
|
||||||
let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
|
let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
|
||||||
if res.len() > 0 {
|
if res.len() > 0 {
|
||||||
|
@ -295,7 +279,7 @@ impl Tasks {
|
||||||
|
|
||||||
pub(crate) fn print_tasks(&self) -> Result<(), Error> {
|
pub(crate) fn print_tasks(&self) -> Result<(), Error> {
|
||||||
let mut lock = stdout().lock();
|
let mut lock = stdout().lock();
|
||||||
if let Some(t) = self.current_task() {
|
if let Some(t) = self.get_current_task() {
|
||||||
let state = t.state_or_default();
|
let state = t.state_or_default();
|
||||||
writeln!(
|
writeln!(
|
||||||
lock,
|
lock,
|
||||||
|
@ -362,7 +346,6 @@ impl Tasks {
|
||||||
.join(" \t")
|
.join(" \t")
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
writeln!(lock)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,23 +355,42 @@ impl Tasks {
|
||||||
self.view = view;
|
self.view = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_filter(&mut self) {
|
||||||
|
self.view.clear();
|
||||||
|
self.tags.clear();
|
||||||
|
info!("Removed all filters");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_tag(&mut self, tag: String) {
|
||||||
|
self.tags.clear();
|
||||||
|
self.add_tag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn add_tag(&mut self, tag: String) {
|
pub(crate) fn add_tag(&mut self, tag: String) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
|
info!("Added tag filter for #{tag}");
|
||||||
self.tags.insert(Hashtag(tag).into());
|
self.tags.insert(Hashtag(tag).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_tag(&mut self, tag: String) {
|
pub(crate) fn remove_tag(&mut self, tag: &str) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
self.tags.retain(|t| !t.content().is_some_and(|value| value.to_string().starts_with(&tag)));
|
let len = self.tags.len();
|
||||||
|
self.tags.retain(|t| !t.content().is_some_and(|value| value.to_string().starts_with(tag)));
|
||||||
|
if self.tags.len() < len {
|
||||||
|
info!("Removed tag filters starting with {tag}");
|
||||||
|
} else {
|
||||||
|
info!("Found no tag filters starting with {tag} to remove");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
|
pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
|
info!("Filtering for {}", state.as_ref().map_or("open tasks".to_string(), |s| format!("state {s}")));
|
||||||
self.state = state;
|
self.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn move_up(&mut self) {
|
pub(crate) fn move_up(&mut self) {
|
||||||
self.move_to(self.current_task().and_then(|t| t.parent_id()).cloned());
|
self.move_to(self.get_current_task().and_then(|t| t.parent_id()).cloned());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn flush(&self) {
|
pub(crate) fn flush(&self) {
|
||||||
|
@ -428,7 +430,12 @@ impl Tasks {
|
||||||
0 => {
|
0 => {
|
||||||
// No match, new task
|
// No match, new task
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
|
if arg.len() > 2 {
|
||||||
Some(self.make_task(arg))
|
Some(self.make_task(arg))
|
||||||
|
} else {
|
||||||
|
warn!("Not creating task under 3 chars to avoid silly mistakes");
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
// One match, activate
|
// One match, activate
|
||||||
|
@ -481,7 +488,9 @@ impl Tasks {
|
||||||
|
|
||||||
/// Sanitizes input
|
/// Sanitizes input
|
||||||
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
||||||
self.submit(self.parse_task(input.trim()))
|
let id = self.submit(self.parse_task(input.trim()));
|
||||||
|
self.state.clone().inspect(|s| self.set_state_for_with(id, s));
|
||||||
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_prop(
|
pub(crate) fn build_prop(
|
||||||
|
@ -579,6 +588,14 @@ impl Tasks {
|
||||||
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) {
|
||||||
|
self.set_state_for(id, comment, match comment {
|
||||||
|
"Closed" => State::Closed,
|
||||||
|
"Done" => State::Done,
|
||||||
|
_ => 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 {
|
||||||
let prop = self.build_prop(
|
let prop = self.build_prop(
|
||||||
state.into(),
|
state.into(),
|
||||||
|
@ -636,7 +653,6 @@ impl Tasks {
|
||||||
self.properties.insert(index, property);
|
self.properties.insert(index, property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats the given seconds according to the given format.
|
/// Formats the given seconds according to the given format.
|
||||||
|
@ -676,6 +692,43 @@ pub(crate) fn join_tasks<'a>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct TimesTracked<'a> {
|
||||||
|
events: Box<dyn Iterator<Item=&'a Event> + 'a>,
|
||||||
|
ids: &'a Vec<EventId>,
|
||||||
|
}
|
||||||
|
impl TimesTracked<'_> {
|
||||||
|
fn from<'b>(events: impl IntoIterator<Item=&'b Event> + 'b, ids: &'b Vec<EventId>) -> TimesTracked<'b> {
|
||||||
|
TimesTracked {
|
||||||
|
events: Box::new(events.into_iter()),
|
||||||
|
ids,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for TimesTracked<'_> {
|
||||||
|
type Item = Duration;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let mut start: Option<u64> = None;
|
||||||
|
while let Some(event) = self.events.next() {
|
||||||
|
match event.tags.first().and_then(|tag| tag.as_standardized()) {
|
||||||
|
Some(TagStandard::Event {
|
||||||
|
event_id,
|
||||||
|
..
|
||||||
|
}) if self.ids.contains(event_id) => {
|
||||||
|
start = start.or(Some(event.created_at.as_u64()))
|
||||||
|
}
|
||||||
|
_ => if let Some(stamp) = start {
|
||||||
|
return Some(Duration::from_secs(event.created_at.as_u64() - stamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return start.map(|stamp| Duration::from_secs(Timestamp::now().as_u64() - stamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ParentIterator<'a> {
|
struct ParentIterator<'a> {
|
||||||
tasks: &'a TaskMap,
|
tasks: &'a TaskMap,
|
||||||
current: Option<EventId>,
|
current: Option<EventId>,
|
||||||
|
@ -822,7 +875,6 @@ mod tasks_test {
|
||||||
"0000000000000000000000000000000000000000000000000000000000000000>test"
|
"0000000000000000000000000000000000000000000000000000000000000000>test"
|
||||||
);
|
);
|
||||||
assert_eq!(tasks.relative_path(dangling), "test");
|
assert_eq!(tasks.relative_path(dangling), "test");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
Loading…
Reference in New Issue