From 823ada7d747bcef33fb8510373d397e8010fce28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 22:41:51 -0500 Subject: [PATCH] Extract shared display formatting into keep-core (#288) --- keep-core/src/display.rs | 95 ++++++++++++++++++++++++ keep-core/src/lib.rs | 2 + keep-desktop/src/screen/bunker.rs | 9 +-- keep-desktop/src/screen/mod.rs | 13 +--- keep-desktop/src/screen/signing_audit.rs | 9 +-- keep-desktop/src/screen/wallet.rs | 7 +- keep-mobile/src/keep_mobile.udl | 4 + keep-mobile/src/lib.rs | 12 +++ 8 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 keep-core/src/display.rs diff --git a/keep-core/src/display.rs b/keep-core/src/display.rs new file mode 100644 index 00000000..ffea4c12 --- /dev/null +++ b/keep-core/src/display.rs @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: © 2026 PrivKey LLC +// SPDX-License-Identifier: AGPL-3.0-or-later + +use chrono::{DateTime, Utc}; + +/// Truncate a string to `prefix` leading chars + `...` + `suffix` trailing chars. +/// +/// Returns the original string unchanged if it is short enough or not ASCII. +pub fn truncate_str(s: &str, prefix: usize, suffix: usize) -> String { + let guard = prefix + suffix + 3; + if !s.is_ascii() || s.len() <= guard { + return s.to_owned(); + } + format!("{}...{}", &s[..prefix], &s[s.len() - suffix..]) +} + +/// Format a Unix timestamp as `"2024-01-15 10:30 UTC"`. +pub fn format_timestamp(ts: i64) -> String { + DateTime::::from_timestamp(ts, 0) + .map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string()) + .unwrap_or_else(|| ts.to_string()) +} + +/// Format a Unix timestamp as `"Jan 15, 2024 10:30:00"` (detailed, no timezone label). +pub fn format_timestamp_detailed(ts: i64) -> String { + DateTime::::from_timestamp(ts, 0) + .map(|dt| dt.format("%b %d, %Y %H:%M:%S").to_string()) + .unwrap_or_else(|| ts.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn truncate_short_string_unchanged() { + assert_eq!(truncate_str("npub1abc", 12, 6), "npub1abc"); + } + + #[test] + fn truncate_non_ascii_unchanged() { + let s = "npub1こんにちは世界abc"; + assert_eq!(truncate_str(s, 12, 6), s); + } + + #[test] + fn truncate_at_guard_boundary_unchanged() { + // prefix=8, suffix=6, guard=17; string of length 17 should be unchanged + let s = "a".repeat(17); + assert_eq!(truncate_str(&s, 8, 6), s); + } + + #[test] + fn truncate_one_over_guard() { + let s = "a".repeat(18); + let result = truncate_str(&s, 8, 6); + assert_eq!(result, "aaaaaaaa...aaaaaa"); + assert_eq!(result.len(), 17); + } + + #[test] + fn truncate_npub_style() { + let npub = "npub1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbb"; + let result = truncate_str(npub, 12, 6); + assert_eq!(&result[..12], "npub1aaaaaaa"); + assert!(result.contains("...")); + assert_eq!(&result[result.len() - 6..], "bbbbbb"); + } + + #[test] + fn truncate_hex_style() { + let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; + let result = truncate_str(hex, 8, 6); + assert_eq!(&result[..8], "abcdef01"); + assert!(result.contains("...")); + assert_eq!(&result[result.len() - 6..], "456789"); + } + + #[test] + fn format_timestamp_valid() { + let result = format_timestamp(1705314600); + assert_eq!(result, "2024-01-15 10:30 UTC"); + } + + #[test] + fn format_timestamp_zero() { + assert_eq!(format_timestamp(0), "1970-01-01 00:00 UTC"); + } + + #[test] + fn format_timestamp_detailed_valid() { + let result = format_timestamp_detailed(1705314600); + assert_eq!(result, "Jan 15, 2024 10:30:00"); + } +} diff --git a/keep-core/src/lib.rs b/keep-core/src/lib.rs index 35a5a28a..a64bdf97 100644 --- a/keep-core/src/lib.rs +++ b/keep-core/src/lib.rs @@ -38,6 +38,8 @@ pub mod audit; pub mod backend; /// Cryptographic primitives for encryption, key derivation, and hashing. pub mod crypto; +/// Display formatting helpers for truncation and timestamps. +pub mod display; /// Multi-source entropy mixing for defense-in-depth randomness. pub mod entropy; /// Error types and result aliases. diff --git a/keep-desktop/src/screen/bunker.rs b/keep-desktop/src/screen/bunker.rs index b5f31a88..211c21d7 100644 --- a/keep-desktop/src/screen/bunker.rs +++ b/keep-desktop/src/screen/bunker.rs @@ -21,14 +21,7 @@ pub struct ConnectedClient { impl ConnectedClient { pub fn truncated_pubkey(&self) -> String { - if self.pubkey.len() <= 16 { - return self.pubkey.clone(); - } - format!( - "{}...{}", - &self.pubkey[..8], - &self.pubkey[self.pubkey.len() - 6..] - ) + keep_core::display::truncate_str(&self.pubkey, 8, 6) } pub fn permission_labels(&self) -> Vec<&'static str> { diff --git a/keep-desktop/src/screen/mod.rs b/keep-desktop/src/screen/mod.rs index 90a5c71c..5ba2af34 100644 --- a/keep-desktop/src/screen/mod.rs +++ b/keep-desktop/src/screen/mod.rs @@ -16,22 +16,13 @@ pub mod signing_audit; pub mod unlock; pub mod wallet; -use chrono::{DateTime, Utc}; - use crate::message::Message; use layout::{NavItem, SidebarState}; -pub fn format_timestamp(ts: i64) -> String { - DateTime::::from_timestamp(ts, 0) - .map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string()) - .unwrap_or_else(|| ts.to_string()) -} +pub use keep_core::display::format_timestamp; pub fn truncate_npub(npub: &str) -> String { - if !npub.is_ascii() || npub.len() <= 20 { - return npub.to_owned(); - } - format!("{}...{}", &npub[..12], &npub[npub.len() - 6..]) + keep_core::display::truncate_str(npub, 12, 6) } pub enum Screen { diff --git a/keep-desktop/src/screen/signing_audit.rs b/keep-desktop/src/screen/signing_audit.rs index ddbaeb7f..c73f7cba 100644 --- a/keep-desktop/src/screen/signing_audit.rs +++ b/keep-desktop/src/screen/signing_audit.rs @@ -263,9 +263,7 @@ impl SigningAuditScreen { ); } - let timestamp = chrono::DateTime::from_timestamp(entry.timestamp, 0) - .map(|dt| dt.format("%b %d, %Y %H:%M:%S").to_string()) - .unwrap_or_else(|| entry.timestamp.to_string()); + let timestamp = keep_core::display::format_timestamp_detailed(entry.timestamp); let time_text = text(timestamp) .size(theme::size::TINY) @@ -280,10 +278,7 @@ impl SigningAuditScreen { } fn truncate_hex(s: &str) -> String { - if s.len() <= 20 || !s.is_ascii() { - return s.to_string(); - } - format!("{}...{}", &s[..8], &s[s.len() - 6..]) + keep_core::display::truncate_str(s, 8, 6) } fn format_request_type(rt: &str) -> String { diff --git a/keep-desktop/src/screen/wallet.rs b/keep-desktop/src/screen/wallet.rs index d82de18f..a6dab7a7 100644 --- a/keep-desktop/src/screen/wallet.rs +++ b/keep-desktop/src/screen/wallet.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; -use chrono::{DateTime, Utc}; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; use iced::{Alignment, Element, Length}; use keep_frost_net::AnnouncedXpub; @@ -483,10 +482,8 @@ impl WalletScreen { .color(theme::color::TEXT_DIM); let created = i64::try_from(entry.created_at) - .ok() - .and_then(|ts| DateTime::::from_timestamp(ts, 0)) - .map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string()) - .unwrap_or_else(|| entry.created_at.to_string()); + .map(keep_core::display::format_timestamp) + .unwrap_or_else(|_| entry.created_at.to_string()); let created_text = text(format!("Created: {created}")) .size(theme::size::SMALL) .color(theme::color::TEXT_MUTED); diff --git a/keep-mobile/src/keep_mobile.udl b/keep-mobile/src/keep_mobile.udl index 37c9c813..11a8c480 100644 --- a/keep-mobile/src/keep_mobile.udl +++ b/keep-mobile/src/keep_mobile.udl @@ -4,6 +4,10 @@ namespace keep_mobile { [Throws=KeepMobileError] ParsedBunkerUrl parse_bunker_url(string url); + + string format_timestamp(i64 ts); + string format_timestamp_detailed(i64 ts); + string truncate_str(string s, u32 prefix_len, u32 suffix_len); }; [Error] diff --git a/keep-mobile/src/lib.rs b/keep-mobile/src/lib.rs index b5ab71a0..5489706c 100644 --- a/keep-mobile/src/lib.rs +++ b/keep-mobile/src/lib.rs @@ -33,6 +33,18 @@ pub use types::{ SignRequestMetadata, ThresholdConfig, WalletDescriptorInfo, }; +pub fn format_timestamp(ts: i64) -> String { + keep_core::display::format_timestamp(ts) +} + +pub fn format_timestamp_detailed(ts: i64) -> String { + keep_core::display::format_timestamp_detailed(ts) +} + +pub fn truncate_str(s: String, prefix_len: u32, suffix_len: u32) -> String { + keep_core::display::truncate_str(&s, prefix_len as usize, suffix_len as usize) +} + use keep_core::frost::{ ShareExport, ShareMetadata, SharePackage, ThresholdConfig as CoreThresholdConfig, TrustedDealer, };