前言
在现代桌面应用中,快捷键是提高用户体验和工作效率的重要工具。许多应用程序都允许用户通过快捷键执行某些操作,Tauri 作为一个跨平台的桌面应用框架,也提供了丰富的功能来支持全局快捷键的实现。
本文将介绍如何在 Tauri 中实现全局快捷键功能,带领你一步步创建一个支持全局快捷键的桌面应用。
安装依赖
pnpm tauri add global-shortcut
pnpm tauri add store
安装完之后,在前端部分就能看到 @tauri-apps/plugin-global-shortcut,前端就可以类似的引入使用了:
import { register } from '@tauri-apps/plugin-global-shortcut';
// when using `"withGlobalTauri": true`, you may use
// const { register } = window.__TAURI__.globalShortcut;
await register('CommandOrControl+Shift+C', () => {
console.log('Shortcut triggered');
});
安装完之后,在Rust 部分就能看到 tauri-plugin-global-shortcut,Rsut 也可以使用了:
pub fn run() {
tauri::Builder::default()
.setup(|app| {
#[cfg(desktop)]
{
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
let ctrl_n_shortcut = Shortcut::new(Some(Modifiers::CONTROL), Code::KeyN);
app.handle().plugin(
tauri_plugin_global_shortcut::Builder::new().with_handler(move |_app, shortcut, event| {
println!("{:?}", shortcut);
if shortcut == &ctrl_n_shortcut {
match event.state() {
ShortcutState::Pressed => {
println!("Ctrl-N Pressed!");
}
ShortcutState::Released => {
println!("Ctrl-N Released!");
}
}
}
})
.build(),
)?;
app.global_shortcut().register(ctrl_n_shortcut)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
权限配置
在 src-tauri/capabilities/default.json 文件中添加:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
]
}
实现全局快捷键
在 src-tauri/src 目录下创建 shortcut.rs 文件:
use tauri::App;
use tauri::AppHandle;
use tauri::Manager;
use tauri::Runtime;
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::Shortcut;
use tauri_plugin_global_shortcut::ShortcutState;
use tauri_plugin_store::JsonValue;
use tauri_plugin_store::StoreExt;
/// Tauri 存储的名称
const COCO_TAURI_STORE: &str = "coco_tauri_store";
/// 用来存储全局快捷键的键值
const COCO_GLOBAL_SHORTCUT: &str = "coco_global_shortcut";
/// macOS 默认的快捷键
#[cfg(target_os = "macos")]
const DEFAULT_SHORTCUT: &str = "command+shift+space";
/// Windows 和 Linux 默认的快捷键
#[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SHORTCUT: &str = "ctrl+shift+space";
/// 在应用启动时设置快捷键
pub fn enable_shortcut(app: &App) {
let store = app
.store(COCO_TAURI_STORE)
.expect("创建存储时不应该失败");
// 如果存在存储的快捷键,使用它
if let Some(stored_shortcut) = store.get(COCO_GLOBAL_SHORTCUT) {
let stored_shortcut_str = match stored_shortcut {
JsonValue::String(str) => str,
unexpected_type => panic!(
"COCO 快捷键应存储为字符串,发现类型: {} ",
unexpected_type
),
};
let stored_shortcut = stored_shortcut_str
.parse::<Shortcut>()
.expect("存储的快捷键字符串应该有效");
_register_shortcut_upon_start(app, stored_shortcut); // 注册存储的快捷键
} else {
// 如果没有存储快捷键,使用默认快捷键
store.set(
COCO_GLOBAL_SHORTCUT,
JsonValue::String(DEFAULT_SHORTCUT.to_string()),
);
let default_shortcut = DEFAULT_SHORTCUT
.parse::<Shortcut>()
.expect("默认快捷键应该有效");
_register_shortcut_upon_start(app, default_shortcut); // 注册默认快捷键
}
}
/// 获取当前存储的快捷键,作为字符串返回
#[tauri::command]
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
let shortcut = _get_shortcut(&app);
Ok(shortcut)
}
/// 获取当前快捷键并在 Tauri 端取消注册
#[tauri::command]
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
let shortcut_str = _get_shortcut(&app);
let shortcut = shortcut_str
.parse::<Shortcut>()
.expect("存储的快捷键字符串应该有效");
// 取消注册快捷键
app.global_shortcut()
.unregister(shortcut)
.expect("取消注册快捷键失败")
}
/// 更改全局快捷键
#[tauri::command]
pub fn change_shortcut<R: Runtime>(
app: AppHandle<R>,
_window: tauri::Window<R>,
key: String,
) -> Result<(), String> {
println!("按键: {}", key);
let shortcut = match key.parse::<Shortcut>() {
Ok(shortcut) => shortcut,
Err(_) => return Err(format!("无效的快捷键 {}", key)),
};
// 存储新的快捷键
let store = app
.get_store(COCO_TAURI_STORE)
.expect("存储应该已经加载或创建");
store.set(COCO_GLOBAL_SHORTCUT, JsonValue::String(key));
// 注册新的快捷键
_register_shortcut(&app, shortcut);
Ok(())
}
/// 注册快捷键的辅助函数,主要用于更新快捷键
fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
let main_window = app.get_webview_window("main").unwrap();
// 注册全局快捷键,按下快捷键时执行指定操作
app.global_shortcut()
.on_shortcut(shortcut, move |_app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
// 判断窗口是否可见,进行切换显示状态
if main_window.is_visible().unwrap() {
main_window.hide().unwrap(); // 隐藏窗口
} else {
main_window.show().unwrap(); // 显示窗口
main_window.set_focus().unwrap(); // 设置窗口焦点
}
}
}
})
.map_err(|err| format!("注册新的快捷键失败 '{}'", err))
.unwrap();
}
/// 注册快捷键的辅助函数,在应用启动时设置快捷键
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
let window = app.get_webview_window("main").unwrap();
// 初始化全局快捷键并设置快捷键事件处理器
app.handle()
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |_app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
// 判断窗口是否可见,进行切换显示状态
if window.is_visible().unwrap() {
window.hide().unwrap(); // 隐藏窗口
} else {
window.show().unwrap(); // 显示窗口
window.set_focus().unwrap(); // 设置窗口焦点
}
}
}
})
.build(),
)
.unwrap();
app.global_shortcut().register(shortcut).unwrap(); // 注册全局快捷键
}
/// 获取存储的全局快捷键,返回为字符串格式
pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
let store = app
.get_store(COCO_TAURI_STORE)
.expect("存储应该已经加载或创建");
match store
.get(COCO_GLOBAL_SHORTCUT)
.expect("快捷键应已存储")
{
JsonValue::String(str) => str,
unexpected_type => panic!(
"COCO 快捷键应该存储为字符串,发现类型: {} ",
unexpected_type
),
}
}
在 src-tauri/src/lib.rs 文件中引入并注册:
mod shortcut;
pub fn run() {
let mut ctx = tauri::generate_context!();
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![
shortcut::change_shortcut,
shortcut::unregister_shortcut,
shortcut::get_current_shortcut,
])
.setup(|app| {
init(app.app_handle());
shortcut::enable_shortcut(app);
enable_autostart(app);
Ok(())
})
.run(ctx)
.expect("error while running tauri application");
}
到此 APP 已经实现了 快捷键启动隐藏的功能了。
Mac 默认的快捷键是 command+shift+space。
Windows 和 Linux 默认的快捷键 ctrl+shift+space。
那么默认了,那用户电脑上使用有冲突,或者个人习惯问题,用户想修改呢?
修改快捷键
那就需要做一个前端界面,在前端界面上让用户操作。
import { useState, useEffect } from "react";
import { isTauri, invoke } from "@tauri-apps/api/core";
// 从 Tauri API 导入工具函数,isTauri 用于检测是否在 Tauri 环境中,invoke 用于调用后端 Rust 命令
import { ShortcutItem } from "./ShortcutItem"; // 引入自定义组件,用于渲染快捷键项
import { Shortcut } from "./shortcut"; // 引入 Shortcut 类型,定义快捷键的类型结构
import { useShortcutEditor } from "@/hooks/useShortcutEditor"; // 自定义 hook,用于快捷键的编辑逻辑
export default function GeneralSettings() {
const [shortcut, setShortcut] = useState<Shortcut>([]); // 声明 state 变量 `shortcut`,用于存储当前的快捷键
/**
* 获取当前的快捷键
* 调用 Tauri 的后端命令 `get_current_shortcut`
* 将结果分割成数组并更新到 `shortcut` 状态中
*/
async function getCurrentShortcut() {
try {
const res: string = await invoke("get_current_shortcut"); // 调用 Tauri 后端的 Rust 命令
console.log("DBG: ", res); // 调试日志,打印获取的快捷键字符串
setShortcut(res?.split("+")); // 将快捷键字符串分割成数组并存储
} catch (err) {
console.error("Failed to fetch shortcut:", err); // 捕获并打印获取快捷键失败的错误
}
}
/**
* 在组件加载时(mount)获取当前的快捷键
*/
useEffect(() => {
getCurrentShortcut(); // 调用获取快捷键的函数
}, []);
/**
* 修改快捷键的逻辑
* @param key - 新的快捷键数组
*/
const changeShortcut = (key: Shortcut) => {
setShortcut(key); // 更新本地状态
if (key.length === 0) return; // 如果快捷键为空数组,则直接返回
invoke("change_shortcut", { key: key?.join("+") }).catch((err) => {
console.error("Failed to save hotkey:", err); // 捕获并打印保存快捷键失败的错误
});
};
// 使用自定义的快捷键编辑 hook,管理快捷键编辑的逻辑
const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } =
useShortcutEditor(shortcut, changeShortcut);
/**
* 开始编辑快捷键
* 调用后端命令取消当前注册的快捷键
*/
const onEditShortcut = async () => {
startEditing(); // 切换到编辑状态
invoke("unregister_shortcut").catch((err) => {
console.error("Failed to save hotkey:", err); // 捕获并打印取消快捷键失败的错误
});
};
/**
* 取消快捷键的编辑
* 将快捷键恢复到之前的状态并重新注册
*/
const onCancelShortcut = async () => {
cancelEditing(); // 退出编辑状态
invoke("change_shortcut", { key: shortcut?.join("+") }).catch((err) => {
console.error("Failed to save hotkey:", err); // 捕获并打印保存快捷键失败的错误
});
};
/**
* 保存快捷键的编辑
* 调用 saveShortcut 以保存编辑后的快捷键
*/
const onSaveShortcut = async () => {
saveShortcut(); // 保存快捷键
};
/**
* 渲染设置界面
*/
return (
<div className="space-y-8">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
General Settings
</h2>
<div className="space-y-6">
{/* 渲染快捷键项 */}
<ShortcutItem
shortcut={shortcut} // 当前快捷键数组
isEditing={isEditing} // 是否处于编辑状态
currentKeys={currentKeys} // 编辑中的快捷键
onEdit={onEditShortcut} // 点击编辑时触发的事件
onSave={onSaveShortcut} // 点击保存时触发的事件
onCancel={onCancelShortcut} // 点击取消时触发的事件
/>
</div>
</div>
</div>
);
}
ShortcutItem.tsx 文件:
import { formatKey, sortKeys } from "@/utils/keyboardUtils"; // 导入工具函数,用于格式化和排序键值
import { X } from "lucide-react"; // 导入 X 图标,用于取消按钮的图标显示
// 定义组件的 props 类型
interface ShortcutItemProps {
shortcut: string[]; // 当前的快捷键数组
isEditing: boolean; // 是否处于编辑模式
currentKeys: string[]; // 当前按下的快捷键
onEdit: () => void; // 编辑快捷键的回调函数
onSave: () => void; // 保存快捷键的回调函数
onCancel: () => void; // 取消编辑的回调函数
}
// 定义 `ShortcutItem` 组件
export function ShortcutItem({
shortcut,
isEditing,
currentKeys,
onEdit,
onSave,
onCancel,
}: ShortcutItemProps) {
/**
* 渲染键值的函数
* @param keys - 需要渲染的键值数组
* @returns 渲染后的键值组件
*/
const renderKeys = (keys: string[]) => {
const sortedKeys = sortKeys(keys); // 对键值数组进行排序
return sortedKeys.map((key, index) => (
<kbd
key={index} // 使用键值数组的索引作为唯一 key
className={`px-2 py-1 text-sm font-semibold rounded shadow-sm bg-gray-100 border-gray-300 text-gray-900 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200`}
>
{formatKey(key)} {/* 格式化并显示键值 */}
</kbd>
));
};
return (
<div
className={`flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-700`}
>
{/* 左侧部分:显示快捷键或编辑状态 */}
<div className="flex items-center gap-4">
{isEditing ? ( // 如果处于编辑模式
<>
<div className="flex gap-1 min-w-[120px] justify-end">
{currentKeys.length > 0 ? ( // 如果当前有按下的键
renderKeys(currentKeys) // 渲染当前按下的键
) : (
<span className={`italic text-gray-500 dark:text-gray-400`}>
Press keys... {/* 提示用户按下键 */}
</span>
)}
</div>
<div className="flex gap-2">
{/* 保存按钮 */}
<button
onClick={onSave} // 点击时触发保存回调
disabled={currentKeys.length < 2} // 如果按下的键少于 2 个,禁用按钮
className={`px-3 py-1 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:text-white dark:hover:bg-blue-700
disabled:opacity-50 disabled:cursor-not-allowed`}
>
Save
</button>
{/* 取消按钮 */}
<button
onClick={onCancel} // 点击时触发取消回调
className={`p-1 rounded text-gray-500 hover:text-gray-700 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-600`}
>
<X className="w-4 h-4" /> {/* 使用 X 图标 */}
</button>
</div>
</>
) : (
// 如果不在编辑模式
<>
<div className="flex gap-1">{renderKeys(shortcut)}</div> {/* 显示当前快捷键 */}
<button
onClick={onEdit} // 点击时触发编辑回调
className={`px-3 py-1 text-sm rounded bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500`}
>
Edit {/* 编辑按钮 */}
</button>
</>
)}
</div>
</div>
);
}
hooks/useShortcutEditor.ts 文件:
import { useState, useCallback, useEffect } from 'react'; // 导入 React hooks
import { useHotkeys } from 'react-hotkeys-hook'; // 导入用于捕获键盘快捷键的 hook
import { Shortcut } from '@/components/Settings/shortcut'; // 导入 Shortcut 类型
import { normalizeKey, isModifierKey, sortKeys } from '@/utils/keyboardUtils'; // 导入用于键盘操作的工具函数
// 定义保留的快捷键,这些快捷键是系统或应用预定义的,用户不能使用它们
const RESERVED_SHORTCUTS = [
// 系统快捷键(Mac)
["Command", "C"],
["Command", "V"],
["Command", "X"],
["Command", "A"],
["Command", "Z"],
["Command", "Q"],
// Windows/Linux 快捷键
["Control", "C"],
["Control", "V"],
["Control", "X"],
["Control", "A"],
["Control", "Z"],
// Coco 特定的快捷键
["Command", "I"],
["Command", "T"],
["Command", "N"],
["Command", "G"],
["Command", "O"],
["Command", "U"],
["Command", "M"],
["Command", "Enter"],
["Command", "ArrowLeft"],
["Command", "ArrowRight"],
["Command", "ArrowUp"],
["Command", "ArrowDown"],
["Command", "0"],
["Command", "1"],
["Command", "2"],
["Command", "3"],
["Command", "4"],
["Command", "5"],
["Command", "6"],
["Command", "7"],
["Command", "8"],
["Command", "9"],
];
// `useShortcutEditor` 是一个自定义 hook,用于处理快捷键编辑的逻辑
export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) {
console.log("shortcut", shortcut) // 输出当前快捷键调试信息
const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态
const [currentKeys, setCurrentKeys] = useState<string[]>([]); // 存储当前按下的键
const [pressedKeys] = useState(new Set<string>()); // 存储按下的键的集合(去重)
// 启动编辑状态
const startEditing = useCallback(() => {
setIsEditing(true);
setCurrentKeys([]); // 清空当前按下的键
}, []);
// 保存当前设置的快捷键
const saveShortcut = async () => {
if (!isEditing || currentKeys.length < 2) return; // 如果没有进入编辑状态或快捷键少于 2 个,不能保存
// 检查是否包含修饰键(例如 Command 或 Control)
const hasModifier = currentKeys.some(isModifierKey);
const hasNonModifier = currentKeys.some(key => !isModifierKey(key));
// 如果没有修饰键或者没有非修饰键,则不允许保存
if (!hasModifier || !hasNonModifier) return;
// 检查当前快捷键是否与保留的快捷键冲突
const isReserved = RESERVED_SHORTCUTS.some(reserved =>
reserved.length === currentKeys.length && // 确保长度一致
reserved.every((key, index) => key.toLowerCase() === currentKeys[index].toLowerCase()) // 比较大小写不敏感
);
if (isReserved) {
console.error("This is a system reserved shortcut"); // 如果是保留的快捷键,输出错误并停止保存
return;
}
// 对快捷键进行排序,确保修饰键在前,其他键在后
const sortedKeys = sortKeys(currentKeys);
// 调用外部传入的 onChange 函数,通知父组件更新快捷键
onChange(sortedKeys);
// 结束编辑状态并清空当前按下的键
setIsEditing(false);
setCurrentKeys([]);
};
// 取消编辑,恢复之前的状态
const cancelEditing = useCallback(() => {
setIsEditing(false);
setCurrentKeys([]); // 清空当前按下的键
}, []);
// 注册键盘事件以捕获按键
useHotkeys(
'*', // 捕获所有键
(e) => {
if (!isEditing) return; // 如果没有处于编辑状态,则不处理
e.preventDefault(); // 阻止默认事件
e.stopPropagation(); // 阻止事件冒泡
const key = normalizeKey(e.code); // 标准化键名
// 更新按下的键
pressedKeys.add(key);
setCurrentKeys(() => {
const keys = Array.from(pressedKeys); // 获取所有按下的键
let modifiers = keys.filter(isModifierKey); // 获取所有修饰键
let nonModifiers = keys.filter(k => !isModifierKey(k)); // 获取所有非修饰键
// 限制修饰键和非修饰键的数量最多为 2 个
if (modifiers.length > 2) {
modifiers = modifiers.slice(0, 2);
}
if (nonModifiers.length > 2) {
nonModifiers = nonModifiers.slice(0, 2);
}
// 合并修饰键和非修饰键
return [...modifiers, ...nonModifiers];
});
},
{
enabled: isEditing, // 只有在编辑状态下才启用
keydown: true, // 捕获按下键
enableOnContentEditable: true // 在内容可编辑区域启用
},
[isEditing, pressedKeys] // 依赖项:isEditing 和 pressedKeys
);
// 注册键盘松开事件
useHotkeys(
'*',
(e) => {
if (!isEditing) return; // 如果没有处于编辑状态,则不处理
const key = normalizeKey(e.code); // 标准化键名
pressedKeys.delete(key); // 从按下的键集合中删除松开的键
},
{
enabled: isEditing, // 只有在编辑状态下才启用
keyup: true, // 捕获松开键
enableOnContentEditable: true // 在内容可编辑区域启用
},
[isEditing, pressedKeys] // 依赖项:isEditing 和 pressedKeys
);
// 清理编辑状态,当组件卸载时,取消编辑状态
useEffect(() => {
return () => {
if (isEditing) {
cancelEditing(); // 如果仍然处于编辑状态,则取消编辑
}
};
}, [isEditing, cancelEditing]); // 依赖项:isEditing 和 cancelEditing
// 返回的对象包含了编辑状态和快捷键的相关方法
return {
isEditing,
currentKeys,
startEditing,
saveShortcut,
cancelEditing
};
}
小结
通过本文的介绍,你可以将全局快捷键集成到你的 Tauri 应用中,进而为用户提供更流畅的操作体验。如果你还没有使用过 Tauri,希望你能通过这篇文章对它有更深入的了解,并开始在自己的项目中尝试这一功能!
开源
最近,我正在基于 Tauri 开发一款项目,名为 Coco。目前已开源,项目仍在不断完善中,欢迎大家前往支持并为项目点亮免费的 star 🌟!
作为个人的第一个 Tauri 项目,开发过程中也是边探索边学习。希望能与志同道合的朋友一起交流经验、共同成长!
代码中如有问题或不足之处,期待小伙伴们的宝贵建议和指导!
- 官网: coco.rs/
- 前端仓库: github.com/infinilabs/…
- 服务端仓库: github.com/infinilabs/…
非常感谢您的支持与关注!