desktop-link 0.1.0

Create shortcut or link files
Documentation
#![forbid(clippy::style)]
#![forbid(clippy::perf)]
#![deny(clippy::pedantic)]
#![forbid(double_negations)]
#![forbid(clippy::allow_attributes_without_reason)]

#[cfg(windows)] mod windows;
#[cfg(unix)] mod xdg;

use std::ffi::OsStr;
use thiserror::Error;
use std::path::{Path, PathBuf};
use std::string::FromUtf16Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Unsupported platform")]
    Unsupported,
    #[error("IO: {0}")]
    Io(#[from] std::io::Error),
    #[error("UTF-16: {0}")]
    Utf16(#[from] FromUtf16Error)
}

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct Link<'a>(Options<'a>);

#[allow(unreachable_code, reason = "Fallback for uncovered platforms")]
const fn get_platform() -> &'static dyn Platform {
    #[cfg(windows)] return &windows::Windows;
    #[cfg(unix)] if !cfg!(target_os = "macos") {
        return &xdg::Xdg;
    }

    &Unsupported
}

/// Builder for link files
impl<'a> Link<'a> {
    /// Prepares a new link that when opened runs the given executable.
    #[must_use]
    pub fn targeting(target: &'a Path) -> Self {
        let mut options = Options {
            target: Path::new(""),
            at: PathBuf::default(),
            working_directory: None,
            icon: None,
            arguments: None,
            name: None,
        };
        options.target = target;
        Self(options)
    }

    /// Updates the display name for this link.
    #[must_use]
    pub fn set_name(&mut self, name: &'a str) -> &mut Self {
        self.0.name = Some(name);
        self
    }

    /// Updates the path to the icon to use for this link
    #[must_use]
    pub fn set_icon(&mut self, icon: &'a Path) -> &mut Self {
        self.0.icon = Some(icon);
        self
    }

    /// Updates the arguments to be passed to the program when started
    pub fn set_arguments(&mut self, arguments: &'a str) -> &mut Self {
        self.0.arguments = Some(arguments);
        self
    }

    /// Updates the working directory for this link.
    pub fn set_working_directory(&mut self, working_directory: &'a Path) -> &mut Self {
        self.0.working_directory = Some(working_directory);
        self
    }

    /// Saves the link at the specified location.
    ///
    /// If a file extension is necessary for the platform, it will be appended to the path.
    ///
    /// # Errors
    /// The specific errors depend on the platform, but if there is an error, the link won't be saved.
    pub fn save(&mut self, at: &Path) -> Result<(), Error> {
        at.clone_into(&mut self.0.at);
        get_platform().create(&mut self.0)
    }

    /// Makes the link available to the platform's conventional application menu.
    ///
    /// # Errors
    /// The specific errors depend on the platform, but if there is an error, the link won't be saved.
    pub fn save_to_menu(&mut self, name: &OsStr) -> Result<(), Error> {
        self.save(&get_platform().get_menu_path(name)?)
    }
}

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
struct Options<'a> {
    name: Option<&'a str>,
    target: &'a Path,
    arguments: Option<&'a str>,
    at: PathBuf,
    icon: Option<&'a Path>,
    working_directory: Option<&'a Path>,
}

trait Platform {
    fn create(&self, options: &mut Options) -> Result<(), Error>;

    fn get_menu_path(&self, name: &OsStr) -> Result<PathBuf, Error>;
}

struct Unsupported;

impl Platform for Unsupported {
    fn create(&self, _: &mut Options) -> Result<(), Error> {
        Err(Error::Unsupported)
    }

    fn get_menu_path(&self, _: &OsStr) -> Result<PathBuf, Error> {
        Err(Error::Unsupported)
    }
}