diff --git a/README.md b/README.md index 26b7952..05744f3 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ To exit the application, press `Ctrl-D`. ### Navigation and Nesting Create tasks and navigate using the shortcuts below. -Whichever task is selected / "active" +Whichever task is active (selected) will be the parent task for newly created tasks and automatically has time-tracking running. To track task progress, @@ -48,7 +48,7 @@ Generally a flat hierarchy is recommended with tags for filtering, since hierarchies cannot be changed. Filtering by a tag is just as easy -as selecting a task and more flexible. +as activating a task and more flexible. Using subtasks has two main advantages: - ability to accumulate time tracked @@ -91,10 +91,11 @@ when the application is terminated regularly. - `TASK` - create task - `.` - clear filters and reload - `.TASK` - + select task by id + + activate task by id + match by task name prefix: if one or more tasks match, filter / activate (tries case-sensitive then case-insensitive) + no match: create & activate task - `.2` - set view depth to `2`, which can be substituted for any number (how many subtask levels to show, default 1) +- `/[TEXT]` - like `.`, but never creates a task Dots can be repeated to move to parent tasks. diff --git a/src/main.rs b/src/main.rs index 9799ca7..a0eb38c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -365,6 +365,38 @@ async fn main() { } } + Some('/') => { + let mut dots = 1; + let mut pos = tasks.get_position(); + for _ in iter.take_while(|c| c == &'/') { + dots += 1; + pos = tasks.get_parent(pos).cloned(); + } + let slice = &input[dots..].to_ascii_lowercase(); + if slice.is_empty() { + tasks.move_to(pos); + continue; + } + if let Ok(depth) = slice.parse::() { + tasks.move_to(pos); + tasks.depth = depth; + } else { + let filtered = tasks + .children_of(pos) + .into_iter() + .filter_map(|child| tasks.get_by_id(&child)) + .filter(|t| t.event.content.to_ascii_lowercase().starts_with(slice)) + .map(|t| t.event.id) + .collect::>(); + if filtered.len() == 1 { + tasks.move_to(filtered.into_iter().nth(0)); + } else { + tasks.move_to(pos); + tasks.set_filter(filtered); + } + } + } + _ => { tasks.filter_or_create(&input); } diff --git a/src/tasks.rs b/src/tasks.rs index 11f575c..1db679f 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -255,6 +255,13 @@ impl Tasks { fn current_task(&self) -> Option<&Task> { self.position.and_then(|id| self.get_by_id(&id)) } + + pub(crate) fn children_of(&self, id: Option) -> impl IntoIterator + '_ { + self.tasks + .values() + .filter(move |t| t.parent_id() == id.as_ref()) + .map(|t| t.get_id()) + } pub(crate) fn current_tasks(&self) -> Vec<&Task> { if self.depth == 0 { @@ -265,13 +272,9 @@ impl Tasks { // Currently ignores filter when it matches nothing return res; } - self.resolve_tasks( - self.tasks - .values() - .filter(|t| t.parent_id() == self.position.as_ref()) - .map(|t| t.get_id()), - ).into_iter() + self.resolve_tasks(self.children_of(self.position)).into_iter() .filter(|t| { + // TODO apply filters in transit let state = t.pure_state(); self.state.as_ref().map_or_else(|| { state == State::Open || ( @@ -388,13 +391,11 @@ impl Tasks { pub(crate) fn flush(&self) { self.sender.flush(); } - - /// Finds out what to do with the given string. - /// Returns an EventId when a new Task was created. - pub(crate) fn filter_or_create(&mut self, arg: &str) -> Option { + + /// Returns ids of tasks matching the filter. + pub(crate) fn get_filtered(&self, arg: &str) -> Vec { if let Ok(id) = EventId::parse(arg) { - self.move_to(Some(id)); - return None; + return vec![id]; } let tasks = self.current_tasks(); let mut filtered: Vec = Vec::with_capacity(tasks.len()); @@ -403,17 +404,23 @@ impl Tasks { for task in tasks { let lowercase = task.event.content.to_ascii_lowercase(); if lowercase == lowercase_arg { - self.move_to(Some(task.event.id)); - return None + return vec![task.event.id] } else if task.event.content.starts_with(arg) { filtered.push(task.event.id) } else if lowercase.starts_with(&lowercase_arg) { filtered_more.push(task.event.id) } } - if filtered.len() == 0 { - filtered = filtered_more + if filtered.len() == 0 { + return filtered_more } + return filtered + } + + /// Finds out what to do with the given string. + /// Returns an EventId if a new Task was created. + pub(crate) fn filter_or_create(&mut self, arg: &str) -> Option { + let filtered = self.get_filtered(arg); match filtered.len() { 0 => { // No match, new task