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
- `>[TEXT]` - Complete 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
- `?TAG` - filter by state (type or description)
- `|TEXT` - Set state for current task from text (also aliased to `/` for now)
- `-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.
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
- `rpath` - name including parent tasks up to active task
- `time` - time tracked
- `ttime` - time tracked including subtasks
- `rtime` - time tracked including subtasks
- TBI: `progress` - how many subtasks are complete
- TBI: `progressp` - subtask completion in percent

View File

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

View File

@ -1,5 +1,6 @@
use std::collections::{BTreeSet, HashSet};
use std::fmt;
use std::ops::Div;
use nostr_sdk::{Event, EventBuilder, EventId, Kind, Tag, Timestamp};
@ -124,7 +125,7 @@ impl Task {
"parentid" => self.parent_id().map(|i| i.to_string()),
"state" => self.state().map(|s| s.to_string()),
"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.iter()
.map(|t| format!("{}", t.content().unwrap()))
@ -163,8 +164,11 @@ impl TaskState {
}
pub(crate) fn matches_label(&self, label: &str) -> bool {
self.state == State::Active
|| self.name.as_ref().is_some_and(|n| n == label)
|| self.state.to_string() == label
|| self
.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 {

View File

@ -37,7 +37,7 @@ impl Tasks {
tasks: Default::default(),
properties: vec![
"state".into(),
"ttime".into(),
"rtime".into(),
"rpath".into(),
"tags".into(),
"desc".into(),
@ -192,7 +192,10 @@ impl Tasks {
self.traverse_up_from(Some(task.event.id))
.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()),
})
.collect::<Vec<String>>()