Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions keep-core/src/display.rs
Original file line number Diff line number Diff line change
@@ -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::<Utc>::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::<Utc>::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");
}
}
2 changes: 2 additions & 0 deletions keep-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 1 addition & 8 deletions keep-desktop/src/screen/bunker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
13 changes: 2 additions & 11 deletions keep-desktop/src/screen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Utc>::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 {
Expand Down
9 changes: 2 additions & 7 deletions keep-desktop/src/screen/signing_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
7 changes: 2 additions & 5 deletions keep-desktop/src/screen/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -483,10 +482,8 @@ impl WalletScreen {
.color(theme::color::TEXT_DIM);

let created = i64::try_from(entry.created_at)
.ok()
.and_then(|ts| DateTime::<Utc>::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);
Expand Down
4 changes: 4 additions & 0 deletions keep-mobile/src/keep_mobile.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions keep-mobile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down