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:
@@ -90,7 +90,11 @@ Options:
|
||||
-m, --move-interval <seconds> Idle time before a sweep fires. Default: 240.
|
||||
-c, --check-interval <seconds> Cursor poll cadence. Default: 10.
|
||||
-d, --step-delay <ms> Pause between synthetic steps. Default: 50.
|
||||
-n, --step-count <pixels> Steps per sweep. Default: 250.
|
||||
-n, --step-count <count> Steps per sweep. Default: 250.
|
||||
-s, --step-size <pixels> Pixels moved per step. Default: 1.
|
||||
-p, --pattern <name> Movement strategy. Default: line.
|
||||
One of: line, diagonal, jitter, walk, arc,
|
||||
figureEight.
|
||||
-V, --verbose Log every sweep, interrupt, and bounds event
|
||||
(default prints only the startup banner).
|
||||
|
||||
@@ -147,6 +151,8 @@ doesn't set.
|
||||
"checkInterval": 10,
|
||||
"stepDelay": 50,
|
||||
"stepCount": 250,
|
||||
"stepSize": 1,
|
||||
"pattern": "line",
|
||||
"verbose": false
|
||||
}
|
||||
```
|
||||
@@ -154,7 +160,8 @@ doesn't set.
|
||||
All keys are optional; supply only the ones you want to override. Keys
|
||||
and units mirror the CLI flags exactly: `moveInterval` and
|
||||
`checkInterval` are seconds, `stepDelay` is milliseconds, `stepCount` is
|
||||
pixels, `verbose` is a boolean.
|
||||
a step count, `stepSize` is pixels-per-step, `pattern` is a movement
|
||||
strategy name, `verbose` is a boolean.
|
||||
|
||||
### Editing
|
||||
|
||||
@@ -181,6 +188,9 @@ The loader is strict:
|
||||
- Root must be a JSON object.
|
||||
- Unknown keys are rejected (catches typos like `"movInterval"`).
|
||||
- Numeric values must be finite and strictly positive.
|
||||
- `pattern` must resolve to a registered strategy name. Matching ignores
|
||||
case and separators (`-`, `_`, spaces), so `figure-eight` and `figureEight`
|
||||
are equivalent.
|
||||
- `verbose` must be a boolean.
|
||||
|
||||
Any validation failure prints a message naming the file and the offending
|
||||
@@ -195,7 +205,7 @@ file with `--config`.
|
||||
|
||||
## How it works
|
||||
|
||||
The source lives under `src/`, split into an entry point plus four logic
|
||||
The source lives under `src/`, split into an entry point plus logic
|
||||
modules:
|
||||
|
||||
- `src/move.ts` is a thin entry point: parses args, dispatches `--help` /
|
||||
@@ -207,7 +217,22 @@ modules:
|
||||
- `src/config.ts` exports the `Config` type (which carries every tunable
|
||||
including `verbose`), `DEFAULT_CONFIG`, `defaultConfigPath`, and the
|
||||
layered `resolveConfig` overlay function.
|
||||
- `src/keeper.ts` owns the synthetic-activity sweep and the idle-watch loop.
|
||||
- `src/keeper.ts` owns the idle-watch loop and the per-sweep glue that
|
||||
wires a strategy to the executor.
|
||||
|
||||
Movement itself is split across three seams so patterns are easy to add
|
||||
and everything but the raw nut.js call is unit-testable:
|
||||
|
||||
- `src/device.ts` is the I/O boundary: a `Device` interface
|
||||
(`getPosition`/`setPosition`/`width`/`height`/`sleep`) plus the nut.js
|
||||
implementation. It's the *only* module that imports nut.js, and it's
|
||||
injectable, so tests drive the loop and executor with a fake.
|
||||
- `src/strategies.ts` holds the pure movement patterns — each a generator
|
||||
of target points given a start, screen size, config, and RNG — plus the
|
||||
registry and name validation. Adding a pattern is one pure function.
|
||||
- `src/executor.ts` is the single `executePath` driver: it rounds targets,
|
||||
applies the strategy's bounds policy, paces steps, detects real-user
|
||||
interruption, and restores the cursor on a clean sweep.
|
||||
|
||||
Defaults live in `src/config.ts` as `DEFAULT_CONFIG`:
|
||||
|
||||
@@ -216,7 +241,9 @@ Defaults live in `src/config.ts` as `DEFAULT_CONFIG`:
|
||||
| `moveInterval` | `4 * 60_000` | `-m`, `--move-interval` | Idle time (ms) required before a synthetic sweep fires. |
|
||||
| `checkInterval` | `10_000` | `-c`, `--check-interval` | How often (ms) the main loop polls the cursor for real activity. |
|
||||
| `stepDelay` | `50` | `-d`, `--step-delay` | Pause (ms) between individual synthetic steps in a sweep. |
|
||||
| `stepCount` | `250` | `-n`, `--step-count` | Pixel-steps per sweep. |
|
||||
| `stepCount` | `250` | `-n`, `--step-count` | Number of steps per sweep. |
|
||||
| `stepSize` | `1` | `-s`, `--step-size` | Pixels moved per step. |
|
||||
| `pattern` | `"line"` | `-p`, `--pattern` | Movement strategy name (see Movement strategies below). |
|
||||
|
||||
`-m` and `-c` are accepted in seconds at the CLI; `resolveConfig` converts
|
||||
to milliseconds before handing the resolved `Config` to `runKeeper`.
|
||||
@@ -231,27 +258,57 @@ to milliseconds before handing the resolved `Config` to `runKeeper`.
|
||||
- Otherwise, if `now - lastActivity >= config.moveInterval`, call
|
||||
`simulateActivity` and reset the idleness clock.
|
||||
|
||||
### Synthetic sweep (`simulateActivity`)
|
||||
### Synthetic sweep (`simulateActivity` + `executePath`)
|
||||
|
||||
1. Read the starting position and current screen dimensions.
|
||||
2. Pick a horizontal direction (`dx = +1` if there's room to the right,
|
||||
else `-1`) so the sweep stays on-screen. Vertical is `dy = 0` for now.
|
||||
3. For each of `config.stepCount` steps:
|
||||
- Compute and bounds-check the next target.
|
||||
1. `simulateActivity` snapshots the starting position and current screen
|
||||
dimensions (re-read every sweep so monitor changes are handled), looks
|
||||
up `config.pattern` in the strategy registry, and builds a `MoveContext`.
|
||||
2. It hands the strategy and context to `executePath`, which drives the
|
||||
sweep. For each target the strategy yields:
|
||||
- Round to whole pixels and apply the strategy's bounds policy
|
||||
(`abort` / `clamp` / `reflect`) to keep it on-screen.
|
||||
- Move the cursor there, sleep `config.stepDelay`.
|
||||
- Re-read the cursor. If it isn't where we put it, the user moved it —
|
||||
log (when `--verbose`) and return early without snapping back.
|
||||
4. On a clean full sweep, restore the cursor to its starting position so
|
||||
- Re-read the cursor. If it isn't at the point we *just commanded*, the
|
||||
user moved it — log (when `--verbose`) and return early without
|
||||
snapping back.
|
||||
3. On a clean full sweep, restore the cursor to its starting position so
|
||||
the next idle-check sees "no movement" and doesn't misread the synthetic
|
||||
activity as real user input.
|
||||
|
||||
Comparing against the last commanded (rounded) point — not the strategy's
|
||||
ideal, possibly fractional target — is what lets curved and stochastic
|
||||
patterns run without every rounded step looking like user activity. The
|
||||
comparison also allows a small (2px) tolerance, and the `clamp`/`reflect`
|
||||
patterns stay a couple of pixels off the screen edge, so sub-pixel cursor
|
||||
placement on scaled or multi-monitor displays isn't misread as the user
|
||||
grabbing the mouse. `line` uses the `abort` policy and is unaffected.
|
||||
|
||||
### Movement strategies
|
||||
|
||||
`config.pattern` selects one of the generators in `src/strategies.ts`:
|
||||
|
||||
| Name | Motion | Bounds |
|
||||
| ------------- | ------------------------------------------------------------- | --------- |
|
||||
| `line` | Straight horizontal sweep (the original behavior). | `abort` |
|
||||
| `diagonal` | Straight line on both axes toward the roomiest corner. | `clamp` |
|
||||
| `jitter` | Small random hops within a local radius that scales with reach. | `clamp` |
|
||||
| `walk` | Cumulative random walk; bounces off the screen edges. | `reflect` |
|
||||
| `arc` | Smooth quadratic-Bézier curve to a random on-screen point. | `clamp` |
|
||||
| `figureEight` | Traces a figure-eight (lemniscate) and returns to the start. | `clamp` |
|
||||
|
||||
`stepCount` is the number of steps; `stepSize` is how many pixels each step
|
||||
travels (so total reach is `stepCount * stepSize`). With the default
|
||||
`stepSize` of 1, `line` produces the identical 1px-per-step path it always
|
||||
has. To add a pattern, write one pure generator and register it — the
|
||||
executor supplies bounds, pacing, interrupt, and restore for free.
|
||||
|
||||
### Why `mouse.config.autoDelayMs = 0`
|
||||
|
||||
nut.js inserts a 100ms delay after every action by default. With two mouse
|
||||
calls per step that would silently more-than-double the duration of a
|
||||
sweep. The script controls cadence itself via `config.stepDelay`, so the
|
||||
implicit delay is disabled at module load (a side effect of importing
|
||||
`keeper.ts`).
|
||||
sweep. The code controls cadence itself via `config.stepDelay`, so the
|
||||
implicit delay is disabled in `createNutDevice` — the single place nut.js
|
||||
is wired up. Importing the movement modules stays side-effect-free.
|
||||
|
||||
## For contributors
|
||||
|
||||
@@ -296,7 +353,10 @@ move --help
|
||||
| `src/configFile.ts` | Optional JSON config-file loader with strict schema validation. |
|
||||
| `src/editor.ts` | `move --edit`: opens the active config file in `$EDITOR`. |
|
||||
| `src/errors.ts` | Shared error types (`CliError`). |
|
||||
| `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. |
|
||||
| `src/keeper.ts` | Idle-watch loop + per-sweep glue (selects a strategy, calls the executor). |
|
||||
| `src/device.ts` | `Device` I/O seam over nut.js (`Point`, `createNutDevice`); the only nut.js importer. |
|
||||
| `src/strategies.ts` | Pure movement-pattern generators, the strategy registry, and name validation. |
|
||||
| `src/executor.ts` | `executePath` driver: bounds policy, pacing, interrupt detection, restore. |
|
||||
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
|
||||
| `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |
|
||||
| `bun.lock` | Bun's lockfile. Commit this. |
|
||||
|
||||
Reference in New Issue
Block a user