diff --git a/client/codemirror/editor_state.ts b/client/codemirror/editor_state.ts index 2262862e..f366fe28 100644 --- a/client/codemirror/editor_state.ts +++ b/client/codemirror/editor_state.ts @@ -341,31 +341,48 @@ export function createCommandKeyBindings(client: Client): KeyBinding[] { const currentEditor = client.documentEditor?.name; const requiredEditor = def.requireEditor; - if (def.key && isValidEditor(currentEditor, requiredEditor)) { - commandKeyBindings.push({ - key: def.key, - mac: def.mac, - run: (): boolean => { - if (def.contexts) { - const context = client.getContext(); - if (!context || !def.contexts.includes(context)) { - return false; - } + if ((def.key || def.mac) && isValidEditor(currentEditor, requiredEditor)) { + const run = (): boolean => { + if (def.contexts) { + const context = client.getContext(); + if (!context || !def.contexts.includes(context)) { + return false; } - Promise.resolve([]) - .then(def.run) - .catch((e: any) => { - client.reportError(e, "key"); - }).then((returnValue: any) => { - // Always be focusing the editor after running a command - if (returnValue !== false) { - client.focus(); - } - }); + } + Promise.resolve([]) + .then(def.run) + .catch((e: any) => { + client.reportError(e, "key"); + }).then((returnValue: any) => { + // Always be focusing the editor after running a command UNLESS it returns false + if (returnValue !== false) { + client.focus(); + } + }); - return true; - }, - }); + return true; + }; + // Only create a generic key handler (non-mac specific) when + // EITHER we're not on a mac, or we're on a mac AND not specific mac key binding is set + if (def.key && (!isMacLike || (isMacLike && !def.mac))) { + if (Array.isArray(def.key)) { + for (const key of def.key) { + commandKeyBindings.push({ key, run }); + } + } else { + commandKeyBindings.push({ key: def.key, run }); + } + } + // Only set mac key handlers if we're on a mac, because... you know, logic + if (def.mac && isMacLike) { + if (Array.isArray(def.mac)) { + for (const key of def.mac) { + commandKeyBindings.push({ mac: key, run }); + } + } else { + commandKeyBindings.push({ mac: def.mac, run }); + } + } } } @@ -400,3 +417,9 @@ export function createKeyBindings(client: Client): Extension { ]); } } + +/** + * Checks if the current platform is Mac-like (Mac, iPhone, iPod, iPad). + * @returns A boolean indicating if the platform is Mac-like. + */ +export const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); diff --git a/client/components/command_palette.tsx b/client/components/command_palette.tsx index ba05bca9..60483cc8 100644 --- a/client/components/command_palette.tsx +++ b/client/components/command_palette.tsx @@ -2,6 +2,7 @@ import { FilterList } from "./filter.tsx"; import { Terminal } from "preact-feather"; import type { Command } from "../types/command.ts"; import type { FilterOption } from "@silverbulletmd/silverbullet/type/client"; +import { isMacLike } from "../codemirror/editor_state.ts"; export function CommandPalette({ commands, @@ -15,7 +16,6 @@ export function CommandPalette({ onTrigger: (command: Command | undefined) => void; }) { const options: FilterOption[] = []; - const isMac = isMacLike(); for (const [name, def] of commands.entries()) { if (def.hide) { continue; @@ -23,7 +23,7 @@ export function CommandPalette({ options.push({ name: name, - hint: isMac && def.mac ? def.mac : def.key, + hint: keyboardHint(def), orderId: def.lastRun !== undefined ? -def.lastRun : def.priority || Infinity, @@ -51,10 +51,20 @@ export function CommandPalette({ ); } -/** - * Checks if the current platform is Mac-like (Mac, iPhone, iPod, iPad). - * @returns A boolean indicating if the platform is Mac-like. - */ -function isMacLike() { - return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); +function keyboardHint(def: Command): string | undefined { + const shortcuts: string[] = []; + if (isMacLike && def.mac) { + if (Array.isArray(def.mac)) { + shortcuts.push(...def.mac); + } else { + shortcuts.push(def.mac); + } + } else if (def.key) { + if (Array.isArray(def.key)) { + shortcuts.push(...def.key); + } else { + shortcuts.push(def.key); + } + } + return shortcuts.length > 0 ? shortcuts.join(" | ") : undefined; } diff --git a/libraries/Library/Std/APIs/Schema.md b/libraries/Library/Std/APIs/Schema.md index b863303f..3921059b 100644 --- a/libraries/Library/Std/APIs/Schema.md +++ b/libraries/Library/Std/APIs/Schema.md @@ -73,4 +73,8 @@ end function schema.func() return {} end + +function schema.null() + return { type = "null" } +end ``` diff --git a/libraries/Library/Std/Config.md b/libraries/Library/Std/Config.md index eebf80ab..c4f4c9b2 100644 --- a/libraries/Library/Std/Config.md +++ b/libraries/Library/Std/Config.md @@ -245,9 +245,21 @@ config.define("commands", { name = schema.string(), contexts = schema.nullableArray "string", priority = schema.nullable "number", - key = schema.nullable "string", - mac = schema.nullable "string", - hid = schema.nullable "boolean", + key = { + anyOf = { + schema.string(), + schema.array("string"), + schema.null() + } + }, + mac = { + anyOf = { + schema.string(), + schema.array("string"), + schema.null() + } + }, + hide = schema.nullable "boolean", requireMode = schema.nullable { type = "string", enum = {"rw", "ro"}, diff --git a/libraries/Library/Std/Infrastructure/Page Templates.md b/libraries/Library/Std/Infrastructure/Page Templates.md index 60da5e16..d49dea9a 100644 --- a/libraries/Library/Std/Infrastructure/Page Templates.md +++ b/libraries/Library/Std/Infrastructure/Page Templates.md @@ -67,7 +67,7 @@ that are built in. ~~~ --- command: Quick Note -key: "Alt-Shift-n" +key: "Ctrl-q q" suggestedName: "Quick notes/${os.date('%Y-%m-%d/%H-%M-%S')}" confirmName: false tags: meta/template/page @@ -138,7 +138,7 @@ end command.define { name = "Page: From Template", - key = "Alt-shift-t", + key = "Ctrl-q t", run = function() local pageTemplates = query[[ from index.tag "meta/template/page" diff --git a/libraries/Library/Std/Page Templates/Page Template.md b/libraries/Library/Std/Page Templates/Page Template.md index 1d39f16a..dd479e11 100644 --- a/libraries/Library/Std/Page Templates/Page Template.md +++ b/libraries/Library/Std/Page Templates/Page Template.md @@ -16,8 +16,8 @@ frontmatter: | ## To trigger the page template with a custom command #command: Create My Custom Page ## To trigger the page template with a custom key - #key: "Alt-Shift-q" - #mac: "Cmd-Shift-q" + #key: "Ctrl-q c" + #mac: "Cmd-Alt-q" ## To prefill frontmatter #frontmatter: | # date: "${"$"}{date.today()}" diff --git a/libraries/Library/Std/Page Templates/Quick Note.md b/libraries/Library/Std/Page Templates/Quick Note.md index 9001a689..2595012b 100644 --- a/libraries/Library/Std/Page Templates/Quick Note.md +++ b/libraries/Library/Std/Page Templates/Quick Note.md @@ -1,7 +1,9 @@ --- command: Quick Note description: A page template to create a quick note -key: "Alt-Shift-n" +key: +- "Ctrl-q q" +- "Ctrl-q Ctrl-q" suggestedName: "Inbox/${os.date('%Y-%m-%d/%H-%M-%S')}" confirmName: false tags: meta/template/page diff --git a/plug-api/types/manifest.ts b/plug-api/types/manifest.ts index 0eb45572..61ec8c96 100644 --- a/plug-api/types/manifest.ts +++ b/plug-api/types/manifest.ts @@ -15,8 +15,8 @@ export type CommandDef = { priority?: number; // Bind to keyboard shortcut - key?: string; - mac?: string; + key?: string | string[]; + mac?: string | string[]; hide?: boolean; requireMode?: "rw" | "ro"; diff --git a/plugs/editor/editor.plug.yaml b/plugs/editor/editor.plug.yaml index 7a9dc5a7..0b9d3c9d 100644 --- a/plugs/editor/editor.plug.yaml +++ b/plugs/editor/editor.plug.yaml @@ -90,7 +90,6 @@ functions: path: editor.ts:refreshAllWidgets command: name: "Widgets: Refresh All" - key: "Alt-q" requireEditor: page languageComplete: @@ -114,7 +113,7 @@ functions: path: "./navigate.ts:navigateCommand" command: name: "Navigate: Home" - key: "Alt-h" + key: "Ctrl-g h" page: "" moveToPos: path: "./editor.ts:moveToPosCommand" @@ -214,7 +213,7 @@ functions: path: ./text.ts:wrapSelection command: name: "Text: Marker" - key: "Alt-m" + key: "Ctrl-Alt-m" wrapper: "==" requireMode: rw requireEditor: page @@ -298,7 +297,9 @@ functions: path: ./outline.ts:moveItemUp command: name: "Outline: Move Up" - key: "Alt-ArrowUp" + key: + - "Alt-ArrowUp" + - "Mod-. k" requireMode: rw requireEditor: page @@ -306,7 +307,9 @@ functions: path: ./outline.ts:moveItemDown command: name: "Outline: Move Down" - key: "Alt-ArrowDown" + key: + - "Alt-ArrowDown" + - "Mod-. j" requireMode: rw requireEditor: page @@ -314,7 +317,7 @@ functions: path: ./outline.ts:indentItem command: name: "Outline: Move Right" - key: "Alt->" + key: "Mod-. l" requireMode: rw requireEditor: page @@ -322,7 +325,7 @@ functions: path: ./outline.ts:outdentItem command: name: "Outline: Move Left" - key: "Alt-<" + key: "Mod-. h" requireMode: rw requireEditor: page @@ -331,31 +334,27 @@ functions: path: ./outline.ts:foldCommand command: name: "Outline: Fold" - key: "Ctrl-Alt-[" requireEditor: page unfoldCommand: path: ./outline.ts:unfoldCommand command: name: "Outline: Unfold" - key: "Ctrl-Alt-]" requireEditor: page toggleFoldCommand: path: ./outline.ts:toggleFoldCommand command: name: "Outline: Toggle Fold" - key: "Alt-Shift-f" + key: "Mod-. Mod-." requireEditor: page foldAllCommand: path: ./outline.ts:foldAllCommand command: name: "Outline: Fold All" - key: "Ctrl-Alt-Shift-[" requireEditor: page unfoldAllCommand: path: ./outline.ts:unfoldAllCommand command: name: "Outline: Unfold All" - key: "Ctrl-Alt-Shift-]" requireEditor: page findInPageCommand: path: editor.ts:findInPageCommand diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index 3ae4c529..52d59dab 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -59,7 +59,6 @@ functions: path: "./refactor.ts:renamePageCommand" command: name: "Page: Rename" - mac: Cmd-Alt-r page: "" requireMode: rw requireEditor: page @@ -73,8 +72,6 @@ functions: path: "./refactor.ts:renamePageLinkCommand" command: name: "Page: Rename Linked Page" - mac: Cmd-Ctrl-Alt-r - key: Ctrl-Shift-Alt-r requireMode: rw requireEditor: page @@ -119,7 +116,7 @@ functions: path: ./task.ts:taskCycleCommand command: name: "Task: Cycle State" - key: Alt-t + key: Mod-. t requireMode: rw requireEditor: page diff --git a/plugs/sync/sync.plug.yaml b/plugs/sync/sync.plug.yaml index 7dcc75cb..06570b7e 100644 --- a/plugs/sync/sync.plug.yaml +++ b/plugs/sync/sync.plug.yaml @@ -1,13 +1,10 @@ name: sync functions: - # Commands syncSpaceCommand: path: "./sync.ts:syncSpaceCommand" command: name: "Sync: Space" - key: "Alt-Shift-s" - mac: "Cmd-Shift-s" syncFileCommand: path: "./sync.ts:syncFileCommand" diff --git a/scripts/playground_space/index.md b/scripts/playground_space/index.md index 2ad337bf..bf1934b3 100644 --- a/scripts/playground_space/index.md +++ b/scripts/playground_space/index.md @@ -6,7 +6,7 @@ So go ahead, play around, but don't keep anything valuable here. It will be gone ## Don’t just sit there, try it! * Click on the page picker (book icon) icon at the top right, or hit `Cmd-k` (Mac) or `Ctrl-k` (Linux and Windows) to open the **page switcher**. Type the name of a non-existent page to create it (although it won’t save in this environment). * Click on the terminal button (top right) or hit `Cmd-/` (Mac) or `Ctrl-/` (Linux and Windows) to open the **command palette** (note that not all commands will work in this mode). -* Select some text and hit `Alt-m` to ==highlight== it, or `Cmd-b` (Mac) or `Ctrl-b` (Windows/Linux) to make it **bold**, or `Cmd-i` (Mac) or `Ctrl-i` (Windows/Linux) to make it _italic_. +* Select some text and hit `Cmd-b` (Mac) or `Ctrl-b` (Windows/Linux) to make it **bold**, or `Cmd-i` (Mac) or `Ctrl-i` (Windows/Linux) to make it _italic_. * Click a link somewhere on this page to navigate there. * Start typing `[[` somewhere to insert a page link (with completion). * [ ] Tap this box 👈 to mark this task as done. @@ -16,4 +16,3 @@ So go ahead, play around, but don't keep anything valuable here. It will be gone * Open this site on your phone or tablet and... it just works! * Are you using a browser with **PWA support** (e.g., any Chromium-based browser or recent version of Safari)? Click on that little icon to the right of your location bar that says “Install SilverBullet” to give SB its own window frame and desktop icon, like it is a stand-alone app. Now, unplug your network cable and reload the page. It still works! - diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 0423c520..f28ab945 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -3,8 +3,22 @@ An attempt at documenting the changes/new features introduced in each release. ## Edge Whenever a commit is pushed to the `main` branch, within ~10 minutes, it will be released as a docker image with the `:v2` tag, and a binary in the [edge release](https://github.com/silverbulletmd/silverbullet/releases/tag/edge). If you want to live on the bleeding edge of SilverBullet goodness (or regression) this is where to do it. -* Migrated core indexer APIs to SB core˜ - +* Upgraded dependencies, specifically CodeMirror. CodeMirror now no longer allows `Alt-` and `Alt-` [[Keyboard Shortcuts]], meaning I had to remap a few existing ones. It’s basically a mission impossible to pick great ones, but I tried: + * `Quick note` is now bound to both `Ctrl-q q` (type `Ctrl-q` first, then hit `q` again) and `Ctrl-q Ctrl-q` (hit `Ctrl-q` twice) + * `Navigate: Home` is now bound to `Ctrl-g h` + * `Text: Marker` is now bound to `Ctrl-Alt-m` + * [[Outlines]] commands generally now use a `Mod-.` (`Cmd-.` on mac, `Ctrl-.` on Linux/Windows) prefix: + * `Outline: Move Right`: `Mod-. l` + * `Outline: Move Left`: `Mod-. h` + * `Outline: Move Up`: `Alt-ArrowUp` still works, but now also adds `Mod-. k` for consistency + * `Outline: Move Down`: `Alt-ArrowDow` still works, but now also adds `Mod-. j` for consistency + * `Outline: Toggle Fold`: `Mod-. Mod-.` + * `Outline: * Fold` (other fold commands): keyboard disabled, readd yourself if you need them (see [[Keyboard Shortcuts]]) + * `Task: Cycle State`: `Mod-. t` + * `Page: Rename` keyboard shortcut removed + * `Page: Rename Linked Page` keyboard shortcut removed + * `Sync: Space` keyboard shortcut removed +* As documented in [[Keyboard Shortcuts]], it is now possible to specify _multiple_ keyboard shortcuts to a commands. ## 2.4.0 * Indexer rework (note: upgrading will start a full space reindex automatically): diff --git a/website/Document Editor.md b/website/Document Editor.md index 88cbe33b..88e9304a 100644 --- a/website/Document Editor.md +++ b/website/Document Editor.md @@ -50,4 +50,4 @@ window.silverbullet.sendMessage("file-saved", { data: new TextEncoder().encode(" ``` If a the document changed and a save is necessary you can send the `file-changed` message. Silverbullet will request a save automatically. -You can also send events from the outside using the `editor.sendMessage` syscall. If you want to communicate the other way around you can call syscalls using `window.silverbullet.syscall(name: string, ...args: any[])`. \ No newline at end of file +You can also send events from the outside using the `editor.sendMessage` syscall. If you want to communicate the other way around you can call syscalls using `window.silverbullet.syscall(name: string, ...args: any[])`. diff --git a/website/Document.md b/website/Document.md index 40bd1f59..a166506d 100644 --- a/website/Document.md +++ b/website/Document.md @@ -32,4 +32,4 @@ These follow the same relative/absolute path rules as links described before. In addition, media can be _sized_ using the following syntax: * Specifying only a width: `![Alt text|300](image.png)` or `![[image.png|300]]` * Specifying only a height: `![Alt text|x300](image.png)` or `![[image.png|x300]]` -* Specifying both width and height: `![Hello|300x300](image.png)` or `![[image.png|300x300]]` \ No newline at end of file +* Specifying both width and height: `![Hello|300x300](image.png)` or `![[image.png|300x300]]` diff --git a/website/Keyboard Shortcuts.md b/website/Keyboard Shortcuts.md new file mode 100644 index 00000000..69989867 --- /dev/null +++ b/website/Keyboard Shortcuts.md @@ -0,0 +1,76 @@ +SilverBullet uses [CodeMirror](https://codemirror.net) under the hood, which does the majority of the keyboard handling. In addition, it allows to for customization of (most) keys on a per-command basis. When you open the [[Command Palette]], you will see the currently assigned keyboard shortcut(s). + +# Modifier keys +Your keyboard shortcut will typically consist of a combination of modifier keys as well as a letter or (special) character or number. The following modifier keys are supported: + +* `Alt` +* `Ctrl` +* `Shift` +* `Cmd`: Command key (Mac only) +* `Meta`: usually the “Windows” key +* `Mod`: An alias for `Ctrl` on Linux and Windows, to `Cmd` on Mac + +The keyboard shortcut is then combined to e.g. `Mod-Alt-t`. + +# Constraints +SilverBullet is constrained in what keyboard can be used in a few ways: +* By the browser: you cannot (usually) override browser keyboard shortcuts, or at least shouldn’t (e.g. `Cmd-t` or `Ctrl-t` for opening a new tab) +* By already used keyboard shortcuts, see [[#Disabling keyboard shortcuts]] below to disable keyboard shortcuts for existing commands. +* By CodeMirror’s restrictions. There a few, here are some we know about: + * `Alt-letter` is not allowed (e.g. `Alt-a`), because it clashes with various (international) keyboards + +# Combos +Generally keyboard shortcuts are single stroke, however it possible to configure them as “combos” (multiple shortcuts entered in sequence). Out of the box, SilverBullet reserves two prefix keys for this “combo” use: + +* `Mod-.` (so “command dot” or Mac and “Ctrl dot” on Linux/Windows), used for various [[Outlines]] related commands +* `Ctrl-q` used and reserved for [[Page Template]] use cases + * `Ctrl-q q` creates a [[^Library/Std/Page Templates/Quick Note]] + * `Ctrl-q t` runs the `Page: From Template` command +* `Ctrl-g` reserved for navigational commands: + * `Ctrl-g h` navigates to the index (home) page + +# Assigning keyboard shortcuts +Keyboard shortcuts can be assigned to [[Command|Commands]] with `key` or `mac`. If only `key` is defined, this keyboard shortcut will be used for all operating systems, if `mac` is set, this keyboard will be used for macOS. + +To (re)assign a keyboard shortcut for an _existing_ command, you can use the [[^Library/Std/APIs/Command#command.update(commandDef)]] API as follows: +```lua +command.update { + name = "Task: Cycle State", + key = "Ctrl-Shift-t", +} +``` + +When you define your own command, you can simply make them part of your command definition: + +```lua +command.define { + name = "My command", + key = "Ctrl-Shift-t", + run = function() + editor.flashNotification "Hello world " + end +} +``` + +You can also specify multiple keyboard shortcuts by passing in a table to either `key` or `mac`: + +```lua +command.define { + name = "My command", + key = {"Ctrl-Shift-t", "Ctrl-Shift-o"}, + run = function() + editor.flashNotification "Hello world " + end +} +``` + +# Disabling keyboard shortcuts +You can use the [[^Library/Std/APIs/Command#command.update(commandDef)]] API to disable keyboard shortcuts of existing commands by setting them to an empty string, for example: + +```lua +command.update { + name = "Task: Cycle State", + key = "", + mac = "" +} +``` diff --git a/website/Outlines.md b/website/Outlines.md index 14aba0fb..e498066f 100644 --- a/website/Outlines.md +++ b/website/Outlines.md @@ -19,17 +19,19 @@ An outline is simply a (nested) bulleted list, for instance: * Solution * Conclusion -SilverBullet offers a number of `Outline` commands to make manipulating such outlines easier, they are: +# Commands +SilverBullet offers a number of `Outline` commands to make manipulating such outlines easier, they are (note that on Mac `Mod` binds to `Cmd`, on Linux and Windows it’s `Ctrl`): * `Outline: Move Up` (`Alt-ArrowUp`): moves the current item and its children up * `Outline: Move Down` (`Alt-ArrowDown`): moves the current item and its children down -* `Outline: Move Right` (`Alt->`): indents the current item and its children one level deeper -* `Outline: Move Left` (`Alt-<`): outdents the current item and its children one level higher +* `Outline: Move Right` (`Mod-. l`): indents the current item and its children one level deeper +* `Outline: Move Left` (`Mod-. h`): outdents the current item and its children one level higher + And for folding outlines: -* `Outline: Fold` (`Ctrl-Alt-[`): folds the current item’s children -* `Outline: Unfold` (`Ctrl-Alt-]`): unfolds the current item’s children -* `Outline: Toggle Fold` (`Ctrl-Shift-f`): toggles the current item’s fold state -* `Outline: Fold All` (`Ctrl-Alt-Shift-[`): folds all sections in the entire page -* `Outline: Unfold All` (`Ctrl-Alt-Shift-]`): unfolds all sections in the entire page +* `Outline: Fold`: folds the current item’s children +* `Outline: Unfold`: unfolds the current item’s children +* `Outline: Toggle Fold` (`Mod-. Mod-.`): toggles the current item’s fold state +* `Outline: Fold All`: folds all sections in the entire page +* `Outline: Unfold All`: unfolds all sections in the entire page