Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
|
8655a21396 | ||
|
d3039cee7f | ||
|
a3befc87da | ||
|
01fde4c112 | ||
|
c836b0e57b | ||
|
32fbf6db89 | ||
|
3652a290b2 |
6 changed files with 499 additions and 388 deletions
775
Cargo.lock
generated
775
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -5,7 +5,7 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
|
|||
readme = "README.md"
|
||||
license = "GPL 3.0"
|
||||
authors = ["melonion"]
|
||||
version = "0.9.3"
|
||||
version = "0.10.0"
|
||||
rust-version = "1.82"
|
||||
edition = "2021"
|
||||
default-run = "mostr"
|
||||
|
@ -21,15 +21,15 @@ regex = "1.11"
|
|||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
colog = "1.3"
|
||||
colored = "2.2"
|
||||
colored = "3.0"
|
||||
rustyline = { git = "https://github.com/xeruf/rustyline", rev = "5364854" }
|
||||
# OS-Specific Abstractions
|
||||
keyring = "3"
|
||||
directories = "5.0"
|
||||
whoami = "1.5"
|
||||
directories = "6.0"
|
||||
whoami = "1.6"
|
||||
# slint = "1.8"
|
||||
# Application Utils
|
||||
itertools = "0.12"
|
||||
itertools = "0.14"
|
||||
chrono = "0.4"
|
||||
parse_datetime = "0.5"
|
||||
interim = { version = "0.1", features = ["chrono"] }
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "1.84.0"
|
||||
channel = "1.87"
|
||||
|
|
|
@ -460,13 +460,11 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Some('>') => {
|
||||
tasks.update_state(arg_default, State::Done);
|
||||
if tasks.custom_time.is_none() { tasks.move_up(); }
|
||||
tasks.update_state_and_up(arg_default, State::Done);
|
||||
}
|
||||
|
||||
Some('<') => {
|
||||
tasks.update_state(arg_default, State::Closed);
|
||||
if tasks.custom_time.is_none() { tasks.move_up(); }
|
||||
tasks.update_state_and_up(arg_default, State::Closed);
|
||||
}
|
||||
|
||||
Some('&') =>
|
||||
|
@ -787,6 +785,7 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
tasks.custom_time = None;
|
||||
tasks.update_position();
|
||||
println!("{}", tasks);
|
||||
}
|
||||
Err(ReadlineError::Eof) => break 'repl,
|
||||
|
|
83
src/tasks.rs
83
src/tasks.rs
|
@ -75,15 +75,14 @@ pub(crate) struct TasksRelay {
|
|||
/// The task properties sorted by
|
||||
sorting: VecDeque<String>, // TODO prefix +/- for asc/desc, no prefix for default
|
||||
|
||||
/// A filtered view of the current tasks.
|
||||
/// Would like this to be Task references
|
||||
/// but that doesn't work unless I start meddling with Rc everywhere.
|
||||
/// Temporary filtered view of the current tasks.
|
||||
/// Would like this to be Task references but that doesn't work unless I start meddling with Rc everywhere.
|
||||
view: Vec<EventId>,
|
||||
search_depth: usize,
|
||||
view_depth: usize,
|
||||
pub(crate) recurse_activities: bool,
|
||||
// Last position used in interface - needs auto-update
|
||||
//position: Option<EventId>,
|
||||
// Last visible position that intuitively interactions should be based on
|
||||
position: Option<EventId>,
|
||||
|
||||
/// Currently active tags
|
||||
tags: BTreeSet<Hashtag>,
|
||||
|
@ -195,6 +194,7 @@ impl TasksRelay {
|
|||
priority: None,
|
||||
keys: vec![sender.pubkey()],
|
||||
own_keys: vec![sender.pubkey()],
|
||||
position: None,
|
||||
|
||||
search_depth: 4,
|
||||
view_depth: 0,
|
||||
|
@ -241,7 +241,23 @@ impl TasksRelay {
|
|||
fn own_key(&self) -> PublicKey { self.sender.pubkey() }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Update the current position
|
||||
// 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> {
|
||||
|
@ -720,8 +736,8 @@ impl TasksRelay {
|
|||
"path" => self.get_task_path(Some(task.get_id())),
|
||||
"rpath" => self.get_relative_path(task.get_id()),
|
||||
// TODO format strings configurable
|
||||
"time" => display_time("MMMm", self.time_tracked(task.get_id())),
|
||||
"rtime" => display_time("HH:MM", self.total_time_tracked(task.get_id())),
|
||||
"time" => format_secs("TTTT", self.time_tracked(task.get_id())),
|
||||
"rtime" => format_secs("TT:TT", self.total_time_tracked(task.get_id())),
|
||||
prop => task.get(prop).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
@ -882,8 +898,10 @@ impl TasksRelay {
|
|||
self.state = state;
|
||||
}
|
||||
|
||||
pub(crate) fn move_up(&mut self) {
|
||||
self.move_to(self.get_current_task().and_then(|t| t.parent_id()).cloned());
|
||||
pub(crate) fn move_up(&mut self) -> Option<EventId> {
|
||||
let parent = self.get_current_task().and_then(|t| t.parent_id()).cloned();
|
||||
self.move_to(parent);
|
||||
parent
|
||||
}
|
||||
|
||||
pub(crate) fn flush(&self) {
|
||||
|
@ -991,7 +1009,6 @@ impl TasksRelay {
|
|||
return;
|
||||
}
|
||||
|
||||
self.view.clear();
|
||||
let pos = self.get_position();
|
||||
if target == pos {
|
||||
debug!("Flushing Tasks because of move in place");
|
||||
|
@ -1003,7 +1020,7 @@ impl TasksRelay {
|
|||
.and_then(|id| self.get_by_id(&id))
|
||||
.is_some_and(|t| t.parent_id() == pos.as_ref())
|
||||
{
|
||||
// FIXME this triggers when moving up and into created task
|
||||
// FIXME this triggers when moving up and into created task, making creation like '..task' not undoable
|
||||
debug!("Flushing Tasks because of move beyond child");
|
||||
self.sender.flush();
|
||||
}
|
||||
|
@ -1014,6 +1031,7 @@ impl TasksRelay {
|
|||
.skip_while(|e| e.created_at.as_u64() > now.as_u64() + MAX_OFFSET)
|
||||
.count() as u64;
|
||||
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.")
|
||||
}
|
||||
self.submit(
|
||||
|
@ -1081,10 +1099,10 @@ impl TasksRelay {
|
|||
/// Returns true if successful, false if there is no current task
|
||||
pub(crate) fn make_dependent_sibling(&mut self, input: &str) -> bool {
|
||||
if let Some(pos) = self.get_position() {
|
||||
self.move_up();
|
||||
let parent = self.move_up();
|
||||
self.make_task_with(
|
||||
input,
|
||||
self.get_position()
|
||||
parent
|
||||
.map(|par| self.make_event_tag_from_id(par, MARKER_PARENT))
|
||||
.into_iter()
|
||||
.chain(once(self.make_event_tag_from_id(pos, MARKER_DEPENDS))),
|
||||
|
@ -1398,9 +1416,14 @@ impl TasksRelay {
|
|||
self.submit(prop)
|
||||
}
|
||||
|
||||
pub(crate) fn update_state(&mut self, comment: &str, state: State) -> Option<EventId> {
|
||||
let id = self.get_position()?;
|
||||
Some(self.set_state_for(id, comment, state))
|
||||
/// Update state of current task (if one is selected) and move out of it
|
||||
pub(crate) fn update_state_and_up(&mut self, comment: &str, state: 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.
|
||||
|
@ -1474,8 +1497,9 @@ impl Display for TasksRelay {
|
|||
}
|
||||
writeln!(
|
||||
lock,
|
||||
"Active from {} (total tracked time {}m) - {} since {}",
|
||||
"Active from {} ({}m, total tracked time {}m) - {} since {}",
|
||||
tracking_stamp.map_or("?".to_string(), |t| format_timestamp_relative(&t)),
|
||||
tracking_stamp.map_or("?".to_string(), |t| (Timestamp::now() - t).as_u64().div(60).to_string()),
|
||||
self.time_tracked(t.get_id()) / 60,
|
||||
state,
|
||||
format_timestamp_relative(&state.get_timestamp())
|
||||
|
@ -1527,7 +1551,7 @@ impl Display for TasksRelay {
|
|||
|
||||
writeln!(lock,
|
||||
"{count} visible tasks{}",
|
||||
display_time(" tracked a total of HHhMMm", total_time)
|
||||
format_secs(" tracked a total of T", total_time)
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1569,20 +1593,23 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Formats the given seconds according to the given format.
|
||||
/// - MMM - minutes
|
||||
/// - MM - minutes of the hour
|
||||
/// - HH - hours
|
||||
///
|
||||
/// Formats seconds using a formatting scheme.
|
||||
/// Returns an empty string if under one minute.
|
||||
fn display_time(format: &str, secs: u64) -> String {
|
||||
fn format_secs(format: &str, secs: u64) -> String {
|
||||
Some(secs / 60)
|
||||
.filter(|t| t > &0)
|
||||
.map_or(String::new(), |mins| {
|
||||
let hours = mins / 60;
|
||||
format
|
||||
.replace("MMM", &format!("{:3}", mins))
|
||||
.replace("HH", &format!("{:02}", mins.div(60)))
|
||||
.replace("MM", &format!("{:02}", mins.rem(60)))
|
||||
.replace("TTTT",
|
||||
&(if mins > 999 { format!("h{:03}", hours) }
|
||||
else { format!("{:>3}m", mins) }))
|
||||
.replace("TT:TT",
|
||||
&(if hours > 99 { format!("h{:>4}", hours) }
|
||||
else { format!("{:>2}:{:02}", hours, mins.rem(60)) }))
|
||||
.replace("T",
|
||||
&(if hours > 99 { format!("{}h", hours) }
|
||||
else { format!("{}h{:02}m", hours, mins) }))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -68,17 +68,19 @@ fn test_recursive_closing() {
|
|||
tasks.custom_time = Some(Timestamp::zero());
|
||||
let parent = tasks.make_task_unwrapped("parent #tag1");
|
||||
tasks.move_to(Some(parent));
|
||||
tasks.update_position();
|
||||
let sub = tasks.make_task_unwrapped("sub #oi # tag2");
|
||||
assert_eq!(
|
||||
tasks.all_hashtags(),
|
||||
["oi", "tag1", "tag2"].into_iter().map(Hashtag::from).collect()
|
||||
);
|
||||
tasks.update_position();
|
||||
tasks.make_note("note with #tag3 # yeah");
|
||||
let all_tags = ["oi", "tag1", "tag2", "tag3", "yeah"].into_iter().map(Hashtag::from).collect();
|
||||
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||
|
||||
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!(
|
||||
tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(),
|
||||
["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from)
|
||||
|
@ -86,7 +88,7 @@ fn test_recursive_closing() {
|
|||
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||
|
||||
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(&parent).unwrap().pure_state(), State::Closed);
|
||||
assert_eq!(tasks.nonclosed_tasks().next(), None);
|
||||
|
@ -193,11 +195,13 @@ fn test_sibling_dependency() {
|
|||
);
|
||||
assert_tasks_view!(tasks, [parent]);
|
||||
tasks.track_at(Timestamp::now(), Some(sub));
|
||||
tasks.update_position();
|
||||
assert_eq!(tasks.get_own_events_history().count(), 1);
|
||||
assert_tasks_view!(tasks, []);
|
||||
|
||||
tasks.make_dependent_sibling("sibling");
|
||||
assert_eq!(tasks.len(), 3);
|
||||
tasks.update_position();
|
||||
assert_eq!(tasks.viewed_tasks().len(), 2);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue