Compare commits

...

5 Commits

Author SHA1 Message Date
xeruf 6de7920af1 feat(main): enable manual state updates 2024-07-29 11:15:13 +03:00
xeruf 8e7b8d3e66 fix(main): prevent overrunning string index on column edits 2024-07-29 09:59:17 +03:00
xeruf b71916c905 feat(task): match state filter case-insensitively 2024-07-29 09:43:13 +03:00
xeruf 5d6b2a2dcb feat(main): trim input strings 2024-07-29 09:32:10 +03:00
xeruf 5723151cfb feat: properly format tracked time 2024-07-29 09:19:23 +03:00
4 changed files with 80 additions and 52 deletions

View File

@ -36,10 +36,14 @@ Dots can be repeated to move to parent tasks
- `:[IND][COL]` - add / remove property column COL to IND or end - `:[IND][COL]` - add / remove property column COL to IND or end
- `>[TEXT]` - Complete active task and move to parent, with optional state description - `>[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]` - Close active task and move to parent, with optional state description
- `#TAG` - filter by tag - `|TEXT` - Set state for current task from text (also aliased to `/` for now)
- `?TAG` - filter by state (type or description)
- `-TEXT` - add text note (comment / description) - `-TEXT` - add text note (comment / description)
Property Filters:
- `#TAG` - filter by tag
- `?TAG` - filter by state (type or description) - plain `?` to reset
State descriptions can be used for example for Kanban columns. State descriptions can be used for example for Kanban columns.
An active tag or state filter will also create new tasks with those corresponding attributes. An active tag or state filter will also create new tasks with those corresponding attributes.
@ -54,7 +58,7 @@ An active tag or state filter will also create new tasks with those correspondin
- `path` - name including parent tasks - `path` - name including parent tasks
- `rpath` - name including parent tasks up to active task - `rpath` - name including parent tasks up to active task
- `time` - time tracked - `time` - time tracked
- `ttime` - time tracked including subtasks - `rtime` - time tracked including subtasks
- TBI: `progress` - how many subtasks are complete - TBI: `progress` - how many subtasks are complete
- TBI: `progressp` - subtask completion in percent - TBI: `progressp` - subtask completion in percent

View File

