From aeb28958de0b8394b21204890fac1d20c7206960 Mon Sep 17 00:00:00 2001 From: ckoch Date: Thu, 23 Jul 2026 17:11:03 -0400 Subject: [PATCH] Initial commit: Teams keep-active tool for Linux (X11) and Windows Cross-platform anti-idle utility that keeps Microsoft Teams showing Available by resetting the OS idle timer with an invisible input signal. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 2 + README.md | 72 ++++++++++++++++++++++++ stay_active.py | 113 ++++++++++++++++++++++++++++++++++++++ stay_active_win.py | 133 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 stay_active.py create mode 100644 stay_active_win.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2ae848 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# teamsTrick + +Keep Microsoft Teams (and any presence system driven by OS idle time) showing +**Available** while you're reading, watching a video, on a call, or otherwise not +touching the keyboard. Teams flips you to *Away* after roughly 5 minutes of no +input; these scripts reset the OS idle timer with a brief, invisible signal so +that never happens. + +No third-party packages — each script uses only the standard OS APIs via Python's +`ctypes`. + +## Scripts + +| Script | Platform | Method | +| --- | --- | --- | +| `stay_active.py` | Linux (X11) | 1px mouse nudge (or key tap) via the X11 XTest extension | +| `stay_active_win.py` | Windows | Invisible F15 key tap (or mouse nudge) via the Win32 `SendInput` API | + +## Requirements + +- Python 3 +- **Linux:** an **X11** session (`echo $XDG_SESSION_TYPE` → `x11`) and the + `libX11` / `libXtst` system libraries (present on essentially every desktop). + Wayland is not supported — XTest does not reset the idle timer there. +- **Windows:** nothing beyond Python. If `python` isn't on your PATH, use `py`. + +Run the script from **inside your desktop session** (not over SSH) so it can talk +to the display / input system. + +## Usage + +### Linux + +```bash +./stay_active.py # default: invisible 1px mouse nudge every 120s +./stay_active.py -i 90 # nudge every 90s +./stay_active.py -m key -k shift # tap Shift instead of moving the mouse +./stay_active.py -q & # quiet, run in the background +``` + +### Windows + +``` +python stay_active_win.py # default: invisible F15 tap every 120s +python stay_active_win.py -i 90 # every 90s +python stay_active_win.py -m mouse # 1px mouse nudge instead +python stay_active_win.py --no-sleep # also keep the PC/display awake +python stay_active_win.py -q # quiet, good for background +``` + +Stop either script with **Ctrl+C**. + +## Options + +| Flag | Description | +| --- | --- | +| `-i`, `--interval` | Seconds between signals (default `120`). Keep it under Teams' ~300s Away threshold. | +| `-m`, `--method` | `mouse` or `key`. Default is `mouse` on Linux, `key` on Windows. | +| `-k`, `--key` | Which key to tap in key mode (Linux: `f13`–`f15`, `shift`). | +| `--no-sleep` | *(Windows only)* Also prevent the system and display from sleeping. | +| `-q`, `--quiet` | Suppress the heartbeat output. | + +## Notes & caveats + +- It only prevents the **idle → Away** transition. It does **not** override a + status you set manually (e.g. *Do Not Disturb*). +- It won't help if the machine **sleeps** — a sleeping PC goes offline regardless. + On Windows use `--no-sleep`; on Linux disable screen lock / suspend if you need + to stay green while away for a long time. +- The signals are designed to be invisible: the mouse nudge returns the cursor to + where it started, and F15 / the chosen keys aren't bound to anything on a normal + desktop. diff --git a/stay_active.py b/stay_active.py new file mode 100755 index 0000000..766c567 --- /dev/null +++ b/stay_active.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""stay_active.py — keep Microsoft Teams (and any OS-idle-based presence) showing 'Available'. + +Teams marks you 'Away' when the OS reports no keyboard/mouse activity for ~5 minutes. +This tool resets the X11 idle timer periodically by tapping a harmless key (F15 by +default — it has no visible effect and is not bound to anything on a normal desktop). + +No third-party packages required: it calls the X11 XTest extension directly via ctypes. +X11 sessions only (XDG_SESSION_TYPE=x11). Press Ctrl+C to stop. +""" + +import argparse +import ctypes +import os +import sys +import time + +# --- X11 / XTest bindings via ctypes ----------------------------------------- + +try: + _x11 = ctypes.CDLL("libX11.so.6") + _xtst = ctypes.CDLL("libXtst.so.6") +except OSError as e: + sys.exit(f"Could not load X11 libraries: {e}\nThis tool requires an X11 session.") + +_x11.XOpenDisplay.restype = ctypes.c_void_p +_x11.XOpenDisplay.argtypes = [ctypes.c_char_p] +_x11.XKeysymToKeycode.restype = ctypes.c_uint +_x11.XKeysymToKeycode.argtypes = [ctypes.c_void_p, ctypes.c_ulong] +_x11.XFlush.argtypes = [ctypes.c_void_p] +_xtst.XTestFakeKeyEvent.argtypes = [ + ctypes.c_void_p, ctypes.c_uint, ctypes.c_int, ctypes.c_ulong +] +_xtst.XTestFakeRelativeMotionEvent.argtypes = [ + ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_ulong +] + +# X keysyms for a few no-op keys. F15 is the safest default. +KEYSYMS = { + "f13": 0xFFCA, + "f14": 0xFFCB, + "f15": 0xFFCC, + "shift": 0xFFE1, # Shift_L — no character, but modifies if held with another key +} + + +def open_display(): + disp = _x11.XOpenDisplay(None) + if not disp: + display = os.environ.get("DISPLAY", "(unset)") + sys.exit(f"Could not open X display (DISPLAY={display}). Run inside your desktop session.") + return disp + + +def tap_key(disp, keycode): + _xtst.XTestFakeKeyEvent(disp, keycode, True, 0) # press + _xtst.XTestFakeKeyEvent(disp, keycode, False, 0) # release + _x11.XFlush(disp) + + +def nudge_mouse(disp): + # Move 1px, then back — resets the idle timer without the cursor visibly moving. + _xtst.XTestFakeRelativeMotionEvent(disp, 1, 0, 0) + _xtst.XTestFakeRelativeMotionEvent(disp, -1, 0, 0) + _x11.XFlush(disp) + + +def main(): + p = argparse.ArgumentParser(description="Keep Teams presence 'Available' by resetting the idle timer.") + p.add_argument("-i", "--interval", type=float, default=120, + help="seconds between key taps (default: 120; keep under Teams' ~300s Away threshold)") + p.add_argument("-m", "--method", choices=["mouse", "key"], default="mouse", + help="how to signal activity (default: mouse — a 1px nudge that returns)") + p.add_argument("-k", "--key", choices=KEYSYMS.keys(), default="shift", + help="key to tap when --method key is used (default: shift — always mapped)") + p.add_argument("-q", "--quiet", action="store_true", help="don't print heartbeat messages") + args = p.parse_args() + + if os.environ.get("XDG_SESSION_TYPE", "").lower() not in ("x11", ""): + print(f"Warning: session type is '{os.environ.get('XDG_SESSION_TYPE')}', not x11. " + "XTest may not reset the idle timer under Wayland.", file=sys.stderr) + + disp = open_display() + + if args.method == "key": + keycode = _x11.XKeysymToKeycode(disp, KEYSYMS[args.key]) + if keycode == 0: + sys.exit(f"Key '{args.key}' has no keycode on this layout. Try --method mouse.") + action = lambda: tap_key(disp, keycode) + label = f"tapping '{args.key}'" + else: + action = lambda: nudge_mouse(disp) + label = "nudging mouse 1px" + + if not args.quiet: + print(f"stay_active: {label} every {args.interval:g}s. Press Ctrl+C to stop.") + + taps = 0 + try: + while True: + action() + taps += 1 + if not args.quiet: + print(f"\r active — {taps} signal(s) sent, last at {time.strftime('%H:%M:%S')}", + end="", flush=True) + time.sleep(args.interval) + except KeyboardInterrupt: + if not args.quiet: + print("\nstay_active: stopped.") + + +if __name__ == "__main__": + main() diff --git a/stay_active_win.py b/stay_active_win.py new file mode 100644 index 0000000..f50c11f --- /dev/null +++ b/stay_active_win.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""stay_active_win.py — keep Microsoft Teams showing 'Available' on Windows. + +Teams marks you 'Away' after ~5 minutes of no keyboard/mouse input. This tool +resets Windows' idle timer periodically by sending a harmless input via the Win32 +SendInput API. It also optionally tells Windows not to sleep/turn off the display +while running. + +No third-party packages required — pure ctypes against user32/kernel32. +Windows only. Press Ctrl+C to stop. +""" + +import argparse +import ctypes +import sys +import time +from ctypes import wintypes + +if not sys.platform.startswith("win"): + sys.exit("This script is for Windows. On Linux use stay_active.py instead.") + +user32 = ctypes.WinDLL("user32", use_last_error=True) +kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + +# --- SendInput structures ---------------------------------------------------- + +ULONG_PTR = wintypes.WPARAM + +INPUT_MOUSE = 0 +INPUT_KEYBOARD = 1 + +MOUSEEVENTF_MOVE = 0x0001 + +KEYEVENTF_KEYUP = 0x0002 +VK_F15 = 0x7E # virtual key; valid even without a physical F15 on the keyboard + +# SetThreadExecutionState flags (keep system/display awake) +ES_CONTINUOUS = 0x80000000 +ES_SYSTEM_REQUIRED = 0x00000001 +ES_DISPLAY_REQUIRED = 0x00000002 + + +class MOUSEINPUT(ctypes.Structure): + _fields_ = [ + ("dx", wintypes.LONG), + ("dy", wintypes.LONG), + ("mouseData", wintypes.DWORD), + ("dwFlags", wintypes.DWORD), + ("time", wintypes.DWORD), + ("dwExtraInfo", ULONG_PTR), + ] + + +class KEYBDINPUT(ctypes.Structure): + _fields_ = [ + ("wVk", wintypes.WORD), + ("wScan", wintypes.WORD), + ("dwFlags", wintypes.DWORD), + ("time", wintypes.DWORD), + ("dwExtraInfo", ULONG_PTR), + ] + + +class _INPUTunion(ctypes.Union): + _fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT)] + + +class INPUT(ctypes.Structure): + _fields_ = [("type", wintypes.DWORD), ("u", _INPUTunion)] + + +def _send(*inputs): + n = len(inputs) + arr = (INPUT * n)(*inputs) + sent = user32.SendInput(n, arr, ctypes.sizeof(INPUT)) + if sent != n: + raise ctypes.WinError(ctypes.get_last_error()) + + +def nudge_mouse(): + # Relative move +1px then -1px: cursor ends where it started, effectively invisible. + right = INPUT(type=INPUT_MOUSE, u=_INPUTunion(mi=MOUSEINPUT(1, 0, 0, MOUSEEVENTF_MOVE, 0, 0))) + left = INPUT(type=INPUT_MOUSE, u=_INPUTunion(mi=MOUSEINPUT(-1, 0, 0, MOUSEEVENTF_MOVE, 0, 0))) + _send(right, left) + + +def tap_f15(): + down = INPUT(type=INPUT_KEYBOARD, u=_INPUTunion(ki=KEYBDINPUT(VK_F15, 0, 0, 0, 0))) + up = INPUT(type=INPUT_KEYBOARD, u=_INPUTunion(ki=KEYBDINPUT(VK_F15, 0, KEYEVENTF_KEYUP, 0, 0))) + _send(down, up) + + +def main(): + p = argparse.ArgumentParser(description="Keep Teams presence 'Available' on Windows.") + p.add_argument("-i", "--interval", type=float, default=120, + help="seconds between signals (default: 120; keep under Teams' ~300s Away threshold)") + p.add_argument("-m", "--method", choices=["mouse", "key"], default="key", + help="how to signal activity (default: key — an invisible F15 tap)") + p.add_argument("--no-sleep", action="store_true", + help="also prevent the system and display from sleeping while running") + p.add_argument("-q", "--quiet", action="store_true", help="don't print heartbeat messages") + args = p.parse_args() + + action = tap_f15 if args.method == "key" else nudge_mouse + label = "tapping F15" if args.method == "key" else "nudging mouse 1px" + + if args.no_sleep: + flags = ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED + kernel32.SetThreadExecutionState(flags) + + if not args.quiet: + awake = " (sleep inhibited)" if args.no_sleep else "" + print(f"stay_active: {label} every {args.interval:g}s{awake}. Press Ctrl+C to stop.") + + taps = 0 + try: + while True: + action() + taps += 1 + if not args.quiet: + print(f"\r active — {taps} signal(s) sent, last at {time.strftime('%H:%M:%S')}", + end="", flush=True) + time.sleep(args.interval) + except KeyboardInterrupt: + if not args.quiet: + print("\nstay_active: stopped.") + finally: + if args.no_sleep: + kernel32.SetThreadExecutionState(ES_CONTINUOUS) # release the keep-awake request + + +if __name__ == "__main__": + main()