#![cfg(all(unix, not(target_os = "macos")))]
use super::{icon_from_svg, Context, Icon, IconError, DEFAULT_ICON_SIZE};
use serde;
use std::io::Read;
use xdg_mime::SharedMimeInfo;
pub const DEFAULT_THEME: &str = env!("JOLLY_DEFAULT_THEME");
const SNIFFSIZE: usize = 8 * 1024;
#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
#[serde(default)]
pub struct Os {
pub theme: String,
xdg_folder: Option<String>,
}
impl Default for Os {
fn default() -> Self {
Self {
theme: DEFAULT_THEME.into(),
xdg_folder: None,
}
}
}
impl super::IconInterface for Os {
fn get_default_icon(&self) -> Result<Icon, IconError> {
self.get_icon_for_iname("text-x-generic")
}
fn get_icon_for_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<Icon, IconError> {
let path = path.as_ref();
let inames = self.get_iname_for_file(path)?;
for iname in &inames {
let icon = self.get_icon_for_iname(iname);
if icon.is_ok() {
return icon;
}
}
Err(format!("No valid icon. inames were {:?}", inames).into())
}
fn get_icon_for_url(&self, url: &str) -> Result<Icon, IconError> {
let iname = self.get_iname_for_url(url)?;
self.get_icon_for_iname(&iname)
}
}
impl Os {
fn get_iname_for_url(&self, p: &str) -> Result<String, IconError> {
use url::Url;
let url = Url::parse(p).context("Url is not valid: {p}")?;
use std::process::Command;
let mut cmd = Command::new("xdg-settings");
cmd.arg("get")
.arg("default-url-scheme-handler")
.arg(url.scheme());
if let Some(f) = &self.xdg_folder {
cmd.env("XDG_DATA_HOME", f);
cmd.env("XDG_DATA_DIRS", f);
cmd.env("HOME", f);
cmd.env("DE", "generic");
}
let output = cmd.output().context("xdg-settings unsuccessful")?;
let mut handler =
String::from_utf8(output.stdout).context("invalid utf8 from xdg-settings")?;
if handler.ends_with("\n") {
handler.pop();
}
if handler.is_empty() {
Err("no scheme handler found".into())
} else {
Ok(handler)
}
}
fn get_iname_for_file<P: AsRef<std::path::Path>>(
&self,
path: P,
) -> Result<Vec<String>, IconError> {
let filename = path
.as_ref()
.as_os_str()
.to_str()
.context("filename not valid unicode")?;
use once_cell::sync::OnceCell;
static MIMEINFO: OnceCell<SharedMimeInfo> = OnceCell::new();
let mimeinfo = match &self.xdg_folder {
Some(f) => {
let m = Box::new(SharedMimeInfo::new_for_directory(f));
Box::leak(m) }
None => MIMEINFO.get_or_init(SharedMimeInfo::new),
};
let data: Option<Vec<_>>;
if let Ok(mut file) = std::fs::File::open(filename) {
let mut buf = vec![0u8; SNIFFSIZE];
if let Ok(numread) = file.read(buf.as_mut_slice()) {
buf.truncate(numread);
data = Some(buf);
} else {
data = None;
}
} else {
data = None;
}
let guess = match data {
Some(buf) => mimeinfo.guess_mime_type().path(filename).data(&buf).guess(),
None => mimeinfo.guess_mime_type().path(filename).guess(),
};
let fn_guess = mimeinfo.get_mime_types_from_file_name(filename);
let allmimes = std::iter::once(guess.mime_type().clone()).chain(fn_guess.into_iter());
let allparents = allmimes
.clone()
.flat_map(|m| mimeinfo.get_parents(&m).unwrap_or_default().into_iter());
Ok(allmimes
.chain(allparents)
.flat_map(|m| mimeinfo.lookup_icon_names(&m).into_iter())
.collect())
}
fn get_icon_for_iname(&self, icon_name: &str) -> Result<Icon, IconError> {
use freedesktop_icons::lookup;
let icon_name = icon_name.strip_suffix(".desktop").unwrap_or(icon_name);
let icon_path = lookup(icon_name)
.with_size(DEFAULT_ICON_SIZE)
.with_theme(&self.theme)
.find()
.ok_or("Could not lookup icon")?;
if icon_path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("png"))
{
Ok(iced::widget::image::Handle::from_path(icon_path))
} else if icon_path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("svg"))
{
icon_from_svg(&icon_path)
} else {
Err(format!(
"unsupported icon file type for icon {}",
icon_path.to_string_lossy()
)
.into())
}
}
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, write};
use std::process::Command;
use tempfile;
use super::*;
use iced::advanced::image::Data;
struct MockXdg(tempfile::TempDir);
impl MockXdg {
fn new() -> Self {
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
create_dir(p.join("applications")).unwrap();
create_dir(p.join("mime")).unwrap();
Self(dir)
}
fn add_app(&self, appname: &str) {
let p = self.0.path().join(format!("applications/{}", appname));
write(p, b"[Desktop Entry]\nExec=/bin/sh").unwrap();
}
fn register_url(&self, url: &str, appname: &str) {
let out = Command::new("xdg-settings")
.args(["set", "default-url-scheme-handler", url, appname])
.env("XDG_DATA_HOME", self.0.path())
.env("XDG_DATA_DIRS", self.0.path())
.env("HOME", self.0.path())
.env("DE", "generic")
.env("XDG_UTILS_DEBUG_LEVEL", "2")
.output()
.unwrap();
println!(
"registering_url: {} -> {} status: {} {}",
url,
appname,
out.status,
String::from_utf8(out.stderr).unwrap()
);
}
fn register_mime(&self, mimetype: &str, extension: &str) {
let filename = mimetype.split("/").last().unwrap();
let filename = self.0.path().join(format!("{}.xml", filename));
let text = format!(
r#"<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="{}">
<glob pattern="*.{}"/>
</mime-type>
</mime-info>
"#,
mimetype, extension
);
write(&filename, text.as_bytes()).unwrap();
let out = Command::new("xdg-mime")
.args([
"install",
"--novendor",
"--mode",
"user",
filename.to_str().unwrap(),
])
.env("XDG_DATA_HOME", self.0.path())
.env("XDG_DATA_DIRS", self.0.path())
.env("HOME", self.0.path())
.env("DE", "generic")
.env("XDG_UTILS_DEBUG_LEVEL", "2")
.output()
.unwrap();
println!(
"registering_mime: {} status: {} {}",
mimetype,
out.status,
String::from_utf8(out.stderr).unwrap()
);
}
fn os(&self, theme: &str) -> Os {
Os {
theme: theme.into(),
xdg_folder: Some(self.0.path().to_str().unwrap().into()),
}
}
}
#[test]
fn test_load_icon() {
let xdg = MockXdg::new();
xdg.add_app("test.desktop");
xdg.register_url("tel", "test.desktop");
xdg.register_mime("text/x-rust", "rs");
let os = xdg.os(DEFAULT_THEME);
assert!(os.get_iname_for_url("http://google.com").is_err());
assert!(os.get_iname_for_url("tel:12345").is_ok());
}
#[test]
fn test_load_file() {
let dir = tempfile::tempdir().unwrap();
let xdg = MockXdg::new();
xdg.register_mime("text/x-rust", "rs");
let os = xdg.os(DEFAULT_THEME);
let file = dir.path().join("test.rs");
std::fs::File::create(&file).unwrap();
let mimetypes = os.get_iname_for_file(file).unwrap();
assert!(
mimetypes.contains(&"text-x-rust".into()),
"actual {:?}",
mimetypes
);
}
#[test]
fn can_load_svg_icons() {
let svg_icon = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("icon/jolly");
let icon = Os::default()
.get_icon_for_iname(svg_icon.as_os_str().to_str().unwrap())
.unwrap();
assert!(matches!(
icon.data(),
Data::Rgba {
width: _,
height: _,
pixels: _
}
));
}
}