app-info 0.1.1

Get the installed apps and icons on the device
Documentation
use app_info::{get_installed_apps, AppInfo, Icon};
use image::ColorType;
use std::fs;
use std::path::{Path, PathBuf};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("Fetching installed applications...");

    // Get all installed applications, icon size is 64x64
    let apps = get_installed_apps(64)?;

    println!("Found {} applications", apps.len());

    // Create output directory
    let output_dir = "app_icons";
    fs::create_dir_all(output_dir)?;

    let mut saved_count = 0;

    for app in apps {
        if let Some(ref icon) = app.icon {
            match save_icon_as_png(&app, icon, output_dir) {
                Ok(filename) => {
                    saved_count += 1;
                    println!("Saved: {}", filename);
                }
                Err(e) => {
                    eprintln!("Error saving icon for {}: {}", app.name, e);
                }
            }
        } else {
            println!("Skipped {} (no icon)", app.name);
        }
    }

    println!(
        "Done! Saved {} icons to the {} directory",
        saved_count, output_dir
    );
    Ok(())
}

fn save_icon_as_png(
    app: &AppInfo,
    icon: &Icon,
    output_dir: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    // Sanitize application name by removing invalid filename characters
    let safe_name = sanitize_filename(&app.name);
    let filename = unique_output_path(Path::new(output_dir), &safe_name);

    image::save_buffer(
        &filename,
        &icon.pixels,
        icon.width,
        icon.height,
        ColorType::Rgba8,
    )?;

    Ok(filename.display().to_string())
}

fn sanitize_filename(name: &str) -> String {
    let sanitized: String = name
        .chars()
        .map(|c| match c {
            // Replace invalid filename characters
            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
            c => c,
        })
        .collect::<String>()
        .trim_matches([' ', '.'])
        .to_string()
        .chars()
        .take(120)
        .collect();
    if sanitized.is_empty() {
        "application".to_string()
    } else {
        sanitized
    }
}

fn unique_output_path(directory: &Path, stem: &str) -> PathBuf {
    let initial = directory.join(format!("{stem}.png"));
    if !initial.exists() {
        return initial;
    }

    for suffix in 2.. {
        let candidate = directory.join(format!("{stem}-{suffix}.png"));
        if !candidate.exists() {
            return candidate;
        }
    }
    unreachable!("an unused numeric filename suffix always exists")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sanitizes_empty_and_invalid_names() {
        assert_eq!(sanitize_filename("..."), "application");
        assert_eq!(sanitize_filename("A/B:C"), "A_B_C");
    }
}