Compare commits
No commits in common. "f240413e2a5935f0991e5f2a7b7d36e7b37fb2c6" and "9da41db42708697eadcf40659045e99a44b8fa2e" have entirely different histories.
f240413e2a
...
9da41db427
|
@ -1,7 +1,6 @@
|
|||
/target
|
||||
/examples
|
||||
|
||||
/.idea
|
||||
relays
|
||||
keys
|
||||
*.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 log::{debug, error, info, trace, warn};
|
||||
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()) }
|
||||
}
|
||||
|
||||
/// Parses the hour from a plain number in the String,
|
||||
/// with max of max_future hours into the future.
|
||||
pub fn parse_hour(str: &str, max_future: i64) -> Option<DateTime<Local>> {
|
||||
str.parse::<u32>().ok().and_then(|hour| {
|
||||
let now = Local::now();
|
||||
#[allow(deprecated)]
|
||||
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 prompt(prompt: &str) -> Option<String> {
|
||||
print!("{} ", prompt);
|
||||
stdout().flush().unwrap();
|
||||
match stdin().lines().next() {
|
||||
Some(Ok(line)) => Some(line),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// Length may vary.
|
||||
pub fn format_datetime_relative(time: DateTime<Local>) -> String {
|
||||
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(),
|
||||
-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,
|
||||
{
|
||||
// For use in format strings but not possible, so need global find-replace
|
||||
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
|
||||
/// Format nostr Timestamp relative to local time
|
||||
/// with optional day specifier or full date depending on distance to today
|
||||
pub fn relative_datetimestamp(stamp: &Timestamp) -> String {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format nostr Timestamp relative to local time
|
||||
/// with optional day specifier or full date depending on distance to today.
|
||||
pub fn format_timestamp_relative(stamp: &Timestamp) -> String {
|
||||
format_as_datetime(stamp, format_datetime_relative)
|
||||
/// Format a nostr timestamp in a sensible comprehensive format
|
||||
pub fn local_datetimestamp(stamp: &Timestamp) -> String {
|
||||
format_stamp(stamp, "%y-%m-%d %a %H:%M")
|
||||
}
|
||||
|
||||
/// Format nostr timestamp with the given format.
|
||||
pub fn format_timestamp(stamp: &Timestamp, format: &str) -> String {
|
||||
format_as_datetime(stamp, |time| time.format(format).to_string())
|
||||
}
|
||||
|
||||
/// 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),
|
||||
/// Format a nostr timestamp with the given format
|
||||
pub fn format_stamp(stamp: &Timestamp, format: &str) -> String {
|
||||
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
|
||||
Single(time) => time.format(format).to_string(),
|
||||
_ => stamp.to_human_datetime(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
132
src/main.rs
132
src/main.rs
|
@ -3,14 +3,13 @@ use std::collections::{HashMap, VecDeque};
|
|||
use std::env::{args, var};
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::iter::once;
|
||||
use std::ops::Sub;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Local;
|
||||
use colored::Colorize;
|
||||
use env_logger::{Builder, Target, WriteStyle};
|
||||
use itertools::Itertools;
|
||||
|
@ -28,7 +27,7 @@ use xdg::BaseDirectories;
|
|||
|
||||
use crate::helpers::*;
|
||||
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};
|
||||
|
||||
mod helpers;
|
||||
|
@ -38,7 +37,6 @@ mod kinds;
|
|||
|
||||
const UNDO_DELAY: u64 = 60;
|
||||
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
|
||||
macro_rules! or_warn {
|
||||
|
@ -141,8 +139,7 @@ pub(crate) enum MostrMessage {
|
|||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// TODO preserve prompt lines
|
||||
async fn main() {
|
||||
let mut rl = Editor::new();
|
||||
|
||||
let mut args = args().skip(1).peekable();
|
||||
|
@ -156,7 +153,7 @@ async fn main() -> Result<()> {
|
|||
} else {
|
||||
let mut builder = colog::default_builder();
|
||||
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
|
||||
}.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 relayfile = config_dir.join("relays");
|
||||
|
||||
let keys = if let Ok(Ok(key)) = fs::read_to_string(&keysfile).map(|s| Keys::from_str(&s)) {
|
||||
key
|
||||
} else {
|
||||
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 = if line.is_empty() {
|
||||
info!("Generating and persisting new key");
|
||||
Keys::generate()
|
||||
} else {
|
||||
Keys::from_str(&line).inspect_err(|_| eprintln!())?
|
||||
};
|
||||
let mut file = match File::create_new(&keysfile) {
|
||||
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 keys = match fs::read_to_string(&keysfile).map(|s| Keys::from_str(&s)) {
|
||||
Ok(Ok(key)) => key,
|
||||
_ => {
|
||||
warn!("Could not read keys from {}", keysfile.to_string_lossy());
|
||||
let keys = prompt("Secret key?")
|
||||
.and_then(|s| or_warn!(Keys::from_str(&s)))
|
||||
.unwrap_or_else(|| {
|
||||
info!("Generating and persisting new key");
|
||||
Keys::generate()
|
||||
});
|
||||
or_warn!(fs::write(&keysfile, keys.secret_key().unwrap().to_string()));
|
||||
keys
|
||||
}
|
||||
};
|
||||
|
||||
let client = Client::new(&keys);
|
||||
|
@ -209,7 +194,7 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
Err(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("://") {
|
||||
line
|
||||
} else {
|
||||
|
@ -238,20 +223,18 @@ async fn main() -> Result<()> {
|
|||
], None).await;
|
||||
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 tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys, metadata.clone());
|
||||
let mut relays: HashMap<Option<Url>, Tasks> =
|
||||
client.relays().await.into_keys().map(|url| (Some(url.clone()), tasks_for_url(Some(url)))).collect();
|
||||
let tasks_for_url = |url: Option<Url>| Tasks::from(url, &tx, &keys);
|
||||
let mut relays: HashMap<Url, Tasks> =
|
||||
client.relays().await.into_keys().map(|url| (url.clone(), tasks_for_url(Some(url)))).collect();
|
||||
|
||||
let sender = tokio::spawn(async move {
|
||||
let mut queue: Option<(Url, Vec<Event>)> = None;
|
||||
|
||||
if let Some(meta) = myMeta.as_ref() {
|
||||
or_warn!(client.set_metadata(meta).await, "Unable to set metadata");
|
||||
if let Ok(user) = var("USER") {
|
||||
let metadata = Metadata::new()
|
||||
.name(user);
|
||||
or_warn!(client.set_metadata(&metadata).await);
|
||||
}
|
||||
|
||||
loop {
|
||||
|
@ -303,15 +286,11 @@ async fn main() -> Result<()> {
|
|||
info!("Shutting down nostr communication thread");
|
||||
});
|
||||
|
||||
if relays.is_empty() {
|
||||
relays.insert(None, tasks_for_url(None));
|
||||
}
|
||||
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 mut local_tasks = Tasks::from(None, &tx, &keys);
|
||||
let mut selected_relay: Option<Url> = relays.keys().nth(0).cloned();
|
||||
|
||||
{
|
||||
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 {
|
||||
tasks.make_task(&argument);
|
||||
}
|
||||
|
@ -319,14 +298,12 @@ async fn main() -> Result<()> {
|
|||
|
||||
loop {
|
||||
trace!("All Root Tasks:\n{}", relays.iter().map(|(url, tasks)|
|
||||
format!("{}: [{}]",
|
||||
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"));
|
||||
format!("{}: [{}]", url, tasks.children_of(None).map(|id| tasks.get_task_title(id)).join("; "))).join("\n"));
|
||||
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!(
|
||||
"{} {}{}) ",
|
||||
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_prompt_suffix().italic(),
|
||||
);
|
||||
|
@ -344,7 +321,7 @@ async fn main() -> Result<()> {
|
|||
"At {} found {} kind {} content \"{}\" tags {:?}",
|
||||
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),
|
||||
None => warn!("Event received from unknown relay {relay_url}: {:?}", *event)
|
||||
}
|
||||
|
@ -363,7 +340,7 @@ async fn main() -> Result<()> {
|
|||
None
|
||||
};
|
||||
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 {
|
||||
None => {
|
||||
debug!("Flushing Tasks because of empty command");
|
||||
|
@ -401,7 +378,7 @@ async fn main() -> Result<()> {
|
|||
None => {
|
||||
tasks.get_current_task().map_or_else(
|
||||
|| 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;
|
||||
}
|
||||
|
@ -426,7 +403,7 @@ async fn main() -> Result<()> {
|
|||
match arg {
|
||||
None => {
|
||||
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.filtered_tasks(tasks.get_position_ref())
|
||||
.filter(|t| t.event.created_at > today)
|
||||
|
@ -454,18 +431,15 @@ async fn main() -> Result<()> {
|
|||
.collect()
|
||||
)
|
||||
} else {
|
||||
parse_hour(arg, 1)
|
||||
.or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local)))
|
||||
.map(|time| {
|
||||
info!("Filtering for tasks created after {}", format_datetime_relative(time));
|
||||
let threshold = time.to_utc().timestamp();
|
||||
tasks.set_filter(
|
||||
tasks.filtered_tasks(tasks.get_position_ref())
|
||||
.filter(|t| t.event.created_at.as_u64() as i64 > threshold)
|
||||
.map(|t| t.event.id)
|
||||
.collect()
|
||||
);
|
||||
});
|
||||
parse_date(arg).map(|time| {
|
||||
info!("Filtering for tasks from {}", time); // TODO localize
|
||||
tasks.set_filter(
|
||||
tasks.filtered_tasks(tasks.get_position_ref())
|
||||
.filter(|t| t.event.created_at.as_u64() as i64 > time.timestamp())
|
||||
.map(|t| t.event.id)
|
||||
.collect()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -546,7 +520,6 @@ async fn main() -> Result<()> {
|
|||
let (label, times) = tasks.times_tracked();
|
||||
println!("{}\n{}", label.italic(), times.rev().take(15).join("\n"));
|
||||
}
|
||||
// TODO show history from author / pubkey
|
||||
} else {
|
||||
let (label, mut times) = tasks.times_tracked();
|
||||
println!("{}\n{}", label.italic(), times.join("\n"));
|
||||
|
@ -560,7 +533,7 @@ async fn main() -> Result<()> {
|
|||
Some(arg) => {
|
||||
if parse_tracking_stamp(arg).map(|stamp| tasks.track_at(stamp, None)).is_none() {
|
||||
// So the error message is not covered up
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -618,9 +591,9 @@ async fn main() -> Result<()> {
|
|||
|
||||
let filtered = tasks.filtered_tasks(pos)
|
||||
.filter(|t| {
|
||||
transform(&t.event.content).contains(slice) ||
|
||||
t.tags.iter().flatten().any(
|
||||
|tag| tag.content().is_some_and(|s| transform(s).contains(slice)))
|
||||
transform(&t.event.content).contains(slice) || t.tags.iter().flatten().any(|tag|
|
||||
tag.content().is_some_and(|s| transform(s).contains(slice))
|
||||
)
|
||||
})
|
||||
.map(|t| t.event.id)
|
||||
.collect_vec();
|
||||
|
@ -636,8 +609,8 @@ async fn main() -> Result<()> {
|
|||
_ =>
|
||||
if Regex::new("^wss?://").unwrap().is_match(&input.trim()) {
|
||||
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))) {
|
||||
selected_relay = url.clone();
|
||||
if let Some((url, tasks)) = relays.iter().find(|(key, _)| key.as_str().starts_with(&input)) {
|
||||
selected_relay = Some(url.clone());
|
||||
or_warn!(tasks.print_tasks());
|
||||
continue;
|
||||
}
|
||||
|
@ -647,7 +620,7 @@ async fn main() -> Result<()> {
|
|||
Ok(_) => {
|
||||
info!("Connecting to {url}");
|
||||
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!();
|
||||
|
||||
drop(tx);
|
||||
drop(local_tasks);
|
||||
drop(relays);
|
||||
|
||||
info!("Submitting pending updates...");
|
||||
or_warn!(sender.await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use itertools::Itertools;
|
|||
use log::{debug, error, info, trace, warn};
|
||||
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};
|
||||
|
||||
pub static MARKER_PARENT: &str = "parent";
|
||||
|
@ -156,7 +156,7 @@ impl Task {
|
|||
"parentid" => self.parent_id().map(|i| i.to_string()),
|
||||
"name" => Some(self.event.content.clone()),
|
||||
"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()),
|
||||
// Dynamic
|
||||
"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::time::Duration;
|
||||
|
||||
use chrono::Local;
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
|
@ -14,7 +15,7 @@ use nostr_sdk::prelude::Marker;
|
|||
use TagStandard::Hashtag;
|
||||
|
||||
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::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState};
|
||||
|
||||
|
@ -103,15 +104,13 @@ impl Display for StateFilter {
|
|||
}
|
||||
|
||||
impl Tasks {
|
||||
pub(crate) fn from(url: Option<Url>, tx: &tokio::sync::mpsc::Sender<MostrMessage>, keys: &Keys, metadata: Option<Metadata>) -> Self {
|
||||
let mut new = Self::with_sender(EventSender {
|
||||
pub(crate) fn from(url: Option<Url>, tx: &tokio::sync::mpsc::Sender<MostrMessage>, keys: &Keys) -> Self {
|
||||
Self::with_sender(EventSender {
|
||||
url,
|
||||
tx: tx.clone(),
|
||||
keys: keys.clone(),
|
||||
queue: Default::default(),
|
||||
});
|
||||
metadata.map(|m| new.users.insert(keys.public_key(), m));
|
||||
new
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_sender(sender: EventSender) -> Self {
|
||||
|
@ -190,7 +189,7 @@ impl Tasks {
|
|||
.join(" "));
|
||||
if new != last {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
@ -207,13 +206,18 @@ impl Tasks {
|
|||
let mut iter = timestamps(set.iter(), &ids).tuples();
|
||||
while let Some(((start, _), (end, _))) = iter.next() {
|
||||
vec.push(format!("{} - {} by {}",
|
||||
format_timestamp_local(start),
|
||||
format_timestamp_relative_to(end, start),
|
||||
local_datetimestamp(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)))
|
||||
}
|
||||
iter.into_buffer()
|
||||
.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
|
||||
}).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))
|
||||
|
@ -396,7 +400,7 @@ impl Tasks {
|
|||
let mut tracking_stamp: Option<Timestamp> = None;
|
||||
for elem in
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
|
@ -405,10 +409,10 @@ impl Tasks {
|
|||
writeln!(
|
||||
lock,
|
||||
"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,
|
||||
state.get_label(),
|
||||
format_timestamp_relative(&state.time)
|
||||
relative_datetimestamp(&state.time)
|
||||
)?;
|
||||
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 {
|
||||
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(
|
||||
build_tracking(task)
|
||||
.custom_created_at(time)
|
||||
|
|
Loading…
Reference in New Issue