8

Using Swift (or objective-C) I want to get the list of apps currently running on the macOS, in order of recent usage. That would be the order in which cmd-tab shows app icons on the Mac.

The following gives me the apps, but not in the order I want.

let apps = NSWorkspace.shared.runningApplications

In the documentation for the .runningApps property, it says:

The order of the array is unspecified, but it is stable, meaning that the relative order of particular apps will not change across multiple calls to runningApplications.

Is there a way to sort/get the apps in the desired order?

Edit:

The approach suggested by in the answer by 'brimstone' seems promising, but CGWindowListCopyWindowInfo returns windows in the front-to-back order only when CGWindowListOption.optionOnScreenOnly is specified. In that case, however, only windows in the current space are returned. cmd-tab however is able to list apps across all spaces.

Does anyone know of any other way? Shouldn't there be a more direct/easier way to do this?

Himanshu P
  • 9,586
  • 6
  • 37
  • 46
Anshuman Sharma
  • 417
  • 2
  • 11

2 Answers2

2

So I was looking at the cmd-tab, and I thought one way to mimic that behavior would be through window hierarchy. CGWindowListOption will return a list of windows on the screen in order of hierarchy - therefore the most recent applications will be first. This would solve your problem of ordering your runningApps property.

let options = CGWindowListOption(arrayLiteral: CGWindowListOption.excludeDesktopElements, CGWindowListOption.optionOnScreenOnly)
let windowList = CGWindowListCopyWindowInfo(options, CGWindowID(0))
let windows = windowList as NSArray? as! [[String: AnyObject]]

Then you can loop through the infoList and retrieve any data you want, for example the names of every application.

for window in windows {
    let name = window[kCGWindowOwnerName as String]!
    print(name)
}

If you still want the NSRunningApplication variable though, you can match the window's Owner PID to the application's PID.

let id = pid_t(window[kCGWindowOwnerPID as String]! as! Int)
let app = apps.filter { $0.processIdentifier == id } .first
app.hide() //Or whatever your desired action is

For me, this returned all the applications I had running in the same order that CMD-Tab showed. However, this method also returned some processes that were items in the menu bar or background processes, such as SystemUIServer and Spotlight.

brimstone
  • 3,370
  • 3
  • 28
  • 49
1

You can subscribe to notification NSWorkspaceDidDeactivateApplicationNotification from [NSWorkspace sharedWorkspace].notificationCenter, and put to dictionary bundleId and timestamp, then use it to sort your array. Disadvantage is app doesn't know anything about apps used before it started to listen notifications.

@property (strong) NSMutableDictionary *appActivity;

...

- (instancetype)init {
    if (self = [super init]) {
        self.appActivity = [NSMutableDictionary dictionary];
        [self setupNotifications];
    }
    return self;
}

- (void)setupNotifications {
    NSNotificationCenter *workspaceNotificationCenter = [NSWorkspace sharedWorkspace].notificationCenter;
    [workspaceNotificationCenter addObserver:self selector:@selector(deactivateApp:) name:NSWorkspaceDidDeactivateApplicationNotification object:nil];
}

- (void)deactivateApp:(NSNotification *)notification {
    NSRunningApplication *app = notification.userInfo[NSWorkspaceApplicationKey];
    if (app.bundleIdentifier) {
        self.appActivity[app.bundleIdentifier] = @([[NSDate date] timeIntervalSince1970]);
    }
}

...

    NSArray<NSRunningApplication *> *apps = [[NSWorkspace sharedWorkspace] runningApplications];

    NSComparator sortByLastAccess = ^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
        NSRunningApplication *item1 = obj1;
        NSRunningApplication *item2 = obj2;
        NSComparisonResult result = NSOrderedSame;
        if (item1.bundleIdentifier && item2.bundleIdentifier) {
            NSNumber *ts1 = self.appActivity[item1.bundleIdentifier];
            NSNumber *ts2 = self.appActivity[item2.bundleIdentifier];
            if (ts1 && ts2) {
                result = [ts2 compare:ts1];
            } else if (ts1) {
                result = NSOrderedAscending;
            } else if (ts2) {
                result = NSOrderedDescending;
            }
        }
        return result;
    };

    apps = [apps sortedArrayUsingComparator:sortByLastAccess];