kairo_core/
handler.rs

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/// Represents an application that can handle specific URL schemes.
10#[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    /// Opens the given URL with this application.
21    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    /// Retrieves all applications that can handle the specified URL scheme.
41    ///
42    /// # Arguments
43    ///
44    /// * `scheme` - The URL scheme to query (e.g., "http", "mailto").
45    /// * `locales` - Optional list of locales for localization. If `None`, it fetches the system's default locales.
46    /// * `search_paths` - Optional list of paths to search for desktop entries. If `None`, it uses the default XDG paths.
47    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                    // Ignore self on the list
69                    && !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    /// Creates an [App] instance from a [freedesktop_desktop_entry::DesktopEntry].
87    ///
88    /// # Arguments
89    ///
90    /// * `de` - The desktop entry to convert.
91    /// * `locales` - Used for localizing the app's name and comment.
92    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}