#[cfg(target_os = "linux")]
use std::path::Path;
use std::sync::{LazyLock, OnceLock};
const ARTWORK_PNG: &[u8] = include_bytes!("../assets/mb-icon-256.png");
#[cfg(target_os = "linux")]
pub const LINUX_APP_ID: &str = "mahbot";
struct Artwork {
rgba: Vec<u8>,
width: u32,
height: u32,
}
fn artwork() -> &'static Artwork {
static ARTWORK: LazyLock<Artwork> = LazyLock::new(|| {
let decode = image::load_from_memory_with_format(ARTWORK_PNG, image::ImageFormat::Png)
.expect("the embedded product icon is a PNG");
let rgba = decode.into_rgba8();
Artwork {
width: rgba.width(),
height: rgba.height(),
rgba: rgba.into_raw(),
}
});
&ARTWORK
}
#[must_use]
pub fn window_icon() -> Option<iced::window::Icon> {
let artwork = artwork();
iced::window::icon::from_rgba(artwork.rgba.clone(), artwork.width, artwork.height)
.map_err(|e| not_placed("the window icon", &e.to_string()))
.ok()
}
#[must_use]
pub(crate) fn widget_image() -> iced::widget::image::Handle {
static HANDLE: OnceLock<iced::widget::image::Handle> = OnceLock::new();
HANDLE
.get_or_init(|| {
let artwork = artwork();
iced::widget::image::Handle::from_rgba(
artwork.width,
artwork.height,
artwork.rgba.clone(),
)
})
.clone()
}
pub fn install_desktop_integration() {
#[cfg(target_os = "macos")]
set_dock_icon();
#[cfg(target_os = "linux")]
install_launcher_entry();
}
fn not_placed(surface: &str, reason: &str) {
crate::boot::boot_diagnostic(format!("{surface} was not placed: {reason}"));
}
#[cfg(target_os = "macos")]
#[expect(clippy::cast_possible_wrap)]
fn set_dock_icon() {
use objc2::{AnyThread, MainThreadMarker};
use objc2_app_kit::{NSApplication, NSBitmapImageRep, NSDeviceRGBColorSpace, NSImage};
use objc2_foundation::NSSize;
let Some(marker) = MainThreadMarker::new() else {
not_placed("the Dock icon", "it can only be set from the main thread");
return;
};
let artwork = artwork();
let (width, height) = (artwork.width as isize, artwork.height as isize);
let bitmap = unsafe {
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
std::ptr::null_mut(),
width,
height,
8,
4,
true,
false,
NSDeviceRGBColorSpace,
width * 4,
32,
)
};
let Some(bitmap) = bitmap else {
not_placed("the Dock icon", "its bitmap could not be allocated");
return;
};
unsafe {
std::ptr::copy_nonoverlapping(
artwork.rgba.as_ptr(),
bitmap.bitmapData(),
artwork.rgba.len(),
);
}
let icon = NSImage::initWithSize(
NSImage::alloc(),
NSSize::new(f64::from(artwork.width), f64::from(artwork.height)),
);
icon.addRepresentation(&bitmap);
unsafe {
NSApplication::sharedApplication(marker).setApplicationIconImage(Some(&icon));
}
}
#[cfg(target_os = "linux")]
fn install_launcher_entry() {
let Some(data_home) = directories::BaseDirs::new().map(|base| base.data_dir().to_path_buf())
else {
not_placed("the launcher entry", "this account has no data directory");
return;
};
let Ok(executable) = std::env::current_exe() else {
not_placed("the launcher entry", "the running file has no path");
return;
};
let entry = data_home
.join("applications")
.join(format!("{LINUX_APP_ID}.desktop"));
let content = launcher_entry_text(&executable);
if let Err(e) = write_if_changed(&entry, content.as_bytes()) {
not_placed(
&format!("the launcher entry at {}", entry.display()),
&e.to_string(),
);
}
for size in [256, 48] {
let Some(png) = scaled_png(artwork(), size) else {
continue;
};
let icon = data_home
.join("icons/hicolor")
.join(format!("{size}x{size}"))
.join("apps")
.join(format!("{LINUX_APP_ID}.png"));
if let Err(e) = write_if_changed(&icon, &png) {
not_placed(
&format!("the launcher's icon at {}", icon.display()),
&e.to_string(),
);
}
}
}
#[cfg(target_os = "linux")]
fn launcher_entry_text(executable: &Path) -> String {
format!(
"[Desktop Entry]\n\
Type=Application\n\
Name=MahBot\n\
Comment={description}\n\
Exec={exec}\n\
Icon={LINUX_APP_ID}\n\
Terminal=false\n\
Categories=Development;\n\
StartupWMClass={LINUX_APP_ID}\n",
description = env!("CARGO_PKG_DESCRIPTION"),
exec = exec_argument(executable),
)
}
#[cfg(target_os = "linux")]
fn exec_argument(executable: &Path) -> String {
let path = executable.to_string_lossy();
let mut argument = String::with_capacity(path.len() + 2);
argument.push('"');
for character in path.chars() {
match character {
'"' | '\\' | '$' | '`' => {
argument.push('\\');
argument.push(character);
}
'%' => argument.push_str("%%"),
_ => argument.push(character),
}
}
argument.push('"');
argument
}
#[cfg(target_os = "linux")]
fn scaled_png(artwork: &Artwork, size: u32) -> Option<Vec<u8>> {
let source = image::RgbaImage::from_raw(artwork.width, artwork.height, artwork.rgba.clone())
.expect("the artwork's pixels match its own dimensions");
let scaled =
image::imageops::resize(&source, size, size, image::imageops::FilterType::Lanczos3);
let mut png = Vec::new();
let encoded = image::DynamicImage::ImageRgba8(scaled)
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png);
match encoded {
Ok(()) => Some(png),
Err(e) => {
not_placed(
"the launcher's icon",
&format!("{size}x{size} could not be encoded: {e}"),
);
None
}
}
}
#[cfg(target_os = "linux")]
fn write_if_changed(path: &Path, content: &[u8]) -> std::io::Result<()> {
if std::fs::read(path).is_ok_and(|current| current == content) {
return Ok(());
}
let Some(parent) = path.parent() else {
return Err(std::io::Error::other("the target has no directory"));
};
std::fs::create_dir_all(parent)?;
let staged = path.with_extension("new");
std::fs::write(&staged, content)?;
if let Err(e) = std::fs::rename(&staged, path) {
let _ = std::fs::remove_file(&staged);
return Err(e);
}
Ok(())
}
#[cfg(target_os = "windows")]
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub(crate) fn apply_window_icons(raw_handle: u64) {
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::WindowsAndMessaging::{
GCLP_HICON, GCLP_HICONSM, GetSystemMetrics, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_SHARED,
LoadImageW, SM_CXICON, SM_CXSMICON, SM_CYICON, SM_CYSMICON, SendMessageW, SetClassLongPtrW,
WM_SETICON,
};
const ICON_RESOURCE_ID: usize = 1;
let window = raw_handle as isize;
unsafe {
let instance = GetModuleHandleW(std::ptr::null());
let resource = ICON_RESOURCE_ID as *const u16;
let big = LoadImageW(
instance,
resource,
IMAGE_ICON,
GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON),
LR_SHARED,
);
let small = LoadImageW(
instance,
resource,
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_SHARED,
);
for (icon, kind, class_index, surface) in [
(big, ICON_BIG, GCLP_HICON, "the taskbar icon"),
(small, ICON_SMALL, GCLP_HICONSM, "the title-bar icon"),
] {
if icon == 0 {
not_placed(surface, &std::io::Error::last_os_error().to_string());
continue;
}
SendMessageW(window, WM_SETICON, kind as usize, icon);
SetClassLongPtrW(window, class_index, icon);
}
}
}