Compare commits

...

6 commits

6 changed files with 55 additions and 26 deletions

2
Cargo.lock generated
View file

@ -1462,7 +1462,7 @@ dependencies = [
[[package]] [[package]]
name = "mostr" name = "mostr"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"chrono", "chrono",
"colog", "colog",

View file

@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
readme = "README.md" readme = "README.md"
license = "GPL 3.0" license = "GPL 3.0"
authors = ["melonion"] authors = ["melonion"]
version = "0.9.2" version = "0.9.3"
rust-version = "1.82" rust-version = "1.82"
edition = "2021" edition = "2021"
default-run = "mostr" default-run = "mostr"

View file

@ -0,0 +1,2 @@
// Mostr Library Crate
// Placeholder for tests to not include main.rs bulk

View file

@ -460,13 +460,11 @@ async fn main() -> Result<()> {
} }
Some('>') => { Some('>') => {
tasks.update_state(arg_default, State::Done); tasks.update_state_and_up(arg_default, State::Done);
if tasks.custom_time.is_none() { tasks.move_up(); }
} }
Some('<') => { Some('<') => {
tasks.update_state(arg_default, State::Closed); tasks.update_state_and_up(arg_default, State::Closed);
if tasks.custom_time.is_none() { tasks.move_up(); }
} }
Some('&') => Some('&') =>
@ -787,6 +785,7 @@ async fn main() -> Result<()> {
} }
} }
tasks.custom_time = None; tasks.custom_time = None;
tasks.update_position();
println!("{}", tasks); println!("{}", tasks);
} }
Err(ReadlineError::Eof) => break 'repl, Err(ReadlineError::Eof) => break 'repl,

View file

