Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Extension API] Custom commands and keybindings #1075

Merged
merged 20 commits into from
Oct 3, 2024
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,35 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d

### Node developers API

<details>
<summary>v1.3.7: Register commands and keybindings</summary>

Extensions can call the following API to register commands and keybindings. Do
note that keybindings defined in core cannot be overwritten, and some keybindings
are reserved by the browser.

```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
alert('TestCommand')
}
}
],
keybindings: [
{
combo: { key: 'k' },
commandId: 'TestCommand'
}
]
})
```

</details>

<details>
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>

Expand Down
28 changes: 28 additions & 0 deletions browser_tests/extensionAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,32 @@ test.describe('Topbar commands', () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo'])
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
})

test('Should allow registering keybindings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const app = window['app']
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
window['TestCommand'] = true
}
}
],
keybindings: [
{
combo: { key: 'k' },
commandId: 'TestCommand'
}
]
})
})

await comfyPage.page.keyboard.press('k')
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
true
)
})
})
19 changes: 17 additions & 2 deletions src/extensions/core/keybinds.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { app } from '../../scripts/app'
import { api } from '../../scripts/api'
import { useToastStore } from '@/stores/toastStore'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useCommandStore } from '@/stores/commandStore'

app.registerExtension({
name: 'Comfy.Keybinds',
init() {
const keybindListener = async function (event) {
const keybindListener = async function (event: KeyboardEvent) {
// Ignore keybindings for legacy jest tests as jest tests don't have
// a Vue app instance or pinia stores.
if (!app.vueAppReady) return

const keyCombo = KeyComboImpl.fromEvent(event)
const keybindingStore = useKeybindingStore()
const commandStore = useCommandStore()
const keybinding = keybindingStore.getKeybinding(keyCombo)
if (keybinding) {
await commandStore.getCommandFunction(keybinding.commandId)()
return
}

const modifierPressed = event.ctrlKey || event.metaKey

// Queue prompt using (ctrl or command) + enter
Expand All @@ -26,7 +41,7 @@ app.registerExtension({
return
}

const target = event.composedPath()[0]
const target = event.composedPath()[0] as HTMLElement
if (
target.tagName === 'TEXTAREA' ||
target.tagName === 'INPUT' ||
Expand Down
6 changes: 6 additions & 0 deletions src/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import type { ToastMessageOptions } from 'primevue/toast'
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
import { useExecutionStore } from '@/stores/executionStore'
import { IWidget } from '@comfyorg/litegraph'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useCommandStore } from '@/stores/commandStore'

export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'

Expand Down Expand Up @@ -2951,6 +2953,10 @@ export class ComfyApp {
if (this.extensions.find((ext) => ext.name === extension.name)) {
throw new Error(`Extension named '${extension.name}' already registered.`)
}
if (this.vueAppReady) {
useKeybindingStore().loadExtensionKeybindings(extension)
useCommandStore().loadExtensionCommands(extension)
}
this.extensions.push(extension)
}

Expand Down
2 changes: 0 additions & 2 deletions src/scripts/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,5 @@ export class ComfyLogging {
if (!this.enabled) return
const source = 'ComfyUI.Logging'
this.addEntry(source, 'debug', { UserAgent: navigator.userAgent })
const systemStats = await api.getSystemStats()
this.addEntry(source, 'debug', systemStats)
}
}
11 changes: 4 additions & 7 deletions src/scripts/ui/menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import { downloadBlob } from '../../utils'
import { ComfyButtonGroup } from '../components/buttonGroup'
import './menu.css'

// Import ComfyButton to make sure it's shimmed and exported by vite
import { ComfyButton } from '../components/button'
import { ComfySplitButton } from '../components/splitButton'
import { ComfyPopup } from '../components/popup'
console.debug(
`Keep following definitions ${ComfyButton} ${ComfySplitButton} ${ComfyPopup}`
)
// Export to make sure following components are shimmed and exported by vite
export { ComfyButton } from '../components/button'
export { ComfySplitButton } from '../components/splitButton'
export { ComfyPopup } from '../components/popup'

export class ComfyAppMenu {
app: ComfyApp
Expand Down
12 changes: 11 additions & 1 deletion src/stores/commandStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useToastStore } from '@/stores/toastStore'
import { showTemplateWorkflowsDialog } from '@/services/dialogService'
import { useQueueStore } from './queueStore'
import { LiteGraph } from '@comfyorg/litegraph'
import { ComfyExtension } from '@/types/comfy'

export interface ComfyCommand {
id: string
Expand Down Expand Up @@ -246,10 +247,19 @@ export const useCommandStore = defineStore('command', () => {
return !!commands.value[command]
}

const loadExtensionCommands = (extension: ComfyExtension) => {
if (extension.commands) {
for (const command of extension.commands) {
registerCommand(command)
}
}
}

return {
getCommand,
getCommandFunction,
registerCommand,
isRegistered
isRegistered,
loadExtensionCommands
}
})
3 changes: 3 additions & 0 deletions src/stores/coreKeybindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Keybinding } from '@/types/keyBindingTypes'

export const CORE_KEYBINDINGS: Keybinding[] = []
15 changes: 15 additions & 0 deletions src/stores/coreSettings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Keybinding } from '@/types/keyBindingTypes'
import { NodeBadgeMode } from '@/types/nodeSource'
import {
LinkReleaseTriggerAction,
Expand Down Expand Up @@ -404,5 +405,19 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'number',
defaultValue: 100,
versionAdded: '1.3.5'
},
{
id: 'Comfy.Keybinding.UnsetBindings',
name: 'Keybindings unset by the user',
type: 'hidden',
defaultValue: [] as Keybinding[],
versionAdded: '1.3.7'
},
{
id: 'Comfy.Keybinding.NewBindings',
name: 'Keybindings set by the user',
type: 'hidden',
defaultValue: [] as Keybinding[],
versionAdded: '1.3.7'
}
]
Loading
Loading