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...");
let apps = get_installed_apps(64)?;
println!("Found {} applications", apps.len());
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>> {
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 {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
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");
}
}