macos active app icon retrieval
This commit is contained in:
@@ -13,6 +13,7 @@ use crate::get_app_handle;
|
||||
pub struct AppMetadata {
|
||||
pub localized: Option<String>,
|
||||
pub unlocalized: Option<String>,
|
||||
pub app_icon_b64: Option<String>,
|
||||
}
|
||||
|
||||
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||
@@ -49,12 +50,15 @@ where
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use super::AppMetadata;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use lazy_static::lazy_static;
|
||||
use objc2::runtime::{AnyClass, AnyObject, Sel};
|
||||
use objc2::{class, msg_send, sel};
|
||||
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;
|
||||
@@ -108,19 +112,50 @@ mod macos {
|
||||
}
|
||||
|
||||
extern "C" fn app_activated(_self: *mut AnyObject, _cmd: Sel, _notif: *mut AnyObject) {
|
||||
let info = unsafe { get_active_app_names_macos_internal() };
|
||||
let info = unsafe { get_active_app_metadata_macos() };
|
||||
if let Some(cb) = CALLBACK.lock().unwrap().as_ref() {
|
||||
cb(info);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_active_app_names_macos_internal() -> AppMetadata {
|
||||
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());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
if front_app.is_null() {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
app_icon_b64: None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,9 +185,95 @@ mod macos {
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let app_icon_b64 = get_active_app_icon_b64(front_app);
|
||||
|
||||
AppMetadata {
|
||||
localized,
|
||||
unlocalized,
|
||||
app_icon_b64,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_active_app_icon_b64(front_app: *mut AnyObject) -> Option<String> {
|
||||
use std::slice;
|
||||
use tracing::warn;
|
||||
|
||||
unsafe {
|
||||
let bundle_url: *mut AnyObject = msg_send![front_app, bundleURL];
|
||||
if bundle_url.is_null() {
|
||||
warn!("Failed to fetch icon: bundleURL null");
|
||||
return None;
|
||||
}
|
||||
|
||||
let path: *mut NSString = msg_send![bundle_url, path];
|
||||
if path.is_null() {
|
||||
warn!("Failed to fetch icon: path null");
|
||||
return None;
|
||||
}
|
||||
|
||||
let path_str = CStr::from_ptr((*path).UTF8String())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
if let Some(cached) = get_cached_icon(&path_str) {
|
||||
return Some(cached);
|
||||
}
|
||||
|
||||
let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||
let icon: *mut AnyObject = msg_send![ws, iconForFile: path];
|
||||
if icon.is_null() {
|
||||
warn!(
|
||||
"Failed to fetch icon for {}: iconForFile returned null",
|
||||
path_str
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let _: () =
|
||||
msg_send![icon, setSize: objc2_foundation::NSSize::new(ICON_SIZE, ICON_SIZE)];
|
||||
|
||||
let tiff: *mut AnyObject = msg_send![icon, TIFFRepresentation];
|
||||
if tiff.is_null() {
|
||||
warn!(
|
||||
"Failed to fetch icon for {}: TIFFRepresentation null",
|
||||
path_str
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let rep: *mut AnyObject = msg_send![class!(NSBitmapImageRep), imageRepWithData: tiff];
|
||||
if rep.is_null() {
|
||||
warn!(
|
||||
"Failed to fetch icon for {}: imageRepWithData null",
|
||||
path_str
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
// 4 = NSBitmapImageFileTypePNG
|
||||
let png_data: *mut AnyObject = msg_send![
|
||||
rep,
|
||||
representationUsingType: 4u64,
|
||||
properties: std::ptr::null::<AnyObject>()
|
||||
];
|
||||
if png_data.is_null() {
|
||||
warn!("Failed to fetch icon for {}: PNG data null", path_str);
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes: *const u8 = msg_send![png_data, bytes];
|
||||
let len: usize = msg_send![png_data, length];
|
||||
if bytes.is_null() || len == 0 {
|
||||
warn!("Failed to fetch icon for {}: empty PNG data", path_str);
|
||||
return None;
|
||||
}
|
||||
|
||||
let slice = slice::from_raw_parts(bytes, len);
|
||||
let encoded = STANDARD.encode(slice);
|
||||
put_cached_icon(&path_str, encoded.clone());
|
||||
|
||||
Some(encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +345,7 @@ mod windows_impl {
|
||||
_dw_ms_event_time: u32,
|
||||
) {
|
||||
if event == EVENT_SYSTEM_FOREGROUND {
|
||||
let names = get_active_app_names_windows(Some(hwnd));
|
||||
let names = get_active_app_metadata_windows(Some(hwnd));
|
||||
CALLBACK.with(|cb| {
|
||||
if let Some(ref cb) = *cb.borrow() {
|
||||
cb(names);
|
||||
@@ -233,13 +354,14 @@ mod windows_impl {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_active_app_names_windows(hwnd_override: Option<HWND>) -> AppMetadata {
|
||||
pub fn get_active_app_metadata_windows(hwnd_override: Option<HWND>) -> AppMetadata {
|
||||
unsafe {
|
||||
let hwnd = hwnd_override.unwrap_or_else(|| GetForegroundWindow());
|
||||
if hwnd.0 == std::ptr::null_mut() {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
app_icon_b64: None,
|
||||
};
|
||||
}
|
||||
let mut pid: u32 = 0;
|
||||
@@ -248,6 +370,7 @@ mod windows_impl {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
app_icon_b64: None,
|
||||
};
|
||||
}
|
||||
let process_handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
|
||||
@@ -256,9 +379,11 @@ mod windows_impl {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
app_icon_b64: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer: [u16; 1024] = [0; 1024];
|
||||
let size = GetModuleFileNameExW(process_handle, None, &mut buffer);
|
||||
let exe_path = if size > 0 {
|
||||
@@ -267,6 +392,7 @@ mod windows_impl {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
app_icon_b64: None,
|
||||
};
|
||||
};
|
||||
let exe_path_str = exe_path.to_string_lossy();
|
||||
@@ -291,6 +417,7 @@ mod windows_impl {
|
||||
AppMetadata {
|
||||
localized,
|
||||
unlocalized,
|
||||
app_icon_b64: None, // TODO: Implement icon fetching for Windows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,14 @@
|
||||
)})
|
||||
</span>
|
||||
|
||||
<span class="font-mono text-xs badge py-3">
|
||||
<span class="font-mono text-xs badge py-3 flex items-center gap-2">
|
||||
{#if appMetadata?.appIconB64}
|
||||
<img
|
||||
src={`data:image/png;base64,${appMetadata.appIconB64}`}
|
||||
alt="Active app icon"
|
||||
class="size-4"
|
||||
/>
|
||||
{/if}
|
||||
{appMetadata?.localized}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AppMetadata = { localized: string | null, unlocalized: string | null, };
|
||||
export type AppMetadata = { localized: string | null, unlocalized: string | null, appIconB64: string | null, };
|
||||
|
||||
Reference in New Issue
Block a user