live metadata display in UI

This commit is contained in:
2026-08-07 03:27:40 +08:00
parent 81e03ba152
commit 151ff9e5c6
7 changed files with 148 additions and 20 deletions
+26 -14
View File
@@ -1,27 +1,38 @@
use device_query::{DeviceEvents, DeviceEventsHandler}; use device_query::{DeviceEvents, DeviceEventsHandler};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::RwLock; use std::sync::RwLock;
use std::time::Duration; use std::time::Duration;
use tauri::AppHandle; use tauri::{AppHandle, Manager};
use tauri_specta::Event;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::watch; use tokio::sync::watch;
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CursorPosition { pub struct CursorPosition {
pub x: f64, pub x: f64,
pub y: f64, pub y: f64,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CursorPositions { pub struct CursorPositions {
pub raw: CursorPosition, pub raw: CursorPosition,
pub mapped: 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<CursorPositions>);
// Was private, but for some reason LSP // Was private, but for some reason LSP
// complains even when there's no external references. // complains even when there's no external references.
// Possibly because of `lazy_static!`. // Possibly because of `lazy_static!`.
@@ -33,8 +44,6 @@ pub struct CursorTask {
lazy_static! { lazy_static! {
pub static ref CURSOR_TASK: Mutex<Option<CursorTask>> = Mutex::new(None); pub static ref CURSOR_TASK: Mutex<Option<CursorTask>> = Mutex::new(None);
pub static ref CURSOR_POSITION: RwLock<CursorPositions> =
RwLock::new(CursorPositions::default());
} }
/// Initialize cursor tracking. /// Initialize cursor tracking.
@@ -63,8 +72,9 @@ pub fn init(app: &AppHandle) {
.expect("Failed to resolve primary monitor") .expect("Failed to resolve primary monitor")
.expect("Failed to resolve primary monitor"); .expect("Failed to resolve primary monitor");
let handle = app.clone();
let task = tauri::async_runtime::spawn(async move { 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); println!("Failed to initialize cursor tracking: {}", e);
} }
}); });
@@ -74,15 +84,16 @@ pub fn init(app: &AppHandle) {
} }
#[inline] #[inline]
fn update_cursor_position(state: CursorPositions) { fn update_cursor_position(handle: &AppHandle, positions: CursorPositions) {
let mut guard = CURSOR_POSITION let state = handle.state::<CursorState>();
.write() let mut guard = state.0.write().expect("Cursor Position lock failed");
.expect("Cursor Position lock failed");
*guard = state.clone(); *guard = positions.clone();
drop(guard);
// TODO: Emit metadata to the app if let Err(error) = (CursorPositionChanged { positions }).emit(handle) {
println!("EVENT: CURSOR POSITION CHANGED\n({:?})", state); eprintln!("Failed to emit cursor position change: {error}");
}
} }
/// Convert absolute to normalized coordinates (0.12, 0.78), or normalized to absolute (1234, 567) /// 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( async fn init_cursor_tracking_i(
mut stop_rx: watch::Receiver<bool>, mut stop_rx: watch::Receiver<bool>,
monitor: tauri::Monitor, monitor: tauri::Monitor,
handle: AppHandle,
) -> Result<(), String> { ) -> Result<(), String> {
println!("Initializing cursor tracking..."); println!("Initializing cursor tracking...");
@@ -161,7 +173,7 @@ async fn init_cursor_tracking_i(
println!("Cursor event consumer started"); println!("Cursor event consumer started");
while let Some(positions) = rx.recv().await { while let Some(positions) = rx.recv().await {
update_cursor_position(positions); update_cursor_position(&handle, positions);
} }
println!("Cursor event consumer stopped (channel closed)"); println!("Cursor event consumer stopped (channel closed)");
}); });
+7 -1
View File
@@ -9,12 +9,16 @@ mod ufa;
mod user; mod user;
mod windowing; mod windowing;
use tauri::Manager;
async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> { async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let handle = app.handle(); let handle = app.handle();
db::init(handle).await?; db::init(handle).await?;
keypair::init(handle).await?; keypair::init(handle).await?;
network::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); cursor::init(handle);
windowing::init(handle); windowing::init(handle);
Ok(()) Ok(())
@@ -68,6 +72,8 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
remotes::RemotesChanged, remotes::RemotesChanged,
profile::ProfileChanged, profile::ProfileChanged,
network::NetworkStatusChanged, network::NetworkStatusChanged,
cursor::CursorPositionChanged,
ufa::ForegroundAppChanged,
]) ])
} }
+24 -4
View File
@@ -2,6 +2,9 @@
UFA: User Focused App UFA: User Focused App
*/ */
use std::sync::RwLock;
use tauri::{AppHandle, Manager};
use tauri_specta::Event;
pub use types::*; pub use types::*;
mod icon_cache; mod icon_cache;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -10,6 +13,15 @@ mod types;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod 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<AppMeta>);
/// Listens for changes in the active (foreground) application and calls the provided callback with metadata. /// 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. /// The implementation varies by platform: macOS uses NSWorkspace notifications, Windows uses WinEventHook.
pub fn init_listener<F>(callback: F) pub fn init_listener<F>(callback: F)
@@ -46,9 +58,17 @@ where
/// Initializes the foreground app change listener /// Initializes the foreground app change listener
/// and emits events to the Tauri app on changes. /// and emits events to the Tauri app on changes.
/// Used for app to emit user foreground app to peers. /// Used for app to emit user foreground app to peers.
pub fn init() { pub fn init(handle: &AppHandle) {
init_listener(|meta: AppMeta| { let handle = handle.clone();
// TODO: Emit metadata to the app
println!("EVENT: FOREGROUND APP CHANGED\n({:?})", meta); init_listener(move |meta: AppMeta| {
let state = handle.state::<ForegroundAppState>();
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}");
}
}); });
} }
+2 -1
View File
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
pub const ICON_SIZE: u32 = 64; pub const ICON_SIZE: u32 = 64;
pub const ICON_CACHE_LIMIT: usize = 50; 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; 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. /// 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")] #[serde(rename_all = "camelCase")]
pub struct AppMeta { pub struct AppMeta {
pub local: Option<String>, pub local: Option<String>,
+12
View File
@@ -50,11 +50,15 @@ async listStatuses() : Promise<ConnectionStatus[]> {
export const events = __makeEvents__<{ export const events = __makeEvents__<{
cursorPositionChanged: CursorPositionChanged,
foregroundAppChanged: ForegroundAppChanged,
friendsChanged: FriendsChanged, friendsChanged: FriendsChanged,
networkStatusChanged: NetworkStatusChanged, networkStatusChanged: NetworkStatusChanged,
profileChanged: ProfileChanged, profileChanged: ProfileChanged,
remotesChanged: RemotesChanged remotesChanged: RemotesChanged
}>({ }>({
cursorPositionChanged: "cursor-position-changed",
foregroundAppChanged: "foreground-app-changed",
friendsChanged: "friends-changed", friendsChanged: "friends-changed",
networkStatusChanged: "network-status-changed", networkStatusChanged: "network-status-changed",
profileChanged: "profile-changed", profileChanged: "profile-changed",
@@ -67,8 +71,16 @@ remotesChanged: "remotes-changed"
/** user-defined types **/ /** 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 ConnectionState = "connecting" | "connected" | "disconnected"
export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState } 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 FriendsChanged = { friends: User[] }
export type NetworkStatusChanged = { statuses: ConnectionStatus[] } export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
export type ProfileChanged = { profile: User } export type ProfileChanged = { profile: User }
+73
View File
@@ -0,0 +1,73 @@
<script lang="ts">
import { onMount } from "svelte";
import {
events,
type AppMeta,
type CursorPositions,
} from "$lib/bindings";
let cursor: CursorPositions | null = null;
let foregroundApp: AppMeta | null = null;
onMount(() => {
let disposed = false;
let stopCursor: (() => void) | undefined;
let stopForegroundApp: (() => void) | undefined;
async function initialize() {
const [cursorUnlisten, foregroundAppUnlisten] = await Promise.all([
events.cursorPositionChanged.listen((event) => {
cursor = event.payload.positions;
}),
events.foregroundAppChanged.listen((event) => {
foregroundApp = event.payload.meta;
}),
]);
if (disposed) {
cursorUnlisten();
foregroundAppUnlisten();
return;
}
stopCursor = cursorUnlisten;
stopForegroundApp = foregroundAppUnlisten;
}
initialize();
return () => {
disposed = true;
stopCursor?.();
stopForegroundApp?.();
};
});
</script>
<section>
<h1>Live metadata</h1>
<h2>Cursor</h2>
{#if cursor}
<p>Raw: {cursor.raw.x}, {cursor.raw.y}</p>
<p>Mapped: {cursor.mapped.x}, {cursor.mapped.y}</p>
{:else}
<p>Waiting for cursor data...</p>
{/if}
<h2>Foreground app</h2>
{#if foregroundApp}
{#if foregroundApp.ico}
<img
src={`data:image/png;base64,${foregroundApp.ico}`}
alt=""
width="64"
height="64"
/>
{/if}
<p>Localized name: {foregroundApp.local ?? "Unavailable"}</p>
<p>Executable name: {foregroundApp.unlocal ?? "Unavailable"}</p>
{:else}
<p>Waiting for foreground app data...</p>
{/if}
</section>
+4
View File
@@ -3,6 +3,7 @@
import ConnectionStatus from "$lib/components/connection-status.svelte"; import ConnectionStatus from "$lib/components/connection-status.svelte";
import Profile from "$lib/components/profile.svelte"; import Profile from "$lib/components/profile.svelte";
import RemotesList from "$lib/components/remotes-list.svelte"; import RemotesList from "$lib/components/remotes-list.svelte";
import LiveMetadata from "$lib/components/live-metadata.svelte";
</script> </script>
<main> <main>
@@ -18,4 +19,7 @@
<div> <div>
<ConnectionStatus /> <ConnectionStatus />
</div> </div>
<div>
<LiveMetadata />
</div>
</main> </main>