desktop_link/
lib.rs

1#![forbid(clippy::style)]
2#![forbid(clippy::perf)]
3#![deny(clippy::pedantic)]
4#![forbid(double_negations)]
5#![forbid(clippy::allow_attributes_without_reason)]
6
7#[cfg(windows)] mod windows;
8#[cfg(unix)] mod xdg;
9
10use std::ffi::OsStr;
11use thiserror::Error;
12use std::path::{Path, PathBuf};
13use std::string::FromUtf16Error;
14
15#[derive(Debug, Error)]
16pub enum Error {
17    #[error("Unsupported platform")]
18    Unsupported,
19    #[error("IO: {0}")]
20    Io(#[from] std::io::Error),
21    #[error("UTF-16: {0}")]
22    Utf16(#[from] FromUtf16Error)
23}
24
25#[derive(Debug, Clone, Hash, Eq, PartialEq)]
26pub struct Link<'a>(Options<'a>);
27
28#[allow(unreachable_code, reason = "Fallback for uncovered platforms")]
29const fn get_platform() -> &'static dyn Platform {
30    #[cfg(windows)] return &windows::Windows;
31    #[cfg(unix)] if !cfg!(target_os = "macos") {
32        return &xdg::Xdg;
33    }
34
35    &Unsupported
36}
37
38/// Builder for link files
39impl<'a> Link<'a> {
40    /// Prepares a new link that when opened runs the given executable.
41    #[must_use]
42    pub fn targeting(target: &'a Path) -> Self {
43        let mut options = Options {
44            target: Path::new(""),
45            at: PathBuf::default(),
46            working_directory: None,
47            icon: None,
48            arguments: None,
49            name: None,
50        };
51        options.target = target;
52        Self(options)
53    }
54
55    /// Updates the display name for this link.
56    #[must_use]
57    pub fn set_name(&mut self, name: &'a str) -> &mut Self {
58        self.0.name = Some(name);
59        self
60    }
61
62    /// Updates the path to the icon to use for this link
63    #[must_use]
64    pub fn set_icon(&mut self, icon: &'a Path) -> &mut Self {
65        self.0.icon = Some(icon);
66        self
67    }
68
69    /// Updates the arguments to be passed to the program when started
70    pub fn set_arguments(&mut self, arguments: &'a str) -> &mut Self {
71        self.0.arguments = Some(arguments);
72        self
73    }
74
75    /// Updates the working directory for this link.
76    pub fn set_working_directory(&mut self, working_directory: &'a Path) -> &mut Self {
77        self.0.working_directory = Some(working_directory);
78        self
79    }
80
81    /// Saves the link at the specified location.
82    ///
83    /// If a file extension is necessary for the platform, it will be appended to the path.
84    ///
85    /// # Errors
86    /// The specific errors depend on the platform, but if there is an error, the link won't be saved.
87    pub fn save(&mut self, at: &Path) -> Result<(), Error> {
88        at.clone_into(&mut self.0.at);
89        get_platform().create(&mut self.0)
90    }
91
92    /// Makes the link available to the platform's conventional application menu.
93    ///
94    /// # Errors
95    /// The specific errors depend on the platform, but if there is an error, the link won't be saved.
96    pub fn save_to_menu(&mut self, name: &OsStr) -> Result<(), Error> {
97        self.save(&get_platform().get_menu_path(name)?)
98    }
99}
100
101#[derive(Debug, Clone, Hash, Eq, PartialEq)]
102struct Options<'a> {
103    name: Option<&'a str>,
104    target: &'a Path,
105    arguments: Option<&'a str>,
106    at: PathBuf,
107    icon: Option<&'a Path>,
108    working_directory: Option<&'a Path>,
109}
110
111trait Platform {
112    fn create(&self, options: &mut Options) -> Result<(), Error>;
113
114    fn get_menu_path(&self, name: &OsStr) -> Result<PathBuf, Error>;
115}
116
117struct Unsupported;
118
119impl Platform for Unsupported {
120    fn create(&self, _: &mut Options) -> Result<(), Error> {
121        Err(Error::Unsupported)
122    }
123
124    fn get_menu_path(&self, _: &OsStr) -> Result<PathBuf, Error> {
125        Err(Error::Unsupported)
126    }
127}