live metadata display in UI
This commit is contained in:
+26
-14
@@ -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<CursorPositions>);
|
||||
|
||||
// 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<Option<CursorTask>> = Mutex::new(None);
|
||||
pub static ref CURSOR_POSITION: RwLock<CursorPositions> =
|
||||
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::<CursorState>();
|
||||
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<bool>,
|
||||
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)");
|
||||
});
|
||||
|
||||
@@ -9,12 +9,16 @@ mod ufa;
|
||||
mod user;
|
||||
mod windowing;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<tauri::Wry> {
|
||||
remotes::RemotesChanged,
|
||||
profile::ProfileChanged,
|
||||
network::NetworkStatusChanged,
|
||||
cursor::CursorPositionChanged,
|
||||
ufa::ForegroundAppChanged,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AppMeta>);
|
||||
|
||||
/// 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<F>(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::<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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
|
||||
@@ -50,11 +50,15 @@ async listStatuses() : Promise<ConnectionStatus[]> {
|
||||
|
||||
|
||||
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 }
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
</script>
|
||||
|
||||
<main>
|
||||
@@ -18,4 +19,7 @@
|
||||
<div>
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
<div>
|
||||
<LiveMetadata />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user