【 Tauri 】实现快捷键中心

227 阅读6分钟

我会在本地创建一个 JSON 数据中心,然后存储到用户的本地电脑 , 用户不仅仅可以存储快捷键的偏好设置,也可以存储其它任何的用户设置。

然后会使用Tauri 官方的Plugin, Global Shortcut 创建一个快捷键注册单心。

image.png

1. 创建JSON Store

use once_cell::sync::OnceCell;
use serde_json::Value;
use std::sync::Arc;
use tauri::{AppHandle, Wry};
use tauri_plugin_store::{Store, StoreExt};

static STORE: OnceCell<Arc<Store<Wry>>> = OnceCell::new();

pub struct JSONStoreManager;

impl JSONStoreManager {
    pub fn init(app: &AppHandle) {
        let store = app.store("apacgenaiassistant.json").unwrap();
        let _ = STORE.set(Arc::clone(&store));
    }

    pub fn set(key: &str, value: Value) -> Result<(), anyhow::Error> {
        if let Some(store) = STORE.get() {
            store.set(key, value);
            store.save()?;
        }
        Ok(())
    }

    pub fn get(key: &str) -> Option<Value> {
        STORE.get().and_then(|store| store.get(key))
    }

    pub fn has(key: &str) -> bool {
        Self::get(key).is_some()
    }
}

2. 创建快捷键注册中心

use std::str::FromStr;

use tauri::AppHandle;
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutEvent};

pub fn bind_shortcut<F: Fn(&AppHandle, &Shortcut, ShortcutEvent) + Send + Sync + 'static>(
    app: &AppHandle,
    shortcut_key: &str,
    handler: F,
) -> Result<(), anyhow::Error> {
    app.global_shortcut().on_shortcut(shortcut_key, handler)?;
    Ok(())
}

pub fn rebind_shortcut<F>(
    app: &AppHandle,
    shortcut: &String,
    original_shortcut: String,
    handler: F,
) -> Result<(), anyhow::Error>
where
    F: Fn(&AppHandle, &Shortcut, ShortcutEvent) + Send + Sync + 'static,
{
    let new_key = Shortcut::from_str(&shortcut)?;
    let original_key = Shortcut::from_str(&original_shortcut)?;
    app.global_shortcut().unregister(original_key)?;
    app.global_shortcut().on_shortcut(new_key, handler)?;

    Ok(())
}

pub fn unbind_shortcut(app: &AppHandle, shortcut_key: &str) -> Result<(), anyhow::Error> {
    app.global_shortcut()
        .unregister(Shortcut::from_str(shortcut_key)?)?;
    Ok(())
}

pub fn unbind_all_shortcuts(app: &AppHandle) -> Result<(), anyhow::Error> {
    app.global_shortcut().unregister_all()?;
    Ok(())
}

3. 在入口文件使用plugin,初始化JSON Storage 和 初始化 用户快捷键个性化配置,并且绑定handler。

use std::sync::{Arc};

 tauri::Builder::default()
        .plugin(tauri_plugin_updater::Builder::new().build())
        .setup(|app| {
          JSONStoreManager::init(&app.handle());
          let app_handle = Arc::new(app.handle().clone());
          init_preferences(&app_handle);
          Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            short_cut_config::get_shortcut_config,
            short_cut_config::update_shortcut_props,
            short_cut_config::rebind_shortcut,
            short_cut_config::unbind_shortcut,
            short_cut_config::bind_shortcut
        ])

4. 我们打开app 时,需要初始化快捷键, 如果用户已经登录,那么我们可以直接初始化绑定正确的快捷键,如果说用户没有登陆,那么我们快捷键则只需要唤醒app。

use crate::{
    types::preferences::ShortcutConfig,
    utils::{json_store::JSONStoreManager, shortcuts_register::bind_shortcut},
    AppState,
};

use super::{
    polish_text_shortcut::bind_polish_text_shortcut,
    quick_notes_shortcut::bind_quick_notes_shortcut, siri_shortcut::bind_siri_shortcut,
};
use dotenvy_macro::dotenv;
use serde_json::json;
use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{Shortcut, ShortcutEvent};

