feat(rust): register plugins, global shortcut, commands

This commit is contained in:
Michael Chihlas
2026-05-08 19:25:14 -04:00
parent c1085e073a
commit ca4bd8da7a
2 changed files with 101 additions and 13 deletions

View File

@@ -34,12 +34,14 @@ pub async fn apply_state<R: Runtime>(
.set_ignore_cursor_events(new_state) .set_ignore_cursor_events(new_state)
.map_err(|e| e.to_string())?; .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 // handler is responsible for restoring opacity to the user's actual
// target — Rust deliberately does NOT call set_opacity again, so there // target — Rust deliberately does NOT set opacity back, so there is no
// is no possibility of the global-shortcut path "snapping back" to a // possibility of the global-shortcut path "snapping back" to a wrong
// wrong value (we don't have a current-opacity getter). // value (we don't have a current-opacity getter).
let _ = overlay.set_opacity(0.4); let _ = overlay.eval("document.documentElement.style.opacity='0.4'");
tokio::time::sleep(Duration::from_millis(180)).await; tokio::time::sleep(Duration::from_millis(180)).await;
app.emit( app.emit(
@@ -102,8 +104,9 @@ pub fn get_hotkey_status(app: AppHandle) -> Result<crate::HotkeyStatus, String>
Ok(guard.clone()) Ok(guard.clone())
} }
/// Set window opacity. Tauri 2's JS WebviewWindow does NOT expose setOpacity /// 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. /// 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] #[tauri::command]
pub fn set_window_opacity<R: Runtime>( pub fn set_window_opacity<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
@@ -113,5 +116,9 @@ pub fn set_window_opacity<R: Runtime>(
let Some(window) = app.get_webview_window(&label) else { let Some(window) = app.get_webview_window(&label) else {
return Err(format!("window not found: {label}")); 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())
} }

View File

@@ -1,14 +1,95 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ mod commands;
#[tauri::command]
fn greet(name: &str) -> String { use std::sync::Mutex;
format!("Hello, {}! You've been greeted from Rust!", name)
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<bool>,
/// 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<HotkeyStatus>,
}
#[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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.manage(AppState {
click_through: Mutex::new(false),
hotkey_status: Mutex::new(HotkeyStatus::Pending),
})
.plugin(tauri_plugin_opener::init()) .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 &registration_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::<AppState>();
*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!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }