app_info/
lib.rs

1pub mod error;
2pub mod macos;
3pub mod window;
4
5use error::{AppInfoError, Result};
6use std::path::PathBuf;
7
8/// Application information
9#[derive(Debug, Clone)]
10pub struct AppInfo {
11    /// Application name
12    pub name: String,
13    /// Application version
14    pub version: Option<String>,
15    /// Application path
16    pub path: PathBuf,
17    /// Application icon (RGBA format)
18    pub icon: Option<Icon>,
19    /// Application bundle identifier (macOS) or ProductCode (Windows)
20    pub identifier: Option<String>,
21    /// Developer/Publisher
22    pub publisher: Option<String>,
23    /// Installation date
24    pub install_date: Option<String>,
25}
26
27/// Icon data
28#[derive(Debug, Clone)]
29pub struct Icon {
30    /// Icon width in pixels
31    pub width: u32,
32    /// Icon height in pixels
33    pub height: u32,
34    /// Pixel data in RGBA format
35    pub pixels: Vec<u8>,
36}
37
38/// Gets all installed applications.
39///
40/// # Arguments
41///
42/// * `icon_size` - The desired icon size. If 0, no icon will be fetched.
43///
44/// # Returns
45///
46/// A vector containing information about all installed applications.
47pub fn get_installed_apps(icon_size: u16) -> Result<Vec<AppInfo>> {
48    #[cfg(target_os = "macos")]
49    return macos::get_installed_apps(icon_size);
50
51    #[cfg(target_os = "windows")]
52    return window::get_installed_apps(icon_size);
53
54    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
55    Err(AppInfoError::UnsupportedPlatform)
56}
57
58/// Finds a specific application by its name.
59///
60/// # Arguments
61///
62/// * `name` - The name of the application to find.
63/// * `icon_size` - The desired icon size. If 0, no icon will be fetched.
64///
65/// # Returns
66///
67/// Information about the matched application.
68pub fn find_app_by_name(name: &str, icon_size: u16) -> Result<AppInfo> {
69    let apps = get_installed_apps(icon_size)?;
70    apps.into_iter()
71        .find(|app| app.name.eq_ignore_ascii_case(name))
72        .ok_or_else(|| AppInfoError::AppNotFound {
73            name: name.to_string(),
74        })
75}
76
77/// Gets the icon for a given file path.
78pub fn get_file_icon(path: impl AsRef<std::path::Path>, size: u16) -> Result<Icon> {
79    let path = path.as_ref();
80    if !path.exists() {
81        return Err(AppInfoError::FileIconError(
82            error::FileIconError::PathDoesNotExist,
83        ));
84    }
85
86    if size == 0 {
87        return Err(AppInfoError::FileIconError(
88            error::FileIconError::NullIconSize,
89        ));
90    }
91
92    #[cfg(target_os = "macos")]
93    return macos::get_file_icon(path, size);
94
95    #[cfg(target_os = "windows")]
96    return window::get_file_icon(path, size);
97
98    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
99    Err(AppInfoError::FileIconError(
100        error::FileIconError::PlatformNotSupported,
101    ))
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_get_installed_apps() {
110        // Test without icons
111        let apps = get_installed_apps(0).expect("Failed to get installed apps");
112        assert!(!apps.is_empty(), "Should find at least one application");
113        println!("Applications found: {:?}", apps);
114
115        // Test with icons
116        let apps_with_icons =
117            get_installed_apps(32).expect("Failed to get installed apps with icons");
118        assert!(!apps_with_icons.is_empty());
119
120        // On supported platforms, at least one app should have an icon
121        #[cfg(any(target_os = "macos", target_os = "windows"))]
122        {
123            let app_with_icon = apps_with_icons.iter().find(|app| app.icon.is_some());
124            assert!(
125                app_with_icon.is_some(),
126                "At least one app should have an icon"
127            );
128
129            if let Some(app) = app_with_icon {
130                let icon = app.icon.as_ref().unwrap();
131                assert_eq!(icon.width, 32);
132                assert_eq!(icon.height, 32);
133                assert!(!icon.pixels.is_empty());
134                assert_eq!(icon.pixels.len(), (32 * 32 * 4) as usize);
135            }
136        }
137    }
138
139    #[test]
140    fn test_find_app_by_name() {
141        let apps = get_installed_apps(0).unwrap();
142        if apps.is_empty() {
143            // Skip this test if no apps are installed
144            return;
145        }
146
147        // Test with the first app from the list
148        let first_app_name = &apps[0].name;
149        let found_app = find_app_by_name(first_app_name, 0).unwrap();
150        assert_eq!(found_app.name, *first_app_name);
151
152        // Test case-insensitivity
153        let found_app_lower = find_app_by_name(&first_app_name.to_lowercase(), 0).unwrap();
154        assert_eq!(found_app_lower.name, *first_app_name);
155
156        // Test for an app that doesn't exist
157        let not_found_result = find_app_by_name("ThisAppSurelyDoesNotExist12345", 0);
158        assert!(matches!(
159            not_found_result,
160            Err(AppInfoError::AppNotFound { .. })
161        ));
162    }
163
164    #[test]
165    fn test_get_file_icon() {
166        // Choose a path that is likely to exist on different platforms
167        let path_to_test = if cfg!(target_os = "macos") {
168            "/System/Applications/Calculator.app"
169        } else if cfg!(target_os = "windows") {
170            "C:\\Windows\\System32\\notepad.exe"
171        } else {
172            // On other OS, we don't have a default path, so skip the rest of the test
173            return;
174        };
175
176        let path = std::path::Path::new(path_to_test);
177
178        // Test with size 0, should return an error
179        let result = get_file_icon(path, 0);
180        assert!(matches!(
181            result,
182            Err(AppInfoError::FileIconError(
183                error::FileIconError::NullIconSize
184            ))
185        ));
186
187        // Test with a non-existent path
188        let result = get_file_icon("/path/to/non/existent/file", 32);
189        assert!(matches!(
190            result,
191            Err(AppInfoError::FileIconError(
192                error::FileIconError::PathDoesNotExist
193            ))
194        ));
195
196        // If the test path exists, test getting a real icon
197        if path.exists() {
198            let icon = get_file_icon(path, 64).expect("Failed to get file icon");
199            assert_eq!(icon.width, 64);
200            assert_eq!(icon.height, 64);
201            assert!(!icon.pixels.is_empty());
202            assert_eq!(icon.pixels.len(), (64 * 64 * 4) as usize);
203        }
204    }
205}