pub fn init_preferences(app_handle: &AppHandle) {
    if let Err(e) = siri_preference(app_handle) {
        log::error!("Error initializing Siri preference: {:?}", e);
    }
    if let Err(e) = polish_text_preference(app_handle) {
        log::error!("Error initializing polish_text preference: {:?}", e);
    }
    if let Err(e) = quick_notes_preference(app_handle) {
        log::error!("Error initializing polish_text preference: {:?}", e);
    }
}

fn siri_preference(app_handle: &AppHandle) -> Result<(), anyhow::Error> {
    let config = if JSONStoreManager::has("siri_key") {
        // 如果已存在配置,则读取存储的配置
        let stored_value = JSONStoreManager::get("siri_key")
            .ok_or_else(|| anyhow::anyhow!("Failed to get siri_key"))?;
        serde_json::from_value(stored_value)?
    } else {
        // 如果不存在,使用默认配置
        let default_config = ShortcutConfig {
            short_cut: "CmdOrCtrl+T".to_string(),
            props: json!({
                "language": "en-US",
                "voice": "en-US-JennyNeural",
                "api_key": dotenv!("AZURE_SUBSCRIPTION_KEY").to_string(),
                "region": dotenv!("AZURE_REGION").to_string(),
                "enabled":true
            }),
        };
        // 存储默认配置
        JSONStoreManager::set("siri_key", json!(default_config))?;
        default_config
    };

    let state = &*app_handle.state::<AppState>();
    let user_info = state
        .user_info
        .lock()
        .map_err(|e| anyhow::anyhow!("Failed to acquire user_info lock: {}", e))?;

    if user_info.is_none() {
        bind_not_login_shortcuts(&app_handle, config.short_cut)?;
        return Ok(());
    }
    bind_siri_shortcut(&app_handle, config.short_cut)?;
    Ok(())
}

fn polish_text_preference(app_handle: &AppHandle) -> Result<(), anyhow::Error> {
    let config = if JSONStoreManager::has("polish_text_key") {
        // 如果已存在配置,则读取存储的配置
        let stored_value = JSONStoreManager::get("polish_text_key")
            .ok_or_else(|| anyhow::anyhow!("Failed to get polish_text_key"))?;
        serde_json::from_value(stored_value)?
    } else {
        // 如果不存在,使用默认配置
        let default_config = ShortcutConfig {
            short_cut: "CmdOrCtrl+G".to_string(),
            props: json!({
                "prompt": "You are a helpful assistant. Rewrite the text to English.",
                "enabled":true
            }),
        };
        JSONStoreManager::set("polish_text_key", json!(default_config))?;
        default_config
    };
    let state = &*app_handle.state::<AppState>();
    let user_info = state
        .user_info
        .lock()
        .map_err(|e| anyhow::anyhow!("Failed to acquire user_info lock: {}", e))?;

    if user_info.is_none() {
        bind_not_login_shortcuts(&app_handle, config.short_cut)?;
        return Ok(());
    }

    // 绑定快捷键
    bind_polish_text_shortcut(&app_handle, config.short_cut)?;
    Ok(())
}

fn quick_notes_preference(app_handle: &AppHandle) -> Result<(), anyhow::Error> {
    let config = if JSONStoreManager::has("quick_notes_key") {
        // 如果已存在配置,则读取存储的配置
        let stored_value = JSONStoreManager::get("quick_notes_key")
            .ok_or_else(|| anyhow::anyhow!("Failed to get quick_notes_key"))?;
        serde_json::from_value(stored_value)?
    } else {
        // 如果不存在,使用默认配置
        let default_config = ShortcutConfig {
            short_cut: "CmdOrCtrl+B".to_string(),
            props: json!({
                "enabled":true
            }),
        };
        JSONStoreManager::set("quick_notes_key", json!(default_config))?;
        default_config
    };
    let state = &*app_handle.state::<AppState>();
    let user_info = state
        .user_info
        .lock()
        .map_err(|e| anyhow::anyhow!("Failed to acquire user_info lock: {}", e))?;

    if user_info.is_none() {
        bind_not_login_shortcuts(&app_handle, config.short_cut)?;
        return Ok(());
    }

    // 绑定快捷键
    bind_quick_notes_shortcut(&app_handle, config.short_cut)?;
    Ok(())
}

