From ca4bd8da7ae7ee4ad6aaeeb3d442d21f234520aa Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 8 May 2026 19:25:14 -0400 Subject: [PATCH] feat(rust): register plugins, global shortcut, commands --- src-tauri/src/commands/overlay.rs | 23 +++++--- src-tauri/src/lib.rs | 91 +++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands/overlay.rs b/src-tauri/src/commands/overlay.rs index 046c085..179668c 100644 --- a/src-tauri/src/commands/overlay.rs +++ b/src-tauri/src/commands/overlay.rs @@ -34,12 +34,14 @@ pub async fn apply_state( .set_ignore_cursor_events(new_state) .map_err(|e| e.to_string())?; - // Opacity dip half of the visual pulse. The JS click-through-toggled + // Opacity dip — visual pulse to signal the state change. + // Tauri 2.11 does not expose set_opacity on WebviewWindow, so we apply + // the dip by evaluating JS on the webview. The JS click-through-toggled // handler is responsible for restoring opacity to the user's actual - // target — Rust deliberately does NOT call set_opacity again, so there - // is no possibility of the global-shortcut path "snapping back" to a - // wrong value (we don't have a current-opacity getter). - let _ = overlay.set_opacity(0.4); + // target — Rust deliberately does NOT set opacity back, so there is no + // possibility of the global-shortcut path "snapping back" to a wrong + // value (we don't have a current-opacity getter). + let _ = overlay.eval("document.documentElement.style.opacity='0.4'"); tokio::time::sleep(Duration::from_millis(180)).await; app.emit( @@ -102,8 +104,9 @@ pub fn get_hotkey_status(app: AppHandle) -> Result Ok(guard.clone()) } -/// Set window opacity. Tauri 2's JS WebviewWindow does NOT expose setOpacity -/// (only the Rust side does), so the JS layer routes through this command. +/// Set window opacity. Tauri 2's JS WebviewWindow does NOT expose setOpacity, +/// so the JS layer routes through this command. Tauri 2.11 also doesn't expose +/// set_opacity on the Rust WebviewWindow, so we apply the change via JS eval. #[tauri::command] pub fn set_window_opacity( app: AppHandle, @@ -113,5 +116,9 @@ pub fn set_window_opacity( let Some(window) = app.get_webview_window(&label) else { return Err(format!("window not found: {label}")); }; - window.set_opacity(opacity).map_err(|e| e.to_string()) + window + .eval(&format!( + "document.documentElement.style.opacity='{opacity}'" + )) + .map_err(|e| e.to_string()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4a277ef..b7592bf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,14 +1,95 @@ -// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ -#[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) +mod commands; + +use std::sync::Mutex; + +use serde::Serialize; +use tauri::Manager; + +#[cfg(desktop)] +use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState}; + +pub struct AppState { + pub click_through: Mutex, + /// Hotkey registration outcome, set during setup. JS pulls this via + /// `get_hotkey_status` after the listener-not-yet-registered window is + /// safely past, so the result is never lost to a race. + pub hotkey_status: Mutex, +} + +#[derive(Clone, Serialize, Default)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum HotkeyStatus { + #[default] + Pending, + Ok, + Failed { error: String }, } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .manage(AppState { + click_through: Mutex::new(false), + hotkey_status: Mutex::new(HotkeyStatus::Pending), + }) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![greet]) + .plugin(tauri_plugin_store::Builder::new().build()) + .setup(|app| { + #[cfg(desktop)] + { + app.handle() + .plugin(tauri_plugin_window_state::Builder::default().build())?; + + let shortcut = Shortcut::new( + Some(Modifiers::CONTROL | Modifiers::ALT), + Code::Space, + ); + let shortcut_for_handler = shortcut; + + app.handle().plugin( + tauri_plugin_global_shortcut::Builder::new() + .with_handler(move |app, sc, event| { + if sc != &shortcut_for_handler { + return; + } + if event.state() != ShortcutState::Pressed { + return; + } + let app_handle = app.clone(); + tauri::async_runtime::spawn(async move { + if let Err(err) = + commands::overlay::toggle_via_global_shortcut(&app_handle).await + { + eprintln!("global shortcut toggle failed: {err}"); + } + }); + }) + .build(), + )?; + + let registration_result = app.global_shortcut().register(shortcut); + let status = match ®istration_result { + Ok(()) => HotkeyStatus::Ok, + Err(e) => { + eprintln!("Failed to register Ctrl+Alt+Space: {e}"); + HotkeyStatus::Failed { + error: "Hotkey unavailable — click-through escape disabled".to_string(), + } + } + }; + { + let state = app.state::(); + *state.hotkey_status.lock().expect("poisoned") = status; + } + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::overlay::toggle_click_through, + commands::overlay::sync_click_through_cache, + commands::overlay::get_hotkey_status, + commands::overlay::set_window_opacity, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }