@energypatrikhu/keysender
Version:
Node.js keyboard and mouse inputs emulator, global hotkey register for Windows
199 lines (155 loc) • 5.71 kB
text/typescript
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;