#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
println!("{:?}", query_default_app("image/jpeg"));
println!("{:?}", query_default_app("text/html"));
println!("{:?}", query_default_app("video/mp4"));
println!("{:?}", query_default_app("application/pdf"));
}
#[test]
fn ini() {
let ini = Ini(String::from("[foo]\n# comment\nbar=baz\n\n[bar]\nbar=foo"));
for (key, value) in ini.iter_section("foo") {
assert_eq!(key, "bar");
assert_eq!(value, "baz");
}
}
}
use std::collections::HashMap;
use std::env;
use std::fs;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Result};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
macro_rules! split_and_chain {
($xdg_vars:ident[$key:literal]) => {
$xdg_vars.get($key).map(String::as_str).unwrap_or("").split(':')
};
($xdg_vars:ident[$key:literal], $($tail_xdg_vars:ident[$tail_key:literal]),+$(,)*) => {
split_and_chain!($xdg_vars[$key]).chain(split_and_chain!($($tail_xdg_vars[$tail_key]),+))
}
}
struct Ini(String);
impl Ini {
fn from_filename(filename: &Path) -> Result<Ini> {
let mut file: File = File::open(filename)?;
let mut contents: Vec<u8> = vec![];
file.read_to_end(&mut contents)?;
let contents_str =
String::from_utf8(contents).map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
Ok(Ini(contents_str))
}
fn iter_section(&self, section: &str) -> impl Iterator<Item = (&str, &str)> {
let section = format!("[{}]", section);
let mut lines = self.0.lines();
loop {
let line = lines.next();
if let Some(line) = line {
if line == section {
break;
}
} else {
break;
}
}
lines
.filter(|line| !line.starts_with('#'))
.take_while(|line| !line.starts_with('['))
.filter_map(|line| {
let split: Vec<_> = line.splitn(2, '=').collect();
if split.len() != 2 {
None
} else {
Some((split[0], split[1]))
}
})
}
}
pub fn query_default_app<T: AsRef<str>>(query: T) -> Result<String> {
let mut xdg_vars: HashMap<String, String> = HashMap::new();
let env_vars: env::Vars = env::vars();
for (k, v) in env_vars {
if k.starts_with("XDG_CONFIG")
|| k.starts_with("XDG_DATA")
|| k.starts_with("XDG_CURRENT_DESKTOP")
|| k == "HOME"
{
xdg_vars.insert(k.to_string(), v.to_string());
}
}
if xdg_vars.contains_key("HOME") && !xdg_vars.contains_key("XDG_DATA_HOME") {
let h = xdg_vars["HOME"].clone();
xdg_vars.insert("XDG_DATA_HOME".to_string(), format!("{}/.local/share", h));
}
if xdg_vars.contains_key("HOME") && !xdg_vars.contains_key("XDG_CONFIG_HOME") {
let h = xdg_vars["HOME"].clone();
xdg_vars.insert("XDG_CONFIG_HOME".to_string(), format!("{}/.config", h));
}
if !xdg_vars.contains_key("XDG_DATA_DIRS") {
xdg_vars.insert(
"XDG_DATA_DIRS".to_string(),
"/usr/local/share:/usr/share".to_string(),
);
}
if !xdg_vars.contains_key("XDG_CONFIG_DIRS") {
xdg_vars.insert("XDG_CONFIG_DIRS".to_string(), "/etc/xdg".to_string());
}
let desktops: Option<Vec<String>> = if xdg_vars.contains_key("XDG_CURRENT_DESKTOP") {
let list = xdg_vars["XDG_CURRENT_DESKTOP"]
.trim()
.split(':')
.map(str::to_ascii_lowercase)
.collect();
Some(list)
} else {
None
};
for p in split_and_chain!(
xdg_vars["XDG_CONFIG_HOME"],
xdg_vars["XDG_CONFIG_DIRS"],
xdg_vars["XDG_DATA_HOME"],
xdg_vars["XDG_DATA_DIRS"],
) {
if let Some(ref d) = desktops {
for desktop in d {
let pb: PathBuf = PathBuf::from(format!(
"{var_value}/{desktop_val}-mimeapps.list",
var_value = p,
desktop_val = desktop
));
if pb.exists() {
if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
return Ok(ret);
}
}
}
}
let pb: PathBuf = PathBuf::from(format!("{var_value}/mimeapps.list", var_value = p));
if pb.exists() {
if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
return Ok(ret);
}
}
}
for p in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) {
if let Some(ref d) = desktops {
for desktop in d {
let pb: PathBuf = PathBuf::from(format!(
"{var_value}/applications/{desktop_val}-mimeapps.list",
var_value = p,
desktop_val = desktop
));
if pb.exists() {
if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
return Ok(ret);
}
}
}
}
let pb: PathBuf = PathBuf::from(format!(
"{var_value}/applications/mimeapps.list",
var_value = p
));
if pb.exists() {
if let Some(ret) = check_mimeapps_list(&pb, &xdg_vars, &query)? {
return Ok(ret);
}
}
}
Err(Error::new(
ErrorKind::NotFound,
format!("No results for mime query: {}", query.as_ref()),
))
}
fn check_mimeapps_list<T: AsRef<str>>(
filename: &Path,
xdg_vars: &HashMap<String, String>,
query: T,
) -> Result<Option<String>> {
let ini = Ini::from_filename(filename)?;
for (key, value) in ini
.iter_section("Added Associations")
.chain(ini.iter_section("Default Applications"))
{
if key != query.as_ref() {
continue;
}
for v in value.split(';') {
if v.trim().is_empty() {
continue;
}
if let Some(b) = desktop_file_to_command(v, xdg_vars)? {
return Ok(Some(b));
}
}
}
Ok(None)
}
fn desktop_file_to_command(
desktop_name: &str,
xdg_vars: &HashMap<String, String>,
) -> Result<Option<String>> {
for dir in split_and_chain!(xdg_vars["XDG_DATA_HOME"], xdg_vars["XDG_DATA_DIRS"]) {
let mut file_path: Option<PathBuf> = None;
let mut p;
if desktop_name.contains('-') {
let v: Vec<&str> = desktop_name.split('-').collect();
let (vendor, app): (&str, &str) = (v[0], v[1]);
p = PathBuf::from(format!(
"{dir}/applications/{vendor}/{app}",
dir = dir,
vendor = vendor,
app = app
));
if p.exists() {
file_path = Some(p);
}
}
if file_path.is_none() {
'indir: for indir in &[format!("{}/applications", dir)] {
p = PathBuf::from(format!(
"{indir}/{desktop}",
indir = indir,
desktop = desktop_name
));
if p.exists() {
file_path = Some(p);
break 'indir;
}
p.pop(); if p.is_dir() {
for entry in fs::read_dir(&p)? {
let mut p = entry?.path().to_owned();
p.push(desktop_name);
if p.exists() {
file_path = Some(p);
break 'indir;
}
}
}
}
}
if let Some(file_path) = file_path {
let ini = Ini::from_filename(&file_path)?;
for (key, value) in ini.iter_section("Desktop Entry") {
if key != "Exec" {
continue;
}
return Ok(Some(String::from(value)));
}
}
}
Ok(None)
}
pub fn query_mime_info<T: AsRef<Path>>(query: T) -> Result<Vec<u8>> {
let command_obj = Command::new("mimetype")
.args(&["--brief", "--dereference"])
.arg(query.as_ref())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.or_else(|_| {
Command::new("file")
.args(&["--brief", "--dereference", "--mime-type"])
.arg(query.as_ref())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
})?;
Ok(drop_right_whitespace(
command_obj.wait_with_output()?.stdout,
))
}
#[inline(always)]
fn drop_right_whitespace(mut vec: Vec<u8>) -> Vec<u8> {
while vec.last() == Some(&b'\n') {
vec.pop();
}
vec
}