Skip to content

Commit 7cb62b0

Browse files
committed
Feat(keymaps): the template pickers get their own shortcuts, ⌘⌥T on macOS (#847)
Creating a note from a template, or inserting one into the current note, had exactly two keyboard routes: Vim's leader (Space t, Space i) and the command palette. With Vim off there was nothing to bind, and rebinding the leader entries could not help, since a leader binding is the one key typed after Space: a chord like ⌘⌥T is not something it can hold. A user on Discord asked for exactly that chord. Two app shortcuts now open the same pickers. "New note from template" ships as ⌘⌥T on macOS, free across the app; on Linux and Windows it starts unbound, since Ctrl+Alt+T opens a terminal on most Linux desktops. "Insert template into current note" ships unbound everywhere, since ⌘⌥I is Electron's DevTools. Both list in Settings → Keymap, take any chord from the recorder or config.toml, and stand down while Settings, a palette, a menu or a dialog is open, the rule tab selection already follows. Insert only answers with a note open, as its palette entry does. The palette shows the chord once one is set and falls back to the leader key only while Vim is on, and the manual lists both. Tests: the Mac default and the unbound Linux and Windows ones, the event a Mac actually sends for ⌘⌥T (Option turns the T into †), no clash between two unbound actions, and the palette's shortcut text. The portable-spelling test skips a default that is deliberately unbound. Verified over CDP in the built app with both stores isolated (check-template-shortcuts.mjs): the packaged 2.55.0 passes 1 of 5 (nothing opens, neither row exists), this build 8 of 8, with Vim off (⌘⌥T from the editor, a template making a note, ⌘⌥Y bound through the recorder opening the insert picker) and with Vim on (⌘⌥T from normal mode, nothing typed). npm run test:run and npm run typecheck clean.
1 parent e28c5ae commit 7cb62b0

8 files changed

Lines changed: 137 additions & 4 deletions

File tree

‎packages/app-core/src/App.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,23 @@ function App(): JSX.Element {
798798
return
799799
}
800800
}
801+
// The template pickers as plain shortcuts, so they no longer need Vim's
802+
// leader (#847). Same overlay rule as tab selection: a picker opened
803+
// under Settings or another palette would strand the user behind it.
804+
if (!tabSelectBlocked && matchesShortcut(e, overrides, 'global.newNoteFromTemplate')) {
805+
e.preventDefault()
806+
state.setTemplatePaletteOpen(true)
807+
return
808+
}
809+
if (
810+
!tabSelectBlocked &&
811+
state.activeNote &&
812+
matchesShortcut(e, overrides, 'global.insertTemplate')
813+
) {
814+
e.preventDefault()
815+
state.openTemplatePaletteForInsert()
816+
return
817+
}
801818
if (matchesShortcut(e, overrides, 'global.exportNotePdf')) {
802819
e.preventDefault()
803820
void state.exportActiveNotePdf()

‎packages/app-core/src/components/HelpView.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ function resolveShortcutKeys(
6666
if (action === 'Search notes (non-Vim mode)') return shortcut(overrides, 'global.searchNotesNonVim')
6767
if (action === 'Open commands') return shortcut(overrides, 'global.commandPalette')
6868
if (action === 'New Quick Note') return shortcut(overrides, 'global.newQuickNote')
69+
if (action === 'New note from template') return shortcut(overrides, 'global.newNoteFromTemplate')
70+
if (action === 'Insert template into current note') return shortcut(overrides, 'global.insertTemplate')
6971
if (action === 'Open Settings') return shortcut(overrides, 'global.openSettings')
7072
if (action === 'Toggle sidebar') return shortcut(overrides, 'global.toggleSidebar')
7173
if (action === 'Toggle connections') return shortcut(overrides, 'global.toggleConnections')

‎packages/app-core/src/lib/commands.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,39 @@ describe('close-tab command shortcut', () => {
270270
})
271271
})
272272

