Files
image-studio/commands/dispatcher.ts
syntaxbullet c3a75cf0d6 feat: add command palette functionality and shortcuts
- Implemented command palette with keyboard shortcut (⌘K) for opening.
- Added commands for opening, closing, setting query, and selecting items in the command palette.
- Created tests for command palette commands and input handling.
- Enhanced layer actions with functions for adding, grouping, and deleting layers.
- Updated UI components to integrate command palette and shortcuts.
2026-07-05 17:15:20 +02:00

132 lines
4.5 KiB
TypeScript

import type { AppState, HistorySnapshot } from "@editor/state";
import type { CommandContext, CommandHistoryPolicy } from "./command";
import type { CommandId, CommandPayloads } from "./payloads";
import type { CommandRegistry } from "./registry";
export type Dispatch = <TCommandId extends CommandId>(commandId: TCommandId, payload: CommandPayloads[TCommandId]) => AppState;
export type CommandDispatcher = {
dispatch: Dispatch;
};
export function createCommandDispatcher(options: {
registry: CommandRegistry;
getState: () => AppState;
setState: (state: AppState) => void;
}): CommandDispatcher {
let deferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined;
return {
dispatch(commandId, payload) {
const command = options.registry.get(commandId);
if (!command) {
throw new Error(`Unknown command: ${commandId}`);
}
const currentState = options.getState();
const historyPolicy = command.history ?? defaultHistoryPolicy;
const deferredSnapshot = historyPolicy.mode === "deferred" && historyPolicy.phase === "begin" ? snapshot(currentState) : undefined;
const context: CommandContext = { state: currentState };
const executedState = command.execute(context, payload);
if (executedState === currentState) {
if (historyPolicy.mode === "deferred" && historyPolicy.phase === "commit") deferredHistory = undefined;
return currentState;
}
const nextState = applyHistoryPolicy({
currentState,
nextState: executedState,
historyPolicy,
deferredSnapshot,
getDeferredHistory: () => deferredHistory,
setDeferredHistory: (nextDeferredHistory) => {
deferredHistory = nextDeferredHistory;
},
});
options.setState(nextState);
return nextState;
},
};
}
const defaultHistoryPolicy: CommandHistoryPolicy = { mode: "auto" };
function applyHistoryPolicy(options: {
currentState: AppState;
nextState: AppState;
historyPolicy: CommandHistoryPolicy;
deferredSnapshot?: HistorySnapshot;
getDeferredHistory: () => { snapshot: HistorySnapshot; changed: boolean } | undefined;
setDeferredHistory: (nextDeferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined) => void;
}): AppState {
const { historyPolicy } = options;
if (historyPolicy.mode === "ignore") {
options.setDeferredHistory(undefined);
return options.nextState;
}
if (historyPolicy.mode === "deferred") {
return applyDeferredHistoryPolicy({ ...options, historyPolicy });
}
return shouldRecordHistory(options.currentState, options.nextState) ? recordHistory(snapshot(options.currentState), options.nextState) : options.nextState;
}
function applyDeferredHistoryPolicy(options: {
currentState: AppState;
nextState: AppState;
historyPolicy: Extract<CommandHistoryPolicy, { mode: "deferred" }>;
deferredSnapshot?: HistorySnapshot;
getDeferredHistory: () => { snapshot: HistorySnapshot; changed: boolean } | undefined;
setDeferredHistory: (nextDeferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined) => void;
}): AppState {
switch (options.historyPolicy.phase) {
case "begin":
options.setDeferredHistory(options.deferredSnapshot ? { snapshot: options.deferredSnapshot, changed: false } : undefined);
return options.nextState;
case "update": {
const deferredHistory = options.getDeferredHistory();
if (deferredHistory && options.currentState.document !== options.nextState.document) {
options.setDeferredHistory({ ...deferredHistory, changed: true });
}
return options.nextState;
}
case "commit": {
const deferredHistory = options.getDeferredHistory();
options.setDeferredHistory(undefined);
return deferredHistory?.changed ? recordHistory(deferredHistory.snapshot, options.nextState) : options.nextState;
}
}
}
function shouldRecordHistory(currentState: AppState, nextState: AppState) {
return currentState.document !== nextState.document;
}
function recordHistory(historySnapshot: HistorySnapshot, nextState: AppState): AppState {
return {
...nextState,
history: {
past: [...nextState.history.past, historySnapshot].slice(-100),
future: [],
},
};
}
function snapshot(state: AppState): HistorySnapshot {
return {
document: state.document,
editor: {
...state.editor,
brushPreview: undefined,
brushStrokePreview: undefined,
commandPalette: {
open: false,
query: "",
selectedIndex: 0,
},
},
};
}