@ -75,15 +75,14 @@ pub(crate) struct TasksRelay {
/// The task properties sorted by /// The task properties sorted by
sorting: VecDeque<String>, // TODO prefix +/- for asc/desc, no prefix for default sorting: VecDeque<String>, // TODO prefix +/- for asc/desc, no prefix for default
/// A filtered view of the current tasks. /// Temporary filtered view of the current tasks.
/// Would like this to be Task references /// Would like this to be Task references but that doesn't work unless I start meddling with Rc everywhere.
/// but that doesn't work unless I start meddling with Rc everywhere.
view: Vec<EventId>, view: Vec<EventId>,
search_depth: usize, search_depth: usize,
view_depth: usize, view_depth: usize,
pub(crate) recurse_activities: bool, pub(crate) recurse_activities: bool,
// Last position used in interface - needs auto-update // Last visible position that intuitively interactions should be based on
//position: Option<EventId>, position: Option<EventId>,
/// Currently active tags /// Currently active tags
tags: BTreeSet<Hashtag>, tags: BTreeSet<Hashtag>,
@ -195,6 +194,7 @@ impl TasksRelay {
priority: None, priority: None,
keys: vec![sender.pubkey()], keys: vec![sender.pubkey()],
own_keys: vec![sender.pubkey()], own_keys: vec![sender.pubkey()],
position: None,
search_depth: 4, search_depth: 4,
view_depth: 0, view_depth: 0,
@ -241,15 +241,27 @@ impl TasksRelay {
fn own_key(&self) -> PublicKey { self.sender.pubkey() } fn own_key(&self) -> PublicKey { self.sender.pubkey() }
pub(crate) fn get_position(&self) -> Option<EventId> { pub(crate) fn get_position(&self) -> Option<EventId> {
self.get_position_at(now()).1 self.position
}
pub(crate) fn calculate_position(&self, time: Option<Timestamp>) -> Option<EventId> {
self.get_position_at(time.unwrap_or(now())).1
} }
pub(crate) fn get_position_timestamped(&self) -> (Timestamp, Option<EventId>) { // Update the current position
self.get_position_at(now()) // Returns whether the position changed
pub(super) fn update_position(&mut self) -> bool {
let new_position = self.calculate_position(None);
if new_position != self.position {
self.view.clear();
self.position = new_position;
return true
}
false
} }
pub(super) fn parse_tracking_stamp_relative(&self, input: &str) -> Option<Timestamp> { pub(super) fn parse_tracking_stamp_relative(&self, input: &str) -> Option<Timestamp> {
let pos = self.get_position_timestamped(); let pos = self.get_position_at(now());
let mut pos_time = pos.1.and_then(|_| Local.timestamp_opt(pos.0.as_u64() as i64, 0).earliest()); let mut pos_time = pos.1.and_then(|_| Local.timestamp_opt(pos.0.as_u64() as i64, 0).earliest());
parse_tracking_stamp(input, pos_time.take_if(|t| Local::now() - *t > TimeDelta::hours(6))) parse_tracking_stamp(input, pos_time.take_if(|t| Local::now() - *t > TimeDelta::hours(6)))
} }
@ -263,6 +275,7 @@ impl TasksRelay {
// TODO binary search // TODO binary search
/// Gets last position change before the given timestamp /// Gets last position change before the given timestamp
/// Returns the position together with when it was set
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()
@ -885,8 +898,10 @@ impl TasksRelay {
self.state = state; self.state = state;
} }
pub(crate) fn move_up(&mut self) { pub(crate) fn move_up(&mut self) -> Option<EventId> {
self.move_to(self.get_current_task().and_then(|t| t.parent_id()).cloned()); let parent = self.get_current_task().and_then(|t| t.parent_id()).cloned();
self.move_to(parent);
parent
} }
pub(crate) fn flush(&self) { pub(crate) fn flush(&self) {
@ -983,6 +998,7 @@ impl TasksRelay {
fn history_from(&self, stamp: Timestamp) -> impl Iterator<Item=&Event> { fn history_from(&self, stamp: Timestamp) -> impl Iterator<Item=&Event> {
self.history.get(&self.sender.pubkey()) self.history.get(&self.sender.pubkey())
.map(|hist| { .map(|hist| {
// TODO deduplicate - get earlier time for duplicated tracking?
hist.values().rev().take_while_inclusive(move |e| e.created_at > stamp) hist.values().rev().take_while_inclusive(move |e| e.created_at > stamp)
}).into_iter().flatten() }).into_iter().flatten()
} }
@ -993,7 +1009,6 @@ impl TasksRelay {
return; return;
} }
self.view.clear();
let pos = self.get_position(); let pos = self.get_position();
if target == pos { if target == pos {
debug!("Flushing Tasks because of move in place"); debug!("Flushing Tasks because of move in place");
@ -1005,6 +1020,7 @@ impl TasksRelay {
.and_then(|id| self.get_by_id(&id)) .and_then(|id| self.get_by_id(&id))
.is_some_and(|t| t.parent_id() == pos.as_ref()) .is_some_and(|t| t.parent_id() == pos.as_ref())
{ {
// FIXME this triggers when moving up and into created task, making creation like '..task' not undoable
debug!("Flushing Tasks because of move beyond child"); debug!("Flushing Tasks because of move beyond child");
self.sender.flush(); self.sender.flush();
} }
@ -1015,6 +1031,7 @@ impl TasksRelay {
.skip_while(|e| e.created_at.as_u64() > now.as_u64() + MAX_OFFSET) .skip_while(|e| e.created_at.as_u64() > now.as_u64() + MAX_OFFSET)
.count() as u64; .count() as u64;
if offset >= MAX_OFFSET { if offset >= MAX_OFFSET {
// This is a very odd edge case when a user moves more than MAX_OFFSET times in MAX_OFFSET seconds so we reject
warn!("Whoa you are moving around quickly! Give me a few seconds to process.") warn!("Whoa you are moving around quickly! Give me a few seconds to process.")
} }
self.submit( self.submit(
@ -1022,7 +1039,7 @@ impl TasksRelay {
.custom_created_at(now + offset), .custom_created_at(now + offset),
); );
} }
// Updates // Updates
pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag { pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
@ -1082,10 +1099,10 @@ impl TasksRelay {
/// Returns true if successful, false if there is no current task /// Returns true if successful, false if there is no current task
pub(crate) fn make_dependent_sibling(&mut self, input: &str) -> bool { pub(crate) fn make_dependent_sibling(&mut self, input: &str) -> bool {
if let Some(pos) = self.get_position() { if let Some(pos) = self.get_position() {
self.move_up(); let parent = self.move_up();
self.make_task_with( self.make_task_with(
input, input,
self.get_position() parent
.map(|par| self.make_event_tag_from_id(par, MARKER_PARENT)) .map(|par| self.make_event_tag_from_id(par, MARKER_PARENT))
.into_iter() .into_iter()
.chain(once(self.make_event_tag_from_id(pos, MARKER_DEPENDS))), .chain(once(self.make_event_tag_from_id(pos, MARKER_DEPENDS))),
@ -1399,9 +1416,14 @@ impl TasksRelay {
self.submit(prop) self.submit(prop)
} }
pub(crate) fn update_state(&mut self, comment: &str, state: State) -> Option<EventId> { /// Update state of current task (if one is selected) and move out of it
let id = self.get_position()?; pub(crate) fn update_state_and_up(&mut self, comment: &str, state: State) {
Some(self.set_state_for(id, comment, state)) if let Some(id) = self.get_position() {
self.set_state_for(id, comment, state);
if self.calculate_position(self.custom_time) == Some(id) {
self.move_up();
}
}
} }
/// Creates a note or activity, depending on whether the parent is a task. /// Creates a note or activity, depending on whether the parent is a task.

View file

@ -68,17 +68,19 @@ fn test_recursive_closing() {
tasks.custom_time = Some(Timestamp::zero()); tasks.custom_time = Some(Timestamp::zero());
let parent = tasks.make_task_unwrapped("parent #tag1"); let parent = tasks.make_task_unwrapped("parent #tag1");
tasks.move_to(Some(parent)); tasks.move_to(Some(parent));
tasks.update_position();
let sub = tasks.make_task_unwrapped("sub #oi # tag2"); let sub = tasks.make_task_unwrapped("sub #oi # tag2");
assert_eq!( assert_eq!(
tasks.all_hashtags(), tasks.all_hashtags(),
["oi", "tag1", "tag2"].into_iter().map(Hashtag::from).collect() ["oi", "tag1", "tag2"].into_iter().map(Hashtag::from).collect()
); );
tasks.update_position();
tasks.make_note("note with #tag3 # yeah"); tasks.make_note("note with #tag3 # yeah");
let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect(); let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect();
assert_eq!(tasks.all_hashtags(), all_tags); assert_eq!(tasks.all_hashtags(), all_tags);
tasks.custom_time = Some(Timestamp::now()); tasks.custom_time = Some(Timestamp::now());
tasks.update_state("Finished #YeaH # oi", State::Done); tasks.update_state_and_up("Finished #YeaH # oi", State::Done);
assert_eq!( assert_eq!(
tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(),
["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from) ["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from)
@ -86,7 +88,7 @@ fn test_recursive_closing() {
assert_eq!(tasks.all_hashtags(), all_tags); assert_eq!(tasks.all_hashtags(), all_tags);
tasks.custom_time = Some(now()); tasks.custom_time = Some(now());
tasks.update_state("Closing Down", State::Closed); tasks.update_state_and_up("Closing Down", State::Closed);
assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed); assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed);
assert_eq!(tasks.get_by_id(&parent).unwrap().pure_state(), State::Closed); assert_eq!(tasks.get_by_id(&parent).unwrap().pure_state(), State::Closed);
assert_eq!(tasks.nonclosed_tasks().next(), None); assert_eq!(tasks.nonclosed_tasks().next(), None);
@ -193,11 +195,13 @@ fn test_sibling_dependency() {
); );
assert_tasks_view!(tasks, [parent]); assert_tasks_view!(tasks, [parent]);
tasks.track_at(Timestamp::now(), Some(sub)); tasks.track_at(Timestamp::now(), Some(sub));
tasks.update_position();
assert_eq!(tasks.get_own_events_history().count(), 1); assert_eq!(tasks.get_own_events_history().count(), 1);
assert_tasks_view!(tasks, []); assert_tasks_view!(tasks, []);
tasks.make_dependent_sibling("sibling"); tasks.make_dependent_sibling("sibling");
assert_eq!(tasks.len(), 3); assert_eq!(tasks.len(), 3);
tasks.update_position();
assert_eq!(tasks.viewed_tasks().len(), 2); assert_eq!(tasks.viewed_tasks().len(), 2);
} }
@ -495,3 +499,5 @@ fn test_itertools() {
assert_eq!("test toast".split(' ').collect_vec().len(), 3); assert_eq!("test toast".split(' ').collect_vec().len(), 3);
assert_eq!("test toast".split_ascii_whitespace().collect_vec().len(), 2); assert_eq!("test toast".split_ascii_whitespace().collect_vec().len(), 2);
} }
// TODO subtask of done task visible in quick access but not accessible