1use 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
18pub 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
29fn is_flatpak_sandbox() -> bool {
31 Path::new("/.flatpak-info").exists()
32}
33
34async 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
62fn 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
84fn 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
95fn 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
102fn 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
117fn 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
140fn 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}