auto_launcher/
linux.rs

1use crate::{AutoLaunch, LinuxLaunchMode, Result};
2use std::{fs, io::Write, path::PathBuf};
3
4/// Linux implement
5impl AutoLaunch {
6    /// Create a new AutoLaunch instance
7    /// - `app_name`: application name
8    /// - `app_path`: application path
9    /// - `launch_mode`: launch mode (XDG Autostart or systemd)
10    /// - `args`: startup args passed to the binary
11    ///
12    /// ## Notes
13    ///
14    /// The parameters of `AutoLaunch::new` are different on each platform.
15    pub fn new(
16        app_name: &str,
17        app_path: &str,
18        launch_mode: LinuxLaunchMode,
19        args: &[impl AsRef<str>],
20    ) -> AutoLaunch {
21        AutoLaunch {
22            app_name: app_name.into(),
23            app_path: app_path.into(),
24            launch_mode,
25            args: args.iter().map(|s| s.as_ref().to_string()).collect(),
26        }
27    }
28
29    /// Enable the AutoLaunch setting
30    ///
31    /// ## Errors
32    ///
33    /// - failed to create directory
34    /// - failed to create file
35    /// - failed to write bytes to the file
36    /// - failed to enable systemd service (if using systemd mode)
37    pub fn enable(&self) -> Result<()> {
38        match self.launch_mode {
39            LinuxLaunchMode::XdgAutostart => self.enable_xdg_autostart(),
40            LinuxLaunchMode::Systemd => self.enable_systemd(),
41        }
42    }
43
44    /// Enable using XDG Autostart (.desktop file)
45    fn enable_xdg_autostart(&self) -> Result<()> {
46        let data = format!(
47            "[Desktop Entry]\n\
48            Type=Application\n\
49            Version=1.0\n\
50            Name={}\n\
51            Comment={} startup script\n\
52            Exec={} {}\n\
53            StartupNotify=false\n\
54            Terminal=false",
55            self.app_name,
56            self.app_name,
57            self.app_path,
58            self.args.join(" ")
59        );
60
61        let dir = get_xdg_autostart_dir();
62        if !dir.exists() {
63            fs::create_dir_all(&dir).or_else(|e| {
64                if e.kind() == std::io::ErrorKind::AlreadyExists {
65                    Ok(())
66                } else {
67                    Err(e)
68                }
69            })?;
70        }
71        let file_path = self.get_xdg_desktop_file();
72        let mut file = fs::OpenOptions::new()
73            .write(true)
74            .create(true)
75            .truncate(true)
76            .open(file_path)?;
77        file.write_all(data.as_bytes())?;
78        Ok(())
79    }
80
81    /// Enable using systemd user service
82    fn enable_systemd(&self) -> Result<()> {
83        // Create systemd service file content
84        let args_str = if self.args.is_empty() {
85            String::new()
86        } else {
87            format!(" {}", self.args.join(" "))
88        };
89
90        let data = format!(
91            "[Unit]\n\
92            Description={}\n\
93            After=default.target\n\
94            \n\
95            [Service]\n\
96            Type=simple\n\
97            ExecStart={}{}\n\
98            Restart=on-failure\n\
99            RestartSec=10\n\
100            \n\
101            [Install]\n\
102            WantedBy=default.target",
103            self.app_name, self.app_path, args_str
104        );
105
106        // Create systemd user directory
107        let dir = get_systemd_user_dir();
108        if !dir.exists() {
109            fs::create_dir_all(&dir).or_else(|e| {
110                if e.kind() == std::io::ErrorKind::AlreadyExists {
111                    Ok(())
112                } else {
113                    Err(e)
114                }
115            })?;
116        }
117
118        // Write service file
119        let service_file = self.get_systemd_service_file();
120        let mut file = fs::OpenOptions::new()
121            .write(true)
122            .create(true)
123            .truncate(true)
124            .open(&service_file)?;
125        file.write_all(data.as_bytes())?;
126
127        // Enable and start the service using systemctl
128        self.systemctl_enable()?;
129
130        Ok(())
131    }
132
133    /// Run systemctl --user enable command
134    fn systemctl_enable(&self) -> Result<()> {
135        let service_name = format!("{}.service", self.app_name);
136        let output = std::process::Command::new("systemctl")
137            .args(&["--user", "enable", &service_name])
138            .output()?;
139
140        if !output.status.success() {
141            return Err(std::io::Error::new(
142                std::io::ErrorKind::Other,
143                format!(
144                    "Failed to enable systemd service: {}",
145                    String::from_utf8_lossy(&output.stderr)
146                ),
147            )
148            .into());
149        }
150
151        Ok(())
152    }
153
154    /// Disable the AutoLaunch setting
155    ///
156    /// ## Errors
157    ///
158    /// - failed to remove file
159    /// - failed to disable systemd service (if using systemd mode)
160    pub fn disable(&self) -> Result<()> {
161        match self.launch_mode {
162            LinuxLaunchMode::XdgAutostart => self.disable_xdg_autostart(),
163            LinuxLaunchMode::Systemd => self.disable_systemd(),
164        }
165    }
166
167    /// Disable XDG Autostart
168    fn disable_xdg_autostart(&self) -> Result<()> {
169        let file = self.get_xdg_desktop_file();
170        if file.exists() {
171            fs::remove_file(file)?;
172        }
173        Ok(())
174    }
175
176    /// Disable systemd user service
177    fn disable_systemd(&self) -> Result<()> {
178        // Disable the service
179        self.systemctl_disable()?;
180
181        // Remove service file
182        let service_file = self.get_systemd_service_file();
183        if service_file.exists() {
184            fs::remove_file(service_file)?;
185        }
186
187        // Reload systemd daemon
188        let _ = std::process::Command::new("systemctl")
189            .args(&["--user", "daemon-reload"])
190            .output();
191
192        Ok(())
193    }
194
195    /// Run systemctl --user disable command
196    fn systemctl_disable(&self) -> Result<()> {
197        let service_name = format!("{}.service", self.app_name);
198        let output = std::process::Command::new("systemctl")
199            .args(&["--user", "disable", &service_name])
200            .output()?;
201
202        // Don't fail if the service is not enabled
203        if !output.status.success() {
204            let stderr = String::from_utf8_lossy(&output.stderr);
205            if !stderr.contains("No such file or directory") && !stderr.contains("not loaded") {
206                return Err(std::io::Error::new(
207                    std::io::ErrorKind::Other,
208                    format!("Failed to disable systemd service: {}", stderr),
209                )
210                .into());
211            }
212        }
213
214        Ok(())
215    }
216
217    /// Check whether the AutoLaunch setting is enabled
218    pub fn is_enabled(&self) -> Result<bool> {
219        match self.launch_mode {
220            LinuxLaunchMode::XdgAutostart => Ok(self.get_xdg_desktop_file().exists()),
221            LinuxLaunchMode::Systemd => self.is_systemd_enabled(),
222        }
223    }
224
225    /// Check if systemd service is enabled
226    fn is_systemd_enabled(&self) -> Result<bool> {
227        let service_name = format!("{}.service", self.app_name);
228        let output = std::process::Command::new("systemctl")
229            .args(&["--user", "is-enabled", &service_name])
230            .output()?;
231
232        // systemctl is-enabled returns:
233        // - "enabled" with exit code 0 if enabled
234        // - "disabled" with exit code 1 if disabled
235        // - other states or errors with other exit codes
236        Ok(output.status.success())
237    }
238
239    /// Get the XDG desktop entry file path
240    fn get_xdg_desktop_file(&self) -> PathBuf {
241        get_xdg_autostart_dir().join(format!("{}.desktop", self.app_name))
242    }
243
244    /// Get the systemd service file path
245    fn get_systemd_service_file(&self) -> PathBuf {
246        get_systemd_user_dir().join(format!("{}.service", self.app_name))
247    }
248}
249
250/// Get the XDG autostart directory
251fn get_xdg_autostart_dir() -> PathBuf {
252    dirs::home_dir().unwrap().join(".config").join("autostart")
253}
254
255/// Get the systemd user service directory
256fn get_systemd_user_dir() -> PathBuf {
257    dirs::home_dir()
258        .unwrap()
259        .join(".config")
260        .join("systemd")
261        .join("user")
262}