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.
Audited comments project-wide against the current implementation:
- scripts/install.sh, scripts/uninstall.sh: header curl URLs pointed at a
root-level install.sh/uninstall.sh, but the files live under scripts/ —
the documented command 404'd. Corrected to the scripts/ path (matching
the README and the actual file location).
- src/move.ts: header said it ties "four logic modules" and omitted
editor.ts; the --edit terminal action was also missing from the order of
operations. Both corrected.
- src/config.ts: numeric Config fields are all milliseconds now; dropped the
stale "pixels" unit left over from stepCount/stepSize.
Comments-only (plus two script header lines); tsc clean, 64 tests 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.
New module src/editor.ts handles the flag end-to-end:
- editorCommand(editor, path) returns the sh -c argv that lets the
shell tokenize multi-word $EDITOR values like 'code --wait'.
Extracted so editor.test.ts can verify construction without
actually launching an editor.
- editConfig(path) checks $EDITOR is set, checks the target file
exists, spawns 'sh -c <editor> "$@" -- <path>' with stdio
inherited, and exits with the editor's status code.
Refuses (CliError -> exit 2) when:
- $EDITOR is unset or empty.
- The target config file doesn't exist. (Same recovery hint as
elsewhere: 'run move once or reinstall'.)
src/cli.ts adds the flag to the parser and printHelp(). src/move.ts
dispatches it after --version and before config-load. Resolution
mirrors the loader: --config <path> wins, else defaultConfigPath().
8 new tests in src/editor.test.ts cover the pure helper and both
refusal paths; the spawn success path is verified via manual
'EDITOR=true move -e' (would otherwise kill the test process).
README Usage block, Configuration section (new Editing subsection),
and Files table all updated to match.
keeper.ts statically imports @nut-tree-fork/nut-js, which dlopens a
sizeable native .node binary. That load dominated cold-start: ~1.2s
for 'move --version' immediately after install, vs ~125ms warm.
For a flag that just prints a string, that's all overhead.
Change: dynamic 'await import("./keeper.ts")' placed after the
--help and --version short-circuits. The dynamic import is wrapped
in try/catch so import-time failures (e.g., missing native binary,
unsupported architecture) route through the same failRuntime() path
as anything thrown by the loop.
Expected cold start for --help and --version drops to ~50-150ms
(Bun + reading a handful of small files, no native module load).
move with no flags pays the same load cost as today.
tsc --noEmit and the test suite remain clean.
The two stderr-fatal paths used to be asymmetric: failUser() is a named
helper with 'move:' prefix and a 'Try --help' hint, while the runKeeper
catch was an inline 'console.error("Error:", err)' + process.exit(1).
failRuntime() now sits next to failUser(), so the entry-point reads as
'one of two well-named fatal paths.' It also prefers err.stack when
available, giving better diagnostics for nut.js / Accessibility-permission
failures than the previous formatter.
Same observable behavior on the success path; failure messages on the
runtime path are now more debuggable.
CliError used to live in cli.ts and was imported by configFile.ts purely
to grab a one-line class — the dependency arrow said 'config-file loader
depends on the CLI parser' when really it just needed a shared error
type.
Moving CliError to its own errors.ts module flattens the graph:
errors.ts (no internal deps)
|
+-- cli.ts
+-- configFile.ts
+-- config.ts (will use it in the next commit)
cli.ts re-exports CliError for any consumer that prefers to keep
importing it from there; the canonical home is now errors.ts.
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.
End users can now set defaults in a config file at:
${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json
CLI flags continue to win when both are set:
CLI flags > config file > built-in defaults
Schema mirrors the CLI flag names and units. Loader is strict: unknown
keys, wrong types, and non-positive numerics are rejected with a clear
message naming the file and key, and the process exits 2.
Changes:
- New src/configFile.ts: existence-aware loader + strict schema
validation. Throws CliError; the entry point converts those to exit 2.
- src/config.ts: ConfigOverrides gains 'verbose'; resolveConfig takes
both file and CLI override layers; new defaultConfigPath() honors
XDG_CONFIG_HOME; new resolveVerbose() layers verbose with the
presence-only-CLI semantics documented.
- src/cli.ts: -C/--config <path> flag. printHelp prints the default
config path and the precedence rule.
- src/move.ts: loads the config file (default XDG path or --config),
passes both override layers into resolveConfig, resolves verbose
separately, exits 2 on any validation failure.
- README: new Configuration section with path, precedence, example,
validation rules, and the verbose CLI-can't-turn-off limitation.
Files table gains src/configFile.ts.
- 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