feat: add ability to schedule any action

This commit is contained in:
xeruf 2024-09-22 20:05:05 +02:00
parent 34657540de
commit 945eb6906a
4 changed files with 87 additions and 51 deletions

View File

@ -106,6 +106,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
- `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up - `|[TASK]` - (un)mark current task as procedure or create a sibling task depending on the current one and move up
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.
Append `@TIME` to any task creation or change command to record the action with the given time.
- `:[IND][PROP]` - add property column PROP at IND or end, - `:[IND][PROP]` - add property column PROP at IND or end,
if it already exists remove property column PROP or IND; empty: list properties if it already exists remove property column PROP or IND; empty: list properties

View File

@ -11,6 +11,25 @@ pub fn some_non_empty(str: &str) -> Option<String> {
if str.is_empty() { None } else { Some(str.to_string()) } if str.is_empty() { None } else { Some(str.to_string()) }
} }
pub fn trim_start_count(str: &str, char: char) -> (&str, usize) {
let len = str.len();
let result = str.trim_start_matches(char);
let dots = len - result.len();
(result, dots)
}
pub trait ToTimestamp {
fn to_timestamp(&self) -> Timestamp;
}
impl<T: TimeZone> ToTimestamp for DateTime<T> {
fn to_timestamp(&self) -> Timestamp {
let stamp = self.to_utc().timestamp();
if let Some(t) = 0u64.checked_add_signed(stamp) {
Timestamp::from(t)
} else { Timestamp::zero() }
}
}
/// Parses the hour from a plain number in the String, /// Parses the hour from a plain number in the String,
/// with max of max_future hours into the future. /// with max of max_future hours into the future.
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> { pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
@ -57,7 +76,7 @@ pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
/// - Otherwise try to parse a relative date /// - Otherwise try to parse a relative date
pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> { pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
if let Some(num) = parse_hour(str, 6) { if let Some(num) = parse_hour(str, 6) {
return Some(Timestamp::from(num.to_utc().timestamp() as u64)); return Some(num.to_timestamp());
} }
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in "); let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
if let Ok(num) = stripped.parse::<i64>() { if let Ok(num) = stripped.parse::<i64>() {

View File

@ -361,34 +361,49 @@ async fn main() -> Result<()> {
relays.values_mut().for_each(|tasks| tasks.process_overflow()); relays.values_mut().for_each(|tasks| tasks.process_overflow());
} }
let mut iter = input.chars(); let tasks = relays.get_mut(&selected_relay).unwrap();
let op = iter.next();
let arg = if input.len() > 1 { let operator = input.chars().next();
Some(input[1..].trim()) let mut command = input;
match operator {
None => {
debug!("Flushing Tasks because of empty command");
tasks.flush();
or_warn!(tasks.print_tasks());
continue;
}
Some('@') => {}
Some(_) => {
if let Some((left, arg)) = command.split_once("@") {
if let Some(time) = parse_hour(arg, 20)
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) {
command = left.to_string();
tasks.custom_time = Some(time.to_timestamp());
}
}
}
}
let arg = if command.len() > 1 {
Some(command[1..].trim())
} else { } else {
None None
}; };
let arg_default = arg.unwrap_or(""); let arg_default = arg.unwrap_or("");
let tasks = relays.get_mut(&selected_relay).unwrap(); match operator {
match op {
None => {
debug!("Flushing Tasks because of empty command");
tasks.flush();
}
Some(':') => { Some(':') => {
let mut iter = arg_default.chars();
let next = iter.next(); let next = iter.next();
let remaining = iter.collect::<String>().trim().to_string();
if let Some(':') = next { if let Some(':') = next {
let str: String = iter.collect(); let props = remaining.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>();
let result = str.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>(); if props.len() == 1 {
if result.len() == 1 { tasks.add_sorting_property(remaining)
tasks.add_sorting_property(str.trim().to_string())
} else { } else {
tasks.set_sorting(result) tasks.set_sorting(props)
} }
} else if let Some(digit) = next.and_then(|s| s.to_digit(10)) { } else if let Some(digit) = 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();
if remaining.is_empty() { if remaining.is_empty() {
tasks.get_columns().remove_at(index); tasks.get_columns().remove_at(index);
} else { } else {
@ -467,11 +482,7 @@ async fn main() -> Result<()> {
.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 from {}", format_datetime_relative(time)); info!("Filtering for tasks from {}", format_datetime_relative(time));
let threshold = time.to_utc().timestamp(); tasks.set_filter_from(time.to_timestamp())
tasks.set_filter_from(
if let Some(t) = 0u64.checked_add_signed(threshold) {
Timestamp::from(t)
} else { Timestamp::zero() })
}) })
.unwrap_or(false) .unwrap_or(false)
} }
@ -607,59 +618,49 @@ async fn main() -> Result<()> {
} }
Some('.') => { Some('.') => {
let mut dots = 1; let (remaining, dots) = trim_start_count(&command, '.');
let mut pos = tasks.get_position_ref(); let pos = tasks.up_by(dots - 1);
for _ in iter.take_while(|c| c == &'.') {
dots += 1;
pos = tasks.get_parent(pos);
}
let slice = input[dots..].trim(); if remaining.is_empty() {
if slice.is_empty() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
if dots > 1 { if dots > 1 {
info!("Moving up {} tasks", dots - 1) info!("Moving up {} tasks", dots - 1)
} else { } else {
tasks.clear_filters(); tasks.clear_filters();
} }
} else if let Ok(depth) = slice.parse::<usize>() { } else if let Ok(depth) = remaining.parse::<usize>() {
if pos != tasks.get_position_ref() { if pos != tasks.get_position_ref() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
} }
tasks.set_depth(depth); tasks.set_depth(depth);
} else { } else {
tasks.filter_or_create(pos.cloned().as_ref(), slice).map(|id| tasks.move_to(Some(id))); tasks.filter_or_create(pos.cloned().as_ref(), &remaining).map(|id| tasks.move_to(Some(id)));
} }
} }
Some('/') => if arg.is_none() { Some('/') => if arg.is_none() {
tasks.move_to(None); tasks.move_to(None);
} else { } else {
let mut dots = 1; let (remaining, dots) = trim_start_count(&command, '/');
let mut pos = tasks.get_position_ref(); let pos = tasks.up_by(dots - 1);
for _ in iter.take_while(|c| c == &'/') {
dots += 1;
pos = tasks.get_parent(pos);
}
let slice = input[dots..].trim(); if remaining.is_empty() {
if slice.is_empty() {
tasks.move_to(pos.cloned()); tasks.move_to(pos.cloned());
if dots > 1 { if dots > 1 {
info!("Moving up {} tasks", dots - 1) info!("Moving up {} tasks", dots - 1)
} }
} 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 !remaining.chars().any(|c| c.is_ascii_uppercase()) {
// Smart-case - case-sensitive if any uppercase char is entered // Smart-case - case-sensitive if any uppercase char is entered
transform = Box::new(|s| s.to_ascii_lowercase()); transform = Box::new(|s| s.to_ascii_lowercase());
} }
let filtered = let filtered =
tasks.get_filtered(|t| { tasks.get_filtered(|t| {
transform(&t.event.content).contains(slice) || transform(&t.event.content).contains(&remaining) ||
t.tags.iter().flatten().any( t.tags.iter().flatten().any(
|tag| tag.content().is_some_and(|s| transform(s).contains(slice))) |tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
}); });
if filtered.len() == 1 { if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().next()); tasks.move_to(filtered.into_iter().next());
@ -671,14 +672,14 @@ async fn main() -> Result<()> {
} }
_ => _ =>
if Regex::new("^wss?://").unwrap().is_match(input.trim()) { if Regex::new("^wss?://").unwrap().is_match(command.trim()) {
tasks.move_to(None); tasks.move_to(None);
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&input))) { if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&command))) {
selected_relay.clone_from(url); selected_relay.clone_from(url);
or_warn!(tasks.print_tasks()); or_warn!(tasks.print_tasks());
continue; continue;
} }
or_warn!(Url::parse(&input), "Failed to parse url {}", input).map(|url| { or_warn!(Url::parse(&command), "Failed to parse url {}", command).map(|url| {
match tx.try_send(MostrMessage::NewRelay(url.clone())) { match tx.try_send(MostrMessage::NewRelay(url.clone())) {
Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"), Err(e) => error!("Nostr communication thread failure, cannot add relay \"{url}\": {e}"),
Ok(_) => { Ok(_) => {
@ -689,16 +690,17 @@ async fn main() -> Result<()> {
} }
}); });
continue; continue;
} else if input.contains('\n') { } else if command.contains('\n') {
input.split('\n').for_each(|line| { command.split('\n').for_each(|line| {
if !line.trim().is_empty() { if !line.trim().is_empty() {
tasks.make_task(line); tasks.make_task(line);
} }
}); });
} else { } else {
tasks.filter_or_create(tasks.get_position().as_ref(), &input); tasks.filter_or_create(tasks.get_position().as_ref(), &command);
} }
} }
tasks.custom_time = None;
or_warn!(tasks.print_tasks()); or_warn!(tasks.print_tasks());
} }
Err(ReadlineError::Eof) => break, Err(ReadlineError::Eof) => break,

View File

@ -76,6 +76,7 @@ pub(crate) struct TasksRelay {
sender: EventSender, sender: EventSender,
overflow: VecDeque<Event>, overflow: VecDeque<Event>,
pub(crate) custom_time: Option<Timestamp>,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -167,6 +168,7 @@ impl TasksRelay {
sender, sender,
overflow: Default::default(), overflow: Default::default(),
custom_time: None,
} }
} }
@ -309,6 +311,14 @@ impl TasksRelay {
// Parents // Parents
pub(crate) fn up_by(&self, count: usize) -> Option<&EventId> {
let mut pos = self.get_position_ref();
for _ in 0..count {
pos = self.get_parent(pos);
}
pos
}
pub(crate) fn get_parent(&self, id: Option<&EventId>) -> Option<&EventId> { pub(crate) fn get_parent(&self, id: Option<&EventId>) -> Option<&EventId> {
id.and_then(|id| self.get_by_id(id)) id.and_then(|id| self.get_by_id(id))
.and_then(|t| t.parent_id()) .and_then(|t| t.parent_id())
@ -597,6 +607,7 @@ impl TasksRelay {
} }
pub(crate) fn set_filter_from(&mut self, time: Timestamp) -> bool { pub(crate) fn set_filter_from(&mut self, time: Timestamp) -> bool {
// TODO filter at both ends
self.set_filter(|t| t.last_state_update() > time) self.set_filter(|t| t.last_state_update() > time)
} }
@ -916,7 +927,10 @@ impl TasksRelay {
} }
/// Sign and queue the event to the relay, returning its id /// Sign and queue the event to the relay, returning its id
fn submit(&mut self, builder: EventBuilder) -> EventId { fn submit(&mut self, mut builder: EventBuilder) -> EventId {
if let Some(stamp) = self.custom_time {
builder = builder.custom_created_at(stamp);
}
let event = self.sender.submit(builder).unwrap(); let event = self.sender.submit(builder).unwrap();
let id = event.id; let id = event.id;
self.add(event); self.add(event);