From 6a121252ad8168bdd2763e1419bc99e4da94e1d4 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 02:34:17 +0000 Subject: [PATCH 1/4] feat: handle Recents in Finder sidebar - Add Recents variant to Target enum - Implement MacOsUrl enum for mapping system URLs to targets - Update README to reflect Recents support --- README.md | 6 ++- src/finder/sidebar.rs | 8 +++ src/system/favorites/url_mapper.rs | 83 ++++++++++++++++++++++++++++-- tests/finder.rs | 19 +++++++ 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4233b9a..8beeb9a 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,15 @@ 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 🚧 **In Progress**: - User-friendly path formatting (show regular paths instead of raw URLs) -- Improve display names for system locations (Recents) 🔜 **Planned**: +- Handle Applications folder (`file:///Applications/`) in Finder sidebar - Add/remove favorites - Command-line interface improvements diff --git a/src/finder/sidebar.rs b/src/finder/sidebar.rs index 4ed7597..db39193 100644 --- a/src/finder/sidebar.rs +++ b/src/finder/sidebar.rs @@ -3,6 +3,7 @@ use std::fmt; #[derive(Debug, PartialEq)] pub enum Target { AirDrop, + Recents, Custom { label: String, path: String }, } @@ -21,6 +22,7 @@ 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::Custom { label, path } => write!(f, "{} -> {}", label, path), } } @@ -47,4 +49,10 @@ 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"); + } } diff --git a/src/system/favorites/url_mapper.rs b/src/system/favorites/url_mapper.rs index a5640e0..a916a0c 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/url_mapper.rs @@ -5,14 +5,89 @@ use crate::{ pub struct TargetUrl(pub Url, pub DisplayName); +enum MacOsUrl { + AirDrop, + Recents, + Custom(String), +} + +impl From<&str> for MacOsUrl { + fn from(url: &str) -> Self { + match url { + "nwnode://domain-AirDrop" => MacOsUrl::AirDrop, + "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/" => { + MacOsUrl::Recents + } + 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::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 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("nwnode://domain-AirDrop"), + create_display_name("AirDrop"), + )); + assert_eq!(target, Target::AirDrop); + } + + #[test] + fn should_convert_recents_url() { + let target = Target::from(TargetUrl( + create_url( + "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/", + ), + create_display_name("Recents"), + )); + assert_eq!(target, Target::Recents); + } + + #[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..f3b8fd2 100644 --- a/tests/finder.rs +++ b/tests/finder.rs @@ -11,6 +11,7 @@ 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/"; } #[test] @@ -97,6 +98,24 @@ 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_multiple_favorites() -> Result<()> { // Arrange From cbb182576bf6d60afb74bbbbc613d7b08495ec76 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 02:40:28 +0000 Subject: [PATCH 2/4] refactor: introduce constants for system URLs - Add constants for AirDrop and Recents URLs in url_mapper - Add pretty assertions for better test output --- src/system/favorites/url_mapper.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/system/favorites/url_mapper.rs b/src/system/favorites/url_mapper.rs index a916a0c..c53e477 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/url_mapper.rs @@ -5,6 +5,9 @@ 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/"; + enum MacOsUrl { AirDrop, Recents, @@ -14,10 +17,8 @@ enum MacOsUrl { impl From<&str> for MacOsUrl { fn from(url: &str) -> Self { match url { - "nwnode://domain-AirDrop" => MacOsUrl::AirDrop, - "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/" => { - MacOsUrl::Recents - } + AIRDROP_URL => MacOsUrl::AirDrop, + RECENTS_URL => MacOsUrl::Recents, path => MacOsUrl::Custom(path.to_string()), } } @@ -44,6 +45,7 @@ mod tests { string::CFString, url::{CFURL, kCFURLPOSIXPathStyle}, }; + use pretty_assertions::assert_eq; use super::*; @@ -62,7 +64,7 @@ mod tests { #[test] fn should_convert_airdrop_url() { let target = Target::from(TargetUrl( - create_url("nwnode://domain-AirDrop"), + create_url(AIRDROP_URL), create_display_name("AirDrop"), )); assert_eq!(target, Target::AirDrop); @@ -71,9 +73,7 @@ mod tests { #[test] fn should_convert_recents_url() { let target = Target::from(TargetUrl( - create_url( - "file:///System/Library/CoreServices/Finder.app/Contents/Resources/MyLibraries/myDocuments.cannedSearch/", - ), + create_url(RECENTS_URL), create_display_name("Recents"), )); assert_eq!(target, Target::Recents); From 048e6673d64aa2bd40f0d3933af019cc78c6e648 Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 02:48:58 +0000 Subject: [PATCH 3/4] feat: handle Applications in Finder sidebar - Add Applications variant to Target enum - Add Applications URL mapping in url_mapper - Update README to reflect Applications support and add Desktop/Downloads to planned features --- README.md | 5 ++++- src/finder/sidebar.rs | 19 +++++++++++++------ src/system/favorites/url_mapper.rs | 13 +++++++++++++ tests/finder.rs | 26 +++++++++++++++++++++----- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8beeb9a..3ace875 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,15 @@ This project is currently in alpha stage. Progress and next steps: - 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) 🔜 **Planned**: -- Handle Applications folder (`file:///Applications/`) in Finder sidebar +- Handle special locations: + - User Desktop (`file:///Users//Desktop/`) + - User Downloads (`file:///Users//Downloads/`) - Add/remove favorites - Command-line interface improvements diff --git a/src/finder/sidebar.rs b/src/finder/sidebar.rs index db39193..b8ec0f2 100644 --- a/src/finder/sidebar.rs +++ b/src/finder/sidebar.rs @@ -4,6 +4,7 @@ use std::fmt; pub enum Target { AirDrop, Recents, + Applications, Custom { label: String, path: String }, } @@ -23,6 +24,7 @@ impl fmt::Display for SidebarItem { 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), } } @@ -30,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] @@ -55,4 +56,10 @@ mod tests { 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 c53e477..8fdc220 100644 --- a/src/system/favorites/url_mapper.rs +++ b/src/system/favorites/url_mapper.rs @@ -7,10 +7,12 @@ 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), } @@ -19,6 +21,7 @@ impl From<&str> for MacOsUrl { match url { AIRDROP_URL => MacOsUrl::AirDrop, RECENTS_URL => MacOsUrl::Recents, + APPLICATIONS_URL => MacOsUrl::Applications, path => MacOsUrl::Custom(path.to_string()), } } @@ -30,6 +33,7 @@ impl From for Target { 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, @@ -79,6 +83,15 @@ mod tests { 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( diff --git a/tests/finder.rs b/tests/finder.rs index f3b8fd2..90b18bf 100644 --- a/tests/finder.rs +++ b/tests/finder.rs @@ -12,6 +12,7 @@ mod constants { 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] @@ -116,15 +117,30 @@ fn should_handle_recents_item() -> 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(), @@ -132,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(); From 5ee33fa27d792bbb76e839633ce1aa08857bf02d Mon Sep 17 00:00:00 2001 From: Happy Gopher Date: Sun, 22 Dec 2024 17:15:00 +0000 Subject: [PATCH 4/4] docs: improve README badges and organization - Add Build Status badge linking to GitHub Actions - Add MSRV (Minimum Supported Rust Version) badge - Add MIT License badge - Fix make commands documentation - Reorganize sections (move Contributing before License) - Improve License section formatting with link --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3ace875..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. @@ -66,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)