Compare commits
No commits in common. "f240413e2a5935f0991e5f2a7b7d36e7b37fb2c6" and "9da41db42708697eadcf40659045e99a44b8fa2e" have entirely different histories.
f240413e2a
...
9da41db427
|
@ -1,7 +1,6 @@
|
||||||
/target
|
/target
|
||||||
/examples
|
/examples
|
||||||
|
|
||||||
/.idea
|
|
||||||
relays
|
relays
|
||||||
keys
|
keys
|
||||||
*.html
|
*.html
|
105
src/helpers.rs
105
src/helpers.rs
|
@ -1,6 +1,7 @@
|
||||||
use std::ops::Sub;
|
use std::fmt::Display;
|
||||||
|
use std::io::{stdin, stdout, Write};
|
||||||
|
|
||||||
use chrono::{DateTime, Local, TimeDelta, TimeZone, Utc};
|
use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
|
||||||
use chrono::LocalResult::Single;
|
use chrono::LocalResult::Single;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::Timestamp;
|
use nostr_sdk::Timestamp;
|
||||||
|
@ -9,20 +10,13 @@ pub fn some_non_empty(str: &str) -> Option<String> {
|
||||||
if str.is_empty() { None } else { Some(str.to_string()) }
|
if str.is_empty() { None } else { Some(str.to_string()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the hour from a plain number in the String,
|
pub fn prompt(prompt: &str) -> Option<String> {
|
||||||
/// with max of max_future hours into the future.
|
print!("{} ", prompt);
|
||||||
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
stdout().flush().unwrap();
|
||||||
str.parse::<u32>().ok().and_then(|hour| {
|
match stdin().lines().next() {
|
||||||
let now = Local::now();
|
Some(Ok(line)) => Some(line),
|
||||||
#[allow(deprecated)]
|
_ => None,
|
||||||
now.date().and_hms_opt(hour, 0, 0).map(|time| {
|
}
|
||||||
if time - now > TimeDelta::hours(max_future) {
|
|
||||||
time.sub(TimeDelta::days(1))
|
|
||||||
} else {
|
|
||||||
time
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
|
pub fn parse_date(str: &str) -> Option<DateTime<Utc>> {
|
||||||
|
@ -56,60 +50,41 @@ pub fn parse_tracking_stamp(str: &str) -> Option<Timestamp> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format DateTime easily comprehensible for human but unambiguous.
|
// For use in format strings but not possible, so need global find-replace
|
||||||
/// Length may vary.
|
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
|
||||||
pub fn format_datetime_relative(time: DateTime<Local>) -> String {
|
/// Format nostr Timestamp relative to local time
|
||||||
let date = time.date_naive();
|
/// with optional day specifier or full date depending on distance to today
|
||||||
let prefix =
|
pub fn relative_datetimestamp(stamp: &Timestamp) -> String {
|
||||||
match Local::now()
|
|
||||||
.date_naive()
|
|
||||||
.signed_duration_since(date)
|
|
||||||
.num_days() {
|
|
||||||
-1 => "tomorrow ".into(),
|
|
||||||
0 => "".into(),
|
|
||||||
1 => "yesterday ".into(),
|
|
||||||
-3..=3 => date.format("%a ").to_string(),
|
|
||||||
//-10..=10 => date.format("%d. %a ").to_string(),
|
|
||||||
-100..=100 => date.format("%b %d ").to_string(),
|
|
||||||
_ => date.format("%y-%m-%d ").to_string(),
|
|
||||||
};
|
|
||||||
format!("{}{}", prefix, time.format("%H:%M"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a nostr timestamp with the given formatting function.
|
|
||||||
pub fn format_as_datetime<F>(stamp: &Timestamp, formatter: F) -> String
|
|
||||||
where
|
|
||||||
F: Fn(DateTime<Local>) -> String,
|
|
||||||
{
|
|
||||||
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
|
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
|
||||||
Single(time) => formatter(time),
|
Single(time) => {
|
||||||
|
let date = time.date_naive();
|
||||||
|
let prefix = match Local::now()
|
||||||
|
.date_naive()
|
||||||
|
.signed_duration_since(date)
|
||||||
|
.num_days()
|
||||||
|
{
|
||||||
|
-1 => "tomorrow ".into(),
|
||||||
|
0 => "".into(),
|
||||||
|
1 => "yesterday ".into(),
|
||||||
|
2..=6 => date.format("last %a ").to_string(),
|
||||||
|
_ => date.format("%y-%m-%d ").to_string(),
|
||||||
|
};
|
||||||
|
format!("{}{}", prefix, time.format("%H:%M"))
|
||||||
|
}
|
||||||
_ => stamp.to_human_datetime(),
|
_ => stamp.to_human_datetime(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format nostr Timestamp relative to local time
|
/// Format a nostr timestamp in a sensible comprehensive format
|
||||||
/// with optional day specifier or full date depending on distance to today.
|
pub fn local_datetimestamp(stamp: &Timestamp) -> String {
|
||||||
pub fn format_timestamp_relative(stamp: &Timestamp) -> String {
|
format_stamp(stamp, "%y-%m-%d %a %H:%M")
|
||||||
format_as_datetime(stamp, format_datetime_relative)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format nostr timestamp with the given format.
|
/// Format a nostr timestamp with the given format
|
||||||
pub fn format_timestamp(stamp: &Timestamp, format: &str) -> String {
|
pub fn format_stamp(stamp: &Timestamp, format: &str) -> String {
|
||||||
format_as_datetime(stamp, |time| time.format(format).to_string())
|
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
|
||||||
}
|
Single(time) => time.format(format).to_string(),
|
||||||
|
_ => stamp.to_human_datetime(),
|
||||||
/// Format nostr timestamp in a sensible comprehensive format with consistent length and consistent sorting.
|
|
||||||
///
|
|
||||||
/// Currently: 18 characters
|
|
||||||
pub fn format_timestamp_local(stamp: &Timestamp) -> String {
|
|
||||||
format_timestamp(stamp, "%y-%m-%d %a %H:%M")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_timestamp_relative_to(stamp: &Timestamp, reference: &Timestamp) -> String {
|
|
||||||
// Rough difference in days
|
|
||||||
match (stamp.as_u64() as i64 - reference.as_u64() as i64) / 80_000 {
|
|
||||||
0 => format_timestamp(stamp, "%H:%M"),
|
|
||||||
-3..=3 => format_timestamp(stamp, "%a %H:%M"),
|
|
||||||
_ => format_timestamp_local(stamp),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
132
src/main.rs
132
src/main.rs
|
@ -3,14 +3,13 @@ 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;
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::ops::Sub;
|
use std::ops::Sub;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::Local;
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use env_logger::{Builder, Target, WriteStyle};
|
use env_logger::{Builder, Target, WriteStyle};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -28,7 +27,7 @@ use xdg::BaseDirectories;
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{KINDS, PROP_KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
use crate::kinds::{KINDS, PROP_KINDS, PROPERTY_COLUMNS, TRACKING_KIND};
|
||||||
use crate::task::{MARKER_DEPENDS, State};
|
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State};
|
||||||
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
use crate::tasks::{PropertyCollection, StateFilter, Tasks};
|
||||||
|
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
@ -38,7 +37,6 @@ mod kinds;
|
||||||
|
|
||||||
const UNDO_DELAY: u64 = 60;
|
const UNDO_DELAY: u64 = 60;
|
||||||
const INACTVITY_DELAY: u64 = 200;
|
const INACTVITY_DELAY: u64 = 200;
|
||||||
const LOCAL_RELAY_NAME: &str = "TEMP";
|
|
||||||
|
|
||||||
/// Turn a Result into an Option, showing a warning on error with optional prefix
|
/// Turn a Result into an Option, showing a warning on error with optional prefix
|
||||||
macro_rules! or_warn {
|
macro_rules! or_warn {
|
||||||
|
@ -141,8 +139,7 @@ pub(crate) enum MostrMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() {
|
||||||
// TODO preserve prompt lines
|
|
||||||
let mut rl = Editor::new();
|
let mut rl = Editor::new();
|
||||||
|
|
||||||
let mut args = args().skip(1).peekable();
|
let mut args = args().skip(1).peekable();
|
||||||
|
@ -156,7 +153,7 @@ async fn main() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
let mut builder = colog::default_builder();
|
let mut builder = colog::default_builder();
|
||||||
builder.filter(Some("nostr-relay-pool"), LevelFilter::Error);
|
builder.filter(Some("nostr-relay-pool"), LevelFilter::Error);
|
||||||
//.filter(Some("nostr-relay-pool::relay::internal"), LevelFilter::Off)
|
//.filter(Some("nostr-relay-pool::relay::internal"), LevelFilter::Off)
|
||||||
builder
|
builder
|
||||||
}.write_style(WriteStyle::Always).target(Target::Pipe(Box::new(rl.get_printer()))).init();
|
}.write_style(WriteStyle::Always).target(Target::Pipe(Box::new(rl.get_printer()))).init();
|
||||||
|
|
||||||
|
@ -166,31 +163,19 @@ async fn main() -> Result<()> {
|
||||||
let keysfile = config_dir.join("key");
|
let keysfile = config_dir.join("key");
|
||||||
let relayfile = config_dir.join("relays");
|
let relayfile = config_dir.join("relays");
|
||||||
|
|
||||||
let keys = if let Ok(Ok(key)) = 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)) {
|
||||||
key
|
Ok(Ok(key)) => key,
|
||||||
} else {
|
_ => {
|
||||||
warn!("Could not read keys from {}", keysfile.to_string_lossy());
|
warn!("Could not read keys from {}", keysfile.to_string_lossy());
|
||||||
let line = rl.readline("Secret key? (leave blank to generate and save a new keypair) ")?;
|
let keys = prompt("Secret key?")
|
||||||
let keys = if line.is_empty() {
|
.and_then(|s| or_warn!(Keys::from_str(&s)))
|
||||||
info!("Generating and persisting new key");
|
.unwrap_or_else(|| {
|
||||||
Keys::generate()
|
info!("Generating and persisting new key");
|
||||||
} else {
|
Keys::generate()
|
||||||
Keys::from_str(&line).inspect_err(|_| eprintln!())?
|
});
|
||||||
};
|
or_warn!(fs::write(&keysfile, keys.secret_key().unwrap().to_string()));
|
||||||
let mut file = match File::create_new(&keysfile) {
|
keys
|
||||||
Ok(file) => file,
|
}
|
||||||
Err(e) => {
|
|
||||||
let line = rl.readline(&format!("Overwrite {}? (enter anything to abort) ", keysfile.to_string_lossy()))?;
|
|
||||||
if line.is_empty() {
|
|
||||||
File::create(&keysfile)?
|
|
||||||
} else {
|
|
||||||
eprintln!();
|
|
||||||
Err(e)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
file.write_all(keys.secret_key().unwrap().to_string().as_bytes())?;
|
|
||||||
keys
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = Client::new(&keys);
|
let client = Client::new(&keys);
|
||||||
|
@ -209,7 +194,7 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not read relays file: {}", e);
|
warn!("Could not read relays file: {}", e);
|
||||||
if let Ok(line) = rl.readline("Relay? ") {
|
if let Some(line) = prompt("Relay?") {
|
||||||
let url = if line.contains("://") {
|
let url = if line.contains("://") {
|
||||||
line
|
line
|
||||||
} else {
|
} else {
|
||||||
|
@ -238,20 +223,18 @@ async fn main() -> Result<()> {
|
||||||
], None).await;
|
], None).await;
|
||||||
info!("Subscribed to updates with {:?}", sub2);
|
info!("Subscribed to updates with {:?}", sub2);
|
||||||
|
|
||||||
let metadata = var("USER").ok().map(
|
|
||||||
|user| Metadata::new().name(user));
|
|
||||||
let myMeta = metadata.clone();
|
|
||||||
|
|
||||||
let (tx, mut rx) = mpsc::channel::<MostrMessage>(64);
|
let (tx, mut rx) = mpsc::channel::<MostrMessage>(64);
|
||||||
let tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys, metadata.clone());
|
let tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys);
|
||||||
let mut relays: HashMap<Option<Url>, Tasks> =
|
let mut relays: HashMap<Url, Tasks> =
|
||||||
client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect();
|
client.relays().await.into_keys().map(|url| (url.clone(), tasks_for_url(Some(url)))).collect();
|
||||||
|
|
||||||
let sender = tokio::spawn(async move {
|
let sender = tokio::spawn(async move {
|
||||||
let mut queue: Option<(Url, Vec<Event>)> = None;
|
let mut queue: Option<(Url, Vec<Event>)> = None;
|
||||||
|
|
||||||
if let Some(meta) = myMeta.as_ref() {
|
if let Ok(user) = var("USER") {
|
||||||
or_warn!(client.set_metadata(meta).await, "Unable to set metadata");
|
let metadata = Metadata::new()
|
||||||
|
.name(user);
|
||||||
|
or_warn!(client.set_metadata(&metadata).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -303,15 +286,11 @@ async fn main() -> Result<()> {
|
||||||
info!("Shutting down nostr communication thread");
|
info!("Shutting down nostr communication thread");
|
||||||
});
|
});
|
||||||
|
|
||||||
if relays.is_empty() {
|
let mut local_tasks = Tasks::from(None, &tx, &keys);
|
||||||
relays.insert(None, tasks_for_url(None));
|
let mut selected_relay: Option<Url> = relays.keys().nth(0).cloned();
|
||||||
}
|
|
||||||
let mut selected_relay: Option<Url> = relays.keys()
|
|
||||||
.find_or_first(|url| url.as_ref().is_some_and(|u| u.scheme() == "wss"))
|
|
||||||
.unwrap().clone();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let tasks = relays.get_mut(&selected_relay).unwrap();
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
|
||||||
for argument in args {
|
for argument in args {
|
||||||
tasks.make_task(&argument);
|
tasks.make_task(&argument);
|
||||||
}
|
}
|
||||||
|
@ -319,14 +298,12 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
trace!("All Root Tasks:\n{}", relays.iter().map(|(url, tasks)|
|
trace!("All Root Tasks:\n{}", relays.iter().map(|(url, tasks)|
|
||||||
format!("{}: [{}]",
|
format!("{}: [{}]", url, tasks.children_of(None).map(|id| tasks.get_task_title(id)).join("; "))).join("\n"));
|
||||||
url.as_ref().map(ToString::to_string).unwrap_or(LOCAL_RELAY_NAME.to_string()),
|
|
||||||
tasks.children_of(None).map(|id| tasks.get_task_title(id)).join("; "))).join("\n"));
|
|
||||||
println!();
|
println!();
|
||||||
let tasks = relays.get(&selected_relay).unwrap();
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get(url)).unwrap_or(&local_tasks);
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
"{} {}{}) ",
|
"{} {}{}) ",
|
||||||
selected_relay.as_ref().map_or(LOCAL_RELAY_NAME.to_string(), |url| url.to_string()).bright_black(),
|
selected_relay.as_ref().map_or("TEMP".to_string(), |url| url.to_string()).bright_black(),
|
||||||
tasks.get_task_path(tasks.get_position()).bold(),
|
tasks.get_task_path(tasks.get_position()).bold(),
|
||||||
tasks.get_prompt_suffix().italic(),
|
tasks.get_prompt_suffix().italic(),
|
||||||
);
|
);
|
||||||
|
@ -344,7 +321,7 @@ async fn main() -> Result<()> {
|
||||||
"At {} found {} kind {} content \"{}\" tags {:?}",
|
"At {} found {} kind {} content \"{}\" tags {:?}",
|
||||||
event.created_at, event.id, event.kind, event.content, event.tags.iter().map(|tag| tag.as_vec()).collect_vec()
|
event.created_at, event.id, event.kind, event.content, event.tags.iter().map(|tag| tag.as_vec()).collect_vec()
|
||||||
);
|
);
|
||||||
match relays.get_mut(&Some(relay_url.clone())) {
|
match relays.get_mut(&relay_url) {
|
||||||
Some(tasks) => tasks.add(*event),
|
Some(tasks) => tasks.add(*event),
|
||||||
None => warn!("Event received from unknown relay {relay_url}: {:?}", *event)
|
None => warn!("Event received from unknown relay {relay_url}: {:?}", *event)
|
||||||
}
|
}
|
||||||
|
@ -363,7 +340,7 @@ async fn main() -> Result<()> {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let arg_default = arg.unwrap_or("");
|
let arg_default = arg.unwrap_or("");
|
||||||
let tasks = relays.get_mut(&selected_relay).unwrap();
|
let tasks = selected_relay.as_ref().and_then(|url| relays.get_mut(&url)).unwrap_or_else(|| &mut local_tasks);
|
||||||
match op {
|
match op {
|
||||||
None => {
|
None => {
|
||||||
debug!("Flushing Tasks because of empty command");
|
debug!("Flushing Tasks because of empty command");
|
||||||
|
@ -401,7 +378,7 @@ async fn main() -> Result<()> {
|
||||||
None => {
|
None => {
|
||||||
tasks.get_current_task().map_or_else(
|
tasks.get_current_task().map_or_else(
|
||||||
|| info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
|
|| info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes"),
|
||||||
|task| println!("{}", task.description_events().map(|e| format!("{} {}", format_timestamp_local(&e.created_at), e.content)).join("\n")),
|
|task| println!("{}", task.description_events().map(|e| format!("{} {}", local_datetimestamp(&e.created_at), e.content)).join("\n")),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -426,7 +403,7 @@ async fn main() -> Result<()> {
|
||||||
match arg {
|
match arg {
|
||||||
None => {
|
None => {
|
||||||
let today = Timestamp::from(Timestamp::now() - 80_000);
|
let today = Timestamp::from(Timestamp::now() - 80_000);
|
||||||
info!("Filtering for tasks created in the last 22 hours");
|
info!("Filtering for tasks from the last 22 hours");
|
||||||
tasks.set_filter(
|
tasks.set_filter(
|
||||||
tasks.filtered_tasks(tasks.get_position_ref())
|
tasks.filtered_tasks(tasks.get_position_ref())
|
||||||
.filter(|t| t.event.created_at > today)
|
.filter(|t| t.event.created_at > today)
|
||||||
|
@ -454,18 +431,15 @@ async fn main() -> Result<()> {
|
||||||
.collect()
|
.collect()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
parse_hour(arg, 1)
|
parse_date(arg).map(|time| {
|
||||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
info!("Filtering for tasks from {}", time); // TODO localize
|
||||||
.map(|time| {
|
tasks.set_filter(
|
||||||
info!("Filtering for tasks created after {}", format_datetime_relative(time));
|
tasks.filtered_tasks(tasks.get_position_ref())
|
||||||
let threshold = time.to_utc().timestamp();
|
.filter(|t| t.event.created_at.as_u64() as i64 > time.timestamp())
|
||||||
tasks.set_filter(
|
.map(|t| t.event.id)
|
||||||
tasks.filtered_tasks(tasks.get_position_ref())
|
.collect()
|
||||||
.filter(|t| t.event.created_at.as_u64() as i64 > threshold)
|
);
|
||||||
.map(|t| t.event.id)
|
});
|
||||||
.collect()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -546,7 +520,6 @@ async fn main() -> Result<()> {
|
||||||
let (label, times) = tasks.times_tracked();
|
let (label, times) = tasks.times_tracked();
|
||||||
println!("{}\n{}", label.italic(), times.rev().take(15).join("\n"));
|
println!("{}\n{}", label.italic(), times.rev().take(15).join("\n"));
|
||||||
}
|
}
|
||||||
// TODO show history from author / pubkey
|
|
||||||
} else {
|
} else {
|
||||||
let (label, mut times) = tasks.times_tracked();
|
let (label, mut times) = tasks.times_tracked();
|
||||||
println!("{}\n{}", label.italic(), times.join("\n"));
|
println!("{}\n{}", label.italic(), times.join("\n"));
|
||||||
|
@ -560,7 +533,7 @@ async fn main() -> Result<()> {
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if parse_tracking_stamp(arg).map(|stamp| tasks.track_at(stamp, None)).is_none() {
|
if parse_tracking_stamp(arg).map(|stamp| tasks.track_at(stamp, None)).is_none() {
|
||||||
// So the error message is not covered up
|
// So the error message is not covered up
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -618,9 +591,9 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let filtered = tasks.filtered_tasks(pos)
|
let filtered = tasks.filtered_tasks(pos)
|
||||||
.filter(|t| {
|
.filter(|t| {
|
||||||
transform(&t.event.content).contains(slice) ||
|
transform(&t.event.content).contains(slice) || t.tags.iter().flatten().any(|tag|
|
||||||
t.tags.iter().flatten().any(
|
tag.content().is_some_and(|s| transform(s).contains(slice))
|
||||||
|tag| tag.content().is_some_and(|s| transform(s).contains(slice)))
|
)
|
||||||
})
|
})
|
||||||
.map(|t| t.event.id)
|
.map(|t| t.event.id)
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
@ -636,8 +609,8 @@ async fn main() -> Result<()> {
|
||||||
_ =>
|
_ =>
|
||||||
if Regex::new("^wss?://").unwrap().is_match(&input.trim()) {
|
if Regex::new("^wss?://").unwrap().is_match(&input.trim()) {
|
||||||
tasks.move_to(None);
|
tasks.move_to(None);
|
||||||
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_ref().is_some_and(|url| url.as_str().starts_with(&input))) {
|
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_str().starts_with(&input)) {
|
||||||
selected_relay = url.clone();
|
selected_relay = Some(url.clone());
|
||||||
or_warn!(tasks.print_tasks());
|
or_warn!(tasks.print_tasks());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -647,7 +620,7 @@ async fn main() -> Result<()> {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Connecting to {url}");
|
info!("Connecting to {url}");
|
||||||
selected_relay = Some(url.clone());
|
selected_relay = Some(url.clone());
|
||||||
relays.insert(selected_relay.clone(), tasks_for_url(selected_relay.clone()));
|
relays.insert(url.clone(), tasks_for_url(Some(url)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -666,10 +639,9 @@ async fn main() -> Result<()> {
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
drop(tx);
|
drop(tx);
|
||||||
|
drop(local_tasks);
|
||||||
drop(relays);
|
drop(relays);
|
||||||
|
|
||||||
info!("Submitting pending updates...");
|
info!("Submitting pending updates...");
|
||||||
or_warn!(sender.await);
|
or_warn!(sender.await);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
|
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
|
||||||
|
|
||||||
use crate::helpers::{format_timestamp_local, some_non_empty};
|
use crate::helpers::{local_datetimestamp, some_non_empty};
|
||||||
use crate::kinds::{is_hashtag, TASK_KIND};
|
use crate::kinds::{is_hashtag, TASK_KIND};
|
||||||
|
|
||||||
pub static MARKER_PARENT: &str = "parent";
|
pub static MARKER_PARENT: &str = "parent";
|
||||||
|
@ -156,7 +156,7 @@ impl Task {
|
||||||
"parentid" => self.parent_id().map(|i| i.to_string()),
|
"parentid" => self.parent_id().map(|i| i.to_string()),
|
||||||
"name" => Some(self.event.content.clone()),
|
"name" => Some(self.event.content.clone()),
|
||||||
"pubkey" => Some(self.event.pubkey.to_string()),
|
"pubkey" => Some(self.event.pubkey.to_string()),
|
||||||
"created" => Some(format_timestamp_local(&self.event.created_at)),
|
"created" => Some(local_datetimestamp(&self.event.created_at)),
|
||||||
"kind" => Some(self.event.kind.to_string()),
|
"kind" => Some(self.event.kind.to_string()),
|
||||||
// Dynamic
|
// Dynamic
|
||||||
"status" => self.state_label().map(|c| c.to_string()),
|
"status" => self.state_label().map(|c| c.to_string()),
|
||||||
|
|
32
src/tasks.rs
32
src/tasks.rs
|
@ -6,6 +6,7 @@ use std::ops::{Div, Rem};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
@ -14,7 +15,7 @@ use nostr_sdk::prelude::Marker;
|
||||||
use TagStandard::Hashtag;
|
use TagStandard::Hashtag;
|
||||||
|
|
||||||
use crate::{EventSender, MostrMessage};
|
use crate::{EventSender, MostrMessage};
|
||||||
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty};
|
use crate::helpers::{format_stamp, local_datetimestamp, parse_tracking_stamp, relative_datetimestamp, some_non_empty};
|
||||||
use crate::kinds::*;
|
use crate::kinds::*;
|
||||||
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
|
use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
|
||||||
|
|
||||||
|
@ -103,15 +104,13 @@ impl Display for StateFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
pub(crate) fn from(url: Option<Url>, tx: &tokio::sync::mpsc::Sender<MostrMessage>, keys: &Keys, metadata: Option<Metadata>) -> Self {
|
pub(crate) fn from(url: Option<Url>, tx: &tokio::sync::mpsc::Sender<MostrMessage>, keys: &Keys) -> Self {
|
||||||
let mut new = Self::with_sender(EventSender {
|
Self::with_sender(EventSender {
|
||||||
url,
|
url,
|
||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
keys: keys.clone(),
|
keys: keys.clone(),
|
||||||
queue: Default::default(),
|
queue: Default::default(),
|
||||||
});
|
})
|
||||||
metadata.map(|m| new.users.insert(keys.public_key(), m));
|
|
||||||
new
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_sender(sender: EventSender) -> Self {
|
pub(crate) fn with_sender(sender: EventSender) -> Self {
|
||||||
|
@ -190,7 +189,7 @@ impl Tasks {
|
||||||
.join(" "));
|
.join(" "));
|
||||||
if new != last {
|
if new != last {
|
||||||
// TODO alternate color with grey between days
|
// TODO alternate color with grey between days
|
||||||
full.push(format!("{} {}", format_timestamp_local(&event.created_at), new.as_ref().unwrap_or(&"---".to_string())));
|
full.push(format!("{:>15} {}", relative_datetimestamp(&event.created_at), new.as_ref().unwrap_or(&"---".to_string())));
|
||||||
last = new;
|
last = new;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,13 +206,18 @@ impl Tasks {
|
||||||
let mut iter = timestamps(set.iter(), &ids).tuples();
|
let mut iter = timestamps(set.iter(), &ids).tuples();
|
||||||
while let Some(((start, _), (end, _))) = iter.next() {
|
while let Some(((start, _), (end, _))) = iter.next() {
|
||||||
vec.push(format!("{} - {} by {}",
|
vec.push(format!("{} - {} by {}",
|
||||||
format_timestamp_local(start),
|
local_datetimestamp(start),
|
||||||
format_timestamp_relative_to(end, start),
|
// Only use full stamp when ambiguous (>1day)
|
||||||
|
if end.as_u64() - start.as_u64() > 80_000 {
|
||||||
|
local_datetimestamp(end)
|
||||||
|
} else {
|
||||||
|
format_stamp(end, "%H:%M")
|
||||||
|
},
|
||||||
self.get_author(key)))
|
self.get_author(key)))
|
||||||
}
|
}
|
||||||
iter.into_buffer()
|
iter.into_buffer()
|
||||||
.for_each(|(stamp, _)|
|
.for_each(|(stamp, _)|
|
||||||
vec.push(format!("{} started by {}", format_timestamp_local(stamp), self.get_author(key))));
|
vec.push(format!("{} started by {}", local_datetimestamp(stamp), self.get_author(key))));
|
||||||
vec
|
vec
|
||||||
}).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people
|
}).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people
|
||||||
(format!("Times Tracked on {:?}", self.get_task_title(&id)), Box::from(history))
|
(format!("Times Tracked on {:?}", self.get_task_title(&id)), Box::from(history))
|
||||||
|
@ -396,7 +400,7 @@ impl Tasks {
|
||||||
let mut tracking_stamp: Option<Timestamp> = None;
|
let mut tracking_stamp: Option<Timestamp> = None;
|
||||||
for elem in
|
for elem in
|
||||||
timestamps(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![t.get_id()])
|
timestamps(self.history.get(&self.sender.pubkey()).into_iter().flatten(), &vec![t.get_id()])
|
||||||
.map(|(e, _)| e) {
|
.map(|(e, o)| e) {
|
||||||
if tracking_stamp.is_some() && elem > now {
|
if tracking_stamp.is_some() && elem > now {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -405,10 +409,10 @@ impl Tasks {
|
||||||
writeln!(
|
writeln!(
|
||||||
lock,
|
lock,
|
||||||
"Tracking since {} (total tracked time {}m) - {} since {}",
|
"Tracking since {} (total tracked time {}m) - {} since {}",
|
||||||
tracking_stamp.map_or("?".to_string(), |t| format_timestamp_relative(&t)),
|
tracking_stamp.map_or("?".to_string(), |t| relative_datetimestamp(&t)),
|
||||||
self.time_tracked(*t.get_id()) / 60,
|
self.time_tracked(*t.get_id()) / 60,
|
||||||
state.get_label(),
|
state.get_label(),
|
||||||
format_timestamp_relative(&state.time)
|
relative_datetimestamp(&state.time)
|
||||||
)?;
|
)?;
|
||||||
writeln!(lock, "{}", t.descriptions().join("\n"))?;
|
writeln!(lock, "{}", t.descriptions().join("\n"))?;
|
||||||
}
|
}
|
||||||
|
@ -732,7 +736,7 @@ impl Tasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId {
|
pub(crate) fn track_at(&mut self, time: Timestamp, task: Option<EventId>) -> EventId {
|
||||||
info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), format_timestamp_relative(&time));
|
info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time));
|
||||||
self.submit(
|
self.submit(
|
||||||
build_tracking(task)
|
build_tracking(task)
|
||||||
.custom_created_at(time)
|
.custom_created_at(time)
|
||||||
|
|
Loading…
Reference in New Issue