Compare commits

..

No commits in common. "fbbdd5eef8efc2b86070ac59cb7c75013fb3664f" and "8d2cf29b830cc46e89be96e41cc13f9e86f931e4" have entirely different histories.

6 changed files with 72 additions and 412 deletions

243
Cargo.lock generated
View File

@ -50,85 +50,12 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.18" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.80" version = "0.1.80"
@ -368,20 +295,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -393,39 +306,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "colog"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c426b7af8d5e0ad79de6713996632ce31f0d68ba84068fb0d654b396e519df0"
dependencies = [
"colored",
"env_logger",
"log",
]
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "colored"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.12" version = "0.2.12"
@ -469,29 +349,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -720,12 +577,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.3.1" version = "1.3.1"
@ -782,29 +633,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -853,12 +681,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -883,12 +705,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.153" version = "0.2.153"
@ -956,13 +772,9 @@ dependencies = [
[[package]] [[package]]
name = "mostr" name = "mostr"
version = "0.2.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"colog",
"colored",
"itertools", "itertools",
"log",
"nostr-sdk", "nostr-sdk",
"once_cell", "once_cell",
"tokio", "tokio",
@ -1079,15 +891,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -1267,35 +1070,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "regex"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.4" version = "0.12.4"
@ -1839,12 +1613,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -1968,15 +1736,6 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View File

@ -5,19 +5,15 @@ repository = "https://forge.ftt.gmbh/janek/mostr"
readme = "README.md" readme = "README.md"
license = "GPL 3.0" license = "GPL 3.0"
authors = ["melonion"] authors = ["melonion"]
version = "0.2.0" version = "0.1.0"
edition = "2021" edition = "2021"
default-run = "mostr" default-run = "mostr"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
xdg = "2.5.2"
itertools = "0.12.1"
log = "0.4.21"
chrono = "0.4.38"
colog = "1.3.0"
colored = "2.1.0"
nostr-sdk = "0.30" nostr-sdk = "0.30"
tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] }
once_cell = "1.19.0" once_cell = "1.19.0"
xdg = "2.5.2"
itertools = "0.12.1"

View File

