Add pluggable movement strategies (v1.3.0)
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.
This commit is contained in:
+27
-6
@@ -22,6 +22,7 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { CliError } from "./errors.ts";
|
||||
import { isPatternName, type PatternName } from "./strategies.ts";
|
||||
|
||||
// Single source of truth for default values. The same file ships in the
|
||||
// install tree and is copied to $XDG_CONFIG_HOME/move/config.json on a
|
||||
@@ -41,7 +42,12 @@ import seedRaw from "../scripts/config.default.json" with { type: "json" };
|
||||
* - `stepDelay` — pause between individual synthetic mouse steps inside
|
||||
* a sweep. Also the window in which the user can
|
||||
* "interrupt" by moving the cursor. Milliseconds.
|
||||
* - `stepCount` — number of pixel-steps in a single sweep. Pixels.
|
||||
* - `stepCount` — number of steps in a single sweep. Count.
|
||||
* - `stepSize` — pixels moved per step. Decouples "how many steps"
|
||||
* from "how far each step travels" so non-linear
|
||||
* patterns can span meaningful distances. Pixels.
|
||||
* - `pattern` — name of the movement strategy to use (see
|
||||
* `strategies.ts`; e.g. `line`, `walk`, `arc`).
|
||||
* - `verbose` — whether per-sweep / interrupt / bounds events are
|
||||
* logged. The startup banner is always printed.
|
||||
*/
|
||||
@@ -50,6 +56,8 @@ export interface Config {
|
||||
readonly checkInterval: number;
|
||||
readonly stepDelay: number;
|
||||
readonly stepCount: number;
|
||||
readonly stepSize: number;
|
||||
readonly pattern: PatternName;
|
||||
readonly verbose: boolean;
|
||||
}
|
||||
|
||||
@@ -63,7 +71,9 @@ interface SeedShape {
|
||||
moveInterval: number; // seconds
|
||||
checkInterval: number; // seconds
|
||||
stepDelay: number; // milliseconds
|
||||
stepCount: number; // pixels
|
||||
stepCount: number; // count
|
||||
stepSize: number; // pixels
|
||||
pattern: string; // strategy name
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
@@ -72,12 +82,15 @@ function assertSeedShape(raw: unknown): asserts raw is SeedShape {
|
||||
throw new Error("scripts/config.default.json: root must be an object");
|
||||
}
|
||||
const r = raw as Record<string, unknown>;
|
||||
for (const key of ["moveInterval", "checkInterval", "stepDelay", "stepCount"] as const) {
|
||||
for (const key of ["moveInterval", "checkInterval", "stepDelay", "stepCount", "stepSize"] as const) {
|
||||
const v = r[key];
|
||||
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
|
||||
throw new Error(`scripts/config.default.json: '${key}' must be a positive finite number (got ${JSON.stringify(v)})`);
|
||||
}
|
||||
}
|
||||
if (typeof r.pattern !== "string" || !isPatternName(r.pattern)) {
|
||||
throw new Error(`scripts/config.default.json: 'pattern' must be a known strategy name (got ${JSON.stringify(r.pattern)})`);
|
||||
}
|
||||
if (typeof r.verbose !== "boolean") {
|
||||
throw new Error(`scripts/config.default.json: 'verbose' must be a boolean (got ${JSON.stringify(r.verbose)})`);
|
||||
}
|
||||
@@ -98,6 +111,8 @@ export const DEFAULT_CONFIG: Config = {
|
||||
checkInterval: seed.checkInterval * 1000,
|
||||
stepDelay: seed.stepDelay,
|
||||
stepCount: seed.stepCount,
|
||||
stepSize: seed.stepSize,
|
||||
pattern: seed.pattern,
|
||||
verbose: seed.verbose,
|
||||
};
|
||||
|
||||
@@ -110,10 +125,12 @@ export const DEFAULT_CONFIG: Config = {
|
||||
* Numeric fields are in CLI / config-file units:
|
||||
* moveInterval, checkInterval — seconds
|
||||
* stepDelay — milliseconds
|
||||
* stepCount — pixels
|
||||
* stepCount — count
|
||||
* stepSize — pixels
|
||||
*
|
||||
* `verbose` is `boolean | undefined` like the numeric fields, so all five
|
||||
* fields share the same "first defined value wins" precedence logic.
|
||||
* `pattern` is a strategy name (`string | undefined`) and `verbose` is
|
||||
* `boolean | undefined`, so every field shares the same "first defined
|
||||
* value wins" precedence logic.
|
||||
*
|
||||
* For the CLI specifically, `verbose` is `undefined` when `-V/--verbose`
|
||||
* was not passed and `true` when it was. There is no CLI off-switch
|
||||
@@ -126,6 +143,8 @@ export interface ConfigOverrides {
|
||||
readonly checkInterval: number | undefined;
|
||||
readonly stepDelay: number | undefined;
|
||||
readonly stepCount: number | undefined;
|
||||
readonly stepSize: number | undefined;
|
||||
readonly pattern: string | undefined;
|
||||
readonly verbose: boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -192,6 +211,8 @@ export function resolveConfig(file: ConfigOverrides | null, cli: ConfigOverrides
|
||||
checkInterval: pickSeconds(cli.checkInterval, file?.checkInterval, DEFAULT_CONFIG.checkInterval),
|
||||
stepDelay: pickRaw(cli.stepDelay, file?.stepDelay, DEFAULT_CONFIG.stepDelay),
|
||||
stepCount: pickRaw(cli.stepCount, file?.stepCount, DEFAULT_CONFIG.stepCount),
|
||||
stepSize: pickRaw(cli.stepSize, file?.stepSize, DEFAULT_CONFIG.stepSize),
|
||||
pattern: pickRaw(cli.pattern, file?.pattern, DEFAULT_CONFIG.pattern),
|
||||
verbose: pickRaw(cli.verbose, file?.verbose, DEFAULT_CONFIG.verbose),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user