Skip to main content

mimick/
autostart.rs

1//! Handles autostart integration for both native installations and sandboxed Flatpak builds.
2//!
3//! Under Flatpak, uses the XDG Background portal to request autostart
4//! permission. On bare-metal installs, writes or removes a `.desktop`
5//! file in `~/.config/autostart/`. The portal request is asynchronous
6//! and may be denied by the user.
7
8use ashpd::WindowIdentifier;
9use ashpd::desktop::background::Background;
10use gtk::prelude::*;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14const AUTOSTART_DESKTOP_ID: &str = "dev.nicx.mimick.desktop";
15const APP_ID: &str = "dev.nicx.mimick";
16const AUTOSTART_REASON: &str = "Reason for requesting background access: Mimick must run in the background to automatically sync media to Immich.";
17
18/// Configure or request autostart registration depending on container and integration style.
19pub async fn apply(window: &impl IsA<gtk::Window>, enable: bool) -> Result<bool, String> {
20    if is_flatpak_sandbox() {
21        request_background_portal(window, enable).await
22    } else if enable {
23        install_desktop_entry().map(|_| true)
24    } else {
25        remove_desktop_entry().map(|_| false)
26    }
27}
28
29/// Check if running inside Flatpak sandbox environment.
30fn is_flatpak_sandbox() -> bool {
31    Path::new("/.flatpak-info").exists()
32}
33
34/// Request autostart state change through the XDG Background portal.
35async fn request_background_portal(
36    window: &impl IsA<gtk::Window>,
37    enable: bool,
38) -> Result<bool, String> {
39    let identifier = match window.as_ref().native() {
40        Some(native) => WindowIdentifier::from_native(&native).await,
41        None => None,
42    };
43
44    let response = Background::request()
45        .identifier(identifier)
46        .reason(AUTOSTART_REASON)
47        .auto_start(enable)
48        .dbus_activatable(false)
49        .send()
50        .await
51        .map_err(|err| format!("Failed to contact the background portal: {err}"))?
52        .response()
53        .map_err(|err| format!("The desktop rejected the autostart request: {err}"))?;
54
55    if enable {
56        Ok(response.auto_start() && response.run_in_background())
57    } else {
58        Ok(response.auto_start())
59    }
60}
61
62/// Install autostart desktop shortcut entry in standard non-flatpak configurations.
63fn install_desktop_entry() -> Result<(), String> {
64    let entry_path = default_autostart_entry_path()?;
65    if let Some(parent) = entry_path.parent() {
66        fs::create_dir_all(parent)
67            .map_err(|err| format!("Failed to create autostart directory: {err}"))?;
68    }
69
70    let executable = std::env::current_exe()
71        .map_err(|err| format!("Failed to resolve the Mimick executable path: {err}"))?;
72    let escaped_exec = escape_desktop_exec_arg(&executable.to_string_lossy());
73
74    let desktop_entry = format!(
75        "[Desktop Entry]\nType=Application\nVersion=1.0\nName=Mimick\nComment=Unofficial Immich desktop client and auto-sync agent\nExec={escaped_exec}\nIcon=dev.nicx.mimick\nTerminal=false\nCategories=Utility;\nX-GNOME-Autostart-enabled=true\nStartupNotify=false\n"
76    );
77
78    fs::write(&entry_path, desktop_entry)
79        .map_err(|err| format!("Failed to write autostart entry: {err}"))?;
80
81    Ok(())
82}
83
84/// Remove autostart desktop shortcut entry if one exists.
85fn remove_desktop_entry() -> Result<(), String> {
86    for entry_path in autostart_entry_paths()? {
87        if entry_path.exists() {
88            fs::remove_file(&entry_path)
89                .map_err(|err| format!("Failed to remove autostart entry: {err}"))?;
90        }
91    }
92    Ok(())
93}
94
95/// Retrieve default system autostart configuration shortcut folder path.
96fn default_autostart_entry_path() -> Result<PathBuf, String> {
97    let config_dir = dirs::config_dir()
98        .ok_or_else(|| "Could not locate the user config directory.".to_string())?;
99    Ok(config_dir.join("autostart").join(AUTOSTART_DESKTOP_ID))
100}
101
102/// Retrieve possible system autostart shortcut path list (sandboxed and unsandboxed).
103fn autostart_entry_paths() -> Result<Vec<PathBuf>, String> {
104    let config_dir = dirs::config_dir()
105        .ok_or_else(|| "Could not locate the user config directory.".to_string())?;
106    let mut paths = vec![config_dir.join("autostart").join(AUTOSTART_DESKTOP_ID)];
107
108    if let Some(host_config_dir) = flatpak_host_config_dir_from(&config_dir) {
109        paths.push(host_config_dir.join("autostart").join(AUTOSTART_DESKTOP_ID));
110    }
111
112    paths.sort();
113    paths.dedup();
114    Ok(paths)
115}
116
117/// Map a sandboxed `~/.var/app/<app-id>/config` path back to the host `~/.config` path.
118fn flatpak_host_config_dir_from(config_dir: &Path) -> Option<PathBuf> {
119    let app_dir = config_dir.parent()?;
120    let app_parent = app_dir.parent()?;
121    let var_dir = app_parent.parent()?;
122    let host_home = var_dir.parent()?;
123
124    if config_dir.file_name()? != "config" {
125        return None;
126    }
127    if app_dir.file_name()? != APP_ID {
128        return None;
129    }
130    if app_parent.file_name()? != "app" {
131        return None;
132    }
133    if var_dir.file_name()? != ".var" {
134        return None;
135    }
136
137    Some(host_home.join(".config"))
138}
139
140/// Escape an executable path for the `Exec=` field of a desktop entry.
141fn escape_desktop_exec_arg(value: &str) -> String {
142    let mut escaped = String::with_capacity(value.len());
143    for ch in value.chars() {
144        match ch {
145            '\\' => escaped.push_str("\\\\"),
146            ' ' => escaped.push_str("\\ "),
147            '\t' => escaped.push_str("\\t"),
148            '\n' => escaped.push_str("\\n"),
149            '"' => escaped.push_str("\\\""),
150            '\'' => escaped.push_str("\\'"),
151            _ => escaped.push(ch),
152        }
153    }
154    escaped
155}
156
157#[cfg(test)]
158mod tests {
159    use super::{escape_desktop_exec_arg, flatpak_host_config_dir_from};
160    use std::path::Path;
161
162    #[test]
163    fn test_escape_desktop_exec_arg() {
164        assert_eq!(
165            escape_desktop_exec_arg("/tmp/My App/mimick"),
166            "/tmp/My\\ App/mimick"
167        );
168    }
169
170    #[test]
171    fn test_flatpak_host_config_dir_from_sandbox_path() {
172        assert_eq!(
173            flatpak_host_config_dir_from(Path::new("/home/user/.var/app/dev.nicx.mimick/config"))
174                .unwrap(),
175            Path::new("/home/user/.config")
176        );
177    }
178}