From bb6fd7b1b99b3ee2e8552966bd78d04e16e005d8 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Fri, 1 May 2026 21:53:56 +0200 Subject: [PATCH] Feature: Add user idle and screen lock detection for macOS --- macos/main.mm | 106 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/macos/main.mm b/macos/main.mm index c928dc6..517800f 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -141,9 +141,113 @@ static std::string getTimeString() return std::string([dateString UTF8String]); } -// Get active application name +// Get user idle time in seconds (time since last keyboard/mouse input) +static double getUserIdleTime() +{ + // CGEventSourceSecondsSinceLastEventType returns seconds since last event + // kCGEventSourceStateCombinedSessionState includes all input sources + CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType( + kCGEventSourceStateCombinedSessionState, + kCGAnyInputEventType + ); + // Defensive: ensure non-negative (edge case protection) + return idleTime > 0 ? idleTime : 0; +} + +// Check if screen is locked +static bool isScreenLocked() +{ + // Method 1: Check CGSession dictionary for screen lock status + CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary(); + if (sessionDict) { + // Check for "CGSSessionScreenIsLocked" key + CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue( + sessionDict, CFSTR("CGSSessionScreenIsLocked")); + if (screenLocked && CFBooleanGetValue(screenLocked)) { + CFRelease(sessionDict); + return true; + } + CFRelease(sessionDict); + } + + // Method 2: Check if loginwindow is frontmost (screen saver / lock screen) + NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + if (frontApp) { + NSString* bundleId = [frontApp bundleIdentifier]; + if ([bundleId isEqualToString:@"com.apple.loginwindow"] || + [bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) { + return true; + } + } + + return false; +} + +// Format time as HH:MM:SS with prefix +static std::string formatStatusTime(const char* prefix, double seconds) +{ + int totalSecs = (int)seconds; + int hours = totalSecs / 3600; + int mins = (totalSecs % 3600) / 60; + int secs = totalSecs % 60; + + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs); + return std::string(buffer); +} + +// Get active application name or idle/locked status (works for background processes) static std::string getActiveApp() { + double idleTime = getUserIdleTime(); + + // Check if screen is locked first + if (isScreenLocked()) { + return formatStatusTime("Locked", idleTime); + } + + // Check user idle time (matches Windows/Linux: 6 seconds threshold) + // If idle for more than 6 seconds, report inactive status + if (idleTime >= 6.0) { + return formatStatusTime("Inactive", idleTime); + } + + // Use CGWindowListCopyWindowInfo to get the frontmost window + // This works reliably even when running as a background/nohup process + CFArrayRef windowList = CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, + kCGNullWindowID + ); + + if (windowList) { + CFIndex count = CFArrayGetCount(windowList); + for (CFIndex i = 0; i < count; i++) { + CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i); + + // Get window layer - layer 0 is normal windows + CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer); + int layer = 0; + if (layerRef) { + CFNumberGetValue(layerRef, kCFNumberIntType, &layer); + } + + // Skip non-normal windows (menu bar, dock, etc.) + if (layer != 0) continue; + + // Get owner name (application name) + CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName); + if (ownerName) { + char buffer[256] = {}; + if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) { + CFRelease(windowList); + return std::string(buffer); + } + } + } + CFRelease(windowList); + } + + // Fallback to NSWorkspace (may not work for background processes) NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; if (app) { NSString* name = [app localizedName];