第十六章 原生能力:托盘、通知、快捷键、剪贴板

第十六章 原生能力:托盘、通知、快捷键、剪贴板#

"桌面应用之所以像桌面应用,是因为它住在操作系统里。"

Tauri 的价值不只是把网页装进窗口,而是让 Web UI 通过受控边界使用系统能力。本章为 Hive 加入系统托盘、桌面通知、全局快捷键和剪贴板。

        mindmap
  root((原生能力))
    系统托盘
      打开窗口
      同步状态
      退出
    通知
      at 提醒
      权限
      点击回调
    快捷键
      全局唤起
      新建笔记
      可关闭
    剪贴板
      复制 Markdown
      粘贴图片
      隐私边界
    

16.1 原生能力的安全边界#

        flowchart TB
    UI["UI 操作"] --> Cmd["IPC Command"]
    Cmd --> Policy{"权限与状态检查"}
    Policy -->|允许| Plugin["Tauri Plugin / OS API"]
    Policy -->|拒绝| Error["结构化错误"]
    Plugin --> OS["操作系统"]
    

原生能力要遵循两个原则:用户可预期、权限最小化。比如全局快捷键必须能在设置里关闭,通知必须尊重系统权限。


16.2 系统托盘#

托盘适合常驻应用:快速打开、显示同步状态、退出应用。

use tauri::menu::{Menu, MenuItem};
use tauri::tray::TrayIconBuilder;

fn setup_tray(app: &tauri::App) -> tauri::Result<()> {
    let open = MenuItem::with_id(app, "open", "打开 Hive", true, None::<&str>)?;
    let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
    let menu = Menu::with_items(app, &[&open, &quit])?;

    TrayIconBuilder::new()
        .menu(&menu)
        .on_menu_event(|app, event| match event.id.as_ref() {
            "open" => {
                if let Some(window) = app.get_webview_window("main") {
                    let _ = window.show();
                    let _ = window.set_focus();
                }
            }
            "quit" => app.exit(0),
            _ => {}
        })
        .build(app)?;
    Ok(())
}

16.3 桌面通知#

通知不是日志。只有用户需要被打断时才发通知,例如有人 @ 你、同步失败需要处理。

#[tauri::command]
async fn notify_message(
    app: tauri::AppHandle,
    room: String,
    sender: String,
    text: String,
) -> Result<(), String> {
    app.notification()
        .builder()
        .title(format!("{} · {}", room, sender))
        .body(text)
        .show()
        .map_err(|e| e.to_string())
}

发布前要验证 macOS、Windows、Linux 的通知行为。不同平台的权限弹窗和点击回调并不完全一致。


16.4 全局快捷键#

快捷键适合高频动作:快速唤起、全局搜索、截图或新建笔记。

use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut};

fn register_shortcuts(app: &tauri::App) -> tauri::Result<()> {
    let shortcut = Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyN);
    app.global_shortcut().on_shortcut(shortcut, |app, _, event| {
        if event.state().is_pressed() {
            let _ = app.emit("shortcut-new-note", ());
        }
    })?;
    Ok(())
}
        sequenceDiagram
    participant OS as OS
    participant Core as Rust Core
    participant UI as WebView
    OS->>Core: Ctrl+Shift+N
    Core->>UI: shortcut-new-note
    UI->>UI: 打开新建笔记
    

16.5 剪贴板#

剪贴板可以支持复制 Markdown、粘贴图片、复制消息链接。注意剪贴板可能包含敏感内容,不要在后台自动读取。

#[tauri::command]
async fn copy_markdown(app: tauri::AppHandle, markdown: String) -> Result<(), String> {
    app.clipboard()
        .write_text(markdown)
        .map_err(|e| e.to_string())
}

剪贴板读取应该由用户动作触发,比如点击“从剪贴板导入”。


16.6 小结#

原生能力让 Tauri 应用融入操作系统,但也会增加权限、安全和跨平台测试成本。Hive 把这些能力集中在 Rust Core,通过命令和事件暴露给前端。

下一章我们讨论安全加固:CSP、capabilities、IPC 权限与代码签名。