グローバルショートカットを登録する

Global Shortcut プラグインの register() と Rust の Builder で、アプリが背面にあっても反応するキーを登録する。default 権限が空な点、再読み込み後の二重登録エラーの避け方も示す。

ウィンドウ 対象: Tauri 2.x 更新日: 読了目安: 約9分 win-031
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 登録と解除
  4. キーの書き方と、複数のキーを登録するとき
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

「Ctrl+Shift+Space でどこからでもランチャーを呼び出す」のように、アプリが背面にあるときや最小化・トレイ常駐中でも反応するキーを登録するには、Global Shortcut プラグインを使います。登録は JS の register() からも Rust からもできますが、JS で登録したハンドラーはそのページと一緒に消えるため、アプリが動いている間ずっと使うキーは Rust で登録するのが確実です。アプリにフォーカスがあるときだけ効けばよいキーなら、メニューのショートカット や keydown で足ります。

前提条件

プラグインを追加します。デスクトップ専用で、iOS / Android では使えません。

npm run tauri add global-shortcut

lib.rs にプラグインの登録(tauri_plugin_global_shortcut::Builder::new().build())が書き足されます。2 章のように Rust でハンドラーを付ける場合は、その行を 2 章のコードに置き換え、プラグインを二重に登録しないようにします。

JS から使う場合は権限が必要です。このプラグインの global-shortcut:default は何も許可しない空のセットなので、使うコマンドを 1 つずつ書きます。Rust だけで登録するなら権限は要りません。次の例は、ショートカットでウィンドウを表示して前面に出すための core:window:allow-show と core:window:allow-set-focus も含めています。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:allow-show",
    "core:window:allow-set-focus",
    "global-shortcut:allow-register",
    "global-shortcut:allow-unregister",
    "global-shortcut:allow-is-registered"
  ]
}

1. フロントエンドから実装する (TypeScript)

登録と解除

ハンドラーはキーを押したとき(Pressed)と離したとき(Released)の 2 回呼ばれるので、state で片方に絞ります。

注意したいのはページの再読み込みです。登録はアプリ側に残り続けますが、ハンドラーは古いページと一緒に消えます。再読み込み後に同じキーを register() すると二重登録のエラーになり、「登録済みなら何もしない」と書くと古い登録が残ったまま何も起きません。そこで、登録済みなら先に外してから登録し直します。isRegistered() はこのアプリが登録したかどうかだけを返し、ほかのアプリが使っているキーでも false です。

import { register, unregister, isRegistered } from '@tauri-apps/plugin-global-shortcut';
import { getCurrentWindow } from '@tauri-apps/api/window';

const SHORTCUT = 'CommandOrControl+Shift+Space';

export async function enableQuickOpen() {
  // 再読み込み前のページで登録したものが残っていれば外す
  if (await isRegistered(SHORTCUT)) await unregister(SHORTCUT);

  await register(SHORTCUT, async (event) => {
    if (event.state !== 'Pressed') return; // 離したときにも呼ばれる
    const win = getCurrentWindow();
    await win.show(); // core:window:allow-show
    await win.setFocus(); // core:window:allow-set-focus
  });
}

export async function disableQuickOpen() {
  await unregister(SHORTCUT);
}

キーの書き方と、複数のキーを登録するとき

文字列は修飾キーを先に、最後にキーを 1 つだけ + でつなぎます。大文字・小文字は区別しません。

書き方意味
CommandOrControl(CmdOrCtrl)macOS では Command、それ以外では Ctrl
Control(Ctrl)/ Shift / Alt(Option)各修飾キー
Super(Cmd / Command)macOS の Command、Windows の Windows キー
A〜Z、0〜9、F1〜F24文字・数字・ファンクションキー
Space / Enter / Escape / Up など特殊キー

+ は区切り文字なので、キーとしては書けません(テンキーの + は NumpadAdd)。

register() に配列を渡すと 1 つのハンドラーで複数のキーを受けられますが、ハンドラーに届く event.shortcut は shift+control+KeyP のような正規化された文字列で、登録した文字列とは一致しません。キーごとに処理を分けるなら、1 つずつ別のハンドラーで登録する方が確実です。

import { register, unregister, isRegistered } from '@tauri-apps/plugin-global-shortcut';

const actions: Record<string, () => void> = {
  'CommandOrControl+Shift+1': () => console.log('メモを開く'),
  'CommandOrControl+Shift+2': () => console.log('検索を開く'),
};

