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; 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)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
@@ -16,6 +20,46 @@ pub struct AppMetadata {
pub app_icon_b64: Option<String>, 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) pub fn listen_for_active_app_changes<F>(callback: F)
where where
F: Fn(AppMetadata) + Send + 'static, F: Fn(AppMetadata) + Send + 'static,
@@ -58,7 +102,6 @@ mod macos {
use objc2_foundation::NSString; use objc2_foundation::NSString;
use std::ffi::CStr; use std::ffi::CStr;
use std::sync::{Mutex, Once}; use std::sync::{Mutex, Once};
use tracing::info;
#[allow(unused_imports)] // for framework linking, not referenced in code #[allow(unused_imports)] // for framework linking, not referenced in code
use objc2_app_kit::NSWorkspace; use objc2_app_kit::NSWorkspace;
@@ -112,46 +155,30 @@ mod macos {
} }
extern "C" fn app_activated(_self: *mut AnyObject, _cmd: Sel, _notif: *mut AnyObject) { 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() { if let Some(cb) = CALLBACK.lock().unwrap().as_ref() {
cb(info); cb(info);
} }
} }
const ICON_SIZE: f64 = 64.0; const ICON_SIZE: f64 = super::ICON_SIZE as f64;
const ICON_CACHE_LIMIT: usize = 50;
lazy_static! {
static ref ICON_CACHE: Mutex<std::collections::VecDeque<(String, String)>> =
Mutex::new(std::collections::VecDeque::new());
}
fn get_cached_icon(path: &str) -> Option<String> { fn get_cached_icon(path: &str) -> Option<String> {
let mut cache = ICON_CACHE.lock().ok()?; super::icon_cache::get(path)
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) { fn put_cached_icon(path: &str, value: String) {
if let Ok(mut cache) = ICON_CACHE.lock() { super::icon_cache::put(path, value);
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();
}
}
} }
pub unsafe fn get_active_app_metadata_macos() -> AppMetadata { /// Retrieves metadata for the currently active application on macOS, including names and icon.
let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace]; pub fn get_active_app_metadata_macos() -> AppMetadata {
let front_app: *mut AnyObject = msg_send![ws, frontmostApplication]; 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() { if front_app.is_null() {
warn!("No frontmost application found");
return AppMetadata { return AppMetadata {
localized: None, localized: None,
unlocalized: 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() { let localized = if name.is_null() {
warn!("Localized name is null for frontmost application");
None None
} else { } else {
Some(unsafe { 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() { let unlocalized = if exe_url.is_null() {
warn!("Executable URL is null for frontmost application");
None None
} else { } 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() { if exe_name.is_null() {
warn!("Executable name is null");
None None
} else { } else {
Some(unsafe { 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! { thread_local! {
static CALLBACK: std::cell::RefCell<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> = static CALLBACK: std::cell::RefCell<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> =
std::cell::RefCell::new(None); 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 { pub fn get_active_app_metadata_windows(hwnd_override: Option<HWND>) -> AppMetadata {
use tracing::warn;
unsafe { unsafe {
let hwnd = hwnd_override.unwrap_or_else(|| GetForegroundWindow()); let hwnd = hwnd_override.unwrap_or_else(|| GetForegroundWindow());
if hwnd.0 == std::ptr::null_mut() { if hwnd.0 == std::ptr::null_mut() {
warn!("No foreground window found");
return AppMetadata { return AppMetadata {
localized: None, localized: None,
unlocalized: None, unlocalized: None,
@@ -367,6 +403,7 @@ mod windows_impl {
let mut pid: u32 = 0; let mut pid: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut pid)); GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid == 0 { if pid == 0 {
warn!("Failed to get process ID for foreground window");
return AppMetadata { return AppMetadata {
localized: None, localized: None,
unlocalized: None, unlocalized: None,
@@ -376,6 +413,7 @@ mod windows_impl {
let process_handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { let process_handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
Ok(h) if !h.is_invalid() => h, Ok(h) if !h.is_invalid() => h,
_ => { _ => {
warn!("Failed to open process {} for querying", pid);
return AppMetadata { return AppMetadata {
localized: None, localized: None,
unlocalized: None, unlocalized: None,
@@ -389,6 +427,7 @@ mod windows_impl {
let exe_path = if size > 0 { let exe_path = if size > 0 {
OsString::from_wide(&buffer[..size as usize]) OsString::from_wide(&buffer[..size as usize])
} else { } else {
warn!("Failed to get module file name for process {}", pid);
return AppMetadata { return AppMetadata {
localized: None, localized: None,
unlocalized: None, unlocalized: None,
@@ -543,31 +582,22 @@ mod windows_impl {
} }
const ICON_SIZE: u32 = 64; 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> { fn get_cached_icon(path: &str) -> Option<String> {
let mut cache = ICON_CACHE.lock().ok()?; super::icon_cache::get(path)
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) { fn put_cached_icon(path: &str, value: String) {
if let Ok(mut cache) = ICON_CACHE.lock() { super::icon_cache::put(path, value);
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 { /// RAII wrapper for Windows HICON handles to ensure proper cleanup.
cache.pop_back(); 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; return None;
} }
let hicon = file_info.hIcon; let icon_handle = IconHandle(file_info.hIcon);
// Get icon info to extract bitmap // Get icon info to extract bitmap
let mut icon_info: ICONINFO = std::mem::zeroed(); let mut icon_info: ICONINFO = std::mem::zeroed();
if GetIconInfo(hicon, &mut icon_info).is_err() { if GetIconInfo(icon_handle.0, &mut icon_info).is_err() {
let _ = DestroyIcon(hicon);
warn!("Failed to get icon info for {}", exe_path); warn!("Failed to get icon info for {}", exe_path);
return None; return None;
} }
@@ -627,7 +656,6 @@ mod windows_impl {
{ {
let _ = DeleteObject(icon_info.hbmColor); let _ = DeleteObject(icon_info.hbmColor);
let _ = DeleteObject(icon_info.hbmMask); let _ = DeleteObject(icon_info.hbmMask);
let _ = DestroyIcon(hicon);
warn!("Failed to get bitmap object for {}", exe_path); warn!("Failed to get bitmap object for {}", exe_path);
return None; return None;
} }
@@ -638,7 +666,11 @@ mod windows_impl {
// Create device contexts // Create device contexts
let hdc_screen = windows::Win32::Graphics::Gdi::GetDC(HWND(ptr::null_mut())); let hdc_screen = windows::Win32::Graphics::Gdi::GetDC(HWND(ptr::null_mut()));
let hdc_mem = CreateCompatibleDC(hdc_screen); 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); let hbm_old = SelectObject(hdc_mem, hbm_new);
// Draw icon // Draw icon
@@ -646,9 +678,9 @@ mod windows_impl {
hdc_mem, hdc_mem,
0, 0,
0, 0,
hicon, icon_handle.0,
ICON_SIZE as i32, super::ICON_SIZE as i32,
ICON_SIZE as i32, super::ICON_SIZE as i32,
0, 0,
None, None,
windows::Win32::UI::WindowsAndMessaging::DI_NORMAL, windows::Win32::UI::WindowsAndMessaging::DI_NORMAL,
@@ -657,14 +689,14 @@ mod windows_impl {
// Prepare BITMAPINFO for GetDIBits // Prepare BITMAPINFO for GetDIBits
let mut bmi: BITMAPINFO = std::mem::zeroed(); let mut bmi: BITMAPINFO = std::mem::zeroed();
bmi.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32; bmi.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bmi.bmiHeader.biWidth = ICON_SIZE as i32; bmi.bmiHeader.biWidth = super::ICON_SIZE as i32;
bmi.bmiHeader.biHeight = -(ICON_SIZE as i32); // Top-down bmi.bmiHeader.biHeight = -(super::ICON_SIZE as i32); // Top-down
bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB.0 as u32; bmi.bmiHeader.biCompression = BI_RGB.0 as u32;
// Allocate buffer for pixel data // 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]; let mut buffer: Vec<u8> = vec![0; buffer_size];
// Get bitmap bits // Get bitmap bits
@@ -672,7 +704,7 @@ mod windows_impl {
hdc_mem, hdc_mem,
hbm_new, hbm_new,
0, 0,
ICON_SIZE, super::ICON_SIZE,
Some(buffer.as_mut_ptr() as *mut _), Some(buffer.as_mut_ptr() as *mut _),
&mut bmi, &mut bmi,
DIB_RGB_COLORS, DIB_RGB_COLORS,
@@ -685,7 +717,6 @@ mod windows_impl {
let _ = windows::Win32::Graphics::Gdi::ReleaseDC(HWND(ptr::null_mut()), hdc_screen); let _ = windows::Win32::Graphics::Gdi::ReleaseDC(HWND(ptr::null_mut()), hdc_screen);
let _ = DeleteObject(icon_info.hbmColor); let _ = DeleteObject(icon_info.hbmColor);
let _ = DeleteObject(icon_info.hbmMask); let _ = DeleteObject(icon_info.hbmMask);
let _ = DestroyIcon(hicon);
if result == 0 { if result == 0 {
warn!("Failed to get bitmap bits for {}", exe_path); 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"; 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() { pub fn init_active_app_changes_listener() {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
listen_for_active_app_changes(|app_names: AppMetadata| { listen_for_active_app_changes(|app_names: AppMetadata| {