agent: wayland — evdev hotkey backend, download resume, SHA256 integrity
Add kon-hotkey crate with evdev-based global hotkey capture that works on Wayland (and X11). Patterns from whisper-overlay: per-device async listeners, inotify hotplug with udev permission retry, watch channel for live config updates. Frontend detects Wayland at startup and selects evdev or tauri-plugin-global-shortcut automatically. Model downloads now support HTTP Range resume for interrupted downloads and optional SHA256 integrity verification (incremental, no second pass). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,29 +41,89 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Global hotkey registration
|
||||
// Global hotkey registration — dual backend
|
||||
// Wayland: evdev via kon-hotkey crate (works without display server)
|
||||
// X11/macOS/Windows: tauri-plugin-global-shortcut (native)
|
||||
let registeredHotkey = null;
|
||||
let hotkeyBackend = $state("unknown"); // "evdev" | "tauri" | "unavailable"
|
||||
|
||||
async function initHotkeyBackend() {
|
||||
try {
|
||||
const isWayland = await invoke("is_wayland_session");
|
||||
if (isWayland) {
|
||||
// Try evdev backend first (Wayland-compatible)
|
||||
try {
|
||||
await invoke("check_hotkey_access");
|
||||
hotkeyBackend = "evdev";
|
||||
console.log("Hotkey backend: evdev (Wayland)");
|
||||
} catch (err) {
|
||||
console.warn("evdev hotkey access denied:", err);
|
||||
console.warn("Add your user to the 'input' group for global hotkeys on Wayland");
|
||||
hotkeyBackend = "unavailable";
|
||||
}
|
||||
} else {
|
||||
hotkeyBackend = "tauri";
|
||||
console.log("Hotkey backend: tauri-plugin-global-shortcut (X11)");
|
||||
}
|
||||
} catch {
|
||||
// Fallback to tauri plugin if detection fails
|
||||
hotkeyBackend = "tauri";
|
||||
}
|
||||
}
|
||||
|
||||
async function registerGlobalHotkey(hotkey) {
|
||||
if (hotkeyBackend === "unknown") return; // not yet initialised
|
||||
|
||||
try {
|
||||
const mod = await import("@tauri-apps/plugin-global-shortcut");
|
||||
if (registeredHotkey) {
|
||||
await mod.unregister(registeredHotkey).catch(() => {});
|
||||
}
|
||||
await mod.register(hotkey, () => {
|
||||
if (page.current !== "dictation") page.current = "dictation";
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new CustomEvent("kon:toggle-recording"));
|
||||
if (hotkeyBackend === "evdev") {
|
||||
// evdev backend: start or update the Rust-side listener
|
||||
if (registeredHotkey) {
|
||||
await invoke("update_evdev_hotkey", { hotkey });
|
||||
} else {
|
||||
await invoke("start_evdev_hotkey", { hotkey });
|
||||
}
|
||||
registeredHotkey = hotkey;
|
||||
} else if (hotkeyBackend === "tauri") {
|
||||
// Tauri plugin backend (X11/macOS/Windows)
|
||||
const mod = await import("@tauri-apps/plugin-global-shortcut");
|
||||
if (registeredHotkey) {
|
||||
await mod.unregister(registeredHotkey).catch(() => {});
|
||||
}
|
||||
await mod.register(hotkey, () => {
|
||||
if (page.current !== "dictation") page.current = "dictation";
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new CustomEvent("kon:toggle-recording"));
|
||||
});
|
||||
});
|
||||
});
|
||||
registeredHotkey = hotkey;
|
||||
registeredHotkey = hotkey;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Hotkey registration failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for evdev hotkey events from the Rust backend
|
||||
let unlistenEvdev = null;
|
||||
async function setupEvdevListener() {
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
unlistenEvdev = await listen("kon:hotkey-pressed", () => {
|
||||
if (page.current !== "dictation") page.current = "dictation";
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new CustomEvent("kon:toggle-recording"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
registerGlobalHotkey(settings.globalHotkey);
|
||||
if (hotkeyBackend === "evdev" && !unlistenEvdev) {
|
||||
setupEvdevListener();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (hotkeyBackend !== "unknown") {
|
||||
registerGlobalHotkey(settings.globalHotkey);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply font size setting as CSS variable (legacy — kept for backwards compat)
|
||||
@@ -100,6 +160,9 @@
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Detect and initialise the correct hotkey backend
|
||||
await initHotkeyBackend();
|
||||
|
||||
try {
|
||||
const whisper = await invoke("list_models");
|
||||
const parakeet = await invoke("list_parakeet_models");
|
||||
@@ -113,11 +176,16 @@
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (registeredHotkey) {
|
||||
if (hotkeyBackend === "evdev") {
|
||||
invoke("stop_evdev_hotkey").catch(() => {});
|
||||
} else if (hotkeyBackend === "tauri" && registeredHotkey) {
|
||||
import("@tauri-apps/plugin-global-shortcut")
|
||||
.then((mod) => mod.unregister(registeredHotkey))
|
||||
.catch(() => {});
|
||||
}
|
||||
if (unlistenEvdev) {
|
||||
unlistenEvdev();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user