Compare commits
4 Commits
dda969e08b
...
b3d70ab0b7
Author | SHA1 | Date |
---|---|---|
xeruf | b3d70ab0b7 | |
xeruf | c83d8a2f55 | |
xeruf | a7d02e60b2 | |
xeruf | 55792ca34f |
|
@ -105,7 +105,7 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
||||||
Dots and slashes can be repeated to move to parent tasks.
|
Dots and slashes can be repeated to move to parent tasks.
|
||||||
|
|
||||||
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (1-indexed)
|
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (1-indexed)
|
||||||
- `::[PROP]` - Sort by property PROP
|
- `::[PROP]` - Sort by property PROP (multiple space-separated values allowed)
|
||||||
- `([TIME]` - insert timetracking with the specified offset in minutes (empty: list tracked times)
|
- `([TIME]` - insert timetracking with the specified offset in minutes (empty: list tracked times)
|
||||||
- `)[TIME]` - stop timetracking with the specified offset in minutes - convenience helper to move to root (empty: stop now)
|
- `)[TIME]` - stop timetracking with the specified offset in minutes - convenience helper to move to root (empty: stop now)
|
||||||
- `>[TEXT]` - complete active task and move to parent, with optional state description
|
- `>[TEXT]` - complete active task and move to parent, with optional state description
|
||||||
|
|
26
src/main.rs
26
src/main.rs
|
@ -1,5 +1,5 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::env::{args, var};
|
use std::env::{args, var};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
@ -21,7 +21,7 @@ use xdg::BaseDirectories;
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
use crate::kinds::{KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
||||||
use crate::task::State;
|
use crate::task::State;
|
||||||
use crate::tasks::{StateFilter, Tasks};
|
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
||||||
|
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod task;
|
mod task;
|
||||||
|
@ -277,21 +277,31 @@ async fn main() {
|
||||||
tasks.flush()
|
tasks.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(':') =>
|
Some(':') => {
|
||||||
if let Some(digit) = iter.next().and_then(|s| s.to_digit(10)) {
|
let next = iter.next();
|
||||||
|
if let Some(':') = next {
|
||||||
|
let str: String = iter.collect();
|
||||||
|
let result = str.split_whitespace().map(|s| s.to_string()).collect::<VecDeque<_>>();
|
||||||
|
if result.len() == 1 {
|
||||||
|
tasks.add_sorting_property(str.trim().to_string())
|
||||||
|
} else {
|
||||||
|
tasks.set_sorting(result)
|
||||||
|
}
|
||||||
|
} 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();
|
let remaining = iter.collect::<String>().trim().to_string();
|
||||||
if remaining.is_empty() {
|
if remaining.is_empty() {
|
||||||
tasks.remove_column(index);
|
tasks.get_columns().remove_at(index);
|
||||||
} else {
|
} else {
|
||||||
tasks.add_or_remove_property_column_at_index(remaining, index);
|
tasks.get_columns().add_or_remove_at(remaining, index);
|
||||||
}
|
}
|
||||||
} else if let Some(arg) = arg {
|
} else if let Some(arg) = arg {
|
||||||
tasks.add_or_remove_property_column(arg);
|
tasks.get_columns().add_or_remove(arg.to_string());
|
||||||
} else {
|
} else {
|
||||||
println!("{}", PROPERTY_COLUMNS);
|
println!("{}", PROPERTY_COLUMNS);
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some(',') =>
|
Some(',') =>
|
||||||
match arg {
|
match arg {
|
||||||
|
|
184
src/tasks.rs
184
src/tasks.rs
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::io::{Error, stdout, Write};
|
use std::io::{Error, stdout, Write};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
@ -30,6 +30,8 @@ pub(crate) struct Tasks {
|
||||||
history: HashMap<PublicKey, BTreeSet<Event>>,
|
history: HashMap<PublicKey, BTreeSet<Event>>,
|
||||||
/// The task properties currently visible
|
/// The task properties currently visible
|
||||||
properties: Vec<String>,
|
properties: Vec<String>,
|
||||||
|
/// The task properties sorted by
|
||||||
|
sorting: VecDeque<String>,
|
||||||
/// Negative: Only Leaf nodes
|
/// Negative: Only Leaf nodes
|
||||||
/// Zero: Only Active node
|
/// Zero: Only Active node
|
||||||
/// Positive: Go down the respective level
|
/// Positive: Go down the respective level
|
||||||
|
@ -39,6 +41,8 @@ pub(crate) struct Tasks {
|
||||||
position: Option<EventId>,
|
position: Option<EventId>,
|
||||||
/// Currently active tags
|
/// Currently active tags
|
||||||
tags: BTreeSet<Tag>,
|
tags: BTreeSet<Tag>,
|
||||||
|
/// Tags filtered out
|
||||||
|
tags_excluded: BTreeSet<Tag>,
|
||||||
/// Current active state
|
/// Current active state
|
||||||
state: StateFilter,
|
state: StateFilter,
|
||||||
/// A filtered view of the current tasks
|
/// A filtered view of the current tasks
|
||||||
|
@ -121,9 +125,14 @@ impl Tasks {
|
||||||
"rpath".into(),
|
"rpath".into(),
|
||||||
"desc".into(),
|
"desc".into(),
|
||||||
],
|
],
|
||||||
|
sorting: VecDeque::from([
|
||||||
|
"state".into(),
|
||||||
|
"name".into(),
|
||||||
|
]),
|
||||||
position: None, // TODO persist position
|
position: None, // TODO persist position
|
||||||
view: Default::default(),
|
view: Default::default(),
|
||||||
tags: Default::default(),
|
tags: Default::default(),
|
||||||
|
tags_excluded: Default::default(),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
sender,
|
sender,
|
||||||
|
@ -259,9 +268,10 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_prompt_suffix(&self) -> String {
|
pub(crate) fn get_prompt_suffix(&self) -> String {
|
||||||
self.tags
|
self.tags.iter()
|
||||||
.iter()
|
|
||||||
.map(|t| format!(" #{}", t.content().unwrap()))
|
.map(|t| format!(" #{}", t.content().unwrap()))
|
||||||
|
.chain(self.tags_excluded.iter()
|
||||||
|
.map(|t| format!(" -#{}", t.content().unwrap())))
|
||||||
.chain(once(self.state.indicator()))
|
.chain(once(self.state.indicator()))
|
||||||
.join("")
|
.join("")
|
||||||
}
|
}
|
||||||
|
@ -358,12 +368,15 @@ impl Tasks {
|
||||||
self.resolve_tasks(self.children_of(self.position)).into_iter()
|
self.resolve_tasks(self.children_of(self.position)).into_iter()
|
||||||
.filter(|t| {
|
.filter(|t| {
|
||||||
// TODO apply filters in transit
|
// TODO apply filters in transit
|
||||||
let state = t.pure_state();
|
self.state.matches(t) &&
|
||||||
self.state.matches(t) && (self.tags.is_empty()
|
t.tags.as_ref().map_or(true, |tags| {
|
||||||
|| t.tags.as_ref().map_or(false, |tags| {
|
tags.iter().find(|tag| self.tags_excluded.contains(tag)).is_none()
|
||||||
let mut iter = tags.iter();
|
}) &&
|
||||||
self.tags.iter().all(|tag| iter.any(|t| t == tag))
|
(self.tags.is_empty() ||
|
||||||
}))
|
t.tags.as_ref().map_or(false, |tags| {
|
||||||
|
let mut iter = tags.iter();
|
||||||
|
self.tags.iter().all(|tag| iter.any(|t| t == tag))
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -399,62 +412,26 @@ impl Tasks {
|
||||||
writeln!(lock, "{}", t.descriptions().join("\n"))?;
|
writeln!(lock, "{}", t.descriptions().join("\n"))?;
|
||||||
}
|
}
|
||||||
// TODO proper column alignment
|
// TODO proper column alignment
|
||||||
// TODO hide empty columns and sorting
|
// TODO hide empty columns
|
||||||
writeln!(lock, "{}", self.properties.join("\t").bold())?;
|
writeln!(lock, "{}", self.properties.join("\t").bold())?;
|
||||||
let mut total_time = 0;
|
let mut total_time = 0;
|
||||||
for task in self.current_tasks() {
|
let mut tasks = self.current_tasks();
|
||||||
let progress =
|
tasks.sort_by_cached_key(|task| {
|
||||||
self
|
self.sorting
|
||||||
.total_progress(task.get_id())
|
.iter()
|
||||||
.filter(|_| task.children.len() > 0);
|
.map(|p| self.get_property(task, p.as_str()))
|
||||||
let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0));
|
.collect_vec()
|
||||||
|
});
|
||||||
|
for task in tasks {
|
||||||
writeln!(
|
writeln!(
|
||||||
lock,
|
lock,
|
||||||
"{}",
|
"{}",
|
||||||
self.properties
|
self.properties
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| match p.as_str() {
|
.map(|p| self.get_property(task, p.as_str()))
|
||||||
"subtasks" => {
|
|
||||||
let mut total = 0;
|
|
||||||
let mut done = 0;
|
|
||||||
for subtask in task.children.iter().filter_map(|id| self.get_by_id(id))
|
|
||||||
{
|
|
||||||
let state = subtask.pure_state();
|
|
||||||
total += &(state != State::Closed).into();
|
|
||||||
done += &(state == State::Done).into();
|
|
||||||
}
|
|
||||||
if total > 0 {
|
|
||||||
format!("{done}/{total}")
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"state" => {
|
|
||||||
if let Some(task) = task.get_dependendees().iter().filter_map(|id| self.get_by_id(id)).find(|t| t.pure_state().is_open()) {
|
|
||||||
return format!("Blocked by \"{}\"", task.get_title()).bright_red().to_string()
|
|
||||||
}
|
|
||||||
let state = task.state_or_default();
|
|
||||||
if state.state.is_open() && progress.is_some_and(|p| p > 0.1) {
|
|
||||||
state.state.colorize(&prog_string)
|
|
||||||
} else {
|
|
||||||
state.get_colored_label()
|
|
||||||
}.to_string()
|
|
||||||
}
|
|
||||||
"progress" => prog_string.clone(),
|
|
||||||
"path" => self.get_task_path(Some(task.event.id)),
|
|
||||||
"rpath" => self.relative_path(task.event.id),
|
|
||||||
// TODO format strings configurable
|
|
||||||
"time" => display_time("MMMm", self.time_tracked(*task.get_id())),
|
|
||||||
"rtime" => {
|
|
||||||
let time = self.total_time_tracked(*task.get_id());
|
|
||||||
total_time += time;
|
|
||||||
display_time("HH:MM", time)
|
|
||||||
}
|
|
||||||
prop => task.get(prop).unwrap_or(String::new()),
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" \t")
|
.join(" \t")
|
||||||
)?;
|
)?;
|
||||||
|
total_time += self.total_time_tracked(task.event.id)
|
||||||
}
|
}
|
||||||
if total_time > 0 {
|
if total_time > 0 {
|
||||||
writeln!(lock, "{}", display_time("Total time tracked on visible tasks: HHh MMm", total_time))?;
|
writeln!(lock, "{}", display_time("Total time tracked on visible tasks: HHh MMm", total_time))?;
|
||||||
|
@ -462,6 +439,48 @@ impl Tasks {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_property(&self, task: &Task, str: &str) -> String {
|
||||||
|
let progress =
|
||||||
|
self
|
||||||
|
.total_progress(task.get_id())
|
||||||
|
.filter(|_| task.children.len() > 0);
|
||||||
|
let prog_string = progress.map_or(String::new(), |p| format!("{:2.0}%", p * 100.0));
|
||||||
|
match str {
|
||||||
|
"subtasks" => {
|
||||||
|
let mut total = 0;
|
||||||
|
let mut done = 0;
|
||||||
|
for subtask in task.children.iter().filter_map(|id| self.get_by_id(id)) {
|
||||||
|
let state = subtask.pure_state();
|
||||||
|
total += &(state != State::Closed).into();
|
||||||
|
done += &(state == State::Done).into();
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
format!("{done}/{total}")
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"state" => {
|
||||||
|
if let Some(task) = task.get_dependendees().iter().filter_map(|id| self.get_by_id(id)).find(|t| t.pure_state().is_open()) {
|
||||||
|
return format!("Blocked by \"{}\"", task.get_title()).bright_red().to_string();
|
||||||
|
}
|
||||||
|
let state = task.state_or_default();
|
||||||
|
if state.state.is_open() && progress.is_some_and(|p| p > 0.1) {
|
||||||
|
state.state.colorize(&prog_string)
|
||||||
|
} else {
|
||||||
|
state.get_colored_label()
|
||||||
|
}.to_string()
|
||||||
|
}
|
||||||
|
"progress" => prog_string.clone(),
|
||||||
|
"path" => self.get_task_path(Some(task.event.id)),
|
||||||
|
"rpath" => self.relative_path(task.event.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())),
|
||||||
|
prop => task.get(prop).unwrap_or(String::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Movement and Selection
|
// Movement and Selection
|
||||||
|
|
||||||
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
|
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
|
||||||
|
@ -471,10 +490,12 @@ impl Tasks {
|
||||||
pub(crate) fn clear_filter(&mut self) {
|
pub(crate) fn clear_filter(&mut self) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
self.tags.clear();
|
self.tags.clear();
|
||||||
|
self.tags_excluded.clear();
|
||||||
info!("Removed all filters");
|
info!("Removed all filters");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_tag(&mut self, tag: String) {
|
pub(crate) fn set_tag(&mut self, tag: String) {
|
||||||
|
self.tags_excluded.clear();
|
||||||
self.tags.clear();
|
self.tags.clear();
|
||||||
self.add_tag(tag);
|
self.add_tag(tag);
|
||||||
}
|
}
|
||||||
|
@ -482,7 +503,9 @@ impl Tasks {
|
||||||
pub(crate) fn add_tag(&mut self, tag: String) {
|
pub(crate) fn add_tag(&mut self, tag: String) {
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
info!("Added tag filter for #{tag}");
|
info!("Added tag filter for #{tag}");
|
||||||
self.tags.insert(Hashtag(tag).into());
|
let tag: Tag = Hashtag(tag).into();
|
||||||
|
self.tags_excluded.remove(&tag);
|
||||||
|
self.tags.insert(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_tag(&mut self, tag: &str) {
|
pub(crate) fn remove_tag(&mut self, tag: &str) {
|
||||||
|
@ -492,7 +515,8 @@ impl Tasks {
|
||||||
if self.tags.len() < len {
|
if self.tags.len() < len {
|
||||||
info!("Removed tag filters starting with {tag}");
|
info!("Removed tag filters starting with {tag}");
|
||||||
} else {
|
} else {
|
||||||
info!("Found no tag filters starting with {tag} to remove");
|
self.tags_excluded.insert(Hashtag(tag.to_string()).into());
|
||||||
|
info!("Excluding #{tag} from view");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,29 +800,53 @@ impl Tasks {
|
||||||
info!("Changed view depth to {depth}");
|
info!("Changed view depth to {depth}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_column(&mut self, index: usize) {
|
pub(crate) fn get_columns(&mut self) -> &mut Vec<String> {
|
||||||
let col = self.properties.remove(index);
|
&mut self.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_sorting(&mut self, vec: VecDeque<String>) {
|
||||||
|
self.sorting = vec;
|
||||||
|
info!("Now sorting by {:?}", self.sorting);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_sorting_property(&mut self, property: String) {
|
||||||
|
// TODO reverse order if already present
|
||||||
|
self.sorting.push_front(property);
|
||||||
|
self.sorting.truncate(4);
|
||||||
|
info!("Now sorting by {:?}", self.sorting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PropertyCollection<T> {
|
||||||
|
fn remove_at(&mut self, index: usize);
|
||||||
|
fn add_or_remove(&mut self, value: T);
|
||||||
|
fn add_or_remove_at(&mut self, value: T, index: usize);
|
||||||
|
}
|
||||||
|
impl <T> PropertyCollection<T> for Vec<T>
|
||||||
|
where T: Display, T: Eq, T: Clone {
|
||||||
|
fn remove_at(&mut self, index: usize) {
|
||||||
|
let col = self.remove(index);
|
||||||
info!("Removed property column \"{col}\"");
|
info!("Removed property column \"{col}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_or_remove_property_column(&mut self, property: &str) {
|
fn add_or_remove(&mut self, property: T) {
|
||||||
match self.properties.iter().position(|s| s == property) {
|
match self.iter().position(|s| s == &property) {
|
||||||
None => {
|
None => {
|
||||||
self.properties.push(property.to_string());
|
|
||||||
info!("Added property column \"{property}\"");
|
info!("Added property column \"{property}\"");
|
||||||
|
self.push(property);
|
||||||
}
|
}
|
||||||
Some(index) => {
|
Some(index) => {
|
||||||
self.properties.remove(index);
|
self.remove_at(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_or_remove_property_column_at_index(&mut self, property: String, index: usize) {
|
fn add_or_remove_at(&mut self, property: T, index: usize) {
|
||||||
if self.properties.get(index) == Some(&property) {
|
if self.get(index) == Some(&property) {
|
||||||
self.properties.remove(index);
|
self.remove_at(index);
|
||||||
} else {
|
} else {
|
||||||
info!("Added property column \"{property}\" at position {}", index + 1);
|
info!("Added property column \"{property}\" at position {}", index + 1);
|
||||||
self.properties.insert(index, property);
|
self.insert(index, property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue