feat: app grid and icons
This commit is contained in:
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod protostar;
|
||||
pub mod xdg;
|
||||
67
src/main.rs
67
src/main.rs
@@ -1,29 +1,56 @@
|
||||
mod desktop_file;
|
||||
mod protostar;
|
||||
|
||||
use manifest_dir_macros::directory_relative_path;
|
||||
use protostar::ProtoStar;
|
||||
use stardust_xr_molecules::fusion::client::Client;
|
||||
use std::{
|
||||
env::{args, current_dir},
|
||||
path::Path,
|
||||
use clap::Parser;
|
||||
use color_eyre::{
|
||||
eyre::{bail, Result},
|
||||
Report,
|
||||
};
|
||||
use manifest_dir_macros::directory_relative_path;
|
||||
use protostar::{protostar::ProtoStar, xdg::parse_desktop_file};
|
||||
use stardust_xr_molecules::fusion::client::Client;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[clap(short, long, default_value_t = 0.1)]
|
||||
size: f32,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
conflicts_with = "command",
|
||||
required_unless_present = "command",
|
||||
conflicts_with = "icon",
|
||||
required_unless_present = "icon"
|
||||
)]
|
||||
desktop_file: Option<PathBuf>,
|
||||
#[clap(short, long, conflicts_with = "desktop_file", requires = "command")]
|
||||
icon: Option<PathBuf>,
|
||||
#[clap(short, long, conflicts_with = "desktop_file", requires = "icon")]
|
||||
command: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let (client, event_loop) = Client::connect_with_async_loop().await.unwrap();
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let args = Args::parse();
|
||||
let (client, event_loop) = Client::connect_with_async_loop().await?;
|
||||
client.set_base_prefixes(&[directory_relative_path!("res")]);
|
||||
|
||||
let _root = client.wrap_root(ProtoStar::new(
|
||||
client.clone(),
|
||||
0.1,
|
||||
current_dir()
|
||||
.unwrap()
|
||||
.join(Path::new(&args().nth(1).unwrap())),
|
||||
));
|
||||
let protostar = if let Some(desktop_file) = args.desktop_file {
|
||||
ProtoStar::create_from_desktop_file(
|
||||
client.get_root(),
|
||||
parse_desktop_file(desktop_file).map_err(|e| Report::msg(e))?,
|
||||
)?
|
||||
} else if let Some(command) = args.command {
|
||||
ProtoStar::new_raw(client.get_root(), None, command)?
|
||||
} else {
|
||||
bail!("No command or desktop file, nothing to launch.");
|
||||
};
|
||||
|
||||
let _root = client.wrap_root(protostar);
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => (),
|
||||
e = event_loop => e.unwrap().unwrap(),
|
||||
}
|
||||
e = event_loop => e??,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
137
src/protostar.rs
137
src/protostar.rs
@@ -1,3 +1,5 @@
|
||||
use crate::xdg::{DesktopFile, Icon, RawIconType};
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use glam::Quat;
|
||||
use mint::Vector3;
|
||||
use nix::unistd::{execv, fork};
|
||||
@@ -5,56 +7,110 @@ use stardust_xr_molecules::{
|
||||
fusion::{
|
||||
client::{Client, LifeCycleHandler, LogicStepInfo},
|
||||
core::values::Transform,
|
||||
drawable::Model,
|
||||
fields::SphereField,
|
||||
drawable::{MaterialParameter, Model, ResourceID},
|
||||
fields::BoxField,
|
||||
node::NodeType,
|
||||
resource::NamespacedResource,
|
||||
spatial::Spatial,
|
||||
startup_settings::StartupSettings,
|
||||
},
|
||||
GrabData, Grabbable,
|
||||
};
|
||||
use std::{env::args, ffi::CString, path::PathBuf, sync::Arc};
|
||||
use std::{f32::consts::PI, ffi::CStr, sync::Arc};
|
||||
use tween::{QuartInOut, Tweener};
|
||||
use ustr::ustr;
|
||||
|
||||
fn model_from_icon(parent: &Spatial, icon: &Icon) -> Result<Model> {
|
||||
let model = match icon {
|
||||
Icon::Png(path) => {
|
||||
let model = Model::create(
|
||||
parent,
|
||||
Transform::from_rotation(Quat::from_rotation_y(PI)),
|
||||
&ResourceID::new_namespaced("protostar", "cartridge"),
|
||||
)?;
|
||||
model.set_material_parameter(
|
||||
0,
|
||||
"diffuse",
|
||||
MaterialParameter::Texture(ResourceID::Direct(path.clone())),
|
||||
)?;
|
||||
model
|
||||
}
|
||||
Icon::Gltf(path) => Model::create(
|
||||
parent,
|
||||
Transform::from_scale([0.05; 3]),
|
||||
&ResourceID::new_direct(path)?,
|
||||
)?,
|
||||
};
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub struct ProtoStar {
|
||||
client: Arc<Client>,
|
||||
grabbable: Grabbable,
|
||||
field: SphereField,
|
||||
field: BoxField,
|
||||
icon: Model,
|
||||
icon_shrink: Option<Tweener<QuartInOut<f32, f64>>>,
|
||||
size: f32,
|
||||
executable_path: PathBuf,
|
||||
icon_shrink: Option<Tweener<f32, f64, QuartInOut>>,
|
||||
execute_command: String,
|
||||
}
|
||||
impl ProtoStar {
|
||||
pub fn new(client: Arc<Client>, size: f32, executable_path: PathBuf) -> Self {
|
||||
let field =
|
||||
SphereField::create(client.get_root(), Vector3::from([0.0; 3]), size * 0.5).unwrap();
|
||||
pub fn create_from_desktop_file(parent: &Spatial, desktop_file: DesktopFile) -> Result<Self> {
|
||||
// dbg!(&desktop_file);
|
||||
let mut raw_icons = desktop_file.get_raw_icons();
|
||||
let last_icon = raw_icons.pop();
|
||||
let icon = raw_icons
|
||||
.into_iter()
|
||||
.find(|i| match i {
|
||||
RawIconType::Png(_) => false,
|
||||
RawIconType::Svg(_) => false,
|
||||
RawIconType::Gltf(_) => true,
|
||||
})
|
||||
.or(last_icon)
|
||||
.map(|i| dbg!(i.process(64)).ok())
|
||||
.ok_or_else(|| eyre!("No compatible icons found"))?;
|
||||
Self::new_raw(
|
||||
parent,
|
||||
icon,
|
||||
desktop_file.command.ok_or_else(|| eyre!("No command"))?,
|
||||
)
|
||||
}
|
||||
pub fn new_raw(parent: &Spatial, icon: Option<Icon>, execute_command: String) -> Result<Self> {
|
||||
let field = BoxField::create(
|
||||
parent,
|
||||
Transform::default(),
|
||||
match icon.as_ref() {
|
||||
Some(Icon::Png(_)) => [0.05, 0.0665, 0.005],
|
||||
_ => [0.05; 3],
|
||||
}
|
||||
.into(),
|
||||
)?;
|
||||
let grabbable = Grabbable::new(
|
||||
client.get_root(),
|
||||
parent,
|
||||
Transform::default(),
|
||||
&field,
|
||||
GrabData { max_distance: 0.05 },
|
||||
)
|
||||
.unwrap();
|
||||
field
|
||||
.set_spatial_parent(grabbable.content_parent())
|
||||
.unwrap();
|
||||
let icon = Model::create(
|
||||
grabbable.content_parent(),
|
||||
Transform::from_scale([size; 3]),
|
||||
&NamespacedResource::new("protostar", "default_icon"),
|
||||
)
|
||||
.unwrap();
|
||||
ProtoStar {
|
||||
client,
|
||||
GrabData {
|
||||
max_distance: 0.025,
|
||||
},
|
||||
)?;
|
||||
field.set_spatial_parent(grabbable.content_parent())?;
|
||||
let icon = icon
|
||||
.map(|i| model_from_icon(grabbable.content_parent(), &i))
|
||||
.unwrap_or_else(|| {
|
||||
Ok(Model::create(
|
||||
grabbable.content_parent(),
|
||||
Transform::from_scale([0.05; 3]),
|
||||
&ResourceID::new_namespaced("protostar", "default_icon"),
|
||||
)?)
|
||||
})?;
|
||||
Ok(ProtoStar {
|
||||
client: parent.client()?,
|
||||
grabbable,
|
||||
field,
|
||||
icon,
|
||||
icon_shrink: None,
|
||||
size,
|
||||
executable_path,
|
||||
}
|
||||
execute_command,
|
||||
})
|
||||
}
|
||||
pub fn content_parent(&self) -> &Spatial {
|
||||
self.grabbable.content_parent()
|
||||
}
|
||||
}
|
||||
impl LifeCycleHandler for ProtoStar {
|
||||
@@ -62,7 +118,8 @@ impl LifeCycleHandler for ProtoStar {
|
||||
self.grabbable.update();
|
||||
|
||||
if let Some(icon_shrink) = &mut self.icon_shrink {
|
||||
if let Some(scale) = icon_shrink.update(info.delta) {
|
||||
if !icon_shrink.is_finished() {
|
||||
let scale = icon_shrink.move_by(info.delta);
|
||||
self.icon
|
||||
.set_scale(None, Vector3::from([scale; 3]))
|
||||
.unwrap();
|
||||
@@ -84,19 +141,21 @@ impl LifeCycleHandler for ProtoStar {
|
||||
startup_settings
|
||||
.set_root(self.grabbable.content_parent())
|
||||
.unwrap();
|
||||
self.icon_shrink = Some(Tweener::new(QuartInOut::new(self.size..=0.0, 0.25)));
|
||||
self.icon_shrink = Some(Tweener::quart_in_out(1.0, 0.0, 0.25));
|
||||
let future = startup_settings.generate_startup_token().unwrap();
|
||||
let executable = self.executable_path.clone();
|
||||
let executable = dbg!(self.execute_command.clone());
|
||||
tokio::task::spawn(async move {
|
||||
std::env::set_var("STARDUST_STARTUP_TOKEN", future.await.unwrap());
|
||||
if unsafe { fork() }.unwrap().is_parent() {
|
||||
let executable = ustr(executable.to_str().unwrap());
|
||||
let args = args()
|
||||
.skip(1)
|
||||
.map(|arg| CString::new(arg))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
execv::<CString>(executable.as_cstr(), args.as_slice()).unwrap();
|
||||
execv::<&CStr>(
|
||||
ustr("/bin/sh").as_cstr(),
|
||||
&[
|
||||
ustr("/bin/sh").as_cstr(),
|
||||
ustr("-c").as_cstr(),
|
||||
ustr(&executable).as_cstr(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::ffi::OsString;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::{env, fs};
|
||||
|
||||
use anyhow::Result;
|
||||
use color_eyre::eyre::Result;
|
||||
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::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::{env, fs};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn get_desktop_files() -> Vec<PathBuf> {
|
||||
const ICON_SIZES: &[&str] = &["128x128", "scalable", "256x256", "64x64", "32x32"];
|
||||
|
||||
pub fn get_desktop_files() -> Vec<PathBuf> {
|
||||
// Get the XDG data directories
|
||||
let xdg_data_dirs =
|
||||
std::env::var("XDG_DATA_DIRS").unwrap_or("/usr/local/share:/usr/share".to_string());
|
||||
@@ -55,9 +57,13 @@ fn test_get_desktop_files() {
|
||||
.any(|file| file.ends_with("gimp.desktop")));
|
||||
}
|
||||
|
||||
fn parse_desktop_file(path: &Path) -> Result<DesktopFile, String> {
|
||||
pub fn parse_desktop_file(path: PathBuf) -> Result<DesktopFile, String> {
|
||||
// Open the file in read-only mode
|
||||
let file = match fs::File::open(path) {
|
||||
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)),
|
||||
};
|
||||
@@ -93,7 +99,13 @@ fn parse_desktop_file(path: &Path) -> Result<DesktopFile, String> {
|
||||
match key {
|
||||
"Name" => name = Some(value.to_string()),
|
||||
"Exec" => command = Some(value.to_string()),
|
||||
"Categories" => categories = value.split(';').map(|s| s.to_string()).collect(),
|
||||
"Categories" => {
|
||||
categories = value
|
||||
.split(';')
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
}
|
||||
"Icon" => icon = Some(value.to_string()),
|
||||
_ => (), // Ignore unknown keys
|
||||
}
|
||||
@@ -101,6 +113,7 @@ fn parse_desktop_file(path: &Path) -> Result<DesktopFile, String> {
|
||||
|
||||
// Create and return a new DesktopFile instance with the parsed values
|
||||
Ok(DesktopFile {
|
||||
path,
|
||||
name,
|
||||
command,
|
||||
categories,
|
||||
@@ -117,7 +130,7 @@ fn test_parse_desktop_file() {
|
||||
fs::write(&file, data).unwrap();
|
||||
|
||||
// Parse the test desktop file
|
||||
let desktop_file = parse_desktop_file(&file).unwrap();
|
||||
let desktop_file = parse_desktop_file(file).unwrap();
|
||||
|
||||
// Check the parsed values
|
||||
assert_eq!(desktop_file.name, Some("Test".to_string()));
|
||||
@@ -129,31 +142,130 @@ fn test_parse_desktop_file() {
|
||||
assert_eq!(desktop_file.icon, Some("test.png".to_string()));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesktopFile {
|
||||
path: PathBuf,
|
||||
pub name: Option<String>,
|
||||
pub command: Option<String>,
|
||||
pub categories: Vec<String>,
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
impl DesktopFile {
|
||||
pub fn get_raw_icons(&self) -> Vec<RawIconType> {
|
||||
// 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() {
|
||||
return RawIconType::from_path(test_icon_path)
|
||||
.map(|i| vec![i])
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let Ok(icon_name) = OsString::from_str(icon_name) else { return Vec::new(); };
|
||||
|
||||
// Get the current icon theme from the XDG_ICON_THEME environment variable, or use "hicolor" as the default theme if the variable is not defined
|
||||
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);
|
||||
|
||||
// Concatenate the XDG_DATA_HOME and XDG_DATA_DIRS directories with the default path for icon themes
|
||||
xdg_data_dirs // XDG_DATA_DIRS directories
|
||||
.flat_map(|dir| {
|
||||
let icons_path = dir.join("icons").join(&icon_theme);
|
||||
ICON_SIZES
|
||||
.iter()
|
||||
.map(|path| icons_path.join(path).join("apps"))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter_map(|dir| {
|
||||
let dir = fs::read_dir(dir).ok()?;
|
||||
Some(
|
||||
dir.filter_map(|e| e.ok())
|
||||
.map(|file| file.path())
|
||||
.filter(|file| file.file_stem() == Some(&icon_name)),
|
||||
)
|
||||
})
|
||||
.flatten()
|
||||
.filter_map(RawIconType::from_path)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum IconType {
|
||||
pub enum RawIconType {
|
||||
Png(PathBuf),
|
||||
Svg(PathBuf),
|
||||
Gltf(PathBuf),
|
||||
}
|
||||
impl IconType {
|
||||
fn to_png(&self, size: u32) -> Option<PathBuf> {
|
||||
match self {
|
||||
IconType::Png(path) => Some(path.clone()),
|
||||
IconType::Svg(path) => {
|
||||
let png_path = path.with_extension("png");
|
||||
render_svg_to_png(path, &png_path, size).ok()?;
|
||||
Some(png_path)
|
||||
}
|
||||
impl RawIconType {
|
||||
pub fn from_path(path: PathBuf) -> Option<RawIconType> {
|
||||
match path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("png") => Some(RawIconType::Png(path)),
|
||||
Some("svg") => Some(RawIconType::Svg(path)),
|
||||
Some("glb") | Some("gltf") => Some(RawIconType::Gltf(path)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(self, size: u32) -> Result<Icon, std::io::Error> {
|
||||
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))
|
||||
}
|
||||
RawIconType::Gltf(path) => Ok(Icon::Gltf(path)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_svg_to_png(svg_path: &PathBuf, png_path: &PathBuf, size: u32) -> Result<()> {
|
||||
#[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()),
|
||||
};
|
||||
|
||||
// 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(&RawIconType::Png(PathBuf::from(
|
||||
"/usr/share/icons/hicolor/16x16/apps/krita.png"
|
||||
))));
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Icon {
|
||||
Png(PathBuf),
|
||||
Gltf(PathBuf),
|
||||
}
|
||||
|
||||
pub fn render_svg_to_png(
|
||||
cache_dir: impl AsRef<Path>,
|
||||
svg_path: impl AsRef<Path>,
|
||||
size: u32,
|
||||
) -> Result<PathBuf, std::io::Error> {
|
||||
let svg_path = fs::canonicalize(svg_path)?;
|
||||
let tree = Tree::from_data(
|
||||
fs::read(svg_path)?.as_slice(),
|
||||
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())
|
||||
.with_extension("png");
|
||||
let mut pixmap = Pixmap::new(size, size).unwrap();
|
||||
render(
|
||||
&tree,
|
||||
@@ -161,101 +273,40 @@ fn render_svg_to_png(svg_path: &PathBuf, png_path: &PathBuf, size: u32) -> Resul
|
||||
Transform::identity(),
|
||||
pixmap.as_mut(),
|
||||
);
|
||||
pixmap.save_png(png_path)?;
|
||||
Ok(())
|
||||
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 input_path = PathBuf::from("test_input.svg");
|
||||
let output_path = PathBuf::from("test_output.png");
|
||||
let svg_path = env::current_dir().unwrap().join("test_input.svg");
|
||||
|
||||
// Write some test SVG data to the input path
|
||||
let test_svg_data = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">
|
||||
<ellipse cx=\"50\" cy=\"80\" rx=\"46\" ry=\"19\" fill=\"#07c\"/>
|
||||
<path d=\"M43,0c-6,25,16,22,1,52c11,3,19,0,19-22c38,18,16,63-12,64c-25,2-55-39-8-94\" fill=\"#e34\"/>
|
||||
<path d=\"M34,41c-6,39,29,32,33,7c39,42-69,63-33-7\" fill=\"#fc2\"/>
|
||||
</svg>";
|
||||
fs::write(&input_path, test_svg_data).unwrap();
|
||||
<ellipse cx=\"50\" cy=\"80\" rx=\"46\" ry=\"19\" fill=\"#07c\"/>
|
||||
<path d=\"M43,0c-6,25,16,22,1,52c11,3,19,0,19-22c38,18,16,63-12,64c-25,2-55-39-8-94\" fill=\"#e34\"/>
|
||||
<path d=\"M34,41c-6,39,29,32,33,7c39,42-69,63-33-7\" fill=\"#fc2\"/>
|
||||
</svg>";
|
||||
fs::write(&svg_path, test_svg_data).unwrap();
|
||||
|
||||
// Call the function with the test input and output paths and a size of 200
|
||||
render_svg_to_png(&input_path, &output_path, 200).unwrap();
|
||||
let png_path = render_svg_to_png(".", &svg_path, 200).unwrap();
|
||||
dbg!(&png_path);
|
||||
|
||||
// Check that the output file exists
|
||||
assert!(output_path.exists());
|
||||
assert!(png_path.exists());
|
||||
|
||||
// Check that the output file is a PNG file
|
||||
assert_eq!(output_path.extension().unwrap(), "png");
|
||||
assert_eq!(png_path.extension().unwrap(), "png");
|
||||
|
||||
// Check that the output file has the expected dimensions
|
||||
let output_image = image::open(&output_path).unwrap();
|
||||
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(&input_path).unwrap();
|
||||
fs::remove_file(&output_path).unwrap();
|
||||
}
|
||||
|
||||
struct DesktopFile {
|
||||
name: Option<String>,
|
||||
command: Option<String>,
|
||||
categories: Vec<String>,
|
||||
icon: Option<String>,
|
||||
}
|
||||
|
||||
impl DesktopFile {
|
||||
fn get_icon_path(&self, size: &str) -> Option<IconType> {
|
||||
// Get the name of the icon from the DesktopFile struct
|
||||
let icon_name = OsString::from_str(self.icon.as_ref()?).ok()?;
|
||||
|
||||
// Get the current icon theme from the XDG_ICON_THEME environment variable, or use "hicolor" as the default theme if the variable is not defined
|
||||
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 xdg_data_dirs = env::var_os("XDG_DATA_DIRS")?;
|
||||
let binding = xdg_data_dirs.into_string().ok()?;
|
||||
let xdg_data_dirs = binding.split(":").map(Path::new);
|
||||
|
||||
// Concatenate the XDG_DATA_HOME and XDG_DATA_DIRS directories with the default path for icon themes
|
||||
let icon = xdg_data_dirs // XDG_DATA_DIRS directories
|
||||
.map(|dir| dir.join("icons").join(&icon_theme).join(size).join("apps"))
|
||||
.filter_map(|dir| {
|
||||
fs::read_dir(dir)
|
||||
.ok()?
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|file| file.path())
|
||||
.find(|file| file.file_stem() == Some(&icon_name))
|
||||
})
|
||||
.last()?;
|
||||
|
||||
// Check the file extension of the icon file and return an option containing the path to the file in an instance of the IconType enum
|
||||
match icon.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("png") => Some(IconType::Png(icon)),
|
||||
Some("svg") => Some(IconType::Svg(icon)),
|
||||
Some("glb") | Some("gltf") => Some(IconType::Gltf(icon)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_get_icon_path() {
|
||||
// Create an instance of the DesktopFile struct with some dummy data
|
||||
let desktop_file = DesktopFile {
|
||||
name: Some("test".into()),
|
||||
command: Some("test-command".into()),
|
||||
categories: vec!["test-category".into()],
|
||||
icon: Some("krita".into()),
|
||||
};
|
||||
|
||||
// Call the get_icon_path() function with a size argument and store the result
|
||||
let icon_path = desktop_file.get_icon_path("16x16");
|
||||
|
||||
// Assert that the get_icon_path() function returns the expected result
|
||||
assert_eq!(
|
||||
icon_path,
|
||||
Some(IconType::Png(PathBuf::from(
|
||||
"/usr/share/icons/hicolor/16x16/apps/krita.png"
|
||||
)))
|
||||
);
|
||||
fs::remove_file(&svg_path).unwrap();
|
||||
fs::remove_file(&png_path).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user