windows support for active app icon retrieval

This commit is contained in:
2026-01-29 23:20:52 +08:00
parent 2ebe3be106
commit ed74d5de26
3 changed files with 206 additions and 14 deletions

22
src-tauri/Cargo.lock generated
View File

@@ -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",

View File

@@ -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",
] }

View File

@@ -301,7 +301,7 @@ mod windows_impl {
where
F: Fn(AppMetadata) + Send + 'static,
{
// Run the hook on a background thread so we dont 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::collections::VecDeque<(String, String)>> =
std::sync::Mutex::new(std::collections::VecDeque::new());
}
fn get_cached_icon(path: &str) -> Option<String> {
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<String> {
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<u16> = 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::<SHFILEINFOW>() 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::<BITMAP>() 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::<BITMAPINFOHEADER>() 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<u8> = 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";