pub fn bind_not_login_shortcuts(
    app_handle: &AppHandle,
    shortcut_key: String,
) -> Result<(), anyhow::Error> {
    // 绑定快捷键,闭包需要接收三个参数
    bind_shortcut(
        app_handle,
        shortcut_key.as_str(),
        |app: &AppHandle, _shortcut: &Shortcut, _event: ShortcutEvent| {
            // 尝试聚焦到主窗口
            if let Some(main) = app.get_webview_window("main") {
                if let Err(e) = main.set_focus() {
                    log::error!("Failed to set focus to 'main' window: {}", e);
                }
            } else {
                log::error!("No 'main' window found");
            }
        },
    )?;

    Ok(())
}

5. handlers 层,前端调用时,根据不同的key去执行不同的快捷键service

use serde_json::{json, Value};
use tauri::AppHandle;

use crate::{
    services::{
        polish_text_shortcut::{
            bind_polish_text_shortcut, rebind_polish_text_shortcut, unbind_polish_text_shortcut,
        },
        quick_notes_shortcut::{
            bind_quick_notes_shortcut, rebind_quick_notes_shortcut, unbind_quick_notes_shortcut,
        },
        siri_shortcut::{bind_siri_shortcut, rebind_siri_shortcut, unbind_siri_shortcut},
    },
    types::preferences::ShortcutConfig,
    utils::json_store::JSONStoreManager,
};

#[tauri::command]
pub async fn get_shortcut_config(key: &str) -> Result<Option<ShortcutConfig>, String> {
    let value = JSONStoreManager::get(key);
    log::info!("value: {:?}", value);
    Ok(value.and_then(|v| serde_json::from_value(v).ok()))
}

