Compare commits
9 Commits
0ee4daea1b
...
bf802e3195
Author | SHA1 | Date |
---|---|---|
xeruf | bf802e3195 | |
xeruf | 03f9e60c6f | |
xeruf | 5b05c53947 | |
xeruf | 7f34a888f3 | |
xeruf | 256c86e06f | |
xeruf | a9509fd4f2 | |
xeruf | 36fe58d3f3 | |
xeruf | 14dcc8f0ff | |
xeruf | 486cbb1ab4 |
21
README.md
21
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,22 +91,25 @@ 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.
|
||||
|
||||
- `:[IND][COL]` - add property column COL at IND or end, if it already exists remove property column COL or IND
|
||||
- `>[TEXT]` - Complete active task and move to parent, with optional state description
|
||||
- `<[TEXT]` - Close active task and move to parent, with optional state description
|
||||
- `|TEXT` - Set state for current task from text (also aliased to `/` for now)
|
||||
- `-TEXT` - add text note (comment / description)
|
||||
- `!TEXT` - Set state for current task from text
|
||||
- `,TEXT` - add text note (comment / description)
|
||||
- `@` - undoes last action (moving in place or upwards or waiting a minute confirms pending actions)
|
||||
|
||||
Property Filters:
|
||||
|
||||
- `#TAG` - filter by tag
|
||||
- `+TAG` - filter by tag
|
||||
- `-TAG` - remove filter by tag
|
||||
- `?STATE` - filter by state (type or description) - plain `?` to reset
|
||||
|
||||
State descriptions can be used for example for Kanban columns or review flows.
|
||||
|
@ -147,10 +150,8 @@ Considering to use Calendar: https://github.com/nostr-protocol/nips/blob/master/
|
|||
|
||||
## Plans
|
||||
|
||||
- Task markdown support?
|
||||
- Time tracking: Active not as task state, ability to postpone task and add planned timestamps (calendar entry)
|
||||
+ Personal time tracking
|
||||
+ Postponing Tasks
|
||||
- Task markdown support? - colored
|
||||
- Time tracking: Ability to postpone task and add planned timestamps (calendar entry)
|
||||
- Parse Hashtag tags from task name
|
||||
- Unified Filter object
|
||||
-> include subtasks of matched tasks
|
||||
|
|
158
src/main.rs
158
src/main.rs
|
@ -1,8 +1,10 @@
|
|||
use std::cell::RefCell;
|
||||
use std::env::{args, var};
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, stdin, stdout, Write};
|
||||
use std::ops::Sub;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc;
|
||||
|
@ -22,21 +24,52 @@ mod tasks;
|
|||
const TASK_KIND: u64 = 1621;
|
||||
const TRACKING_KIND: u64 = 1650;
|
||||
|
||||
type Events = Vec<Event>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EventSender {
|
||||
tx: Sender<Event>,
|
||||
tx: Sender<Events>,
|
||||
keys: Keys,
|
||||
queue: RefCell<Events>,
|
||||
}
|
||||
impl EventSender {
|
||||
fn submit(&self, event_builder: EventBuilder) -> Option<Event> {
|
||||
or_print(event_builder.to_event(&self.keys)).inspect(|event| {
|
||||
or_print(self.tx.send(event.clone()));
|
||||
})
|
||||
fn submit(&self, event_builder: EventBuilder) -> Result<Event> {
|
||||
if let Some(event) = self.queue.borrow().first() {
|
||||
// Flush if oldest event older than a minute
|
||||
if event.created_at < Timestamp::now().sub(60u64) {
|
||||
debug!("Flushing Event Queue because it is older than a minute");
|
||||
self.flush();
|
||||
}
|
||||
}
|
||||
let mut queue = self.queue.borrow_mut();
|
||||
Ok(event_builder.to_event(&self.keys).inspect(|event| {
|
||||
if event.kind.as_u64() == TRACKING_KIND {
|
||||
queue.retain(|e| {
|
||||
e.kind.as_u64() != TRACKING_KIND
|
||||
});
|
||||
}
|
||||
queue.push(event.clone());
|
||||
})?)
|
||||
}
|
||||
fn flush(&self) {
|
||||
debug!("Flushing {} events from queue", self.queue.borrow().len());
|
||||
if self.queue.borrow().len() > 0 {
|
||||
or_print(self.tx.send(self.clear()));
|
||||
}
|
||||
}
|
||||
fn clear(&self) -> Events {
|
||||
trace!("Cleared queue: {:?}", self.queue.borrow());
|
||||
self.queue.replace(Vec::with_capacity(3))
|
||||
}
|
||||
pub(crate) fn pubkey(&self) -> PublicKey {
|
||||
self.keys.public_key()
|
||||
}
|
||||
}
|
||||
impl Drop for EventSender {
|
||||
fn drop(&mut self) {
|
||||
self.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn or_print<T, U: Display>(result: Result<T, U>) -> Option<T> {
|
||||
match result {
|
||||
|
@ -137,10 +170,11 @@ async fn main() {
|
|||
|
||||
client.connect().await;
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Event>();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut tasks: Tasks = Tasks::from(EventSender {
|
||||
keys: keys.clone(),
|
||||
keys,
|
||||
tx,
|
||||
queue: Default::default(),
|
||||
});
|
||||
|
||||
let sub_id: SubscriptionId = client.subscribe(vec![Filter::new()], None).await;
|
||||
|
@ -168,9 +202,9 @@ async fn main() {
|
|||
|
||||
let sender = tokio::spawn(async move {
|
||||
while let Ok(e) = rx.recv() {
|
||||
trace!("Sending {}", e.id);
|
||||
// TODO send in batches
|
||||
let _ = client.send_event(e).await;
|
||||
trace!("Sending {:?}", e);
|
||||
// TODO batch up further
|
||||
let _ = client.batch_event(e, RelaySendOptions::new()).await;
|
||||
}
|
||||
info!("Stopping listeners...");
|
||||
client.unsubscribe_all().await;
|
||||
|
@ -220,7 +254,9 @@ async fn main() {
|
|||
""
|
||||
};
|
||||
match op {
|
||||
None => {}
|
||||
None => {
|
||||
tasks.flush()
|
||||
}
|
||||
|
||||
Some(':') => match iter.next().and_then(|s| s.to_digit(10)) {
|
||||
Some(digit) => {
|
||||
|
@ -268,42 +304,53 @@ async fn main() {
|
|||
}
|
||||
},
|
||||
|
||||
Some('?') => {
|
||||
tasks.set_state_filter(Some(arg.to_string()).filter(|s| !s.is_empty()));
|
||||
}
|
||||
|
||||
Some('-') => tasks.add_note(arg),
|
||||
Some(',') => tasks.add_note(arg),
|
||||
|
||||
Some('>') => {
|
||||
tasks.update_state(arg, |_| Some(State::Done));
|
||||
tasks.update_state(arg, State::Done);
|
||||
tasks.move_up();
|
||||
}
|
||||
|
||||
Some('<') => {
|
||||
tasks.update_state(arg, |_| Some(State::Closed));
|
||||
tasks.update_state(arg, State::Closed);
|
||||
tasks.move_up();
|
||||
}
|
||||
|
||||
Some('|') | Some('/') => match tasks.get_position() {
|
||||
Some('@') => {
|
||||
tasks.undo();
|
||||
}
|
||||
|
||||
Some('?') => {
|
||||
tasks.set_state_filter(Some(arg.to_string()).filter(|s| !s.is_empty()));
|
||||
}
|
||||
|
||||
Some('!') => match tasks.get_position() {
|
||||
None => {
|
||||
warn!("First select a task to set its state!");
|
||||
}
|
||||
Some(id) => {
|
||||
tasks.set_state_for(&id, arg);
|
||||
tasks.move_to(tasks.get_position());
|
||||
tasks.set_state_for(id, arg, match arg {
|
||||
"Closed" => State::Closed,
|
||||
"Done" => State::Done,
|
||||
_ => State::Open,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Some('#') => {
|
||||
Some('#') | Some('+') => {
|
||||
tasks.add_tag(arg.to_string());
|
||||
}
|
||||
|
||||
Some('-') => {
|
||||
tasks.remove_tag(arg.to_string())
|
||||
}
|
||||
|
||||
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);
|
||||
pos = tasks.get_parent(pos).cloned();
|
||||
}
|
||||
let slice = &input[dots..];
|
||||
if slice.is_empty() {
|
||||
|
@ -313,52 +360,45 @@ async fn main() {
|
|||
if let Ok(depth) = slice.parse::<i8>() {
|
||||
tasks.move_to(pos);
|
||||
tasks.depth = depth;
|
||||
} else {
|
||||
tasks.filter_or_create(slice).map(|id| tasks.move_to(Some(id)));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
pos = EventId::parse(slice).ok().or_else(|| {
|
||||
// TODO check what is more intuitive:
|
||||
// currently resets filters before filtering again, maybe keep them
|
||||
if let Ok(depth) = slice.parse::<i8>() {
|
||||
tasks.move_to(pos);
|
||||
let mut filtered: Vec<EventId> = tasks
|
||||
.current_tasks()
|
||||
tasks.depth = depth;
|
||||
} else {
|
||||
let filtered = tasks
|
||||
.children_of(pos)
|
||||
.into_iter()
|
||||
.filter(|t| t.event.content.starts_with(slice))
|
||||
.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.is_empty() {
|
||||
let lowercase = slice.to_ascii_lowercase();
|
||||
filtered = tasks
|
||||
.current_tasks()
|
||||
.into_iter()
|
||||
.filter(|t| {
|
||||
t.event.content.to_ascii_lowercase().starts_with(&lowercase)
|
||||
})
|
||||
.map(|t| t.event.id)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
if filtered.len() == 1 {
|
||||
tasks.move_to(filtered.into_iter().nth(0));
|
||||
} else {
|
||||
tasks.move_to(pos);
|
||||
tasks.set_filter(filtered);
|
||||
}
|
||||
match filtered.len() {
|
||||
0 => {
|
||||
// No match, new task
|
||||
tasks.make_task(slice)
|
||||
}
|
||||
1 => {
|
||||
// One match, activate
|
||||
Some(filtered.first().unwrap().clone())
|
||||
}
|
||||
_ => {
|
||||
// Multiple match, filter
|
||||
tasks.set_filter(filtered);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
if pos != None {
|
||||
tasks.move_to(pos);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
tasks.make_task(&input);
|
||||
tasks.filter_or_create(&input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
25
src/task.rs
25
src/task.rs
|
@ -39,8 +39,8 @@ impl Task {
|
|||
&self.event.id
|
||||
}
|
||||
|
||||
pub(crate) fn parent_id(&self) -> Option<EventId> {
|
||||
self.parents.first().cloned()
|
||||
pub(crate) fn parent_id(&self) -> Option<&EventId> {
|
||||
self.parents.first()
|
||||
}
|
||||
|
||||
pub(crate) fn get_title(&self) -> String {
|
||||
|
@ -68,7 +68,7 @@ impl Task {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn state(&self) -> Option<TaskState> {
|
||||
self.states().max_by_key(|t| t.time)
|
||||
}
|
||||
|
@ -77,21 +77,8 @@ impl Task {
|
|||
self.state().map_or(State::Open, |s| s.state)
|
||||
}
|
||||
|
||||
pub(crate) fn set_state(
|
||||
&mut self,
|
||||
sender: &EventSender,
|
||||
state: State,
|
||||
comment: &str,
|
||||
) -> Option<Event> {
|
||||
sender
|
||||
.submit(EventBuilder::new(
|
||||
state.into(),
|
||||
comment,
|
||||
vec![Tag::event(self.event.id)],
|
||||
))
|
||||
.inspect(|e| {
|
||||
self.props.insert(e.clone());
|
||||
})
|
||||
pub(crate) fn state_or_default(&self) -> TaskState {
|
||||
self.state().unwrap_or_else(|| self.default_state())
|
||||
}
|
||||
|
||||
fn default_state(&self) -> TaskState {
|
||||
|
@ -119,7 +106,7 @@ impl Task {
|
|||
match property {
|
||||
"id" => Some(self.event.id.to_string()),
|
||||
"parentid" => self.parent_id().map(|i| i.to_string()),
|
||||
"state" => self.state().map(|s| s.to_string()),
|
||||
"state" => Some(self.state_or_default().get_label()),
|
||||
"name" => Some(self.event.content.clone()),
|
||||
"desc" => self.descriptions().last().cloned(),
|
||||
"description" => Some(self.descriptions().join(" ")),
|
||||
|
|
302
src/tasks.rs
302
src/tasks.rs
|
@ -56,7 +56,7 @@ impl Tasks {
|
|||
position: None,
|
||||
view: Default::default(),
|
||||
tags: Default::default(),
|
||||
state: Some(State::Open.to_string()),
|
||||
state: None,
|
||||
depth: 1,
|
||||
sender,
|
||||
}
|
||||
|
@ -67,14 +67,13 @@ impl Tasks {
|
|||
// Accessors
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn get_by_id(&self, id: &EventId) -> Option<&Task> {
|
||||
self.tasks.get(id)
|
||||
}
|
||||
pub(crate) fn get_by_id(&self, id: &EventId) -> Option<&Task> { self.tasks.get(id) }
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn get_position(&self) -> Option<EventId> {
|
||||
self.position
|
||||
}
|
||||
pub(crate) fn get_position(&self) -> Option<EventId> { self.position }
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn len(&self) -> usize { self.tasks.len() }
|
||||
|
||||
/// Ids of all subtasks found for id, including itself
|
||||
fn get_subtasks(&self, id: EventId) -> Vec<EventId> {
|
||||
|
@ -151,12 +150,18 @@ impl Tasks {
|
|||
State::Closed => None,
|
||||
State::Done => Some(1.0),
|
||||
_ => {
|
||||
let count = t.children.len() as f32;
|
||||
let mut sum = 0f32;
|
||||
let mut count = 0;
|
||||
for prog in t.children.iter().filter_map(|e| self.total_progress(e)) {
|
||||
sum += prog;
|
||||
count += 1;
|
||||
}
|
||||
Some(
|
||||
t.children
|
||||
.iter()
|
||||
.filter_map(|e| self.total_progress(e).map(|p| p / count))
|
||||
.sum(),
|
||||
if count > 0 {
|
||||
sum / (count as f32)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -164,7 +169,7 @@ impl Tasks {
|
|||
|
||||
// Parents
|
||||
|
||||
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))
|
||||
.and_then(|t| t.parent_id())
|
||||
}
|
||||
|
@ -250,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<EventId>) -> impl IntoIterator<Item=&EventId> + '_ {
|
||||
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 {
|
||||
|
@ -260,15 +272,17 @@ impl Tasks {
|
|||
// Currently ignores filter when it matches nothing
|
||||
return res;
|
||||
}
|
||||
self.resolve_tasks(
|
||||
self.tasks
|
||||
.values()
|
||||
.filter(|t| t.parent_id() == self.position)
|
||||
.map(|t| t.get_id()),
|
||||
).into_iter()
|
||||
self.resolve_tasks(self.children_of(self.position)).into_iter()
|
||||
.filter(|t| {
|
||||
self.state.as_ref().map_or(true, |state| {
|
||||
t.state().is_some_and(|t| t.matches_label(state))
|
||||
// TODO apply filters in transit
|
||||
let state = t.pure_state();
|
||||
self.state.as_ref().map_or_else(|| {
|
||||
state == State::Open || (
|
||||
state == State::Done &&
|
||||
t.parent_id() != None
|
||||
)
|
||||
}, |filter| {
|
||||
t.state().is_some_and(|t| t.matches_label(filter))
|
||||
}) && (self.tags.is_empty()
|
||||
|| t.tags.as_ref().map_or(false, |tags| {
|
||||
let mut iter = tags.iter();
|
||||
|
@ -281,31 +295,30 @@ impl Tasks {
|
|||
pub(crate) fn print_tasks(&self) -> Result<(), Error> {
|
||||
let mut lock = stdout().lock();
|
||||
if let Some(t) = self.current_task() {
|
||||
if let Some(state) = t.state() {
|
||||
writeln!(
|
||||
lock,
|
||||
"{} since {} (total tracked time {}m)",
|
||||
state.get_label(),
|
||||
match Local.timestamp_opt(state.time.as_i64(), 0) {
|
||||
Single(time) => {
|
||||
let date = time.date_naive();
|
||||
let prefix = match Local::now()
|
||||
.date_naive()
|
||||
.signed_duration_since(date)
|
||||
.num_days()
|
||||
{
|
||||
0 => "".into(),
|
||||
1 => "yesterday ".into(),
|
||||
2..=6 => date.format("%a ").to_string(),
|
||||
_ => date.format("%y-%m-%d ").to_string(),
|
||||
};
|
||||
format!("{}{}", prefix, time.format("%H:%M"))
|
||||
}
|
||||
_ => state.time.to_human_datetime(),
|
||||
},
|
||||
self.time_tracked(t.get_id()) / 60
|
||||
)?;
|
||||
}
|
||||
let state = t.state_or_default();
|
||||
writeln!(
|
||||
lock,
|
||||
"{} since {} (total tracked time {}m)",
|
||||
state.get_label(),
|
||||
match Local.timestamp_opt(state.time.as_i64(), 0) {
|
||||
Single(time) => {
|
||||
let date = time.date_naive();
|
||||
let prefix = match Local::now()
|
||||
.date_naive()
|
||||
.signed_duration_since(date)
|
||||
.num_days()
|
||||
{
|
||||
0 => "".into(),
|
||||
1 => "yesterday ".into(),
|
||||
2..=6 => date.format("%a ").to_string(),
|
||||
_ => date.format("%y-%m-%d ").to_string(),
|
||||
};
|
||||
format!("{}{}", prefix, time.format("%H:%M"))
|
||||
}
|
||||
_ => state.time.to_human_datetime(),
|
||||
},
|
||||
self.time_tracked(t.get_id()) / 60
|
||||
)?;
|
||||
writeln!(lock, "{}", t.descriptions().join("\n"))?;
|
||||
}
|
||||
// TODO proper columns
|
||||
|
@ -334,6 +347,7 @@ impl Tasks {
|
|||
}
|
||||
"progress" => self
|
||||
.total_progress(task.get_id())
|
||||
.filter(|_| task.children.len() > 0)
|
||||
.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0)),
|
||||
"path" => self.get_task_path(Some(task.event.id)),
|
||||
"rpath" => self.relative_path(task.event.id),
|
||||
|
@ -360,31 +374,91 @@ impl Tasks {
|
|||
self.tags.insert(Hashtag(tag));
|
||||
}
|
||||
|
||||
pub(crate) fn remove_tag(&mut self, tag: String) {
|
||||
self.view.clear();
|
||||
self.tags.retain(|t| !t.content().is_some_and(|value| value.to_string().starts_with(&tag)));
|
||||
}
|
||||
|
||||
pub(crate) fn set_state_filter(&mut self, state: Option<String>) {
|
||||
self.view.clear();
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
pub(crate) fn move_up(&mut self) {
|
||||
self.move_to(self.current_task().and_then(|t| t.parent_id()))
|
||||
self.move_to(self.current_task().and_then(|t| t.parent_id()).cloned());
|
||||
}
|
||||
|
||||
pub(crate) fn flush(&self) {
|
||||
self.sender.flush();
|
||||
}
|
||||
|
||||
/// Returns ids of tasks matching the filter.
|
||||
pub(crate) fn get_filtered(&self, arg: &str) -> Vec<EventId> {
|
||||
if let Ok(id) = EventId::parse(arg) {
|
||||
return vec![id];
|
||||
}
|
||||
let tasks = self.current_tasks();
|
||||
let mut filtered: Vec<EventId> = Vec::with_capacity(tasks.len());
|
||||
let lowercase_arg = arg.to_ascii_lowercase();
|
||||
let mut filtered_more: Vec<EventId> = Vec::with_capacity(tasks.len());
|
||||
for task in tasks {
|
||||
let lowercase = task.event.content.to_ascii_lowercase();
|
||||
if lowercase == lowercase_arg {
|
||||
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 {
|
||||
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<EventId> {
|
||||
let filtered = self.get_filtered(arg);
|
||||
match filtered.len() {
|
||||
0 => {
|
||||
// No match, new task
|
||||
self.view.clear();
|
||||
Some(self.make_task(arg))
|
||||
}
|
||||
1 => {
|
||||
// One match, activate
|
||||
self.move_to(filtered.into_iter().nth(0));
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
// Multiple match, filter
|
||||
self.set_filter(filtered);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_to(&mut self, id: Option<EventId>) {
|
||||
self.view.clear();
|
||||
self.tags.clear(); // TODO unsure if this is needed, needs alternative way to clear
|
||||
if id == self.position {
|
||||
debug!("Flushing Tasks because of move in place");
|
||||
self.flush();
|
||||
return;
|
||||
}
|
||||
self.position = id;
|
||||
self.sender.submit(
|
||||
self.submit(
|
||||
EventBuilder::new(
|
||||
Kind::from(TRACKING_KIND),
|
||||
"",
|
||||
id.iter().map(|id| Tag::event(id.clone())),
|
||||
)
|
||||
).map(|e| {
|
||||
self.add(e);
|
||||
});
|
||||
);
|
||||
if !id.and_then(|id| self.tasks.get(&id)).is_some_and(|t| t.parent_id() == self.position.as_ref()) {
|
||||
debug!("Flushing Tasks because of move");
|
||||
self.flush();
|
||||
}
|
||||
self.position = id;
|
||||
}
|
||||
|
||||
// Updates
|
||||
|
@ -409,14 +483,28 @@ impl Tasks {
|
|||
}
|
||||
|
||||
/// Sanitizes input
|
||||
pub(crate) fn make_task(&mut self, input: &str) -> Option<EventId> {
|
||||
self.sender.submit(self.build_task(input.trim())).map(|e| {
|
||||
let id = e.id;
|
||||
self.add_task(e);
|
||||
let state = self.state.clone().unwrap_or("Open".to_string());
|
||||
self.set_state_for(&id, &state);
|
||||
id
|
||||
})
|
||||
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
||||
self.submit(self.build_task(input.trim()))
|
||||
}
|
||||
|
||||
pub(crate) fn build_prop(
|
||||
&mut self,
|
||||
kind: Kind,
|
||||
comment: &str,
|
||||
id: EventId,
|
||||
) -> EventBuilder {
|
||||
EventBuilder::new(
|
||||
kind,
|
||||
comment,
|
||||
vec![Tag::event(id)],
|
||||
)
|
||||
}
|
||||
|
||||
fn submit(&mut self, builder: EventBuilder) -> EventId {
|
||||
let event = self.sender.submit(builder).unwrap();
|
||||
let id = event.id;
|
||||
self.add(event);
|
||||
id
|
||||
}
|
||||
|
||||
pub(crate) fn add(&mut self, event: Event) {
|
||||
|
@ -448,49 +536,44 @@ impl Tasks {
|
|||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_state_for(&mut self, id: &EventId, comment: &str) -> Option<Event> {
|
||||
let t = self.tasks.get_mut(id);
|
||||
t.and_then(|task| {
|
||||
task.set_state(
|
||||
&self.sender,
|
||||
match comment {
|
||||
"Closed" => State::Closed,
|
||||
"Done" => State::Done,
|
||||
_ => State::Open,
|
||||
},
|
||||
comment,
|
||||
)
|
||||
})
|
||||
pub(crate) fn undo(&mut self) {
|
||||
self.sender.clear().into_iter().rev().for_each(|event| {
|
||||
self.remove(&event)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_state_for<F>(&mut self, id: &EventId, comment: &str, f: F) -> Option<Event>
|
||||
where
|
||||
F: FnOnce(&Task) -> Option<State>,
|
||||
{
|
||||
self.tasks
|
||||
.get_mut(id)
|
||||
.and_then(|task| f(task).and_then(|state| task.set_state(&self.sender, state, comment)))
|
||||
fn remove(&mut self, event: &Event) {
|
||||
if let Some(pos) = self.position {
|
||||
if pos == event.id {
|
||||
self.move_up()
|
||||
}
|
||||
}
|
||||
self.tasks.remove(&event.id);
|
||||
self.history.get_mut(&self.sender.pubkey()).map(|t| t.remove(event));
|
||||
self.referenced_tasks(event, |t| { t.props.remove(event); });
|
||||
}
|
||||
|
||||
pub(crate) fn update_state<F>(&mut self, comment: &str, f: F) -> Option<Event>
|
||||
where
|
||||
F: FnOnce(&Task) -> Option<State>,
|
||||
pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId {
|
||||
let prop = self.build_prop(
|
||||
state.into(),
|
||||
comment,
|
||||
id,
|
||||
);
|
||||
self.submit(prop)
|
||||
}
|
||||
|
||||
pub(crate) fn update_state(&mut self, comment: &str, state: State)
|
||||
{
|
||||
self.position
|
||||
.and_then(|id| self.update_state_for(&id, comment, f))
|
||||
.map(|id| self.set_state_for(id, comment, state));
|
||||
}
|
||||
|
||||
pub(crate) fn add_note(&mut self, note: &str) {
|
||||
match self.position {
|
||||
None => warn!("Cannot add note '{}' without active task", note),
|
||||
Some(id) => {
|
||||
self.sender
|
||||
.submit(EventBuilder::text_note(note, vec![]))
|
||||
.map(|e| {
|
||||
self.tasks.get_mut(&id).map(|t| {
|
||||
t.props.insert(e.clone());
|
||||
});
|
||||
});
|
||||
let prop = self.build_prop(Kind::TextNote, note, id);
|
||||
self.submit(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -541,7 +624,7 @@ impl<'a> Iterator for ParentIterator<'a> {
|
|||
self.current.and_then(|id| self.tasks.get(&id)).map(|t| {
|
||||
self.prev.map(|id| assert!(t.children.contains(&id)));
|
||||
self.prev = self.current;
|
||||
self.current = t.parent_id();
|
||||
self.current = t.parent_id().cloned();
|
||||
t
|
||||
})
|
||||
}
|
||||
|
@ -556,50 +639,51 @@ fn test_depth() {
|
|||
let mut tasks = Tasks::from(EventSender {
|
||||
tx,
|
||||
keys: Keys::generate(),
|
||||
queue: Default::default(),
|
||||
});
|
||||
|
||||
let t1 = tasks.make_task("t1");
|
||||
let task1 = tasks.get_by_id(&t1.unwrap()).unwrap();
|
||||
let task1 = tasks.get_by_id(&t1).unwrap();
|
||||
assert_eq!(tasks.depth, 1);
|
||||
assert_eq!(task1.state().unwrap().get_label(), "Open");
|
||||
assert_eq!(task1.pure_state(), State::Open);
|
||||
debug!("{:?}", tasks);
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
tasks.depth = 0;
|
||||
assert_eq!(tasks.current_tasks().len(), 0);
|
||||
|
||||
tasks.move_to(t1);
|
||||
tasks.move_to(Some(t1));
|
||||
tasks.depth = 2;
|
||||
assert_eq!(tasks.current_tasks().len(), 0);
|
||||
let t2 = tasks.make_task("t2");
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
assert_eq!(tasks.get_task_path(t2), "t1>t2");
|
||||
assert_eq!(tasks.relative_path(t2.unwrap()), "t2");
|
||||
assert_eq!(tasks.get_task_path(Some(t2)), "t1>t2");
|
||||
assert_eq!(tasks.relative_path(t2), "t2");
|
||||
let t3 = tasks.make_task("t3");
|
||||
assert_eq!(tasks.current_tasks().len(), 2);
|
||||
|
||||
tasks.move_to(t2);
|
||||
tasks.move_to(Some(t2));
|
||||
assert_eq!(tasks.current_tasks().len(), 0);
|
||||
let t4 = tasks.make_task("t4");
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
assert_eq!(tasks.get_task_path(t4), "t1>t2>t4");
|
||||
assert_eq!(tasks.relative_path(t4.unwrap()), "t4");
|
||||
assert_eq!(tasks.get_task_path(Some(t4)), "t1>t2>t4");
|
||||
assert_eq!(tasks.relative_path(t4), "t4");
|
||||
tasks.depth = 2;
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
tasks.depth = -1;
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
|
||||
tasks.move_to(t1);
|
||||
assert_eq!(tasks.relative_path(t4.unwrap()), "t2>t4");
|
||||
tasks.move_to(Some(t1));
|
||||
assert_eq!(tasks.relative_path(t4), "t2>t4");
|
||||
assert_eq!(tasks.current_tasks().len(), 2);
|
||||
tasks.depth = 2;
|
||||
assert_eq!(tasks.current_tasks().len(), 3);
|
||||
tasks.set_filter(vec![t2.unwrap()]);
|
||||
tasks.set_filter(vec![t2]);
|
||||
assert_eq!(tasks.current_tasks().len(), 2);
|
||||
tasks.depth = 1;
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
tasks.depth = -1;
|
||||
assert_eq!(tasks.current_tasks().len(), 1);
|
||||
tasks.set_filter(vec![t2.unwrap(), t3.unwrap()]);
|
||||
tasks.set_filter(vec![t2, t3]);
|
||||
assert_eq!(tasks.current_tasks().len(), 2);
|
||||
tasks.depth = 2;
|
||||
assert_eq!(tasks.current_tasks().len(), 3);
|
||||
|
@ -618,20 +702,20 @@ fn test_depth() {
|
|||
assert_eq!(tasks.current_tasks().len(), 2);
|
||||
|
||||
let empty = tasks.make_task("");
|
||||
let empty_task = tasks.get_by_id(&empty.unwrap()).unwrap();
|
||||
let empty_task = tasks.get_by_id(&empty).unwrap();
|
||||
let empty_id = empty_task.event.id.to_string();
|
||||
assert_eq!(empty_task.get_title(), empty_id);
|
||||
assert_eq!(tasks.get_task_path(empty), empty_id);
|
||||
assert_eq!(tasks.get_task_path(Some(empty)), empty_id);
|
||||
|
||||
let zero = EventId::all_zeros();
|
||||
assert_eq!(tasks.get_task_path(Some(zero)), zero.to_string());
|
||||
tasks.move_to(Some(zero));
|
||||
let dangling = tasks.make_task("test");
|
||||
assert_eq!(
|
||||
tasks.get_task_path(dangling),
|
||||
tasks.get_task_path(Some(dangling)),
|
||||
"0000000000000000000000000000000000000000000000000000000000000000>test"
|
||||
);
|
||||
assert_eq!(tasks.relative_path(dangling.unwrap()), "test");
|
||||
assert_eq!(tasks.relative_path(dangling), "test");
|
||||
|
||||
use itertools::Itertools;
|
||||
assert_eq!("test toast".split(' ').collect_vec().len(), 3);
|
||||
|
|
Loading…
Reference in New Issue