export async function registerAll() {
  for (const [key, action] of Object.entries(actions)) {
    if (await isRegistered(key)) await unregister(key);
    await register(key, (e) => {
      if (e.state === 'Pressed') action();
    });
  }
}

2. バックエンドから実装する (Rust)

アプリが動いている間ずっと使うキーは、プラグインの Builder に with_shortcuts() と with_handler() を渡して登録します。ページの再読み込みの影響を受けず、ウィンドウが非表示でも動きます。with_handler() のハンドラーは、JS から登録したキーでも呼ばれるので、shortcut を見て自分のキーだけを処理します。文字列から作れば CommandOrControl も OS に合わせて解釈されます。

use tauri::{Emitter, Manager};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // プラグインはデスクトップ専用なので、use も含めて cfg(desktop) の中に書く
            #[cfg(desktop)]
            {
                use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};

                let toggle: Shortcut = "CommandOrControl+Shift+Space".parse()?;
                let new_note: Shortcut = "CommandOrControl+Shift+N".parse()?;

                app.handle().plugin(
                    tauri_plugin_global_shortcut::Builder::new()
                        .with_shortcuts([toggle, new_note])?
                        .with_handler(move |app, shortcut, event| {
                            if event.state != ShortcutState::Pressed {
                                return;
                            }
                            if shortcut == &toggle {
                                // 表示中なら隠し、隠れていれば前面に出す
                                if let Some(win) = app.get_webview_window("main") {
                                    if win.is_visible().unwrap_or(false) {
                                        let _ = win.hide();
                                    } else {
                                        let _ = win.show();
                                        let _ = win.set_focus();
                                    }
                                }
                            } else if shortcut == &new_note {
                                // 処理は画面側に任せる
                                let _ = app.emit("new-note", ());
                            }
                        })
                        .build(),
                )?;
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

後から追加・変更するときは、GlobalShortcutExt を読み込んで app.global_shortcut().on_shortcut("...", ハンドラー) や unregister() を呼びます。フロントエンドは new-note を Rust からのイベント として受け取ります。

import { listen } from '@tauri-apps/api/event';

await listen('new-note', () => console.log('新しいメモを作る'));

動作確認

npm run tauri dev で起動し、ブラウザなど別のアプリを前面にした状態で Ctrl+Shift+Space(macOS は Cmd+Shift+Space)を押すと、メインウィンドウが隠れ、もう一度押すと前面に戻ります。JS 版の enableQuickOpen() を呼んだ後にページを再読み込みし、もう一度呼んでもエラーにならず、キーが効き続けます。

よくあるエラーと対処法

  • 「global-shortcut.register not allowed. Permissions associated with this command: global-shortcut:allow-register」: 権限がありません。global-shortcut:default では足りないので allow-* を書きます。リリースビルドでは「Command plugin:global-shortcut|register not allowed by ACL」だけになります。
  • 「HotKey already registered」または「Unable to register hotkey」で始まるエラー: 同じキーを二重に登録しています(再読み込み後の再登録など)。先に unregister() します。ほかのアプリが使っている組み合わせでも起きることがあります。
  • 「Couldn't recognize "..." as a valid key for hotkey」: キー名の綴りが違います(Plus など)。表の書き方に直します。修飾キーを後ろに書くと「Invalid hotkey format」になります。
  • エラーは出ないのに反応しない: ほかのアプリが先にそのキーを使っていると、登録できてもハンドラーが呼ばれないことがあります。組み合わせを変えます。再読み込み後に「登録済みなら何もしない」としている場合も同じ症状になります。
  • 1 回押しただけで処理が 2 回動く: Pressed と Released の両方で呼ばれています。state で絞ります。

OS ごとの違いと注意点

  • Windows / macOS / Linux: 対応しています。CommandOrControl は macOS では Command、Windows と Linux では Ctrl になります。
  • iOS / Android: 非対応です。モバイル向けにもビルドするなら、Rust のコードは例のように #[cfg(desktop)] で囲みます。
  • 共通: 登録したキーはシステム全体で使われるので、Ctrl+C のようにほかのアプリでよく使う組み合わせは避け、修飾キーを 2 つ以上含めます。設定画面でユーザーが変えられるようにしておくと衝突に対処しやすくなります。
  • 共通: 呼び出しに使うなら、ウィンドウの表示・非表示 や トレイ常駐 と組み合わせます。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する