refactor: migrate keybindings to DDD structure (#8369)

## Summary

Migrate keybindings domain to `src/platform/keybindings/` following DDD
principles.

## Changes

- **What**: Consolidate keybinding-related code (types, store, service,
defaults, reserved keys) into a single domain module with flat structure
- Extracted `KeyComboImpl` and `KeybindingImpl` classes into separate
files
- Updated all consumers to import from new location
- Colocated tests with source files
- Updated stores/README.md and services/README.md to remove migrated
entries

## Review Focus

- Verify all import paths were updated correctly
- Check that the flat structure is appropriate (vs nested core/data/ui
layers)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8369-refactor-migrate-keybindings-to-DDD-structure-2f66d73d36508120b169dc737075fb45)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Christian Byrne
2026-01-29 18:40:58 -08:00
committed by GitHub
parent 13311a46ea
commit 47113b117e
25 changed files with 328 additions and 449 deletions

View File

@@ -123,7 +123,6 @@ The following table lists ALL 46 store instances in the system as of 2025-09-01:
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
| modelStore.ts | useModelStore | Manages AI models information | Models |

View File

@@ -2,11 +2,10 @@ import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import type { ComfyExtension } from '@/types/comfy'
import { useKeybindingStore } from './keybindingStore'
import type { KeybindingImpl } from './keybindingStore'
export interface ComfyCommand {
id: string
function: (metadata?: Record<string, unknown>) => void | Promise<void>

View File

@@ -1,425 +0,0 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
describe('useKeybindingStore', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('should add and retrieve default keybindings', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'A', ctrl: true }
})
store.addDefaultKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
})
it('should add and retrieve user keybindings', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'B', alt: true }
})
store.addUserKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
})
it('should get keybindings by command id', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'C', ctrl: true }
})
store.addDefaultKeybinding(keybinding)
expect(store.getKeybindingsByCommandId('test.command')).toEqual([
keybinding
])
})
it('should override default keybindings with user keybindings', () => {
const store = useKeybindingStore()
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command1',
combo: { key: 'C', ctrl: true }
})
const userKeybinding = new KeybindingImpl({
commandId: 'test.command2',
combo: { key: 'C', ctrl: true }
})
store.addDefaultKeybinding(defaultKeybinding)
store.addUserKeybinding(userKeybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
})
it('Should allow binding to unsetted default keybindings', () => {
const store = useKeybindingStore()
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command1',
combo: { key: 'C', ctrl: true }
})
store.addDefaultKeybinding(defaultKeybinding)
store.unsetKeybinding(defaultKeybinding)
const userKeybinding = new KeybindingImpl({
commandId: 'test.command2',
combo: { key: 'C', ctrl: true }
})
store.addUserKeybinding(userKeybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
})
it('should unset user keybindings', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'D', meta: true }
})
store.addUserKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
store.unsetKeybinding(keybinding)
expect(store.keybindings).toHaveLength(0)
})
it('should unset default keybindings', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'E', ctrl: true, alt: true }
})
store.addDefaultKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
store.unsetKeybinding(keybinding)
expect(store.keybindings).toHaveLength(0)
})
it('should throw an error when adding duplicate default keybindings', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'F', shift: true }
})
store.addDefaultKeybinding(keybinding)
expect(() => store.addDefaultKeybinding(keybinding)).toThrow()
})
it('should allow adding duplicate user keybindings', () => {
const store = useKeybindingStore()
const keybinding1 = new KeybindingImpl({
commandId: 'test.command1',
combo: { key: 'G', ctrl: true }
})
const keybinding2 = new KeybindingImpl({
commandId: 'test.command2',
combo: { key: 'G', ctrl: true }
})
store.addUserKeybinding(keybinding1)
store.addUserKeybinding(keybinding2)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(keybinding2.combo)).toEqual(keybinding2)
})
it('should not throw an error when unsetting non-existent keybindings', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'H', alt: true, shift: true }
})
expect(() => store.unsetKeybinding(keybinding)).not.toThrow()
})
it('should not throw an error when unsetting unknown keybinding', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'I', ctrl: true }
})
store.addUserKeybinding(keybinding)
expect(() =>
store.unsetKeybinding(
new KeybindingImpl({
commandId: 'test.foo',
combo: { key: 'I', ctrl: true }
})
)
).not.toThrow()
})
it('should remove unset keybinding when adding back a default keybinding', () => {
const store = useKeybindingStore()
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'I', ctrl: true }
})
// Add default keybinding
store.addDefaultKeybinding(defaultKeybinding)
expect(store.keybindings).toHaveLength(1)
// Unset the default keybinding
store.unsetKeybinding(defaultKeybinding)
expect(store.keybindings).toHaveLength(0)
// Add the same keybinding as a user keybinding
store.addUserKeybinding(defaultKeybinding)
// Check that the keybinding is back and not in the unset list
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(defaultKeybinding.combo)).toEqual(
defaultKeybinding
)
})
it('Should accept same keybinding from default and user', () => {
const store = useKeybindingStore()
const keybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'J', ctrl: true }
})
// Add default keybinding.
// This can happen when we change default keybindings.
store.addDefaultKeybinding(keybinding)
// Add user keybinding.
store.addUserKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
})
it('Should keep previously customized keybindings after default keybindings change', () => {
// Initially command 'foo' was bound to 'K, Ctrl'. User unset it and bound the
// command to 'A, Ctrl'.
// Now we change the default keybindings of 'foo' to 'A, Ctrl'.
// The user customized keybinding should be kept.
const store = useKeybindingStore()
const userUnsetKeybindings = [
new KeybindingImpl({
commandId: 'foo',
combo: { key: 'K', ctrl: true }
})
]
const userNewKeybindings = [
new KeybindingImpl({
commandId: 'foo',
combo: { key: 'A', ctrl: true }
})
]
const newCoreKeybindings = [
new KeybindingImpl({
commandId: 'foo',
combo: { key: 'A', ctrl: true }
})
]
for (const keybinding of newCoreKeybindings) {
store.addDefaultKeybinding(keybinding)
}
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
userNewKeybindings[0]
)
for (const keybinding of userUnsetKeybindings) {
store.unsetKeybinding(keybinding)
}
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
userNewKeybindings[0]
)
for (const keybinding of userNewKeybindings) {
store.addUserKeybinding(keybinding)
}
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
userNewKeybindings[0]
)
})
it('should replace the previous keybinding with a new one for the same combo and unset the old command', () => {
const store = useKeybindingStore()
const oldKeybinding = new KeybindingImpl({
commandId: 'command1',
combo: { key: 'A', ctrl: true }
})
store.addUserKeybinding(oldKeybinding)
const newKeybinding = new KeybindingImpl({
commandId: 'command2',
combo: { key: 'A', ctrl: true }
})
store.updateKeybindingOnCommand(newKeybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(newKeybinding.combo)?.commandId).toBe('command2')
expect(store.getKeybindingsByCommandId('command1')).toHaveLength(0)
})
it('should return false when no default or current keybinding exists during reset', () => {
const store = useKeybindingStore()
const result = store.resetKeybindingForCommand('nonexistent.command')
expect(result).toBe(false)
})
it('should return false when current keybinding equals default keybinding', () => {
const store = useKeybindingStore()
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'L', ctrl: true }
})
store.addDefaultKeybinding(defaultKeybinding)
const result = store.resetKeybindingForCommand('test.command')
expect(result).toBe(false)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybindingByCommandId('test.command')).toEqual(
defaultKeybinding
)
})
it('should unset user keybinding when no default keybinding exists and return true', () => {
const store = useKeybindingStore()
const userKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'M', ctrl: true }
})
store.addUserKeybinding(userKeybinding)
expect(store.keybindings).toHaveLength(1)
const result = store.resetKeybindingForCommand('test.command')
expect(result).toBe(true)
expect(store.keybindings).toHaveLength(0)
})
it('should restore default keybinding when user has overridden it and return true', () => {
const store = useKeybindingStore()
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'N', ctrl: true }
})
const userKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'O', alt: true }
})
store.addDefaultKeybinding(defaultKeybinding)
store.updateKeybindingOnCommand(userKeybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybindingByCommandId('test.command')).toEqual(
userKeybinding
)
const result = store.resetKeybindingForCommand('test.command')
expect(result).toBe(true)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybindingByCommandId('test.command')).toEqual(
defaultKeybinding
)
})
it('should remove unset record and restore default keybinding when user has unset it', () => {
const store = useKeybindingStore()
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'P', ctrl: true }
})
store.addDefaultKeybinding(defaultKeybinding)
store.unsetKeybinding(defaultKeybinding)
expect(store.keybindings).toHaveLength(0)
const serializedCombo = defaultKeybinding.combo.serialize()
const userUnsetKeybindings = store.getUserUnsetKeybindings()
expect(userUnsetKeybindings[serializedCombo]).toBeTruthy()
expect(
userUnsetKeybindings[serializedCombo].equals(defaultKeybinding)
).toBe(true)
const result = store.resetKeybindingForCommand('test.command')
expect(result).toBe(true)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybindingByCommandId('test.command')).toEqual(
defaultKeybinding
)
expect(store.getUserUnsetKeybindings()[serializedCombo]).toBeUndefined()
})
it('should handle complex scenario with both unset and user keybindings', () => {
const store = useKeybindingStore()
// Create default keybinding
const defaultKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'Q', ctrl: true }
})
store.addDefaultKeybinding(defaultKeybinding)
// Unset default keybinding
store.unsetKeybinding(defaultKeybinding)
expect(store.keybindings).toHaveLength(0)
// Add user keybinding with different combo
const userKeybinding = new KeybindingImpl({
commandId: 'test.command',
combo: { key: 'R', alt: true }
})
store.addUserKeybinding(userKeybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybindingByCommandId('test.command')).toEqual(
userKeybinding
)
// Reset keybinding to default
const result = store.resetKeybindingForCommand('test.command')
expect(result).toBe(true)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybindingByCommandId('test.command')).toEqual(
defaultKeybinding
)
})
})

