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};