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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -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.
|
||||||
Executable
+113
@@ -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()
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user