Notifications & Shortcuts
Native OS notifications and the keyboard-shortcut registry.
Two small main-process subsystems round out the desktop shell: native OS notifications and a keyboard-shortcut registry. Both are wired through Conveyor and honor user settings.
Native notifications
lib/main/notifications.ts (NotificationsService) wraps Electron's
Notification. The renderer fires one through the notification Conveyor
domain:
const { notify } = useConveyor('notification')
await notify({ title: 'Export complete', body: 'Your backup was saved.' })notify() returns a typed result rather than throwing, so the caller can react
to why a notification didn't show:
| Result | When |
|---|---|
{ ok: true } | Shown. |
{ ok: false, reason: 'disabled' } | The user turned notifications off (settings.notifications, default on). |
{ ok: false, reason: 'unsupported' } | Notification.isSupported() is false on this platform; the service also logs a warning. |
The Notifications settings tab toggles settings.notifications, which the
service checks on every call — no restart needed.
Windows identity (AUMID)
For toasts to attribute to your app (and survive install), Windows needs a stable AppUserModelID. The template pins it to the app id and sets it at startup:
// lib/main/main.ts
electronApp.setAppUserModelId(APP_USER_MODEL_ID)APP_USER_MODEL_ID equals APP_ID (com.soarstarter.electron) in
shared/constants.ts. pnpm rebrand keeps it aligned with the installer
identity — if notifications are silent on Windows, this is the first thing to
check (see Troubleshooting). macOS shows
notifications without extra identity setup.
Trying it
The Showcase notifications demo is the live test — it calls notify() and shows
the returned reason, so you can watch the disabled/unsupported branches directly.
See Showcase.
Keyboard shortcuts
lib/main/shortcuts.ts (ShortcutRegistry) is a registry of named shortcuts
with per-user overrides. Each definition has an id, a default accelerator, a
scope, and a description:
// lib/main/shortcuts.ts
export const DEFAULT_SHORTCUTS = [
{ id: 'nav.home', defaultAccelerator: 'CmdOrCtrl+1', scope: 'menu', description: 'Open Home' },
{ id: 'nav.open-file', defaultAccelerator: 'CmdOrCtrl+O', scope: 'menu', description: 'Open File' },
{ id: 'nav.settings', defaultAccelerator: 'CmdOrCtrl+,', scope: 'menu', description: 'Open Settings' },
{ id: 'palette.open', defaultAccelerator: 'CmdOrCtrl+K', scope: 'in-window', description: 'Open Command Palette' },
]| Scope | Bound by | Notes |
|---|---|---|
menu | The native application menu (accelerators). | Active only while the app is focused. |
in-window | The renderer (e.g. the command palette). | App-local key handling, not an OS hotkey. |
global | globalShortcut — system-wide. | Supported by the registry, but no global shortcut ships by default. |
get(id) resolves the user override (settings.shortcuts[id]) or the default,
and resolveForPlatform swaps CmdOrCtrl for Cmd on macOS and Ctrl
elsewhere. At startup, validateAtStartup() logs a warning if two ids resolve to
the same accelerator.
registerGlobal() refuses any id whose scope isn't global, and the four
built-in shortcuts are menu/in-window only — so the template registers no
system-wide hotkeys out of the box. To add one, define a global-scope
shortcut and call registerGlobal(id, handler) during startup.
The Shortcuts settings tab
ShortcutsSection calls useConveyor('shortcut').list() and renders each
shortcut's description, id, and resolved accelerator. It is read-only today —
a display of what's bound. The override plumbing already exists end to end (the
shortcuts map in the settings schema, and get() honoring it), so building a
rebinding control is a matter of writing overrides through updateSettings —
see Settings Store.
Command palette vs global shortcut
The command palette opens with Cmd/Ctrl+K, but that is an in-window
shortcut handled by the renderer (app/components/command-palette/) — not a
global hotkey. It only fires while a window is focused. Treat it as app UI, not
an OS-level binding. The palette's contents (routes, settings, recent files) come
from command sources — see UI & Pages.
