diff --git a/Cargo.lock b/Cargo.lock index 2dfdd5b..a46c15b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.11.1" @@ -286,6 +295,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -344,6 +362,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cxx" version = "1.0.83" @@ -394,6 +422,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories" version = "4.0.1" @@ -582,6 +620,16 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -1232,6 +1280,7 @@ dependencies = [ "nix", "resvg", "rustc-hash", + "sha2", "stardust-xr-molecules", "tempdir", "tokio", @@ -1535,6 +1584,17 @@ dependencies = [ "syn", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1949,6 +2009,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f114398da254e78168e12edec0ece6b4ca15a97262ecd8e5efd5025e3fc30204" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-bidi" version = "0.3.8" diff --git a/Cargo.toml b/Cargo.toml index d800206..db08348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ mint = "0.5.9" nix = "0.26.1" resvg = "0.28.0" rustc-hash = "1.1.0" +sha2 = "0.10.6" stardust-xr-molecules = "0.17.0" tokio = { version = "1.24.1", features = ["full"] } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } diff --git a/README.md b/README.md index 7879546..bf01292 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # protostar + Prototype application launcher + +TODO: + +1) ~~Make it work with all icons (should be done)~~ + +2. ~~If the right sized png exists, then use that, otherwise rasterize it (a bit janky but it works)~~ +3. ~~Use XDG_CACHE_DIR to rasterize svgs (done)~~ +4. Make sure it's using the current icon theme +5. Design a better app launcher ui diff --git a/examples/app_grid.rs b/examples/app_grid.rs index 393c421..a31838f 100644 --- a/examples/app_grid.rs +++ b/examples/app_grid.rs @@ -41,6 +41,7 @@ impl AppGrid { let apps = get_desktop_files() .into_iter() .filter_map(|d| parse_desktop_file(d).ok()) + .filter(|d| !d.no_display) .enumerate() .filter(|(i, _)| *i <= APP_LIMIT) .filter_map(|(i, a)| { diff --git a/src/protostar.rs b/src/protostar.rs index 89dc36d..aefc336 100644 --- a/src/protostar.rs +++ b/src/protostar.rs @@ -64,7 +64,7 @@ impl ProtoStar { RawIconType::Gltf(_) => true, }) .or(last_icon) - .map(|i| dbg!(i.process(64)).ok()) + .map(|i| i.process(128).ok()) .ok_or_else(|| eyre!("No compatible icons found"))?; Self::new_raw( parent, @@ -144,6 +144,7 @@ impl RootHandler for ProtoStar { self.icon_shrink = Some(Tweener::quart_in_out(1.0, 0.0, 0.25)); let future = startup_settings.generate_startup_token().unwrap(); let executable = dbg!(self.execute_command.clone()); + //TODO: split the executable string for the args tokio::task::spawn(async move { std::env::set_var("STARDUST_STARTUP_TOKEN", future.await.unwrap()); if unsafe { fork() }.unwrap().is_parent() { diff --git a/src/xdg.rs b/src/xdg.rs index bd18044..ea24c0d 100644 --- a/src/xdg.rs +++ b/src/xdg.rs @@ -4,37 +4,50 @@ 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::io::{BufRead, BufReader, ErrorKind, self}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{env, fs}; use walkdir::WalkDir; +use sha2::{Sha224, Digest}; -const ICON_SIZES: &[&str] = &["128x128", "scalable", "256x256", "64x64", "32x32"]; +const ICON_SIZES: &[&str] = &["64x64", "32x32", "scalable", "128x128"]; + +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 { - // Get the XDG data directories - let xdg_data_dirs = - std::env::var("XDG_DATA_DIRS").unwrap_or("/usr/local/share:/usr/share".to_string()); - - // Append the applications directory to each data directory - let app_dirs = xdg_data_dirs - .split(":") - .map(|dir| Path::new(dir).join("applications")); - - // Get the user's local applications directory - let local_app_dir = dirs::home_dir() - .unwrap() - .join(".local") - .join("share") - .join("applications"); - let desktop_extension = OsString::from_str("desktop").unwrap(); - // Get the list of directories to search + let app_dirs = get_app_dirs(); + dbg!(&app_dirs); app_dirs - .chain(Some(local_app_dir)) - .filter(|dir| dir.exists() && dir.is_dir()) + .into_iter() .flat_map(|dir| { // Follow symlinks and recursively search directories WalkDir::new(dir) @@ -75,6 +88,7 @@ pub fn parse_desktop_file(path: PathBuf) -> Result { 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() { @@ -107,6 +121,10 @@ pub fn parse_desktop_file(path: PathBuf) -> Result { .collect() } "Icon" => icon = Some(value.to_string()), + "NoDisplay" => no_display = match value{ + "true" => true, + _ => false + }, _ => (), // Ignore unknown keys } } @@ -118,6 +136,7 @@ pub fn parse_desktop_file(path: PathBuf) -> Result { command, categories, icon, + no_display, }) } @@ -149,6 +168,7 @@ pub struct DesktopFile { pub command: Option, pub categories: Vec, pub icon: Option, + pub no_display: bool, } impl DesktopFile { pub fn get_raw_icons(&self) -> Vec { @@ -167,12 +187,11 @@ impl DesktopFile { let icon_theme = env::var_os("XDG_ICON_THEME").unwrap_or("hicolor".into()); // Get the XDG_DATA_HOME and XDG_DATA_DIRS environment variables, and split the XDG_DATA_DIRS variable into a list of directories - let Some(xdg_data_dirs) = env::var_os("XDG_DATA_DIRS") else { return Vec::new(); }; - let Ok(binding) = xdg_data_dirs.into_string() else { return Vec::new(); }; - let xdg_data_dirs = binding.split(":").map(Path::new); + let xdg_data_dirs = get_data_dirs(); // Concatenate the XDG_DATA_HOME and XDG_DATA_DIRS directories with the default path for icon themes xdg_data_dirs // XDG_DATA_DIRS directories + .into_iter() .flat_map(|dir| { let icons_path = dir.join("icons").join(&icon_theme); ICON_SIZES @@ -214,9 +233,7 @@ impl RawIconType { match self { RawIconType::Png(path) => Ok(Icon::Png(path)), RawIconType::Svg(path) => { - let png_path = path.with_extension("png"); - render_svg_to_png(path, &png_path, size)?; - Ok(Icon::Png(png_path)) + Ok(Icon::Png(get_png_from_svg(&path, size)?)) } RawIconType::Gltf(path) => Ok(Icon::Gltf(path)), } @@ -232,6 +249,7 @@ fn test_get_icon_path() { 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 @@ -240,7 +258,7 @@ fn test_get_icon_path() { // Assert that the get_icon_path() function returns the expected result assert!(icon_paths.contains(&RawIconType::Png(PathBuf::from( - "/usr/share/icons/hicolor/16x16/apps/krita.png" + "/usr/share/icons/hicolor/32x32/apps/krita.png" )))); } @@ -250,22 +268,41 @@ pub enum Icon { Gltf(PathBuf), } -pub fn render_svg_to_png( - cache_dir: impl AsRef, - svg_path: impl AsRef, - size: u32, -) -> Result { +pub fn get_png_from_svg(svg_path: impl AsRef, size: u32,) -> Result { let svg_path = fs::canonicalize(svg_path)?; let tree = Tree::from_data( fs::read(svg_path.as_path())?.as_slice(), &resvg::usvg::Options::default(), ) .map_err(|_| ErrorKind::InvalidData)?; - create_dir_all(cache_dir.as_ref())?; - let png_path = cache_dir - .as_ref() - .join(svg_path.file_name().unwrap()) + + 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"); + + //TODO: come up with a better way to cache images system + let mut hasher = Sha224::new(); + let mut svg_file = fs::File::open(&svg_path)?; + io::copy(&mut svg_file, &mut hasher)?; + let hash_bytes = hasher.finalize(); + + let png_path = image_cache_dir + .join(format!("{}-{:02x}",svg_path.with_extension("").file_name().unwrap().to_str().unwrap(), hash_bytes)) .with_extension("png"); + + if png_path.exists() { + return Ok(png_path) + } + let mut pixmap = Pixmap::new(size, size).unwrap(); render( &tree, @@ -293,7 +330,7 @@ fn test_render_svg_to_png() { 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 = render_svg_to_png(".", &svg_path, 200).unwrap(); + let png_path = get_png_from_svg(&svg_path, 200).unwrap(); dbg!(&png_path); // Check that the output file exists