Add loop mode (--loop): repeat movement until user activity
Introduce a continuous "loop" setting so a triggered sweep keeps the
cursor moving until the user moves the mouse (or Ctrl+C), instead of
firing a single sweep.
- strategies.ts: add optional `loopPath` to MovementStrategy; give `line`
and `diagonal` infinite loop generators that pick a direction once and
ramp forever (4px/step). Their finite `path` and declared `bounds` are
unchanged, so single-sweep behavior is identical.
- executor.ts: add ExecuteOptions { restore?, bounds?, loop? }. Omitting
options reproduces the original single-sweep contract exactly.
- keeper.ts: in loop mode, run an infinite loopPath once (stopped only by
interruption) or chain a finite path cycle after cycle; force `reflect`
bounds for every pattern and suppress the between-cycle restore, so
line/diagonal bounce edge-to-edge instead of stopping at the first edge.
- config plumbing: new boolean `loop` through config.default.json,
config.ts, configFile.ts, cli.ts (-l/--loop), and move.ts, mirroring
the existing `verbose` precedence.
- docs: README loop-mode section + usage/validation updates; CHANGELOG
Unreleased entry.
- tests: loopPath generators, executor options (bounds override, loop
selection, restore suppression), config/configFile loop plumbing, and
keeper-level loop behavior (ramps far vs. bounded single-sweep, chained
cycles). 79 pass.
This commit is contained in:
+48
-12
@@ -24,7 +24,7 @@
|
||||
*/
|
||||
|
||||
import { createNutDevice, type Device, type Point } from "./device.ts";
|
||||
import { executePath, type Logger } from "./executor.ts";
|
||||
import { executePath, type Logger, type SweepOutcome } from "./executor.ts";
|
||||
import { DEFAULT_PATTERN, STRATEGIES, type MoveContext } from "./strategies.ts";
|
||||
|
||||
import type { Config } from "./config.ts";
|
||||
@@ -46,24 +46,60 @@ function makeLogger(verbose: boolean): Logger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a single synthetic mouse-activity sweep.
|
||||
* Perform synthetic mouse activity once the keeper decides the cursor is
|
||||
* idle.
|
||||
*
|
||||
* Snapshots the cursor and screen (re-read every call so monitor changes
|
||||
* are handled), selects the configured strategy from the registry, and
|
||||
* hands the resulting path to `executePath`, which owns bounds, pacing,
|
||||
* interrupt detection, and restore-on-clean. An unknown `config.pattern`
|
||||
* falls back to the default strategy defensively; validation at the CLI /
|
||||
* config-file boundary should prevent that from ever happening.
|
||||
* Snapshots the screen (re-read every call so monitor changes are handled)
|
||||
* and selects the configured strategy from the registry. An unknown
|
||||
* `config.pattern` falls back to the default strategy defensively; validation
|
||||
* at the CLI / config-file boundary should prevent that from ever happening.
|
||||
*
|
||||
* Single-sweep mode (`config.loop === false`) runs exactly one sweep via
|
||||
* `executePath`, which owns bounds, pacing, interrupt detection, and
|
||||
* restore-on-clean — unchanged from before loop mode existed.
|
||||
*
|
||||
* Loop mode (`config.loop === true`) keeps the cursor moving until the
|
||||
* user moves the mouse (or Ctrl+C). Two things change for every pattern:
|
||||
* the cursor is never restored between iterations (`restore: false`), and the
|
||||
* bounds policy is forced to `reflect` so edge-seeking paths bounce off the
|
||||
* screen instead of aborting (`line`) or sticking in a corner (`clamp`).
|
||||
* Patterns that define an infinite `loopPath` (`line`, `diagonal`) run it once
|
||||
* and are stopped only by interruption; the rest have their finite `path`
|
||||
* chained, re-read from the cursor's current position each cycle. Per-cycle
|
||||
* event logs are suppressed to avoid unbounded output — one line brackets the
|
||||
* run at each end.
|
||||
*/
|
||||
async function simulateActivity(config: Config, log: Logger, device: Device): Promise<void> {
|
||||
const start: Point = await device.getPosition();
|
||||
const width: number = await device.width();
|
||||
const height: number = await device.height();
|
||||
|
||||
const strategy = STRATEGIES[config.pattern] ?? STRATEGIES[DEFAULT_PATTERN]!;
|
||||
const ctx: MoveContext = { start, width, height, rng: Math.random };
|
||||
|
||||
await executePath(strategy, ctx, device, log, config);
|
||||
if (!config.loop) {
|
||||
const start: Point = await device.getPosition();
|
||||
const ctx: MoveContext = { start, width, height, rng: Math.random };
|
||||
await executePath(strategy, ctx, device, log, config);
|
||||
return;
|
||||
}
|
||||
|
||||
log.event(`Loop mode (${strategy.name}); repeating until you move the mouse.`);
|
||||
const cycleLog: Logger = { info: log.info, event: (): void => {} };
|
||||
const loopOpts = { restore: false, bounds: "reflect" as const, loop: true };
|
||||
|
||||
let cycles = 0;
|
||||
let outcome: SweepOutcome;
|
||||
do {
|
||||
const start: Point = await device.getPosition();
|
||||
const ctx: MoveContext = { start, width, height, rng: Math.random };
|
||||
outcome = await executePath(strategy, ctx, device, cycleLog, config, loopOpts);
|
||||
cycles++;
|
||||
// Spin guard for the chained-repeat path: a finite strategy that
|
||||
// yielded nothing would otherwise return "completed" instantly in a
|
||||
// tight loop. Sleeping one stepDelay makes that harmless. An infinite
|
||||
// loopPath never returns "completed", so this branch is skipped there.
|
||||
if (outcome === "completed") await device.sleep(config.stepDelay);
|
||||
} while (outcome === "completed");
|
||||
|
||||
log.event(`Loop run ended after ${cycles} cycle(s): ${outcome}.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user