use cached::proc_macro::cached; use color_eyre::eyre::Result; use linicon; use resvg::render; use resvg::tiny_skia::{Pixmap, Transform}; use resvg::usvg::{FitTo, Tree}; use std::ffi::OsString; use std::fs::create_dir_all; use std::io::{BufRead, BufReader, ErrorKind}; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{env, fs}; use walkdir::WalkDir; fn get_data_dirs() -> Vec { let xdg_data_dirs_str = std::env::var("XDG_DATA_DIRS").unwrap_or_default(); let xdg_data_dirs = xdg_data_dirs_str .split(":") .filter_map(|dir| PathBuf::from_str(dir).ok()); let data_home = dirs::home_dir() .unwrap_or(PathBuf::from_str("/usr/share/").expect( "No XDG_DATA_DIR set, no HOME directory found and no /usr/share direcotry found", )) .join(".local") .join("share"); xdg_data_dirs .chain([data_home].into_iter()) .filter(|dir| dir.exists() && dir.is_dir()) .collect() } fn get_app_dirs() -> Vec { get_data_dirs() .into_iter() .map(|dir| dir.join("applications")) .filter(|dir| dir.exists() && dir.is_dir()) .collect() } pub fn get_desktop_files() -> Vec { let desktop_extension = OsString::from_str("desktop").unwrap(); // Get the list of directories to search let app_dirs = get_app_dirs(); app_dirs .into_iter() .flat_map(|dir| { // Follow symlinks and recursively search directories WalkDir::new(dir) .follow_links(true) .into_iter() .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().is_file()) .map(|entry| entry.path().to_path_buf()) }) .filter(|path| path.extension() == Some(&desktop_extension)) .collect::>() } #[test] fn test_get_desktop_files() { let desktop_files = get_desktop_files(); dbg!(&desktop_files); assert!(desktop_files .iter() .any(|file| file.ends_with("gimp.desktop"))); } pub fn parse_desktop_file(path: PathBuf) -> Result { // Open the file in read-only mode let file = match fs::File::open( env::current_dir() .map_err(|e| e.to_string())? .join(path.clone()), ) { Ok(file) => file, Err(err) => return Err(format!("Failed to open file: {}", err)), }; let reader = BufReader::new(file); // Create temporary variables to hold the parsed values let mut name = None; let mut command = None; let mut categories = Vec::new(); let mut icon = None; let mut no_display = false; // Loop through each line of the file for line in reader.lines() { let line = match line { Ok(line) => line, Err(err) => return Err(format!("Failed to read line: {}", err)), }; // Skip empty lines and lines that start with "#" (comments) if line.is_empty() || line.starts_with('#') { continue; } // Split the line into a key-value pair by looking for the first "=" character let parts = line.split_once('='); let (key, value) = match parts { Some((key, value)) => (key, value), None => continue, }; // Parse the key-value pair based on the key match key { "Name" => name = Some(value.to_string()), "Exec" => command = Some(value.to_string()), "Categories" => { categories = value .split(';') .map(|s| s.to_string()) .filter(|s| !s.is_empty()) .collect() } "Icon" => icon = Some(value.to_string()), "NoDisplay" => { no_display = match value { "true" => true, _ => false, } } _ => (), // Ignore unknown keys } } // Create and return a new DesktopFile instance with the parsed values Ok(DesktopFile { path, name, command, categories, icon, no_display, }) } #[test] fn test_parse_desktop_file() { // Create a temporary directory and a test desktop file let dir = tempdir::TempDir::new("test").unwrap(); let file = dir.path().join("test.desktop"); let data = "[Desktop Entry]\nName=Test\nExec=test\nCategories=A;B;C\nIcon=test.png"; fs::write(&file, data).unwrap(); // Parse the test desktop file let desktop_file = parse_desktop_file(file).unwrap(); // Check the parsed values assert_eq!(desktop_file.name, Some("Test".to_string())); assert_eq!(desktop_file.command, Some("test".to_string())); assert_eq!( desktop_file.categories, vec!["A".to_string(), "B".to_string(), "C".to_string()] ); assert_eq!(desktop_file.icon, Some("test.png".to_string())); } #[derive(Debug, Clone)] pub struct DesktopFile { path: PathBuf, pub name: Option, pub command: Option, pub categories: Vec, pub icon: Option, pub no_display: bool, } impl DesktopFile { pub fn get_raw_icons(&self) -> Vec { // Get the name of the icon from the DesktopFile struct let Some(icon_name) = self.icon.as_ref() else { return Vec::new(); }; let test_icon_path = self.path.join(Path::new(icon_name)); if test_icon_path.exists() { if let Some(icon) = Icon::from_path(test_icon_path, 128) { return vec![icon]; } } let cache_icon_path = get_image_cache_dir().join(icon_name).canonicalize(); if cache_icon_path.is_ok() { return vec![Icon::from_path(cache_icon_path.unwrap(), 128).unwrap()]; } let mut icons_iter = linicon::lookup_icon(icon_name) .use_fallback_themes(false) .peekable(); if icons_iter.peek().is_none() { //dbg!("No icons found in current theme"); icons_iter = linicon::lookup_icon(icon_name).peekable(); } let sized_png: Vec = icons_iter .filter_map(|i| i.ok()) .filter(|i| i.icon_type != linicon::IconType::XMP) //TODO: support XMP .map(|i| Icon::from_path(i.path, i.max_size - 2).unwrap()) .collect(); sized_png } } #[derive(Debug, PartialEq, Eq, Clone)] pub struct Icon { pub icon_type: IconType, pub path: PathBuf, pub size: u16, } #[derive(Debug, PartialEq, Eq, Clone)] pub enum IconType { Png, Svg, Gltf, } impl Icon { pub fn from_path(path: PathBuf, size: u16) -> Option { let icon_type = match path.extension().and_then(|ext| ext.to_str()) { Some("png") => Some(IconType::Png), Some("svg") => Some(IconType::Svg), Some("glb") | Some("gltf") => Some(IconType::Gltf), _ => return None, } .unwrap(); return Some(Icon { icon_type, path, size, }); } pub fn cached_process(self, size: u16) -> Result { let new_path = get_image_cache_dir().join(self.path.with_extension("").file_name().unwrap()); if !new_path.exists() { _ = symlink(self.path.clone(), new_path); } match self.icon_type { IconType::Svg => Ok(Icon::from_path(get_png_from_svg(self.path, size)?, size).unwrap()), _ => Ok(self), } } } #[test] fn test_get_icon_path() { // Create an instance of the DesktopFile struct with some dummy data let desktop_file = DesktopFile { path: PathBuf::new(), name: None, command: None, categories: vec![], icon: Some("krita".into()), no_display: false, }; // Call the get_icon_path() function with a size argument and store the result let icon_paths = desktop_file.get_raw_icons(); dbg!(&icon_paths); // Assert that the get_icon_path() function returns the expected result assert!(icon_paths.contains( &Icon::from_path( PathBuf::from("/usr/share/icons/hicolor/32x32/apps/krita.png"), 32 ) .unwrap() )); } #[cached] pub fn get_image_cache_dir() -> PathBuf { let cache_dir; if let Ok(xdg_cache_home) = std::env::var("XDG_CACHE_HOME") { cache_dir = PathBuf::from_str(&xdg_cache_home).unwrap_or(dirs::home_dir().unwrap().join(".cache")) } else { cache_dir = dirs::home_dir().unwrap().join(".cache"); } let image_cache_dir = cache_dir.join("protostar_icon_cache"); create_dir_all(&image_cache_dir).expect("Could not create image cache directory"); return image_cache_dir; } pub fn get_png_from_svg(svg_path: impl AsRef, size: u16) -> Result { let svg_path = fs::canonicalize(svg_path)?; let svg_data = fs::read(svg_path.as_path())?; let tree = Tree::from_data(svg_data.as_slice(), &resvg::usvg::Options::default()) .map_err(|_| ErrorKind::InvalidData)?; let png_path = get_image_cache_dir().join(format!( "{}-{}.png", svg_path.file_name().unwrap().to_str().unwrap(), svg_data.len() )); if png_path.exists() { return Ok(png_path); } let mut pixmap = Pixmap::new(size.into(), size.into()).unwrap(); render( &tree, FitTo::Width(size.into()), Transform::identity(), pixmap.as_mut(), ); pixmap .save_png(&png_path) .map_err(|_| ErrorKind::InvalidData)?; Ok(png_path) } #[test] fn test_render_svg_to_png() { use image::GenericImageView; // Create temporary input and output paths let svg_path = env::current_dir().unwrap().join("test_input.svg"); // Write some test SVG data to the input path let test_svg_data = " "; fs::write(&svg_path, test_svg_data).unwrap(); // Call the function with the test input and output paths and a size of 200 let png_path = get_png_from_svg(&svg_path, 200).unwrap(); dbg!(&png_path); // Check that the output file exists assert!(png_path.exists()); // Check that the output file is a PNG file assert_eq!(png_path.extension().unwrap(), "png"); // Check that the output file has the expected dimensions let output_image = image::open(&png_path).unwrap(); assert_eq!(output_image.dimensions(), (200, 200)); // Delete the temporary input and output files fs::remove_file(&svg_path).unwrap(); fs::remove_file(&png_path).unwrap(); }