use std::path::Path;
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as _, PropMode, Window};
use x11rb::wrapper::ConnectionExt as _;
use crate::platform::window_icon::{
IconDegradedReason, IconError, IconScope, IconSource, IconSupport, IconUnsupportedReason,
};
#[cfg(test)]
#[path = "window_icon/tests_support.rs"]
mod tests_support;
#[derive(Debug)]
pub(super) struct Rgba {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
const MAX_SIDE: u32 = 512;
pub(super) fn window_id() -> Option<Window> {
parse_window_id(&std::env::var("WINDOWID").ok()?)
}
fn parse_window_id(raw: &str) -> Option<Window> {
let trimmed = raw.trim();
let parsed = trimmed
.strip_prefix("0x")
.and_then(|hex| u32::from_str_radix(hex, 16).ok())
.or_else(|| trimmed.parse::<u32>().ok())?;
(parsed != 0).then_some(parsed)
}
pub fn icon_support(scope: IconScope) -> IconSupport {
if matches!(scope, IconScope::Child { .. }) {
return IconSupport::Unsupported(IconUnsupportedReason::LinuxChildScope);
}
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
return IconSupport::Unsupported(IconUnsupportedReason::Wayland);
}
if std::env::var_os("DISPLAY").is_none() {
return IconSupport::Unsupported(IconUnsupportedReason::LinuxNoDisplay);
}
if window_id().is_none() {
return IconSupport::Degraded(IconDegradedReason::LinuxNameOnly);
}
IconSupport::Available
}
pub fn set_icon(scope: IconScope, source: &IconSource) -> Result<(), IconError> {
if let IconSupport::Unsupported(reason) = icon_support(scope) {
return Err(IconError::Unsupported(reason));
}
let window = window_id().ok_or(IconError::Unsupported(
IconUnsupportedReason::TargetDisappeared,
))?;
let image = decode(source)?;
write_property(window, &image)
}
fn decode(source: &IconSource) -> Result<Rgba, IconError> {
match source {
IconSource::Path(path) => {
let bytes = std::fs::read(path).map_err(|source| IconError::Load {
path: path.clone(),
source,
})?;
decode_bytes(&bytes, Some(path))
}
IconSource::Bytes(bytes) => decode_bytes(bytes, None),
IconSource::Stock(_) => Err(IconError::Unsupported(
IconUnsupportedReason::StockNeedsPixels,
)),
}
}
const PNG_MAGIC: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
fn decode_bytes(bytes: &[u8], path: Option<&Path>) -> Result<Rgba, IconError> {
if bytes.len() >= PNG_MAGIC.len() && bytes[..PNG_MAGIC.len()] == PNG_MAGIC {
return decode_png(bytes);
}
if let Ok(span) = crate::platform::window_icon::ico::best_image(bytes) {
let inner = &bytes[span.offset..span.offset + span.len];
if inner.len() >= PNG_MAGIC.len() && inner[..PNG_MAGIC.len()] == PNG_MAGIC {
return decode_png(inner);
}
return Err(IconError::Unsupported(
IconUnsupportedReason::UnknownImageFormat,
));
}
let _ = path;
Err(IconError::Unsupported(
IconUnsupportedReason::UnknownImageFormat,
))
}
fn decode_png(bytes: &[u8]) -> Result<Rgba, IconError> {
let decoder = png::Decoder::new(bytes);
let mut reader = decoder
.read_info()
.map_err(|e| IconError::Apply(std::io::Error::other(e.to_string())))?;
let mut buffer = vec![0; reader.output_buffer_size()];
let info = reader
.next_frame(&mut buffer)
.map_err(|e| IconError::Apply(std::io::Error::other(e.to_string())))?;
if info.width > MAX_SIDE || info.height > MAX_SIDE {
return Err(IconError::Unsupported(
IconUnsupportedReason::OversizedIcon,
));
}
let pixels = match info.color_type {
png::ColorType::Rgba => buffer[..info.buffer_size()].to_vec(),
png::ColorType::Rgb => buffer[..info.buffer_size()]
.chunks_exact(3)
.flat_map(|p| [p[0], p[1], p[2], 0xff])
.collect(),
other => {
let _ = other;
return Err(IconError::Unsupported(
IconUnsupportedReason::UnsupportedPngColorType,
));
}
};
Ok(Rgba {
width: info.width,
height: info.height,
pixels,
})
}
pub(super) fn to_cardinals(image: &Rgba) -> Vec<u32> {
let mut data = Vec::with_capacity(2 + (image.width * image.height) as usize);
data.push(image.width);
data.push(image.height);
for pixel in image.pixels.chunks_exact(4) {
data.push(
(u32::from(pixel[3]) << 24)
| (u32::from(pixel[0]) << 16)
| (u32::from(pixel[1]) << 8)
| u32::from(pixel[2]),
);
}
data
}
fn write_property(window: Window, image: &Rgba) -> Result<(), IconError> {
let (connection, _screen) =
x11rb::connect(None).map_err(|e| IconError::Apply(std::io::Error::other(e.to_string())))?;
let cookie = connection
.intern_atom(false, b"_NET_WM_ICON")
.map_err(|e| IconError::Apply(std::io::Error::other(e.to_string())))?;
let atom = cookie
.reply()
.map_err(|e| IconError::Apply(std::io::Error::other(e.to_string())))?
.atom;
let data = to_cardinals(image);
connection
.change_property32(PropMode::REPLACE, window, atom, AtomEnum::CARDINAL, &data)
.map_err(|e: x11rb::errors::ConnectionError| {
IconError::Apply(std::io::Error::other(e.to_string()))
})?
.check()
.map_err(|e: x11rb::errors::ReplyError| {
IconError::Apply(std::io::Error::other(e.to_string()))
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cardinals_lead_with_dimensions_then_argb() {
let image = Rgba {
width: 2,
height: 1,
pixels: vec![0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x80],
};
assert_eq!(to_cardinals(&image), vec![2, 1, 0xffff_0000, 0x8000_00ff]);
}
#[test]
fn a_windowid_is_read_as_decimal_or_hex() {
assert_eq!(parse_window_id("12345"), Some(12345));
assert_eq!(parse_window_id("0x3039"), Some(0x3039));
assert_eq!(parse_window_id(" 42 "), Some(42));
assert_eq!(parse_window_id("not a window"), None);
}
#[test]
fn a_zero_windowid_is_rejected() {
assert_eq!(parse_window_id("0"), None);
assert_eq!(parse_window_id("0x0"), None);
}
#[test]
fn a_child_scope_is_refused_rather_than_silently_hitting_our_own_window() {
let support = icon_support(IconScope::Child { pid: 4242 });
match support {
IconSupport::Unsupported(IconUnsupportedReason::LinuxChildScope) => {}
other => panic!("a child's window is not identifiable on X11, got {other:?}"),
}
}
#[test]
fn an_rgb_png_gains_full_alpha_rather_than_being_refused() {
let png = super::tests_support::rgb_png(1, 1, [0x10, 0x20, 0x30]);
let image = decode_png(&png).expect("an RGB PNG is an ordinary icon");
assert_eq!(image.pixels, vec![0x10, 0x20, 0x30, 0xff]);
}
#[test]
fn a_non_image_is_refused_with_a_reason_naming_the_accepted_formats() {
let error = decode_bytes(b"not an image at all", None)
.expect_err("arbitrary bytes are not an icon");
match error {
IconError::Unsupported(IconUnsupportedReason::UnknownImageFormat) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn an_oversized_png_is_refused_before_it_reaches_the_socket() {
let png = super::tests_support::rgb_png(MAX_SIDE + 1, 1, [0, 0, 0]);
let error = decode_png(&png).expect_err("an oversized icon must be refused");
assert!(matches!(
error,
IconError::Unsupported(IconUnsupportedReason::OversizedIcon)
));
}
#[test]
fn a_stock_icon_is_refused_because_x11_needs_pixels() {
let error = decode(&IconSource::Stock(crate::platform::window_icon::StockIcon::Shield))
.expect_err("a theme name is not an image");
match error {
IconError::Unsupported(IconUnsupportedReason::StockNeedsPixels) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
}