windows support for active app icon retrieval
This commit is contained in:
22
src-tauri/Cargo.lock
generated
22
src-tauri/Cargo.lock
generated
@@ -1866,7 +1866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1989,6 +1989,7 @@ dependencies = [
|
|||||||
"gif",
|
"gif",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"png 0.18.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2412,7 +2413,7 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -3142,6 +3143,19 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"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]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
@@ -4403,7 +4417,7 @@ dependencies = [
|
|||||||
"ico",
|
"ico",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"plist",
|
"plist",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -5080,7 +5094,7 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
|
|||||||
@@ -4,19 +4,15 @@ version = "0.1.0"
|
|||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
# The `_lib` suffix may seem redundant but it is necessary
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
# 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
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
name = "friendolls_desktop_lib"
|
name = "friendolls_desktop_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["macos-private-api", "unstable", "tray-icon"] }
|
tauri = { version = "2", features = ["macos-private-api", "unstable", "tray-icon"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
@@ -44,16 +40,14 @@ flate2 = "1.0.28"
|
|||||||
rust_socketio = "0.6.0"
|
rust_socketio = "0.6.0"
|
||||||
tracing-appender = "0.2.4"
|
tracing-appender = "0.2.4"
|
||||||
tauri-plugin-dialog = "2.4.2"
|
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"
|
gif = "0.14.1"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
enigo = { version = "0.6.1", features = ["wayland"] }
|
enigo = { version = "0.6.1", features = ["wayland"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
tauri-plugin-positioner = "2"
|
tauri-plugin-positioner = "2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.6.3"
|
objc2 = "0.6.3"
|
||||||
objc2-app-kit = { version = "0.3.2", features = [
|
objc2-app-kit = { version = "0.3.2", features = [
|
||||||
@@ -63,7 +57,6 @@ objc2-app-kit = { version = "0.3.2", features = [
|
|||||||
"NSRunningApplication"
|
"NSRunningApplication"
|
||||||
] }
|
] }
|
||||||
objc2-foundation = { version = "0.3.2", features = ["NSNotification", "NSString", "NSURL"] }
|
objc2-foundation = { version = "0.3.2", features = ["NSNotification", "NSString", "NSURL"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows = { version = "0.58", features = [
|
windows = { version = "0.58", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
@@ -71,9 +64,11 @@ windows = { version = "0.58", features = [
|
|||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_UI_Shell_PropertiesSystem",
|
"Win32_UI_Shell_PropertiesSystem",
|
||||||
|
"Win32_UI_Shell",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_UI_Accessibility",
|
"Win32_UI_Accessibility",
|
||||||
"Win32_Storage_FileSystem",
|
"Win32_Storage_FileSystem",
|
||||||
"Win32_Globalization",
|
"Win32_Globalization",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ mod windows_impl {
|
|||||||
where
|
where
|
||||||
F: Fn(AppMetadata) + Send + 'static,
|
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 {
|
std::thread::spawn(move || unsafe {
|
||||||
let hook = SetWinEventHook(
|
let hook = SetWinEventHook(
|
||||||
EVENT_SYSTEM_FOREGROUND,
|
EVENT_SYSTEM_FOREGROUND,
|
||||||
@@ -414,10 +414,13 @@ mod windows_impl {
|
|||||||
let localized = get_file_description(&exe_path_str).or_else(|| unlocalized.clone());
|
let localized = get_file_description(&exe_path_str).or_else(|| unlocalized.clone());
|
||||||
(localized, unlocalized)
|
(localized, unlocalized)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let app_icon_b64 = get_active_app_icon_b64(&exe_path_str);
|
||||||
|
|
||||||
AppMetadata {
|
AppMetadata {
|
||||||
localized,
|
localized,
|
||||||
unlocalized,
|
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";
|
pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";
|
||||||
|
|||||||
Reference in New Issue
Block a user