From d00ee33eabe9af5eac4fde080d3d34cb82dfb812 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sat, 3 Oct 2026 11:00:59 +0700 Subject: [PATCH] consolidate shared helpers into utils crate --- Cargo.lock | 1 + crates/signed_core/Cargo.toml | 1 + crates/signed_core/src/inbox.rs | 13 +---- crates/signed_state/src/backend.rs | 14 +----- crates/signed_state/src/checkouts.rs | 18 +------ crates/signed_state/src/repo.rs | 27 +++-------- crates/signed_ui/src/lib.rs | 2 - crates/signed_ui/src/util.rs | 38 --------------- crates/utils/src/lib.rs | 6 ++- crates/utils/src/pubkey.rs | 47 +++---------------- crates/utils/src/repo_url.rs | 20 ++++++++ crates/utils/src/text.rs | 21 +++++++++ crates/utils/src/time.rs | 39 ++++++++------- .../workspace/src/views/pull_requests/new.rs | 12 +---- crates/workspace/src/views/repo/about.rs | 3 +- crates/workspace/src/views/repo/mod.rs | 4 +- 16 files changed, 89 insertions(+), 177 deletions(-) delete mode 100644 crates/signed_ui/src/util.rs create mode 100644 crates/utils/src/repo_url.rs create mode 100644 crates/utils/src/text.rs diff --git a/Cargo.lock b/Cargo.lock index 758a67a..b025955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8134,6 +8134,7 @@ version = "0.2.0-alpha" dependencies = [ "nostr", "serde", + "utils", ] [[package]] diff --git a/crates/signed_core/Cargo.toml b/crates/signed_core/Cargo.toml index f4d90a4..a6da80d 100644 --- a/crates/signed_core/Cargo.toml +++ b/crates/signed_core/Cargo.toml @@ -7,3 +7,4 @@ publish.workspace = true [dependencies] nostr.workspace = true serde.workspace = true +utils = { path = "../utils" } diff --git a/crates/signed_core/src/inbox.rs b/crates/signed_core/src/inbox.rs index 9b88a36..7889bba 100644 --- a/crates/signed_core/src/inbox.rs +++ b/crates/signed_core/src/inbox.rs @@ -188,8 +188,8 @@ where .map(|root| { let mut events = groups.remove(&root).unwrap_or_default(); let mut own_events = own_groups.remove(&root).unwrap_or_default(); - sort_newest_first(&mut events); - sort_newest_first(&mut own_events); + utils::sort_newest_first(&mut events); + utils::sort_newest_first(&mut own_events); let root_event = lookup(root); @@ -218,15 +218,6 @@ where items } -/// Sort thread events newest first, ties broken by id. -fn sort_newest_first(events: &mut [Event]) { - events.sort_by(|a, b| { - b.created_at - .cmp(&a.created_at) - .then_with(|| b.id.to_hex().cmp(&a.id.to_hex())) - }); -} - /// Read and archive state of the inbox, a high-water-mark model. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct InboxReadState { diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index 23b85d8..589fbc5 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -1418,7 +1418,7 @@ impl PushOutcome { self.failing() .map(|server| { let reason = - flatten_whitespace(server.reason.as_deref().unwrap_or("unknown error")); + utils::flatten_whitespace(server.reason.as_deref().unwrap_or("unknown error")); format!("{}: {reason}", server.relay) }) .collect::>() @@ -1441,18 +1441,6 @@ impl PushOutcome { } } -fn flatten_whitespace(text: &str) -> String { - const MAX_CHARS: usize = 200; - let flat: String = text.split_whitespace().collect::>().join(" "); - if flat.chars().count() <= MAX_CHARS { - flat - } else { - let mut clipped: String = flat.chars().take(MAX_CHARS).collect(); - clipped.push('…'); - clipped - } -} - /// Reasons a push attempt should be retried with a freshly staged state /// event and a fresh git advertisement. /// diff --git a/crates/signed_state/src/checkouts.rs b/crates/signed_state/src/checkouts.rs index 0544492..67f48f5 100644 --- a/crates/signed_state/src/checkouts.rs +++ b/crates/signed_state/src/checkouts.rs @@ -7,6 +7,7 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription}; use nostr::prelude::*; use settings::{CheckoutRecord, SettingsStore}; use signed_core::{Announcement, RepoAddr}; +use utils::same_repo_url; use crate::backend::{Backend, BackendEvent}; use crate::git_store::repo_mirror_root; @@ -511,23 +512,6 @@ impl CheckoutsStore { } } -fn url_identity(url: &str) -> Option<(String, Option, String)> { - let parsed = Url::parse(url).ok()?; - let host = parsed.host_str()?.to_ascii_lowercase(); - let mut path = parsed.path().trim_matches('/').to_owned(); - if let Some(stripped) = path.strip_suffix(".git") { - path = stripped.to_owned(); - } - Some((host, parsed.port(), path)) -} - -fn same_repo_url(a: &str, b: &str) -> bool { - match (url_identity(a), url_identity(b)) { - (Some(a), Some(b)) => a == b, - _ => a == b, - } -} - fn resolve_associations<'a>( remembered: &[Remembered], scanned: &[(PathBuf, Option, Option)], diff --git a/crates/signed_state/src/repo.rs b/crates/signed_state/src/repo.rs index 0174abd..bd2a52e 100644 --- a/crates/signed_state/src/repo.rs +++ b/crates/signed_state/src/repo.rs @@ -392,12 +392,12 @@ impl RepoStore { .into_iter() .filter(|e| !deletions.is_deleted(e)); - let announcement = latest(all_announcements) + let announcement = utils::latest(all_announcements) .as_ref() .and_then(Announcement::from_event); let all_states = states.into_iter().filter(|e| !deletions.is_deleted(e)); - let state = latest(all_states).map(|state| parse_state(&state)); + let state = utils::latest(all_states).map(|state| parse_state(&state)); let (mut issues, mut patches, mut pull_requests, mut statuses, mut comments) = (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()); @@ -450,10 +450,10 @@ impl RepoStore { } } - sort_newest_first(&mut issues); - sort_newest_first(&mut patches); - sort_newest_first(&mut pull_requests); - sort_oldest_first(&mut comments); + utils::sort_newest_first(&mut issues); + utils::sort_newest_first(&mut patches); + utils::sort_newest_first(&mut pull_requests); + utils::sort_oldest_first(&mut comments); let maintainers = announcement .as_ref() @@ -1447,13 +1447,6 @@ impl RepoStore { } } -fn latest(events: I) -> Option -where - I: IntoIterator, -{ - events.into_iter().max_by_key(|e| e.created_at) -} - fn status_of(status_by_root: &HashMap, root: &Event) -> RepoStatus { status_by_root .get(&root.id) @@ -1492,14 +1485,6 @@ fn resolve_statuses( .collect() } -fn sort_newest_first(events: &mut [Event]) { - events.sort_by_key(|e| std::cmp::Reverse(e.created_at)); -} - -fn sort_oldest_first(events: &mut [Event]) { - events.sort_by_key(|e| e.created_at); -} - /// The proposed commit of a `git format-patch` output. /// It is the `From ` header on the first line. fn patch_current_commit(patch: &str) -> Option<&str> { diff --git a/crates/signed_ui/src/lib.rs b/crates/signed_ui/src/lib.rs index bad4e4d..e3ab674 100644 --- a/crates/signed_ui/src/lib.rs +++ b/crates/signed_ui/src/lib.rs @@ -11,7 +11,6 @@ mod title_bar; mod tree_row; pub mod copy_row; -pub mod util; pub use avatar::Avatar; pub use copy_row::{copy_row, menu_copy_row}; @@ -25,4 +24,3 @@ pub use setting::{SelectOption, setting_block, setting_row}; pub use status_badge::status_badge; pub use title_bar::title_bar_drag_handlers; pub use tree_row::tree_row; -pub use util::middle_truncate; diff --git a/crates/signed_ui/src/util.rs b/crates/signed_ui/src/util.rs deleted file mode 100644 index 64d187a..0000000 --- a/crates/signed_ui/src/util.rs +++ /dev/null @@ -1,38 +0,0 @@ -/// `[head chars]...[tail chars]` middle truncation. -/// -/// Values too short for the ellipsis to save space are left alone. -pub fn middle_truncate(value: &str, head: usize, tail: usize) -> String { - let len = value.chars().count(); - if len <= head + tail + 3 { - return value.to_string(); - } - let head: String = value.chars().take(head).collect(); - let tail: String = value.chars().skip(len - tail).collect(); - format!("{head}...{tail}") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn middle_truncates_long_values_only() { - assert_eq!( - middle_truncate( - "a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d", - 10, - 10, - ), - "a008def157...ad57a3564d" - ); - assert_eq!( - middle_truncate( - "30617:a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d:ngit", - 10, - 10 - ), - "30617:a008...3564d:ngit" - ); - assert_eq!(middle_truncate("short", 10, 10), "short"); - } -} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 0fe56c3..14398f5 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,5 +1,9 @@ mod pubkey; +mod repo_url; +mod text; mod time; pub use pubkey::shorten_pubkey; -pub use time::{relative_time, relative_time_secs}; +pub use repo_url::same_repo_url; +pub use text::{flatten_whitespace, middle_truncate}; +pub use time::{latest, relative_time, relative_time_secs, sort_newest_first, sort_oldest_first}; diff --git a/crates/utils/src/pubkey.rs b/crates/utils/src/pubkey.rs index 65b7b01..d8d1ea0 100644 --- a/crates/utils/src/pubkey.rs +++ b/crates/utils/src/pubkey.rs @@ -1,49 +1,14 @@ use nostr::prelude::*; -/// Shorten a [`PublicKey`] to `npub1abc...wxyz` form. +use crate::text::middle_truncate; + +const HEAD_CHARS: usize = 9; +const TAIL_CHARS: usize = 4; + pub fn shorten_pubkey(public_key: PublicKey) -> String { let encoded = public_key .to_bech32() .unwrap_or_else(|_| public_key.to_hex()); - truncate_middle(&encoded) -} - -fn truncate_middle(value: &str) -> String { - const HEAD_CHARS: usize = 9; - const TAIL_CHARS: usize = 4; - - let length = value.chars().count(); - if length <= HEAD_CHARS + TAIL_CHARS + 3 { - return value.to_owned(); - } - - let head: String = value.chars().take(HEAD_CHARS).collect(); - let tail: String = value.chars().skip(length - TAIL_CHARS).collect(); - - format!("{head}...{tail}") -} - -#[cfg(test)] -mod tests { - use super::*; - - const PUBLIC_KEY_HEX: &str = "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272"; - - #[test] - fn shortens_a_valid_pubkey() { - let public_key = PublicKey::from_hex(PUBLIC_KEY_HEX).expect("valid pubkey"); - let npub = public_key.to_bech32().expect("valid pubkey encodes"); - - assert_eq!( - shorten_pubkey(public_key), - format!("{}...{}", &npub[..9], &npub[npub.len() - 4..]) - ); - } - - #[test] - fn leaves_short_values_intact() { - assert_eq!(truncate_middle("npub1short"), "npub1short"); - assert_eq!(truncate_middle("thirteenchars"), "thirteenchars"); - } + middle_truncate(&encoded, HEAD_CHARS, TAIL_CHARS) } diff --git a/crates/utils/src/repo_url.rs b/crates/utils/src/repo_url.rs new file mode 100644 index 0000000..cc9bbbd --- /dev/null +++ b/crates/utils/src/repo_url.rs @@ -0,0 +1,20 @@ +use nostr::prelude::*; + +/// Compare git URLs ignoring scheme, trailing `.git` and path slashes, so a +/// grasp announce URL matches its https origin. Unparseable values compare literally. +pub fn same_repo_url(a: &str, b: &str) -> bool { + match (url_identity(a), url_identity(b)) { + (Some(a), Some(b)) => a == b, + _ => a == b, + } +} + +fn url_identity(url: &str) -> Option<(String, Option, String)> { + let parsed = Url::parse(url).ok()?; + let host = parsed.host_str()?.to_ascii_lowercase(); + let mut path = parsed.path().trim_matches('/').to_owned(); + if let Some(stripped) = path.strip_suffix(".git") { + path = stripped.to_owned(); + } + Some((host, parsed.port(), path)) +} diff --git a/crates/utils/src/text.rs b/crates/utils/src/text.rs new file mode 100644 index 0000000..c79c6b5 --- /dev/null +++ b/crates/utils/src/text.rs @@ -0,0 +1,21 @@ +pub fn middle_truncate(value: &str, head: usize, tail: usize) -> String { + let len = value.chars().count(); + if len <= head + tail + 3 { + return value.to_string(); + } + let head: String = value.chars().take(head).collect(); + let tail: String = value.chars().skip(len - tail).collect(); + format!("{head}...{tail}") +} + +pub fn flatten_whitespace(text: &str) -> String { + const MAX_CHARS: usize = 200; + let flat = text.split_whitespace().collect::>().join(" "); + if flat.chars().count() <= MAX_CHARS { + flat + } else { + let mut clipped: String = flat.chars().take(MAX_CHARS).collect(); + clipped.push('…'); + clipped + } +} diff --git a/crates/utils/src/time.rs b/crates/utils/src/time.rs index 230ddf9..4070cf0 100644 --- a/crates/utils/src/time.rs +++ b/crates/utils/src/time.rs @@ -1,6 +1,24 @@ use nostr::prelude::*; -/// Format a timestamp as a short relative time, e.g. `3h ago`. +pub fn sort_newest_first(events: &mut [Event]) { + events.sort_by(|a, b| { + b.created_at + .cmp(&a.created_at) + .then_with(|| b.id.to_hex().cmp(&a.id.to_hex())) + }); +} + +pub fn sort_oldest_first(events: &mut [Event]) { + events.sort_by_key(|e| e.created_at); +} + +pub fn latest(events: I) -> Option +where + I: IntoIterator, +{ + events.into_iter().max_by_key(|e| e.created_at) +} + pub fn relative_time(timestamp: Timestamp) -> String { let now = Timestamp::now().as_secs(); let secs = now.saturating_sub(timestamp.as_secs()); @@ -20,25 +38,6 @@ pub fn relative_time(timestamp: Timestamp) -> String { } } -/// Format a unix timestamp in seconds as a short relative time, e.g. `3h ago`. pub fn relative_time_secs(secs: i64) -> String { relative_time(Timestamp::from_secs(secs.max(0) as u64)) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn formats_relative_time() { - let now = Timestamp::now(); - - assert_eq!(relative_time(now), "just now"); - assert_eq!(relative_time(now - 300), "5m ago"); - assert_eq!(relative_time(now - 7_200), "2h ago"); - assert_eq!(relative_time(now - 3 * 86_400), "3d ago"); - assert_eq!(relative_time(now - 60 * 86_400), "2mo ago"); - assert_eq!(relative_time(now - 800 * 86_400), "2y ago"); - assert_eq!(relative_time(now + 600), "just now"); - } -} diff --git a/crates/workspace/src/views/pull_requests/new.rs b/crates/workspace/src/views/pull_requests/new.rs index 4d29ba1..88e417c 100644 --- a/crates/workspace/src/views/pull_requests/new.rs +++ b/crates/workspace/src/views/pull_requests/new.rs @@ -30,6 +30,7 @@ use signed_state::{ Backend, CheckoutsStore, RepoListStore, RepoStore, ensure_repo_mirror, repo_mirror_path, }; use signed_ui::{CountBadge, placeholder, ref_selector_trigger}; +use utils::middle_truncate; use crate::views::commit_diff::{COMMIT_ROW_HEIGHT, CommitDiffView, DiffPane, commit_row}; use crate::views::{repo_tab_avatar, tab_title}; @@ -104,17 +105,8 @@ fn shorten_owner(owner: &PublicKey) -> String { hex.chars().take(10).collect() } -/// Truncate a label for the fixed-width controls of the compare bar. fn truncate_label(label: &str) -> SharedString { - const MAX: usize = 18; - let mut chars = label.chars(); - let (prefix, rest) = (chars.by_ref().take(MAX).collect::(), chars.next()); - let label = if rest.is_some() { - format!("{}…", &prefix[..prefix.len().saturating_sub(1)]) - } else { - prefix - }; - SharedString::from(label) + SharedString::from(middle_truncate(label, 18, 0)) } fn checkout_source_item( diff --git a/crates/workspace/src/views/repo/about.rs b/crates/workspace/src/views/repo/about.rs index bfd6fbc..145bcdf 100644 --- a/crates/workspace/src/views/repo/about.rs +++ b/crates/workspace/src/views/repo/about.rs @@ -5,7 +5,8 @@ use gpui_component::{ActiveTheme, StyledExt, WindowExt, h_flex, v_flex}; use nostr::prelude::PublicKey; use signed_core::Announcement; use signed_state::ProfileStore; -use signed_ui::{Avatar, middle_truncate}; +use signed_ui::Avatar; +use utils::middle_truncate; pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) { window.open_dialog(cx, move |dialog, _window, cx| { diff --git a/crates/workspace/src/views/repo/mod.rs b/crates/workspace/src/views/repo/mod.rs index 8161042..fcb7a93 100644 --- a/crates/workspace/src/views/repo/mod.rs +++ b/crates/workspace/src/views/repo/mod.rs @@ -31,9 +31,9 @@ use signed_state::{ pr_proposes_checkout, }; use signed_ui::{ - Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, middle_truncate, - ref_selector_trigger, + Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, ref_selector_trigger, }; +use utils::middle_truncate; mod about; mod actions;