Pick a different movement pattern every time a sweep is triggered, so the
motion varies across the day instead of repeating one shape.
- strategies.ts: add the `random` sentinel, `SELECTABLE_PATTERN_NAMES`,
`isSelectablePattern`, and `createRandomPicker`. `random` is deliberately
NOT a registry entry: it has no path of its own, so `STRATEGIES` stays a
total lookup and `PATTERN_NAMES` keeps listing only real generators. The
picker is a closure over `last`, giving a uniform draw that never returns
the same pattern twice in a row. Building CANONICAL_PATTERNS from the
selectable list makes both validation boundaries accept `random` (and
loose spellings) for free, and extends the normalization-collision
assertion to cover the sentinel.
- cli.ts: add `-r`/`--random` plus an exported `selectPattern` holding the
conflict rule. `-r` is sugar for `--pattern random`, so the two agreeing
is a no-op while `-r -p arc` is rejected as contradictory. The flag folds
into `pattern`, so ConfigOverrides, resolveConfig, and move.ts are
untouched. `parseCliArgs` now takes its argv as an optional parameter so
the flag surface is testable without process.argv.
- keeper.ts: resolve `random` via the picker once per trigger, before the
loop-mode branch, so a pick holds for a whole loop run rather than
changing mid-run. runKeeper builds one picker for the process, so the
no-repeat memory spans sweeps minutes apart. Because the pick is a real
strategy, --verbose logs the concrete pattern name and a pick with an
infinite loopPath still bounces edge-to-edge under --loop.
- config.ts / configFile.ts: accept the sentinel where a pattern is valid,
and quote the selectable list in errors. No `random` boolean config key —
the file spells it "pattern": "random".
executor.ts and move.ts needed no changes.
Tests: new tests/cli.test.ts (the file had no coverage before) covering the
flag surface and the conflict rule; picker tests pinning the no-repeat and
full-registry-coverage properties; keeper tests pinning once-per-trigger and
once-per-loop-run.
The executor kept every commanded point on-screen via a per-strategy
BoundsPolicy of abort / clamp / reflect. Measured against the real
strategies, the other two earned nothing: abort truncated a sweep at the
first edge (line on a narrow screen ran only 90 of 250 steps), and clamp
could park the cursor against an edge (a monotonic ramp stalled 162 steps
in a row) -- both counter to the program's whole purpose of keeping the
cursor moving. reflect bounces off the edge and keeps going, and is
already what line/diagonal need in loop mode. arc's declared clamp was
provably dead code (it clamps its own endpoint, so no sample ever leaves
the screen).
Collapse to reflect-only:
- strategies.ts: remove the BoundsPolicy type and the `bounds` field from
the interface and all six strategies. Keep the local clamp() helper --
it's arc's endpoint geometry, not an on-screen policy; docstring says so.
- executor.ts: resolveTarget loses its policy parameter and its null
return and just reflects both axes; delete clampInt; SweepOutcome drops
"aborted"; ExecuteOptions drops `bounds`; remove the Out of bounds log.
- keeper.ts: loopOpts is now { restore: false, loop: true } -- the
reflect override added with loop mode is redundant.
- tests: drop the abort-outcome, clamp, and bounds-override tests; simplify
fixed() to take no policy; add a regression test that a monotonic ramp
past an edge never yields two identical points in a row (the guarantee
that motivated removing clamp).
Behavior is unchanged for every pattern at normal cursor positions
(verified: line's normal sweep is byte-identical). The only differences
are at a screen edge, where motion now bounces instead of stopping. No
config keys, flags, or pattern names changed.
Docs updated to match, including in-code comments, the README strategies
table (Bounds column removed) and verbose description, the sequence
diagram (resolveTarget signature + getPosition/width ordering + a loop-mode
note), and a CHANGELOG Changed entry.
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.
The stepCount and stepSize knobs were two controls for one quantity users
actually care about (reach), and the number of steps is an implementation
detail nobody meaningfully tunes. Each pattern has a natural size and
resolution — a jitter is inherently small, an arc a broad curve — so those
now live as constants in each strategy rather than as global config.
- strategies.ts: each pattern defines its own step count and size; MoveContext
drops `config` down to pure geometry (start/width/height/rng), and the
module no longer imports Config at all (dissolving the type-only-import
cycle workaround). line stays byte-for-byte: 250 one-pixel steps.
- executor.ts: executePath takes `config` for pacing (stepDelay); the path
itself needs nothing from it.
- config.ts / cli.ts / move.ts / config.default.json: drop stepCount and
stepSize from the type, seed, validation, resolver, CLI flags (-n, -s),
and help. stepDelay stays as the one pacing lever.
- configFile.ts: tolerate the removed keys instead of rejecting them — every
pre-1.3.0 install seeded stepCount, so a hard "unknown key" failure on
upgrade is avoided. They're ignored with a one-line stderr notice; genuine
unknown keys still error.
The -n/--step-count CLI flag (shipped since 1.0.0) is now an unknown option;
config files degrade gracefully, command lines don't. Stays in the unpushed
1.3.0 release. 64 tests pass.
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.
Replaces 'ReturnType<typeof makeLogger>' with a small named interface so
simulateActivity's signature reads as (config, log: Logger) instead of a
type-derivation chain. Also makes it cleaner to substitute a mock logger
if simulateActivity is ever unit-tested.
No behavior change.
The assignment used to run at import time, mutating the shared nut.js
singleton the second anything imported keeper.ts. Moved into the top
of runKeeper(), where the only caller actually needs it.
Same observable behavior — runKeeper is called exactly once per process
— but keeper.ts is now import-side-effect-free, which makes it
straightforward to import for tests or future tooling without touching
global mouse state.
Verbose was awkwardly a separate runKeeper argument with a separate
resolveVerbose helper, even though it's just another tunable on the
same precedence ladder as the numeric fields. This commit collapses it.
Changes:
- Config gains 'readonly verbose: boolean'. DEFAULT_CONFIG sets it to
false.
- resolveConfig now returns the full Config including verbose. Verbose
layers with the same 'first defined value wins' precedence as the
numeric fields.
- resolveVerbose is gone.
- ParsedCliArgs.verbose becomes boolean | undefined: undefined when -V
was not passed, true when it was. parseCliArgs maps accordingly.
This lets the layered resolver treat verbose uniformly.
- runKeeper takes a single Config arg. The internal logger reads from
config.verbose at the top of runKeeper.
- move.ts no longer plumbs verbose separately; the single resolved
Config drives everything.
- README How-it-works and Files-table entries updated to match.
Behavior verified for all five verbose-precedence cases (no file/no
flag, no file/-V, file:true/no flag, file:false/-V, file:false/no
flag) and config-error scenarios remain intact.
- src/move.ts entry point with CLI parsing, --help, --version
- src/cli.ts: parseCliArgs, printHelp, ParsedCliArgs, CliError, VERSION
- src/config.ts: Config type, DEFAULT_CONFIG, resolveConfig
- src/keeper.ts: synthetic-activity sweep + idle-watch loop
- package.json bin entry + shebang for 'bun link' global install
- install.sh: contributor bootstrap (will be repurposed; see
DISTRIBUTION-PLAN.md for the end-user installer design)
- DISTRIBUTION-PLAN.md captures the tabled end-user distribution work