#[tauri::command]
pub async fn update_shortcut_props(key: String, props: Value) -> Result<(), String> {
    let mut config: ShortcutConfig = JSONStoreManager::get(&key)
        .and_then(|v| serde_json::from_value(v).ok())
        .ok_or("Config not found")?;

    config.props = props;
    JSONStoreManager::set(&key, json!(config)).map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn rebind_shortcut(
    key: String,
    shortcut_key: String,
    original_shortcut: String,
    app_handle: AppHandle,
) -> Result<(), String> {
    if key == "siri_key" {
        rebind_siri_shortcut(&app_handle, shortcut_key, original_shortcut)
            .map_err(|e| e.to_string())
    } else if key == "polish_text_key" {
        rebind_polish_text_shortcut(&app_handle, shortcut_key, original_shortcut)
            .map_err(|e| e.to_string())
    } else if key == "quick_notes_key" {
        rebind_quick_notes_shortcut(&app_handle, shortcut_key, original_shortcut)
            .map_err(|e| e.to_string())
    } else {
        Err("Invalid key".to_string())
    }
}

// 解绑快捷键
#[tauri::command]
pub async fn unbind_shortcut(
    key: String,
    shortcut_key: String,
    app_handle: AppHandle,
) -> Result<(), String> {
    if key == "siri_key" {
        unbind_siri_shortcut(&app_handle, shortcut_key).map_err(|e| e.to_string())
    } else if key == "polish_text_key" {
        unbind_polish_text_shortcut(&app_handle, shortcut_key).map_err(|e| e.to_string())
    } else if key == "quick_notes_key" {
        unbind_quick_notes_shortcut(&app_handle, shortcut_key).map_err(|e| e.to_string())
    } else {
        Err("Invalid key".to_string())
    }
}

#[tauri::command]
pub async fn bind_shortcut(
    key: String,
    shortcut_key: String,
    app_handle: AppHandle,
) -> Result<(), String> {
    if key == "siri_key" {
        bind_siri_shortcut(&app_handle, shortcut_key).map_err(|e| e.to_string())
    } else if key == "polish_text_key" {
        bind_polish_text_shortcut(&app_handle, shortcut_key).map_err(|e| e.to_string())
    } else if key == "quick_notes_key" {
        bind_quick_notes_shortcut(&app_handle, shortcut_key).map_err(|e| e.to_string())
    } else {
        Err("Invalid key".to_string())
    }
}

6. 此处拿siri shortcut service 举例。

use std::{fs::File, io::BufReader};

use rodio::{Decoder, OutputStream, Sink};
use serde_json::json;
use tauri::{path::BaseDirectory, AppHandle, Emitter, Manager, PhysicalPosition, Position};
use tauri_plugin_global_shortcut::{Shortcut, ShortcutEvent, ShortcutState};

use crate::{
    types::preferences::ShortcutConfig,
    utils::{
        json_store::JSONStoreManager,
        shortcuts_register::{bind_shortcut, rebind_shortcut, unbind_shortcut},
    },
};

pub fn bind_siri_shortcut(app: &AppHandle, shortcut_key: String) -> Result<(), anyhow::Error> {
    bind_shortcut(app, shortcut_key.as_str(), siri_handler)?;
    Ok(())
}

pub fn rebind_siri_shortcut(
    app: &AppHandle,
    shortcut_key: String,
    original_shortcut: String,
) -> Result<(), anyhow::Error> {
    rebind_shortcut(app, &shortcut_key, original_shortcut, siri_handler)?;

    if let Some(config) = JSONStoreManager::get("siri_key") {
        let shortcut_config = ShortcutConfig {
            short_cut: shortcut_key,
            props: config.get("props").cloned().unwrap_or(json!({})),
        };
        JSONStoreManager::set("siri_key", json!(shortcut_config))?;
    }

    Ok(())
}

pub fn unbind_siri_shortcut(app: &AppHandle, shortcut_key: String) -> Result<(), anyhow::Error> {
    unbind_shortcut(app, shortcut_key.as_str())?;
    Ok(())
}

fn siri_handler(app: &AppHandle, _shortcut: &Shortcut, event: ShortcutEvent) {
    if event.state() == ShortcutState::Released {
        return;
    }

    let voice_bubble = match app.get_webview_window("voice_bubble") {
        Some(window) => window,
        None => {
            log::error!("Failed to find 'voice_bubble' window");
            return;
        }
    };

    let monitor = match voice_bubble.current_monitor() {
        Ok(Some(monitor)) => monitor,
        Ok(None) => {
            log::error!("No monitor found");
            return;
        }
        Err(e) => {
            log::error!("Failed to get current monitor: {}", e);
            return;
        }
    };

    let screen_size = monitor.size();
    let window_size = match voice_bubble.inner_size() {
        Ok(size) => size,
        Err(e) => {
            log::error!("Failed to get window size: {}", e);
            return;
        }
    };

    let margin_right = 50.0; // 与右边的距离
    let margin_top = 80.0; // 与顶部的距离
    let x = screen_size.width as f64 - window_size.width as f64 - margin_right;
    let y = margin_top;
    let primary_position = monitor.position();

    if let Err(err) = voice_bubble.set_position(Position::Physical(PhysicalPosition {
        x: (primary_position.x as f64 + x) as i32,
        y: (primary_position.y as f64 + y) as i32,
    })) {
        log::error!("Failed to set Siri position: {:?}", err);
    }

    if let Err(err) = voice_bubble.set_always_on_top(true) {
        log::error!("Failed to set Siri to always be on top: {:?}", err);
    }

    match voice_bubble.show() {
        Ok(_) => {
            // 在新线程中播放音频,避免阻塞主线程
            let app_handle = app.clone();
            std::thread::spawn(move || {
                if let Err(e) = play_activation_sound(&app_handle) {
                    log::error!("无法播放激活提示音: {}", e);
                }
            });
        }
        Err(e) => {
            log::error!("Failed to show Siri window: {:?}", e);
        }
    }
    if let Err(e) = app.emit_to("voice_bubble", "voice_bubble_show", ()) {
        log::error!("Failed to emit 'voice_bubble_show' event: {}", e);
    }
}

fn play_activation_sound(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
    // 获取音频文件路径
    let audio_path = app
        .path()
        .resolve("src/libs/resources/siri_start.mp3", BaseDirectory::Resource)?;

    // 创建音频输出流
    let (_stream, stream_handle) = OutputStream::try_default()?;
    let sink = Sink::try_new(&stream_handle)?;

    // 读取音频文件
    let file = File::open(audio_path)?;
    let reader = BufReader::new(file);
    let source = Decoder::new(reader)?;

    // 播放音频
    sink.append(source);
    sink.sleep_until_end();

    Ok(())
}