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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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/<user>/Desktop/`)
- User Downloads (`file:///Users/<user>/Downloads/`)
- Add/remove favorites
- Command-line interface improvements

Expand All @@ -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.
[MIT](LICENSE)
27 changes: 21 additions & 6 deletions src/finder/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::fmt;
#[derive(Debug, PartialEq)]
pub enum Target {
AirDrop,
Recents,
Applications,
Custom { label: String, path: String },
}

Expand All @@ -21,30 +23,43 @@ 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),
}
}
}

#[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]
fn should_create_sidebar_item_with_airdrop() {
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");
}
}
96 changes: 92 additions & 4 deletions src/system/favorites/url_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TargetUrl> 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(),
});
}
}
45 changes: 40 additions & 5 deletions tests/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -97,23 +99,56 @@ 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(),
}),
];
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();
Expand Down