「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 つ以上含めます。設定画面でユーザーが変えられるようにしておくと衝突に対処しやすくなります。 - 共通: 呼び出しに使うなら、ウィンドウの表示・非表示 や トレイ常駐 と組み合わせます。
