From ed74d5de2662e8c7bb7fd0cb22de1ab20adf23cd Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Thu, 29 Jan 2026 23:20:52 +0800 Subject: [PATCH] windows support for active app icon retrieval --- src-tauri/Cargo.lock | 22 +++- src-tauri/Cargo.toml | 11 +- src-tauri/src/services/active_app.rs | 187 ++++++++++++++++++++++++++- 3 files changed, 206 insertions(+), 14 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b854461..77666d9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1866,7 +1866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -1989,6 +1989,7 @@ dependencies = [ "gif", "moxcms", "num-traits", + "png 0.18.0", ] [[package]] @@ -2412,7 +2413,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", @@ -3142,6 +3143,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -4403,7 +4417,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -5080,7 +5094,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5475021..b6f44f6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,19 +4,15 @@ version = "0.1.0" description = "A Tauri App" authors = ["you"] edition = "2021" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] # The `_lib` suffix may seem redundant but it is necessary # to make the lib name unique and wouldn't conflict with the bin name. # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 name = "friendolls_desktop_lib" crate-type = ["staticlib", "cdylib", "rlib"] - [build-dependencies] tauri-build = { version = "2", features = [] } - [dependencies] tauri = { version = "2", features = ["macos-private-api", "unstable", "tray-icon"] } tauri-plugin-opener = "2" @@ -44,16 +40,14 @@ flate2 = "1.0.28" rust_socketio = "0.6.0" tracing-appender = "0.2.4" tauri-plugin-dialog = "2.4.2" -image = {version = "0.25.9", default-features = false, features = ["gif"] } +image = {version = "0.25.9", default-features = false, features = ["gif", "png"] } gif = "0.14.1" raw-window-handle = "0.6" enigo = { version = "0.6.1", features = ["wayland"] } lazy_static = "1.5.0" - [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" tauri-plugin-positioner = "2" - [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.3" objc2-app-kit = { version = "0.3.2", features = [ @@ -63,7 +57,6 @@ objc2-app-kit = { version = "0.3.2", features = [ "NSRunningApplication" ] } objc2-foundation = { version = "0.3.2", features = ["NSNotification", "NSString", "NSURL"] } - [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.58", features = [ "Win32_Foundation", @@ -71,9 +64,11 @@ windows = { version = "0.58", features = [ "Win32_UI_WindowsAndMessaging", "Win32_System_Threading", "Win32_UI_Shell_PropertiesSystem", + "Win32_UI_Shell", "Win32_System_Com", "Win32_UI_Accessibility", "Win32_Storage_FileSystem", "Win32_Globalization", "Win32_UI_Input_KeyboardAndMouse", + "Win32_Graphics_Gdi", ] } diff --git a/src-tauri/src/services/active_app.rs b/src-tauri/src/services/active_app.rs index 43001e5..607c133 100644 --- a/src-tauri/src/services/active_app.rs +++ b/src-tauri/src/services/active_app.rs @@ -301,7 +301,7 @@ mod windows_impl { where F: Fn(AppMetadata) + Send + 'static, { - // Run the hook on a background thread so we don’t block the caller. + // Run the hook on a background thread so we don't block the caller. std::thread::spawn(move || unsafe { let hook = SetWinEventHook( EVENT_SYSTEM_FOREGROUND, @@ -414,10 +414,13 @@ mod windows_impl { let localized = get_file_description(&exe_path_str).or_else(|| unlocalized.clone()); (localized, unlocalized) }; + + let app_icon_b64 = get_active_app_icon_b64(&exe_path_str); + AppMetadata { localized, unlocalized, - app_icon_b64: None, // TODO: Implement icon fetching for Windows + app_icon_b64, } } } @@ -538,6 +541,186 @@ mod windows_impl { } } } + + const ICON_SIZE: u32 = 64; + const ICON_CACHE_LIMIT: usize = 50; + + lazy_static::lazy_static! { + static ref ICON_CACHE: std::sync::Mutex> = + std::sync::Mutex::new(std::collections::VecDeque::new()); + } + + fn get_cached_icon(path: &str) -> Option { + let mut cache = ICON_CACHE.lock().ok()?; + if let Some(pos) = cache.iter().position(|(p, _)| p == path) { + let (_, value) = cache.remove(pos)?; + cache.push_front((path.to_string(), value.clone())); + return Some(value); + } + None + } + + fn put_cached_icon(path: &str, value: String) { + if let Ok(mut cache) = ICON_CACHE.lock() { + if let Some(pos) = cache.iter().position(|(p, _)| p == path) { + cache.remove(pos); + } + cache.push_front((path.to_string(), value)); + if cache.len() > ICON_CACHE_LIMIT { + cache.pop_back(); + } + } + } + + fn get_active_app_icon_b64(exe_path: &str) -> Option { + use base64::engine::general_purpose::STANDARD; + use base64::Engine; + use tracing::warn; + use windows::Win32::Graphics::Gdi::{ + CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDIBits, + GetObjectW, SelectObject, BITMAP, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, + }; + use windows::Win32::UI::Shell::{SHGetFileInfoW, SHFILEINFOW, SHGFI_ICON, SHGFI_LARGEICON}; + use windows::Win32::UI::WindowsAndMessaging::{DestroyIcon, GetIconInfo, ICONINFO}; + + // Check cache first + if let Some(cached) = get_cached_icon(exe_path) { + return Some(cached); + } + + unsafe { + let wide_path: Vec = exe_path.encode_utf16().chain(iter::once(0)).collect(); + let mut file_info: SHFILEINFOW = std::mem::zeroed(); + + use windows::Win32::Storage::FileSystem::FILE_FLAGS_AND_ATTRIBUTES; + + let result = SHGetFileInfoW( + PCWSTR(wide_path.as_ptr()), + FILE_FLAGS_AND_ATTRIBUTES(0), + Some(&mut file_info), + std::mem::size_of::() as u32, + SHGFI_ICON | SHGFI_LARGEICON, + ); + + if result == 0 || file_info.hIcon.is_invalid() { + warn!("Failed to get icon for {}", exe_path); + return None; + } + + let hicon = file_info.hIcon; + + // Get icon info to extract bitmap + let mut icon_info: ICONINFO = std::mem::zeroed(); + if GetIconInfo(hicon, &mut icon_info).is_err() { + let _ = DestroyIcon(hicon); + warn!("Failed to get icon info for {}", exe_path); + return None; + } + + // Get bitmap dimensions + let mut bitmap: BITMAP = std::mem::zeroed(); + if GetObjectW( + icon_info.hbmColor, + std::mem::size_of::() as i32, + Some(&mut bitmap as *mut _ as *mut _), + ) == 0 + { + let _ = DeleteObject(icon_info.hbmColor); + let _ = DeleteObject(icon_info.hbmMask); + let _ = DestroyIcon(hicon); + warn!("Failed to get bitmap object for {}", exe_path); + return None; + } + + let width = bitmap.bmWidth; + let height = bitmap.bmHeight; + + // Create device contexts + let hdc_screen = windows::Win32::Graphics::Gdi::GetDC(HWND(ptr::null_mut())); + let hdc_mem = CreateCompatibleDC(hdc_screen); + let hbm_new = CreateCompatibleBitmap(hdc_screen, ICON_SIZE as i32, ICON_SIZE as i32); + let hbm_old = SelectObject(hdc_mem, hbm_new); + + // Draw icon + let _ = windows::Win32::UI::WindowsAndMessaging::DrawIconEx( + hdc_mem, + 0, + 0, + hicon, + ICON_SIZE as i32, + ICON_SIZE as i32, + 0, + None, + windows::Win32::UI::WindowsAndMessaging::DI_NORMAL, + ); + + // Prepare BITMAPINFO for GetDIBits + let mut bmi: BITMAPINFO = std::mem::zeroed(); + bmi.bmiHeader.biSize = std::mem::size_of::() as u32; + bmi.bmiHeader.biWidth = ICON_SIZE as i32; + bmi.bmiHeader.biHeight = -(ICON_SIZE as i32); // Top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB.0 as u32; + + // Allocate buffer for pixel data + let buffer_size = (ICON_SIZE * ICON_SIZE * 4) as usize; + let mut buffer: Vec = vec![0; buffer_size]; + + // Get bitmap bits + let result = GetDIBits( + hdc_mem, + hbm_new, + 0, + ICON_SIZE, + Some(buffer.as_mut_ptr() as *mut _), + &mut bmi, + DIB_RGB_COLORS, + ); + + // Clean up + let _ = SelectObject(hdc_mem, hbm_old); + let _ = DeleteObject(hbm_new); + let _ = DeleteDC(hdc_mem); + let _ = windows::Win32::Graphics::Gdi::ReleaseDC(HWND(ptr::null_mut()), hdc_screen); + let _ = DeleteObject(icon_info.hbmColor); + let _ = DeleteObject(icon_info.hbmMask); + let _ = DestroyIcon(hicon); + + if result == 0 { + warn!("Failed to get bitmap bits for {}", exe_path); + return None; + } + + // Convert BGRA to RGBA + for i in (0..buffer.len()).step_by(4) { + buffer.swap(i, i + 2); // Swap B and R + } + + // Encode as PNG using image crate + let img = match image::RgbaImage::from_raw(ICON_SIZE, ICON_SIZE, buffer) { + Some(img) => img, + None => { + warn!("Failed to create image from buffer for {}", exe_path); + return None; + } + }; + + let mut png_buffer = Vec::new(); + if let Err(e) = img.write_to( + &mut std::io::Cursor::new(&mut png_buffer), + image::ImageFormat::Png, + ) { + warn!("Failed to encode PNG for {}: {}", exe_path, e); + return None; + } + + let encoded = STANDARD.encode(&png_buffer); + put_cached_icon(exe_path, encoded.clone()); + + Some(encoded) + } + } } pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";