integration of active_app module
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1184,6 +1184,7 @@ dependencies = [
|
||||
"gif",
|
||||
"image",
|
||||
"keyring",
|
||||
"lazy_static",
|
||||
"objc2 0.6.3",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.2",
|
||||
|
||||
@@ -48,6 +48,7 @@ image = {version = "0.25.9", default-features = false, features = ["gif"] }
|
||||
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"
|
||||
@@ -55,11 +56,24 @@ tauri-plugin-positioner = "2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.6.3"
|
||||
objc2-app-kit = { version = "0.3.2", features = ["NSWindow", "NSWindowScripting"] }
|
||||
objc2-foundation = "0.3.2"
|
||||
objc2-app-kit = { version = "0.3.2", features = [
|
||||
"NSWindow",
|
||||
"NSWindowScripting",
|
||||
"NSWorkspace",
|
||||
"NSRunningApplication"
|
||||
] }
|
||||
objc2-foundation = { version = "0.3.2", features = ["NSNotification", "NSString", "NSURL"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_Globalization",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
] }
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
lock_w,
|
||||
remotes::health::{HealthError, HealthRemote},
|
||||
services::{
|
||||
active_app::init_active_app_changes_listener,
|
||||
auth::{get_access_token, get_tokens},
|
||||
health_manager::show_health_manager_with_error,
|
||||
scene::{close_splash_window, open_scene_window, open_splash_window},
|
||||
@@ -23,6 +24,10 @@ pub async fn start_fdoll() {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
guard.tray = Some(tray);
|
||||
}
|
||||
|
||||
// Begin listening for foreground app changes
|
||||
init_active_app_changes_listener();
|
||||
|
||||
if let Err(err) = init_startup_sequence().await {
|
||||
tracing::error!("startup sequence failed: {err}");
|
||||
show_health_manager_with_error(Some(err.to_string()));
|
||||
|
||||
425
src-tauri/src/services/active_app.rs
Normal file
425
src-tauri/src/services/active_app.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use std::fmt::Debug;
|
||||
use ts_rs::TS;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Emitter;
|
||||
use tracing::error;
|
||||
|
||||
use crate::get_app_handle;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AppMetadata {
|
||||
pub localized: Option<String>,
|
||||
pub unlocalized: Option<String>,
|
||||
}
|
||||
|
||||
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||
where
|
||||
F: Fn(AppMetadata) + Send + 'static,
|
||||
{
|
||||
listen_impl(callback)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn listen_impl<F>(callback: F)
|
||||
where
|
||||
F: Fn(AppMetadata) + Send + 'static,
|
||||
{
|
||||
macos::listen_for_active_app_changes(callback);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn listen_impl<F>(callback: F)
|
||||
where
|
||||
F: Fn(AppMetadata) + Send + 'static,
|
||||
{
|
||||
windows_impl::listen_for_active_app_changes(callback);
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
fn listen_impl<F>(_callback: F)
|
||||
where
|
||||
F: Fn(AppMetadata) + Send + 'static,
|
||||
{
|
||||
// no-op on unsupported platforms
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use super::AppMetadata;
|
||||
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};
|
||||
|
||||
#[allow(unused_imports)] // for framework linking, not referenced in code
|
||||
use objc2_app_kit::NSWorkspace;
|
||||
|
||||
lazy_static! {
|
||||
static ref CALLBACK: Mutex<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> =
|
||||
Mutex::new(None);
|
||||
}
|
||||
static INIT_OBSERVER: Once = Once::new();
|
||||
|
||||
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||
where
|
||||
F: Fn(AppMetadata) + Send + 'static,
|
||||
{
|
||||
INIT_OBSERVER.call_once(|| {
|
||||
register_objc_observer_class();
|
||||
});
|
||||
*CALLBACK.lock().unwrap() = Some(Box::new(callback));
|
||||
|
||||
unsafe {
|
||||
let cls = AnyClass::get(CStr::from_bytes_with_nul(b"RustActiveAppObserver\0").unwrap())
|
||||
.unwrap();
|
||||
let observer: *mut AnyObject = msg_send![cls, new];
|
||||
|
||||
let ws: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||
let nc: *mut AnyObject = msg_send![ws, notificationCenter];
|
||||
let notif_name = NSString::from_str("NSWorkspaceDidActivateApplicationNotification");
|
||||
let _: () = msg_send![
|
||||
nc,
|
||||
addObserver: observer,
|
||||
selector: sel!(appActivated:),
|
||||
name: &*notif_name,
|
||||
object: ws
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
fn register_objc_observer_class() {
|
||||
use objc2::runtime::ClassBuilder;
|
||||
|
||||
let cname = CStr::from_bytes_with_nul(b"RustActiveAppObserver\0").unwrap();
|
||||
let super_cls = class!(NSObject);
|
||||
let mut builder = ClassBuilder::new(&cname, super_cls).unwrap();
|
||||
unsafe {
|
||||
builder.add_method(
|
||||
sel!(appActivated:),
|
||||
app_activated as extern "C" fn(*mut AnyObject, Sel, *mut AnyObject),
|
||||
);
|
||||
}
|
||||
builder.register();
|
||||
}
|
||||
|
||||
extern "C" fn app_activated(_self: *mut AnyObject, _cmd: Sel, _notif: *mut AnyObject) {
|
||||
let info = unsafe { get_active_app_names_macos_internal() };
|
||||
if let Some(cb) = CALLBACK.lock().unwrap().as_ref() {
|
||||
cb(info);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_active_app_names_macos_internal() -> 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,
|
||||
};
|
||||
}
|
||||
|
||||
let name: *mut NSString = msg_send![front_app, localizedName];
|
||||
let localized = if name.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe {
|
||||
CStr::from_ptr((*name).UTF8String())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
};
|
||||
|
||||
let exe_url: *mut AnyObject = msg_send![front_app, executableURL];
|
||||
let unlocalized = if exe_url.is_null() {
|
||||
None
|
||||
} else {
|
||||
let exe_name: *mut NSString = msg_send![exe_url, lastPathComponent];
|
||||
if exe_name.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe {
|
||||
CStr::from_ptr((*exe_name).UTF8String())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
}
|
||||
};
|
||||
AppMetadata {
|
||||
localized,
|
||||
unlocalized,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_impl {
|
||||
use super::AppMetadata;
|
||||
use std::ffi::OsString;
|
||||
use std::iter;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Globalization::GetUserDefaultLangID;
|
||||
use windows::Win32::System::ProcessStatus::GetModuleFileNameExW;
|
||||
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
|
||||
use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
DispatchMessageW, GetForegroundWindow, GetMessageW, GetWindowTextW,
|
||||
GetWindowThreadProcessId, EVENT_SYSTEM_FOREGROUND, MSG, WINEVENT_OUTOFCONTEXT,
|
||||
};
|
||||
|
||||
pub fn listen_for_active_app_changes<F>(callback: F)
|
||||
where
|
||||
F: Fn(AppMetadata) + Send + 'static,
|
||||
{
|
||||
// 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,
|
||||
EVENT_SYSTEM_FOREGROUND,
|
||||
None,
|
||||
Some(win_event_proc::<F>),
|
||||
0,
|
||||
0,
|
||||
WINEVENT_OUTOFCONTEXT,
|
||||
);
|
||||
|
||||
if hook.is_invalid() {
|
||||
eprintln!("Failed to set event hook");
|
||||
return;
|
||||
}
|
||||
|
||||
// store callback in TLS for this thread
|
||||
CALLBACK.with(|cb| cb.replace(Some(Box::new(callback))));
|
||||
|
||||
let mut msg = MSG::default();
|
||||
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
let _ = UnhookWinEvent(hook);
|
||||
});
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CALLBACK: std::cell::RefCell<Option<Box<dyn Fn(AppMetadata) + Send + 'static>>> =
|
||||
std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
unsafe extern "system" fn win_event_proc<F>(
|
||||
_h_win_event_hook: HWINEVENTHOOK,
|
||||
event: u32,
|
||||
hwnd: HWND,
|
||||
_id_object: i32,
|
||||
_id_child: i32,
|
||||
_id_event_thread: u32,
|
||||
_dw_ms_event_time: u32,
|
||||
) {
|
||||
if event == EVENT_SYSTEM_FOREGROUND {
|
||||
let names = get_active_app_names_windows(Some(hwnd));
|
||||
CALLBACK.with(|cb| {
|
||||
if let Some(ref cb) = *cb.borrow() {
|
||||
cb(names);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_active_app_names_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,
|
||||
};
|
||||
}
|
||||
let mut pid: u32 = 0;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut pid));
|
||||
if pid == 0 {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
};
|
||||
}
|
||||
let process_handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
|
||||
Ok(h) if !h.is_invalid() => h,
|
||||
_ => {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
let mut buffer: [u16; 1024] = [0; 1024];
|
||||
let size = GetModuleFileNameExW(process_handle, None, &mut buffer);
|
||||
let exe_path = if size > 0 {
|
||||
OsString::from_wide(&buffer[..size as usize])
|
||||
} else {
|
||||
return AppMetadata {
|
||||
localized: None,
|
||||
unlocalized: None,
|
||||
};
|
||||
};
|
||||
let exe_path_str = exe_path.to_string_lossy();
|
||||
let exe_name = Path::new(&*exe_path_str)
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("");
|
||||
let is_uwp = exe_name.eq_ignore_ascii_case("ApplicationFrameHost.exe");
|
||||
let (localized, unlocalized) = if is_uwp {
|
||||
(
|
||||
get_window_title(hwnd),
|
||||
Some("ApplicationFrameHost".to_string()),
|
||||
)
|
||||
} else {
|
||||
let unlocalized = Path::new(&*exe_path_str)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_string());
|
||||
let localized = get_file_description(&exe_path_str).or_else(|| unlocalized.clone());
|
||||
(localized, unlocalized)
|
||||
};
|
||||
AppMetadata {
|
||||
localized,
|
||||
unlocalized,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window_title(hwnd: HWND) -> Option<String> {
|
||||
unsafe {
|
||||
let mut buffer: [u16; 512] = [0; 512];
|
||||
let len = GetWindowTextW(hwnd, &mut buffer);
|
||||
if len == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
OsString::from_wide(&buffer[..len as usize])
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_description(exe_path: &str) -> Option<String> {
|
||||
unsafe {
|
||||
let wide_path: Vec<u16> = exe_path.encode_utf16().chain(iter::once(0)).collect();
|
||||
let size = windows::Win32::Storage::FileSystem::GetFileVersionInfoSizeW(
|
||||
PCWSTR(wide_path.as_ptr()),
|
||||
None,
|
||||
);
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buffer: Vec<u8> = vec![0; size as usize];
|
||||
let result = windows::Win32::Storage::FileSystem::GetFileVersionInfoW(
|
||||
PCWSTR(wide_path.as_ptr()),
|
||||
0,
|
||||
size,
|
||||
buffer.as_mut_ptr() as *mut _,
|
||||
);
|
||||
if result.is_err() {
|
||||
return None;
|
||||
}
|
||||
let mut translations_ptr: *mut u8 = ptr::null_mut();
|
||||
let mut translations_len: u32 = 0;
|
||||
let translation_query: Vec<u16> = "\\VarFileInfo\\Translation"
|
||||
.encode_utf16()
|
||||
.chain(iter::once(0))
|
||||
.collect();
|
||||
let trans_result = windows::Win32::Storage::FileSystem::VerQueryValueW(
|
||||
buffer.as_ptr() as *const _,
|
||||
PCWSTR(translation_query.as_ptr()),
|
||||
&mut translations_ptr as *mut _ as *mut *mut _,
|
||||
&mut translations_len,
|
||||
);
|
||||
let system_lang_id = GetUserDefaultLangID();
|
||||
if trans_result.as_bool() && translations_len >= 4 {
|
||||
let translations = std::slice::from_raw_parts(
|
||||
translations_ptr as *const u16,
|
||||
(translations_len / 2) as usize,
|
||||
);
|
||||
let mut lang_attempts = Vec::new();
|
||||
for i in (0..translations.len()).step_by(2) {
|
||||
if i + 1 < translations.len() {
|
||||
let lang = translations[i];
|
||||
let codepage = translations[i + 1];
|
||||
if lang == system_lang_id {
|
||||
lang_attempts.insert(0, (lang, codepage));
|
||||
} else {
|
||||
lang_attempts.push((lang, codepage));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (lang, codepage) in lang_attempts {
|
||||
let query = format!(
|
||||
"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
|
||||
lang, codepage
|
||||
);
|
||||
let wide_query: Vec<u16> = query.encode_utf16().chain(iter::once(0)).collect();
|
||||
let mut value_ptr: *mut u8 = ptr::null_mut();
|
||||
let mut value_len: u32 = 0;
|
||||
let query_result = windows::Win32::Storage::FileSystem::VerQueryValueW(
|
||||
buffer.as_ptr() as *const _,
|
||||
PCWSTR(wide_query.as_ptr()),
|
||||
&mut value_ptr as *mut _ as *mut *mut _,
|
||||
&mut value_len,
|
||||
);
|
||||
if query_result.as_bool() && !value_ptr.is_null() && value_len > 0 {
|
||||
let wide_str = std::slice::from_raw_parts(
|
||||
value_ptr as *const u16,
|
||||
(value_len as usize).saturating_sub(1),
|
||||
);
|
||||
let description =
|
||||
OsString::from_wide(wide_str).to_string_lossy().into_owned();
|
||||
if !description.is_empty() {
|
||||
return Some(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let fallback_query: Vec<u16> = "\\StringFileInfo\\040904b0\\FileDescription"
|
||||
.encode_utf16()
|
||||
.chain(iter::once(0))
|
||||
.collect();
|
||||
let mut value_ptr: *mut u8 = ptr::null_mut();
|
||||
let mut value_len: u32 = 0;
|
||||
let query_result = windows::Win32::Storage::FileSystem::VerQueryValueW(
|
||||
buffer.as_ptr() as *const _,
|
||||
PCWSTR(fallback_query.as_ptr()),
|
||||
&mut value_ptr as *mut _ as *mut *mut _,
|
||||
&mut value_len,
|
||||
);
|
||||
if query_result.as_bool() && !value_ptr.is_null() && value_len > 0 {
|
||||
let wide_str = std::slice::from_raw_parts(
|
||||
value_ptr as *const u16,
|
||||
(value_len as usize).saturating_sub(1),
|
||||
);
|
||||
Some(OsString::from_wide(wide_str).to_string_lossy().into_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static ACTIVE_APP_CHANGED: &str = "active-app-changed";
|
||||
|
||||
pub fn init_active_app_changes_listener() {
|
||||
let app_handle = get_app_handle();
|
||||
listen_for_active_app_changes(|app_names: AppMetadata| {
|
||||
if let Err(e) = app_handle.emit(ACTIVE_APP_CHANGED, app_names) {
|
||||
error!("Failed to emit active app changed event: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod active_app;
|
||||
pub mod app_menu;
|
||||
pub mod auth;
|
||||
pub mod client_config_manager;
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import DesktopPet from "./components/DesktopPet.svelte";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { onMount } from "svelte";
|
||||
import type { AppMetadata } from "../../types/bindings/AppMetadata";
|
||||
|
||||
let innerWidth = 0;
|
||||
let innerHeight = 0;
|
||||
let innerWidth = $state(0);
|
||||
let innerHeight = $state(0);
|
||||
|
||||
$: isInteractive = $sceneInteractive;
|
||||
let isInteractive = $derived($sceneInteractive);
|
||||
|
||||
function getFriendById(userId: string) {
|
||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||
@@ -29,6 +32,18 @@
|
||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||
return friend?.friend.activeDoll;
|
||||
}
|
||||
|
||||
let appMetadata: AppMetadata | null = $state(null);
|
||||
|
||||
onMount(() => {
|
||||
const unlisten = listen<AppMetadata>("active-app-changed", (event) => {
|
||||
appMetadata = event.payload;
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((u) => u());
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth bind:innerHeight />
|
||||
@@ -63,11 +78,14 @@
|
||||
)})
|
||||
</span>
|
||||
|
||||
<span class="font-mono text-xs badge py-3">
|
||||
{appMetadata?.localized}
|
||||
</span>
|
||||
|
||||
{#if Object.keys($friendsCursorPositions).length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
||||
{@const dollConfig = getFriendDoll(userId)}
|
||||
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
||||
<span class="font-bold">{getFriendById(userId).name}</span>
|
||||
<div class="flex flex-col font-mono">
|
||||
|
||||
3
src/types/bindings/AppMetadata.ts
Normal file
3
src/types/bindings/AppMetadata.ts
Normal file
@@ -0,0 +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, };
|
||||
Reference in New Issue
Block a user