forked from janek/mostr
feat: add ability to schedule any action
This commit is contained in:
parent
34657540de
commit
945eb6906a
|
@ -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
|
||||||
|
|
|
@ -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>() {
|
||||||
|
|
100
src/main.rs
100
src/main.rs
|
@ -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,
|
||||||
|
|
16
src/tasks.rs
16
src/tasks.rs
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue