use crate::{
common::{DesktopEntry, ExecMode, UserPath},
config::Config,
error::{Error, Result},
};
use derive_more::{Deref, Display};
use enum_dispatch::enum_dispatch;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::{
convert::TryFrom,
ffi::OsString,
fmt::Display,
hash::{Hash, Hasher},
path::PathBuf,
str::FromStr,
};
use tracing::{debug, info, warn};
#[enum_dispatch(Handleable)]
#[derive(Display, Debug, PartialEq, Eq, Hash)]
pub enum Handler {
DesktopHandler,
RegexHandler,
}
#[cfg(test)]
impl Handler {
pub fn new(name: &str) -> Self {
Handler::DesktopHandler(DesktopHandler::assume_valid(name.into()))
}
}
#[enum_dispatch]
pub trait Handleable {
fn get_entry(&self) -> Result<DesktopEntry>;
#[mutants::skip] fn open(&self, config: &Config, args: Vec<String>) -> Result<()> {
self.get_entry()?.exec(config, ExecMode::Open, args)
}
}
#[derive(
Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct DesktopHandler(OsString);
impl Display for DesktopHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.to_string_lossy())
}
}
impl FromStr for DesktopHandler {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(DesktopHandler(s.into()))
}
}
impl Handleable for DesktopHandler {
fn get_entry(&self) -> Result<DesktopEntry> {
DesktopEntry::try_from(Self::get_path(&self.0)?)
}
}
impl DesktopHandler {
pub fn assume_valid(name: OsString) -> Self {
Self(name)
}
pub fn get_path(name: &std::ffi::OsStr) -> Result<PathBuf> {
if cfg!(test) {
Ok(PathBuf::from(name))
} else {
let mut path = PathBuf::from("applications");
path.push(name);
Ok(xdg::BaseDirectories::new()?
.find_data_file(path)
.ok_or_else(|| {
Error::NotFound(name.to_string_lossy().into())
})?)
}
}
#[mutants::skip] pub fn launch(&self, config: &Config, args: Vec<String>) -> Result<()> {
info!("Launching `{}` with args: {:?}", self, args);
self.get_entry()?.exec(config, ExecMode::Launch, args)
}
pub fn warn_if_invalid(&self) {
if let Err(e) = self.get_entry() {
warn!("The desktop entry `{}` is invalid: {}", self, e);
}
}
}
#[derive(Display, Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[display(fmt = "\"{}\" (Regex Handler)", exec)]
pub struct RegexHandler {
exec: String,
#[serde(default)]
terminal: bool,
regexes: RegexSet,
}
impl RegexHandler {
fn is_match(&self, path: &str) -> bool {
let is_match = self.regexes.is_match(path);
if is_match {
let matches = self
.regexes
.matches(path)
.into_iter()
.map(|index| &self.regexes.patterns()[index])
.collect_vec();
debug!(
"Regex matches found in `{}` for `{}`: {:?}",
self, path, matches
);
} else {
debug!("No regex matches found in `{}` for `{}`", self, path);
}
is_match
}
}
impl Handleable for RegexHandler {
fn get_entry(&self) -> Result<DesktopEntry> {
Ok(DesktopEntry::fake_entry(&self.exec, self.terminal))
}
}
#[derive(Deref, Debug, Clone, Deserialize)]
struct RegexSet(#[serde(with = "serde_regex")] regex::RegexSet);
#[cfg(test)]
impl RegexSet {
pub fn new<I, S>(exprs: I) -> Result<Self>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
Ok(RegexSet(regex::RegexSet::new(exprs)?))
}
}
impl PartialEq for RegexSet {
#[mutants::skip] fn eq(&self, other: &Self) -> bool {
self.patterns() == other.patterns()
}
}
impl Eq for RegexSet {}
impl Hash for RegexSet {
#[mutants::skip] fn hash<H: Hasher>(&self, state: &mut H) {
self.patterns().hash(state);
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RegexApps(Vec<RegexHandler>);
impl RegexApps {
pub fn get_handler(&self, path: &UserPath) -> Result<RegexHandler> {
Ok(self
.0
.iter()
.find(|app| app.is_match(&path.to_string()))
.ok_or_else(|| Error::NotFound(path.to_string()))?
.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::DesktopEntry;
use url::Url;
#[test]
fn regex_handlers() -> Result<()> {
let exec: &str = "freetube %u";
let regexes: &[String] =
&[String::from(r"(https://)?(www\.)?youtu(be\.com|\.be)/*")];
let regex_handler = RegexHandler {
exec: String::from(exec),
terminal: false,
regexes: RegexSet::new(regexes)?,
};
let regex_apps = RegexApps(vec![regex_handler.clone()]);
assert_eq!(
regex_apps
.get_handler(&UserPath::Url(Url::parse(
"https://youtu.be/dQw4w9WgXcQ"
)?))?
.get_entry()?,
DesktopEntry {
exec: exec.to_string(),
terminal: false,
..Default::default()
}
);
assert!(regex_apps
.get_handler(&UserPath::Url(Url::parse(
"https://en.wikipedia.org",
)?))
.is_err());
Ok(())
}
}