Compare commits

..

No commits in common. "f240413e2a5935f0991e5f2a7b7d36e7b37fb2c6" and "9da41db42708697eadcf40659045e99a44b8fa2e" have entirely different histories.

5 changed files with 112 additions and 162 deletions

1
.gitignore vendored
View File

@ -1,7 +1,6 @@
/target /target
/examples /examples
/.idea
relays relays
keys keys
*.html *.html

View File

@ -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
/// 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) => {
let date = time.date_naive(); let date = time.date_naive();
let prefix = let prefix = match Local::now()
match Local::now()
.date_naive() .date_naive()
.signed_duration_since(date) .signed_duration_since(date)
.num_days() { .num_days()
{
-1 => "tomorrow ".into(), -1 => "tomorrow ".into(),
0 => "".into(), 0 => "".into(),
1 => "yesterday ".into(), 1 => "yesterday ".into(),
-3..=3 => date.format("%a ").to_string(), 2..=6 => date.format("last %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(), _ => date.format("%y-%m-%d ").to_string(),
}; };
format!("{}{}", prefix, time.format("%H:%M")) 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) {
Single(time) => formatter(time),
_ => 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),
} }
} }

View File

@ -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();
@ -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)))
.unwrap_or_else(|| {
info!("Generating and persisting new key"); info!("Generating and persisting new key");
Keys::generate() Keys::generate()
} else { });
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) {
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 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,14 +431,11 @@ 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| {
info!("Filtering for tasks created after {}", format_datetime_relative(time));
let threshold = time.to_utc().timestamp();
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.as_u64() as i64 > threshold) .filter(|t| t.event.created_at.as_u64() as i64 > time.timestamp())
.map(|t| t.event.id) .map(|t| t.event.id)
.collect() .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(())
} }

View File

@ -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()),

View File

@ -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)