enhancements to active_app.rs

This commit is contained in:
2026-01-29 23:48:33 +08:00
parent ed74d5de26
commit e15cf16817

View File

@@ -7,6 +7,10 @@ use tracing::error;
use crate::get_app_handle;
const ICON_SIZE: u32 = 64;
const ICON_CACHE_LIMIT: usize = 50;
/// Metadata for the currently active application, including localized and unlocalized names, and an optional base64-encoded icon.
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
@@ -16,6 +20,46 @@ pub struct AppMetadata {
pub app_icon_b64: Option<String>,
}
/// Module for caching application icons as base64-encoded strings to improve performance.
/// Uses an LRU-style cache with a fixed capacity.
mod icon_cache {
use lazy_static::lazy_static;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
lazy_static! {
static ref CACHE: Mutex<VecDeque<(Arc<str>, Arc<str>)>> = Mutex::new(VecDeque::new());
}
/// Retrieves a cached icon by path, moving it to the front of the cache if found.
pub fn get(path: &str) -> Option<String> {
let mut cache = CACHE.lock().unwrap();
if let Some(pos) = cache.iter().position(|(p, _)| p.as_ref() == path) {
let (_, value) = cache.remove(pos).expect("position exists");
cache.push_front((Arc::from(path), value.clone()));
Some(value.as_ref().to_string())
} else {
None
}
}
/// Stores an icon in the cache, evicting the oldest entry if the cache is full.
pub fn put(path: &str, value: String) {
let mut cache = CACHE.lock().unwrap();
let path_arc = Arc::from(path);
let value_arc = Arc::from(value.as_str());
if let Some(pos) = cache.iter().position(|(p, _)| p.as_ref() == path) {
cache.remove(pos);
}
cache.push_front((path_arc, value_arc));
if cache.len() > super::ICON_CACHE_LIMIT {
cache.pop_back();
}
}
}
/// Listens for changes in the active (foreground) application and calls the provided callback with metadata.
/// The implementation varies by platform: macOS uses NSWorkspace notifications, Windows uses WinEventHook.
pub fn listen_for_active_app_changes<F>(callback: F)
where
F: Fn(AppMetadata) + Send + 'static,
@@ -58,7 +102,6 @@ mod macos {
use objc2_foundation::NSString;
use std::ffi::CStr;
use std::sync::{Mutex, Once};
use tracing::info;
#[allow(unused_imports)] // for framework linking, not referenced in code
use objc2_app_kit::NSWorkspace;
@@ -112,46 +155,30 @@ mod macos {
}
extern "C" fn app_activated(_self: *mut AnyObject, _cmd: Sel, _notif: *mut AnyObject) {
let info = unsafe { get_active_app_metadata_macos() };
let info = get_active_app_metadata_macos();
if let Some(cb) = CALLBACK.lock().unwrap().as_ref() {
cb(info);
}
}
const ICON_SIZE: f64 = 64.0;
const ICON_CACHE_LIMIT: usize = 50;
lazy_static! {
static ref ICON_CACHE: Mutex<std::collections::VecDeque<(String, String)>> =
Mutex::new(std::collections::VecDeque::new());
}
const ICON_SIZE: f64 = super::ICON_SIZE as f64;
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
super::icon_cache::get(path)
}
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();
}
}
super::icon_cache::put(path, value);
}
pub unsafe fn get_active_app_metadata_macos() -> AppMetadata {
let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
let front_app: *mut AnyObject = msg_send![ws, frontmostApplication];
/// Retrieves metadata for the currently active application on macOS, including names and icon.
pub fn get_active_app_metadata_macos() -> AppMetadata {
use tracing::warn;
let ws: *mut AnyObject = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
let front_app: *mut AnyObject = unsafe { msg_send![ws, frontmostApplication] };
if front_app.is_null() {
warn!("No frontmost application found");
return AppMetadata {
localized: None,
unlocalized: None,
@@ -159,8 +186,9 @@ mod macos {
};
}
let name: *mut NSString = msg_send![front_app, localizedName];
let name: *mut NSString = unsafe { msg_send![front_app, localizedName] };
let localized = if name.is_null() {
warn!("Localized name is null for frontmost application");
None
} else {
Some(unsafe {
@@ -170,12 +198,14 @@ mod macos {
})
};
let exe_url: *mut AnyObject = msg_send![front_app, executableURL];
let exe_url: *mut AnyObject = unsafe { msg_send![front_app, executableURL] };
let unlocalized = if exe_url.is_null() {
warn!("Executable URL is null for frontmost application");
None
} else {
let exe_name: *mut NSString = msg_send![exe_url, lastPathComponent];
let exe_name: *mut NSString = unsafe { msg_send![exe_url, lastPathComponent] };
if exe_name.is_null() {
warn!("Executable name is null");
None
} else {
Some(unsafe {
@@ -330,6 +360,8 @@ mod windows_impl {
});
}
// RefCell is safe here because the callback is stored and accessed only within the dedicated event loop thread.
// No concurrent access occurs since WinEventHook runs on a single background thread.
thread_local! {
static CALLBACK: std::cell::RefCell<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> =
std::cell::RefCell::new(None);
@@ -354,10 +386,14 @@ mod windows_impl {
}
}
/// Retrieves metadata for the active application on Windows, optionally using a provided window handle.
pub fn get_active_app_metadata_windows(hwnd_override: Option<HWND>) -> AppMetadata {
use tracing::warn;
unsafe {
let hwnd = hwnd_override.unwrap_or_else(|| GetForegroundWindow());
if hwnd.0 == std::ptr::null_mut() {
warn!("No foreground window found");
return AppMetadata {
localized: None,
unlocalized: None,
@@ -367,6 +403,7 @@ mod windows_impl {
let mut pid: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid == 0 {
warn!("Failed to get process ID for foreground window");
return AppMetadata {
localized: None,
unlocalized: None,
@@ -376,6 +413,7 @@ mod windows_impl {
let process_handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
Ok(h) if !h.is_invalid() => h,
_ => {
warn!("Failed to open process {} for querying", pid);
return AppMetadata {
localized: None,
unlocalized: None,
@@ -389,6 +427,7 @@ mod windows_impl {
let exe_path = if size > 0 {
OsString::from_wide(&buffer[..size as usize])
} else {
warn!("Failed to get module file name for process {}", pid);
return AppMetadata {
localized: None,
unlocalized: None,
@@ -543,31 +582,22 @@ 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
super::icon_cache::get(path)
}
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);
super::icon_cache::put(path, value);
}
cache.push_front((path.to_string(), value));
if cache.len() > ICON_CACHE_LIMIT {
cache.pop_back();
/// RAII wrapper for Windows HICON handles to ensure proper cleanup.
struct IconHandle(windows::Win32::UI::WindowsAndMessaging::HICON);
impl Drop for IconHandle {
fn drop(&mut self) {
unsafe {
windows::Win32::UI::WindowsAndMessaging::DestroyIcon(self.0);
}
}
}
@@ -607,12 +637,11 @@ mod windows_impl {
return None;
}
let hicon = file_info.hIcon;
let icon_handle = IconHandle(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);
if GetIconInfo(icon_handle.0, &mut icon_info).is_err() {
warn!("Failed to get icon info for {}", exe_path);
return None;
}
@@ -627,7 +656,6 @@ mod windows_impl {
{
let _ = DeleteObject(icon_info.hbmColor);
let _ = DeleteObject(icon_info.hbmMask);
let _ = DestroyIcon(hicon);
warn!("Failed to get bitmap object for {}", exe_path);
return None;
}
@@ -638,7 +666,11 @@ mod windows_impl {
// 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_new = CreateCompatibleBitmap(
hdc_screen,
super::ICON_SIZE as i32,
super::ICON_SIZE as i32,
);
let hbm_old = SelectObject(hdc_mem, hbm_new);
// Draw icon
@@ -646,9 +678,9 @@ mod windows_impl {
hdc_mem,
0,
0,
hicon,
ICON_SIZE as i32,
ICON_SIZE as i32,
icon_handle.0,
super::ICON_SIZE as i32,
super::ICON_SIZE as i32,
0,
None,
windows::Win32::UI::WindowsAndMessaging::DI_NORMAL,
@@ -657,14 +689,14 @@ mod windows_impl {
// 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.biWidth = super::ICON_SIZE as i32;
bmi.bmiHeader.biHeight = -(super::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 buffer_size = (super::ICON_SIZE * super::ICON_SIZE * 4) as usize;
let mut buffer: Vec<u8> = vec![0; buffer_size];
// Get bitmap bits
@@ -672,7 +704,7 @@ mod windows_impl {
hdc_mem,
hbm_new,
0,
ICON_SIZE,
super::ICON_SIZE,
Some(buffer.as_mut_ptr() as *mut _),
&mut bmi,
DIB_RGB_COLORS,
@@ -685,7 +717,6 @@ mod windows_impl {
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);
@@ -725,6 +756,7 @@ mod windows_impl {
pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";
/// Initializes the active app change listener and emits events to the Tauri app on changes.
pub fn init_active_app_changes_listener() {
let app_handle = get_app_handle();
listen_for_active_app_changes(|app_names: AppMetadata| {