From 151ff9e5c68dcb27a667a0604252cc768e23d76b Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Fri, 7 Aug 2026 03:27:40 +0800 Subject: [PATCH] live metadata display in UI --- src-tauri/src/cursor/mod.rs | 40 +++++++++----- src-tauri/src/lib.rs | 8 ++- src-tauri/src/ufa/mod.rs | 28 ++++++++-- src-tauri/src/ufa/types.rs | 3 +- src/lib/bindings.ts | 12 ++++ src/lib/components/live-metadata.svelte | 73 +++++++++++++++++++++++++ src/routes/+page.svelte | 4 ++ 7 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 src/lib/components/live-metadata.svelte diff --git a/src-tauri/src/cursor/mod.rs b/src-tauri/src/cursor/mod.rs index f0d0590..b354a31 100644 --- a/src-tauri/src/cursor/mod.rs +++ b/src-tauri/src/cursor/mod.rs @@ -1,27 +1,38 @@ use device_query::{DeviceEvents, DeviceEventsHandler}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use specta::Type; use std::sync::Mutex; use std::sync::RwLock; use std::time::Duration; -use tauri::AppHandle; +use tauri::{AppHandle, Manager}; +use tauri_specta::Event; use tokio::sync::mpsc; use tokio::sync::watch; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, Type)] #[serde(rename_all = "camelCase")] pub struct CursorPosition { pub x: f64, pub y: f64, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, Type)] #[serde(rename_all = "camelCase")] pub struct CursorPositions { pub raw: CursorPosition, pub mapped: CursorPosition, } +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[serde(rename_all = "camelCase")] +pub struct CursorPositionChanged { + pub positions: CursorPositions, +} + +#[derive(Default)] +pub struct CursorState(RwLock); + // Was private, but for some reason LSP // complains even when there's no external references. // Possibly because of `lazy_static!`. @@ -33,8 +44,6 @@ pub struct CursorTask { lazy_static! { pub static ref CURSOR_TASK: Mutex> = Mutex::new(None); - pub static ref CURSOR_POSITION: RwLock = - RwLock::new(CursorPositions::default()); } /// Initialize cursor tracking. @@ -63,8 +72,9 @@ pub fn init(app: &AppHandle) { .expect("Failed to resolve primary monitor") .expect("Failed to resolve primary monitor"); + let handle = app.clone(); let task = tauri::async_runtime::spawn(async move { - if let Err(e) = init_cursor_tracking_i(stop_rx, primary_monitor).await { + if let Err(e) = init_cursor_tracking_i(stop_rx, primary_monitor, handle).await { println!("Failed to initialize cursor tracking: {}", e); } }); @@ -74,15 +84,16 @@ pub fn init(app: &AppHandle) { } #[inline] -fn update_cursor_position(state: CursorPositions) { - let mut guard = CURSOR_POSITION - .write() - .expect("Cursor Position lock failed"); +fn update_cursor_position(handle: &AppHandle, positions: CursorPositions) { + let state = handle.state::(); + let mut guard = state.0.write().expect("Cursor Position lock failed"); - *guard = state.clone(); + *guard = positions.clone(); + drop(guard); - // TODO: Emit metadata to the app - println!("EVENT: CURSOR POSITION CHANGED\n({:?})", state); + if let Err(error) = (CursorPositionChanged { positions }).emit(handle) { + eprintln!("Failed to emit cursor position change: {error}"); + } } /// Convert absolute to normalized coordinates (0.12, 0.78), or normalized to absolute (1234, 567) @@ -147,6 +158,7 @@ pub async fn stop_cursor_tracking() { async fn init_cursor_tracking_i( mut stop_rx: watch::Receiver, monitor: tauri::Monitor, + handle: AppHandle, ) -> Result<(), String> { println!("Initializing cursor tracking..."); @@ -161,7 +173,7 @@ async fn init_cursor_tracking_i( println!("Cursor event consumer started"); while let Some(positions) = rx.recv().await { - update_cursor_position(positions); + update_cursor_position(&handle, positions); } println!("Cursor event consumer stopped (channel closed)"); }); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 57eee0b..e68b42e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,12 +9,16 @@ mod ufa; mod user; mod windowing; +use tauri::Manager; + async fn launch_app(app: &tauri::App) -> Result<(), Box> { let handle = app.handle(); db::init(handle).await?; keypair::init(handle).await?; network::init(handle).await?; - ufa::init(); + app.manage(ufa::ForegroundAppState::default()); + app.manage(cursor::CursorState::default()); + ufa::init(handle); cursor::init(handle); windowing::init(handle); Ok(()) @@ -68,6 +72,8 @@ fn specta_builder() -> tauri_specta::Builder { remotes::RemotesChanged, profile::ProfileChanged, network::NetworkStatusChanged, + cursor::CursorPositionChanged, + ufa::ForegroundAppChanged, ]) } diff --git a/src-tauri/src/ufa/mod.rs b/src-tauri/src/ufa/mod.rs index c2e33fa..00365c9 100644 --- a/src-tauri/src/ufa/mod.rs +++ b/src-tauri/src/ufa/mod.rs @@ -2,6 +2,9 @@ UFA: User Focused App */ +use std::sync::RwLock; +use tauri::{AppHandle, Manager}; +use tauri_specta::Event; pub use types::*; mod icon_cache; #[cfg(target_os = "macos")] @@ -10,6 +13,15 @@ mod types; #[cfg(target_os = "windows")] mod windows; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type, Event)] +#[serde(rename_all = "camelCase")] +pub struct ForegroundAppChanged { + pub meta: AppMeta, +} + +#[derive(Default)] +pub struct ForegroundAppState(RwLock); + /// 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 init_listener(callback: F) @@ -46,9 +58,17 @@ where /// Initializes the foreground app change listener /// and emits events to the Tauri app on changes. /// Used for app to emit user foreground app to peers. -pub fn init() { - init_listener(|meta: AppMeta| { - // TODO: Emit metadata to the app - println!("EVENT: FOREGROUND APP CHANGED\n({:?})", meta); +pub fn init(handle: &AppHandle) { + let handle = handle.clone(); + + init_listener(move |meta: AppMeta| { + let state = handle.state::(); + let mut current = state.0.write().expect("Foreground App lock failed"); + *current = meta.clone(); + drop(current); + + if let Err(error) = (ForegroundAppChanged { meta }).emit(&handle) { + eprintln!("Failed to emit foreground app change: {error}"); + } }); } diff --git a/src-tauri/src/ufa/types.rs b/src-tauri/src/ufa/types.rs index 130a8d2..ce09b1b 100644 --- a/src-tauri/src/ufa/types.rs +++ b/src-tauri/src/ufa/types.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use specta::Type; pub const ICON_SIZE: u32 = 64; pub const ICON_CACHE_LIMIT: usize = 50; @@ -10,7 +11,7 @@ pub const ICON_CACHE_LIMIT: usize = 50; pub const MAX_ICON_B64_SIZE: usize = 50_000; /// Metadata for the currently active application, including localized and unlocalized names, and an optional base64-encoded icon. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, Type)] #[serde(rename_all = "camelCase")] pub struct AppMeta { pub local: Option, diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index 446d277..2308265 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -50,11 +50,15 @@ async listStatuses() : Promise { export const events = __makeEvents__<{ +cursorPositionChanged: CursorPositionChanged, +foregroundAppChanged: ForegroundAppChanged, friendsChanged: FriendsChanged, networkStatusChanged: NetworkStatusChanged, profileChanged: ProfileChanged, remotesChanged: RemotesChanged }>({ +cursorPositionChanged: "cursor-position-changed", +foregroundAppChanged: "foreground-app-changed", friendsChanged: "friends-changed", networkStatusChanged: "network-status-changed", profileChanged: "profile-changed", @@ -67,8 +71,16 @@ remotesChanged: "remotes-changed" /** user-defined types **/ +/** + * Metadata for the currently active application, including localized and unlocalized names, and an optional base64-encoded icon. + */ +export type AppMeta = { local: string | null; unlocal: string | null; ico: string | null } export type ConnectionState = "connecting" | "connected" | "disconnected" export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState } +export type CursorPosition = { x: number; y: number } +export type CursorPositionChanged = { positions: CursorPositions } +export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition } +export type ForegroundAppChanged = { meta: AppMeta } export type FriendsChanged = { friends: User[] } export type NetworkStatusChanged = { statuses: ConnectionStatus[] } export type ProfileChanged = { profile: User } diff --git a/src/lib/components/live-metadata.svelte b/src/lib/components/live-metadata.svelte new file mode 100644 index 0000000..11b6096 --- /dev/null +++ b/src/lib/components/live-metadata.svelte @@ -0,0 +1,73 @@ + + +
+

Live metadata

+ +

Cursor

+ {#if cursor} +

Raw: {cursor.raw.x}, {cursor.raw.y}

+

Mapped: {cursor.mapped.x}, {cursor.mapped.y}

+ {:else} +

Waiting for cursor data...

+ {/if} + +

Foreground app

+ {#if foregroundApp} + {#if foregroundApp.ico} + + {/if} +

Localized name: {foregroundApp.local ?? "Unavailable"}

+

Executable name: {foregroundApp.unlocal ?? "Unavailable"}

+ {:else} +

Waiting for foreground app data...

+ {/if} +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9dc496d..e4bbe0b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,6 +3,7 @@ import ConnectionStatus from "$lib/components/connection-status.svelte"; import Profile from "$lib/components/profile.svelte"; import RemotesList from "$lib/components/remotes-list.svelte"; + import LiveMetadata from "$lib/components/live-metadata.svelte";
@@ -18,4 +19,7 @@
+
+ +