From a476be3933bfd137b68e75dc131ba82da56756e1 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sun, 12 Oct 2025 20:51:31 -0700 Subject: [PATCH] [refactor] Extract keybinding functionality into @comfyorg/keybinding package Create framework-agnostic keybinding package following domain-driven design patterns. Move pure business logic to package while keeping Vue integration in workbench layer. Changes: - Add @comfyorg/keybinding package with KeyComboImpl and KeybindingImpl classes - Move core keybindings and reserved key constants to package - Update workbench layer to import from package with backward compatibility - Update all imports across codebase to use package exports - Maintain existing API surface for consumers --- browser_tests/fixtures/ComfyPage.ts | 2 +- browser_tests/tests/dialog.spec.ts | 2 +- package.json | 1 + packages/keybinding/package.json | 29 +++++ .../src}/constants/coreKeybindings.ts | 2 +- .../src/constants}/reservedKeyCombos.ts | 0 packages/keybinding/src/index.ts | 11 ++ packages/keybinding/src/models/KeyCombo.ts | 83 +++++++++++++ packages/keybinding/src/models/Keybinding.ts | 22 ++++ .../keybinding/src}/types/keybinding.ts | 0 packages/keybinding/tsconfig.json | 12 ++ pnpm-lock.yaml | 13 ++ .../settings/constants/coreSettings.ts | 3 +- src/schemas/apiSchema.ts | 2 +- src/scripts/app.ts | 6 +- src/types/comfy.ts | 3 +- .../keybindings/services/keybindingService.ts | 13 +- .../keybindings/stores/keybindingStore.ts | 115 +----------------- .../services/keybindingService.escape.test.ts | 12 +- tests-ui/tests/store/keybindingStore.test.ts | 6 +- 20 files changed, 200 insertions(+), 137 deletions(-) create mode 100644 packages/keybinding/package.json rename {src/platform/keybinding => packages/keybinding/src}/constants/coreKeybindings.ts (97%) rename {src/base/keybinding => packages/keybinding/src/constants}/reservedKeyCombos.ts (100%) create mode 100644 packages/keybinding/src/index.ts create mode 100644 packages/keybinding/src/models/KeyCombo.ts create mode 100644 packages/keybinding/src/models/Keybinding.ts rename {src/platform/keybinding => packages/keybinding/src}/types/keybinding.ts (100%) create mode 100644 packages/keybinding/tsconfig.json diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 19796f4c4..aea884d07 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1,3 +1,4 @@ +import type { KeyCombo } from '@comfyorg/keybinding' import type { APIRequestContext, Locator, Page } from '@playwright/test' import { expect } from '@playwright/test' import { test as base } from '@playwright/test' @@ -6,7 +7,6 @@ import * as fs from 'fs' import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph' import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' -import type { KeyCombo } from '../../src/schemas/keyBindingSchema' import type { useWorkspaceStore } from '../../src/stores/workspaceStore' import { NodeBadgeMode } from '../../src/types/nodeSource' import { ComfyActionbar } from '../helpers/actionbar' diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index 7459acf58..a9a15ff22 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,7 +1,7 @@ +import type { Keybinding } from '@comfyorg/keybinding' import type { Locator } from '@playwright/test' import { expect } from '@playwright/test' -import type { Keybinding } from '../../src/schemas/keyBindingSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { diff --git a/package.json b/package.json index bdfdc0378..8dac86192 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "0.4.73-0", "@comfyorg/design-system": "workspace:*", + "@comfyorg/keybinding": "workspace:*", "@comfyorg/registry-types": "workspace:*", "@comfyorg/tailwind-utils": "workspace:*", "@iconify/json": "^2.2.380", diff --git a/packages/keybinding/package.json b/packages/keybinding/package.json new file mode 100644 index 000000000..7ac2dd8aa --- /dev/null +++ b/packages/keybinding/package.json @@ -0,0 +1,29 @@ +{ + "name": "@comfyorg/keybinding", + "version": "1.0.0", + "type": "module", + "description": "Framework-agnostic keybinding system for ComfyUI", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "nx": { + "tags": [ + "scope:shared", + "type:lib" + ] + }, + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "typescript": "^5.4.5" + } +} \ No newline at end of file diff --git a/src/platform/keybinding/constants/coreKeybindings.ts b/packages/keybinding/src/constants/coreKeybindings.ts similarity index 97% rename from src/platform/keybinding/constants/coreKeybindings.ts rename to packages/keybinding/src/constants/coreKeybindings.ts index f8aec98e0..5ef691d6f 100644 --- a/src/platform/keybinding/constants/coreKeybindings.ts +++ b/packages/keybinding/src/constants/coreKeybindings.ts @@ -1,4 +1,4 @@ -import type { Keybinding } from '@/platform/keybinding/types/keybinding' +import type { Keybinding } from '../types/keybinding' export const CORE_KEYBINDINGS: Keybinding[] = [ { diff --git a/src/base/keybinding/reservedKeyCombos.ts b/packages/keybinding/src/constants/reservedKeyCombos.ts similarity index 100% rename from src/base/keybinding/reservedKeyCombos.ts rename to packages/keybinding/src/constants/reservedKeyCombos.ts diff --git a/packages/keybinding/src/index.ts b/packages/keybinding/src/index.ts new file mode 100644 index 000000000..67763c8f9 --- /dev/null +++ b/packages/keybinding/src/index.ts @@ -0,0 +1,11 @@ +// Types +export type * from './types/keybinding' +export { zKeybinding } from './types/keybinding' + +// Models (Implementation classes) +export { KeyComboImpl } from './models/KeyCombo' +export { KeybindingImpl } from './models/Keybinding' + +// Constants +export { CORE_KEYBINDINGS } from './constants/coreKeybindings' +export { RESERVED_BY_TEXT_INPUT } from './constants/reservedKeyCombos' diff --git a/packages/keybinding/src/models/KeyCombo.ts b/packages/keybinding/src/models/KeyCombo.ts new file mode 100644 index 000000000..3eccdb2e7 --- /dev/null +++ b/packages/keybinding/src/models/KeyCombo.ts @@ -0,0 +1,83 @@ +import { RESERVED_BY_TEXT_INPUT } from '../constants/reservedKeyCombos' +import type { KeyCombo } from '../types/keybinding' + +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 { + return other instanceof KeyComboImpl + ? this.key.toUpperCase() === other.key.toUpperCase() && + this.ctrl === other.ctrl && + this.alt === other.alt && + this.shift === other.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 + } +} diff --git a/packages/keybinding/src/models/Keybinding.ts b/packages/keybinding/src/models/Keybinding.ts new file mode 100644 index 000000000..29d100a2d --- /dev/null +++ b/packages/keybinding/src/models/Keybinding.ts @@ -0,0 +1,22 @@ +import type { Keybinding } from '../types/keybinding' +import { KeyComboImpl } from './KeyCombo' + +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 { + return other instanceof KeybindingImpl + ? this.commandId === other.commandId && + this.combo.equals(other.combo) && + this.targetElementId === other.targetElementId + : false + } +} diff --git a/src/platform/keybinding/types/keybinding.ts b/packages/keybinding/src/types/keybinding.ts similarity index 100% rename from src/platform/keybinding/types/keybinding.ts rename to packages/keybinding/src/types/keybinding.ts diff --git a/packages/keybinding/tsconfig.json b/packages/keybinding/tsconfig.json new file mode 100644 index 000000000..b7171acd8 --- /dev/null +++ b/packages/keybinding/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2768d1af2..bfb114457 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@comfyorg/design-system': specifier: workspace:* version: link:packages/design-system + '@comfyorg/keybinding': + specifier: workspace:* + version: link:packages/keybinding '@comfyorg/registry-types': specifier: workspace:* version: link:packages/registry-types @@ -371,6 +374,16 @@ importers: specifier: ^5.4.5 version: 5.9.2 + packages/keybinding: + dependencies: + zod: + specifier: ^3.22.4 + version: 3.24.1 + devDependencies: + typescript: + specifier: ^5.4.5 + version: 5.9.2 + packages/registry-types: {} packages/shared-frontend-utils: diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts index fb6fc1039..5d1aea9c5 100644 --- a/src/platform/settings/constants/coreSettings.ts +++ b/src/platform/settings/constants/coreSettings.ts @@ -1,5 +1,6 @@ +import type { Keybinding } from '@comfyorg/keybinding' + import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph' -import type { Keybinding } from '@/platform/keybinding/types/keybinding' import { useSettingStore } from '@/platform/settings/settingStore' import type { SettingParams } from '@/platform/settings/types' import type { ColorPalettes } from '@/schemas/colorPaletteSchema' diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 8e67ef88e..d8770a6c3 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -1,7 +1,7 @@ +import { zKeybinding } from '@comfyorg/keybinding' import { z } from 'zod' import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph' -import { zKeybinding } from '@/platform/keybinding/types/keybinding' import { zComfyWorkflow, zNodeId diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 70837aa37..f2c72986a 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1,3 +1,4 @@ +import { KeyComboImpl } from '@comfyorg/keybinding' import _ from 'es-toolkit/compat' import type { ToastMessageOptions } from 'primevue/toast' import { reactive } from 'vue' @@ -80,10 +81,7 @@ import { } from '@/utils/migration/migrateReroute' import { getSelectedModelsMetadata } from '@/utils/modelMetadataUtil' import { deserialiseAndCreate } from '@/utils/vintageClipboard' -import { - KeyComboImpl, - useKeybindingStore -} from '@/workbench/keybindings/stores/keybindingStore' +import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore' import { type ComfyApi, PromptExecutionError, api } from './api' import { defaultGraph } from './defaultGraph' diff --git a/src/types/comfy.ts b/src/types/comfy.ts index 1a2752f72..c894aaba7 100644 --- a/src/types/comfy.ts +++ b/src/types/comfy.ts @@ -1,6 +1,7 @@ +import type { Keybinding } from '@comfyorg/keybinding' + import type { Positionable } from '@/lib/litegraph/src/interfaces' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' -import type { Keybinding } from '@/platform/keybinding/types/keybinding' import type { SettingParams } from '@/platform/settings/types' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' diff --git a/src/workbench/keybindings/services/keybindingService.ts b/src/workbench/keybindings/services/keybindingService.ts index e440f6433..3c60df903 100644 --- a/src/workbench/keybindings/services/keybindingService.ts +++ b/src/workbench/keybindings/services/keybindingService.ts @@ -1,13 +1,14 @@ -import { CORE_KEYBINDINGS } from '@/platform/keybinding/constants/coreKeybindings' +import { + CORE_KEYBINDINGS, + KeyComboImpl, + KeybindingImpl +} from '@comfyorg/keybinding' + import { useSettingStore } from '@/platform/settings/settingStore' import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' import { useDialogStore } from '@/stores/dialogStore' -import { - KeyComboImpl, - KeybindingImpl, - useKeybindingStore -} from '@/workbench/keybindings/stores/keybindingStore' +import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore' export const useKeybindingService = () => { const keybindingStore = useKeybindingStore() diff --git a/src/workbench/keybindings/stores/keybindingStore.ts b/src/workbench/keybindings/stores/keybindingStore.ts index 9ed01a288..c866a0fb1 100644 --- a/src/workbench/keybindings/stores/keybindingStore.ts +++ b/src/workbench/keybindings/stores/keybindingStore.ts @@ -1,118 +1,11 @@ +import { KeyComboImpl, KeybindingImpl } from '@comfyorg/keybinding' import _ from 'es-toolkit/compat' import { defineStore } from 'pinia' import type { Ref } from 'vue' -import { computed, ref, toRaw } from 'vue' +import { computed, ref } from 'vue' -import { RESERVED_BY_TEXT_INPUT } from '@/base/keybinding/reservedKeyCombos' -import type { - KeyCombo, - Keybinding -} from '@/platform/keybinding/types/keybinding' - -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 - } -} +// Re-export classes from package for backward compatibility +export { KeybindingImpl, KeyComboImpl } export const useKeybindingStore = defineStore('keybinding', () => { /** diff --git a/tests-ui/tests/services/keybindingService.escape.test.ts b/tests-ui/tests/services/keybindingService.escape.test.ts index cfbe41dd2..82fad6d79 100644 --- a/tests-ui/tests/services/keybindingService.escape.test.ts +++ b/tests-ui/tests/services/keybindingService.escape.test.ts @@ -1,15 +1,15 @@ +import { + CORE_KEYBINDINGS, + KeyComboImpl, + KeybindingImpl +} from '@comfyorg/keybinding' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { CORE_KEYBINDINGS } from '@/platform/keybinding/constants/coreKeybindings' import { useCommandStore } from '@/stores/commandStore' import { useDialogStore } from '@/stores/dialogStore' import { useKeybindingService } from '@/workbench/keybindings/services/keybindingService' -import { - KeyComboImpl, - KeybindingImpl, - useKeybindingStore -} from '@/workbench/keybindings/stores/keybindingStore' +import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore' // Mock stores vi.mock('@/platform/settings/settingStore', () => ({ diff --git a/tests-ui/tests/store/keybindingStore.test.ts b/tests-ui/tests/store/keybindingStore.test.ts index 3a339cd9a..dc25d14fa 100644 --- a/tests-ui/tests/store/keybindingStore.test.ts +++ b/tests-ui/tests/store/keybindingStore.test.ts @@ -1,10 +1,8 @@ +import { KeybindingImpl } from '@comfyorg/keybinding' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' -import { - KeybindingImpl, - useKeybindingStore -} from '@/workbench/keybindings/stores/keybindingStore' +import { useKeybindingStore } from '@/workbench/keybindings/stores/keybindingStore' describe('useKeybindingStore', () => { beforeEach(() => {