Compare commits
6 Commits
bf802e3195
...
a2505e94fb
Author | SHA1 | Date |
---|---|---|
xeruf | a2505e94fb | |
xeruf | 8c2c279238 | |
xeruf | aa468f80c5 | |
xeruf | 55d856c75d | |
xeruf | e16e21a477 | |
xeruf | 9619435c03 |
|
@ -100,9 +100,10 @@ when the application is terminated regularly.
|
||||||
Dots can be repeated to move to parent tasks.
|
Dots can be repeated to move to parent tasks.
|
||||||
|
|
||||||
- `:[IND][COL]` - add property column COL at IND or end, if it already exists remove property column COL or IND
|
- `:[IND][COL]` - add property column COL at IND or end, if it already exists remove property column COL or IND
|
||||||
- `>[TEXT]` - Complete active task and move to parent, with optional state description
|
- `*[TIME]` - add timetracking with the specified offset
|
||||||
- `<[TEXT]` - Close active task and move to parent, with optional state description
|
- `>[TEXT]` - complete active task and move to parent, with optional state description
|
||||||
- `!TEXT` - Set state for current task from text
|
- `<[TEXT]` - close active task and move to parent, with optional state description
|
||||||
|
- `!TEXT` - set state for current task from text
|
||||||
- `,TEXT` - add text note (comment / description)
|
- `,TEXT` - add text note (comment / description)
|
||||||
- `@` - undoes last action (moving in place or upwards or waiting a minute confirms pending actions)
|
- `@` - undoes last action (moving in place or upwards or waiting a minute confirms pending actions)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
use itertools::Itertools;
|
||||||
|
use log::info;
|
||||||
|
use nostr_sdk::{Alphabet, EventBuilder, EventId, GenericTagValue, Kind, Tag};
|
||||||
|
|
||||||
|
pub const TASK_KIND: u64 = 1621;
|
||||||
|
pub const TRACKING_KIND: u64 = 1650;
|
||||||
|
|
||||||
|
pub(crate) fn build_tracking<I>(id: I) -> EventBuilder
|
||||||
|
where I: IntoIterator<Item=EventId> {
|
||||||
|
EventBuilder::new(
|
||||||
|
Kind::from(TRACKING_KIND),
|
||||||
|
"",
|
||||||
|
id.into_iter().map(|id| Tag::event(id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_task(name: &str, tags: Vec<Tag>) -> EventBuilder {
|
||||||
|
info!("Created task \"{name}\" with tags [{}]", tags.iter().map(|tag| format_tag(tag)).join(", "));
|
||||||
|
EventBuilder::new(Kind::from(TASK_KIND), name, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_tag(tag: &Tag) -> String {
|
||||||
|
tag.content().map(|c| {
|
||||||
|
match c {
|
||||||
|
GenericTagValue::PublicKey(key) => format!("Key: {}", key.to_string()[..8].to_string()),
|
||||||
|
GenericTagValue::EventId(id) => format!("Parent: {}", id.to_string()[..8].to_string()),
|
||||||
|
GenericTagValue::String(str) => {
|
||||||
|
if is_hashtag(tag) {
|
||||||
|
format!("#{str}")
|
||||||
|
} else {
|
||||||
|
str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).unwrap_or_else(|| format!("Kind {}", tag.kind()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_hashtag(tag: &Tag) -> bool {
|
||||||
|
tag.single_letter_tag()
|
||||||
|
.is_some_and(|sltag| sltag.character == Alphabet::T)
|
||||||
|
}
|
||||||
|
|
45
src/main.rs
45
src/main.rs
|
@ -10,19 +10,19 @@ use std::str::FromStr;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
|
||||||
|
use crate::kinds::TRACKING_KIND;
|
||||||
use crate::task::State;
|
use crate::task::State;
|
||||||
use crate::tasks::Tasks;
|
use crate::tasks::Tasks;
|
||||||
|
|
||||||
mod task;
|
mod task;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
mod kinds;
|
||||||
const TASK_KIND: u64 = 1621;
|
|
||||||
const TRACKING_KIND: u64 = 1650;
|
|
||||||
|
|
||||||
type Events = Vec<Event>;
|
type Events = Vec<Event>;
|
||||||
|
|
||||||
|
@ -34,11 +34,16 @@ struct EventSender {
|
||||||
}
|
}
|
||||||
impl EventSender {
|
impl EventSender {
|
||||||
fn submit(&self, event_builder: EventBuilder) -> Result<Event> {
|
fn submit(&self, event_builder: EventBuilder) -> Result<Event> {
|
||||||
if let Some(event) = self.queue.borrow().first() {
|
{
|
||||||
// Flush if oldest event older than a minute
|
// Flush if oldest event older than a minute
|
||||||
if event.created_at < Timestamp::now().sub(60u64) {
|
let borrow = self.queue.borrow();
|
||||||
debug!("Flushing Event Queue because it is older than a minute");
|
if let Some(event) = borrow.first() {
|
||||||
self.flush();
|
let old = event.created_at < Timestamp::now().sub(60u64);
|
||||||
|
drop(borrow);
|
||||||
|
if old {
|
||||||
|
debug!("Flushing event queue because it is older than a minute");
|
||||||
|
self.force_flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut queue = self.queue.borrow_mut();
|
let mut queue = self.queue.borrow_mut();
|
||||||
|
@ -51,11 +56,16 @@ impl EventSender {
|
||||||
queue.push(event.clone());
|
queue.push(event.clone());
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
fn flush(&self) {
|
/// Sends all pending events
|
||||||
|
fn force_flush(&self) {
|
||||||
debug!("Flushing {} events from queue", self.queue.borrow().len());
|
debug!("Flushing {} events from queue", self.queue.borrow().len());
|
||||||
if self.queue.borrow().len() > 0 {
|
|
||||||
or_print(self.tx.send(self.clear()));
|
or_print(self.tx.send(self.clear()));
|
||||||
}
|
}
|
||||||
|
/// Sends all pending events if there is a non-tracking event
|
||||||
|
fn flush(&self) {
|
||||||
|
if self.queue.borrow().iter().any(|event| event.kind.as_u64() != TRACKING_KIND) {
|
||||||
|
self.force_flush()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn clear(&self) -> Events {
|
fn clear(&self) -> Events {
|
||||||
trace!("Cleared queue: {:?}", self.queue.borrow());
|
trace!("Cleared queue: {:?}", self.queue.borrow());
|
||||||
|
@ -67,7 +77,7 @@ impl EventSender {
|
||||||
}
|
}
|
||||||
impl Drop for EventSender {
|
impl Drop for EventSender {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.flush()
|
self.force_flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,6 +265,7 @@ async fn main() {
|
||||||
};
|
};
|
||||||
match op {
|
match op {
|
||||||
None => {
|
None => {
|
||||||
|
debug!("Flushing Tasks because of empty command");
|
||||||
tasks.flush()
|
tasks.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,7 +315,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Some(',') => tasks.add_note(arg),
|
Some(',') => tasks.make_note(arg),
|
||||||
|
|
||||||
Some('>') => {
|
Some('>') => {
|
||||||
tasks.update_state(arg, State::Done);
|
tasks.update_state(arg, State::Done);
|
||||||
|
@ -342,7 +353,17 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('-') => {
|
Some('-') => {
|
||||||
tasks.remove_tag(arg.to_string())
|
tasks.remove_tag(arg.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some('*') => {
|
||||||
|
if let Ok(num) = arg.parse::<i64>() {
|
||||||
|
tasks.track_at(Timestamp::now() + num);
|
||||||
|
} else if let Ok(date) = DateTime::parse_from_rfc3339(arg) {
|
||||||
|
tasks.track_at(Timestamp::from(date.to_utc().timestamp() as u64));
|
||||||
|
} else {
|
||||||
|
warn!("Cannot parse {arg}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('.') => {
|
Some('.') => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, Kind, Tag, Timestamp};
|
use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, Kind, Tag, Timestamp};
|
||||||
|
|
||||||
use crate::EventSender;
|
use crate::kinds::is_hashtag;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) struct Task {
|
pub(crate) struct Task {
|
||||||
|
@ -110,10 +110,7 @@ impl Task {
|
||||||
"name" => Some(self.event.content.clone()),
|
"name" => Some(self.event.content.clone()),
|
||||||
"desc" => self.descriptions().last().cloned(),
|
"desc" => self.descriptions().last().cloned(),
|
||||||
"description" => Some(self.descriptions().join(" ")),
|
"description" => Some(self.descriptions().join(" ")),
|
||||||
"hashtags" => self.filter_tags(|tag| {
|
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
|
||||||
tag.single_letter_tag()
|
|
||||||
.is_some_and(|sltag| sltag.character == Alphabet::T)
|
|
||||||
}),
|
|
||||||
"tags" => self.filter_tags(|_| true),
|
"tags" => self.filter_tags(|_| true),
|
||||||
"alltags" => Some(format!("{:?}", self.tags)),
|
"alltags" => Some(format!("{:?}", self.tags)),
|
||||||
"props" => Some(format!(
|
"props" => Some(format!(
|
||||||
|
|
71
src/tasks.rs
71
src/tasks.rs
|
@ -8,10 +8,11 @@ use chrono::LocalResult::Single;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::{Event, EventBuilder, EventId, Kind, PublicKey, Tag, Timestamp};
|
use nostr_sdk::{Event, EventBuilder, EventId, GenericTagValue, Kind, PublicKey, Tag, Timestamp};
|
||||||
use nostr_sdk::Tag::Hashtag;
|
use nostr_sdk::Tag::Hashtag;
|
||||||
|
|
||||||
use crate::{EventSender, TASK_KIND, TRACKING_KIND};
|
use crate::EventSender;
|
||||||
|
use crate::kinds::*;
|
||||||
use crate::task::{State, Task};
|
use crate::task::{State, Task};
|
||||||
|
|
||||||
type TaskMap = HashMap<EventId, Task>;
|
type TaskMap = HashMap<EventId, Task>;
|
||||||
|
@ -447,13 +448,7 @@ impl Tasks {
|
||||||
self.flush();
|
self.flush();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.submit(
|
self.submit(build_tracking(id));
|
||||||
EventBuilder::new(
|
|
||||||
Kind::from(TRACKING_KIND),
|
|
||||||
"",
|
|
||||||
id.iter().map(|id| Tag::event(id.clone())),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if !id.and_then(|id| self.tasks.get(&id)).is_some_and(|t| t.parent_id() == self.position.as_ref()) {
|
if !id.and_then(|id| self.tasks.get(&id)).is_some_and(|t| t.parent_id() == self.position.as_ref()) {
|
||||||
debug!("Flushing Tasks because of move");
|
debug!("Flushing Tasks because of move");
|
||||||
self.flush();
|
self.flush();
|
||||||
|
@ -464,11 +459,11 @@ impl Tasks {
|
||||||
// Updates
|
// Updates
|
||||||
|
|
||||||
/// Expects sanitized input
|
/// Expects sanitized input
|
||||||
pub(crate) fn build_task(&self, input: &str) -> EventBuilder {
|
pub(crate) fn parse_task(&self, input: &str) -> EventBuilder {
|
||||||
let mut tags: Vec<Tag> = self.tags.iter().cloned().collect();
|
let mut tags: Vec<Tag> = self.tags.iter().cloned().collect();
|
||||||
self.position.inspect(|p| tags.push(Tag::event(*p)));
|
self.position.inspect(|p| tags.push(Tag::event(*p)));
|
||||||
return match input.split_once(": ") {
|
match input.split_once(": ") {
|
||||||
None => EventBuilder::new(Kind::from(TASK_KIND), input, tags),
|
None => build_task(input, tags),
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
tags.append(
|
tags.append(
|
||||||
&mut s
|
&mut s
|
||||||
|
@ -477,14 +472,14 @@ impl Tasks {
|
||||||
.map(|t| Hashtag(t.to_string()))
|
.map(|t| Hashtag(t.to_string()))
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
EventBuilder::new(Kind::from(TASK_KIND), s.0, tags)
|
build_task(s.0, tags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.build_task(input.trim()))
|
self.submit(self.parse_task(input.trim()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_prop(
|
pub(crate) fn build_prop(
|
||||||
|
@ -500,6 +495,18 @@ impl Tasks {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_task_title(&self, id: &EventId) -> String {
|
||||||
|
self.tasks.get(id).map_or(id.to_string(), |t| t.get_title())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn track_at(&mut self, time: Timestamp) -> EventId {
|
||||||
|
info!("Tracking \"{:?}\" from {}", self.position.map(|id| self.get_task_title(&id)), time.to_human_datetime());
|
||||||
|
self.submit(
|
||||||
|
build_tracking(self.get_position())
|
||||||
|
.custom_created_at(time)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn submit(&mut self, builder: EventBuilder) -> EventId {
|
fn submit(&mut self, builder: EventBuilder) -> EventId {
|
||||||
let event = self.sender.submit(builder).unwrap();
|
let event = self.sender.submit(builder).unwrap();
|
||||||
let id = event.id;
|
let id = event.id;
|
||||||
|
@ -562,13 +569,12 @@ impl Tasks {
|
||||||
self.submit(prop)
|
self.submit(prop)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_state(&mut self, comment: &str, state: State)
|
pub(crate) fn update_state(&mut self, comment: &str, state: State) {
|
||||||
{
|
|
||||||
self.position
|
self.position
|
||||||
.map(|id| self.set_state_for(id, comment, state));
|
.map(|id| self.set_state_for(id, comment, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_note(&mut self, note: &str) {
|
pub(crate) fn make_note(&mut self, note: &str) {
|
||||||
match self.position {
|
match self.position {
|
||||||
None => warn!("Cannot add note '{}' without active task", note),
|
None => warn!("Cannot add note '{}' without active task", note),
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
|
@ -579,13 +585,18 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formats the given seconds according to the given format.
|
||||||
|
/// MMM - minutes
|
||||||
|
/// MM - minutes of the hour
|
||||||
|
/// HH - hours
|
||||||
|
/// Returns an empty string if under a minute.
|
||||||
fn display_time(format: &str, secs: u64) -> String {
|
fn display_time(format: &str, secs: u64) -> String {
|
||||||
Some(secs / 60)
|
Some(secs / 60)
|
||||||
.filter(|t| t > &0)
|
.filter(|t| t > &0)
|
||||||
.map_or(String::new(), |mins| format
|
.map_or(String::new(), |mins| format
|
||||||
|
.replace("MMM", &format!("{:3}", mins))
|
||||||
.replace("HH", &format!("{:02}", mins.div(60)))
|
.replace("HH", &format!("{:02}", mins.div(60)))
|
||||||
.replace("MM", &format!("{:02}", mins.rem(60)))
|
.replace("MM", &format!("{:02}", mins.rem(60)))
|
||||||
.replace("MMM", &format!("{:3}", mins)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,17 +641,21 @@ impl<'a> Iterator for ParentIterator<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn stub_tasks() -> Tasks {
|
||||||
fn test_depth() {
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use nostr_sdk::Keys;
|
use nostr_sdk::Keys;
|
||||||
|
|
||||||
let (tx, _rx) = mpsc::channel();
|
let (tx, _rx) = mpsc::channel();
|
||||||
let mut tasks = Tasks::from(EventSender {
|
Tasks::from(EventSender {
|
||||||
tx,
|
tx,
|
||||||
keys: Keys::generate(),
|
keys: Keys::generate(),
|
||||||
queue: Default::default(),
|
queue: Default::default(),
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_depth() {
|
||||||
|
let mut tasks = stub_tasks();
|
||||||
|
|
||||||
let t1 = tasks.make_task("t1");
|
let t1 = tasks.make_task("t1");
|
||||||
let task1 = tasks.get_by_id(&t1).unwrap();
|
let task1 = tasks.get_by_id(&t1).unwrap();
|
||||||
|
@ -700,12 +715,22 @@ fn test_depth() {
|
||||||
assert_eq!(tasks.current_tasks().len(), 4);
|
assert_eq!(tasks.current_tasks().len(), 4);
|
||||||
tasks.depth = -1;
|
tasks.depth = -1;
|
||||||
assert_eq!(tasks.current_tasks().len(), 2);
|
assert_eq!(tasks.current_tasks().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_task_title_fallback_to_id() {
|
||||||
|
let mut tasks = stub_tasks();
|
||||||
|
|
||||||
let empty = tasks.make_task("");
|
let empty = tasks.make_task("");
|
||||||
let empty_task = tasks.get_by_id(&empty).unwrap();
|
let empty_task = tasks.get_by_id(&empty).unwrap();
|
||||||
let empty_id = empty_task.event.id.to_string();
|
let empty_id = empty_task.event.id.to_string();
|
||||||
assert_eq!(empty_task.get_title(), empty_id);
|
assert_eq!(empty_task.get_title(), empty_id);
|
||||||
assert_eq!(tasks.get_task_path(Some(empty)), 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();
|
let zero = EventId::all_zeros();
|
||||||
assert_eq!(tasks.get_task_path(Some(zero)), zero.to_string());
|
assert_eq!(tasks.get_task_path(Some(zero)), zero.to_string());
|
||||||
|
|
Loading…
Reference in New Issue