refactor(commands): share app command manifest
This commit is contained in:
@@ -334,6 +334,12 @@ The renderer uses `viewOrdering` helpers to convert drag or command-palette move
|
||||
- Plain click / `Enter` open the focused note without replacing the current Neighborhood.
|
||||
- Cmd/Ctrl-click and Cmd/Ctrl-`Enter` open the note and pivot the note list into that note's Neighborhood.
|
||||
|
||||
## Command Surface
|
||||
|
||||
`src/shared/appCommandManifest.json` is the cross-runtime source for stable app command IDs, menu structure, display labels, accelerators, deterministic shortcut QA metadata, and native menu enablement groups. The renderer imports it through `src/hooks/appCommandCatalog.ts`, which derives `APP_COMMAND_IDS`, shortcut lookup maps, Linux titlebar menu sections, native-menu command membership, and test helpers. Tauri includes the same JSON in `src-tauri/src/menu.rs` and uses it to build custom menu items, emit overridden menu item IDs such as the quick-open alias as their primary command IDs, and toggle state-dependent menu items from manifest groups.
|
||||
|
||||
Domain command builders still own context-sensitive command-palette entries, availability, and execution callbacks. The manifest owns metadata that must stay identical across native menus, renderer shortcuts, deterministic QA bridges, and the Linux fallback menu; OS-native menu items such as Undo, Copy/Paste, Services, Quit, and Window controls remain local to the native menu implementation.
|
||||
|
||||
## File System Integration
|
||||
|
||||
### Vault Scanning (Rust)
|
||||
|
||||
@@ -822,7 +822,7 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
|
||||
| `useUnifiedSearch` | Query, results, loading state | Keyword search |
|
||||
| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, AutoGit thresholds, default AI agent, Gitignored-content visibility, All Notes file visibility) | Persistent settings |
|
||||
| `useVaultConfig` | Per-vault UI preferences, AI permission mode | Vault-specific config |
|
||||
| `appCommandDispatcher` | Canonical shortcut/menu command IDs | Shared execution path for renderer and native menu commands |
|
||||
| `appCommandDispatcher` | Manifest-backed shortcut/menu command IDs | Shared execution path for renderer and native menu commands |
|
||||
|
||||
Data flows unidirectionally: `App` passes data and callbacks as props to child components. No child-to-child communication — everything goes through `App`.
|
||||
|
||||
@@ -847,13 +847,14 @@ Selection-dependent actions are wired through the command palette and the native
|
||||
|
||||
Shortcut routing is explicit:
|
||||
|
||||
- `appCommandCatalog.ts` is the shared shortcut manifest for command IDs, modifier rules, and deterministic QA metadata
|
||||
- `src/shared/appCommandManifest.json` is the shared command/menu metadata source for command IDs, menu labels, accelerators, native-menu enablement groups, and deterministic QA flags
|
||||
- `appCommandCatalog.ts` derives renderer command IDs, shortcut lookup maps, Linux menu sections, and QA metadata from that manifest
|
||||
- `formatShortcutDisplay()` derives platform-accurate visible shortcut labels (`⌘` on macOS, `Ctrl` on Windows/Linux) from that same manifest so menus, tooltips, and command-palette copy stay aligned with real accelerators
|
||||
- `useAppKeyboard` is the primary execution path for real shortcut keypresses, including Tauri runs
|
||||
- macOS browser-reserved chords such as `Cmd+O`, `Cmd+F`, and `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path
|
||||
- `Cmd+Shift+V` uses the same command path for "Paste without Formatting"; `plainTextPaste.ts` reads text through the native clipboard command in Tauri and inserts it through the active rich/raw editor target or the focused browser text control
|
||||
- `Cmd+F` is surface-aware: editor focus opens current-note find/replace in raw CodeMirror, note-list focus preserves note-list search, and native menu enablement follows focus availability events so only one `Cmd+F` menu item is active
|
||||
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same command IDs for native menu clicks, accelerators, and custom titlebar menu actions
|
||||
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same manifest-derived command IDs for native menu clicks, accelerators, and custom titlebar menu actions
|
||||
- `appCommandDispatcher.ts` suppresses the paired native-menu/renderer echo from a single shortcut so the command runs once
|
||||
- Deterministic QA uses two explicit proof paths from the shared manifest:
|
||||
- renderer shortcut-event proof through `window.__laputaTest.triggerShortcutCommand()`
|
||||
|
||||
31
docs/adr/0106-shared-app-command-manifest.md
Normal file
31
docs/adr/0106-shared-app-command-manifest.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0106"
|
||||
title: "Shared app command manifest"
|
||||
status: active
|
||||
date: 2026-05-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria command metadata was split across several runtime surfaces: TypeScript owned shortcut lookup and command-palette shortcut display, Rust owned native menu IDs, labels, accelerators, aliases, and enablement groups, and the Linux titlebar fallback menu duplicated another command list. Adding or changing a command required carefully editing multiple files that could drift while still compiling.
|
||||
|
||||
The existing renderer-first shortcut model and native-menu dedupe remain correct, but they need a single source for metadata that must be identical across those surfaces.
|
||||
|
||||
## Decision
|
||||
|
||||
**Tolaria stores cross-runtime app command metadata in `src/shared/appCommandManifest.json`, and both the renderer and Tauri native menu derive their command/menu IDs, accelerators, menu labels, menu aliases, enablement groups, and deterministic QA metadata from it.** Context-sensitive command-palette builders still own availability and execution callbacks, and OS-native menu entries remain local to the native menu implementation.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Shared JSON manifest included by TypeScript and Rust** (chosen): works in both runtimes without code generation, keeps menu metadata reviewable, and lets tests validate drift directly.
|
||||
- **Generate TypeScript and Rust constants from a schema**: gives stronger compile-time types but adds a build step and a generated-file maintenance burden for a small manifest.
|
||||
- **Keep duplicated constants with more tests**: reduces immediate refactor scope, but still forces every command change through parallel manual edits.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New app commands that appear in native menus or shortcut QA must be added to `src/shared/appCommandManifest.json`.
|
||||
- `appCommandCatalog.ts` is responsible for turning the manifest into typed renderer helpers such as `APP_COMMAND_IDS`, shortcut lookup maps, Linux menu sections, and deterministic QA definitions.
|
||||
- `src-tauri/src/menu.rs` includes the same manifest JSON, builds custom menu items from it, maps overridden menu item IDs such as `file-quick-open-alias` back to their primary command IDs, and resolves state-dependent enablement groups from manifest entries.
|
||||
- Platform-native menu items such as Undo, Redo, Copy, Paste, Select All, Services, Quit, and Window controls stay in Rust because they are OS affordances, not Tolaria app commands.
|
||||
- Command-palette builders continue to own dynamic labels, filtering, enabled state, and callbacks where those depend on current app state.
|
||||
@@ -158,3 +158,4 @@ proposed → active → superseded
|
||||
| [0103](0103-adapter-specific-ai-permission-semantics.md) | Adapter-specific AI permission semantics | active |
|
||||
| [0104](0104-tauri-frontend-readiness-watchdog.md) | Tauri frontend readiness watchdog | active |
|
||||
| [0105](0105-editor-correctness-and-responsiveness-contract.md) | Editor correctness and responsiveness contract | active |
|
||||
| [0106](0106-shared-app-command-manifest.md) | Shared app command manifest | active |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import type { AppCommandId } from '../hooks/appCommandCatalog'
|
||||
import { APP_COMMAND_DEFINITIONS, APP_COMMAND_IDS } from '../hooks/appCommandCatalog'
|
||||
import { APP_COMMAND_MENU_SECTIONS } from '../hooks/appCommandCatalog'
|
||||
import { Button } from './ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -17,7 +16,13 @@ import {
|
||||
|
||||
type MenuItem =
|
||||
| { kind: 'separator' }
|
||||
| { kind: 'command'; commandId: MenuCommandId; label: string }
|
||||
| {
|
||||
kind: 'command'
|
||||
commandId: string
|
||||
label: string
|
||||
menuItemId: string
|
||||
shortcut?: string
|
||||
}
|
||||
| { kind: 'action'; action: () => void; label: string; shortcut?: string }
|
||||
|
||||
type MenuSection = {
|
||||
@@ -25,91 +30,8 @@ type MenuSection = {
|
||||
label: string
|
||||
}
|
||||
|
||||
type MenuCommandId = AppCommandId | 'edit-toggle-note-list-search'
|
||||
|
||||
const MENU_SECTIONS: ReadonlyArray<MenuSection> = [
|
||||
{
|
||||
label: 'File',
|
||||
items: [
|
||||
{ kind: 'command', label: 'New Note', commandId: APP_COMMAND_IDS.fileNewNote },
|
||||
{ kind: 'command', label: 'New Type', commandId: APP_COMMAND_IDS.fileNewType },
|
||||
{ kind: 'command', label: 'Quick Open', commandId: APP_COMMAND_IDS.fileQuickOpen },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Save', commandId: APP_COMMAND_IDS.fileSave },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
items: [
|
||||
{ kind: 'command', label: 'Find in Note', commandId: APP_COMMAND_IDS.editFindInNote },
|
||||
{ kind: 'command', label: 'Replace in Note', commandId: APP_COMMAND_IDS.editReplaceInNote },
|
||||
{ kind: 'command', label: 'Find in Vault', commandId: APP_COMMAND_IDS.editFindInVault },
|
||||
{ kind: 'command', label: 'Paste without Formatting', commandId: APP_COMMAND_IDS.editPastePlainText },
|
||||
{ kind: 'command', label: 'Toggle Note List Search', commandId: 'edit-toggle-note-list-search' },
|
||||
{ kind: 'command', label: 'Toggle Diff Mode', commandId: APP_COMMAND_IDS.editToggleDiff },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
items: [
|
||||
{ kind: 'command', label: 'Editor Only', commandId: APP_COMMAND_IDS.viewEditorOnly },
|
||||
{ kind: 'command', label: 'Editor + Notes', commandId: APP_COMMAND_IDS.viewEditorList },
|
||||
{ kind: 'command', label: 'All Panels', commandId: APP_COMMAND_IDS.viewAll },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Toggle Properties Panel', commandId: APP_COMMAND_IDS.viewToggleProperties },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Zoom In', commandId: APP_COMMAND_IDS.viewZoomIn },
|
||||
{ kind: 'command', label: 'Zoom Out', commandId: APP_COMMAND_IDS.viewZoomOut },
|
||||
{ kind: 'command', label: 'Actual Size', commandId: APP_COMMAND_IDS.viewZoomReset },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Command Palette', commandId: APP_COMMAND_IDS.viewCommandPalette },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Go',
|
||||
items: [
|
||||
{ kind: 'command', label: 'All Notes', commandId: APP_COMMAND_IDS.goAllNotes },
|
||||
{ kind: 'command', label: 'Archived', commandId: APP_COMMAND_IDS.goArchived },
|
||||
{ kind: 'command', label: 'Changes', commandId: APP_COMMAND_IDS.goChanges },
|
||||
{ kind: 'command', label: 'Inbox', commandId: APP_COMMAND_IDS.goInbox },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Go Back', commandId: APP_COMMAND_IDS.viewGoBack },
|
||||
{ kind: 'command', label: 'Go Forward', commandId: APP_COMMAND_IDS.viewGoForward },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Note',
|
||||
items: [
|
||||
{ kind: 'command', label: 'Toggle Organized', commandId: APP_COMMAND_IDS.noteToggleOrganized },
|
||||
{ kind: 'command', label: 'Archive Note', commandId: APP_COMMAND_IDS.noteArchive },
|
||||
{ kind: 'command', label: 'Delete Note', commandId: APP_COMMAND_IDS.noteDelete },
|
||||
{ kind: 'command', label: 'Restore Deleted Note', commandId: APP_COMMAND_IDS.noteRestoreDeleted },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Open in New Window', commandId: APP_COMMAND_IDS.noteOpenInNewWindow },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Toggle Raw Editor', commandId: APP_COMMAND_IDS.editToggleRawEditor },
|
||||
{ kind: 'command', label: 'Toggle AI Panel', commandId: APP_COMMAND_IDS.viewToggleAiChat },
|
||||
{ kind: 'command', label: 'Toggle Backlinks', commandId: APP_COMMAND_IDS.viewToggleBacklinks },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Vault',
|
||||
items: [
|
||||
{ kind: 'command', label: 'Open Vault…', commandId: APP_COMMAND_IDS.vaultOpen },
|
||||
{ kind: 'command', label: 'Remove Vault from List', commandId: APP_COMMAND_IDS.vaultRemove },
|
||||
{ kind: 'command', label: 'Restore Getting Started', commandId: APP_COMMAND_IDS.vaultRestoreGettingStarted },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Add Remote…', commandId: APP_COMMAND_IDS.vaultAddRemote },
|
||||
{ kind: 'command', label: 'Commit & Push', commandId: APP_COMMAND_IDS.vaultCommitPush },
|
||||
{ kind: 'command', label: 'Pull from Remote', commandId: APP_COMMAND_IDS.vaultPull },
|
||||
{ kind: 'command', label: 'Resolve Conflicts', commandId: APP_COMMAND_IDS.vaultResolveConflicts },
|
||||
{ kind: 'command', label: 'View Pending Changes', commandId: APP_COMMAND_IDS.vaultViewChanges },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', label: 'Reload Vault', commandId: APP_COMMAND_IDS.vaultReload },
|
||||
{ kind: 'command', label: 'Repair Vault', commandId: APP_COMMAND_IDS.vaultRepair },
|
||||
{ kind: 'command', label: 'Set Up External AI Tools…', commandId: APP_COMMAND_IDS.vaultInstallMcp },
|
||||
],
|
||||
},
|
||||
...APP_COMMAND_MENU_SECTIONS,
|
||||
{
|
||||
label: 'Window',
|
||||
items: [
|
||||
@@ -121,31 +43,8 @@ const MENU_SECTIONS: ReadonlyArray<MenuSection> = [
|
||||
},
|
||||
]
|
||||
|
||||
function formatShortcutKey(key: string): string {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
return '←'
|
||||
case 'ArrowRight':
|
||||
return '→'
|
||||
default:
|
||||
return key.length === 1 ? key.toUpperCase() : key
|
||||
}
|
||||
}
|
||||
|
||||
function getLinuxShortcut(commandId: MenuCommandId): string | null {
|
||||
if (commandId === 'edit-toggle-note-list-search') {
|
||||
return 'Ctrl+F'
|
||||
}
|
||||
|
||||
const shortcut = APP_COMMAND_DEFINITIONS[commandId].shortcut
|
||||
if (!shortcut) return null
|
||||
|
||||
const modifier = shortcut.combo === 'command-or-ctrl' ? 'Ctrl' : 'Ctrl+Shift'
|
||||
return `${modifier}+${formatShortcutKey(shortcut.key)}`
|
||||
}
|
||||
|
||||
function triggerMenuCommand(commandId: MenuCommandId): void {
|
||||
void invoke('trigger_menu_command', { id: commandId }).catch(() => {})
|
||||
function triggerMenuCommand(menuItemId: string): void {
|
||||
void invoke('trigger_menu_command', { id: menuItemId }).catch(() => {})
|
||||
}
|
||||
|
||||
function HamburgerIcon() {
|
||||
@@ -184,15 +83,14 @@ export function LinuxMenuButton() {
|
||||
}
|
||||
|
||||
if (item.kind === 'command') {
|
||||
const shortcut = getLinuxShortcut(item.commandId)
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={item.commandId}
|
||||
onSelect={() => triggerMenuCommand(item.commandId)}
|
||||
key={item.menuItemId}
|
||||
onSelect={() => triggerMenuCommand(item.menuItemId)}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{shortcut && (
|
||||
<DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>
|
||||
{item.shortcut && (
|
||||
<DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ const {
|
||||
}))
|
||||
|
||||
vi.mock('../utils/platform', () => ({
|
||||
isMac: () => false,
|
||||
shouldUseLinuxWindowChrome: vi.fn(),
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,56 +1,11 @@
|
||||
import appCommandManifest from '../shared/appCommandManifest.json' with { type: 'json' }
|
||||
import type { SidebarFilter } from '../types'
|
||||
import { isMac } from '../utils/platform'
|
||||
import type { ViewMode } from './useViewMode'
|
||||
|
||||
export const APP_COMMAND_IDS = {
|
||||
appSettings: 'app-settings',
|
||||
appCheckForUpdates: 'app-check-for-updates',
|
||||
fileNewNote: 'file-new-note',
|
||||
fileNewType: 'file-new-type',
|
||||
fileQuickOpen: 'file-quick-open',
|
||||
fileSave: 'file-save',
|
||||
editFindInNote: 'edit-find-in-note',
|
||||
editReplaceInNote: 'edit-replace-in-note',
|
||||
editFindInVault: 'edit-find-in-vault',
|
||||
editPastePlainText: 'edit-paste-plain-text',
|
||||
editToggleRawEditor: 'edit-toggle-raw-editor',
|
||||
editToggleDiff: 'edit-toggle-diff',
|
||||
viewEditorOnly: 'view-editor-only',
|
||||
viewEditorList: 'view-editor-list',
|
||||
viewAll: 'view-all',
|
||||
viewToggleProperties: 'view-toggle-properties',
|
||||
viewToggleAiChat: 'view-toggle-ai-chat',
|
||||
viewToggleBacklinks: 'view-toggle-backlinks',
|
||||
viewCommandPalette: 'view-command-palette',
|
||||
viewZoomIn: 'view-zoom-in',
|
||||
viewZoomOut: 'view-zoom-out',
|
||||
viewZoomReset: 'view-zoom-reset',
|
||||
viewGoBack: 'view-go-back',
|
||||
viewGoForward: 'view-go-forward',
|
||||
goAllNotes: 'go-all-notes',
|
||||
goArchived: 'go-archived',
|
||||
goChanges: 'go-changes',
|
||||
goInbox: 'go-inbox',
|
||||
noteToggleOrganized: 'note-toggle-organized',
|
||||
noteToggleFavorite: 'note-toggle-favorite',
|
||||
noteArchive: 'note-archive',
|
||||
noteDelete: 'note-delete',
|
||||
noteOpenInNewWindow: 'note-open-in-new-window',
|
||||
noteRestoreDeleted: 'note-restore-deleted',
|
||||
vaultOpen: 'vault-open',
|
||||
vaultRemove: 'vault-remove',
|
||||
vaultRestoreGettingStarted: 'vault-restore-getting-started',
|
||||
vaultAddRemote: 'vault-add-remote',
|
||||
vaultCommitPush: 'vault-commit-push',
|
||||
vaultPull: 'vault-pull',
|
||||
vaultResolveConflicts: 'vault-resolve-conflicts',
|
||||
vaultViewChanges: 'vault-view-changes',
|
||||
vaultInstallMcp: 'vault-install-mcp',
|
||||
vaultReload: 'vault-reload',
|
||||
vaultRepair: 'vault-repair',
|
||||
} as const
|
||||
type AppCommandKey = keyof typeof appCommandManifest.commands
|
||||
type ShortcutEventLike = Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'key' | 'code'>
|
||||
|
||||
export type AppCommandId = (typeof APP_COMMAND_IDS)[keyof typeof APP_COMMAND_IDS]
|
||||
export type AppCommandShortcutCombo =
|
||||
| 'command-or-ctrl'
|
||||
| 'command-or-ctrl-shift'
|
||||
@@ -58,7 +13,6 @@ export type AppCommandShortcutCombo =
|
||||
export type AppCommandDeterministicQaMode =
|
||||
| 'renderer-shortcut-event'
|
||||
| 'native-menu-command'
|
||||
type ShortcutEventLike = Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'key' | 'code'>
|
||||
|
||||
export interface AppCommandDeterministicQaDefinition {
|
||||
preferredMode: AppCommandDeterministicQaMode
|
||||
@@ -131,6 +85,19 @@ interface AppCommandShortcutDefinition {
|
||||
display: string
|
||||
}
|
||||
|
||||
interface AppCommandManifestShortcutDefinition extends AppCommandShortcutDefinition {
|
||||
accelerator: string
|
||||
requiresManualNativeAcceleratorQa?: boolean
|
||||
}
|
||||
|
||||
interface AppCommandManifestDefinition {
|
||||
id: string
|
||||
route: AppCommandRoute
|
||||
menuOwned: boolean
|
||||
shortcut?: AppCommandManifestShortcutDefinition
|
||||
preferredShortcutQaMode?: AppCommandDeterministicQaMode
|
||||
}
|
||||
|
||||
export interface AppCommandDefinition {
|
||||
route: AppCommandRoute
|
||||
menuOwned: boolean
|
||||
@@ -138,241 +105,183 @@ export interface AppCommandDefinition {
|
||||
preferredShortcutQaMode?: AppCommandDeterministicQaMode
|
||||
}
|
||||
|
||||
export const APP_COMMAND_DEFINITIONS: Record<AppCommandId, AppCommandDefinition> = {
|
||||
[APP_COMMAND_IDS.appSettings]: {
|
||||
route: { kind: 'handler', handler: 'onOpenSettings' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: ',', display: '⌘,' },
|
||||
},
|
||||
[APP_COMMAND_IDS.appCheckForUpdates]: {
|
||||
route: { kind: 'handler', handler: 'onCheckForUpdates' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.fileNewNote]: {
|
||||
route: { kind: 'handler', handler: 'onCreateNote' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'n', code: 'KeyN', display: '⌘N' },
|
||||
},
|
||||
[APP_COMMAND_IDS.fileNewType]: {
|
||||
route: { kind: 'handler', handler: 'onCreateType' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.fileQuickOpen]: {
|
||||
route: { kind: 'handler', handler: 'onQuickOpen' },
|
||||
menuOwned: true,
|
||||
shortcut: {
|
||||
combo: 'command-or-ctrl',
|
||||
key: 'p',
|
||||
aliases: ['o'],
|
||||
code: 'KeyP',
|
||||
display: '⌘P / ⌘O',
|
||||
},
|
||||
},
|
||||
[APP_COMMAND_IDS.fileSave]: {
|
||||
route: { kind: 'handler', handler: 'onSave' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 's', code: 'KeyS', display: '⌘S' },
|
||||
},
|
||||
[APP_COMMAND_IDS.editFindInNote]: {
|
||||
route: { kind: 'handler', handler: 'onFindInNote' },
|
||||
menuOwned: true,
|
||||
preferredShortcutQaMode: 'renderer-shortcut-event',
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'f', code: 'KeyF', display: '⌘F' },
|
||||
},
|
||||
[APP_COMMAND_IDS.editReplaceInNote]: {
|
||||
route: { kind: 'handler', handler: 'onReplaceInNote' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.editFindInVault]: {
|
||||
route: { kind: 'handler', handler: 'onSearch' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl-shift', key: 'f', code: 'KeyF', display: '⌘⇧F' },
|
||||
},
|
||||
[APP_COMMAND_IDS.editPastePlainText]: {
|
||||
route: { kind: 'handler', handler: 'onPastePlainText' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl-shift', key: 'v', code: 'KeyV', display: '⌘⇧V' },
|
||||
},
|
||||
[APP_COMMAND_IDS.editToggleRawEditor]: {
|
||||
route: { kind: 'handler', handler: 'onToggleRawEditor' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '\\', display: '⌘\\' },
|
||||
},
|
||||
[APP_COMMAND_IDS.editToggleDiff]: {
|
||||
route: { kind: 'handler', handler: 'onToggleDiff' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.viewEditorOnly]: {
|
||||
route: { kind: 'view-mode', value: 'editor-only' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '1', display: '⌘1' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewEditorList]: {
|
||||
route: { kind: 'view-mode', value: 'editor-list' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '2', display: '⌘2' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewAll]: {
|
||||
route: { kind: 'view-mode', value: 'all' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '3', display: '⌘3' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewToggleProperties]: {
|
||||
route: { kind: 'handler', handler: 'onToggleInspector' },
|
||||
menuOwned: true,
|
||||
preferredShortcutQaMode: 'renderer-shortcut-event',
|
||||
shortcut: { combo: 'command-or-ctrl-shift', key: 'i', code: 'KeyI', display: '⌘⇧I' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewToggleAiChat]: {
|
||||
route: { kind: 'handler', handler: 'onToggleAIChat' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl-shift', key: 'l', code: 'KeyL', display: '⌘⇧L' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewToggleBacklinks]: {
|
||||
route: { kind: 'handler', handler: 'onToggleInspector' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.viewCommandPalette]: {
|
||||
route: { kind: 'handler', handler: 'onCommandPalette' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'k', code: 'KeyK', display: '⌘K' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewZoomIn]: {
|
||||
route: { kind: 'handler', handler: 'onZoomIn' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '=', aliases: ['+'], display: '⌘=' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewZoomOut]: {
|
||||
route: { kind: 'handler', handler: 'onZoomOut' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '-', display: '⌘-' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewZoomReset]: {
|
||||
route: { kind: 'handler', handler: 'onZoomReset' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: '0', display: '⌘0' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewGoBack]: {
|
||||
route: { kind: 'handler', handler: 'onGoBack' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'ArrowLeft', code: 'ArrowLeft', display: '⌘←' },
|
||||
},
|
||||
[APP_COMMAND_IDS.viewGoForward]: {
|
||||
route: { kind: 'handler', handler: 'onGoForward' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'ArrowRight', code: 'ArrowRight', display: '⌘→' },
|
||||
},
|
||||
[APP_COMMAND_IDS.goAllNotes]: {
|
||||
route: { kind: 'filter', value: 'all' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.goArchived]: {
|
||||
route: { kind: 'filter', value: 'archived' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.goChanges]: {
|
||||
route: { kind: 'filter', value: 'changes' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.goInbox]: {
|
||||
route: { kind: 'filter', value: 'inbox' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.noteToggleOrganized]: {
|
||||
route: { kind: 'active-tab-handler', handler: 'onToggleOrganized' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'e', code: 'KeyE', display: '⌘E' },
|
||||
},
|
||||
[APP_COMMAND_IDS.noteToggleFavorite]: {
|
||||
route: { kind: 'active-tab-handler', handler: 'onToggleFavorite' },
|
||||
menuOwned: false,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'd', code: 'KeyD', display: '⌘D' },
|
||||
},
|
||||
[APP_COMMAND_IDS.noteArchive]: {
|
||||
route: { kind: 'active-tab-handler', handler: 'onArchiveNote' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.noteDelete]: {
|
||||
route: { kind: 'active-tab-handler', handler: 'onDeleteNote' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl', key: 'Backspace', aliases: ['Delete'], display: '⌘⌫' },
|
||||
},
|
||||
[APP_COMMAND_IDS.noteOpenInNewWindow]: {
|
||||
route: { kind: 'handler', handler: 'onOpenInNewWindow' },
|
||||
menuOwned: true,
|
||||
shortcut: { combo: 'command-or-ctrl-shift', key: 'o', code: 'KeyO', display: '⌘⇧O' },
|
||||
},
|
||||
[APP_COMMAND_IDS.noteRestoreDeleted]: {
|
||||
route: { kind: 'handler', handler: 'onRestoreDeletedNote' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultOpen]: {
|
||||
route: { kind: 'handler', handler: 'onOpenVault' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultRemove]: {
|
||||
route: { kind: 'handler', handler: 'onRemoveActiveVault' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultRestoreGettingStarted]: {
|
||||
route: { kind: 'handler', handler: 'onRestoreGettingStarted' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultAddRemote]: {
|
||||
route: { kind: 'handler', handler: 'onAddRemote' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultCommitPush]: {
|
||||
route: { kind: 'handler', handler: 'onCommitPush' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultPull]: {
|
||||
route: { kind: 'handler', handler: 'onPull' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultResolveConflicts]: {
|
||||
route: { kind: 'handler', handler: 'onResolveConflicts' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultViewChanges]: {
|
||||
route: { kind: 'handler', handler: 'onViewChanges' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultInstallMcp]: {
|
||||
route: { kind: 'handler', handler: 'onInstallMcp' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultReload]: {
|
||||
route: { kind: 'handler', handler: 'onReloadVault' },
|
||||
menuOwned: true,
|
||||
},
|
||||
[APP_COMMAND_IDS.vaultRepair]: {
|
||||
route: { kind: 'handler', handler: 'onRepairVault' },
|
||||
menuOwned: true,
|
||||
},
|
||||
type PlatformLabel = string | {
|
||||
macos?: string
|
||||
windows?: string
|
||||
linux?: string
|
||||
default: string
|
||||
}
|
||||
|
||||
type AppCommandMenuManifestItem =
|
||||
| { kind: 'separator' }
|
||||
| {
|
||||
kind: 'command'
|
||||
command: AppCommandKey
|
||||
id?: string
|
||||
label: PlatformLabel
|
||||
accelerator?: string | null
|
||||
enabled?: boolean
|
||||
}
|
||||
| {
|
||||
kind: 'menu-event'
|
||||
id: string
|
||||
label: PlatformLabel
|
||||
accelerator?: string | null
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
interface AppCommandMenuManifestSection {
|
||||
label: string
|
||||
items: AppCommandMenuManifestItem[]
|
||||
}
|
||||
|
||||
export type AppCommandMenuItem =
|
||||
| { kind: 'separator' }
|
||||
| {
|
||||
kind: 'command'
|
||||
commandId: string
|
||||
menuItemId: string
|
||||
label: string
|
||||
shortcut?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
type AppCommandMenuStateGroupReference =
|
||||
| { command: AppCommandKey }
|
||||
| { id: string }
|
||||
|
||||
type AppCommandMenuStateGroupName = keyof typeof appCommandManifest.menuStateGroups
|
||||
|
||||
const APP_COMMAND_MANIFEST_COMMANDS = appCommandManifest.commands as Record<AppCommandKey, AppCommandManifestDefinition>
|
||||
const APP_COMMAND_MANIFEST_MENUS = appCommandManifest.menus as AppCommandMenuManifestSection[]
|
||||
const APP_COMMAND_MANIFEST_APP_MENU = appCommandManifest.appMenu as AppCommandMenuManifestItem[]
|
||||
const APP_COMMAND_MANIFEST_STATE_GROUPS = appCommandManifest.menuStateGroups as Record<
|
||||
AppCommandMenuStateGroupName,
|
||||
AppCommandMenuStateGroupReference[]
|
||||
>
|
||||
|
||||
export const APP_COMMAND_IDS = Object.fromEntries(
|
||||
Object.entries(APP_COMMAND_MANIFEST_COMMANDS).map(([key, command]) => [key, command.id]),
|
||||
) as { readonly [K in AppCommandKey]: string }
|
||||
|
||||
export type AppCommandId = (typeof APP_COMMAND_IDS)[AppCommandKey]
|
||||
|
||||
const APP_COMMAND_SET = new Set<string>(Object.values(APP_COMMAND_IDS))
|
||||
|
||||
function toShortcutDefinition(
|
||||
shortcut: AppCommandManifestShortcutDefinition | undefined,
|
||||
): AppCommandShortcutDefinition | undefined {
|
||||
if (!shortcut) return undefined
|
||||
|
||||
return {
|
||||
combo: shortcut.combo,
|
||||
key: shortcut.key,
|
||||
aliases: shortcut.aliases,
|
||||
code: shortcut.code,
|
||||
display: shortcut.display,
|
||||
}
|
||||
}
|
||||
|
||||
export const APP_COMMAND_DEFINITIONS = Object.fromEntries(
|
||||
Object.values(APP_COMMAND_MANIFEST_COMMANDS).map((command) => [
|
||||
command.id,
|
||||
{
|
||||
route: command.route,
|
||||
menuOwned: command.menuOwned,
|
||||
shortcut: toShortcutDefinition(command.shortcut),
|
||||
preferredShortcutQaMode: command.preferredShortcutQaMode,
|
||||
},
|
||||
]),
|
||||
) as Record<AppCommandId, AppCommandDefinition>
|
||||
|
||||
function resolvePlatformLabel(label: PlatformLabel): string {
|
||||
if (typeof label === 'string') return label
|
||||
if (isMac() && label.macos) return label.macos
|
||||
return label.default
|
||||
}
|
||||
|
||||
function formatAcceleratorDisplay(accelerator: string): string {
|
||||
const commandPrefix = isMac() ? '⌘' : 'Ctrl+'
|
||||
const commandShiftPrefix = isMac() ? '⌘⇧' : 'Ctrl+Shift+'
|
||||
|
||||
return accelerator
|
||||
.replaceAll('CmdOrCtrl+Shift+', commandShiftPrefix)
|
||||
.replaceAll('CmdOrCtrl+', commandPrefix)
|
||||
.replaceAll('Backspace', isMac() ? '⌫' : 'Backspace')
|
||||
.replaceAll('Delete', isMac() ? '⌦' : 'Delete')
|
||||
.replaceAll('Left', isMac() ? '←' : 'Left')
|
||||
.replaceAll('Right', isMac() ? '→' : 'Right')
|
||||
.replaceAll('Enter', isMac() ? '↵' : 'Enter')
|
||||
}
|
||||
|
||||
function menuShortcutForCommand(
|
||||
item: Extract<AppCommandMenuManifestItem, { kind: 'command' }>,
|
||||
command: AppCommandManifestDefinition,
|
||||
): string | undefined {
|
||||
if (typeof item.accelerator === 'string') return formatAcceleratorDisplay(item.accelerator)
|
||||
if (command.shortcut) return formatShortcutDisplay(command.shortcut)
|
||||
return undefined
|
||||
}
|
||||
|
||||
function toMenuItem(item: AppCommandMenuManifestItem): AppCommandMenuItem {
|
||||
if (item.kind === 'separator') return { kind: 'separator' }
|
||||
|
||||
if (item.kind === 'menu-event') {
|
||||
return {
|
||||
kind: 'command',
|
||||
commandId: item.id,
|
||||
menuItemId: item.id,
|
||||
label: resolvePlatformLabel(item.label),
|
||||
shortcut: typeof item.accelerator === 'string'
|
||||
? formatAcceleratorDisplay(item.accelerator)
|
||||
: undefined,
|
||||
enabled: item.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
const command = APP_COMMAND_MANIFEST_COMMANDS[item.command]
|
||||
return {
|
||||
kind: 'command',
|
||||
commandId: command.id,
|
||||
menuItemId: item.id ?? command.id,
|
||||
label: resolvePlatformLabel(item.label),
|
||||
shortcut: menuShortcutForCommand(item, command),
|
||||
enabled: item.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
function menuCommandIds(items: AppCommandMenuItem[]): string[] {
|
||||
return items.flatMap(item => item.kind === 'command' ? [item.commandId] : [])
|
||||
}
|
||||
|
||||
export const APP_COMMAND_MENU_SECTIONS = APP_COMMAND_MANIFEST_MENUS.map(section => ({
|
||||
label: section.label,
|
||||
items: section.items.map(toMenuItem),
|
||||
}))
|
||||
|
||||
const APP_COMMAND_APP_MENU_ITEMS = APP_COMMAND_MANIFEST_APP_MENU.map(toMenuItem)
|
||||
|
||||
export const APP_COMMAND_MENU_STATE_GROUPS = Object.fromEntries(
|
||||
Object.entries(APP_COMMAND_MANIFEST_STATE_GROUPS).map(([name, references]) => [
|
||||
name,
|
||||
references.map(reference => 'command' in reference
|
||||
? APP_COMMAND_MANIFEST_COMMANDS[reference.command].id
|
||||
: reference.id),
|
||||
]),
|
||||
) as Record<AppCommandMenuStateGroupName, string[]>
|
||||
|
||||
const NATIVE_MENU_COMMAND_SET = new Set<string>(
|
||||
(Object.entries(APP_COMMAND_DEFINITIONS) as Array<[AppCommandId, AppCommandDefinition]>)
|
||||
.filter(([, definition]) => definition.menuOwned)
|
||||
.map(([id]) => id),
|
||||
[
|
||||
...APP_COMMAND_MENU_SECTIONS.flatMap(section => menuCommandIds(section.items)),
|
||||
...menuCommandIds(APP_COMMAND_APP_MENU_ITEMS),
|
||||
].filter(id => APP_COMMAND_SET.has(id)),
|
||||
)
|
||||
|
||||
const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set<AppCommandId>([
|
||||
APP_COMMAND_IDS.appSettings,
|
||||
APP_COMMAND_IDS.fileNewNote,
|
||||
APP_COMMAND_IDS.fileQuickOpen,
|
||||
APP_COMMAND_IDS.fileSave,
|
||||
APP_COMMAND_IDS.editFindInNote,
|
||||
APP_COMMAND_IDS.editFindInVault,
|
||||
APP_COMMAND_IDS.editPastePlainText,
|
||||
APP_COMMAND_IDS.viewToggleAiChat,
|
||||
APP_COMMAND_IDS.viewCommandPalette,
|
||||
APP_COMMAND_IDS.noteToggleOrganized,
|
||||
APP_COMMAND_IDS.noteToggleFavorite,
|
||||
])
|
||||
const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set<AppCommandId>(
|
||||
Object.values(APP_COMMAND_MANIFEST_COMMANDS)
|
||||
.filter(command => command.shortcut?.requiresManualNativeAcceleratorQa)
|
||||
.map(command => command.id),
|
||||
)
|
||||
|
||||
const shortcutKeyMaps = {
|
||||
'command-or-ctrl': new Map<string, AppCommandId>(),
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from './appCommandDispatcher'
|
||||
import {
|
||||
getDeterministicShortcutQaDefinition,
|
||||
APP_COMMAND_MENU_SECTIONS,
|
||||
APP_COMMAND_MENU_STATE_GROUPS,
|
||||
getShortcutEventInit,
|
||||
} from './appCommandCatalog'
|
||||
|
||||
@@ -103,6 +105,31 @@ describe('appCommandDispatcher', () => {
|
||||
expect(isNativeMenuCommandId(APP_COMMAND_IDS.noteToggleFavorite)).toBe(false)
|
||||
})
|
||||
|
||||
it('derives native menu command IDs from the shared command menu manifest', () => {
|
||||
const menuCommandIds = APP_COMMAND_MENU_SECTIONS.flatMap(section =>
|
||||
section.items.flatMap(item => item.kind === 'command' ? [item.commandId] : []),
|
||||
)
|
||||
|
||||
expect(menuCommandIds).toContain(APP_COMMAND_IDS.fileNewNote)
|
||||
expect(menuCommandIds).toContain(APP_COMMAND_IDS.editPastePlainText)
|
||||
expect(menuCommandIds).toContain(APP_COMMAND_IDS.viewGoBack)
|
||||
expect(menuCommandIds).not.toContain(APP_COMMAND_IDS.noteToggleFavorite)
|
||||
})
|
||||
|
||||
it('keeps native menu state groups inside the shared command menu manifest', () => {
|
||||
const menuCommandIds = new Set(
|
||||
APP_COMMAND_MENU_SECTIONS.flatMap(section =>
|
||||
section.items.flatMap(item => item.kind === 'command' ? [item.menuItemId] : []),
|
||||
),
|
||||
)
|
||||
|
||||
for (const commandIds of Object.values(APP_COMMAND_MENU_STATE_GROUPS)) {
|
||||
for (const commandId of commandIds) {
|
||||
expect(menuCommandIds.has(commandId)).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('finds raw editor, AI, and plain-text paste shortcuts from the shared catalog', () => {
|
||||
expect(findShortcutCommandId('command-or-ctrl', 'o', 'KeyO')).toBe(APP_COMMAND_IDS.fileQuickOpen)
|
||||
expect(findShortcutCommandId('command-or-ctrl', '\\')).toBe(APP_COMMAND_IDS.editToggleRawEditor)
|
||||
|
||||
536
src/shared/appCommandManifest.json
Normal file
536
src/shared/appCommandManifest.json
Normal file
@@ -0,0 +1,536 @@
|
||||
{
|
||||
"commands": {
|
||||
"appSettings": {
|
||||
"id": "app-settings",
|
||||
"route": { "kind": "handler", "handler": "onOpenSettings" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": ",",
|
||||
"display": "⌘,",
|
||||
"accelerator": "CmdOrCtrl+,",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"appCheckForUpdates": {
|
||||
"id": "app-check-for-updates",
|
||||
"route": { "kind": "handler", "handler": "onCheckForUpdates" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"fileNewNote": {
|
||||
"id": "file-new-note",
|
||||
"route": { "kind": "handler", "handler": "onCreateNote" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "n",
|
||||
"code": "KeyN",
|
||||
"display": "⌘N",
|
||||
"accelerator": "CmdOrCtrl+N",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"fileNewType": {
|
||||
"id": "file-new-type",
|
||||
"route": { "kind": "handler", "handler": "onCreateType" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"fileQuickOpen": {
|
||||
"id": "file-quick-open",
|
||||
"route": { "kind": "handler", "handler": "onQuickOpen" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "p",
|
||||
"aliases": ["o"],
|
||||
"code": "KeyP",
|
||||
"display": "⌘P / ⌘O",
|
||||
"accelerator": "CmdOrCtrl+P",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"fileSave": {
|
||||
"id": "file-save",
|
||||
"route": { "kind": "handler", "handler": "onSave" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "s",
|
||||
"code": "KeyS",
|
||||
"display": "⌘S",
|
||||
"accelerator": "CmdOrCtrl+S",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"editFindInNote": {
|
||||
"id": "edit-find-in-note",
|
||||
"route": { "kind": "handler", "handler": "onFindInNote" },
|
||||
"menuOwned": true,
|
||||
"preferredShortcutQaMode": "renderer-shortcut-event",
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "f",
|
||||
"code": "KeyF",
|
||||
"display": "⌘F",
|
||||
"accelerator": "CmdOrCtrl+F",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"editReplaceInNote": {
|
||||
"id": "edit-replace-in-note",
|
||||
"route": { "kind": "handler", "handler": "onReplaceInNote" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"editFindInVault": {
|
||||
"id": "edit-find-in-vault",
|
||||
"route": { "kind": "handler", "handler": "onSearch" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl-shift",
|
||||
"key": "f",
|
||||
"code": "KeyF",
|
||||
"display": "⌘⇧F",
|
||||
"accelerator": "CmdOrCtrl+Shift+F",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"editPastePlainText": {
|
||||
"id": "edit-paste-plain-text",
|
||||
"route": { "kind": "handler", "handler": "onPastePlainText" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl-shift",
|
||||
"key": "v",
|
||||
"code": "KeyV",
|
||||
"display": "⌘⇧V",
|
||||
"accelerator": "CmdOrCtrl+Shift+V",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"editToggleRawEditor": {
|
||||
"id": "edit-toggle-raw-editor",
|
||||
"route": { "kind": "handler", "handler": "onToggleRawEditor" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "\\",
|
||||
"display": "⌘\\",
|
||||
"accelerator": "CmdOrCtrl+\\"
|
||||
}
|
||||
},
|
||||
"editToggleDiff": {
|
||||
"id": "edit-toggle-diff",
|
||||
"route": { "kind": "handler", "handler": "onToggleDiff" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"viewEditorOnly": {
|
||||
"id": "view-editor-only",
|
||||
"route": { "kind": "view-mode", "value": "editor-only" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "1",
|
||||
"display": "⌘1",
|
||||
"accelerator": "CmdOrCtrl+1"
|
||||
}
|
||||
},
|
||||
"viewEditorList": {
|
||||
"id": "view-editor-list",
|
||||
"route": { "kind": "view-mode", "value": "editor-list" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "2",
|
||||
"display": "⌘2",
|
||||
"accelerator": "CmdOrCtrl+2"
|
||||
}
|
||||
},
|
||||
"viewAll": {
|
||||
"id": "view-all",
|
||||
"route": { "kind": "view-mode", "value": "all" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "3",
|
||||
"display": "⌘3",
|
||||
"accelerator": "CmdOrCtrl+3"
|
||||
}
|
||||
},
|
||||
"viewToggleProperties": {
|
||||
"id": "view-toggle-properties",
|
||||
"route": { "kind": "handler", "handler": "onToggleInspector" },
|
||||
"menuOwned": true,
|
||||
"preferredShortcutQaMode": "renderer-shortcut-event",
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl-shift",
|
||||
"key": "i",
|
||||
"code": "KeyI",
|
||||
"display": "⌘⇧I",
|
||||
"accelerator": "CmdOrCtrl+Shift+I"
|
||||
}
|
||||
},
|
||||
"viewToggleAiChat": {
|
||||
"id": "view-toggle-ai-chat",
|
||||
"route": { "kind": "handler", "handler": "onToggleAIChat" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl-shift",
|
||||
"key": "l",
|
||||
"code": "KeyL",
|
||||
"display": "⌘⇧L",
|
||||
"accelerator": "CmdOrCtrl+Shift+L",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"viewToggleBacklinks": {
|
||||
"id": "view-toggle-backlinks",
|
||||
"route": { "kind": "handler", "handler": "onToggleInspector" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"viewCommandPalette": {
|
||||
"id": "view-command-palette",
|
||||
"route": { "kind": "handler", "handler": "onCommandPalette" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "k",
|
||||
"code": "KeyK",
|
||||
"display": "⌘K",
|
||||
"accelerator": "CmdOrCtrl+K",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"viewZoomIn": {
|
||||
"id": "view-zoom-in",
|
||||
"route": { "kind": "handler", "handler": "onZoomIn" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "=",
|
||||
"aliases": ["+"],
|
||||
"display": "⌘=",
|
||||
"accelerator": "CmdOrCtrl+="
|
||||
}
|
||||
},
|
||||
"viewZoomOut": {
|
||||
"id": "view-zoom-out",
|
||||
"route": { "kind": "handler", "handler": "onZoomOut" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "-",
|
||||
"display": "⌘-",
|
||||
"accelerator": "CmdOrCtrl+-"
|
||||
}
|
||||
},
|
||||
"viewZoomReset": {
|
||||
"id": "view-zoom-reset",
|
||||
"route": { "kind": "handler", "handler": "onZoomReset" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "0",
|
||||
"display": "⌘0",
|
||||
"accelerator": "CmdOrCtrl+0"
|
||||
}
|
||||
},
|
||||
"viewGoBack": {
|
||||
"id": "view-go-back",
|
||||
"route": { "kind": "handler", "handler": "onGoBack" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "ArrowLeft",
|
||||
"code": "ArrowLeft",
|
||||
"display": "⌘←",
|
||||
"accelerator": "CmdOrCtrl+Left"
|
||||
}
|
||||
},
|
||||
"viewGoForward": {
|
||||
"id": "view-go-forward",
|
||||
"route": { "kind": "handler", "handler": "onGoForward" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "ArrowRight",
|
||||
"code": "ArrowRight",
|
||||
"display": "⌘→",
|
||||
"accelerator": "CmdOrCtrl+Right"
|
||||
}
|
||||
},
|
||||
"goAllNotes": {
|
||||
"id": "go-all-notes",
|
||||
"route": { "kind": "filter", "value": "all" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"goArchived": {
|
||||
"id": "go-archived",
|
||||
"route": { "kind": "filter", "value": "archived" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"goChanges": {
|
||||
"id": "go-changes",
|
||||
"route": { "kind": "filter", "value": "changes" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"goInbox": {
|
||||
"id": "go-inbox",
|
||||
"route": { "kind": "filter", "value": "inbox" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"noteToggleOrganized": {
|
||||
"id": "note-toggle-organized",
|
||||
"route": { "kind": "active-tab-handler", "handler": "onToggleOrganized" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "e",
|
||||
"code": "KeyE",
|
||||
"display": "⌘E",
|
||||
"accelerator": "CmdOrCtrl+E",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"noteToggleFavorite": {
|
||||
"id": "note-toggle-favorite",
|
||||
"route": { "kind": "active-tab-handler", "handler": "onToggleFavorite" },
|
||||
"menuOwned": false,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "d",
|
||||
"code": "KeyD",
|
||||
"display": "⌘D",
|
||||
"accelerator": "CmdOrCtrl+D",
|
||||
"requiresManualNativeAcceleratorQa": true
|
||||
}
|
||||
},
|
||||
"noteArchive": {
|
||||
"id": "note-archive",
|
||||
"route": { "kind": "active-tab-handler", "handler": "onArchiveNote" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"noteDelete": {
|
||||
"id": "note-delete",
|
||||
"route": { "kind": "active-tab-handler", "handler": "onDeleteNote" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl",
|
||||
"key": "Backspace",
|
||||
"aliases": ["Delete"],
|
||||
"display": "⌘⌫",
|
||||
"accelerator": "CmdOrCtrl+Backspace"
|
||||
}
|
||||
},
|
||||
"noteOpenInNewWindow": {
|
||||
"id": "note-open-in-new-window",
|
||||
"route": { "kind": "handler", "handler": "onOpenInNewWindow" },
|
||||
"menuOwned": true,
|
||||
"shortcut": {
|
||||
"combo": "command-or-ctrl-shift",
|
||||
"key": "o",
|
||||
"code": "KeyO",
|
||||
"display": "⌘⇧O",
|
||||
"accelerator": "CmdOrCtrl+Shift+O"
|
||||
}
|
||||
},
|
||||
"noteRestoreDeleted": {
|
||||
"id": "note-restore-deleted",
|
||||
"route": { "kind": "handler", "handler": "onRestoreDeletedNote" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultOpen": {
|
||||
"id": "vault-open",
|
||||
"route": { "kind": "handler", "handler": "onOpenVault" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultRemove": {
|
||||
"id": "vault-remove",
|
||||
"route": { "kind": "handler", "handler": "onRemoveActiveVault" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultRestoreGettingStarted": {
|
||||
"id": "vault-restore-getting-started",
|
||||
"route": { "kind": "handler", "handler": "onRestoreGettingStarted" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultAddRemote": {
|
||||
"id": "vault-add-remote",
|
||||
"route": { "kind": "handler", "handler": "onAddRemote" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultCommitPush": {
|
||||
"id": "vault-commit-push",
|
||||
"route": { "kind": "handler", "handler": "onCommitPush" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultPull": {
|
||||
"id": "vault-pull",
|
||||
"route": { "kind": "handler", "handler": "onPull" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultResolveConflicts": {
|
||||
"id": "vault-resolve-conflicts",
|
||||
"route": { "kind": "handler", "handler": "onResolveConflicts" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultViewChanges": {
|
||||
"id": "vault-view-changes",
|
||||
"route": { "kind": "handler", "handler": "onViewChanges" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultInstallMcp": {
|
||||
"id": "vault-install-mcp",
|
||||
"route": { "kind": "handler", "handler": "onInstallMcp" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultReload": {
|
||||
"id": "vault-reload",
|
||||
"route": { "kind": "handler", "handler": "onReloadVault" },
|
||||
"menuOwned": true
|
||||
},
|
||||
"vaultRepair": {
|
||||
"id": "vault-repair",
|
||||
"route": { "kind": "handler", "handler": "onRepairVault" },
|
||||
"menuOwned": true
|
||||
}
|
||||
},
|
||||
"menus": [
|
||||
{
|
||||
"label": "File",
|
||||
"items": [
|
||||
{ "kind": "command", "command": "fileNewNote", "label": "New Note" },
|
||||
{ "kind": "command", "command": "fileNewType", "label": "New Type" },
|
||||
{ "kind": "command", "command": "fileQuickOpen", "label": "Quick Open" },
|
||||
{
|
||||
"kind": "command",
|
||||
"command": "fileQuickOpen",
|
||||
"id": "file-quick-open-alias",
|
||||
"label": { "macos": "Quick Open (Cmd+O)", "default": "Quick Open (Ctrl+O)" },
|
||||
"accelerator": "CmdOrCtrl+O"
|
||||
},
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "fileSave", "label": "Save" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Edit",
|
||||
"items": [
|
||||
{ "kind": "command", "command": "editPastePlainText", "label": "Paste without Formatting" },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "editFindInNote", "label": "Find in Note", "enabled": false },
|
||||
{ "kind": "command", "command": "editReplaceInNote", "label": "Replace in Note", "enabled": false },
|
||||
{ "kind": "command", "command": "editFindInVault", "label": "Find in Vault" },
|
||||
{
|
||||
"kind": "menu-event",
|
||||
"id": "edit-toggle-note-list-search",
|
||||
"label": "Toggle Note List Search",
|
||||
"accelerator": "CmdOrCtrl+F",
|
||||
"enabled": false
|
||||
},
|
||||
{ "kind": "command", "command": "editToggleDiff", "label": "Toggle Diff Mode" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "View",
|
||||
"items": [
|
||||
{ "kind": "command", "command": "viewEditorOnly", "label": "Editor Only" },
|
||||
{ "kind": "command", "command": "viewEditorList", "label": "Editor + Notes" },
|
||||
{ "kind": "command", "command": "viewAll", "label": "All Panels" },
|
||||
{ "kind": "separator" },
|
||||
{
|
||||
"kind": "command",
|
||||
"command": "viewToggleProperties",
|
||||
"label": "Toggle Properties Panel",
|
||||
"accelerator": null
|
||||
},
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "viewZoomIn", "label": "Zoom In" },
|
||||
{ "kind": "command", "command": "viewZoomOut", "label": "Zoom Out" },
|
||||
{ "kind": "command", "command": "viewZoomReset", "label": "Actual Size" },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "viewCommandPalette", "label": "Command Palette" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Go",
|
||||
"items": [
|
||||
{ "kind": "command", "command": "goAllNotes", "label": "All Notes" },
|
||||
{ "kind": "command", "command": "goArchived", "label": "Archived" },
|
||||
{ "kind": "command", "command": "goChanges", "label": "Changes" },
|
||||
{ "kind": "command", "command": "goInbox", "label": "Inbox" },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "viewGoBack", "label": "Go Back" },
|
||||
{ "kind": "command", "command": "viewGoForward", "label": "Go Forward" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Note",
|
||||
"items": [
|
||||
{ "kind": "command", "command": "noteToggleOrganized", "label": "Toggle Organized" },
|
||||
{ "kind": "command", "command": "noteArchive", "label": "Archive Note" },
|
||||
{ "kind": "command", "command": "noteDelete", "label": "Delete Note" },
|
||||
{ "kind": "command", "command": "noteRestoreDeleted", "label": "Restore Deleted Note", "enabled": false },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "noteOpenInNewWindow", "label": "Open in New Window" },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "editToggleRawEditor", "label": "Toggle Raw Editor" },
|
||||
{ "kind": "command", "command": "viewToggleAiChat", "label": "Toggle AI Panel" },
|
||||
{ "kind": "command", "command": "viewToggleBacklinks", "label": "Toggle Backlinks" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Vault",
|
||||
"items": [
|
||||
{ "kind": "command", "command": "vaultOpen", "label": "Open Vault…" },
|
||||
{ "kind": "command", "command": "vaultRemove", "label": "Remove Vault from List" },
|
||||
{ "kind": "command", "command": "vaultRestoreGettingStarted", "label": "Restore Getting Started" },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "vaultAddRemote", "label": "Add Remote…", "enabled": false },
|
||||
{ "kind": "command", "command": "vaultCommitPush", "label": "Commit & Push" },
|
||||
{ "kind": "command", "command": "vaultPull", "label": "Pull from Remote" },
|
||||
{ "kind": "command", "command": "vaultResolveConflicts", "label": "Resolve Conflicts", "enabled": false },
|
||||
{ "kind": "command", "command": "vaultViewChanges", "label": "View Pending Changes" },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "vaultReload", "label": "Reload Vault" },
|
||||
{ "kind": "command", "command": "vaultRepair", "label": "Repair Vault" },
|
||||
{ "kind": "command", "command": "vaultInstallMcp", "label": "Set Up External AI Tools…" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"appMenu": [
|
||||
{ "kind": "command", "command": "appCheckForUpdates", "label": "Check for Updates..." },
|
||||
{ "kind": "separator" },
|
||||
{ "kind": "command", "command": "appSettings", "label": "Settings..." }
|
||||
],
|
||||
"menuStateGroups": {
|
||||
"noteDependent": [
|
||||
{ "command": "fileSave" },
|
||||
{ "command": "noteToggleOrganized" },
|
||||
{ "command": "noteArchive" },
|
||||
{ "command": "noteDelete" },
|
||||
{ "command": "editToggleRawEditor" },
|
||||
{ "command": "editToggleDiff" },
|
||||
{ "command": "viewToggleBacklinks" },
|
||||
{ "command": "noteOpenInNewWindow" }
|
||||
],
|
||||
"editorFindDependent": [
|
||||
{ "command": "editFindInNote" },
|
||||
{ "command": "editReplaceInNote" }
|
||||
],
|
||||
"noteListSearchDependent": [
|
||||
{ "id": "edit-toggle-note-list-search" }
|
||||
],
|
||||
"restoreDeletedDependent": [
|
||||
{ "command": "noteRestoreDeleted" }
|
||||
],
|
||||
"gitCommitDependent": [
|
||||
{ "command": "vaultCommitPush" }
|
||||
],
|
||||
"gitConflictDependent": [
|
||||
{ "command": "vaultResolveConflicts" }
|
||||
],
|
||||
"gitNoRemoteDependent": [
|
||||
{ "command": "vaultAddRemote" }
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user