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<u32> {
22        log::info!(
23            "Opening URL '{}' with application '{}'",
24            url,
25            self.path.display()
26        );
27
28        let locales = fde::get_languages_from_env();
29        let de = fde::DesktopEntry::from_path(self.path.clone(), Some(&locales))?;
30
31        let (cmd, args) = ExecParser::new(&de, &locales).parse_with_uris(&[url.as_str()])?;
32        log::debug!("Executing command: '{}' with args: {:?}", cmd, args);
33
34        let program = Command::new(cmd).args(args).spawn()?;
35
36        Ok(program.id())
37    }
38
39    /// Retrieves all applications that can handle the specified URL scheme.
40    ///
41    /// # Arguments
42    ///
43    /// * `scheme` - The URL scheme to query (e.g., "http", "mailto").
44    /// * `locales` - Optional list of locales for localization. If `None`, it fetches the system's default locales.
45    /// * `search_paths` - Optional list of paths to search for desktop entries. If `None`, it uses the default XDG paths.
46    pub fn handlers_for_scheme(
47        scheme: &str,
48        locales: Option<Vec<String>>,
49        search_paths: Option<Vec<PathBuf>>,
50    ) -> Result<Vec<Self>> {
51        let locales = locales.unwrap_or_else(fde::get_languages_from_env);
52        let search_paths = search_paths.unwrap_or_else(|| fde::default_paths().collect());
53
54        log::debug!(
55            "Searching for applications handling scheme '{}' in paths: {:?}",
56            scheme,
57            search_paths
58        );
59
60        let entries = fde::Iter::new(search_paths.into_iter()).entries(Some(&locales));
61        let scheme_handler_mime = format!("x-scheme-handler/{}", scheme)
62            .as_str()
63            .parse::<Mime>()?;
64
65        let apps = entries
66            .filter(|de| {
67                de.mime_type()
68                    .is_some_and(|mime| mime.contains(&scheme_handler_mime.essence_str()))
69                    // Ignore self on the list
70                    && !de.id().eq_ignore_ascii_case("kairo")
71            })
72            .map(|entry| Self::from_desktop_entry(entry, &locales))
73            .collect::<Vec<_>>();
74
75        log::info!(
76            "Found {} applications with support for '{}'",
77            apps.len(),
78            scheme_handler_mime
79        );
80
81        if apps.is_empty() {
82            return Err(Error::NoHandlersFound(scheme.to_string()));
83        }
84
85        Ok(apps)
86    }
87
88    /// Creates an [App] instance from a [freedesktop_desktop_entry::DesktopEntry].
89    ///
90    /// # Arguments
91    ///
92    /// * `de` - The desktop entry to convert.
93    /// * `locales` - Used for localizing the app's name and comment.
94    pub fn from_desktop_entry<L: AsRef<str>>(de: fde::DesktopEntry, locales: &[L]) -> Self {
95        let appid = de.appid.clone();
96        let name = de
97            .name(locales)
98            .map(|name| name.into())
99            .unwrap_or_else(|| appid.clone());
100
101        Self {
102            appid,
103            name,
104            comment: de.comment(locales).map(|comment| comment.into()),
105            icon: de
106                .icon()
107                .map(fde::IconSource::from_unknown)
108                .unwrap_or_default(),
109            path: de.path,
110        }
111    }
112
113    pub fn icon_path(&self, icon_size: u16) -> Option<std::path::PathBuf> {
114        log::debug!(
115            "Fetching icon for appid={} icon={:?}",
116            self.appid,
117            self.icon
118        );
119
120        match &self.icon {
121            fde::IconSource::Path(path) => Some(path.to_owned()),
122            fde::IconSource::Name(name) => freedesktop_icons::lookup(name)
123                .with_size(icon_size)
124                .with_cache()
125                .find(),
126        }
127    }
128}