UNPKG

@energypatrikhu/keysender

Version:

Node.js keyboard and mouse inputs emulator, global hotkey register for Windows

199 lines (155 loc) 5.71 kB
import { noop, sleep } from "."; import { DEFAULT_DELAY, Reason } from "../constants"; import { Delay } from "../types"; type HotkeyOnceModeOptions<This> = { /** * * if `"once"` - {@link HotkeyOnceModeOptions.action action} will call one time for each hotkey press */ mode: "once"; /** * Method to be executed after the {@link HotkeyOptions.key hotkey} is pressed */ action(this: This): void | Promise<void>; }; type HotkeyRestModesOptions<This, R> = { /** * * if `"hold"` - {@link HotkeyRestModesOptions.action action} will repeat every {@link HotkeyRestModesOptions.delay delay} milliseconds while hotkey is pressed or {@link HotkeyRestModesOptions.action action} returns `true` * * if `"toggle"` - {@link HotkeyRestModesOptions.action action} starts repeat repeat every {@link HotkeyRestModesOptions.delay delay} milliseconds after hotkey first time pressed and stops after hotkey second time pressed or {@link HotkeyRestModesOptions.action action} returns `false` */ mode: "toggle" | "hold"; /** * Method to check if hotkey is need to be executing * @default () => true */ isEnable?(this: This): boolean | Promise<boolean>; /** * Method to be executed before the {@link HotkeyRestModesOptions.action action} loop */ before?(this: This): void | Promise<void>; /** * @see {@link HotkeyRestModesOptions.mode mode} */ action(this: This): boolean | Promise<boolean>; /** * Method to be executed after the {@link HotkeyRestModesOptions.action action} loop * @param reason - reason of {@link HotkeyRestModesOptions.action action} loop ending, can be {@link Reason} (if ended by action - {@link Reason.BY_ACTION}, if ended by keyboard - {@link Reason.BY_KEYBOARD}) or any value from {@link This.stop stop} */ after?(this: This, reason: Reason | R): void | Promise<void>; /** * Delay in milliseconds between {@link HotkeyRestModesOptions.action action} executions * @default 35 */ delay?: Delay; }; export type HotkeyOptions<This, S, R> = ([S] extends [never] ? { defaultState?: undefined; } : { defaultState: S; }) & (HotkeyOnceModeOptions<This> | HotkeyRestModesOptions<This, R>); declare class _Hotkey<S, R> { /** * Any state that also can be used via `this` in {@link HotkeyRestModesOptions.isEnable isEnable}, {@link HotkeyRestModesOptions.before before}, {@link HotkeyRestModesOptions.action action}, {@link HotkeyRestModesOptions.after after} methods */ state: S; isRunning: boolean; /** * Stops the loop of {@link HotkeyRestModesOptions.action action} executing * @param [reason=Reason.BY_STOP] - reason to {@link HotkeyRestModesOptions.after after}, if not provided defaults to {@link Reason.BY_STOP} */ stop(reason?: Reason.BY_STOP | R): Promise<void> | undefined; protected _getButtonState(): boolean; } export type Hotkey<S, R> = Omit<_Hotkey<S, R>, never>; function handleAction<S, R>( this: _Hotkey<S, R>, options: HotkeyOptions<_Hotkey<S, R>, S, R> ) { if ("defaultState" in options) { this.state = options.defaultState!; } const { mode } = options; if (mode === "once") { const action: OmitThisParameter<typeof options.action> = options.action.bind(this); this.stop = () => { throw new Error('available only for "toggle" and "hold" modes'); }; return async () => { if (!this.isRunning) { this.isRunning = true; await action(); this.isRunning = false; } }; } let innerToggler = false; let outerToggler = true; let reason: Reason | R | undefined; let completion: Promise<void>; this.stop = function (_reason) { if (this.isRunning) { reason = arguments.length ? _reason : Reason.BY_STOP; outerToggler = false; return completion.then(() => { reason = undefined; outerToggler = true; }); } }; const action: OmitThisParameter<typeof options.action> = options.action.bind(this); const isEnable: OmitThisParameter<NonNullable<typeof options.isEnable>> = options.isEnable?.bind(this) || (() => true); const before: OmitThisParameter<NonNullable<typeof options.before>> = options.before?.bind(this) || noop; let after: () => void | Promise<void>; if (options.after) { const fn: OmitThisParameter<NonNullable<typeof options.after>> = options.after.bind(this); after = () => fn( outerToggler ? innerToggler ? Reason.BY_ACTION : Reason.BY_KEYBOARD : reason! ); } else { after = noop; } const { delay = DEFAULT_DELAY } = options; const getFn = (getInnerToggler: () => boolean) => async () => { if (!this.isRunning) { let resolve: () => void; completion = new Promise<void>((_resolve) => { resolve = _resolve; }); this.isRunning = true; if (await isEnable()) { await before(); while (outerToggler && getInnerToggler() && (await action())) { await sleep(delay); } await after(); } this.isRunning = innerToggler = false; resolve!(); } }; if (mode === "hold") { return getFn(() => (innerToggler = this._getButtonState())); } if (mode === "toggle") { const fn = getFn(() => innerToggler); return () => { if ((innerToggler = !innerToggler)) { fn(); } }; } throw new Error("Wrong mode"); } export default handleAction;