Skip to main content

auto_launcher/
lib.rs

1//! Auto launch any application or executable at startup. Supports Windows, Mac (via AppleScript or Launch Agent), and Linux.
2//!
3//! ## Usage
4//!
5//! The parameters of `AutoLaunch::new` are different on each platform.
6//! See the function definition or the demo below for details.
7//!
8//! Or you can construct the AutoLaunch by using `AutoLaunchBuilder`.
9//!
10//! ```rust
11//! # #[cfg(target_os = "linux")]
12//! # mod linux {
13//! use auto_launcher::{AutoLaunch, LinuxLaunchMode};
14//!
15//! fn main() {
16//!     let app_name = "the-app";
17//!     let app_path = "/path/to/the-app";
18//!     let args = &["--minimized"];
19//!     // Use XDG Autostart by default, or use LinuxLaunchMode::SystemdUser for systemd
20//!     let auto = AutoLaunch::new(app_name, app_path, LinuxLaunchMode::XdgAutostart, args);
21//!
22//!     // enable the auto launch
23//!     auto.enable().is_ok();
24//!     auto.is_enabled().unwrap();
25//!
26//!     // disable the auto launch
27//!     auto.disable().is_ok();
28//!     auto.is_enabled().unwrap();
29//! }
30//! # }
31//! ```
32//!
33//! ### macOS
34//!
35//! macOS supports five ways to achieve auto launch:
36//! - **Launch Agent (User)**: Uses plist files in `~/Library/LaunchAgents/` (default)
37//! - **Launch Agent (System)**: Uses plist files in `/Library/LaunchAgents/` (runs as the logged-in user)
38//! - **Launch Daemon (System)**: Uses plist files in `/Library/LaunchDaemons/` (runs as root)
39//! - **AppleScript**: Uses AppleScript to add login items
40//! - **SMAppService**: Uses the SMAppService API (macOS 13+)
41//!
42//! **Note**:
43//! - The `app_path` should be a absolute path and exists. Otherwise, it will cause an error when `enable`.
44//! - In case using AppleScript, the `app_name` should be same as the basename of `app_path`, or it will be corrected automatically.
45//! - In case using AppleScript, only `--hidden` and `--minimized` in `args` are valid, which means that hide the app on launch.
46//!
47//! ```rust
48//! # #[cfg(target_os = "macos")]
49//! # mod macos {
50//! use auto_launcher::{AutoLaunch, MacOSLaunchMode};
51//!
52//! fn main() {
53//!     let app_name = "the-app";
54//!     let app_path = "/path/to/the-app.app";
55//!     let args = &["--minimized"];
56//!     let bundle_identifiers = &["com.github.auto-launch-test"];
57//!     // Use Launch Agent by default, or use MacOSLaunchMode::AppleScript
58//!     let auto = AutoLaunch::new(app_name, app_path, MacOSLaunchMode::LaunchAgentUser, args, bundle_identifiers, "");
59//!
60//!     // enable the auto launch
61//!     auto.enable().is_ok();
62//!     auto.is_enabled().unwrap();
63//!
64//!     // disable the auto launch
65//!     auto.disable().is_ok();
66//!     auto.is_enabled().unwrap();
67//! }
68//! # }
69//! ```
70//!
71//! ### Windows
72//!
73//! On Windows, it will add a registry entry under either `\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run` (system-wide) or
74//! `\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run` (current user only).
75//!
76//! By default we try to apply the auto launch to the system registry, which requires admin privileges and applies the auto launch to any user in the system.
77//! If there's no permission to do so, we fallback to enabling it to the current user only.
78//! To change this behavior, specify the [`WindowsEnableMode`] when creating the [`AutoLaunch`] instance.
79//!
80//! ```rust
81//! # #[cfg(target_os = "windows")]
82//! # mod win {
83//! use auto_launcher::{AutoLaunch, WindowsEnableMode};
84//!
85//! fn main() {
86//!     let app_name = "the-app";
87//!     let app_path = "C:\\path\\to\\the-app.exe";
88//!     let args = &["--minimized"];
89//!     let enable_mode = WindowsEnableMode::CurrentUser;
90//!     let auto = AutoLaunch::new(app_name, app_path, enable_mode, args);
91//!
92//!     // enable the auto launch
93//!     auto.enable().is_ok();
94//!     auto.is_enabled().unwrap();
95//!
96//!     // disable the auto launch
97//!     auto.disable().is_ok();
98//!     auto.is_enabled().unwrap();
99//! }
100//! # }
101//! ```
102//!
103//! ### Builder
104//!
105//! AutoLaunch Builder helps to eliminate the constructor difference
106//! on various platforms.
107//!
108//! ```rust
109//! use auto_launcher::*;
110//!
111//! # fn example() -> std::result::Result<(), Box<dyn std::error::Error>> {
112//! let auto = AutoLaunchBuilder::new()
113//!     .set_app_name("the-app")
114//!     .set_app_path("/path/to/the-app")
115//!     .set_macos_launch_mode(MacOSLaunchMode::LaunchAgentUser)
116//!     .set_args(&["--minimized"])
117//!     .build()?;
118//!
119//! auto.enable()?;
120//! auto.is_enabled()?;
121//!
122//! auto.disable()?;
123//! auto.is_enabled()?;
124//! # Ok(())
125//! # }
126//! ```
127//!
128
129#[derive(thiserror::Error, Debug)]
130pub enum Error {
131    #[error("app_name shouldn't be None")]
132    AppNameNotSpecified,
133    #[error("app_path shouldn't be None")]
134    AppPathNotSpecified,
135    #[error("app path doesn't exist: {0}")]
136    AppPathDoesntExist(std::path::PathBuf),
137    #[error("app path is not absolute: {0}")]
138    AppPathIsNotAbsolute(std::path::PathBuf),
139    #[error("Failed to execute apple script with status: {0}")]
140    AppleScriptFailed(i32),
141    #[error("Failed to register app with SMAppService with status: {0}")]
142    SMAppServiceRegistrationFailed(u32),
143    #[error("Failed to unregister app with SMAppService with status: {0}")]
144    SMAppServiceUnregistrationFailed(u32),
145    #[error("Unsupported target os")]
146    UnsupportedOS,
147    #[error(transparent)]
148    Io(#[from] std::io::Error),
149}
150
151pub type Result<T> = std::result::Result<T, Error>;
152
153#[cfg(target_os = "linux")]
154mod linux;
155#[cfg(target_os = "macos")]
156mod macos;
157#[cfg(target_os = "windows")]
158mod windows;
159
160/// The parameters of `AutoLaunch::new` are different on each platform.
161///
162/// ### Linux
163///
164/// ```rust
165/// # #[cfg(target_os = "linux")]
166/// # {
167/// # use auto_launcher::{AutoLaunch, LinuxLaunchMode};
168/// # let app_name = "the-app";
169/// # let app_path = "/path/to/the-app";
170/// # let launch_mode = LinuxLaunchMode::XdgAutostart;
171/// # let args = &["--minimized"];
172/// AutoLaunch::new(app_name, app_path, launch_mode, args);
173/// # }
174/// ```
175///
176/// ### Macos
177///
178/// ```rust
179/// # #[cfg(target_os = "macos")]
180/// # {
181/// # use auto_launcher::{AutoLaunch, MacOSLaunchMode};
182/// # let app_name = "the-app";
183/// # let app_path = "/path/to/the-app";
184/// # let launch_mode = MacOSLaunchMode::LaunchAgentUser;
185/// # let args = &["--minimized"];
186/// # let bundle_identifiers = &["com.github.auto-launch-test"];
187/// AutoLaunch::new(app_name, app_path, launch_mode, args, bundle_identifiers, "");
188/// # }
189/// ```
190///
191/// ### Windows
192///
193/// ```rust
194/// # #[cfg(target_os = "windows")]
195/// # {
196/// # use auto_launcher::{AutoLaunch, WindowsEnableMode};
197/// # let app_name = "the-app";
198/// # let app_path = "/path/to/the-app";
199/// # let args = &["--minimized"];
200/// # let enable_mode = WindowsEnableMode::CurrentUser;
201/// AutoLaunch::new(app_name, app_path, enable_mode, args);
202/// # }
203/// ```
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct AutoLaunch {
206    /// The application name
207    pub(crate) app_name: String,
208
209    /// The application executable path (absolute path will be better)
210    pub(crate) app_path: String,
211
212    /// Args passed to the binary on startup
213    pub(crate) args: Vec<String>,
214
215    #[cfg(target_os = "linux")]
216    /// Launch mode for Linux (XDG Autostart or systemd)
217    pub(crate) launch_mode: LinuxLaunchMode,
218
219    #[cfg(target_os = "macos")]
220    /// Launch mode for macOS (Launch Agent, AppleScript, or SMAppService)
221    pub(crate) launch_mode: MacOSLaunchMode,
222
223    #[cfg(target_os = "macos")]
224    /// Bundle identifiers
225    pub(crate) bundle_identifiers: Vec<String>,
226
227    #[cfg(target_os = "macos")]
228    /// Extra config in plist file for Launch Agent
229    pub(crate) agent_extra_config: String,
230
231    #[cfg(windows)]
232    pub(crate) enable_mode: WindowsEnableMode,
233}
234
235impl AutoLaunch {
236    /// check whether it is support the platform
237    ///
238    /// ## Usage
239    ///
240    /// ```rust
241    /// use auto_launcher::AutoLaunch;
242    ///
243    /// dbg!(AutoLaunch::is_support());
244    /// ```
245    pub fn is_support() -> bool {
246        cfg!(any(
247            target_os = "linux",
248            target_os = "macos",
249            target_os = "windows",
250        ))
251    }
252
253    /// get the application name
254    pub fn get_app_name(&self) -> &str {
255        &self.app_name
256    }
257
258    /// get the application path
259    pub fn get_app_path(&self) -> &str {
260        &self.app_path
261    }
262
263    /// get the args
264    pub fn get_args(&self) -> &[String] {
265        &self.args
266    }
267}
268
269#[derive(Debug, Default, Clone)]
270/// AutoLaunch Builder helps to eliminate the constructor difference
271/// on various platforms.
272///
273/// ## Notes
274///
275/// The builder will not check whether the app_path matches the platform-specify file path.
276///
277/// ## Usage
278///
279/// ```rust
280/// use auto_launcher::*;
281///
282/// # fn example() -> std::result::Result<(), Box<dyn std::error::Error>> {
283/// let auto = AutoLaunchBuilder::new()
284///     .set_app_name("the-app")
285///     .set_app_path("/path/to/the-app")
286///     .set_macos_launch_mode(MacOSLaunchMode::LaunchAgentUser)
287///     .set_args(&["--minimized"])
288///     .build()?;
289///
290/// auto.enable()?;
291/// auto.is_enabled()?;
292///
293/// auto.disable()?;
294/// auto.is_enabled()?;
295/// # Ok(())
296/// # }
297/// ```
298pub struct AutoLaunchBuilder {
299    pub app_name: Option<String>,
300
301    pub app_path: Option<String>,
302
303    pub macos_launch_mode: MacOSLaunchMode,
304
305    pub bundle_identifiers: Option<Vec<String>>,
306
307    pub agent_extra_config: Option<String>,
308
309    pub windows_enable_mode: WindowsEnableMode,
310
311    pub linux_launch_mode: LinuxLaunchMode,
312
313    pub args: Option<Vec<String>>,
314}
315
316/// Determines how the auto launch is enabled on Linux.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
318pub enum LinuxLaunchMode {
319    /// Use XDG Autostart (.desktop file in ~/.config/autostart/)
320    #[default]
321    XdgAutostart,
322    /// Use systemd user service (~/.config/systemd/user/)
323    SystemdUser,
324    /// Use systemd system service (/etc/systemd/system/)
325    SystemdSystem,
326}
327
328impl LinuxLaunchMode {
329    #[deprecated(since = "1.0.0", note = "Use `LinuxLaunchMode::SystemdUser` instead")]
330    #[allow(non_upper_case_globals)]
331    pub const Systemd: Self = Self::SystemdUser;
332}
333
334/// Determines how the auto launch is enabled on macOS.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
336pub enum MacOSLaunchMode {
337    /// Use Launch Agent (plist file in ~/Library/LaunchAgents/).
338    /// Runs as the current user. No elevated privileges required.
339    #[default]
340    LaunchAgentUser,
341    /// Use Launch Agent (plist file in /Library/LaunchAgents/).
342    /// Visible to all users, but still runs as the logged-in user (not root).
343    /// Writing to /Library/LaunchAgents/ requires root/sudo.
344    LaunchAgentSystem,
345    /// Use Launch Daemon (plist file in /Library/LaunchDaemons/).
346    /// Runs as root (system-level). Writing to /Library/LaunchDaemons/ requires root/sudo.
347    LaunchDaemonSystem,
348    /// Use AppleScript to add login item.
349    AppleScript,
350    /// Use SMAppService API to enable the auto launch (macOS 13+).
351    SMAppService,
352}
353
354impl MacOSLaunchMode {
355    #[deprecated(
356        since = "0.6.0",
357        note = "Use `MacOSLaunchMode::LaunchAgentUser` instead"
358    )]
359    #[allow(non_upper_case_globals)]
360    pub const LaunchAgent: Self = Self::LaunchAgentUser;
361}
362
363/// Determines how the auto launch is enabled on Windows.
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
365pub enum WindowsEnableMode {
366    /// Dynamically tries to enable the auto launch for the system (admin privileges required),
367    /// fallbacks to the current user if there is no permission to modify the system registry.
368    #[default]
369    Dynamic,
370    /// Enables the auto launch for the current user only. Does not require admin permissions.
371    CurrentUser,
372    /// Enables the auto launch for all users. Requires admin permissions.
373    System,
374}
375
376impl AutoLaunchBuilder {
377    pub fn new() -> AutoLaunchBuilder {
378        AutoLaunchBuilder::default()
379    }
380
381    /// Set the `app_name`
382    pub fn set_app_name(&mut self, name: &str) -> &mut Self {
383        self.app_name = Some(name.into());
384        self
385    }
386
387    /// Set the `app_path`
388    pub fn set_app_path(&mut self, path: &str) -> &mut Self {
389        self.app_path = Some(path.into());
390        self
391    }
392
393    /// Set the [`MacOSLaunchMode`].
394    /// This setting only works on macOS
395    pub fn set_macos_launch_mode(&mut self, mode: MacOSLaunchMode) -> &mut Self {
396        self.macos_launch_mode = mode;
397        self
398    }
399
400    /// Set the `use_launch_agent` (deprecated: use `set_macos_launch_mode` instead)
401    /// This setting only works on macOS
402    #[deprecated(since = "0.6.0", note = "Use `set_macos_launch_mode` instead")]
403    pub fn set_use_launch_agent(&mut self, use_launch_agent: bool) -> &mut Self {
404        self.macos_launch_mode = if use_launch_agent {
405            MacOSLaunchMode::LaunchAgentUser
406        } else {
407            MacOSLaunchMode::AppleScript
408        };
409        self
410    }
411
412    /// Set the `bundle_identifiers`
413    /// This setting only works on macOS
414    pub fn set_bundle_identifiers(&mut self, bundle_identifiers: &[impl AsRef<str>]) -> &mut Self {
415        self.bundle_identifiers = Some(
416            bundle_identifiers
417                .iter()
418                .map(|s| s.as_ref().to_string())
419                .collect(),
420        );
421        self
422    }
423
424    /// Set the `agent_extra_config`
425    /// This setting only works on macOS
426    pub fn set_agent_extra_config(&mut self, config: &str) -> &mut Self {
427        self.agent_extra_config = Some(config.into());
428        self
429    }
430
431    /// Set the [`WindowsEnableMode`].
432    /// This setting only works on Windows
433    pub fn set_windows_enable_mode(&mut self, mode: WindowsEnableMode) -> &mut Self {
434        self.windows_enable_mode = mode;
435        self
436    }
437
438    /// Set the [`LinuxLaunchMode`].
439    /// This setting only works on Linux
440    pub fn set_linux_launch_mode(&mut self, mode: LinuxLaunchMode) -> &mut Self {
441        self.linux_launch_mode = mode;
442        self
443    }
444
445    /// Set the args
446    pub fn set_args(&mut self, args: &[impl AsRef<str>]) -> &mut Self {
447        self.args = Some(args.iter().map(|s| s.as_ref().to_string()).collect());
448        self
449    }
450
451    /// Construct a AutoLaunch instance
452    ///
453    /// ## Errors
454    ///
455    #[allow(clippy::needless_return)]
456    /// - `app_name` is none
457    /// - `app_path` is none
458    /// - Unsupported target OS
459    pub fn build(&self) -> Result<AutoLaunch> {
460        let default_str = String::new();
461        /*
462         * When SMAppService is used, app_name and app_path are ignored. This
463         * is because the SMAppService API is used to register the running app.
464         *
465         * We also need to check whether the os version is compatible with SMAppService.
466         */
467        let (app_name, app_path) = if self.macos_launch_mode == MacOSLaunchMode::SMAppService {
468            let info = os_info::get();
469            match info.version() {
470                os_info::Version::Semantic(major, _, _) => {
471                    if *major < 13 {
472                        return Err(Error::UnsupportedOS);
473                    }
474                }
475                _ => return Err(Error::UnsupportedOS),
476            };
477
478            (
479                self.app_name.as_ref().unwrap_or(&default_str),
480                self.app_path.as_ref().unwrap_or(&default_str),
481            )
482        } else {
483            (
484                self.app_name.as_ref().ok_or(Error::AppNameNotSpecified)?,
485                self.app_path.as_ref().ok_or(Error::AppPathNotSpecified)?,
486            )
487        };
488        let args = self.args.clone().unwrap_or_default();
489
490        #[cfg(target_os = "linux")]
491        return Ok(AutoLaunch::new(
492            app_name,
493            app_path,
494            self.linux_launch_mode,
495            &args,
496        ));
497        #[cfg(target_os = "macos")]
498        {
499            let bundle_identifiers = self.bundle_identifiers.clone().unwrap_or_default();
500            let agent_extra_config = self.agent_extra_config.as_ref().map_or("", |v| v);
501            return Ok(AutoLaunch::new(
502                app_name,
503                app_path,
504                self.macos_launch_mode,
505                &args,
506                &bundle_identifiers,
507                agent_extra_config,
508            ));
509        }
510        #[cfg(target_os = "windows")]
511        return Ok(AutoLaunch::new(
512            app_name,
513            app_path,
514            self.windows_enable_mode,
515            &args,
516        ));
517
518        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
519        return Err(Error::UnsupportedOS);
520    }
521}