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:

ResultWhen
{ 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' },
]
ScopeBound byNotes
menuThe native application menu (accelerators).Active only while the app is focused.
in-windowThe renderer (e.g. the command palette).App-local key handling, not an OS hotkey.
globalglobalShortcut — 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.

On this page