Why "stay awake" APIs don't keep you green on Teams

2026-11-03来源 dev.to C#English

You get up for a coffee, answer the door or grab something from the printer. Five minutes later Teams has switched you to Away, even though you're back at your desk a minute after that.

The obvious fix is one of the many "keep awake" tools: caffeinate or Amphetamine on a Mac, PowerToys Awake on Windows. They don't help: Teams still goes yellow. So I dug into why, and ended up writing a small app, KeepMeOn, for macOS and Windows. This post is about what I learned: there are two different "idle" concepts in an operating system, and keep-awake tools only touch one of them.

Two kinds of "idle"

1. Power management idle. This decides when the display dims, when the screen saver starts and when the computer goes to sleep. Apps can hold it off with a power assertion:

  • macOS: IOPMAssertionCreateWithName (what caffeinate and most keep-awake apps use) or IOPMAssertionDeclareUserActivity
  • Windows: SetThreadExecutionState(ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED)

2. Input idle time. This is a plain counter: how long since the last mouse, keyboard or trackpad event. Any app can read it:

  • macOS: CGEventSourceSecondsSinceLastEventType
  • Windows: GetLastInputInfo

Teams decides you're Away after roughly five minutes in which that input counter keeps growing. Power assertions don't reset it. They keep the screen on and the machine awake, but as far as the input counter is concerned, nobody has touched the computer.

I measured it rather than trusting the docs. On a Windows 11 machine, calling SetThreadExecutionState did nothing to the input idle time: it kept climbing. On macOS, IOPMAssertionDeclareUserActivity postponed display sleep but left the counter Teams reads untouched.

What the usual workarounds do

  • Keep-awake apps (caffeinate, Amphetamine, PowerToys Awake): they hold a power assertion. Great for long downloads or presentations, but the input counter still grows, so Teams still goes Away.
  • Mouse jigglers: they move the cursor a pixel back and forth. That does reset the counter, but it moves your actual cursor, can wake hover menus and tooltips, and fights with you if you come back mid-jiggle.
  • Pressing a real key (F15, Shift‚Ķ): resets the counter too, but some apps react to it, and holding a modifier at the wrong moment turns your next click into a Shift-click.

What I wanted was an input event that resets the counter while every app ignores it.

macOS: an "unchanged modifiers" event

macOS has an event type for modifier key changes, flagsChanged. It fires when you press or release ⌘, ⌥, ⇧, ⌃ or Caps Lock, and it carries the full set of modifiers that are now down.

If you post a flagsChanged event whose flags are exactly the current flags, nothing changes for any app: no key went down, no key went up, no character was typed. But it's still an input event, so the idle counter resets.

func postNeutralEvent() -> Bool { let current = CGEventSource.flagsState(.combinedSessionState) // Skip it if the user is holding a modifier right now. let held: CGEventFlags = [.maskShift, .maskControl, .maskAlternate, .maskCommand, .maskSecondaryFn] guard CGPreflightPostEventAccess(), current.intersection(held).isEmpty, let event = CGEvent(source: CGEventSource(stateID: .hidSystemState)) else { return false } event.type = .flagsChanged event.flags = current // same modifiers as now: at most Caps Lock event.setIntegerValueField(.eventSourceUserData, value: 0x4B4D_4F4E) // "KMON" tag event.post(tap: .cghidEventTap) return true } 

A few details that matter:

  • It needs the Accessibility permission. Posting events is gated by macOS; CGPreflightPostEventAccess() tells you whether you have it, and CGRequestPostEventAccess() adds the app to the Accessibility list. macOS checks the permission when the app launches, so after granting it you have to relaunch.
  • It never runs while a modifier is held. If you're in the middle of a ‚åò-drag, the app doesn't touch anything.
  • It's tagged. The eventSourceUserData field carries a marker, so anyone watching with an event tap can tell these events apart from real input.

I still declare user activity with IOPMAssertionDeclareUserActivity as well: on its own it doesn't keep you green, but it does stop the display from sleeping, which you want too.

Windows: the key-up of a key that doesn't exist

Windows has a range of virtual key codes that aren't assigned to anything: 0x97 to 0x9F. If you send a key-up for one of them with SendInput, no app reacts, because there's no such key and it was never pressed. For Windows it still counts as input, so GetLastInputInfo resets.

const ushort UnassignedKey = 0x97; var input = new INPUT { type = INPUT_KEYBOARD, u = new InputUnion { ki = new KEYBDINPUT { wVk = UnassignedKey, dwFlags = KEYEVENTF_KEYUP, dwExtraInfo = new IntPtr(0x4B4D4F4E), // "KMON" tag }, }, }; SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT))); 

On Windows, no permission is needed. On a Windows 11 test machine this brought the idle time from 144 s to 0.3 s without moving the cursor.

I also tried a zero-pixel mouse move (MOUSEEVENTF_MOVE with dx = dy = 0). It works too, but I went with the unassigned key-up. One limitation: if an app running as administrator is in the foreground, Windows' UIPI protection can block injected input from a normal-privilege process.

To make sure this keeps working, the GitHub Actions workflow runs a small probe on a real Windows runner. It sends each candidate signal and checks that the idle counter resets and the cursor doesn't move.

Only when you're actually away

Resetting the counter every minute would be easy and wrong: it would hide the fact that you're actually gone for an hour. So the app has a few rules:

  • It only acts after 2 minutes without real input. While you work, it does nothing at all.
  • It checks every 60 seconds. With these defaults, the idle counter peaks at about 3 minutes, comfortably under Teams' 5. (In a 7.5-minute run without input on a Mac, the highest value I saw was 181 s.)
  • It pauses when the screen is locked, when the computer or display is asleep, and optionally outside work hours and during a lunch break.
  • If you're leaving for longer, lock the screen. KeepMeOn stops, and Teams correctly shows you as Away.

That last point is the whole idea: it's for the coffee break, not for pretending to be at your desk all afternoon. If your computer is managed by your company, check that this kind of tool is allowed. KeepMeOn stays visible on purpose: running process, login item and, on a Mac, an entry in the Accessibility list.

The app

KeepMeOn is free and open source (MIT):

  • Mac: native Swift and SwiftUI menu bar app, under 1 MB, macOS 13 or later, signed and notarized.
  • Windows: C# WinForms tray app on .NET Framework 4.8, which ships with Windows 10 and 11. The installer is about 2 MB.
  • No dependencies, no account, no analytics. It only contacts GitHub to check for updates, and you can turn that off.
  • English and Italian.

If you've found a cleaner way to reset the input idle time, or a case where these signals don't work, I'd love to hear about it in the comments.

I used AI coding tools while building the app and writing this post. The measurements and the code are from the project's repository.

原文:https://www.a8q9.cn/en/article/why-stay-awake-apis-don-t-keep-you-green-on-teams-9bd6bd.html

相关文章