# 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 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. -m, --move-interval Idle time before a sweep fires. Default: 240. -c, --check-interval Cursor poll cadence. Default: 10. -d, --step-delay Pause between synthetic steps. Default: 50. -n, --step-count Steps per sweep. Default: 250. -V, --verbose Log every sweep, interrupt, and bounds event (default prints only the startup banner). ``` 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 ` 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, verbose)`. - `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, `DEFAULT_CONFIG`, `defaultConfigPath`, and the layered `resolveConfig` / `resolveVerbose` overlay functions. - `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, `DEFAULT_CONFIG`, `defaultConfigPath`, and the layered `resolveConfig` / `resolveVerbose` 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`.