我会在本地创建一个 JSON 数据中心,然后存储到用户的本地电脑 , 用户不仅仅可以存储快捷键的偏好设置,也可以存储其它任何的用户设置。
然后会使用Tauri 官方的Plugin, Global Shortcut 创建一个快捷键注册单心。
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(())
}