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:
2026-04-03 17:50:48 +01:00
parent 1933604176
commit 8e70cf9ff9
12 changed files with 804 additions and 18 deletions

View File

@@ -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>