//! Глобальные горячие клавиши (docs/client-tauri.md §5). //! //! Push-to-talk работает при неактивном окне: системный перехват ловит нажатие //! и отпускание выбранной комбинации и передаёт состояние в веб-клиент, а тот //! включает микрофон (в самом клиенте должен быть выбран режим «Push-to-talk»). //! Дополнительно доступны переключение микрофона и «не слышу». use tauri::{AppHandle, Manager, Runtime}; use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; use crate::bridge::{self, WebMessage}; use crate::settings::Settings; use crate::state::AppState; /// Палитра команд (docs/client-tauri.md §5): комбинация не настраивается — /// это системный жест «быстрый переход», одинаковый во всех сборках. const PALETTE_SHORTCUT: &str = "CommandOrControl+K"; /// Обработчик нажатий: плагин вызывает его для любой зарегистрированной комбинации. pub fn on_shortcut( app: &AppHandle, shortcut: &Shortcut, event: tauri_plugin_global_shortcut::ShortcutEvent, ) { let settings = app.state::().settings(); let pressed = event.state == ShortcutState::Pressed; let ptt = parse(&settings.ptt_shortcut); if ptt.as_ref() == Some(shortcut) { bridge::send(app, &WebMessage::Ptt { active: pressed }); return; } // Остальные шорткаты срабатывают по нажатию, чтобы не дублировать действие. if !pressed { return; } if parse(&settings.mute_shortcut).as_ref() == Some(shortcut) { bridge::send(app, &WebMessage::ToggleMic); return; } if parse(&settings.deafen_shortcut).as_ref() == Some(shortcut) { bridge::send(app, &WebMessage::ToggleDeafen); return; } // Быстрый переход: окно поднимаем и просим клиент открыть палитру команд. if parse(PALETTE_SHORTCUT).as_ref() == Some(shortcut) { crate::window::show_main(app); bridge::send(app, &WebMessage::OpenPalette); } } /// Перерегистрирует комбинации после изменения настроек. pub fn apply(app: &AppHandle, settings: &Settings) -> Result<(), String> { let shortcuts = app.global_shortcut(); shortcuts .unregister_all() .map_err(|error| format!("не удалось снять старые шорткаты: {error}"))?; // Палитра команд регистрируется всегда и последней: если пользователь занял // её комбинацию своим действием, второй раз её регистрировать нельзя — // сработает то действие, чья проверка в `on_shortcut` стоит раньше. let wanted = [ settings.ptt_shortcut.as_str(), settings.mute_shortcut.as_str(), settings.deafen_shortcut.as_str(), PALETTE_SHORTCUT, ]; // Повторы отсекаются по разобранной комбинации: «Cmd+K» и «Command+K» — // одна и та же клавиша, и вторая регистрация вернула бы ошибку. let mut registered: Vec = Vec::new(); for spec in wanted { let trimmed = spec.trim(); if trimmed.is_empty() { continue; } let Some(parsed) = parse(trimmed) else { continue; }; if registered.contains(&parsed) { continue; } shortcuts .register(trimmed) .map_err(|error| format!("комбинация «{trimmed}» не зарегистрирована: {error}"))?; registered.push(parsed); } Ok(()) } /// Разбирает строку комбинации; пустая строка и мусор дают `None`. fn parse(spec: &str) -> Option { let trimmed = spec.trim(); if trimmed.is_empty() { return None; } trimmed.parse::().ok() }