1use std::{path::PathBuf, process::Command};
2
3use freedesktop_desktop_entry as fde;
4use mime::Mime;
5use url::Url;
6
7use crate::{Error, Result, exec::ExecParser};
8
9#[derive(Clone, Debug)]
11pub struct UrlHandlerApp {
12 pub appid: String,
13 pub name: String,
14 pub comment: Option<String>,
15 pub icon: fde::IconSource,
16 pub path: PathBuf,
17}
18
19impl UrlHandlerApp {
20 pub fn open_url(&self, url: Url) -> Result<()> {
22 log::info!("Opening URL with {}: {url}", self.appid);
23
24 let locales = fde::get_languages_from_env();
25 let de = fde::DesktopEntry::from_path(self.path.clone(), Some(&locales))?;
26
27 let (cmd, args) = ExecParser::new(&de, &locales).parse_with_uris(&[url.as_str()])?;
28 log::debug!("Executing command: '{cmd}' with args: {args:?}");
29
30 let mut program = Command::new(cmd).args(args).spawn()?;
31 let status = program.wait()?;
32
33 if !status.success() {
34 return Err(Error::OpenUrl(self.appid.clone(), status));
35 }
36
37 Ok(())
38 }
39
40 pub fn handlers_for_scheme(
48 scheme: &str,
49 locales: Option<Vec<String>>,
50 search_paths: Option<Vec<PathBuf>>,
51 ) -> Result<Vec<Self>> {
52 let locales = locales.unwrap_or_else(fde::get_languages_from_env);
53 let search_paths = search_paths.unwrap_or_else(|| fde::default_paths().collect());
54
55 log::debug!(
56 "Searching for applications handling scheme '{scheme}' in paths: {search_paths:?}"
57 );
58
59 let entries = fde::Iter::new(search_paths.into_iter()).entries(Some(&locales));
60 let scheme_handler_mime = format!("x-scheme-handler/{scheme}")
61 .as_str()
62 .parse::<Mime>()?;
63
64 let apps = entries
65 .filter(|de| {
66 de.mime_type()
67 .is_some_and(|mime| mime.contains(&scheme_handler_mime.essence_str()))
68 && !de.id().eq_ignore_ascii_case("kairo")
70 })
71 .map(|entry| Self::from_desktop_entry(entry, &locales))
72 .collect::<Vec<_>>();
73
74 log::info!(
75 "Found {} applications with support for '{scheme_handler_mime}'",
76 apps.len(),
77 );
78
79 if apps.is_empty() {
80 return Err(Error::NoHandlersFound(scheme.to_string()));
81 }
82
83 Ok(apps)
84 }
85
86 pub fn from_desktop_entry<L: AsRef<str>>(de: fde::DesktopEntry, locales: &[L]) -> Self {
93 let appid = de.appid.clone();
94 let name = de
95 .name(locales)
96 .map(|name| name.into())
97 .unwrap_or_else(|| appid.clone());
98
99 Self {
100 appid,
101 name,
102 comment: de.comment(locales).map(|comment| comment.into()),
103 icon: de
104 .icon()
105 .map(fde::IconSource::from_unknown)
106 .unwrap_or_default(),
107 path: de.path,
108 }
109 }
110
111 pub fn icon_path(&self, icon_size: u16) -> Option<std::path::PathBuf> {
112 log::debug!(
113 "Fetching icon for appid={} icon={:?}",
114 self.appid,
115 self.icon
116 );
117
118 match &self.icon {
119 fde::IconSource::Path(path) => Some(path.to_owned()),
120 fde::IconSource::Name(name) => freedesktop_icons::lookup(name)
121 .with_size(icon_size)
122 .with_cache()
123 .find(),
124 }
125 }
126}