Skip to main content

alt_icons/
lib.rs

1//! A Windows executable that permanently changes its own icon.
2//!
3//! The model is borrowed from iOS alternate icons: a closed set of icons, declared
4//! at build time, switched by name at runtime. What is *not* borrowed is where the
5//! icon lives. On iOS and Android the thing that changes is the launcher entry; here
6//! it is the executable file itself, so the new icon is what Explorer shows for the
7//! `.exe`, and it survives a reboot.
8//!
9//! ```ignore
10//! alt_icons::include_icons!();
11//!
12//! fn main() -> Result<(), alt_icons::Error> {
13//!     alt_icons::init()?;
14//!     alt_icons::set_icon(AppIcon::Dark)?;
15//!     Ok(())
16//! }
17//! ```
18//!
19//! Declare the icon set in `build.rs` with `alt_icons::build::configure`. The `build` feature
20//! enables this helper when `alt-icons` is used as a build-dependency. A build script
21//! runs before macro expansion, so it cannot read a list declared by a macro, and
22//! the default icon has to be baked at build time or the binary you ship has no icon
23//! at all.
24//!
25//! # What this costs
26//!
27//! Every swap rewrites the executable, so its hash changes. Heuristic antivirus and
28//! SmartScreen reputation both notice a binary that rewrites itself. The default
29//! icon is baked at build time precisely so this only ever happens when your user
30//! asks for a different icon, never on first launch of a fresh download.
31//!
32//! Signing is incompatible: rewriting the PE invalidates an Authenticode signature.
33
34use std::fmt;
35use std::path::PathBuf;
36
37#[cfg(feature = "build")]
38pub mod build;
39
40#[cfg(windows)]
41mod ico;
42#[cfg(windows)]
43mod resources;
44#[cfg(windows)]
45mod swap;
46#[cfg(windows)]
47mod win;
48
49#[cfg(windows)]
50pub use swap::init_with_options;
51#[cfg(windows)]
52pub use swap::{current_icon, init, set_icon};
53
54/// Runtime behavior, configured once near the start of the application.
55#[derive(Clone, Copy, Debug)]
56pub struct Options {
57    /// Remove renamed executables automatically after their users exit, and clean
58    /// up leftovers on startup. Set to `false` to preserve `.old` files instead.
59    /// Abandoned `.new` staging files are cleaned up regardless of this option.
60    pub cleanup_old: bool,
61}
62
63impl Default for Options {
64    fn default() -> Self {
65        Self { cleanup_old: true }
66    }
67}
68
69/// One icon in the set. Implemented by the `AppIcon` enum that `build::configure`
70/// generates; there is no reason to implement it by hand.
71pub trait Icon {
72    /// The variant's name, stored in the binary so [`current_icon`] can report it.
73    fn name(&self) -> &'static str;
74    /// The raw `.ico` bytes, embedded at build time.
75    fn bytes(&self) -> &'static [u8];
76}
77
78/// Pulls in the generated `AppIcon` enum. Call once, at the root of your crate.
79#[macro_export]
80macro_rules! include_icons {
81    () => {
82        include!(concat!(env!("OUT_DIR"), "/alt_icons.rs"));
83    };
84}
85
86/// Everything that can go wrong during a swap.
87#[derive(Debug)]
88pub enum Error {
89    /// Another process is mid-swap on this same executable.
90    SwapInProgress,
91    /// The directory holding the executable cannot be written to.
92    ///
93    /// The crate deliberately stops here rather than copying itself somewhere
94    /// writable or prompting for elevation: a binary that clones itself around the
95    /// disk to change a cosmetic detail is exactly the behaviour that gets it
96    /// flagged, and a UAC prompt in the middle of an icon change is worse than the
97    /// icon not changing.
98    PathNotWritable(PathBuf),
99    /// The `.ico` could not be parsed. Nothing was written.
100    MalformedIcon(&'static str),
101    /// A Win32 call failed, named along with its `GetLastError` code.
102    Windows(&'static str, u32),
103    /// A filesystem operation failed.
104    Io(std::io::Error),
105    /// The crate only does anything on Windows.
106    UnsupportedPlatform,
107}
108
109impl fmt::Display for Error {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Error::SwapInProgress => {
113                write!(
114                    f,
115                    "another process is already changing this executable's icon"
116                )
117            }
118            Error::PathNotWritable(dir) => {
119                write!(f, "cannot write to {}", dir.display())
120            }
121            Error::MalformedIcon(why) => write!(f, "malformed icon: {why}"),
122            Error::Windows(call, code) => write!(f, "{call} failed with error {code}"),
123            Error::Io(err) => write!(f, "{err}"),
124            Error::UnsupportedPlatform => write!(f, "alt-icons only works on Windows"),
125        }
126    }
127}
128
129impl std::error::Error for Error {
130    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
131        match self {
132            Error::Io(err) => Some(err),
133            _ => None,
134        }
135    }
136}
137
138impl From<std::io::Error> for Error {
139    fn from(err: std::io::Error) -> Self {
140        Error::Io(err)
141    }
142}
143
144#[cfg(not(windows))]
145mod stubs {
146    use super::{Error, Icon, Options};
147
148    /// No-op away from Windows, so a cross-platform crate still builds.
149    pub fn init() -> Result<(), Error> {
150        Ok(())
151    }
152
153    /// No-op away from Windows, just like [`init`].
154    pub fn init_with_options(_options: Options) -> Result<(), Error> {
155        Ok(())
156    }
157
158    pub fn set_icon<I: Icon>(_icon: I) -> Result<(), Error> {
159        Err(Error::UnsupportedPlatform)
160    }
161
162    pub fn current_icon() -> Result<Option<String>, Error> {
163        Ok(None)
164    }
165}
166
167#[cfg(not(windows))]
168pub use stubs::{current_icon, init, init_with_options, set_icon};