# 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/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/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`. ## How it works The source lives under `src/`, split into an entry point plus three logic modules: - `src/move.ts` is a thin entry point: parses args, dispatches `--help` / `--version`, resolves the runtime config, and calls `runKeeper(config, verbose)`. - `src/cli.ts` owns argument parsing, validation, and help/version output. - `src/config.ts` exports the `Config` type, `DEFAULT_CONFIG`, and the `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 ./dev-setup.sh ``` `dev-setup.sh` verifies Bun is installed and runs `bun install` (with devDependencies, unlike the end-user `install.sh`). 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 | | ------------------- | ----------------------------------------------------------------------------- | | `install.sh` | End-user installer; curl-pipeable from Gitea. | | `uninstall.sh` | End-user uninstaller; curl-pipeable from Gitea. | | `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`, and `resolveConfig` overlay. | | `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`.