End-user installation collapses to a single command: curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh Changes: - install.sh rewritten as a POSIX sh, curl-pipeable end-user installer. Honors XDG_DATA_HOME and XDG_BIN_HOME (de facto). Idempotent via a version-marker file at $INSTALL_DIR/.installed-version; MOVE_FORCE=1 overrides. Hard-fails if Bun is missing (no auto-install). Includes a safety guard refusing rm -rf on too-broad install dirs. - uninstall.sh added; same curl-pipe pattern, shared XDG path resolution, leaves Bun alone. - dev-setup.sh added (= the previous install.sh content, retitled for contributors and pointing at bun run start / bun link / install.sh). - README updated: curl one-liner Install section, XDG behavior tables, new Uninstall section, new 'For contributors' section, Files table rows for all three scripts.
8.2 KiB
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 >= 1.0.0
- macOS or Linux (relies on
@nut-tree-fork/nut-jsfor cross-platform mouse + screen control) - On macOS: Accessibility permission for the terminal running Bun (System Settings > Privacy & Security > Accessibility)
Install
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_HOMEis 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
move
move --help
Stop with Ctrl+C. If SIGINT arrives mid-sweep, the cursor stays at
whichever step was last commanded — by design.
Uninstall
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
Usage: move [options]
Options:
-h, --help Show this help and exit.
-v, --version Print version and exit.
-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).
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.tsis a thin entry point: parses args, dispatches--help/--version, resolves the runtime config, and callsrunKeeper(config, verbose).src/cli.tsowns argument parsing, validation, and help/version output.src/config.tsexports theConfigtype,DEFAULT_CONFIG, and theresolveConfigoverlay function.src/keeper.tsowns 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)
- Print the startup banner (unconditional).
- Snapshot
lastPosandlastActivity = now. - Every
config.checkInterval:- If the cursor moved since the last check, the user is active — reset
lastActivityandlastPos, continue. - Otherwise, if
now - lastActivity >= config.moveInterval, callsimulateActivityand reset the idleness clock.
- If the cursor moved since the last check, the user is active — reset
Synthetic sweep (simulateActivity)
- Read the starting position and current screen dimensions.
- Pick a horizontal direction (
dx = +1if there's room to the right, else-1) so the sweep stays on-screen. Vertical isdy = 0for now. - For each of
config.stepCountsteps:- 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.
- 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:
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:
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:
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.