Skip to content

Keyboard shortcuts

Sabine handles two kinds of keyboard shortcuts beyond the ones a page listens for itself:

  • Global shortcuts reach the app wherever the keyboard focus is, for example to summon a palette from any app.
  • Shortcut inhibiting lets a focused window receive keys the desktop would otherwise keep for itself, such as Alt+Tab or Super, which remote desktop clients, terminals and games need.

Register global shortcuts on the window with .global_shortcut(...), once for each shortcut:

use sabine::prelude::*;
use sabine::{GlobalShortcutRegistration, Shortcut, ShortcutModifiers};
fn main() {
SabineWindow::main(|window| {
Ok(window
.palette()
.title("Launcher")
.size(640, 420)
.global_shortcut(GlobalShortcutRegistration {
id: "toggle".into(),
shortcut: Shortcut {
modifiers: ShortcutModifiers {
ctrl: true,
shift: true,
..ShortcutModifiers::default()
},
key: "Space".into(),
},
action: "toggle-launcher".into(),
description: Some("Show or hide the launcher".into()),
}))
});
}
Field Meaning
id Identifies the shortcut in events, and in the desktop’s shortcut settings on Linux.
shortcut The keys: modifiers plus one key.
action Any string you want back when the shortcut fires.
description What the shortcut does, shown in the desktop’s settings on Linux. Defaults to action.

ShortcutModifiers has ctrl, alt, shift and meta. meta is the Super key on Linux, the Windows key on Windows and Command on macOS. Shortcut::new(key) makes a shortcut with no modifiers.

Keys are named the same way on every desktop:

  • a single character: K, 5, /, -, [
  • a common key name: Space, Enter, Escape, Tab, Backspace, Delete, Insert, Home, End, PageUp, PageDown, Up, Down, Left, Right, PrintScreen, Pause, CapsLock, NumLock, ScrollLock, or F1 to F24
  • a KeyboardEvent.code value: KeyK, Digit5, NumpadAdd, MediaPlayPause, AudioVolumeUp

Common names ignore case, and a few short forms work too, such as Esc, Return, PgUp and PgDn. On Linux you can also use any XKB key name, such as XF86Calculator.

Each press reaches the app’s pages as events.globalShortcut:

import { appWindow, events } from "@lantharos/sabine";
events.globalShortcut(({ action, activationToken }) => {
if (action !== "toggle-launcher") return;
if (appWindow.visible) {
appWindow.hide();
} else {
appWindow.show();
appWindow.focus(activationToken ?? undefined);
}
});
Field Type Meaning
id string The shortcut’s id.
action string The shortcut’s action.
activationToken string | null Pass to appWindow.focus so the desktop lets the window take focus.

Wayland compositors only let a window take focus with an activation token, which the desktop hands over with the shortcut. Pass it to appWindow.focus; on other desktops it is null and focusing works without it.

Rust code hears each press too, with .on_desktop_event(...), also while no window is open. An app built with .background() can open its window from there:

use sabine::{GlobalShortcutActivation, PlatformEvent};
window.background().on_desktop_event(|app, event| {
if let PlatformEvent::GlobalShortcut(GlobalShortcutActivation {
action,
activation_token,
..
}) = event
&& action == "toggle-launcher"
{
app.present(activation_token.clone());
}
})

A shortcut the desktop refuses, for example because another app already holds it, is recorded in the app’s diagnostics and reported to its pages as events.globalShortcutFailed. The app and its other shortcuts keep working.

import { events } from "@lantharos/sabine";
events.globalShortcutFailed(({ id, message }) => {
showHint(`The shortcut ${id} is not available: ${message}`);
});

The event carries the shortcut’s id, its action and a message.

Sabine binds global shortcuts through the XDG desktop portal’s GlobalShortcuts interface. All of an app’s shortcuts go into one portal session, so the desktop asks about them together, and people can change the keys in their desktop’s settings; the id and action stay the same. The keys you register are the preferred trigger the desktop starts from. A shortcut the desktop leaves out of the session is reported as failed.

The portal identifies an app by the desktop entry named after its app id, such as com.example.launcher.desktop. An installed entry is left alone. Until the app is installed, Sabine writes a hidden stand-in entry in ~/.local/share/applications that starts the running executable and carries the window’s title as its name, and removes it once a real entry exists. Development runs use the .dev app id, so their shortcuts are listed separately from the installed app’s.

Desktops act on some keys before any app sees them: Alt+Tab, the Super or Windows key, Command+Tab or Command+Space. A window that needs those keys, such as a remote desktop or virtual machine viewer, can ask for them while it has focus:

import { appWindow } from "@lantharos/sabine";
try {
await appWindow.inhibitShortcuts(true);
} catch (error) {
showHint(String(error));
}
// Give the desktop its shortcuts back.
await appWindow.inhibitShortcuts(false);

While inhibiting is on and the window has focus, those keys arrive in the page as ordinary keydown and keyup events. When the window loses focus, the desktop’s shortcuts work again, and they stop reaching the desktop once the window is focused again. Calling inhibitShortcuts(false) ends it.

inhibitShortcuts(true) rejects with a message when the desktop doesn’t allow it.

The window asks the Wayland compositor through the keyboard shortcuts inhibit protocol, which the compositor applies whenever the window has keyboard focus. Some compositors ask the person to allow it first. On compositors without the protocol, inhibitShortcuts(true) rejects.