273+
describe('template command shortcuts (#847)', () => {
274+
it('show the template shortcut once one is set, and the leader key only with Vim on', async () => {
275+
const { buildCommands, useStore } = await loadCommands()
276+
// Linux: the template shortcuts ship unbound there.
277+
Object.defineProperty(window, 'zen', {
278+
configurable: true,
279+
value: { ...window.zen, platformSync: () => 'linux' }
280+
})
281+
const shortcutOf = (id: string) => buildCommands().find((c) => c.id === id)?.shortcut
282+
283+
// Insert only exists with a note open.
284+
useStore.setState({
285+
vimMode: false,
286+
keymapOverrides: {},
287+
activeNote: { folder: 'inbox', path: 'inbox/Plan.md', title: 'Plan', body: '' } as never
288+
})
289+
expect(buildCommands().some((c) => c.id === 'template.insert')).toBe(true)
290+
expect(shortcutOf('template.create')).toBeFalsy()
291+
expect(shortcutOf('template.insert')).toBeFalsy()
292+
293+
useStore.setState({ vimMode: true })
294+
expect(shortcutOf('template.create')).toMatch(/ t$/)
295+
expect(shortcutOf('template.insert')).toMatch(/ i$/)
296+
297+
useStore.setState({
298+
vimMode: false,
299+
keymapOverrides: { 'global.newNoteFromTemplate': 'Alt+Mod+T', 'global.insertTemplate': 'Alt+Mod+Y' }
300+
})
301+
expect(shortcutOf('template.create')).toMatch(/T$/)
302+
expect(shortcutOf('template.insert')).toMatch(/Y$/)
303+
})
304+
})
305+
273306
describe('New Note in Current Folder (#403)', () => {
274307
it('creates in the active note folder, not the sidebar browse view', async () => {
275308
const { buildCommands, useStore } = await loadCommands()

‎packages/app-core/src/lib/commands.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,19 @@ export function buildCommands(options?: { includeUnavailable?: boolean }): Comma
196196
title: 'New Note from Template…',
197197
category: 'Note',
198198
keywords: 'template scaffold adr rfc meeting daily weekly boilerplate new',
199-
shortcut: leaderShortcut('vim.leaderTemplatePicker'),
199+
shortcut:
200+
shortcut('global.newNoteFromTemplate') ||
201+
(getState().vimMode ? leaderShortcut('vim.leaderTemplatePicker') : undefined),
200202
run: () => getState().setTemplatePaletteOpen(true)
201203
},
202204
{
203205
id: 'template.insert',
204206
title: 'Insert Template into Current Note…',
205207
category: 'Note',
206208
keywords: 'template insert apply into current note scaffold fill',
207-
shortcut: leaderShortcut('vim.leaderInsertTemplate'),
209+
shortcut:
210+
shortcut('global.insertTemplate') ||
211+
(getState().vimMode ? leaderShortcut('vim.leaderInsertTemplate') : undefined),
208212
when: () => !!getState().activeNote,
209213
run: () => getState().openTemplatePaletteForInsert()
210214
},

‎packages/app-core/src/lib/help.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const HELP_HOW_TO_GUIDES: HelpCard[] = [
104104
{
105105
title: 'Start a note from a template',
106106
body:
107-
'Open the template picker with `Space t`, the `:template` (or `:tmpl`) ex command, or the “New Note from Template…” command palette entry. Pick a built-in template — engineering ones like ADR, RFC/Design Doc, Bug Report, Postmortem, Meeting Notes, and 1:1, or personal ones like Daily Note, Weekly Review, Reading Notes, Journal, Project Kickoff, and To-do — or one of your own. ZenNotes then asks which folder to create it in (defaulting to the folder you are viewing) and fills in variables like the date and week before placing your cursor where the template marks it. To create straight into a specific folder, right-click that folder in the sidebar and choose “New from template”.'
107+
'Open the template picker with `Space t`, the `:template` (or `:tmpl`) ex command, or the “New Note from Template…” command palette entry. It has a shortcut of its own too, which works with Vim mode on or off: `Alt+Mod+T` (⌘⌥T) on macOS, and on Linux and Windows whatever key you give “New note from template” under Settings → Keymap. Pick a built-in template — engineering ones like ADR, RFC/Design Doc, Bug Report, Postmortem, Meeting Notes, and 1:1, or personal ones like Daily Note, Weekly Review, Reading Notes, Journal, Project Kickoff, and To-do — or one of your own. ZenNotes then asks which folder to create it in (defaulting to the folder you are viewing) and fills in variables like the date and week before placing your cursor where the template marks it. To create straight into a specific folder, right-click that folder in the sidebar and choose “New from template”.'
108108
},
109109
{
110110
title: 'Make and edit your own templates',
@@ -495,6 +495,8 @@ export const HELP_SHORTCUT_SECTIONS: HelpShortcutSection[] = [
495495
{ keys: 'Shift+Mod+P', action: 'Open commands', detail: 'Open the command palette.' },
496496
{ keys: 'Mod+N', action: 'New note in current folder', detail: 'Create a note in the active note\u2019s folder (or the browsed folder when no note is open) and focus its title. On Linux and Windows this wins over Vim\u2019s Ctrl+N (cursor down). Rebindable under Settings \u2192 Keymaps.' },
497497
{ keys: 'Shift+Mod+N', action: 'New Quick Note', detail: 'Create a quick capture note in the main window and focus its title.' },
498+
{ keys: 'Alt+Mod+T (macOS)', action: 'New note from template', detail: 'Open the template picker to create a note, with Vim mode on or off. Bound by default on macOS; on Linux and Windows it starts unbound, since Ctrl+Alt+T opens a terminal on most Linux desktops, so set a key under Settings → Keymap.' },
499+
{ keys: 'Unbound', action: 'Insert template into current note', detail: 'Pick a template and insert it at the cursor of the active note, with Vim mode on or off. Unbound until you set a key under Settings → Keymap.' },
498500
{ keys: 'Shift+Mod+Space', action: 'Open quick capture window', detail: 'Open the floating, always-on-top capture window. Bound system-wide (CommandOrControl+Shift+Space by default) so it works over any app; change it under Settings → Editor.' },
499501
{ keys: 'Mod+,', action: 'Open Settings', detail: 'Open settings for appearance, editor behavior, fonts, vault controls, and app details. Settings opens on its search: type to filter, move through the results with ↑/↓ (or Ctrl+J / Ctrl+K), and press Enter to jump to the picked setting. From anywhere in Settings, Mod+F (or / in Vim mode) returns to the search.' },
500502
{ keys: 'Mod+1', action: 'Toggle sidebar', detail: 'Hide or show the left sidebar.' },
@@ -555,7 +557,7 @@ export const HELP_SHORTCUT_SECTIONS: HelpShortcutSection[] = [
555557
{ keys: 'Space r', action: 'Review Cloud conflicts', detail: 'Open whatever Cloud sync is waiting on: the queue of files two devices changed at once, one decision at a time, or the vault settings question, which lists each setting that differs with this device’s value beside the cloud’s and lets you pick a side per setting. The binding and the command palette entry appear only while something is waiting, and open the same dialog as Review in the status bar.' },
556558
{ keys: 'Space g', action: 'Open atlas', detail: 'Open the Atlas view: the whole vault drawn as a map of notes and links.' },
557559
{ keys: 'Space q', action: 'Quick capture window', detail: 'Open the floating, always-on-top capture window, same as the global hotkey.' },
558-
{ keys: 'Space i', action: 'Insert template into note', detail: 'Pick a template and insert it at the cursor of the active note, instead of creating a new note from it.' },
560+
{ keys: 'Space i', action: 'Insert template into note', detail: 'Pick a template and insert it at the cursor of the active note, instead of creating a new note from it. Without Vim mode, give “Insert template into current note” a key under Settings → Keymap.' },
559561
{ keys: 'Space c', action: 'Toggle calendar', detail: 'Show or hide the calendar panel for the active pane.' },
560562
{ keys: 'Space l s', action: 'Toggle favorite', detail: 'Add or remove the active note from the Favorites section shown in the sidebar and on the home view. Folders join it from their context menu.' },
561563
{ keys: 'Space, then pause', action: 'Show leader hints', detail: 'If enabled in Settings, open a which-key style guide for the next available leader actions. Sticky mode keeps it open until `Space` or `Esc`.' },

‎packages/app-core/src/lib/keymaps.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,49 @@ describe('sequenceTokenFromEvent', () => {
230230
})
231231
})
232232

233+
describe('template shortcuts (#847)', () => {
234+
it('bind New note from template to ⌘⌥T on the Mac and leave Linux and Windows unbound', () => {
235+
withPlatform('darwin', () => {
236+
expect(getDefaultKeymapBinding('global.newNoteFromTemplate')).toBe('Alt+Mod+T')
237+
expect(getDefaultKeymapBinding('global.insertTemplate')).toBe('')
238+
})
239+
for (const platform of ['linux', 'win32'] as const) {
240+
withPlatform(platform, () => {
241+
expect(getDefaultKeymapBinding('global.newNoteFromTemplate')).toBe('')
242+
expect(getDefaultKeymapBinding('global.insertTemplate')).toBe('')
243+
})
244+
}
245+
})
246+
247+
it('fire on the chord a Mac actually sends, and never while unbound', () => {
248+
withPlatform('darwin', () => {
249+
// Option turns T into †; the binding still has to match the physical key.
250+
const cmdOptionT = fakeEvent({ key: '†', code: 'KeyT', metaKey: true, altKey: true })
251+
expect(matchesShortcut(cmdOptionT, {}, 'global.newNoteFromTemplate')).toBe(true)
252+
expect(matchesShortcut(cmdOptionT, {}, 'global.insertTemplate')).toBe(false)
253+
})
254+
withPlatform('linux', () => {
255+
const ctrlAltT = fakeEvent({ key: 't', code: 'KeyT', ctrlKey: true, altKey: true })
256+
expect(matchesShortcut(ctrlAltT, {}, 'global.newNoteFromTemplate')).toBe(false)
257+
const bound = { 'global.newNoteFromTemplate': 'Alt+Mod+T' }
258+
expect(matchesShortcut(ctrlAltT, bound, 'global.newNoteFromTemplate')).toBe(true)
259+
})
260+
})
261+
262+
it('have no key to show while unbound, and never clash with each other', () => {
263+
withPlatform('linux', () => {
264+
expect(getKeymapDisplay({}, 'global.insertTemplate')).toBe('')
265+
expect(findKeymapConflict({}, 'global.newNoteFromTemplate', '')).toBeNull()
266+
expect(findKeymapConflict({}, 'global.insertTemplate', 'Alt+Mod+Y')).toBeNull()
267+
})
268+
withPlatform('darwin', () => {
269+
expect(findKeymapConflict({}, 'global.insertTemplate', 'Alt+Mod+T')?.id).toBe(
270+
'global.newNoteFromTemplate'
271+
)
272+
})
273+
})
274+
})
275+
233276
describe('leader keymap definitions', () => {
234277
it('keeps the recent-note toggle portable with a literal Ctrl+Tab Mac default', () => {
235278
withPlatform('darwin', () => {
@@ -252,6 +295,9 @@ describe('leader keymap definitions', () => {
252295
// unchanged on every platform.
253296
for (const def of getKeymapDefinitions()) {
254297
if (def.kind !== 'shortcut') continue
298+
// An action that ships unbound (the template shortcuts off the Mac,
299+
// #847) has no spelling to round-trip.
300+
if (def.defaultBinding === '') continue
255301
for (const platform of ['darwin', 'linux', 'win32'] as const) {
256302
const roundTripped = withPlatform(platform, () =>
257303
normalizeShortcutBinding(def.defaultBinding)

‎packages/app-core/src/lib/keymaps.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type KeymapId =
1414
| "global.commandPalette"
1515
| "global.newQuickNote"
1616
| "global.newNoteHere"
17+
| "global.newNoteFromTemplate"
18+
| "global.insertTemplate"
1719
| "global.openSettings"
1820
| "global.openFile"
1921
| "global.toggleSidebar"
@@ -225,6 +227,31 @@ const KEYMAP_DEFINITIONS: KeymapDefinition[] = [
225227
"Create a note in the active note's folder (or the browsed folder when no note is open) and focus its title. Wins over Vim's Ctrl+N (cursor down) when Mod is Ctrl.",
226228
defaultBinding: "Mod+N",
227229
},
230+
// The template actions were reachable only through Vim's leader (Space t,
231+
// Space i) and the palette, so with Vim off no key could reach them (#847).
232+
// Windows and Linux ship unbound: Ctrl+Alt+T opens a terminal on most Linux
233+
// desktops, and Ctrl/Cmd+Alt+I is Electron's DevTools.
234+
{
235+
id: "global.newNoteFromTemplate",
236+
kind: "shortcut",
237+
scope: "app",
238+
group: "global",
239+
title: "New note from template",
240+
description:
241+
"Open the template picker to create a note, as Vim's Leader t does. Unbound on Windows and Linux until you set a key.",
242+
defaultBinding: "",
243+
defaultBindingMac: "Alt+Mod+T",
244+
},
245+
{
246+
id: "global.insertTemplate",
247+
kind: "shortcut",
248+
scope: "app",
249+
group: "global",
250+
title: "Insert template into current note",
251+
description:
252+
"Render a template into the note you are editing, as Vim's Leader i does. Unbound until you set a key.",
253+
defaultBinding: "",
254+
},
228255
{
229256
id: "global.openSettings",
230257
kind: "shortcut",

‎packages/shared-domain/src/keymaps-catalog.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export const KEYMAP_CATALOG: KeymapCatalogEntry[] = [
4646
{ id: "global.commandPalette", group: "global", defaultBinding: "Shift+Mod+P", title: "Open command palette" },
4747
{ id: "global.newQuickNote", group: "global", defaultBinding: "Shift+Mod+N", title: "New quick note" },
4848
{ id: "global.newNoteHere", group: "global", defaultBinding: "Mod+N", title: "New note in current folder" },
49+
{ id: "global.newNoteFromTemplate", group: "global", defaultBinding: "", defaultBindingMac: "Alt+Mod+T", title: "New note from template" },
50+
{ id: "global.insertTemplate", group: "global", defaultBinding: "", title: "Insert template into current note" },
4951
{ id: "global.openSettings", group: "global", defaultBinding: "Mod+,", title: "Open settings" },
5052
{ id: "global.openFile", group: "global", defaultBinding: "Mod+O", title: "Open file" },
5153
{ id: "global.toggleSidebar", group: "global", defaultBinding: "Mod+1", title: "Toggle sidebar" },

0 commit comments

Comments
 (0)