View File

@@ -1,355 +0,0 @@
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import type { Ref } from 'vue'
import { computed, ref, toRaw } from 'vue'
import { RESERVED_BY_TEXT_INPUT } from '@/constants/reservedKeyCombos'
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetElementId?: string
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.targetElementId = obj.targetElementId
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeybindingImpl
? this.commandId === raw.commandId &&
this.combo.equals(raw.combo) &&
this.targetElementId === raw.targetElementId
: false
}
}
export class KeyComboImpl implements KeyCombo {
key: string
// ctrl or meta(cmd on mac)
ctrl: boolean
alt: boolean
shift: boolean
constructor(obj: KeyCombo) {
this.key = obj.key
this.ctrl = obj.ctrl ?? false
this.alt = obj.alt ?? false
this.shift = obj.shift ?? false
}
static fromEvent(event: KeyboardEvent) {
return new KeyComboImpl({
key: event.key,
ctrl: event.ctrlKey || event.metaKey,
alt: event.altKey,
shift: event.shiftKey
})
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeyComboImpl
? this.key.toUpperCase() === raw.key.toUpperCase() &&
this.ctrl === raw.ctrl &&
this.alt === raw.alt &&
this.shift === raw.shift
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
get hasModifier(): boolean {
return this.ctrl || this.alt || this.shift
}
get isModifier(): boolean {
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
}
get modifierCount(): number {
const modifiers = [this.ctrl, this.alt, this.shift]
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
}
get isShiftOnly(): boolean {
return this.shift && this.modifierCount === 1
}
get isReservedByTextInput(): boolean {
return (
!this.hasModifier ||
this.isShiftOnly ||
RESERVED_BY_TEXT_INPUT.has(this.toString())
)
}
getKeySequences(): string[] {
const sequences: string[] = []
if (this.ctrl) {
sequences.push('Ctrl')
}
if (this.alt) {
sequences.push('Alt')
}
if (this.shift) {
sequences.push('Shift')
}
sequences.push(this.key)
return sequences
}
}
export const useKeybindingStore = defineStore('keybinding', () => {
/**
* Default keybindings provided by core and extensions.
*/
const defaultKeybindings = ref<Record<string, KeybindingImpl>>({})
/**
* User-defined keybindings.
*/
const userKeybindings = ref<Record<string, KeybindingImpl>>({})
/**
* User-defined keybindings that unset default keybindings.
*/
const userUnsetKeybindings = ref<Record<string, KeybindingImpl>>({})
/**
* Get user-defined keybindings.
*/
function getUserKeybindings() {
return userKeybindings.value
}
/**
* Get user-defined keybindings that unset default keybindings.
*/
function getUserUnsetKeybindings() {
return userUnsetKeybindings.value
}
const keybindingByKeyCombo = computed<Record<string, KeybindingImpl>>(() => {
const result: Record<string, KeybindingImpl> = {
...defaultKeybindings.value
}
for (const keybinding of Object.values(userUnsetKeybindings.value)) {
const serializedCombo = keybinding.combo.serialize()
if (result[serializedCombo]?.equals(keybinding)) {
delete result[serializedCombo]
}
}
return {
...result,
...userKeybindings.value
}
})
const keybindings = computed<KeybindingImpl[]>(() =>
Object.values(keybindingByKeyCombo.value)
)
function getKeybinding(combo: KeyComboImpl) {
return keybindingByKeyCombo.value[combo.serialize()]
}
const keybindingsByCommandId = computed<Record<string, KeybindingImpl[]>>(
() => {
return _.groupBy(keybindings.value, 'commandId')
}
)
function getKeybindingsByCommandId(commandId: string) {
return keybindingsByCommandId.value[commandId] ?? []
}
const defaultKeybindingsByCommandId = computed<
Record<string, KeybindingImpl[]>
>(() => {
return _.groupBy(Object.values(defaultKeybindings.value), 'commandId')
})
function getKeybindingByCommandId(commandId: string) {
return getKeybindingsByCommandId(commandId)[0]
}
/**
* Adds a keybinding to the specified target reference.
*
* @param target - A ref that holds a record of keybindings. The keys represent
* serialized key combos, and the values are `KeybindingImpl` objects.
* @param keybinding - The keybinding to add, represented as a `KeybindingImpl` object.
* @param options - An options object.
* @param options.existOk - If true, allows overwriting an existing keybinding with the
* same combo. Defaults to false.
*
* @throws {Error} Throws an error if a keybinding with the same combo already exists in
* the target and `existOk` is false.
*/
function addKeybinding(
target: Ref<Record<string, KeybindingImpl>>,
keybinding: KeybindingImpl,
{ existOk = false }: { existOk: boolean }
) {
if (!existOk && keybinding.combo.serialize() in target.value) {
throw new Error(
`Keybinding on ${keybinding.combo} already exists on ${
target.value[keybinding.combo.serialize()].commandId
}`
)
}
target.value[keybinding.combo.serialize()] = keybinding
}
function addDefaultKeybinding(keybinding: KeybindingImpl) {
addKeybinding(defaultKeybindings, keybinding, { existOk: false })
}
function addUserKeybinding(keybinding: KeybindingImpl) {
const defaultKeybinding =
defaultKeybindings.value[keybinding.combo.serialize()]
const userUnsetKeybinding =
userUnsetKeybindings.value[keybinding.combo.serialize()]
// User is adding back a keybinding that was an unsetted default keybinding.
if (
keybinding.equals(defaultKeybinding) &&
keybinding.equals(userUnsetKeybinding)
) {
delete userUnsetKeybindings.value[keybinding.combo.serialize()]
return
}
// Unset keybinding on default keybinding if it exists and is not the same as userUnsetKeybinding
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
unsetKeybinding(defaultKeybinding)
}
addKeybinding(userKeybindings, keybinding, { existOk: true })
}
function unsetKeybinding(keybinding: KeybindingImpl) {
const serializedCombo = keybinding.combo.serialize()
if (!(serializedCombo in keybindingByKeyCombo.value)) {
console.warn(
`Trying to unset non-exist keybinding: ${JSON.stringify(keybinding)}`
)
return
}
if (userKeybindings.value[serializedCombo]?.equals(keybinding)) {
delete userKeybindings.value[serializedCombo]
return
}
if (defaultKeybindings.value[serializedCombo]?.equals(keybinding)) {
addKeybinding(userUnsetKeybindings, keybinding, { existOk: false })
return
}
console.warn(`Unset unknown keybinding: ${JSON.stringify(keybinding)}`)
}
/**
* Update the keybinding on given command if it is different from the current keybinding.
*
* @returns true if the keybinding is updated, false otherwise.
*/
function updateKeybindingOnCommand(keybinding: KeybindingImpl): boolean {
const currentKeybinding = getKeybindingByCommandId(keybinding.commandId)
if (currentKeybinding?.equals(keybinding)) {
return false
}
if (currentKeybinding) {
unsetKeybinding(currentKeybinding)
}
addUserKeybinding(keybinding)
return true
}
function resetAllKeybindings() {
userKeybindings.value = {}
userUnsetKeybindings.value = {}
}
/**
* Resets the keybinding for a given command to its default value.
*
* @param commandId - The commandId of the keybind to be reset
* @returns `true` if changes were made, `false` if not
*/
function resetKeybindingForCommand(commandId: string): boolean {
const currentKeybinding = getKeybindingByCommandId(commandId)
const defaultKeybinding =
defaultKeybindingsByCommandId.value[commandId]?.[0]
// No default keybinding exists, need to remove any user binding
if (!defaultKeybinding) {
if (currentKeybinding) {
unsetKeybinding(currentKeybinding)
return true
}
return false
}
// Current binding equals default binding, no changes needed
if (currentKeybinding?.equals(defaultKeybinding)) {
return false
}
// Unset current keybinding if exists
if (currentKeybinding) {
unsetKeybinding(currentKeybinding)
}
// Remove the unset record if it exists
const serializedCombo = defaultKeybinding.combo.serialize()
if (
userUnsetKeybindings.value[serializedCombo]?.equals(defaultKeybinding)
) {
delete userUnsetKeybindings.value[serializedCombo]
}
return true
}
function isCommandKeybindingModified(commandId: string): boolean {
const currentKeybinding: KeybindingImpl | undefined =
getKeybindingByCommandId(commandId)
const defaultKeybinding: KeybindingImpl | undefined =
defaultKeybindingsByCommandId.value[commandId]?.[0]
return !(
(currentKeybinding === undefined && defaultKeybinding === undefined) ||
currentKeybinding?.equals(defaultKeybinding)
)
}
return {
keybindings,
getUserKeybindings,
getUserUnsetKeybindings,
getKeybinding,
getKeybindingsByCommandId,
getKeybindingByCommandId,
addDefaultKeybinding,
addUserKeybinding,
unsetKeybinding,
updateKeybindingOnCommand,
resetAllKeybindings,
resetKeybindingForCommand,
isCommandKeybindingModified
}
})