mirror of
https://github.com/logseq/logseq.git
synced 2026-05-16 17:02:34 +00:00
* enhance(libs): add Commands proxy and unified command APIs * enhance(plugin): improve unregistering of simple and palette commands * enhance(plugin): add unregister functionality for plugin commands * chore(libs): add logseq.Commands API guide and implementation * enhance(libs): introduce LSPluginNet for HTTP client functionality * fix(ipc): ensure proper handling of IPC messages and improve plugin call structure * enhance(libs): add HTTP methods (GET, HEAD, POST, PUT, PATCH, DELETE) and error handling * enhance(api): support additional identifier types for opening blocks in sidebar * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Tienson Qin <tiensonqin@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
401 lines
11 KiB
Markdown
401 lines
11 KiB
Markdown
# `logseq.Commands` API Guide
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
import '@logseq/libs'
|
|
|
|
async function main() {
|
|
const unregister = logseq.Commands.register(
|
|
'insert-hello',
|
|
{
|
|
title: 'Insert hello',
|
|
placement: 'slash',
|
|
},
|
|
async () => {
|
|
await logseq.Editor.insertAtEditingCursor('Hello from command!')
|
|
}
|
|
)
|
|
|
|
logseq.beforeunload(async () => {
|
|
unregister && unregister()
|
|
})
|
|
}
|
|
|
|
logseq.ready(main).catch(console.error)
|
|
```
|
|
|
|
## API surface
|
|
|
|
### `Commands.register(id, options, action?)`
|
|
|
|
Register a command and return an unregister handler.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(id, options, action)
|
|
```
|
|
|
|
The same command can also put its handler in `options.handler`:
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register('my-command', {
|
|
title: 'My command',
|
|
placement: 'palette',
|
|
handler: async () => {
|
|
await logseq.UI.showMsg('Run')
|
|
},
|
|
})
|
|
```
|
|
|
|
Return value:
|
|
|
|
- `() => void`: unregister handler.
|
|
- `false`: registration failed, usually because handler/action is missing or invalid.
|
|
|
|
The unregister handler is idempotent. Calling it multiple times is safe.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register('temporary-command', {
|
|
title: 'Temporary command',
|
|
placement: 'palette',
|
|
handler: () => console.log('run'),
|
|
})
|
|
|
|
unregister && unregister()
|
|
unregister && unregister() // safe no-op
|
|
```
|
|
|
|
### `Commands.execute(id, ...args)`
|
|
|
|
Execute a built-in or plugin command.
|
|
|
|
```ts
|
|
await logseq.Commands.execute('logseq.go/home')
|
|
await logseq.Commands.execute('my-plugin/some-command')
|
|
await logseq.Commands.execute('my-plugin.commands.some-command')
|
|
await logseq.Commands.execute('local-command')
|
|
```
|
|
|
|
Supported id forms:
|
|
|
|
| Form | Meaning |
|
|
| ------------------------ | ----------------------------------------------------------- |
|
|
| `logseq.*` | Built-in command, forwarded to `App.invokeExternalCommand`. |
|
|
| `plugin-id/key` | Command registered by another plugin. |
|
|
| `plugin-id.commands.key` | Backward-compatible plugin command address. |
|
|
| `key` | Command registered by the current plugin. |
|
|
|
|
Plugin command keys are normalized to match host registration rules:
|
|
|
|
- trim whitespace;
|
|
- whitespace becomes `_`;
|
|
- lower-case;
|
|
- `:` becomes `-`;
|
|
- numeric first character becomes `_1`, `_2`, etc.
|
|
|
|
## Register options
|
|
|
|
```ts
|
|
type CommandRegisterOptions = {
|
|
key?: string
|
|
title?: string
|
|
label?: string
|
|
desc?: string
|
|
handler?: CommandCallback | BlockCommandCallback | Array<SlashCommandAction>
|
|
when?: string | string[]
|
|
placement?: CommandPlacement
|
|
placements?: CommandPlacement[]
|
|
keybinding?: SimpleCommandKeybinding | string
|
|
extras?: Record<string, any>
|
|
type?: string
|
|
palette?: boolean
|
|
}
|
|
```
|
|
|
|
Important fields:
|
|
|
|
- `key`: stable command key. Defaults to `id`.
|
|
- `title`: display title. `label` is also accepted for compatibility.
|
|
- `desc`: command palette description. Defaults to title/label in host palette rendering.
|
|
- `handler`: command callback. Can be passed here or as third argument.
|
|
- `placement`: single UI surface.
|
|
- `placements`: multiple UI surfaces for one logical command.
|
|
- `keybinding`: string or `{ mode, binding, mac }` shortcut descriptor.
|
|
- `when`: v1 stores this as metadata for future host-side condition evaluation.
|
|
- `extras`: placement-specific metadata, e.g. PDF highlight menu options.
|
|
|
|
## Placements
|
|
|
|
```ts
|
|
type CommandPlacement =
|
|
| 'palette'
|
|
| 'shortcut'
|
|
| 'slash'
|
|
| 'block-context-menu'
|
|
| 'highlight-context-menu'
|
|
| 'page-menu'
|
|
| 'simple'
|
|
```
|
|
|
|
### `palette`
|
|
|
|
Register a command palette item.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'open-dashboard',
|
|
{
|
|
title: 'Open dashboard',
|
|
placement: 'palette',
|
|
},
|
|
async () => {
|
|
logseq.showMainUI()
|
|
}
|
|
)
|
|
```
|
|
|
|
### `shortcut`
|
|
|
|
Register a keyboard shortcut without necessarily showing it in the command palette.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'toggle-panel',
|
|
{
|
|
title: 'Toggle panel',
|
|
placement: 'shortcut',
|
|
keybinding: {
|
|
mode: 'global',
|
|
binding: 'mod+shift+p',
|
|
mac: 'cmd+shift+p',
|
|
},
|
|
},
|
|
() => {
|
|
logseq.toggleMainUI()
|
|
}
|
|
)
|
|
```
|
|
|
|
Shortcut modes:
|
|
|
|
| Mode | Meaning |
|
|
| ------------- | -------------------------------- |
|
|
| `global` | Active globally. |
|
|
| `non-editing` | Active when not editing a block. |
|
|
| `editing` | Active when editing a block. |
|
|
|
|
### `slash`
|
|
|
|
Register a slash command.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'insert-current-date',
|
|
{
|
|
title: 'Insert current date',
|
|
placement: 'slash',
|
|
},
|
|
async () => {
|
|
await logseq.Editor.insertAtEditingCursor(
|
|
new Date().toISOString().slice(0, 10)
|
|
)
|
|
}
|
|
)
|
|
```
|
|
|
|
Slash commands also support the legacy action-step form:
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'big-bang',
|
|
{ title: 'Big Bang', placement: 'slash' },
|
|
[['editor/input', 'Hello'], ['editor/clear-current-slash']]
|
|
)
|
|
```
|
|
|
|
### `block-context-menu`
|
|
|
|
Register a block context menu item.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'copy-block-uuid',
|
|
{
|
|
title: 'Copy block UUID',
|
|
placement: 'block-context-menu',
|
|
},
|
|
async ({ uuid }) => {
|
|
await navigator.clipboard.writeText(uuid)
|
|
}
|
|
)
|
|
```
|
|
|
|
### `page-menu`
|
|
|
|
Register a page menu item.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'analyze-page',
|
|
{
|
|
title: 'Analyze page',
|
|
placement: 'page-menu',
|
|
},
|
|
async ({ page }) => {
|
|
console.log('Analyze page', page)
|
|
}
|
|
)
|
|
```
|
|
|
|
### `highlight-context-menu`
|
|
|
|
Register a PDF highlight context menu item.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'send-highlight-to-inbox',
|
|
{
|
|
title: 'Send highlight to inbox',
|
|
placement: 'highlight-context-menu',
|
|
extras: { clearSelection: true },
|
|
},
|
|
async ({ content }) => {
|
|
await logseq.Editor.appendBlockInPage('Inbox', content)
|
|
}
|
|
)
|
|
```
|
|
|
|
### `simple`
|
|
|
|
Register a command without visible UI placement. This is useful when the command is intended to be executed by another plugin through `Commands.execute`.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'sync-now',
|
|
{
|
|
title: 'Sync now',
|
|
placement: 'simple',
|
|
},
|
|
async () => {
|
|
await syncNow()
|
|
}
|
|
)
|
|
```
|
|
|
|
## Multiple placements
|
|
|
|
Use `placements` when one logical command should appear in multiple places.
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'toggle-focus-mode',
|
|
{
|
|
title: 'Toggle focus mode',
|
|
placements: ['palette', 'shortcut'],
|
|
keybinding: 'mod+shift+f',
|
|
},
|
|
() => {
|
|
toggleFocusMode()
|
|
}
|
|
)
|
|
|
|
// Removes both palette and shortcut registrations.
|
|
unregister && unregister()
|
|
```
|
|
|
|
## Dynamic registration patterns
|
|
|
|
### Enable or disable a command from settings
|
|
|
|
```ts
|
|
let unregisterExport: (() => void) | false | undefined
|
|
|
|
function registerExportCommand() {
|
|
unregisterExport = logseq.Commands.register(
|
|
'export-current-page',
|
|
{ title: 'Export current page', placement: 'palette' },
|
|
exportCurrentPage
|
|
)
|
|
}
|
|
|
|
function unregisterExportCommand() {
|
|
unregisterExport && unregisterExport()
|
|
unregisterExport = undefined
|
|
}
|
|
|
|
async function main() {
|
|
if (logseq.settings?.enableExport) registerExportCommand()
|
|
|
|
logseq.onSettingsChanged((next) => {
|
|
unregisterExportCommand()
|
|
if (next.enableExport) registerExportCommand()
|
|
})
|
|
|
|
logseq.beforeunload(async () => {
|
|
unregisterExportCommand()
|
|
})
|
|
}
|
|
```
|
|
|
|
### Register temporary commands
|
|
|
|
```ts
|
|
const unregister = logseq.Commands.register(
|
|
'temporary-review-action',
|
|
{ title: 'Approve review item', placement: 'palette' },
|
|
approveCurrentReviewItem
|
|
)
|
|
|
|
setTimeout(() => {
|
|
unregister && unregister()
|
|
}, 60_000)
|
|
```
|
|
|
|
## Compatibility with existing APIs
|
|
|
|
Old APIs remain supported. Internally, they share the same command registration path as v1 `logseq.Commands`.
|
|
|
|
| Existing API | Commands equivalent |
|
|
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
| `App.registerCommandPalette({ key, label }, action)` | `Commands.register(key, { title: label, placement: 'palette' }, action)` |
|
|
| `App.registerCommandShortcut(keybinding, action, opts)` | `Commands.register(opts.key, { ...opts, placement: 'shortcut', keybinding }, action)` |
|
|
| `Editor.registerSlashCommand(tag, action)` | `Commands.register(tag, { title: tag, placement: 'slash' }, action)` |
|
|
| `Editor.registerBlockContextMenuItem(label, action)` | `Commands.register(key, { title: label, placement: 'block-context-menu' }, action)` |
|
|
| `Editor.registerHighlightContextMenuItem(label, action, opts)` | `Commands.register(key, { title: label, placement: 'highlight-context-menu', extras: opts }, action)` |
|
|
| `App.registerPageMenuItem(tag, action)` | `Commands.register(key, { title: tag, placement: 'page-menu' }, action)` |
|
|
|
|
These old APIs also return unregister handlers after v1.
|
|
|
|
## Cleanup semantics
|
|
|
|
Commands are cleaned in two ways:
|
|
|
|
1. **Manual unregister**: call the function returned by `register`.
|
|
2. **Plugin lifecycle cleanup**: when the plugin is disabled, reloaded, unloaded, or uninstalled, the host removes all commands registered by that plugin.
|
|
|
|
Manual unregister removes:
|
|
|
|
- local SDK hook listener;
|
|
- simple command registry entry;
|
|
- command palette entry;
|
|
- shortcut registry entry;
|
|
- slash command entry, if any.
|
|
|
|
Plugin lifecycle cleanup removes all commands by plugin id, including palette-only commands without shortcuts.
|
|
|
|
## v1 limitations
|
|
|
|
- `when` is metadata only. The host does not yet evaluate VSCode-like context expressions such as `editor.inCodeBlock` or `graph.isDb`.
|
|
- `Commands.onDidExecute(listener)` is not implemented in v1.
|
|
- Command collision handling still follows existing host behavior. Prefer stable, plugin-scoped keys.
|
|
- `Commands.execute` works with registered plugin commands and built-in `logseq.*` commands; it is not a general RPC replacement for arbitrary plugin model methods.
|
|
|
|
## Best practices
|
|
|
|
- Use a stable `key` for commands that other plugins may call.
|
|
- Keep `title` user-facing and concise.
|
|
- Store unregister handlers for dynamic commands and call them before re-registering.
|
|
- Always clean up dynamic registrations in `logseq.beforeunload`.
|
|
- Prefer `placements` over registering the same handler several times manually.
|
|
- Use `placement: 'simple'` for plugin-to-plugin automation commands that should not appear in UI.
|