Turn the hardcoded straight-line sweep into a strategy system behind three
seams so new patterns are easy to add and, for the first time, testable
without nut.js or a real screen:
- src/device.ts: injectable Device seam over nut.js (autoDelayMs lives
here now); the only module that touches the native lib.
- src/strategies.ts: pure per-pattern path generators + registry + lenient
name resolution. Ships line, diagonal, jitter, walk,
arc, figureEight.
- src/executor.ts: single executePath driver owning bounds policy
(abort/clamp/reflect), pacing, interrupt detection, and
restore-on-clean.
keeper.ts's simulateActivity now selects a strategy and delegates to the
executor; the default `line` pattern is byte-for-byte the previous behavior.
New config surface, layered CLI > file > default with strict validation:
- -p/--pattern <name> movement strategy (names matched case/-/_-insensitive)
- -s/--step-size <px> pixels per step; stepCount is now a step *count*
Robustness for the new edge-seeking patterns: interrupt detection compares
against the last commanded (rounded) point with a 2px tolerance, and
clamp/reflect stay a couple pixels off the screen edge, so sub-pixel cursor
placement on scaled/multi-monitor displays isn't misread as user activity.
jitter's radius scales with sweep length so it moves at the default stepSize.
Tests: new suites for strategies, the executor (all bounds policies,
rounding, interrupt, tolerance, pacing), and the keeper loop; config and
configFile suites extended for pattern/stepSize. editor.test.ts moved to
tests/ for consistency. 64 pass.
137 lines
5.2 KiB
TypeScript
Executable File
137 lines
5.2 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* move.ts
|
|
* -------
|
|
* Entry point for the `move` CLI.
|
|
*
|
|
* Thin shim that ties the four logic modules together:
|
|
* - `cli.ts` parses and validates `process.argv`.
|
|
* - `configFile.ts` loads and validates the JSON config file.
|
|
* - `config.ts` holds defaults and the layered `resolveConfig` overlay.
|
|
* - `keeper.ts` owns the synthetic-activity sweep and idle-watch loop.
|
|
*
|
|
* Order of operations:
|
|
* 1. Parse CLI args. Bad input -> stderr + usage hint, exit 2.
|
|
* 2. `--help` / `--version` short-circuit before any I/O, config load, or
|
|
* mouse work. `keeper.ts` is also lazy-imported (see below) so these
|
|
* flags don't pay the cost of loading the nut.js native binary.
|
|
* 3. Load + validate the config file (default XDG path, or `--config
|
|
* <path>` if supplied). Validation failures share the exit-2 path.
|
|
* 4. Resolve the full `Config` (CLI > file > DEFAULT_CONFIG) — verbose
|
|
* lives inside `Config` and is layered with the same precedence as
|
|
* the numeric fields.
|
|
* 5. Lazy-import `keeper.ts` (dynamic import keeps nut.js out of the
|
|
* `--help` / `--version` startup path) and run it. Any unhandled
|
|
* rejection — from the import itself or from the loop — exits 1.
|
|
*
|
|
* Runtime: Bun (uses `@nut-tree-fork/nut-js` via `keeper.ts`). The shebang
|
|
* above lets this file run as a real CLI once linked via `bun link`.
|
|
*/
|
|
|
|
import { parseCliArgs, printHelp, VERSION } from "./cli.ts";
|
|
import type { ParsedCliArgs } from "./cli.ts";
|
|
import { defaultConfigPath, resolveConfig } from "./config.ts";
|
|
import type { ConfigOverrides } from "./config.ts";
|
|
import { loadConfigFile } from "./configFile.ts";
|
|
import { editConfig } from "./editor.ts";
|
|
|
|
// `keeper.ts` is intentionally NOT statically imported here. It transitively
|
|
// pulls in `@nut-tree-fork/nut-js`, which in turn dlopens a sizeable native
|
|
// `.node` binary. On a cold first run that load dominates startup (~1 s on
|
|
// macOS). For `--help` and `--version` we never actually need nut.js, so we
|
|
// defer the import to the only branch that actually runs the keeper loop.
|
|
// See the dynamic `await import("./keeper.ts")` near the bottom of the file.
|
|
|
|
/**
|
|
* Print a user-error message and exit 2. Used for anything that comes from
|
|
* invalid input: unknown CLI flags, bad numbers, malformed or missing
|
|
* config files, unresolvable default paths. The accompanying "Try 'move
|
|
* --help'" pointer is appropriate for these cases.
|
|
*
|
|
* Returns `never` so callers can invoke it without TypeScript flagging
|
|
* "variable might be undefined" downstream.
|
|
*/
|
|
function failUser(err: unknown): never {
|
|
const msg: string = err instanceof Error ? err.message : String(err);
|
|
process.stderr.write(`move: ${msg}\nTry 'move --help' for more information.\n`);
|
|
process.exit(2);
|
|
}
|
|
|
|
/**
|
|
* Print a runtime-failure message and exit 1. Used for anything the user
|
|
* couldn't have prevented from the command line: nut.js errors, missing
|
|
* Accessibility permission on macOS, unexpected exceptions from the
|
|
* keeper loop. Prefers the stack trace when available since these failures
|
|
* usually need a developer to interpret.
|
|
*/
|
|
function failRuntime(err: unknown): never {
|
|
const detail: string = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
process.stderr.write(`move: runtime error: ${detail}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let cliArgs: ParsedCliArgs;
|
|
try {
|
|
cliArgs = parseCliArgs();
|
|
} catch (err: unknown) {
|
|
failUser(err);
|
|
}
|
|
|
|
if (cliArgs.help) {
|
|
// printHelp() resolves defaultConfigPath(), which can throw CliError
|
|
// when neither $XDG_CONFIG_HOME nor $HOME is set.
|
|
try {
|
|
printHelp();
|
|
} catch (err: unknown) {
|
|
failUser(err);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
if (cliArgs.version) {
|
|
process.stdout.write(`move ${VERSION}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (cliArgs.edit) {
|
|
// Edit is a fully terminal action: open the config file in $EDITOR and
|
|
// hand the user's terminal over. Path resolution mirrors loadConfigFile's:
|
|
// honor `--config <path>` if set, else use the XDG default.
|
|
try {
|
|
const path: string = cliArgs.config ?? defaultConfigPath();
|
|
editConfig(path);
|
|
} catch (err: unknown) {
|
|
failUser(err);
|
|
}
|
|
}
|
|
|
|
let fileOverrides: ConfigOverrides | null;
|
|
try {
|
|
fileOverrides = loadConfigFile(cliArgs.config);
|
|
} catch (err: unknown) {
|
|
failUser(err);
|
|
}
|
|
|
|
const cliOverrides: ConfigOverrides = {
|
|
moveInterval: cliArgs.moveInterval,
|
|
checkInterval: cliArgs.checkInterval,
|
|
stepDelay: cliArgs.stepDelay,
|
|
stepCount: cliArgs.stepCount,
|
|
stepSize: cliArgs.stepSize,
|
|
pattern: cliArgs.pattern,
|
|
verbose: cliArgs.verbose,
|
|
};
|
|
|
|
const config = resolveConfig(fileOverrides, cliOverrides);
|
|
|
|
// Dynamic import so nut.js and the rest of the keeper machinery aren't
|
|
// loaded for invocations that exit early (--help, --version, validation
|
|
// failures). The `try` covers import-time failures too (e.g., a missing
|
|
// nut.js native binary), routing them through the same runtime-failure
|
|
// path as anything raised by the loop itself.
|
|
try {
|
|
const { runKeeper } = await import("./keeper.ts");
|
|
runKeeper(config).catch(failRuntime);
|
|
} catch (err: unknown) {
|
|
failRuntime(err);
|
|
}
|