Compare commits
5 Commits
b19f2a85d8
...
6de7920af1
Author | SHA1 | Date |
---|---|---|
xeruf | 6de7920af1 | |
xeruf | 8e7b8d3e66 | |
xeruf | b71916c905 | |
xeruf | 5d6b2a2dcb | |
xeruf | 5723151cfb |
10
README.md
10
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
105
src/main.rs
105
src/main.rs
|
@ -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,30 +87,28 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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| {
|
|
||||||
if bool {
|
|
||||||
or_print(fs::write(&relayfile, url));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
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| {
|
||||||
|
if bool {
|
||||||
|
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('.') => {
|
||||||
|
|
10
src/task.rs
10
src/task.rs
|
@ -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 {
|
||||||
|
|
|
@ -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>>()
|
||||||
|
|
Loading…
Reference in New Issue