2026-09-22 21:46:01 +03:00
|
|
|
//! Глобальные горячие клавиши (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;
|
|
|
|
|
|
2026-09-22 23:17:27 +03:00
|
|
|
/// Палитра команд (docs/client-tauri.md §5): комбинация не настраивается —
|
|
|
|
|
/// это системный жест «быстрый переход», одинаковый во всех сборках.
|
|
|
|
|
const PALETTE_SHORTCUT: &str = "CommandOrControl+K";
|
|
|
|
|
|
2026-09-22 21:46:01 +03:00
|
|
|
/// Обработчик нажатий: плагин вызывает его для любой зарегистрированной комбинации.
|
2026-09-22 21:55:19 +03:00
|
|
|
pub fn on_shortcut<R: Runtime>(
|
|
|
|
|
app: &AppHandle<R>,
|
|
|
|
|
shortcut: &Shortcut,
|
|
|
|
|
event: tauri_plugin_global_shortcut::ShortcutEvent,
|
|
|
|
|
) {
|
2026-09-22 21:46:01 +03:00
|
|
|
let settings = app.state::<AppState>().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);
|
2026-09-22 23:17:27 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Быстрый переход: окно поднимаем и просим клиент открыть палитру команд.
|
|
|
|
|
if parse(PALETTE_SHORTCUT).as_ref() == Some(shortcut) {
|
|
|
|
|
crate::window::show_main(app);
|
|
|
|
|
bridge::send(app, &WebMessage::OpenPalette);
|
2026-09-22 21:46:01 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Перерегистрирует комбинации после изменения настроек.
|
|
|
|
|
pub fn apply<R: Runtime>(app: &AppHandle<R>, settings: &Settings) -> Result<(), String> {
|
|
|
|
|
let shortcuts = app.global_shortcut();
|
|
|
|
|
shortcuts
|
|
|
|
|
.unregister_all()
|
|
|
|
|
.map_err(|error| format!("не удалось снять старые шорткаты: {error}"))?;
|
|
|
|
|
|
2026-09-22 23:17:27 +03:00
|
|
|
// Палитра команд регистрируется всегда и последней: если пользователь занял
|
|
|
|
|
// её комбинацию своим действием, второй раз её регистрировать нельзя —
|
|
|
|
|
// сработает то действие, чья проверка в `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<Shortcut> = 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) {
|
2026-09-22 21:46:01 +03:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
shortcuts
|
2026-09-22 23:17:27 +03:00
|
|
|
.register(trimmed)
|
|
|
|
|
.map_err(|error| format!("комбинация «{trimmed}» не зарегистрирована: {error}"))?;
|
|
|
|
|
registered.push(parsed);
|
2026-09-22 21:46:01 +03:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Разбирает строку комбинации; пустая строка и мусор дают `None`.
|
|
|
|
|
fn parse(spec: &str) -> Option<Shortcut> {
|
|
|
|
|
let trimmed = spec.trim();
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
trimmed.parse::<Shortcut>().ok()
|
|
|
|
|
}
|