Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MacOS - Get running apps ordered by most recently used first

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?

like image 797
Anshuman Sharma Avatar asked Oct 26 '17 06:10

Anshuman Sharma


2 Answers

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.

like image 59
brimstone Avatar answered Nov 17 '22 16:11

brimstone


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];
like image 25
Ivan Klymchuk Avatar answered Nov 17 '22 14:11

Ivan Klymchuk