18 Commits
Author SHA1 Message Date
nokeo08 d38949edb4 Add random pattern selection (-r / --pattern random)
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.
2026-08-18 14:36:29 -05:00
nokeo08 c8942bb380 Collapse bounds policies to reflect-only; drop abort and clamp
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.
2026-08-17 15:53:49 -05:00
nokeo08 7e632b3e9d 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.
2026-08-17 14:36:18 -05:00
nokeo08 c002a6d902 Fix stale comments to match current code
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.
2026-08-14 13:39:15 -05:00
nokeo08 ec33648e74 Remove stepCount/stepSize; patterns own their geometry
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.
2026-08-14 12:56:22 -05:00
nokeo08 db3310c247 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.
2026-08-13 15:36:22 -05:00
nokeo08 10dcc1791a Add -e/--edit flag: open config file in $EDITOR
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.
2026-06-17 22:20:27 -05:00
nokeo08 cc7a487aca Lazy-import keeper.ts so --help and --version skip nut.js load
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.
2026-06-17 21:57:42 -05:00
nokeo08 fec35a2333 Move tests out of src/ into a top-level tests/ directory
src/ now contains only source code; tests live alongside it under
tests/. Bun's test runner discovers '**/*.test.ts' so no test-runner
config change is needed.

Changes:
  - tests/config.test.ts        (was src/config.test.ts)
  - tests/configFile.test.ts    (was src/configFile.test.ts)
  - Imports updated: ./config.ts -> ../src/config.ts (likewise for
    configFile.ts and errors.ts).
  - tsconfig.json include adds 'tests/**/*.ts' so tsc type-checks
    the test files too.

All 25 tests still pass; tsc clean.
2026-06-17 21:05:20 -05:00
nokeo08 1a857de5ed Add Bun tests for resolveConfig and loadConfigFile
Two new test files exercise the layered config resolver and the JSON
config-file loader:

  src/config.test.ts        25 cases:
    - resolveConfig precedence (CLI > file > default) per field
    - seconds-to-ms conversion at the resolver boundary
    - verbose precedence across all four cells
    - defaultConfigPath: XDG honored, HOME fallback, empty-as-unset,
      throws CliError when both are missing

  src/configFile.test.ts    11 cases:
    - valid file -> overrides, with undefined for unspecified keys
    - missing explicit path throws
    - malformed JSON / non-object root throws with file path
    - unknown key / wrong type / non-positive number all throw with
      the offending key named
    - default path missing -> null (silent default)

Runs via 'bun test' (or 'bun run test', now wired in package.json).
All 25 tests pass on the current codebase.
2026-06-17 16:34:27 -05:00
nokeo08 5af253283e Introduce failRuntime() to mirror failUser() in move.ts
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.
2026-06-17 16:32:29 -05:00
nokeo08 7120c06bbe Introduce named Logger interface in keeper.ts
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.
2026-06-17 16:24:56 -05:00
nokeo08 d5656efe5b Move 'mouse.config.autoDelayMs = 0' from module-load to runKeeper
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.
2026-06-17 16:24:24 -05:00
nokeo08 8eac51cd45 Extract CliError into src/errors.ts
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.
2026-06-17 16:22:33 -05:00
nokeo08 7714a6ad1a Seed default config on install; share defaults JSON with runtime
The defaults now live in a single file, scripts/config.default.json:

- src/config.ts imports it via 'with { type: "json" }' and derives
  DEFAULT_CONFIG (with seconds->ms conversion at the boundary), so the
  CLI help text always matches what the seeded user config contains.
- scripts/install.sh copies the same file to
  $XDG_CONFIG_HOME/move/config.json only if no file is already there.
  Existing configs (yours or from a previous install) are never
  overwritten.
- scripts/uninstall.sh does not touch the user config file at all,
  following the strict Unix convention. It does print a one-line
  notice pointing at the path so the user can rm -rf the dir
  themselves if they want a fully-clean removal.
- tsconfig.json gains resolveJsonModule:true so tsc accepts the JSON
  import.
- A small runtime assertion in src/config.ts (assertSeedShape) fails
  loudly if the seed file is missing keys or has wrong types.
- README Configuration section documents the seed behavior; Uninstall
  section documents the leave-config-behind policy with the rm-it-
  yourself command; Files table gains scripts/config.default.json.
2026-06-17 13:13:49 -05:00
nokeo08 940113019d Move verbose into Config; drop resolveVerbose
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.
2026-06-17 12:45:46 -05:00
nokeo08 df9305c6ac Add JSON config file support (XDG-respecting)
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.
2026-06-17 12:27:50 -05:00
nokeo08 83d91c5630 Initial commit: move CLI (milestone 2)
- 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
2026-06-15 14:31:31 -05:00