diff --git a/README.md b/README.md index 4233b9a..b7656a9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # FavKit +[![Build Status](https://github.com/screwyprof/favkit/actions/workflows/rust.yml/badge.svg)](https://github.com/screwyprof/favkit/actions/workflows/rust.yml) [![codecov](https://codecov.io/gh/screwyprof/favkit/graph/badge.svg?token=B5ARXL56RN)](https://codecov.io/gh/screwyprof/favkit) +[![Minimum Rust Version](https://img.shields.io/badge/MSRV-nightly-red)](https://github.com/rust-lang/api-guidelines/discussions/231) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) A modern Rust library and CLI tool for managing macOS Finder favorites. This project is a modern replacement for the abandoned [mysides](https://github.com/mosen/mysides) tool. @@ -35,13 +38,18 @@ This project is currently in alpha stage. Progress and next steps: ✅ **Completed**: - Basic viewing of Finder favorites -- Proper display names for special locations (AirDrop) +- Proper display names for special locations: + - AirDrop: Shows as "AirDrop" without exposing internal URL + - Recents: Shows as "Recents" without exposing internal URL + - Applications: Shows as "Applications" without exposing internal URL 🚧 **In Progress**: - User-friendly path formatting (show regular paths instead of raw URLs) -- Improve display names for system locations (Recents) 🔜 **Planned**: +- Handle special locations: + - User Desktop (`file:///Users//Desktop/`) + - User Downloads (`file:///Users//Downloads/`) - Add/remove favorites - Command-line interface improvements @@ -61,16 +69,18 @@ The project uses nix + direnv for reproducible development environment: direnv allow # See all available commands -make test +make +# or +make help # Run development tools -make # run linters, tests and build project +make all # run linters, tests and build project ``` -## License +## Contributing -MIT +See our [Contributing Guide](CONTRIBUTING.md) for details on how to get involved with the project. -## Contributing +## License -See our [Contributing Guide](CONTRIBUTING.md) for details on how to get involved with the project. \ No newline at end of file +[MIT](LICENSE) diff --git a/src/finder/sidebar.rs b/src/finder/sidebar.rs index 4ed7597..b8ec0f2 100644 --- a/src/finder/sidebar.rs +++ b/src/finder/sidebar.rs @@ -3,6 +3,8 @@ use std::fmt; #[derive(Debug, PartialEq)] pub enum Target { AirDrop, + Recents, + Applications, Custom { label: String, path: String }, } @@ -21,6 +23,8 @@ impl fmt::Display for SidebarItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.target { Target::AirDrop => write!(f, "AirDrop"), + Target::Recents => write!(f, "Recents"), + Target::Applications => write!(f, "Applications"), Target::Custom { label, path } => write!(f, "{} -> {}", label, path), } } @@ -28,18 +32,17 @@ impl fmt::Display for SidebarItem { #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; + use super::*; #[test] - fn should_create_sidebar_item_with_display_name() { + fn should_create_sidebar_item_with_custom_target() { let item = SidebarItem::new(Target::Custom { label: "Documents".to_string(), - path: "file:///Users/user/Documents".to_string(), + path: "/Users/user/Documents".to_string(), }); - assert_eq!( - format!("{}", item), - "Documents -> file:///Users/user/Documents" - ); + assert_eq!(format!("{}", item), "Documents -> /Users/user/Documents"); } #[test] @@ -47,4 +50,16 @@ mod tests { let item = SidebarItem::new(Target::AirDrop); assert_eq!(format!("{}", item), "AirDrop"); } + + #[test] + fn should_create_sidebar_item_with_recents() { + let item = SidebarItem::new(Target::Recents); + assert_eq!(format!("{}", item), "Recents"); + } + + #[test] + fn should_create_sidebar_item_with_applications() { + let item = SidebarItem::new(Target::Applications); + assert_eq!(format!("{}", item), "Applications"); + } } diff --git a/src/system/favorites/url_mapper.rs b/src/system/favorites/url_mapper.rs index a5640e0..8fdc220 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/url_mapper.rs @@ -5,14 +5,102 @@ 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/"; + +enum MacOsUrl { + AirDrop, + Recents, + Applications, + Custom(String), +} + +impl From<&str> for MacOsUrl { + fn from(url: &str) -> Self { + match url { + AIRDROP_URL => MacOsUrl::AirDrop, + RECENTS_URL => MacOsUrl::Recents, + APPLICATIONS_URL => MacOsUrl::Applications, + path => MacOsUrl::Custom(path.to_string()), + } + } +} + impl From for Target { fn from(target: TargetUrl) -> Self { - match target.0.to_string().as_str() { - "nwnode://domain-AirDrop" => Target::AirDrop, - path => Target::Custom { + let url = target.0.to_string(); + match MacOsUrl::from(url.as_str()) { + MacOsUrl::AirDrop => Target::AirDrop, + MacOsUrl::Recents => Target::Recents, + MacOsUrl::Applications => Target::Applications, + MacOsUrl::Custom(path) => Target::Custom { label: target.1.to_string(), - path: path.to_string(), + path, }, } } } + +#[cfg(test)] +mod tests { + use core_foundation::{ + base::TCFType, + string::CFString, + url::{CFURL, kCFURLPOSIXPathStyle}, + }; + use pretty_assertions::assert_eq; + + use super::*; + + fn create_url(path: &str) -> Url { + let cf_string = CFString::new(path); + let is_dir = path.ends_with('/'); + let cf_url = CFURL::from_file_system_path(cf_string, kCFURLPOSIXPathStyle, is_dir); + Url::try_from(cf_url.as_concrete_TypeRef()).unwrap() + } + + fn create_display_name(name: &str) -> DisplayName { + let cf_string = CFString::new(name); + DisplayName::try_from(cf_string.as_concrete_TypeRef()).unwrap() + } + + #[test] + fn should_convert_airdrop_url() { + let target = Target::from(TargetUrl( + create_url(AIRDROP_URL), + create_display_name("AirDrop"), + )); + assert_eq!(target, Target::AirDrop); + } + + #[test] + fn should_convert_recents_url() { + let target = Target::from(TargetUrl( + create_url(RECENTS_URL), + create_display_name("Recents"), + )); + assert_eq!(target, Target::Recents); + } + + #[test] + fn should_convert_applications_url() { + let target = Target::from(TargetUrl( + create_url(APPLICATIONS_URL), + create_display_name("Applications"), + )); + assert_eq!(target, Target::Applications); + } + + #[test] + fn should_convert_custom_url() { + let target = Target::from(TargetUrl( + create_url("file:///Users/user/Documents"), + create_display_name("Documents"), + )); + assert_eq!(target, Target::Custom { + label: "Documents".to_string(), + path: "file:///Users/user/Documents".to_string(), + }); + } +} diff --git a/tests/finder.rs b/tests/finder.rs index d6c6387..90b18bf 100644 --- a/tests/finder.rs +++ b/tests/finder.rs @@ -11,6 +11,8 @@ mod constants { pub const DOCUMENTS_NAME: &str = "Documents"; pub const DOCUMENTS_PATH: &str = "/Users/user/Documents/"; 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/"; } #[test] @@ -97,15 +99,48 @@ fn should_handle_airdrop_item() -> Result<()> { Ok(()) } +#[test] +fn should_handle_recents_item() -> Result<()> { + // Arrange + let expected_result = vec![SidebarItem::new(Target::Recents)]; + let favorites = FavoritesBuilder::new() + .add_item(Some("Recents"), constants::RECENTS_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_applications_item() -> Result<()> { + // Arrange + let expected_result = vec![SidebarItem::new(Target::Applications)]; + let favorites = FavoritesBuilder::new() + .add_item(Some("Applications"), constants::APPLICATIONS_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::Custom { - label: "Applications".to_string(), - path: "file:///Applications/".to_string(), - }), + SidebarItem::new(Target::Applications), SidebarItem::new(Target::Custom { label: "Downloads".to_string(), path: "file:///Users/user/Downloads/".to_string(), @@ -113,7 +148,7 @@ fn should_handle_multiple_favorites() -> Result<()> { ]; let favorites = FavoritesBuilder::new() .add_item(None, constants::AIRDROP_URL) - .add_item(Some("Applications"), "/Applications/") + .add_item(Some("Applications"), constants::APPLICATIONS_URL) .add_item(Some("Downloads"), "/Users/user/Downloads/") .build(); let mock_api = MockMacOsApiBuilder::new().with_favorites(favorites).build();