Download
143 lines 4.6 KiBRustModified 2026-08-01 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
//! Fetches window icons (_NET_WM_ICON) directly over X11 and converts them
//! to PNG data URIs for the frontend. Icons are cached per window class so
//! each app is only fetched and encoded once.

use base64::Engine;
use std::collections::HashMap;
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt};
use x11rb::rust_connection::RustConnection;

/// Preferred icon size: frames closest to this edge length win.
const TARGET_SIZE: i64 = 64;

pub struct IconFetcher {
    conn: Option<(RustConnection, u32)>, // connection + _NET_WM_ICON atom
    cache: HashMap<String, Option<String>>,
}

impl IconFetcher {
    pub fn new() -> Self {
        let conn = x11rb::connect(None).ok().and_then(|(conn, _)| {
            let atom = conn
                .intern_atom(false, b"_NET_WM_ICON")
                .ok()?
                .reply()
                .ok()?
                .atom;
            Some((conn, atom))
        });
        if conn.is_none() {
            eprintln!("divebar: no X connection; window icons disabled");
        }
        Self {
            conn,
            cache: HashMap::new(),
        }
    }

    pub fn icon_for(&mut self, window: u32, cache_key: &str) -> Option<String> {
        if let Some(cached) = self.cache.get(cache_key) {
            return cached.clone();
        }
        let icon = self.fetch(window);
        self.cache.insert(cache_key.to_string(), icon.clone());
        icon
    }

    fn fetch(&self, window: u32) -> Option<String> {
        let (conn, atom) = self.conn.as_ref()?;
        let reply = conn
            .get_property(false, window, *atom, AtomEnum::CARDINAL, 0, u32::MAX)
            .ok()?
            .reply()
            .ok()?;
        let data: Vec<u32> = reply.value32()?.collect();
        let (width, height, pixels) = best_frame(&data)?;

        // _NET_WM_ICON pixels are ARGB; PNG wants RGBA.
        let mut rgba = Vec::with_capacity(pixels.len() * 4);
        for &px in pixels {
            rgba.extend_from_slice(&[(px >> 16) as u8, (px >> 8) as u8, px as u8, (px >> 24) as u8]);
        }
        encode_png_data_uri(width, height, &rgba)
    }
}

/// Encodes raw RGBA pixels as a PNG data URI.
pub fn encode_png_data_uri(width: u32, height: u32, rgba: &[u8]) -> Option<String> {
    let mut png_data = Vec::new();
    {
        let mut encoder = png::Encoder::new(&mut png_data, width, height);
        encoder.set_color(png::ColorType::Rgba);
        encoder.set_depth(png::BitDepth::Eight);
        let mut writer = encoder.write_header().ok()?;
        writer.write_image_data(rgba).ok()?;
    }
    Some(format!(
        "data:image/png;base64,{}",
        base64::engine::general_purpose::STANDARD.encode(&png_data)
    ))
}

/// _NET_WM_ICON holds a sequence of (width, height, w*h pixels) frames;
/// picks the one closest to TARGET_SIZE.
fn best_frame(data: &[u32]) -> Option<(u32, u32, &[u32])> {
    let mut best: Option<(u32, u32, &[u32])> = None;
    let mut best_score = i64::MAX;
    let mut i = 0usize;
    while i + 2 <= data.len() {
        let (w, h) = (data[i] as usize, data[i + 1] as usize);
        if w == 0 || h == 0 || w > 1024 || h > 1024 || i + 2 + w * h > data.len() {
            break;
        }
        let frame = &data[i + 2..i + 2 + w * h];
        let score = (w.max(h) as i64 - TARGET_SIZE).abs();
        if score < best_score {
            best_score = score;
            best = Some((w as u32, h as u32, frame));
        }
        i += 2 + w * h;
    }
    best
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Builds a (width, height, pixels…) frame; pixel values are arbitrary.
    fn frame(w: u32, h: u32) -> Vec<u32> {
        let mut v = vec![w, h];
        v.extend(std::iter::repeat_n(0xffcc_0000, (w * h) as usize));
        v
    }

    #[test]
    fn picks_frame_closest_to_target_size() {
        let mut data = frame(16, 16);
        data.extend(frame(64, 64));
        data.extend(frame(512, 512));
        let (w, h, pixels) = best_frame(&data).unwrap();
        assert_eq!((w, h), (64, 64));
        assert_eq!(pixels.len(), 64 * 64);
    }

    #[test]
    fn single_frame_wins_regardless_of_size() {
        let data = frame(16, 16);
        assert_eq!(best_frame(&data).map(|(w, h, _)| (w, h)), Some((16, 16)));
    }

    #[test]
    fn truncated_frame_is_ignored() {
        let mut data = frame(16, 16);
        data.extend([64u32, 64]); // header claims 64x64 but pixels are missing
        assert_eq!(best_frame(&data).map(|(w, h, _)| (w, h)), Some((16, 16)));
    }

    #[test]
    fn empty_or_zero_sized_is_none() {
        assert_eq!(best_frame(&[]).map(|(w, h, _)| (w, h)), None);
        assert_eq!(best_frame(&[0, 0]).map(|(w, h, _)| (w, h)), None);
    }
}