@ -3,7 +3,6 @@ use std::fmt::Display;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::{BufRead, BufReader, stdin, stdout, Write}; use std::io::{BufRead, BufReader, stdin, stdout, Write};
use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::mpsc; use std::sync::mpsc;
@ -88,8 +87,7 @@ async fn main() {
Ok(relay) => { Ok(relay) => {
or_print(client.add_relay(relay).await); or_print(client.add_relay(relay).await);
} }
_ => { _ => match File::open(&relayfile).map(|f| BufReader::new(f).lines().flatten()) {
match File::open(&relayfile).map(|f| BufReader::new(f).lines().flatten()) {
Ok(lines) => { Ok(lines) => {
for line in lines { for line in lines {
or_print(client.add_relay(line).await); or_print(client.add_relay(line).await);
@ -98,20 +96,19 @@ async fn main() {
Err(e) => { Err(e) => {
eprintln!("Could not read relays file: {}", e); eprintln!("Could not read relays file: {}", e);
if let Some(line) = prompt("Relay?") { if let Some(line) = prompt("Relay?") {
let url = if line.contains("://") { line } else { "wss://".to_string() + &line }; let url = if line.contains("://") {
or_print( line
client } else {
.add_relay(url.clone()) "wss://".to_string() + &line
.await, };
).map(|bool| { or_print(client.add_relay(url.clone()).await).map(|bool| {
if bool { if bool {
or_print(fs::write(&relayfile, url)); or_print(fs::write(&relayfile, url));
} }
}); });
}; };
} }
} },
}
} }
//let proxy = Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9050))); //let proxy = Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9050)));
@ -188,7 +185,11 @@ async fn main() {
loop { loop {
tasks.print_tasks(); tasks.print_tasks();
print!(" {}{}) ", tasks.get_task_path(tasks.get_position()), tasks.get_prompt_suffix()); print!(
" {}{}) ",
tasks.get_task_path(tasks.get_position()),
tasks.get_prompt_suffix()
);
stdout().flush().unwrap(); stdout().flush().unwrap();
match lines.next() { match lines.next() {
Some(Ok(input)) => { Some(Ok(input)) => {
@ -206,28 +207,34 @@ async fn main() {
let mut iter = input.chars(); let mut iter = input.chars();
let op = iter.next(); let op = iter.next();
let arg = if input.len() > 1 {
input[1..].trim()
} else {
""
};
match op { match op {
None => {} None => {}
Some(':') => match input[1..2].parse::<usize>() { Some(':') => match iter.next().and_then(|s| s.to_digit(10)) {
Ok(index) => { Some(digit) => {
if input.len() == 2 { let index = digit as usize;
let remaining = iter.collect::<String>().trim().to_string();
if remaining.is_empty() {
tasks.properties.remove(index); tasks.properties.remove(index);
continue; continue;
} }
let value = input[2..].to_string(); let value = input[2..].trim().to_string();
if tasks.properties.get(index) == Some(&value) { if tasks.properties.get(index) == Some(&value) {
tasks.properties.remove(index); tasks.properties.remove(index);
} else { } else {
tasks.properties.insert(index, value); tasks.properties.insert(index, value);
} }
} }
Err(_) => { None => {
let prop = &input[1..]; let pos = tasks.properties.iter().position(|s| s == arg);
let pos = tasks.properties.iter().position(|s| s == &prop);
match pos { match pos {
None => { None => {
tasks.properties.push(prop.to_string()); tasks.properties.push(arg.to_string());
} }
Some(i) => { Some(i) => {
tasks.properties.remove(i); tasks.properties.remove(i);
@ -237,25 +244,35 @@ async fn main() {
}, },
Some('?') => { Some('?') => {
let arg = &input[1..];
tasks.set_state_filter(Some(arg.to_string()).filter(|s| !s.is_empty())); tasks.set_state_filter(Some(arg.to_string()).filter(|s| !s.is_empty()));
} }
Some('-') => tasks.add_note(&input[1..]), Some('-') => tasks.add_note(arg),
Some('>') | Some('<') => { Some('>') => {
tasks.update_state(&input[1..], |_| { tasks.update_state(arg, |_| Some(State::Done));
Some(if op.unwrap() == '<' { tasks.move_up();
State::Closed }
} else {
State::Done Some('<') => {
}) tasks.update_state(arg, |_| Some(State::Closed));
}); tasks.move_up();
tasks.move_up() }
Some('|') | Some('/') => {
match tasks.get_position() {
None => {
println!("First select a task to set its state!");
}
Some(id) => {
tasks.set_state_for(&id, arg);
tasks.move_to(tasks.get_position());
}
}
} }
Some('#') => { Some('#') => {
tasks.add_tag(input[1..].to_string()); tasks.add_tag(arg.to_string());
} }
Some('.') => { Some('.') => {

View File

@ -1,5 +1,6 @@
use std::collections::{BTreeSet, HashSet}; use std::collections::{BTreeSet, HashSet};
use std::fmt; use std::fmt;
use std::ops::Div;
use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, Timestamp}; use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, Timestamp};
@ -124,7 +125,7 @@ impl Task {
"parentid" => self.parent_id().map(|i| i.to_string()), "parentid" => self.parent_id().map(|i| i.to_string()),
"state" => self.state().map(|s| s.to_string()), "state" => self.state().map(|s| s.to_string()),
"name" => Some(self.event.content.clone()), "name" => Some(self.event.content.clone()),
"time" => Some(self.time_tracked().to_string()), // TODO: format properly "time" => Some(format!("{}m", self.time_tracked().div(60))),
"tags" => self.tags.as_ref().map(|tags| { "tags" => self.tags.as_ref().map(|tags| {
tags.iter() tags.iter()
.map(|t| format!("{}", t.content().unwrap())) .map(|t| format!("{}", t.content().unwrap()))
@ -163,8 +164,11 @@ impl TaskState {
} }
pub(crate) fn matches_label(&self, label: &str) -> bool { pub(crate) fn matches_label(&self, label: &str) -> bool {
self.state == State::Active self.state == State::Active
|| self.name.as_ref().is_some_and(|n| n == label) || self
|| self.state.to_string() == label .name
.as_ref()
.is_some_and(|n| n.eq_ignore_ascii_case(label))
|| self.state.to_string().eq_ignore_ascii_case(label)
} }
} }
impl fmt::Display for TaskState { impl fmt::Display for TaskState {

View File

@ -37,7 +37,7 @@ impl Tasks {
tasks: Default::default(), tasks: Default::default(),
properties: vec![ properties: vec![
"state".into(), "state".into(),
"ttime".into(), "rtime".into(),
"rpath".into(), "rpath".into(),
"tags".into(), "tags".into(),
"desc".into(), "desc".into(),
@ -192,7 +192,10 @@ impl Tasks {
self.traverse_up_from(Some(task.event.id)) self.traverse_up_from(Some(task.event.id))
.take_while(|t| Some(t.event.id) != self.position) .take_while(|t| Some(t.event.id) != self.position)
), ),
"ttime" => self.total_time_tracked(&task.event.id).to_string(), "rtime" => {
let time = self.total_time_tracked(&task.event.id);
format!("{:02}:{:02}", time / 3600, time / 60 % 60)
},
prop => task.get(prop).unwrap_or(String::new()), prop => task.get(prop).unwrap_or(String::new()),
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()