@ -6,18 +6,11 @@ A nested task chat, powered by nostr!
First, start a nostr relay, such as First, start a nostr relay, such as
- https://github.com/coracle-social/bucket for local development - https://github.com/coracle-social/bucket for local development
- https://github.com/rnostr/rnostr for production use - rnostr for production use
Run development build with:
cargo run cargo run
Creating a test task: Creating a test task: nostril --envelope --content "test task" --kind 1630 | websocat ws://localhost:4736
`nostril --envelope --content "test task" --kind 1621 | websocat ws://localhost:4736`
Install latest build:
cargo install --path . --offline
## Principles ## Principles
@ -63,10 +56,9 @@ An active tag or state filter will also set that attribute for newly created tas
- `parentid` - `parentid`
- `name` - `name`
- `state` - `state`
- `tags`
- `hashtags` - `hashtags`
- `tags` - values of all nostr tags associated with the event, except event tags - `desc` - accumulated notes of the task
- `desc` - last note on the task
- `description` - accumulated notes on the task
- `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
@ -74,9 +66,7 @@ An active tag or state filter will also set that attribute for newly created tas
- 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
For debugging: `props`, `alltags`, `descriptions` For debugging: `props` - Task Property Events
TODO: Combined formatting and recursion specifiers
## Plans ## Plans
@ -86,8 +76,7 @@ TODO: Combined formatting and recursion specifiers
- Unified Filter object - Unified Filter object
-> include sub -> include sub
- Time tracking: Active not as task state, ability to postpone task and add planned timestamps (calendar entry) - Time tracking: Active not as task state, ability to postpone task and add planned timestamps (calendar entry)
- Web Interface, Messenger integrations
- TUI - Clear terminal? - TUI - Clear terminal?
- Expiry (no need to fetch potential years of history) - Expiry (no need to fetch potential years of history)
- Offline caching
- Web Interface, Messenger integrations
- Relay: filter out task state updates within few seconds, also on client side - Relay: filter out task state updates within few seconds, also on client side

View File

@ -8,8 +8,6 @@ use std::str::FromStr;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use colored::Colorize;
use log::{debug, error, info, trace, warn};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use xdg::BaseDirectories; use xdg::BaseDirectories;
@ -48,7 +46,7 @@ fn or_print<T, U: Display>(result: Result<T, U>) -> Option<T> {
match result { match result {
Ok(value) => Some(value), Ok(value) => Some(value),
Err(error) => { Err(error) => {
warn!("{}", error); eprintln!("{}", error);
None None
} }
} }
@ -65,8 +63,6 @@ fn prompt(prompt: &str) -> Option<String> {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
colog::init();
let config_dir = or_print(BaseDirectories::new()) let config_dir = or_print(BaseDirectories::new())
.and_then(|d| or_print(d.create_config_directory("mostr"))) .and_then(|d| or_print(d.create_config_directory("mostr")))
.unwrap_or(PathBuf::new()); .unwrap_or(PathBuf::new());
@ -76,7 +72,7 @@ async fn main() {
let keys = match fs::read_to_string(&keysfile).map(|s| Keys::from_str(&s)) { let keys = match fs::read_to_string(&keysfile).map(|s| Keys::from_str(&s)) {
Ok(Ok(key)) => key, Ok(Ok(key)) => key,
_ => { _ => {
warn!("Could not read keys from {}", keysfile.to_string_lossy()); eprintln!("Could not read keys from {}", keysfile.to_string_lossy());
let keys = prompt("Secret Key?") let keys = prompt("Secret Key?")
.and_then(|s| or_print(Keys::from_str(&s))) .and_then(|s| or_print(Keys::from_str(&s)))
.unwrap_or_else(|| Keys::generate()); .unwrap_or_else(|| Keys::generate());
@ -86,7 +82,7 @@ async fn main() {
}; };
let client = Client::new(&keys); let client = Client::new(&keys);
info!("My public key: {}", keys.public_key()); println!("My public key: {}", keys.public_key());
match var("MOSTR_RELAY") { match var("MOSTR_RELAY") {
Ok(relay) => { Ok(relay) => {
or_print(client.add_relay(relay).await); or_print(client.add_relay(relay).await);
@ -98,7 +94,7 @@ async fn main() {
} }
} }
Err(e) => { Err(e) => {
warn!("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("://") { let url = if line.contains("://") {
line line
@ -150,7 +146,7 @@ async fn main() {
}); });
let sub_id: SubscriptionId = client.subscribe(vec![Filter::new()], None).await; let sub_id: SubscriptionId = client.subscribe(vec![Filter::new()], None).await;
info!("Subscribed with {}", sub_id); eprintln!("Subscribed with {}", sub_id);
let mut notifications = client.notifications(); let mut notifications = client.notifications();
/*println!("Finding existing events"); /*println!("Finding existing events");
@ -174,11 +170,10 @@ async fn main() {
let sender = tokio::spawn(async move { let sender = tokio::spawn(async move {
while let Ok(e) = rx.recv() { while let Ok(e) = rx.recv() {
trace!("Sending {}", e.id); //eprintln!("Sending {}", e.id);
// TODO send in batches
let _ = client.send_event(e).await; let _ = client.send_event(e).await;
} }
info!("Stopping listeners..."); println!("Stopping listeners...");
client.unsubscribe_all().await; client.unsubscribe_all().await;
}); });
for argument in args().skip(1) { for argument in args().skip(1) {
@ -188,21 +183,16 @@ async fn main() {
println!(); println!();
let mut lines = stdin().lines(); let mut lines = stdin().lines();
loop { loop {
or_print(tasks.print_tasks()); tasks.print_tasks();
print!( print!(
"{}",
format!(
" {}{}) ", " {}{}) ",
tasks.get_task_path(tasks.get_position()), tasks.get_task_path(tasks.get_position()),
tasks.get_prompt_suffix() tasks.get_prompt_suffix()
)
.italic()
); );
stdout().flush().unwrap(); stdout().flush().unwrap();
match lines.next() { match lines.next() {
Some(Ok(input)) => { Some(Ok(input)) => {
let mut count = 0;
while let Ok(notification) = notifications.try_recv() { while let Ok(notification) = notifications.try_recv() {
if let RelayPoolNotification::Event { if let RelayPoolNotification::Event {
subscription_id, subscription_id,
@ -212,12 +202,8 @@ async fn main() {
{ {
print_event(&event); print_event(&event);
tasks.add(*event); tasks.add(*event);
count += 1;
} }
} }
if count > 0 {
info!("Received {count} updates");
}
let mut iter = input.chars(); let mut iter = input.chars();
let op = iter.next(); let op = iter.next();
@ -275,7 +261,7 @@ async fn main() {
Some('|') | Some('/') => match tasks.get_position() { Some('|') | Some('/') => match tasks.get_position() {
None => { None => {
warn!("First select a task to set its state!"); println!("First select a task to set its state!");
} }
Some(id) => { Some(id) => {
tasks.set_state_for(&id, arg); tasks.set_state_for(&id, arg);
@ -351,13 +337,12 @@ async fn main() {
} }
} }
} }
Some(Err(e)) => warn!("{}", e), Some(Err(e)) => eprintln!("{}", e),
None => break, None => break,
} }
} }
println!(); println!();
// TODO optionally continue
tasks.update_state("", |t| { tasks.update_state("", |t| {
if t.pure_state() == State::Active { if t.pure_state() == State::Active {
Some(State::Open) Some(State::Open)
@ -367,12 +352,12 @@ async fn main() {
}); });
drop(tasks); drop(tasks);
info!("Submitting pending changes..."); eprintln!("Submitting pending changes...");
or_print(sender.await); or_print(sender.await);
} }
fn print_event(event: &Event) { fn print_event(event: &Event) {
debug!( eprintln!(
"At {} found {} kind {} '{}' {:?}", "At {} found {} kind {} '{}' {:?}",
event.created_at, event.id, event.kind, event.content, event.tags event.created_at, event.id, event.kind, event.content, event.tags
); );

View File

@ -4,7 +4,6 @@ use std::ops::Div;
use itertools::Either::{Left, Right}; use itertools::Either::{Left, Right};
use itertools::Itertools; use itertools::Itertools;
use log::{debug, error, info, trace, warn};
use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, Kind, Tag, Timestamp}; use nostr_sdk::{Alphabet, Event, EventBuilder, EventId, Kind, Tag, Timestamp};
use crate::EventSender; use crate::EventSender;
@ -21,9 +20,11 @@ pub(crate) struct Task {
impl Task { impl Task {
pub(crate) fn new(event: Event) -> Task { pub(crate) fn new(event: Event) -> Task {
let (parents, tags) = event.tags.iter().partition_map(|tag| match tag { let (parents, tags) = event.tags.iter().partition_map(|tag| {
match tag {
Tag::Event { event_id, .. } => return Left(event_id), Tag::Event { event_id, .. } => return Left(event_id),
_ => Right(tag.clone()), _ => Right(tag.clone())
}
}); });
Task { Task {
children: Default::default(), children: Default::default(),
@ -48,7 +49,7 @@ impl Task {
.unwrap_or_else(|| self.get_id().to_string()) .unwrap_or_else(|| self.get_id().to_string())
} }
pub(crate) fn descriptions(&self) -> impl Iterator<Item = &String> + '_ { fn descriptions(&self) -> impl Iterator<Item = &String> + '_ {
self.props.iter().filter_map(|event| { self.props.iter().filter_map(|event| {
if event.kind == Kind::TextNote { if event.kind == Kind::TextNote {
Some(&event.content) Some(&event.content)
@ -61,7 +62,11 @@ impl Task {
fn states(&self) -> impl Iterator<Item = TaskState> + '_ { fn states(&self) -> impl Iterator<Item = TaskState> + '_ {
self.props.iter().filter_map(|event| { self.props.iter().filter_map(|event| {
event.kind.try_into().ok().map(|s| TaskState { event.kind.try_into().ok().map(|s| TaskState {
name: Some(event.content.clone()).filter(|c| !c.is_empty()), name: if event.content.is_empty() {
None
} else {
Some(event.content.clone())
},
state: s, state: s,
time: event.created_at.clone(), time: event.created_at.clone(),
}) })
@ -124,9 +129,7 @@ impl Task {
} }
fn filter_tags<P>(&self, predicate: P) -> Option<String> fn filter_tags<P>(&self, predicate: P) -> Option<String>
where where P: FnMut(&&Tag) -> bool{
P: FnMut(&&Tag) -> bool,
{
self.tags.as_ref().map(|tags| { self.tags.as_ref().map(|tags| {
tags.into_iter() tags.into_iter()
.filter(predicate) .filter(predicate)
@ -143,12 +146,7 @@ impl Task {
"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(format!("{}m", self.time_tracked().div(60))), "time" => Some(format!("{}m", self.time_tracked().div(60))),
"desc" => self.descriptions().last().cloned(), "hashtags" => self.filter_tags(|tag| tag.single_letter_tag().is_some_and(|sltag| sltag.character == Alphabet::T)),
"description" => Some(self.descriptions().join(" ")),
"hashtags" => self.filter_tags(|tag| {
tag.single_letter_tag()
.is_some_and(|sltag| sltag.character == Alphabet::T)
}),
"tags" => self.filter_tags(|_| true), "tags" => self.filter_tags(|_| true),
"alltags" => Some(format!("{:?}", self.tags)), "alltags" => Some(format!("{:?}", self.tags)),
"props" => Some(format!( "props" => Some(format!(
@ -158,12 +156,10 @@ impl Task {
.map(|e| format!("{} kind {} '{}'", e.created_at, e.kind, e.content)) .map(|e| format!("{} kind {} '{}'", e.created_at, e.kind, e.content))
.collect::<Vec<String>>() .collect::<Vec<String>>()
)), )),
"descriptions" => Some(format!( "descriptions" => Some(format!("{:?}", self.descriptions().collect::<Vec<&String>>())),
"{:?}", "desc" | "description" => self.descriptions().last().cloned(),
self.descriptions().collect::<Vec<&String>>()
)),
_ => { _ => {
warn!("Unknown task property {}", property); eprintln!("Unknown task property {}", property);
None None
} }
} }
@ -173,7 +169,7 @@ impl Task {
pub(crate) struct TaskState { pub(crate) struct TaskState {
state: State, state: State,
name: Option<String>, name: Option<String>,
pub(crate) time: Timestamp, time: Timestamp,
} }
impl TaskState { impl TaskState {
pub(crate) fn get_label(&self) -> String { pub(crate) fn get_label(&self) -> String {

View File

@ -1,12 +1,6 @@
use std::collections::{BTreeSet, HashMap}; use std::collections::{BTreeSet, HashMap};
use std::io::{Error, stdout, Write};
use std::iter::once; use std::iter::once;
use chrono::{Local, TimeZone};
use chrono::LocalResult::Single;
use colored::Colorize;
use itertools::Itertools;
use log::{debug, error, info, trace, warn};
use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, Tag}; use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, Tag};
use nostr_sdk::Tag::Hashtag; use nostr_sdk::Tag::Hashtag;
@ -97,7 +91,7 @@ impl Tasks {
} }
pub(crate) fn get_task_path(&self, id: Option<EventId>) -> String { pub(crate) fn get_task_path(&self, id: Option<EventId>) -> String {
join_tasks(self.traverse_up_from(id), true) join_tasks(self.traverse_up_from(id))
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.or_else(|| id.map(|id| id.to_string())) .or_else(|| id.map(|id| id.to_string()))
.unwrap_or(String::new()) .unwrap_or(String::new())
@ -111,15 +105,6 @@ impl Tasks {
} }
} }
fn relative_path(&self, id: EventId) -> String {
join_tasks(
self.traverse_up_from(Some(id))
.take_while(|t| Some(t.event.id) != self.position),
false,
)
.unwrap_or(id.to_string())
}
// Helpers // Helpers
fn resolve_tasks<'a>(&self, iter: impl IntoIterator<Item = &'a EventId>) -> Vec<&Task> { fn resolve_tasks<'a>(&self, iter: impl IntoIterator<Item = &'a EventId>) -> Vec<&Task> {
@ -165,13 +150,13 @@ impl Tasks {
} }
} }
fn current_task(&self) -> Option<&Task> {
self.position.and_then(|id| self.tasks.get(&id))
}
pub(crate) fn current_tasks(&self) -> Vec<&Task> { pub(crate) fn current_tasks(&self) -> Vec<&Task> {
if self.depth == 0 { if self.depth == 0 {
return self.current_task().into_iter().collect(); return self
.position
.and_then(|id| self.tasks.get(&id))
.into_iter()
.collect();
} }
let res: Vec<&Task> = self.resolve_tasks(self.view.iter()); let res: Vec<&Task> = self.resolve_tasks(self.view.iter());
if res.len() > 0 { if res.len() > 0 {
@ -197,47 +182,20 @@ impl Tasks {
.collect() .collect()
} }
pub(crate) fn print_tasks(&self) -> Result<(), Error> { pub(crate) fn print_tasks(&self) {
let mut lock = stdout().lock(); println!("{}", self.properties.join("\t"));
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(),
},
t.time_tracked() / 60
)?;
}
writeln!(lock, "{}", t.descriptions().join("\n"))?;
}
// TODO proper columns
writeln!(lock, "{}", self.properties.join("\t").bold())?;
for task in self.current_tasks() { for task in self.current_tasks() {
writeln!( println!(
lock,
"{}", "{}",
self.properties self.properties
.iter() .iter()
.map(|p| match p.as_str() { .map(|p| match p.as_str() {
"path" => self.get_task_path(Some(task.event.id)), "path" => self.get_task_path(Some(task.event.id)),
"rpath" => self.relative_path(task.event.id), "rpath" => join_tasks(
self.traverse_up_from(Some(task.event.id))
.take_while(|t| Some(t.event.id) != self.position)
)
.unwrap_or(task.event.id.to_string()),
"rtime" => { "rtime" => {
let time = self.total_time_tracked(&task.event.id); let time = self.total_time_tracked(&task.event.id);
format!("{:02}:{:02}", time / 3600, time / 60 % 60) format!("{:02}:{:02}", time / 3600, time / 60 % 60)
@ -246,10 +204,9 @@ impl Tasks {
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" \t") .join(" \t")
)?; );
} }
writeln!(lock)?; println!();
Ok(())
} }
// Movement and Selection // Movement and Selection
@ -269,7 +226,11 @@ impl Tasks {
} }
pub(crate) fn move_up(&mut self) { pub(crate) fn move_up(&mut self) {
self.move_to(self.current_task().and_then(|t| t.parent_id())) self.move_to(
self.position
.and_then(|id| self.tasks.get(&id))
.and_then(|t| t.parent_id()),
)
} }
pub(crate) fn move_to(&mut self, id: Option<EventId>) { pub(crate) fn move_to(&mut self, id: Option<EventId>) {
@ -305,13 +266,7 @@ impl Tasks {
return match input.split_once(": ") { return match input.split_once(": ") {
None => EventBuilder::new(Kind::from(TASK_KIND), input, tags), None => EventBuilder::new(Kind::from(TASK_KIND), input, tags),
Some(s) => { Some(s) => {
tags.append( tags.append(&mut s.1.split_ascii_whitespace().map(|t| Hashtag(t.to_string())).collect());
&mut s
.1
.split_ascii_whitespace()
.map(|t| Hashtag(t.to_string()))
.collect(),
);
EventBuilder::new(Kind::from(TASK_KIND), s.0, tags) EventBuilder::new(Kind::from(TASK_KIND), s.0, tags)
} }
}; };
@ -341,7 +296,7 @@ impl Tasks {
t.children.insert(event.id); t.children.insert(event.id);
}); });
if self.tasks.contains_key(&event.id) { if self.tasks.contains_key(&event.id) {
debug!("Did not insert duplicate event {}", event.id); //eprintln!("Did not insert duplicate event {}", event.id);
} else { } else {
self.tasks.insert(event.id, Task::new(event)); self.tasks.insert(event.id, Task::new(event));
} }
@ -387,7 +342,7 @@ impl Tasks {
pub(crate) fn add_note(&mut self, note: &str) { pub(crate) fn add_note(&mut self, note: &str) {
match self.position { match self.position {
None => warn!("Cannot add note '{}' without active task", note), None => eprintln!("Cannot add note '{}' without active task", note),
Some(id) => { Some(id) => {
self.sender self.sender
.submit(EventBuilder::text_note(note, vec![])) .submit(EventBuilder::text_note(note, vec![]))
@ -401,23 +356,18 @@ impl Tasks {
} }
} }
pub(crate) fn join_tasks<'a>( pub(crate) fn join_tasks<'a>(iter: impl Iterator<Item = &'a Task>) -> Option<String> {
iter: impl Iterator<Item = &'a Task>,
include_last_id: bool,
) -> Option<String> {
let tasks: Vec<&Task> = iter.collect(); let tasks: Vec<&Task> = iter.collect();
tasks tasks
.iter() .iter()
.map(|t| t.get_title()) .map(|t| t.get_title())
.chain(if include_last_id { .chain(
tasks tasks
.last() .last()
.and_then(|t| t.parent_id()) .and_then(|t| t.parent_id())
.map(|id| id.to_string()) .map(|id| id.to_string())
.into_iter() .into_iter(),
} else { )
None.into_iter()
})
.fold(None, |acc, val| { .fold(None, |acc, val| {
Some(acc.map_or_else(|| val.clone(), |cur| format!("{}>{}", val, cur))) Some(acc.map_or_else(|| val.clone(), |cur| format!("{}>{}", val, cur)))
}) })
@ -456,7 +406,7 @@ fn test_depth() {
let task1 = tasks.get_by_id(&t1.unwrap()).unwrap(); let task1 = tasks.get_by_id(&t1.unwrap()).unwrap();
assert_eq!(tasks.depth, 1); assert_eq!(tasks.depth, 1);
assert_eq!(task1.state().unwrap().get_label(), "Open"); assert_eq!(task1.state().unwrap().get_label(), "Open");
debug!("{:?}", tasks); //eprintln!("{:?}", tasks);
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.current_tasks().len(), 1);
tasks.depth = 0; tasks.depth = 0;
assert_eq!(tasks.current_tasks().len(), 0); assert_eq!(tasks.current_tasks().len(), 0);
@ -466,8 +416,6 @@ fn test_depth() {
assert_eq!(tasks.current_tasks().len(), 0); assert_eq!(tasks.current_tasks().len(), 0);
let t2 = tasks.make_task("t2"); let t2 = tasks.make_task("t2");
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.current_tasks().len(), 1);
assert_eq!(tasks.get_task_path(t2), "t1>t2");
assert_eq!(tasks.relative_path(t2.unwrap()), "t2");
let t3 = tasks.make_task("t3"); let t3 = tasks.make_task("t3");
assert_eq!(tasks.current_tasks().len(), 2); assert_eq!(tasks.current_tasks().len(), 2);
@ -475,15 +423,12 @@ fn test_depth() {
assert_eq!(tasks.current_tasks().len(), 0); assert_eq!(tasks.current_tasks().len(), 0);
let t4 = tasks.make_task("t4"); let t4 = tasks.make_task("t4");
assert_eq!(tasks.current_tasks().len(), 1); 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");
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.current_tasks().len(), 1);
tasks.depth = -1; tasks.depth = -1;
assert_eq!(tasks.current_tasks().len(), 1); assert_eq!(tasks.current_tasks().len(), 1);
tasks.move_to(t1); tasks.move_to(t1);
assert_eq!(tasks.relative_path(t4.unwrap()), "t2>t4");
assert_eq!(tasks.current_tasks().len(), 2); assert_eq!(tasks.current_tasks().len(), 2);
tasks.depth = 2; tasks.depth = 2;
assert_eq!(tasks.current_tasks().len(), 3); assert_eq!(tasks.current_tasks().len(), 3);
@ -519,18 +464,8 @@ fn test_depth() {
let zero = EventId::all_zeros(); let zero = EventId::all_zeros();
assert_eq!(tasks.get_task_path(Some(zero)), zero.to_string()); 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),
"0000000000000000000000000000000000000000000000000000000000000000>test"
);
assert_eq!(tasks.relative_path(dangling.unwrap()), "test");
use itertools::Itertools; use itertools::Itertools;
assert_eq!("test toast".split(' ').collect_vec().len(), 3); assert_eq!("test toast".split(' ').collect_vec().len(), 3);
assert_eq!( assert_eq!("test toast".split_ascii_whitespace().collect_vec().len(), 2);
"test toast".split_ascii_whitespace().collect_vec().len(),
2
);
} }