alt-icons 1.2.3

A Windows executable that permanently changes its own icon, iOS-style: a closed set declared at build time, switched by name.
Documentation
//! A Windows executable that permanently changes its own icon.
//!
//! The model is borrowed from iOS alternate icons: a closed set of icons, declared
//! at build time, switched by name at runtime. What is *not* borrowed is where the
//! icon lives. On iOS and Android the thing that changes is the launcher entry; here
//! it is the executable file itself, so the new icon is what Explorer shows for the
//! `.exe`, and it survives a reboot.
//!
//! ```ignore
//! alt_icons::include_icons!();
//!
//! fn main() -> Result<(), alt_icons::Error> {
//!     alt_icons::init()?;
//!     alt_icons::set_icon(AppIcon::Dark)?;
//!     Ok(())
//! }
//! ```
//!
//! Declare the icon set in `build.rs` with `alt_icons::build::configure`. The `build` feature
//! enables this helper when `alt-icons` is used as a build-dependency. A build script
//! runs before macro expansion, so it cannot read a list declared by a macro, and
//! the default icon has to be baked at build time or the binary you ship has no icon
//! at all.
//!
//! # What this costs
//!
//! Every swap rewrites the executable, so its hash changes. Heuristic antivirus and
//! SmartScreen reputation both notice a binary that rewrites itself. The default
//! icon is baked at build time precisely so this only ever happens when your user
//! asks for a different icon, never on first launch of a fresh download.
//!
//! Signing is incompatible: rewriting the PE invalidates an Authenticode signature.

use std::fmt;
use std::path::PathBuf;

#[cfg(feature = "build")]
pub mod build;

#[cfg(windows)]
mod ico;
#[cfg(windows)]
mod resources;
#[cfg(windows)]
mod swap;
#[cfg(windows)]
mod win;

#[cfg(windows)]
pub use swap::init_with_options;
#[cfg(windows)]
pub use swap::{current_icon, init, set_icon};

/// Runtime behavior, configured once near the start of the application.
#[derive(Clone, Copy, Debug)]
pub struct Options {
    /// Remove renamed executables automatically after their users exit, and clean
    /// up leftovers on startup. Set to `false` to preserve `.old` files instead.
    /// Abandoned `.new` staging files are cleaned up regardless of this option.
    pub cleanup_old: bool,
}

impl Default for Options {
    fn default() -> Self {
        Self { cleanup_old: true }
    }
}

/// One icon in the set. Implemented by the `AppIcon` enum that `build::configure`
/// generates; there is no reason to implement it by hand.
pub trait Icon {
    /// The variant's name, stored in the binary so [`current_icon`] can report it.
    fn name(&self) -> &'static str;
    /// The raw `.ico` bytes, embedded at build time.
    fn bytes(&self) -> &'static [u8];
}

/// Pulls in the generated `AppIcon` enum. Call once, at the root of your crate.
#[macro_export]
macro_rules! include_icons {
    () => {
        include!(concat!(env!("OUT_DIR"), "/alt_icons.rs"));
    };
}

/// Everything that can go wrong during a swap.
#[derive(Debug)]
pub enum Error {
    /// Another process is mid-swap on this same executable.
    SwapInProgress,
    /// The directory holding the executable cannot be written to.
    ///
    /// The crate deliberately stops here rather than copying itself somewhere
    /// writable or prompting for elevation: a binary that clones itself around the
    /// disk to change a cosmetic detail is exactly the behaviour that gets it
    /// flagged, and a UAC prompt in the middle of an icon change is worse than the
    /// icon not changing.
    PathNotWritable(PathBuf),
    /// The `.ico` could not be parsed. Nothing was written.
    MalformedIcon(&'static str),
    /// A Win32 call failed, named along with its `GetLastError` code.
    Windows(&'static str, u32),
    /// A filesystem operation failed.
    Io(std::io::Error),
    /// The crate only does anything on Windows.
    UnsupportedPlatform,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::SwapInProgress => {
                write!(
                    f,
                    "another process is already changing this executable's icon"
                )
            }
            Error::PathNotWritable(dir) => {
                write!(f, "cannot write to {}", dir.display())
            }
            Error::MalformedIcon(why) => write!(f, "malformed icon: {why}"),
            Error::Windows(call, code) => write!(f, "{call} failed with error {code}"),
            Error::Io(err) => write!(f, "{err}"),
            Error::UnsupportedPlatform => write!(f, "alt-icons only works on Windows"),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Io(err) => Some(err),
            _ => None,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        Error::Io(err)
    }
}

#[cfg(not(windows))]
mod stubs {
    use super::{Error, Icon, Options};

    /// No-op away from Windows, so a cross-platform crate still builds.
    pub fn init() -> Result<(), Error> {
        Ok(())
    }

    /// No-op away from Windows, just like [`init`].
    pub fn init_with_options(_options: Options) -> Result<(), Error> {
        Ok(())
    }

    pub fn set_icon<I: Icon>(_icon: I) -> Result<(), Error> {
        Err(Error::UnsupportedPlatform)
    }

    pub fn current_icon() -> Result<Option<String>, Error> {
        Ok(None)
    }
}

#[cfg(not(windows))]
pub use stubs::{current_icon, init, init_with_options, set_icon};