mod apps;
mod comment;
mod exec;
mod keyfile;
mod launch;
mod mime;
mod program;
#[cfg(test)]
mod fixture;
#[cfg(test)]
mod tests;
use std::ffi::OsStr;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
pub use apps::{Apps, DesktopApp};
pub use launch::{LaunchError, Launched, graphical_session};
pub use mime::MimeDb;
use crate::diagnostics::{Diagnostic, Location};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct XdgDirs {
pub data_home: Option<PathBuf>,
pub data_dirs: Vec<PathBuf>,
pub config_home: Option<PathBuf>,
pub config_dirs: Vec<PathBuf>,
pub desktops: Vec<String>,
}
impl XdgDirs {
pub fn from_env(lookup: impl Fn(&str) -> Option<String>) -> Self {
let var = |name: &str| lookup(name).filter(|value| !value.is_empty());
let home = var("HOME").map(PathBuf::from).filter(|path| path.is_absolute());
let own = |name: &str, below_home: &str| {
var(name)
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.or_else(|| home.as_ref().map(|home| home.join(below_home)))
};
let system = |name: &str, default: &[&str]| {
let named: Vec<PathBuf> = var(name).map(|value| absolute_folders(&value)).unwrap_or_default();
if named.is_empty() { default.iter().map(PathBuf::from).collect() } else { named }
};
Self {
data_home: own("XDG_DATA_HOME", ".local/share"),
data_dirs: system("XDG_DATA_DIRS", &["/usr/local/share", "/usr/share"]),
config_home: own("XDG_CONFIG_HOME", ".config"),
config_dirs: system("XDG_CONFIG_DIRS", &["/etc/xdg"]),
desktops: var("XDG_CURRENT_DESKTOP")
.map(|value| value.split(':').filter(|name| !name.is_empty()).map(str::to_lowercase).collect())
.unwrap_or_default(),
}
}
fn data(&self) -> impl Iterator<Item = &PathBuf> {
self.data_home.iter().chain(&self.data_dirs)
}
fn config(&self) -> impl Iterator<Item = &PathBuf> {
self.config_home.iter().chain(&self.config_dirs)
}
}
fn absolute_folders(list: &str) -> Vec<PathBuf> {
list.split(':').map(PathBuf::from).filter(|path| path.is_absolute()).collect()
}
#[derive(Debug, Clone)]
pub struct Openers {
pub mime: MimeDb,
pub apps: Apps,
}
impl Openers {
#[must_use]
pub fn load(dirs: &XdgDirs, lang: &str, path_var: Option<&OsStr>) -> Self {
Self { mime: MimeDb::load(dirs), apps: Apps::load(dirs, lang, path_var) }
}
#[must_use]
pub fn diagnostics(&self) -> Vec<&Diagnostic> {
self.mime.diagnostics().iter().chain(self.apps.diagnostics()).collect()
}
#[must_use]
pub fn for_file(&self, path: &Path) -> Choices {
let mime = self.mime.canonical(&self.mime.sniff(path));
let apps: Vec<DesktopApp> = self.apps.for_mime(&self.mime, &mime).into_iter().cloned().collect();
let default =
self.apps.default_for(&self.mime, &mime).and_then(|chosen| apps.iter().position(|app| app.id == chosen.id));
Choices { mime, apps, default }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Choices {
pub mime: String,
pub apps: Vec<DesktopApp>,
pub default: Option<usize>,
}
const LARGEST_FILE: u64 = 16 << 20;
fn read_small(path: &Path, diagnostics: &mut Vec<Diagnostic>) -> Option<Vec<u8>> {
let meta = match fs::metadata(path) {
Ok(meta) => meta,
Err(error) if error.kind() == ErrorKind::NotFound => return None,
Err(error) => {
diagnostics.push(Diagnostic::warning(None, format!("{}: {error}", path.display())));
return None;
}
};
let problem = if !meta.is_file() {
"not a regular file; it is not read"
} else if meta.len() > LARGEST_FILE {
"larger than 16 MiB; it is not read"
} else {
return match fs::read(path) {
Ok(bytes) => Some(bytes),
Err(error) => {
diagnostics.push(Diagnostic::warning(None, format!("{}: {error}", path.display())));
None
}
};
};
diagnostics.push(Diagnostic::warning(None, format!("{}: {problem}", path.display())));
None
}
fn lines<'a>(bytes: &'a [u8], path: &Path, diagnostics: &mut Vec<Diagnostic>) -> Vec<(usize, &'a str)> {
let mut out = Vec::new();
for (index, line) in bytes.split(|&byte| byte == b'\n').enumerate() {
match std::str::from_utf8(line) {
Ok(line) => out.push((index + 1, line.strip_suffix('\r').unwrap_or(line))),
Err(_) => warn(diagnostics, path, index + 1, "the line is not UTF-8; it is skipped"),
}
}
out
}
fn warn(diagnostics: &mut Vec<Diagnostic>, path: &Path, line: usize, message: &str) {
let location = Location { file: path.display().to_string(), line, column: 1 };
diagnostics.push(Diagnostic::warning(Some(location), message));
}