940113019d
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.
271 lines
10 KiB
Markdown
271 lines
10 KiB
Markdown
# Teams Status Keeper
|
|
|
|
Keeps Microsoft Teams (or any presence-tracking app) from marking you as "Away"
|
|
by nudging the mouse cursor when the machine has been idle long enough to
|
|
trigger an idle timeout.
|
|
|
|
Real user movement always wins: the script never fires while the user is
|
|
actively using the mouse, and any synthetic sweep aborts the moment the
|
|
cursor leaves the position the script just commanded.
|
|
|
|
## Requirements
|
|
|
|
- [Bun](https://bun.sh) >= 1.0.0
|
|
- macOS or Linux (relies on
|
|
[`@nut-tree-fork/nut-js`](https://github.com/nut-tree-fork/nut.js) for
|
|
cross-platform mouse + screen control)
|
|
- On macOS: Accessibility permission for the terminal running Bun
|
|
(System Settings > Privacy & Security > Accessibility)
|
|
|
|
## Install
|
|
|
|
```sh
|
|
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/scripts/install.sh | sh
|
|
```
|
|
|
|
This fetches the latest `master` from Gitea, runs `bun install --production`
|
|
under the install dir, and drops a `move` wrapper on your bin dir.
|
|
|
|
The installer respects the XDG Base Directory Specification:
|
|
|
|
- Source lives at `$XDG_DATA_HOME/move` (default `~/.local/share/move`).
|
|
- Wrapper goes to `$XDG_BIN_HOME/move` (default `~/.local/bin/move`).
|
|
`XDG_BIN_HOME` is the widely-recognized de facto convention; XDG itself
|
|
doesn't standardize a user bin dir.
|
|
|
|
Env vars (all optional):
|
|
|
|
| Var | Default | Purpose |
|
|
| --- | ------- | ------- |
|
|
| `MOVE_VERSION` | `master` | Branch or tag to install. Pin with e.g. `v1.0.0`. |
|
|
| `MOVE_FORCE` | unset | Set to `1` to reinstall when the same version is already present. |
|
|
| `XDG_DATA_HOME` | `$HOME/.local/share` | Where the source tree is installed (under `move/`). |
|
|
| `XDG_BIN_HOME` | `$HOME/.local/bin` | Where the `move` wrapper is placed. |
|
|
|
|
Bun must already be installed; the installer fails with a clear pointer
|
|
to <https://bun.sh> if it isn't.
|
|
|
|
If your bin dir isn't on `PATH`, the installer prints the line to add to
|
|
your shell rc. It will not modify rc files for you.
|
|
|
|
## Run
|
|
|
|
```sh
|
|
move
|
|
move --help
|
|
```
|
|
|
|
Stop with `Ctrl+C`. If `SIGINT` arrives mid-sweep, the cursor stays at
|
|
whichever step was last commanded — by design.
|
|
|
|
## Uninstall
|
|
|
|
```sh
|
|
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/scripts/uninstall.sh | sh
|
|
```
|
|
|
|
Removes the wrapper at `$XDG_BIN_HOME/move` and the install tree at
|
|
`$XDG_DATA_HOME/move`. Bun stays — it's your runtime, not ours.
|
|
|
|
## Usage
|
|
|
|
```text
|
|
Usage: move [options]
|
|
|
|
Options:
|
|
-h, --help Show this help and exit.
|
|
-v, --version Print version and exit.
|
|
-C, --config <path> Load defaults from a JSON config file.
|
|
Default path: see the Configuration section.
|
|
-m, --move-interval <seconds> Idle time before a sweep fires. Default: 240.
|
|
-c, --check-interval <seconds> Cursor poll cadence. Default: 10.
|
|
-d, --step-delay <ms> Pause between synthetic steps. Default: 50.
|
|
-n, --step-count <pixels> Steps per sweep. Default: 250.
|
|
-V, --verbose Log every sweep, interrupt, and bounds event
|
|
(default prints only the startup banner).
|
|
|
|
Precedence (highest wins): CLI flags > config file > built-in defaults.
|
|
```
|
|
|
|
Run `move --help` for the resolved default config-file path on your
|
|
system. Numeric overrides are layered onto the defaults via
|
|
`resolveConfig` in `src/config.ts`; time-valued inputs (`-m`, `-c`) are
|
|
expressed in seconds at the CLI boundary and converted to milliseconds
|
|
internally.
|
|
|
|
Logging is **quiet by default**: only the startup banner ("Teams Status
|
|
Keeper started…") and any error from an unhandled rejection print on a
|
|
default run. `-V` / `--verbose` opens up per-sweep, user-interrupt, and
|
|
out-of-bounds events.
|
|
|
|
Invalid input (unknown flag, missing value, non-positive number) prints an
|
|
error to `stderr` and exits with code `2`.
|
|
|
|
## Configuration
|
|
|
|
`move` reads an optional JSON config file at:
|
|
|
|
```
|
|
${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json
|
|
```
|
|
|
|
If the file is missing, defaults are used (no config file is required).
|
|
Pass `-C` / `--config <path>` to point at a different file; in that mode
|
|
the file must exist.
|
|
|
|
### Precedence
|
|
|
|
```
|
|
CLI flags > config file > built-in defaults
|
|
```
|
|
|
|
CLI flags always win. The config file fills in any flag the user didn't
|
|
pass on the command line. Built-in defaults fill in anything the file
|
|
doesn't set.
|
|
|
|
### Example
|
|
|
|
```jsonc
|
|
{
|
|
"moveInterval": 240,
|
|
"checkInterval": 10,
|
|
"stepDelay": 50,
|
|
"stepCount": 250,
|
|
"verbose": false
|
|
}
|
|
```
|
|
|
|
All keys are optional; supply only the ones you want to override. Keys
|
|
and units mirror the CLI flags exactly: `moveInterval` and
|
|
`checkInterval` are seconds, `stepDelay` is milliseconds, `stepCount` is
|
|
pixels, `verbose` is a boolean.
|
|
|
|
### Validation
|
|
|
|
The loader is strict:
|
|
|
|
- Root must be a JSON object.
|
|
- Unknown keys are rejected (catches typos like `"movInterval"`).
|
|
- Numeric values must be finite and strictly positive.
|
|
- `verbose` must be a boolean.
|
|
|
|
Any validation failure prints a message naming the file and the offending
|
|
key to `stderr` and exits `2`.
|
|
|
|
### Known limitation: `verbose` can be turned on but not off from the CLI
|
|
|
|
`--verbose` is a presence-only flag (there is no `--no-verbose`). If the
|
|
config file sets `"verbose": true`, the CLI cannot force quiet mode in
|
|
that invocation. Workarounds: edit the file, or point at a different
|
|
file with `--config`.
|
|
|
|
## How it works
|
|
|
|
The source lives under `src/`, split into an entry point plus four logic
|
|
modules:
|
|
|
|
- `src/move.ts` is a thin entry point: parses args, dispatches `--help` /
|
|
`--version`, loads the config file, resolves the layered runtime
|
|
config, and calls `runKeeper(config)`.
|
|
- `src/cli.ts` owns argument parsing, validation, and help/version output.
|
|
- `src/configFile.ts` owns optional JSON config-file loading + strict
|
|
schema validation.
|
|
- `src/config.ts` exports the `Config` type (which carries every tunable
|
|
including `verbose`), `DEFAULT_CONFIG`, `defaultConfigPath`, and the
|
|
layered `resolveConfig` overlay function.
|
|
- `src/keeper.ts` owns the synthetic-activity sweep and the idle-watch loop.
|
|
|
|
Defaults live in `src/config.ts` as `DEFAULT_CONFIG`:
|
|
|
|
| Field | Default | CLI flag | Purpose |
|
|
| --------------- | ------------ | ------------------------- | ---------------------------------------------------------------- |
|
|
| `moveInterval` | `4 * 60_000` | `-m`, `--move-interval` | Idle time (ms) required before a synthetic sweep fires. |
|
|
| `checkInterval` | `10_000` | `-c`, `--check-interval` | How often (ms) the main loop polls the cursor for real activity. |
|
|
| `stepDelay` | `50` | `-d`, `--step-delay` | Pause (ms) between individual synthetic steps in a sweep. |
|
|
| `stepCount` | `250` | `-n`, `--step-count` | Pixel-steps per sweep. |
|
|
|
|
`-m` and `-c` are accepted in seconds at the CLI; `resolveConfig` converts
|
|
to milliseconds before handing the resolved `Config` to `runKeeper`.
|
|
|
|
### Main loop (`runKeeper`)
|
|
|
|
1. Print the startup banner (unconditional).
|
|
2. Snapshot `lastPos` and `lastActivity = now`.
|
|
3. Every `config.checkInterval`:
|
|
- If the cursor moved since the last check, the user is active — reset
|
|
`lastActivity` and `lastPos`, continue.
|
|
- Otherwise, if `now - lastActivity >= config.moveInterval`, call
|
|
`simulateActivity` and reset the idleness clock.
|
|
|
|
### Synthetic sweep (`simulateActivity`)
|
|
|
|
1. Read the starting position and current screen dimensions.
|
|
2. Pick a horizontal direction (`dx = +1` if there's room to the right,
|
|
else `-1`) so the sweep stays on-screen. Vertical is `dy = 0` for now.
|
|
3. For each of `config.stepCount` steps:
|
|
- Compute and bounds-check the next target.
|
|
- Move the cursor there, sleep `config.stepDelay`.
|
|
- Re-read the cursor. If it isn't where we put it, the user moved it —
|
|
log (when `--verbose`) and return early without snapping back.
|
|
4. On a clean full sweep, restore the cursor to its starting position so
|
|
the next idle-check sees "no movement" and doesn't misread the synthetic
|
|
activity as real user input.
|
|
|
|
### Why `mouse.config.autoDelayMs = 0`
|
|
|
|
nut.js inserts a 100ms delay after every action by default. With two mouse
|
|
calls per step that would silently more-than-double the duration of a
|
|
sweep. The script controls cadence itself via `config.stepDelay`, so the
|
|
implicit delay is disabled at module load (a side effect of importing
|
|
`keeper.ts`).
|
|
|
|
## For contributors
|
|
|
|
Clone the repo and bootstrap a dev environment:
|
|
|
|
```sh
|
|
git clone https://gitea.cahlen.com/nokeo08/Move.git
|
|
cd Move
|
|
./scripts/dev-setup.sh
|
|
```
|
|
|
|
`scripts/dev-setup.sh` verifies Bun is installed and runs `bun install`
|
|
(with devDependencies, unlike the end-user `scripts/install.sh`). It
|
|
operates at the repo root regardless of the CWD you invoke it from.
|
|
|
|
Run from the source tree:
|
|
|
|
```sh
|
|
bun run start # via the package.json script
|
|
bun run src/move.ts # direct
|
|
bun run src/move.ts --help
|
|
```
|
|
|
|
Or install a global `move` pointed at your checkout:
|
|
|
|
```sh
|
|
bun link
|
|
move --help
|
|
```
|
|
|
|
## Files
|
|
|
|
| File | Purpose |
|
|
| ------------------- | ----------------------------------------------------------------------------- |
|
|
| `scripts/install.sh` | End-user installer; curl-pipeable from Gitea. |
|
|
| `scripts/uninstall.sh` | End-user uninstaller; curl-pipeable from Gitea. |
|
|
| `scripts/dev-setup.sh` | Contributor bootstrap (verify Bun + `bun install`). |
|
|
| `src/move.ts` | CLI entry point: parses args, dispatches help/version, starts the loop. |
|
|
| `src/cli.ts` | Argument parsing, validation, and help/version output. |
|
|
| `src/config.ts` | `Config` type (carries every tunable, including `verbose`), `DEFAULT_CONFIG`, `defaultConfigPath`, and the layered `resolveConfig` overlay. |
|
|
| `src/configFile.ts` | Optional JSON config-file loader with strict schema validation. |
|
|
| `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. |
|
|
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
|
|
| `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |
|
|
| `bun.lock` | Bun's lockfile. Commit this. |
|
|
| `LICENSE` | GPLv3 license text. |
|
|
|
|
## License
|
|
|
|
GPL-3.0-only. See `LICENSE`.
|