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:
2026-08-17 14:36:18 -05:00
parent 1ad724cd33
commit 7e632b3e9d
15 changed files with 397 additions and 31 deletions
+16
View File
@@ -17,6 +17,7 @@ const NONE: ConfigOverrides = {
stepDelay: undefined,
pattern: undefined,
verbose: undefined,
loop: undefined,
};
describe("resolveConfig", () => {
@@ -78,6 +79,21 @@ describe("resolveConfig", () => {
const cfg = resolveConfig(null, NONE);
expect(cfg.verbose).toBe(DEFAULT_CONFIG.verbose);
});
test("loop: CLI true wins over file false", () => {
const cfg = resolveConfig({ ...NONE, loop: false }, { ...NONE, loop: true });
expect(cfg.loop).toBe(true);
});
test("loop: file true wins over default (no CLI)", () => {
const cfg = resolveConfig({ ...NONE, loop: true }, NONE);
expect(cfg.loop).toBe(true);
});
test("loop: falls back to DEFAULT_CONFIG.loop when neither set", () => {
const cfg = resolveConfig(null, NONE);
expect(cfg.loop).toBe(DEFAULT_CONFIG.loop);
});
});
describe("defaultConfigPath", () => {