From 2ade2b9bbd14ea5c638a2b29cf426f86c3050065 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 19:24:17 +0000 Subject: [PATCH 01/11] feat: add special handling for Desktop folder - Add Desktop as a special Target type alongside Downloads - Add path detection for user's Desktop folder (/Users/*/Desktop/) - Display Desktop target as ~/Desktop in sidebar - Add acceptance test for Desktop folder handling --- src/finder/sidebar.rs | 8 ++++++++ src/system/favorites/url_mapper.rs | 17 +++++++++++++++++ tests/finder.rs | 20 ++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/finder/sidebar.rs b/src/finder/sidebar.rs index 0da9eef..1008d39 100644 --- a/src/finder/sidebar.rs +++ b/src/finder/sidebar.rs @@ -6,6 +6,7 @@ pub enum Target { Recents, Applications, Downloads, + Desktop, Custom { label: String, path: String }, } @@ -27,6 +28,7 @@ impl fmt::Display for SidebarItem { Target::Recents => write!(f, "Recents"), Target::Applications => write!(f, "Applications"), Target::Downloads => write!(f, "~/Downloads"), + Target::Desktop => write!(f, "~/Desktop"), Target::Custom { label, path } => write!(f, "{} -> {}", label, path), } } @@ -70,4 +72,10 @@ mod tests { let item = SidebarItem::new(Target::Downloads); assert_eq!(format!("{}", item), "~/Downloads"); } + + #[test] + fn should_create_sidebar_item_with_desktop() { + let item = SidebarItem::new(Target::Desktop); + assert_eq!(format!("{}", item), "~/Desktop"); + } } diff --git a/src/system/favorites/url_mapper.rs b/src/system/favorites/url_mapper.rs index dea1df6..74fb888 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/url_mapper.rs @@ -16,6 +16,13 @@ fn is_downloads_url(url: &str) -> bool { && url_path.starts_with("/Users/") } +fn is_desktop_url(url: &str) -> bool { + let url_path = url.strip_prefix("file://").unwrap_or(url); + url_path.matches('/').count() == 4 + && url_path.ends_with("/Desktop/") + && url_path.starts_with("/Users/") +} + impl From for Target { fn from(target: TargetUrl) -> Self { let url = target.0.to_string(); @@ -25,6 +32,7 @@ impl From for Target { RECENTS_URL => Target::Recents, APPLICATIONS_URL => Target::Applications, path if is_downloads_url(path) => Target::Downloads, + path if is_desktop_url(path) => Target::Desktop, path => Target::Custom { label: target.1.to_string(), path: path.to_string(), @@ -103,4 +111,13 @@ mod tests { path: "file:///Users/user/Documents/".to_string(), }); } + + #[test] + fn should_convert_desktop_url() { + let target = Target::from(TargetUrl( + create_url("file:///Users/user/Desktop/"), + create_display_name("Desktop"), + )); + assert_eq!(target, Target::Desktop); + } } diff --git a/tests/finder.rs b/tests/finder.rs index 0d00f92..d6fd00b 100644 --- a/tests/finder.rs +++ b/tests/finder.rs @@ -12,6 +12,7 @@ mod constants { pub const RECENTS_LABEL: &str = "Recents"; pub const APPLICATIONS_LABEL: &str = "Applications"; pub const DOWNLOADS_LABEL: &str = "Downloads"; + pub const DESKTOP_LABEL: &str = "Desktop"; pub const DOCUMENTS_LABEL: &str = "Documents"; // URLs @@ -19,6 +20,7 @@ mod constants { pub const RECENTS_URL: &str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; pub const APPLICATIONS_URL: &str = "file:///Applications/"; pub const DOWNLOADS_URL: &str = "file:///Users/user/Downloads/"; + pub const DESKTOP_URL: &str = "file:///Users/user/Desktop/"; pub const DOCUMENTS_URL: &str = "file:///Users/user/Documents/"; } @@ -189,3 +191,21 @@ fn should_handle_multiple_favorites() -> Result<()> { assert_eq!(result, expected_result); Ok(()) } + +#[test] +fn should_handle_desktop_item() -> Result<()> { + // Arrange + let expected_result = vec![SidebarItem::new(Target::Desktop)]; + let favorites = FavoritesBuilder::new() + .add_item(Some(constants::DESKTOP_LABEL), constants::DESKTOP_URL) + .build(); + let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); + let finder = Finder::new(mock_api); + + // Act + let result = finder.get_favorites_list()?; + + // Assert + assert_eq!(result, expected_result); + Ok(()) +} From 8619ecf8ba8629e25209d3b895da331a4feed3d8 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 19:48:52 +0000 Subject: [PATCH 02/11] refactor: improve MacOsPath implementation - Replace custom URL handling with MacOsPath type - Use standard Rust traits (From, Display) instead of custom methods - Centralize path detection logic in MacOsPath --- src/system/favorites/url_mapper.rs | 96 +++++++++++++++++++----------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/src/system/favorites/url_mapper.rs b/src/system/favorites/url_mapper.rs index 74fb888..bb954c8 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/url_mapper.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::{ finder::Target, system::favorites::{DisplayName, Url}, @@ -5,42 +7,64 @@ use crate::{ pub struct TargetUrl(pub Url, pub DisplayName); -const AIRDROP_URL: &str = "nwnode://domain-AirDrop"; -const RECENTS_URL: &str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; -const APPLICATIONS_URL: &str = "file:///Applications/"; +struct MacOsPath(Url); + +impl MacOsPath { + const AIRDROP: &'static str = "nwnode://domain-AirDrop"; + const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; + const APPLICATIONS: &'static str = "file:///Applications/"; -fn is_downloads_url(url: &str) -> bool { - let url_path = url.strip_prefix("file://").unwrap_or(url); - url_path.matches('/').count() == 4 - && url_path.ends_with("/Downloads/") - && url_path.starts_with("/Users/") + fn is_special_folder(&self, folder: &str) -> bool { + let path = self.0.to_string(); + let url_path = path.strip_prefix("file://").unwrap_or(&path); + url_path.matches('/').count() == 4 + && url_path.ends_with(&format!("/{}/", folder)) + && url_path.starts_with("/Users/") + } } -fn is_desktop_url(url: &str) -> bool { - let url_path = url.strip_prefix("file://").unwrap_or(url); - url_path.matches('/').count() == 4 - && url_path.ends_with("/Desktop/") - && url_path.starts_with("/Users/") +impl fmt::Display for MacOsPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } } -impl From for Target { - fn from(target: TargetUrl) -> Self { - let url = target.0.to_string(); +impl From for MacOsPath { + fn from(url: Url) -> Self { + Self(url) + } +} +impl From for Target { + fn from(path: MacOsPath) -> Self { + let url = path.to_string(); match url.as_str() { - AIRDROP_URL => Target::AirDrop, - RECENTS_URL => Target::Recents, - APPLICATIONS_URL => Target::Applications, - path if is_downloads_url(path) => Target::Downloads, - path if is_desktop_url(path) => Target::Desktop, - path => Target::Custom { - label: target.1.to_string(), - path: path.to_string(), + MacOsPath::AIRDROP => Target::AirDrop, + MacOsPath::RECENTS => Target::Recents, + MacOsPath::APPLICATIONS => Target::Applications, + _ if path.is_special_folder("Downloads") => Target::Downloads, + _ if path.is_special_folder("Desktop") => Target::Desktop, + path_str => Target::Custom { + label: String::new(), // Will be overridden + path: path_str.to_string(), }, } } } +impl From for Target { + fn from(TargetUrl(url, name): TargetUrl) -> Self { + let mut target = Target::from(MacOsPath::from(url)); + if let Target::Custom { label: _, path } = &target { + target = Target::Custom { + label: name.to_string(), + path: path.clone(), + }; + } + target + } +} + #[cfg(test)] mod tests { use core_foundation::{ @@ -67,7 +91,7 @@ mod tests { #[test] fn should_convert_airdrop_url() { let target = Target::from(TargetUrl( - create_url(AIRDROP_URL), + create_url(MacOsPath::AIRDROP), create_display_name("AirDrop"), )); assert_eq!(target, Target::AirDrop); @@ -76,7 +100,7 @@ mod tests { #[test] fn should_convert_recents_url() { let target = Target::from(TargetUrl( - create_url(RECENTS_URL), + create_url(MacOsPath::RECENTS), create_display_name("Recents"), )); assert_eq!(target, Target::Recents); @@ -85,7 +109,7 @@ mod tests { #[test] fn should_convert_applications_url() { let target = Target::from(TargetUrl( - create_url(APPLICATIONS_URL), + create_url(MacOsPath::APPLICATIONS), create_display_name("Applications"), )); assert_eq!(target, Target::Applications); @@ -100,6 +124,15 @@ mod tests { assert_eq!(target, Target::Downloads); } + #[test] + fn should_convert_desktop_url() { + let target = Target::from(TargetUrl( + create_url("file:///Users/user/Desktop/"), + create_display_name("Desktop"), + )); + assert_eq!(target, Target::Desktop); + } + #[test] fn should_convert_custom_url() { let target = Target::from(TargetUrl( @@ -111,13 +144,4 @@ mod tests { path: "file:///Users/user/Documents/".to_string(), }); } - - #[test] - fn should_convert_desktop_url() { - let target = Target::from(TargetUrl( - create_url("file:///Users/user/Desktop/"), - create_display_name("Desktop"), - )); - assert_eq!(target, Target::Desktop); - } } From 74480cead1f926bd7c4beabc877a9607eeb6167f Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 20:03:43 +0000 Subject: [PATCH 03/11] refactor: simplify favorites item representation - Replace TargetUrl and MacOsPath with a single FavoriteItem type - Move item-related code to favorites/item.rs - Improve naming to better reflect domain concepts --- .../favorites/{url_mapper.rs => item.rs} | 68 ++++++++----------- src/system/favorites/mod.rs | 6 +- 2 files changed, 30 insertions(+), 44 deletions(-) rename src/system/favorites/{url_mapper.rs => item.rs} (68%) diff --git a/src/system/favorites/url_mapper.rs b/src/system/favorites/item.rs similarity index 68% rename from src/system/favorites/url_mapper.rs rename to src/system/favorites/item.rs index bb954c8..ddc24a6 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/item.rs @@ -5,17 +5,22 @@ use crate::{ system::favorites::{DisplayName, Url}, }; -pub struct TargetUrl(pub Url, pub DisplayName); - -struct MacOsPath(Url); +pub struct FavoriteItem { + url: Url, + name: DisplayName, +} -impl MacOsPath { +impl FavoriteItem { const AIRDROP: &'static str = "nwnode://domain-AirDrop"; const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; const APPLICATIONS: &'static str = "file:///Applications/"; + pub fn new(url: Url, name: DisplayName) -> Self { + Self { url, name } + } + fn is_special_folder(&self, folder: &str) -> bool { - let path = self.0.to_string(); + let path = self.url.to_string(); let url_path = path.strip_prefix("file://").unwrap_or(&path); url_path.matches('/').count() == 4 && url_path.ends_with(&format!("/{}/", folder)) @@ -23,48 +28,29 @@ impl MacOsPath { } } -impl fmt::Display for MacOsPath { +impl fmt::Display for FavoriteItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + write!(f, "{}", self.url) } } -impl From for MacOsPath { - fn from(url: Url) -> Self { - Self(url) - } -} - -impl From for Target { - fn from(path: MacOsPath) -> Self { +impl From for Target { + fn from(path: FavoriteItem) -> Self { let url = path.to_string(); match url.as_str() { - MacOsPath::AIRDROP => Target::AirDrop, - MacOsPath::RECENTS => Target::Recents, - MacOsPath::APPLICATIONS => Target::Applications, + FavoriteItem::AIRDROP => Target::AirDrop, + FavoriteItem::RECENTS => Target::Recents, + FavoriteItem::APPLICATIONS => Target::Applications, _ if path.is_special_folder("Downloads") => Target::Downloads, _ if path.is_special_folder("Desktop") => Target::Desktop, path_str => Target::Custom { - label: String::new(), // Will be overridden + label: path.name.to_string(), path: path_str.to_string(), }, } } } -impl From for Target { - fn from(TargetUrl(url, name): TargetUrl) -> Self { - let mut target = Target::from(MacOsPath::from(url)); - if let Target::Custom { label: _, path } = &target { - target = Target::Custom { - label: name.to_string(), - path: path.clone(), - }; - } - target - } -} - #[cfg(test)] mod tests { use core_foundation::{ @@ -90,8 +76,8 @@ mod tests { #[test] fn should_convert_airdrop_url() { - let target = Target::from(TargetUrl( - create_url(MacOsPath::AIRDROP), + let target = Target::from(FavoriteItem::new( + create_url(FavoriteItem::AIRDROP), create_display_name("AirDrop"), )); assert_eq!(target, Target::AirDrop); @@ -99,8 +85,8 @@ mod tests { #[test] fn should_convert_recents_url() { - let target = Target::from(TargetUrl( - create_url(MacOsPath::RECENTS), + let target = Target::from(FavoriteItem::new( + create_url(FavoriteItem::RECENTS), create_display_name("Recents"), )); assert_eq!(target, Target::Recents); @@ -108,8 +94,8 @@ mod tests { #[test] fn should_convert_applications_url() { - let target = Target::from(TargetUrl( - create_url(MacOsPath::APPLICATIONS), + let target = Target::from(FavoriteItem::new( + create_url(FavoriteItem::APPLICATIONS), create_display_name("Applications"), )); assert_eq!(target, Target::Applications); @@ -117,7 +103,7 @@ mod tests { #[test] fn should_convert_downloads_url() { - let target = Target::from(TargetUrl( + let target = Target::from(FavoriteItem::new( create_url("file:///Users/user/Downloads/"), create_display_name("Downloads"), )); @@ -126,7 +112,7 @@ mod tests { #[test] fn should_convert_desktop_url() { - let target = Target::from(TargetUrl( + let target = Target::from(FavoriteItem::new( create_url("file:///Users/user/Desktop/"), create_display_name("Desktop"), )); @@ -135,7 +121,7 @@ mod tests { #[test] fn should_convert_custom_url() { - let target = Target::from(TargetUrl( + let target = Target::from(FavoriteItem::new( create_url("file:///Users/user/Documents/"), create_display_name("Documents"), )); diff --git a/src/system/favorites/mod.rs b/src/system/favorites/mod.rs index 1d8b654..b2ef05d 100644 --- a/src/system/favorites/mod.rs +++ b/src/system/favorites/mod.rs @@ -1,20 +1,20 @@ mod display_name; mod errors; mod handle; +mod item; mod snapshot; mod snapshot_item; mod url; -mod url_mapper; use core_foundation::base::kCFAllocatorDefault; use core_services::{LSSharedFileListResolutionFlags, kLSSharedFileListFavoriteItems}; pub use display_name::DisplayName; pub use errors::FavoritesError; pub use handle::FavoritesHandle; +pub use item::FavoriteItem; pub use snapshot::Snapshot; pub use snapshot_item::SnapshotItem; pub use url::Url; -pub use url_mapper::TargetUrl; use crate::{ finder::{Result, SidebarItem, Target, favorites::FavoritesApi}, @@ -72,7 +72,7 @@ impl Favorites { unsafe fn convert_item(&self, item: SnapshotItem) -> Result { let url = unsafe { self.copy_resolved_url(&item) }?; let name = unsafe { self.copy_display_name(&item) }?; - let target = Target::from(TargetUrl(url, name)); + let target = Target::from(FavoriteItem::new(url, name)); Ok(SidebarItem::new(target)) } } From 97d2c1c5b61e06559157e63eaa0ab84ee2438713 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 20:36:04 +0000 Subject: [PATCH 04/11] refactor: introduce MacOsUrl enum for URL classification - Add MacOsUrl enum to handle all URL types (AirDrop, Recents, Applications, Downloads, Desktop, Custom) - Move URL constants and path validation logic from FavoriteItem to MacOsUrl - Implement From<&str> for MacOsUrl for direct URL parsing - Add helper methods is_user_downloads and is_user_desktop - Add USER_HOME_FOLDER_DEPTH constant for path validation --- src/system/favorites/item.rs | 92 ++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index ddc24a6..d2783ca 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -1,51 +1,76 @@ -use std::fmt; - use crate::{ finder::Target, system::favorites::{DisplayName, Url}, }; -pub struct FavoriteItem { - url: Url, - name: DisplayName, +#[derive(Debug)] +enum MacOsUrl { + AirDrop, + Recents, + Applications, + Downloads, + Desktop, + Custom(String), } -impl FavoriteItem { +impl MacOsUrl { const AIRDROP: &'static str = "nwnode://domain-AirDrop"; const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; const APPLICATIONS: &'static str = "file:///Applications/"; + const USER_HOME_FOLDER_DEPTH: usize = 4; // /Users/username/folder/ - pub fn new(url: Url, name: DisplayName) -> Self { - Self { url, name } + fn is_user_downloads(url: &str) -> bool { + Self::is_user_folder(url, "Downloads") } - fn is_special_folder(&self, folder: &str) -> bool { - let path = self.url.to_string(); - let url_path = path.strip_prefix("file://").unwrap_or(&path); - url_path.matches('/').count() == 4 + fn is_user_desktop(url: &str) -> bool { + Self::is_user_folder(url, "Desktop") + } + + fn is_user_folder(url: &str, folder: &str) -> bool { + let url_path = url.strip_prefix("file://").unwrap_or(url); + url_path.matches('/').count() == Self::USER_HOME_FOLDER_DEPTH && url_path.ends_with(&format!("/{}/", folder)) && url_path.starts_with("/Users/") } } -impl fmt::Display for FavoriteItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.url) +impl From<&str> for MacOsUrl { + fn from(url: &str) -> Self { + match url { + Self::AIRDROP => Self::AirDrop, + Self::RECENTS => Self::Recents, + Self::APPLICATIONS => Self::Applications, + path if Self::is_user_desktop(path) => Self::Desktop, + path if Self::is_user_downloads(path) => Self::Downloads, + path => Self::Custom(path.to_string()), + } + } +} + +pub struct FavoriteItem { + url: Url, + name: DisplayName, +} + +impl FavoriteItem { + pub fn new(url: Url, name: DisplayName) -> Self { + Self { url, name } } } impl From for Target { - fn from(path: FavoriteItem) -> Self { - let url = path.to_string(); - match url.as_str() { - FavoriteItem::AIRDROP => Target::AirDrop, - FavoriteItem::RECENTS => Target::Recents, - FavoriteItem::APPLICATIONS => Target::Applications, - _ if path.is_special_folder("Downloads") => Target::Downloads, - _ if path.is_special_folder("Desktop") => Target::Desktop, - path_str => Target::Custom { - label: path.name.to_string(), - path: path_str.to_string(), + fn from(item: FavoriteItem) -> Self { + let url = item.url.to_string(); + match MacOsUrl::from(url.as_str()) { + MacOsUrl::AirDrop => Target::AirDrop, + MacOsUrl::Recents => Target::Recents, + MacOsUrl::Applications => Target::Applications, + MacOsUrl::Downloads => Target::Downloads, + MacOsUrl::Desktop => Target::Desktop, + MacOsUrl::Custom(path) => Target::Custom { + label: item.name.to_string(), + path, }, } } @@ -77,7 +102,7 @@ mod tests { #[test] fn should_convert_airdrop_url() { let target = Target::from(FavoriteItem::new( - create_url(FavoriteItem::AIRDROP), + create_url(MacOsUrl::AIRDROP), create_display_name("AirDrop"), )); assert_eq!(target, Target::AirDrop); @@ -86,7 +111,7 @@ mod tests { #[test] fn should_convert_recents_url() { let target = Target::from(FavoriteItem::new( - create_url(FavoriteItem::RECENTS), + create_url(MacOsUrl::RECENTS), create_display_name("Recents"), )); assert_eq!(target, Target::Recents); @@ -95,7 +120,7 @@ mod tests { #[test] fn should_convert_applications_url() { let target = Target::from(FavoriteItem::new( - create_url(FavoriteItem::APPLICATIONS), + create_url(MacOsUrl::APPLICATIONS), create_display_name("Applications"), )); assert_eq!(target, Target::Applications); @@ -130,4 +155,13 @@ mod tests { path: "file:///Users/user/Documents/".to_string(), }); } + + #[test] + fn should_not_recognize_deep_downloads_path() { + let target = Target::from(FavoriteItem::new( + create_url("file:///Users/user/Documents/Downloads/"), + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); + } } From 2ce03dd00dadf28f4b2dd6b5d74d06d93b19cdd3 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 21:18:50 +0000 Subject: [PATCH 05/11] refactor: improve MacOsUrl path validation - Extract path validation logic into is_inside_home_dir helper method - Make MacOsUrl public and derive common traits (Debug, Clone, PartialEq, Eq) - Make constants public for reuse in tests - Add Display implementation for FavoriteItem - Add comprehensive test coverage for path validation edge cases - Use AsRef for more flexible string handling - Use functional style with Option methods for prefix handling --- src/system/favorites/item.rs | 117 ++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 10 deletions(-) diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index d2783ca..6d4bdd4 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -1,10 +1,12 @@ +use std::fmt; + use crate::{ finder::Target, system::favorites::{DisplayName, Url}, }; -#[derive(Debug)] -enum MacOsUrl { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacOsUrl { AirDrop, Recents, Applications, @@ -14,22 +16,31 @@ enum MacOsUrl { } impl MacOsUrl { - const AIRDROP: &'static str = "nwnode://domain-AirDrop"; - const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; - const APPLICATIONS: &'static str = "file:///Applications/"; + pub const AIRDROP: &'static str = "nwnode://domain-AirDrop"; + pub const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; + pub const APPLICATIONS: &'static str = "file:///Applications/"; const USER_HOME_FOLDER_DEPTH: usize = 4; // /Users/username/folder/ - fn is_user_downloads(url: &str) -> bool { + fn is_user_downloads(url: impl AsRef) -> bool { Self::is_user_folder(url, "Downloads") } - fn is_user_desktop(url: &str) -> bool { + fn is_user_desktop(url: impl AsRef) -> bool { Self::is_user_folder(url, "Desktop") } - fn is_user_folder(url: &str, folder: &str) -> bool { - let url_path = url.strip_prefix("file://").unwrap_or(url); - url_path.matches('/').count() == Self::USER_HOME_FOLDER_DEPTH + fn is_user_folder(url: impl AsRef, folder: impl AsRef) -> bool { + url.as_ref() + .strip_prefix("file://") + .filter(|url_path| Self::is_inside_home_dir(url_path, folder.as_ref())) + .is_some() + } + + fn is_inside_home_dir(url_path: &str, folder: &str) -> bool { + url_path + .matches('/') + .count() + .eq(&Self::USER_HOME_FOLDER_DEPTH) && url_path.ends_with(&format!("/{}/", folder)) && url_path.starts_with("/Users/") } @@ -48,6 +59,7 @@ impl From<&str> for MacOsUrl { } } +#[derive(Debug, Clone)] pub struct FavoriteItem { url: Url, name: DisplayName, @@ -59,6 +71,12 @@ impl FavoriteItem { } } +impl fmt::Display for FavoriteItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} -> {}", self.name, self.url) + } +} + impl From for Target { fn from(item: FavoriteItem) -> Self { let url = item.url.to_string(); @@ -164,4 +182,83 @@ mod tests { )); assert!(matches!(target, Target::Custom { .. })); } + + #[test] + fn should_not_recognize_shallow_path() { + let target = Target::from(FavoriteItem::new( + create_url("file:///Users/Downloads/"), // Only 3 slashes + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); + } + + #[test] + fn should_format_favorite_item() { + let item = FavoriteItem::new( + create_url("file:///Users/user/Documents/"), + create_display_name("Documents"), + ); + assert_eq!( + format!("{}", item), + "Documents -> file:///Users/user/Documents/" + ); + } + + #[test] + fn should_not_recognize_non_user_path_with_correct_depth() { + let target = Target::from(FavoriteItem::new( + create_url("file:///System/Library/Desktop/"), + create_display_name("Desktop"), + )); + assert!(matches!(target, Target::Custom { .. })); + } + + #[test] + fn should_not_recognize_path_without_file_prefix() { + let raw_path = "/Users/user/Downloads/"; + let url = MacOsUrl::from(raw_path); + assert!(matches!(url, MacOsUrl::Custom(_))); + } + + #[test] + fn should_not_recognize_path_with_different_prefix() { + let path = "http:///Users/user/Downloads/"; + let url = MacOsUrl::from(path); + assert!(matches!(url, MacOsUrl::Custom(_))); + } + + #[test] + fn should_not_recognize_path_with_correct_depth_but_wrong_structure() { + let target = Target::from(FavoriteItem::new( + create_url("file:///one/two/three/four/"), // Right depth but wrong structure + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); + } + #[test] + fn should_not_recognize_path_with_correct_depth_and_prefix_but_wrong_ending() { + let target = Target::from(FavoriteItem::new( + create_url("file:///Users/user/WrongFolder/"), // Right depth and prefix, wrong ending + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); + } + + #[test] + fn should_not_recognize_path_with_correct_ending_but_wrong_structure() { + let target = Target::from(FavoriteItem::new( + create_url("file:///var/tmp/Downloads/"), // Right ending but wrong depth and prefix + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); + } + + #[test] + fn should_not_recognize_path_with_correct_prefix_but_wrong_structure() { + let target = Target::from(FavoriteItem::new( + create_url("file:///Users/Documents/"), // Right prefix but wrong depth and ending + create_display_name("Documents"), + )); + assert!(matches!(target, Target::Custom { .. })); + } } From 0dea957d7187b47d474971ed76c256299e20e4f6 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 22:22:58 +0000 Subject: [PATCH 06/11] refactor: simplify MacOsUrl path validation - Remove redundant is_user_downloads and is_user_desktop helper methods - Add USER_PREFIX, DOWNLOADS, and DESKTOP constants for better maintainability - Simplify is_user_folder by removing separate is_inside_home_dir function - Use direct path component validation instead of string manipulation - Make path validation more explicit with take(2) and component checks - Use constants in pattern matching for better readability --- README.md | 10 +++++---- src/system/favorites/item.rs | 40 ++++++++++++++---------------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b7656a9..630078b 100644 --- a/README.md +++ b/README.md @@ -42,17 +42,19 @@ This project is currently in alpha stage. Progress and next steps: - AirDrop: Shows as "AirDrop" without exposing internal URL - Recents: Shows as "Recents" without exposing internal URL - Applications: Shows as "Applications" without exposing internal URL + - Desktop: Shows as "~/Desktop" for user's desktop folder + - Downloads: Shows as "~/Downloads" for user's downloads folder +- User-friendly path formatting (show regular paths instead of raw URLs) 🚧 **In Progress**: -- User-friendly path formatting (show regular paths instead of raw URLs) +- Support for custom folder locations 🔜 **Planned**: -- Handle special locations: - - User Desktop (`file:///Users//Desktop/`) - - User Downloads (`file:///Users//Downloads/`) - Add/remove favorites - Command-line interface improvements +- Configuration options + ## Documentation - [Architecture Overview](docs/architecture.md) diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index 6d4bdd4..7e6432d 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -16,34 +16,24 @@ pub enum MacOsUrl { } impl MacOsUrl { - pub const AIRDROP: &'static str = "nwnode://domain-AirDrop"; - pub const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; - pub const APPLICATIONS: &'static str = "file:///Applications/"; - const USER_HOME_FOLDER_DEPTH: usize = 4; // /Users/username/folder/ - - fn is_user_downloads(url: impl AsRef) -> bool { - Self::is_user_folder(url, "Downloads") - } - - fn is_user_desktop(url: impl AsRef) -> bool { - Self::is_user_folder(url, "Desktop") - } + const AIRDROP: &'static str = "nwnode://domain-AirDrop"; + const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; + const APPLICATIONS: &'static str = "file:///Applications/"; + const USER_PREFIX: &'static str = "file:///Users/"; + const DOWNLOADS: &'static str = "Downloads"; + const DESKTOP: &'static str = "Desktop"; fn is_user_folder(url: impl AsRef, folder: impl AsRef) -> bool { url.as_ref() - .strip_prefix("file://") - .filter(|url_path| Self::is_inside_home_dir(url_path, folder.as_ref())) + .strip_prefix(Self::USER_PREFIX) + .and_then(|rest| { + let mut parts = rest.split('/').take(2); + let _username = parts.next()?; + let folder_name = parts.next()?; + (folder_name == folder.as_ref()).then_some(()) + }) .is_some() } - - fn is_inside_home_dir(url_path: &str, folder: &str) -> bool { - url_path - .matches('/') - .count() - .eq(&Self::USER_HOME_FOLDER_DEPTH) - && url_path.ends_with(&format!("/{}/", folder)) - && url_path.starts_with("/Users/") - } } impl From<&str> for MacOsUrl { @@ -52,8 +42,8 @@ impl From<&str> for MacOsUrl { Self::AIRDROP => Self::AirDrop, Self::RECENTS => Self::Recents, Self::APPLICATIONS => Self::Applications, - path if Self::is_user_desktop(path) => Self::Desktop, - path if Self::is_user_downloads(path) => Self::Downloads, + _ if Self::is_user_folder(url, Self::DESKTOP) => Self::Desktop, + _ if Self::is_user_folder(url, Self::DOWNLOADS) => Self::Downloads, path => Self::Custom(path.to_string()), } } From ce4374469fb6e81672b11f9cd185938f79eb7cfb Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 22:39:50 +0000 Subject: [PATCH 07/11] feat(favorites): handle custom locations - Strip file:// prefix and trailing slash from custom location paths - Use display name as label for custom locations - Update tests to verify custom location handling - Add constants for Projects location in tests - Remove redundant test case for Documents location The changes ensure custom locations are displayed in a user-friendly format: Label -> last part of the URL Path -> absolute path without file:// prefix and trailing slash --- src/system/favorites/item.rs | 29 +++++++++++--- tests/finder.rs | 77 ++++++++++++++++++++++-------------- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index 7e6432d..5b45a02 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -76,10 +76,16 @@ impl From for Target { MacOsUrl::Applications => Target::Applications, MacOsUrl::Downloads => Target::Downloads, MacOsUrl::Desktop => Target::Desktop, - MacOsUrl::Custom(path) => Target::Custom { - label: item.name.to_string(), - path, - }, + MacOsUrl::Custom(path) => { + let clean_path = path + .strip_prefix("file://") + .map(|p| p.strip_suffix('/').unwrap_or(p)) + .unwrap_or(&path); + Target::Custom { + label: item.name.to_string(), + path: clean_path.to_string(), + } + } } } } @@ -160,7 +166,7 @@ mod tests { )); assert_eq!(target, Target::Custom { label: "Documents".to_string(), - path: "file:///Users/user/Documents/".to_string(), + path: "/Users/user/Documents".to_string(), }); } @@ -251,4 +257,17 @@ mod tests { )); assert!(matches!(target, Target::Custom { .. })); } + + #[test] + fn should_format_custom_location() { + let url = "file:///Users/happygopher/Documents/Projects/"; + let target = Target::from(FavoriteItem::new( + create_url(url), + create_display_name("Projects"), + )); + assert_eq!(target, Target::Custom { + label: "Projects".to_string(), + path: "/Users/happygopher/Documents/Projects".to_string(), + }); + } } diff --git a/tests/finder.rs b/tests/finder.rs index d6fd00b..bff6412 100644 --- a/tests/finder.rs +++ b/tests/finder.rs @@ -8,20 +8,29 @@ mod mock; use mock::{favorites::FavoritesBuilder, mac_os_api::MockMacOsApiBuilder}; mod constants { - // Display Labels + // AirDrop + pub const AIRDROP_URL: &str = "nwnode://domain-AirDrop"; + + // Recents pub const RECENTS_LABEL: &str = "Recents"; + pub const RECENTS_URL: &str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; + + // Applications pub const APPLICATIONS_LABEL: &str = "Applications"; - pub const DOWNLOADS_LABEL: &str = "Downloads"; + pub const APPLICATIONS_URL: &str = "file:///Applications/"; + + // Desktop pub const DESKTOP_LABEL: &str = "Desktop"; - pub const DOCUMENTS_LABEL: &str = "Documents"; + pub const DESKTOP_URL: &str = "file:///Users/user/Desktop/"; - // URLs - pub const AIRDROP_URL: &str = "nwnode://domain-AirDrop"; - pub const RECENTS_URL: &str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; - pub const APPLICATIONS_URL: &str = "file:///Applications/"; + // Downloads + pub const DOWNLOADS_LABEL: &str = "Downloads"; pub const DOWNLOADS_URL: &str = "file:///Users/user/Downloads/"; - pub const DESKTOP_URL: &str = "file:///Users/user/Desktop/"; - pub const DOCUMENTS_URL: &str = "file:///Users/user/Documents/"; + + // Projects + pub const PROJECTS_LABEL: &str = "Projects"; + pub const PROJECTS_PATH: &str = "/Users/user/Projects"; + pub const PROJECTS_URL: &str = "file:///Users/user/Projects/"; } #[test] @@ -69,27 +78,6 @@ fn should_return_empty_list_when_no_favorites() -> Result<()> { Ok(()) } -#[test] -fn should_return_favorite_with_display_name_and_url() -> Result<()> { - // Arrange - let expected_result = vec![SidebarItem::new(Target::Custom { - label: constants::DOCUMENTS_LABEL.to_string(), - path: constants::DOCUMENTS_URL.to_string(), - })]; - let favorites = FavoritesBuilder::new() - .add_item(Some(constants::DOCUMENTS_LABEL), constants::DOCUMENTS_URL) - .build(); - let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); - let finder = Finder::new(mock_api); - - // Act - let result = finder.get_favorites_list()?; - - // Assert - assert_eq!(result, expected_result); - Ok(()) -} - #[test] fn should_handle_airdrop_item() -> Result<()> { // Arrange @@ -172,6 +160,11 @@ fn should_handle_multiple_favorites() -> Result<()> { SidebarItem::new(Target::AirDrop), SidebarItem::new(Target::Applications), SidebarItem::new(Target::Downloads), + SidebarItem::new(Target::Desktop), + SidebarItem::new(Target::Custom { + label: constants::PROJECTS_LABEL.to_string(), + path: constants::PROJECTS_PATH.to_string(), + }), ]; let favorites = FavoritesBuilder::new() .add_item(None, constants::AIRDROP_URL) @@ -180,6 +173,8 @@ fn should_handle_multiple_favorites() -> Result<()> { constants::APPLICATIONS_URL, ) .add_item(Some(constants::DOWNLOADS_LABEL), constants::DOWNLOADS_URL) + .add_item(Some(constants::DESKTOP_LABEL), constants::DESKTOP_URL) + .add_item(Some(constants::PROJECTS_LABEL), constants::PROJECTS_URL) .build(); let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); let finder = Finder::new(mock_api); @@ -209,3 +204,25 @@ fn should_handle_desktop_item() -> Result<()> { assert_eq!(result, expected_result); Ok(()) } + +#[test] +fn should_handle_custom_location() -> Result<()> { + // Arrange + let expected_result = vec![SidebarItem::new(Target::Custom { + label: constants::PROJECTS_LABEL.to_string(), + path: constants::PROJECTS_PATH.to_string(), + })]; + + let favorites = FavoritesBuilder::new() + .add_item(Some(constants::PROJECTS_LABEL), constants::PROJECTS_URL) + .build(); + let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); + let finder = Finder::new(mock_api); + + // Act + let result = finder.get_favorites_list()?; + + // Assert + assert_eq!(result, expected_result); + Ok(()) +} From 934bf6671c99c9d1cdf6e6397fd45847c2ef6ec7 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 23:25:00 +0000 Subject: [PATCH 08/11] refactor(favorites): simplify URL path handling - Move clean_path function to MacOsUrl impl block - Implement From instead of From<&str> for MacOsUrl - Remove duplicate path cleaning logic from Target conversion - Update tests to use proper URL creation --- src/system/favorites/item.rs | 58 ++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index 5b45a02..6723aa1 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -23,6 +23,14 @@ impl MacOsUrl { const DOWNLOADS: &'static str = "Downloads"; const DESKTOP: &'static str = "Desktop"; + fn clean_path(url: impl AsRef) -> String { + url.as_ref() + .strip_prefix("file://") + .and_then(|p| p.strip_suffix('/')) + .unwrap_or(url.as_ref()) + .to_string() + } + fn is_user_folder(url: impl AsRef, folder: impl AsRef) -> bool { url.as_ref() .strip_prefix(Self::USER_PREFIX) @@ -36,15 +44,17 @@ impl MacOsUrl { } } -impl From<&str> for MacOsUrl { - fn from(url: &str) -> Self { - match url { +impl From for MacOsUrl { + fn from(url: Url) -> Self { + let url_str = url.to_string(); + + match url_str.as_str() { Self::AIRDROP => Self::AirDrop, Self::RECENTS => Self::Recents, Self::APPLICATIONS => Self::Applications, - _ if Self::is_user_folder(url, Self::DESKTOP) => Self::Desktop, - _ if Self::is_user_folder(url, Self::DOWNLOADS) => Self::Downloads, - path => Self::Custom(path.to_string()), + url if Self::is_user_folder(url, Self::DESKTOP) => Self::Desktop, + url if Self::is_user_folder(url, Self::DOWNLOADS) => Self::Downloads, + url => Self::Custom(Self::clean_path(url)), } } } @@ -69,23 +79,16 @@ impl fmt::Display for FavoriteItem { impl From for Target { fn from(item: FavoriteItem) -> Self { - let url = item.url.to_string(); - match MacOsUrl::from(url.as_str()) { + match MacOsUrl::from(item.url) { MacOsUrl::AirDrop => Target::AirDrop, MacOsUrl::Recents => Target::Recents, MacOsUrl::Applications => Target::Applications, MacOsUrl::Downloads => Target::Downloads, MacOsUrl::Desktop => Target::Desktop, - MacOsUrl::Custom(path) => { - let clean_path = path - .strip_prefix("file://") - .map(|p| p.strip_suffix('/').unwrap_or(p)) - .unwrap_or(&path); - Target::Custom { - label: item.name.to_string(), - path: clean_path.to_string(), - } - } + MacOsUrl::Custom(path) => Target::Custom { + label: item.name.to_string(), + path, + }, } } } @@ -211,15 +214,17 @@ mod tests { #[test] fn should_not_recognize_path_without_file_prefix() { - let raw_path = "/Users/user/Downloads/"; - let url = MacOsUrl::from(raw_path); - assert!(matches!(url, MacOsUrl::Custom(_))); + let target = Target::from(FavoriteItem::new( + create_url("smb:///Users/user/Downloads/"), + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); } #[test] fn should_not_recognize_path_with_different_prefix() { let path = "http:///Users/user/Downloads/"; - let url = MacOsUrl::from(path); + let url = MacOsUrl::from(create_url(path)); assert!(matches!(url, MacOsUrl::Custom(_))); } @@ -270,4 +275,13 @@ mod tests { path: "/Users/happygopher/Documents/Projects".to_string(), }); } + + #[test] + fn should_treat_non_file_url_as_custom() { + let target = Target::from(FavoriteItem::new( + create_url("http:///Users/user/Downloads/"), + create_display_name("Downloads"), + )); + assert!(matches!(target, Target::Custom { .. })); + } } From 40bd2fa27a4835122e99f0c4dad048ddec9b63fb Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Mon, 23 Dec 2024 00:22:06 +0000 Subject: [PATCH 09/11] refactor(target): add constructor for custom target - Add Target::custom constructor in domain layer - Update all Target::Custom creations to use the constructor - Update tests to use the new constructor --- src/finder/sidebar.rs | 16 +++++++++++----- src/system/favorites/item.rs | 9 +++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/finder/sidebar.rs b/src/finder/sidebar.rs index 1008d39..3289508 100644 --- a/src/finder/sidebar.rs +++ b/src/finder/sidebar.rs @@ -10,6 +10,15 @@ pub enum Target { Custom { label: String, path: String }, } +impl Target { + pub fn custom(label: impl Into, path: impl Into) -> Self { + Self::Custom { + label: label.into(), + path: path.into(), + } + } +} + #[derive(Debug, PartialEq)] pub struct SidebarItem { target: Target, @@ -42,11 +51,8 @@ mod tests { #[test] fn should_create_sidebar_item_with_custom_target() { - let item = SidebarItem::new(Target::Custom { - label: "Documents".to_string(), - path: "/Users/user/Documents".to_string(), - }); - assert_eq!(format!("{}", item), "Documents -> /Users/user/Documents"); + let item = SidebarItem::new(Target::custom("Projects", "/Users/user/Projects")); + assert_eq!(format!("{}", item), "Projects -> /Users/user/Projects"); } #[test] diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index 6723aa1..0c5cd34 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -85,10 +85,7 @@ impl From for Target { MacOsUrl::Applications => Target::Applications, MacOsUrl::Downloads => Target::Downloads, MacOsUrl::Desktop => Target::Desktop, - MacOsUrl::Custom(path) => Target::Custom { - label: item.name.to_string(), - path, - }, + MacOsUrl::Custom(path) => Target::custom(item.name.to_string(), path), } } } @@ -265,14 +262,14 @@ mod tests { #[test] fn should_format_custom_location() { - let url = "file:///Users/happygopher/Documents/Projects/"; + let url = "file:///Users/user/Documents/Projects/"; let target = Target::from(FavoriteItem::new( create_url(url), create_display_name("Projects"), )); assert_eq!(target, Target::Custom { label: "Projects".to_string(), - path: "/Users/happygopher/Documents/Projects".to_string(), + path: "/Users/user/Documents/Projects".to_string(), }); } From 97192222407053053021be713d0dc05a8a822a62 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Mon, 23 Dec 2024 00:39:22 +0000 Subject: [PATCH 10/11] test(favorites): simplify path recognition tests - Rename test to better describe what it verifies - Remove redundant test cases that check the same behavior - Keep only essential test for deep path recognition --- src/system/favorites/item.rs | 95 +----------------------------------- 1 file changed, 2 insertions(+), 93 deletions(-) diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index 0c5cd34..a07881d 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -171,18 +171,9 @@ mod tests { } #[test] - fn should_not_recognize_deep_downloads_path() { + fn should_not_recognize_deep_downloads_path_as_downloads() { let target = Target::from(FavoriteItem::new( - create_url("file:///Users/user/Documents/Downloads/"), - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - - #[test] - fn should_not_recognize_shallow_path() { - let target = Target::from(FavoriteItem::new( - create_url("file:///Users/Downloads/"), // Only 3 slashes + create_url("file:///Users/user/Projects/Downloads/"), create_display_name("Downloads"), )); assert!(matches!(target, Target::Custom { .. })); @@ -199,86 +190,4 @@ mod tests { "Documents -> file:///Users/user/Documents/" ); } - - #[test] - fn should_not_recognize_non_user_path_with_correct_depth() { - let target = Target::from(FavoriteItem::new( - create_url("file:///System/Library/Desktop/"), - create_display_name("Desktop"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - - #[test] - fn should_not_recognize_path_without_file_prefix() { - let target = Target::from(FavoriteItem::new( - create_url("smb:///Users/user/Downloads/"), - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - - #[test] - fn should_not_recognize_path_with_different_prefix() { - let path = "http:///Users/user/Downloads/"; - let url = MacOsUrl::from(create_url(path)); - assert!(matches!(url, MacOsUrl::Custom(_))); - } - - #[test] - fn should_not_recognize_path_with_correct_depth_but_wrong_structure() { - let target = Target::from(FavoriteItem::new( - create_url("file:///one/two/three/four/"), // Right depth but wrong structure - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - #[test] - fn should_not_recognize_path_with_correct_depth_and_prefix_but_wrong_ending() { - let target = Target::from(FavoriteItem::new( - create_url("file:///Users/user/WrongFolder/"), // Right depth and prefix, wrong ending - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - - #[test] - fn should_not_recognize_path_with_correct_ending_but_wrong_structure() { - let target = Target::from(FavoriteItem::new( - create_url("file:///var/tmp/Downloads/"), // Right ending but wrong depth and prefix - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - - #[test] - fn should_not_recognize_path_with_correct_prefix_but_wrong_structure() { - let target = Target::from(FavoriteItem::new( - create_url("file:///Users/Documents/"), // Right prefix but wrong depth and ending - create_display_name("Documents"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - - #[test] - fn should_format_custom_location() { - let url = "file:///Users/user/Documents/Projects/"; - let target = Target::from(FavoriteItem::new( - create_url(url), - create_display_name("Projects"), - )); - assert_eq!(target, Target::Custom { - label: "Projects".to_string(), - path: "/Users/user/Documents/Projects".to_string(), - }); - } - - #[test] - fn should_treat_non_file_url_as_custom() { - let target = Target::from(FavoriteItem::new( - create_url("http:///Users/user/Downloads/"), - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } } From c5fd7b3f96414f024d44c5c18a02fbce9b76ea4a Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Mon, 23 Dec 2024 00:50:51 +0000 Subject: [PATCH 11/11] featsimplify target handling to use full paths - Remove special Desktop and Downloads targets - Treat all user directory items as custom locations - Update tests to reflect simplified target handling - Remove unused constants and test cases BREAKING CHANGE: Desktop and Downloads are now handled as custom locations with full paths instead of special targets with ~ notation. --- src/finder/sidebar.rs | 16 ---------- src/system/favorites/item.rs | 62 ++++-------------------------------- tests/finder.rs | 48 ---------------------------- 3 files changed, 7 insertions(+), 119 deletions(-) diff --git a/src/finder/sidebar.rs b/src/finder/sidebar.rs index 3289508..0f9cd39 100644 --- a/src/finder/sidebar.rs +++ b/src/finder/sidebar.rs @@ -5,8 +5,6 @@ pub enum Target { AirDrop, Recents, Applications, - Downloads, - Desktop, Custom { label: String, path: String }, } @@ -36,8 +34,6 @@ impl fmt::Display for SidebarItem { Target::AirDrop => write!(f, "AirDrop"), Target::Recents => write!(f, "Recents"), Target::Applications => write!(f, "Applications"), - Target::Downloads => write!(f, "~/Downloads"), - Target::Desktop => write!(f, "~/Desktop"), Target::Custom { label, path } => write!(f, "{} -> {}", label, path), } } @@ -72,16 +68,4 @@ mod tests { let item = SidebarItem::new(Target::Applications); assert_eq!(format!("{}", item), "Applications"); } - - #[test] - fn should_create_sidebar_item_with_downloads() { - let item = SidebarItem::new(Target::Downloads); - assert_eq!(format!("{}", item), "~/Downloads"); - } - - #[test] - fn should_create_sidebar_item_with_desktop() { - let item = SidebarItem::new(Target::Desktop); - assert_eq!(format!("{}", item), "~/Desktop"); - } } diff --git a/src/system/favorites/item.rs b/src/system/favorites/item.rs index a07881d..7a9ee10 100644 --- a/src/system/favorites/item.rs +++ b/src/system/favorites/item.rs @@ -10,8 +10,6 @@ pub enum MacOsUrl { AirDrop, Recents, Applications, - Downloads, - Desktop, Custom(String), } @@ -19,9 +17,6 @@ impl MacOsUrl { const AIRDROP: &'static str = "nwnode://domain-AirDrop"; const RECENTS: &'static str = "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/"; const APPLICATIONS: &'static str = "file:///Applications/"; - const USER_PREFIX: &'static str = "file:///Users/"; - const DOWNLOADS: &'static str = "Downloads"; - const DESKTOP: &'static str = "Desktop"; fn clean_path(url: impl AsRef) -> String { url.as_ref() @@ -30,18 +25,6 @@ impl MacOsUrl { .unwrap_or(url.as_ref()) .to_string() } - - fn is_user_folder(url: impl AsRef, folder: impl AsRef) -> bool { - url.as_ref() - .strip_prefix(Self::USER_PREFIX) - .and_then(|rest| { - let mut parts = rest.split('/').take(2); - let _username = parts.next()?; - let folder_name = parts.next()?; - (folder_name == folder.as_ref()).then_some(()) - }) - .is_some() - } } impl From for MacOsUrl { @@ -52,8 +35,6 @@ impl From for MacOsUrl { Self::AIRDROP => Self::AirDrop, Self::RECENTS => Self::Recents, Self::APPLICATIONS => Self::Applications, - url if Self::is_user_folder(url, Self::DESKTOP) => Self::Desktop, - url if Self::is_user_folder(url, Self::DOWNLOADS) => Self::Downloads, url => Self::Custom(Self::clean_path(url)), } } @@ -83,8 +64,6 @@ impl From for Target { MacOsUrl::AirDrop => Target::AirDrop, MacOsUrl::Recents => Target::Recents, MacOsUrl::Applications => Target::Applications, - MacOsUrl::Downloads => Target::Downloads, - MacOsUrl::Desktop => Target::Desktop, MacOsUrl::Custom(path) => Target::custom(item.name.to_string(), path), } } @@ -140,54 +119,27 @@ mod tests { assert_eq!(target, Target::Applications); } - #[test] - fn should_convert_downloads_url() { - let target = Target::from(FavoriteItem::new( - create_url("file:///Users/user/Downloads/"), - create_display_name("Downloads"), - )); - assert_eq!(target, Target::Downloads); - } - - #[test] - fn should_convert_desktop_url() { - let target = Target::from(FavoriteItem::new( - create_url("file:///Users/user/Desktop/"), - create_display_name("Desktop"), - )); - assert_eq!(target, Target::Desktop); - } - #[test] fn should_convert_custom_url() { let target = Target::from(FavoriteItem::new( - create_url("file:///Users/user/Documents/"), - create_display_name("Documents"), + create_url("file:///Users/user/Projects/"), + create_display_name("Projects"), )); assert_eq!(target, Target::Custom { - label: "Documents".to_string(), - path: "/Users/user/Documents".to_string(), + label: "Projects".to_string(), + path: "/Users/user/Projects".to_string(), }); } - #[test] - fn should_not_recognize_deep_downloads_path_as_downloads() { - let target = Target::from(FavoriteItem::new( - create_url("file:///Users/user/Projects/Downloads/"), - create_display_name("Downloads"), - )); - assert!(matches!(target, Target::Custom { .. })); - } - #[test] fn should_format_favorite_item() { let item = FavoriteItem::new( - create_url("file:///Users/user/Documents/"), - create_display_name("Documents"), + create_url("file:///Users/user/Projects/"), + create_display_name("Projects"), ); assert_eq!( format!("{}", item), - "Documents -> file:///Users/user/Documents/" + "Projects -> file:///Users/user/Projects/" ); } } diff --git a/tests/finder.rs b/tests/finder.rs index bff6412..40264d5 100644 --- a/tests/finder.rs +++ b/tests/finder.rs @@ -19,14 +19,6 @@ mod constants { pub const APPLICATIONS_LABEL: &str = "Applications"; pub const APPLICATIONS_URL: &str = "file:///Applications/"; - // Desktop - pub const DESKTOP_LABEL: &str = "Desktop"; - pub const DESKTOP_URL: &str = "file:///Users/user/Desktop/"; - - // Downloads - pub const DOWNLOADS_LABEL: &str = "Downloads"; - pub const DOWNLOADS_URL: &str = "file:///Users/user/Downloads/"; - // Projects pub const PROJECTS_LABEL: &str = "Projects"; pub const PROJECTS_PATH: &str = "/Users/user/Projects"; @@ -135,32 +127,12 @@ fn should_handle_applications_item() -> Result<()> { Ok(()) } -#[test] -fn should_handle_downloads_item() -> Result<()> { - // Arrange - let expected_result = vec![SidebarItem::new(Target::Downloads)]; - let favorites = FavoritesBuilder::new() - .add_item(Some(constants::DOWNLOADS_LABEL), constants::DOWNLOADS_URL) - .build(); - let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); - let finder = Finder::new(mock_api); - - // Act - let result = finder.get_favorites_list()?; - - // Assert - assert_eq!(result, expected_result); - Ok(()) -} - #[test] fn should_handle_multiple_favorites() -> Result<()> { // Arrange let expected_result = vec![ SidebarItem::new(Target::AirDrop), SidebarItem::new(Target::Applications), - SidebarItem::new(Target::Downloads), - SidebarItem::new(Target::Desktop), SidebarItem::new(Target::Custom { label: constants::PROJECTS_LABEL.to_string(), path: constants::PROJECTS_PATH.to_string(), @@ -172,8 +144,6 @@ fn should_handle_multiple_favorites() -> Result<()> { Some(constants::APPLICATIONS_LABEL), constants::APPLICATIONS_URL, ) - .add_item(Some(constants::DOWNLOADS_LABEL), constants::DOWNLOADS_URL) - .add_item(Some(constants::DESKTOP_LABEL), constants::DESKTOP_URL) .add_item(Some(constants::PROJECTS_LABEL), constants::PROJECTS_URL) .build(); let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); @@ -187,24 +157,6 @@ fn should_handle_multiple_favorites() -> Result<()> { Ok(()) } -#[test] -fn should_handle_desktop_item() -> Result<()> { - // Arrange - let expected_result = vec![SidebarItem::new(Target::Desktop)]; - let favorites = FavoritesBuilder::new() - .add_item(Some(constants::DESKTOP_LABEL), constants::DESKTOP_URL) - .build(); - let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build(); - let finder = Finder::new(mock_api); - - // Act - let result = finder.get_favorites_list()?; - - // Assert - assert_eq!(result, expected_result); - Ok(()) -} - #[test] fn should_handle_custom_location() -> Result<()> { // Arrange