initial commit
This commit is contained in:
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of build tools
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
lib/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
1543
package-lock.json
generated
Normal file
1543
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "hyperf-keybinds",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest",
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.3.0",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"module": "dist/index.esm.js",
|
||||||
|
"types": "dist/index.d.ts"
|
||||||
|
}
|
||||||
88
src/createKeyHandler.ts
Normal file
88
src/createKeyHandler.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { KeybindEmitter } from "./keybindEmitter";
|
||||||
|
import { KeyTree } from "./keyTree";
|
||||||
|
import { KeybindEventTypes, ModifierKey, type CommandMap, type KeyCommand, type KeyHandlerReturn, type Modifiers } from "./types";
|
||||||
|
|
||||||
|
export function createKeyHandler(commandMaps : CommandMap[], timeout : number = 5000): KeyHandlerReturn | null {
|
||||||
|
const emitter = new KeybindEmitter();
|
||||||
|
const tree = KeyTree.buildTree(commandMaps);
|
||||||
|
if (tree === null) return null;
|
||||||
|
|
||||||
|
const sequenceProgress : KeyCommand[] = [];
|
||||||
|
var currentTreeNode : KeyTree | null = null;
|
||||||
|
var timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const tryResetTimeout = () => {
|
||||||
|
if (timeoutId !== null){
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryResetFields = () => {
|
||||||
|
currentTreeNode = null;
|
||||||
|
sequenceProgress.length = 0;
|
||||||
|
tryResetTimeout();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTimeout = () => {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
emitter.emit({type: KeybindEventTypes.SequenceTimeout});
|
||||||
|
tryResetFields();
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = (event: KeyboardEvent) => {
|
||||||
|
const command = eventToKeyCommand(event);
|
||||||
|
|
||||||
|
// Get next tree node
|
||||||
|
if (currentTreeNode === null){
|
||||||
|
currentTreeNode = tree.hasKeyCommand(command);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
currentTreeNode = currentTreeNode.hasKeyCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We either:
|
||||||
|
// 1. Reached the end of a sequence
|
||||||
|
// 2. Entered a nonexistant sequence (currentTreeNode === null)
|
||||||
|
// 3. Are partway through a sequence
|
||||||
|
// If 2, emit no match, reset all vars.
|
||||||
|
// Else, if 1: call callback, reset all vars.
|
||||||
|
// Else, if 3: set timeout to reset sequence.
|
||||||
|
|
||||||
|
if (currentTreeNode === null){
|
||||||
|
emitter.emit({type: KeybindEventTypes.SequenceNoMatch});
|
||||||
|
tryResetFields();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sequenceProgress.push(command);
|
||||||
|
if (currentTreeNode.isEnd){
|
||||||
|
emitter.emit({type: KeybindEventTypes.SequenceProgress, pressedKeys: sequenceProgress });
|
||||||
|
emitter.emit({type: KeybindEventTypes.CommandMatched, callback: currentTreeNode.callback})
|
||||||
|
tryResetFields();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
tryResetTimeout();
|
||||||
|
emitter.emit({type: KeybindEventTypes.SequenceProgress, pressedKeys: sequenceProgress });
|
||||||
|
startTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {handler, emitter};
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventToKeyCommand(event: KeyboardEvent): KeyCommand{
|
||||||
|
const mods = getModifiers(event);
|
||||||
|
return {
|
||||||
|
key: event.code,
|
||||||
|
modifiers: mods
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getModifiers(event: KeyboardEvent): Modifiers {
|
||||||
|
const mods: Modifiers = [];
|
||||||
|
if (event.shiftKey) mods.push(ModifierKey.Shift);
|
||||||
|
if (event.ctrlKey) mods.push(ModifierKey.Control);
|
||||||
|
if (event.altKey) mods.push(ModifierKey.Alt);
|
||||||
|
if (event.metaKey) mods.push(ModifierKey.Meta);
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
1
src/index.ts
Normal file
1
src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { createKeyHandler } from './createKeyHandler';
|
||||||
89
src/keyTree.ts
Normal file
89
src/keyTree.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { CommandMap, KeyCommand } from "./types";
|
||||||
|
|
||||||
|
// Like a trie, but for keys!
|
||||||
|
export class KeyTree {
|
||||||
|
#children : Record<string, KeyTree> = {};
|
||||||
|
#endcallback : (() => void) | null = null;
|
||||||
|
|
||||||
|
public get isEnd(){
|
||||||
|
return this.#endcallback !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get children(){
|
||||||
|
return this.#children;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCallback(callback : () => void): void {
|
||||||
|
this.#endcallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get callback(): (() => void) | null {
|
||||||
|
return this.#endcallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasKeyCommand(keyCommand: KeyCommand): KeyTree | null {
|
||||||
|
const searchTree = this.#children[this.keyCommandToString(keyCommand)];
|
||||||
|
if (searchTree !== undefined)
|
||||||
|
return searchTree;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canAddCommands(keyCommands: KeyCommand[]): boolean{
|
||||||
|
var t = this;
|
||||||
|
for (var command of keyCommands)
|
||||||
|
{
|
||||||
|
if (t.isEnd) return false;
|
||||||
|
var t2 = t.hasKeyCommand(command);
|
||||||
|
if (t2 === undefined) return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommand(keyCommand: KeyCommand, callback : (() => void) | null): KeyTree | null{
|
||||||
|
const t = this.hasKeyCommand(keyCommand);
|
||||||
|
if (t === null){
|
||||||
|
const t2 = new KeyTree();
|
||||||
|
this.children[this.keyCommandToString(keyCommand)] = t2;
|
||||||
|
if (callback != null)
|
||||||
|
t2.setCallback(callback);
|
||||||
|
return t2;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if (t.isEnd || callback != null) return null;
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommands(commandMap : CommandMap): boolean {
|
||||||
|
// First traverse and see if it's possible to add
|
||||||
|
if (!this.canAddCommands(commandMap.command)) return false;
|
||||||
|
|
||||||
|
let t : KeyTree = this;
|
||||||
|
for (const [ix, command] of commandMap.command.entries()){
|
||||||
|
const isLast = ix === commandMap.command.length - 1;
|
||||||
|
const callback = isLast ? commandMap.callback : null;
|
||||||
|
var t2 = t.addCommand(command, callback);
|
||||||
|
if (t2 === null) return false;
|
||||||
|
t = t2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCommandToString(keyCommand: KeyCommand): string{
|
||||||
|
const sortedModifiers = [...keyCommand.modifiers].sort();
|
||||||
|
const modifierString = sortedModifiers.join('+');
|
||||||
|
const separator = sortedModifiers.length > 0 ? '+' : '';
|
||||||
|
return `${modifierString}${separator}${keyCommand.key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static buildTree = (commandMaps : CommandMap[]) : KeyTree | null =>{
|
||||||
|
const tree = new KeyTree();
|
||||||
|
for (const commandMap of commandMaps){
|
||||||
|
if (!tree.addCommands(commandMap)) return null;
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/keybindEmitter.ts
Normal file
22
src/keybindEmitter.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { KeybindEvent } from "./types";
|
||||||
|
|
||||||
|
type Listener = (event: KeybindEvent) => void;
|
||||||
|
|
||||||
|
export class KeybindEmitter {
|
||||||
|
private listeners: Listener[] = [];
|
||||||
|
|
||||||
|
on(listener: Listener) {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
return () => this.off(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(listener: Listener) {
|
||||||
|
this.listeners = this.listeners.filter(l => l !== listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<T extends KeybindEvent["type"]>(
|
||||||
|
event: Extract<KeybindEvent, { type: T }>
|
||||||
|
) {
|
||||||
|
for (const l of this.listeners) l(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/test/createKeyHandler.test.ts
Normal file
155
src/test/createKeyHandler.test.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { createKeyHandler } from "../createKeyHandler";
|
||||||
|
import { makeCommand } from "./keyTree.test";
|
||||||
|
import { KeybindEventTypes } from "../types";
|
||||||
|
|
||||||
|
describe("createKeyHandler", () => {
|
||||||
|
it("emits the right event for matching key", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const command = makeCommand("KeyA");
|
||||||
|
const callback = vi.fn();
|
||||||
|
const keyHandlerReturn = createKeyHandler([
|
||||||
|
{
|
||||||
|
command: [command],
|
||||||
|
callback: callback
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(keyHandlerReturn).not.toBe(null);
|
||||||
|
|
||||||
|
const handler = keyHandlerReturn?.handler;
|
||||||
|
const emitter = keyHandlerReturn?.emitter;
|
||||||
|
const listener = vi.fn();
|
||||||
|
emitter?.on(listener);
|
||||||
|
|
||||||
|
const event = { code: command.key, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false } as KeyboardEvent;
|
||||||
|
handler?.(event);
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: KeybindEventTypes.SequenceProgress,
|
||||||
|
pressedKeys: expect.any(Array)
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: KeybindEventTypes.CommandMatched,
|
||||||
|
callback: expect.any(Function)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const commandMatchedEvent = listener.mock.calls.find(
|
||||||
|
call => call[0].type === KeybindEventTypes.CommandMatched
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
expect(commandMatchedEvent).toBeDefined();
|
||||||
|
commandMatchedEvent?.callback();
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits sequenceTimeout if timeout occurs", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const delay = 1000;
|
||||||
|
|
||||||
|
// Two-key sequence
|
||||||
|
const command1 = makeCommand("KeyA");
|
||||||
|
const command2 = makeCommand("KeyB");
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const keyHandlerReturn = createKeyHandler([
|
||||||
|
{ command: [command1, command2], callback }
|
||||||
|
], delay);
|
||||||
|
|
||||||
|
const handler = keyHandlerReturn?.handler;
|
||||||
|
const emitter = keyHandlerReturn?.emitter;
|
||||||
|
const listener = vi.fn();
|
||||||
|
emitter?.on(listener);
|
||||||
|
|
||||||
|
// Press only the first key
|
||||||
|
const event1 = { code: command1.key, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false } as KeyboardEvent;
|
||||||
|
handler?.(event1);
|
||||||
|
|
||||||
|
// SequenceProgress should be emitted for first key
|
||||||
|
expect(listener).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: KeybindEventTypes.SequenceProgress,
|
||||||
|
pressedKeys: expect.any(Array),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Advance timers past the delay to trigger timeout
|
||||||
|
vi.advanceTimersByTime(delay * 2);
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: KeybindEventTypes.SequenceTimeout })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only 2 emissions
|
||||||
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits ending output twice if key pressed twice", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const command = makeCommand("KeyA");
|
||||||
|
const callback = vi.fn();
|
||||||
|
const keyHandlerReturn = createKeyHandler([
|
||||||
|
{ command: [command], callback }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handler = keyHandlerReturn?.handler;
|
||||||
|
const emitter = keyHandlerReturn?.emitter;
|
||||||
|
const listener = vi.fn();
|
||||||
|
emitter?.on(listener);
|
||||||
|
|
||||||
|
const event = { code: command.key, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false } as KeyboardEvent;
|
||||||
|
|
||||||
|
// Press key twice
|
||||||
|
handler?.(event);
|
||||||
|
handler?.(event);
|
||||||
|
|
||||||
|
const matchedCalls = listener.mock.calls.filter(
|
||||||
|
call => call[0].type === KeybindEventTypes.CommandMatched
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchedCalls.length).toBe(2);
|
||||||
|
|
||||||
|
matchedCalls.forEach(call => call[0].callback());
|
||||||
|
expect(callback).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits sequenceProgress when first key of a sequence is pressed", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const command1 = makeCommand("KeyA");
|
||||||
|
const command2 = makeCommand("KeyB");
|
||||||
|
const callback = vi.fn();
|
||||||
|
const keyHandlerReturn = createKeyHandler([
|
||||||
|
{ command: [command1, command2], callback }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handler = keyHandlerReturn?.handler;
|
||||||
|
const emitter = keyHandlerReturn?.emitter;
|
||||||
|
const listener = vi.fn();
|
||||||
|
emitter?.on(listener);
|
||||||
|
|
||||||
|
const event = { code: command1.key, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false } as KeyboardEvent;
|
||||||
|
handler?.(event);
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: KeybindEventTypes.SequenceProgress,
|
||||||
|
pressedKeys: expect.any(Array)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure callback hasn't been called yet
|
||||||
|
const matchedCalls = listener.mock.calls.filter(
|
||||||
|
call => call[0].type === KeybindEventTypes.CommandMatched
|
||||||
|
);
|
||||||
|
expect(matchedCalls.length).toBe(0);
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
79
src/test/keyTree.test.ts
Normal file
79
src/test/keyTree.test.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { createKeyHandler } from "../createKeyHandler";
|
||||||
|
import { ModifierKey, type CommandMap, type KeyCommand } from "../types";
|
||||||
|
import { KeyTree } from "../keyTree";
|
||||||
|
|
||||||
|
export const makeCommand = (key: string, modifiers: ModifierKey[] = []): KeyCommand => ({
|
||||||
|
key,
|
||||||
|
modifiers,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeCommandMap = (command: KeyCommand[], callback: () => void): CommandMap => ({
|
||||||
|
command,
|
||||||
|
callback
|
||||||
|
})
|
||||||
|
describe("KeyTree", () => {
|
||||||
|
it("adding single command works", () => {
|
||||||
|
const command = makeCommand("KeyA");
|
||||||
|
const callback = vi.fn();
|
||||||
|
const commandMap = makeCommandMap([command], callback);
|
||||||
|
const tree = KeyTree.buildTree([commandMap]);
|
||||||
|
expect(tree).toBeInstanceOf(KeyTree);
|
||||||
|
|
||||||
|
const child = tree?.hasKeyCommand(command);
|
||||||
|
expect(child).not.toBeNull();
|
||||||
|
expect(child?.isEnd).toBe(true);
|
||||||
|
|
||||||
|
child?.callback?.();
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prevents adding conflicting commands", () => {
|
||||||
|
const command = makeCommand("KeyA");
|
||||||
|
const callback = vi.fn();
|
||||||
|
const commandMap1 = makeCommandMap([command], callback);
|
||||||
|
const commandMap2 = makeCommandMap([command], callback);
|
||||||
|
const tree = KeyTree.buildTree([commandMap1, commandMap2]);
|
||||||
|
expect(tree).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows branching commands", () => {
|
||||||
|
const commandA = makeCommand("KeyA");
|
||||||
|
const commandB = makeCommand("KeyB");
|
||||||
|
const commandC = makeCommand("KeyC");
|
||||||
|
const callback1 = vi.fn();
|
||||||
|
const callback2 = vi.fn();
|
||||||
|
const commandMap1 = makeCommandMap([commandA, commandB], callback1);
|
||||||
|
const commandMap2 = makeCommandMap([commandA, commandC], callback2);
|
||||||
|
const tree = KeyTree.buildTree([commandMap1, commandMap2]);
|
||||||
|
expect(tree).toBeInstanceOf(KeyTree);
|
||||||
|
|
||||||
|
const child1 = tree?.hasKeyCommand(commandA)?.hasKeyCommand(commandB);
|
||||||
|
expect(child1).not.toBeNull();
|
||||||
|
expect(child1?.isEnd).toBe(true);
|
||||||
|
|
||||||
|
child1?.callback?.();
|
||||||
|
expect(callback1).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const child2 = tree?.hasKeyCommand(commandA)?.hasKeyCommand(commandC);
|
||||||
|
expect(child2).not.toBeNull();
|
||||||
|
expect(child2?.isEnd).toBe(true);
|
||||||
|
|
||||||
|
child2?.callback?.();
|
||||||
|
expect(callback2).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates consistent key string with modifiers", () => {
|
||||||
|
const tree = new KeyTree();
|
||||||
|
const keyStr = tree.keyCommandToString(makeCommand("A", [ModifierKey.Alt, ModifierKey.Control, ModifierKey.Shift]));
|
||||||
|
const keyStr2 = tree.keyCommandToString(makeCommand("A", [ModifierKey.Shift, ModifierKey.Control, ModifierKey.Alt]));
|
||||||
|
expect(keyStr).toBe(keyStr2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't create matching key string with subset modifiers", () => {
|
||||||
|
const tree = new KeyTree();
|
||||||
|
const keyStr = tree.keyCommandToString(makeCommand("A", [ModifierKey.Alt, ModifierKey.Control, ModifierKey.Shift]));
|
||||||
|
const keyStr2 = tree.keyCommandToString(makeCommand("A", [ModifierKey.Shift, ModifierKey.Control]));
|
||||||
|
expect(keyStr).not.toBe(keyStr2);
|
||||||
|
})
|
||||||
|
});
|
||||||
41
src/types.ts
Normal file
41
src/types.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { KeybindEmitter } from "./keybindEmitter";
|
||||||
|
|
||||||
|
export type KeyCode = string;
|
||||||
|
export enum ModifierKey {
|
||||||
|
Shift = 'Shift',
|
||||||
|
Control = 'Ctrl',
|
||||||
|
Alt = 'Alt',
|
||||||
|
Meta = 'Meta'
|
||||||
|
}
|
||||||
|
export type Modifiers = Array<ModifierKey>;
|
||||||
|
|
||||||
|
export interface KeyCommand{
|
||||||
|
key: KeyCode;
|
||||||
|
modifiers: Modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandMap {
|
||||||
|
command: KeyCommand[];
|
||||||
|
callback: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeybindEventTypes = {
|
||||||
|
SequenceProgress: "sequenceProgress",
|
||||||
|
CommandMatched: "commandMatched",
|
||||||
|
SequenceTimeout: "sequenceTimeout",
|
||||||
|
SequenceNoMatch: "sequenceNoMatch",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type KeybindEventType = (typeof KeybindEventTypes)[keyof typeof KeybindEventTypes];
|
||||||
|
|
||||||
|
export type KeybindEvent =
|
||||||
|
| { type: typeof KeybindEventTypes.SequenceProgress; pressedKeys: KeyCommand[] }
|
||||||
|
| { type: typeof KeybindEventTypes.CommandMatched; callback: (() => void) | null }
|
||||||
|
| { type: typeof KeybindEventTypes.SequenceTimeout; }
|
||||||
|
| { type: typeof KeybindEventTypes.SequenceNoMatch; };
|
||||||
|
|
||||||
|
export type KeyEventHandler = (event: KeyboardEvent) => void;
|
||||||
|
export interface KeyHandlerReturn {
|
||||||
|
handler: KeyEventHandler;
|
||||||
|
emitter: KeybindEmitter;
|
||||||
|
}
|
||||||
48
tsconfig.json
Normal file
48
tsconfig.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
// Visit https://aka.ms/tsconfig to read more about this file
|
||||||
|
"compilerOptions": {
|
||||||
|
// File Layout
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
|
||||||
|
// Environment Settings
|
||||||
|
// See also https://aka.ms/tsconfig/module
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "es2019",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"types": ["vitest", "node"],
|
||||||
|
// For nodejs:
|
||||||
|
// "lib": ["esnext"],
|
||||||
|
// "types": ["node"],
|
||||||
|
// and npm install -D @types/node
|
||||||
|
|
||||||
|
// Other Outputs
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
|
||||||
|
// Stricter Typechecking Options
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
|
||||||
|
// Style Options
|
||||||
|
// "noImplicitReturns": true,
|
||||||
|
// "noImplicitOverride": true,
|
||||||
|
// "noUnusedLocals": true,
|
||||||
|
// "noUnusedParameters": true,
|
||||||
|
// "noFallthroughCasesInSwitch": true,
|
||||||
|
// "noPropertyAccessFromIndexSignature": true,
|
||||||
|
|
||||||
|
// Recommended Options
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user