diff --git a/src/helpers.rs b/src/helpers.rs index 99792e6..06ae77f 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -57,41 +57,60 @@ pub fn parse_tracking_stamp(str: &str) -> Option { }) } -// 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 { +/// Format DateTime easily comprehensible for human but unambiguous. +/// Length may vary. +pub fn format_datetime_relative(time: DateTime) -> 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(stamp: &Timestamp, formatter: F) -> String +where + F: Fn(DateTime) -> String, +{ match Local.timestamp_opt(stamp.as_u64() as i64, 0) { - 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")) - } + Single(time) => formatter(time), _ => stamp.to_human_datetime(), } } -/// 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 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 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(), +/// 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), } -} - +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 0a31653..49070b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -392,7 +392,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!("{} {}", local_datetimestamp(&e.created_at), e.content)).join("\n")), + |task| println!("{}", task.description_events().map(|e| format!("{} {}", format_timestamp_local(&e.created_at), e.content)).join("\n")), ); continue; } @@ -448,10 +448,11 @@ async fn main() -> Result<()> { parse_hour(arg, 1) .or_else(|| parse_date(arg).map(|utc| utc.with_timezone(&Local))) .map(|time| { - info!("Filtering for tasks created after {}", 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 > time.to_utc().timestamp()) + .filter(|t| t.event.created_at.as_u64() as i64 > threshold) .map(|t| t.event.id) .collect() ); diff --git a/src/task.rs b/src/task.rs index 2572148..dd6ff93 100644 --- a/src/task.rs +++ b/src/task.rs @@ -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::{local_datetimestamp, some_non_empty}; +use crate::helpers::{format_timestamp_local, 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(local_datetimestamp(&self.event.created_at)), + "created" => Some(format_timestamp_local(&self.event.created_at)), "kind" => Some(self.event.kind.to_string()), // Dynamic "status" => self.state_label().map(|c| c.to_string()), diff --git a/src/tasks.rs b/src/tasks.rs index fd0ad74..7e708ee 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -6,7 +6,6 @@ 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}; @@ -15,7 +14,7 @@ use nostr_sdk::prelude::Marker; use TagStandard::Hashtag; use crate::{EventSender, MostrMessage}; -use crate::helpers::{format_stamp, local_datetimestamp, parse_tracking_stamp, relative_datetimestamp, some_non_empty}; +use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty}; use crate::kinds::*; use crate::task::{MARKER_DEPENDS, MARKER_PARENT, State, Task, TaskState}; @@ -189,7 +188,7 @@ impl Tasks { .join(" ")); if new != last { // TODO alternate color with grey between days - full.push(format!("{:>15} {}", relative_datetimestamp(&event.created_at), new.as_ref().unwrap_or(&"---".to_string()))); + full.push(format!("{} {}", format_timestamp_local(&event.created_at), new.as_ref().unwrap_or(&"---".to_string()))); last = new; } } @@ -206,18 +205,13 @@ impl Tasks { let mut iter = timestamps(set.iter(), &ids).tuples(); while let Some(((start, _), (end, _))) = iter.next() { vec.push(format!("{} - {} by {}", - 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") - }, + format_timestamp_local(start), + format_timestamp_relative_to(end, start), self.get_author(key))) } iter.into_buffer() .for_each(|(stamp, _)| - vec.push(format!("{} started by {}", local_datetimestamp(stamp), self.get_author(key)))); + vec.push(format!("{} started by {}", format_timestamp_local(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)) @@ -409,10 +403,10 @@ impl Tasks { writeln!( lock, "Tracking since {} (total tracked time {}m) - {} since {}", - tracking_stamp.map_or("?".to_string(), |t| relative_datetimestamp(&t)), + tracking_stamp.map_or("?".to_string(), |t| format_timestamp_relative(&t)), self.time_tracked(*t.get_id()) / 60, state.get_label(), - relative_datetimestamp(&state.time) + format_timestamp_relative(&state.time) )?; writeln!(lock, "{}", t.descriptions().join("\n"))?; } @@ -736,7 +730,7 @@ impl Tasks { } pub(crate) fn track_at(&mut self, time: Timestamp, task: Option) -> EventId { - info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), relative_datetimestamp(&time)); + info!("{} from {}", task.map_or(String::from("Stopping time-tracking"), |id| format!("Tracking \"{}\"", self.get_task_title(&id))), format_timestamp_relative(&time)); self.submit( build_tracking(task) .custom_created_at(time)