use crate::platform::window_icon::{
IconDegradedReason, IconError, IconScope, IconSource, IconSupport, IconUnsupportedReason,
StockIcon,
};
use std::os::windows::ffi::OsStrExt as _;
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, LPARAM, TRUE};
use winapi::shared::windef::{HICON, HWND};
use winapi::um::wincon::GetConsoleWindow;
use winapi::um::winuser::{
CreateIconFromResourceEx, EnumWindows, GetClassNameW, GetWindowThreadProcessId, LoadIconW,
LoadImageW, SendMessageW, IDI_APPLICATION, IDI_ERROR, IDI_INFORMATION, IDI_SHIELD,
IDI_WARNING, IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON,
};
const ICON_SMALL: usize = 0;
const ICON_BIG: usize = 1;
const CONHOST_CLASS: &str = "ConsoleWindowClass";
fn console_window() -> Option<HWND> {
let hwnd = unsafe { GetConsoleWindow() };
(!hwnd.is_null()).then_some(hwnd)
}
fn class_name(hwnd: HWND) -> String {
let mut buffer = [0u16; 256];
let len = unsafe { GetClassNameW(hwnd, buffer.as_mut_ptr(), buffer.len() as i32) };
if len <= 0 {
return String::new();
}
String::from_utf16_lossy(&buffer[..len as usize])
}
fn window_for(scope: IconScope) -> Option<HWND> {
match scope {
IconScope::Host => console_window(),
IconScope::Child { pid } => console_window_of_pid(pid),
}
}
fn console_window_of_pid(pid: u32) -> Option<HWND> {
struct Search {
pid: u32,
found: HWND,
}
unsafe extern "system" fn visit(hwnd: HWND, lparam: LPARAM) -> BOOL {
let search = &mut *(lparam as *mut Search);
let mut owner: DWORD = 0;
GetWindowThreadProcessId(hwnd, &mut owner);
if owner == search.pid && class_name(hwnd) == CONHOST_CLASS {
search.found = hwnd;
return FALSE; }
TRUE
}
let mut search = Search {
pid,
found: std::ptr::null_mut(),
};
unsafe { EnumWindows(Some(visit), &mut search as *mut Search as LPARAM) };
(!search.found.is_null()).then_some(search.found)
}
pub fn icon_support(scope: IconScope) -> IconSupport {
if let IconScope::Child { pid } = scope {
return match console_window_of_pid(pid) {
Some(_) => IconSupport::Available,
None => IconSupport::Unsupported(IconUnsupportedReason::ChildHasNoConsole),
};
}
if std::env::var_os("WT_SESSION").is_some() {
return IconSupport::Degraded(IconDegradedReason::WindowsTerminal);
}
let Some(hwnd) = console_window() else {
return IconSupport::Unsupported(IconUnsupportedReason::NoConsole);
};
if class_name(hwnd) == CONHOST_CLASS {
return IconSupport::Available;
}
IconSupport::Degraded(IconDegradedReason::NonClassicWindowsHost)
}
fn load_from_path(path: &std::path::Path) -> Result<HICON, IconError> {
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
wide.push(0);
let icon = unsafe {
LoadImageW(
std::ptr::null_mut(),
wide.as_ptr(),
IMAGE_ICON,
0,
0,
LR_LOADFROMFILE | LR_DEFAULTSIZE,
)
} as HICON;
if icon.is_null() {
return Err(IconError::Load {
path: path.to_path_buf(),
source: std::io::Error::last_os_error(),
});
}
Ok(icon)
}
fn load_from_bytes(bytes: &[u8]) -> Result<HICON, IconError> {
let span = crate::platform::window_icon::ico::best_image(bytes).map_err(IconError::Decode)?;
let image = &bytes[span.offset..span.offset + span.len];
const ICON_RESOURCE_VERSION: DWORD = 0x0003_0000;
let icon = unsafe {
CreateIconFromResourceEx(
image.as_ptr() as *mut u8,
image.len() as DWORD,
TRUE,
ICON_RESOURCE_VERSION,
0,
0,
LR_DEFAULTSIZE,
)
};
if icon.is_null() {
return Err(IconError::Apply(std::io::Error::last_os_error()));
}
Ok(icon)
}
pub fn load_stock(stock: StockIcon) -> Result<HICON, IconError> {
let name = match stock {
StockIcon::Application => IDI_APPLICATION,
StockIcon::Warning => IDI_WARNING,
StockIcon::Error => IDI_ERROR,
StockIcon::Information => IDI_INFORMATION,
StockIcon::Shield => IDI_SHIELD,
};
let icon = unsafe { LoadIconW(std::ptr::null_mut(), name) };
if icon.is_null() {
return Err(IconError::Apply(std::io::Error::last_os_error()));
}
Ok(icon)
}
pub fn set_icon(scope: IconScope, source: &IconSource) -> Result<(), IconError> {
let hwnd = window_for(scope)
.ok_or(IconError::Unsupported(IconUnsupportedReason::TargetDisappeared))?;
let icon = match source {
IconSource::Path(path) => load_from_path(path)?,
IconSource::Bytes(bytes) => load_from_bytes(bytes)?,
IconSource::Stock(stock) => load_stock(*stock)?,
};
unsafe {
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, icon as isize);
SendMessageW(hwnd, WM_SETICON, ICON_BIG, icon as isize);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn windows_terminal_is_detected_by_env_and_names_its_remedy() {
let previous = std::env::var_os("WT_SESSION");
unsafe { std::env::set_var("WT_SESSION", "test-session") };
let support = icon_support(IconScope::Host);
match previous {
Some(value) => unsafe { std::env::set_var("WT_SESSION", value) },
None => unsafe { std::env::remove_var("WT_SESSION") },
}
match support {
IconSupport::Degraded(IconDegradedReason::WindowsTerminal) => {}
other => panic!("WT_SESSION must yield Degraded, got {other:?}"),
}
}
#[test]
fn the_os_supplies_every_stock_icon() {
for stock in [
StockIcon::Application,
StockIcon::Warning,
StockIcon::Error,
StockIcon::Information,
StockIcon::Shield,
] {
let icon = load_stock(stock)
.unwrap_or_else(|error| panic!("the OS declined {stock:?}: {error}"));
assert!(!icon.is_null(), "{stock:?} produced a null icon");
}
}
#[test]
fn a_childless_pid_reason_names_the_console_window() {
let support = icon_support(IconScope::Child { pid: 0 });
assert_eq!(
support,
IconSupport::Unsupported(IconUnsupportedReason::ChildHasNoConsole)
);
}
}