Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a list of recently opened files?

I am currently in the process of building an AppleScript application. (Note that this is a plain AppleScript "application" created by using AppleScript Editor to create an AppleScript and then saving that script as an application). For this app, I need to know the filepaths of the recently opened files for the current user. I've tried many different methods so far, but none seem to be giving me the information that I need.

I've tried the following:

  • I've tried the com.apple.recentitems.plist file, however the filepath information contained inside this file seems to be in Hex format, which when converted into ASCII is full of a lot of mumbo-jumbo, and the format of the mumbo-jumbo seems to change for different files and on different computers.
  • I've tried using the unix find command to filter files opened in the past day, however here I can only choose between recently modified and recently accessed, not recently opened. Recently modified files won't include opened files which aren't modified (like an image that is opened but not edited), and recently accessed files seems to show all files which have been visible in Finder in thumbnail view, even if they haven't been opened by the user.
  • I've tried venturing into Objective-C and the LaunchServices/LSSharedFileList.h file for getting kLSSharedFileListRecentDocumentItems (similar to this answer), however I've never used Objective-C before so I've not been able to get anything to work properly with this.

Any help that anyone could provide to help me to get a list of the recently opened files for the current user would be very much appreciated. Furthermore, any help with being able to write the list of recently changed files to a text file would be great.

like image 202
sebthedev Avatar asked Nov 03 '22 07:11

sebthedev


2 Answers

Your solution probably works, but perhaps what I came up with might be more workable.

Of the proposed 3 options you came up with, indeed the LSSharedFile* commands are the best solution.

With the advent of AppleScript script bundles (.scptd) and scripts that can be saved as application bundles (.app), you can quite easily include custom executables inside the bundle. As a result, you can make use of these executables to accomplish what you need to do if you couldn't accomplish it with AppleScript alone or the default BSD subsystem tools.

So, in Xcode I created a new "Foundation Tool" project which used the following code:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        UInt32 resolveFlags = 0;
        NSArray *arguments = [[NSProcessInfo processInfo] arguments];
        if (arguments.count > 1) {
            NSArray *revisedArguments = [arguments
                        subarrayWithRange:NSMakeRange(1, arguments.count - 1)];
            for (NSString *arg in revisedArguments) {
                if ([arg isEqualToString:@"--noUserInteraction"]) {
                    resolveFlags |= kLSSharedFileListNoUserInteraction;
                } else if ([arg isEqualToString:@"--doNotMountVolumes"]) {
                    resolveFlags |= kLSSharedFileListDoNotMountVolumes;
                }
            }
        }
        LSSharedFileListRef sharedFileListRef = LSSharedFileListCreate(NULL,
                              kLSSharedFileListRecentDocumentItems, NULL);
        NSArray *sharedFileListItemRefs =
           (NSArray *)LSSharedFileListCopySnapshot(sharedFileListRef, NULL);
        for (id itemRef in sharedFileListItemRefs) {
            NSURL *resolvedURL = nil;
            LSSharedFileListItemResolve((LSSharedFileListItemRef)itemRef,
                 resolveFlags, (CFURLRef *)&resolvedURL, NULL);
            if (resolvedURL) {
                printf("%s\n", resolvedURL.path.fileSystemRepresentation);
                [resolvedURL release];
            }
        }
        if (sharedFileListRef) CFRelease(sharedFileListRef);
        [sharedFileListItemRefs release];
        return EXIT_SUCCESS;
    }
}

While this code is similar to yours, instead of having to write the results to an intermediary file, it simply prints the file paths to standard out. This should allow dramatically-simplified coding on the AppleScript end of things. The result of building this Xcode project is a single "Unix Executable File" named recentItemsFinagler instead of an application bundle.

To make use of this built executable in an AppleScript, you first need to make sure the script is saved as a Script Bundle or Application and then the Bundle Contents toolbar item should be enabled like in the image below:

enter image description here

Clicking that toolbar item shows the drawer which shows the contents of the script bundle or application bundle. To add your custom executable, just drag and drop it from the Finder like in the image below:

enter image description here

This will copy it into the script bundle like shown:

enter image description here

To locate this executable at runtime, you can use the path to resource AppleScript command which is part of the StandardAdditions.osax. (Note that depending on how you try to make use of the path to resource command in your script, you may encounter "Resource not found" errors when you run the script from within AppleScript Editor as opposed to running it by double-clicking on it in the Finder. If you encounter this type of an error, first make sure you've saved all of your changes to the script, then quit out of AppleScript Editor, then launch the script application by double-clicking on it in the Finder, after the script finishes running, re-open AppleScript Editor, then re-open the script application, then try running from within AppleScript Editor and see if it works).

So, to make use of this recentItemsFinagler in your AppleScript, you could do something like the following:

property recentItemsFinagler : missing value

if (recentItemsFinagler = missing value) then
    set recentItemsFinagler to (path to resource "recentItemsFinagler")
end if

set toolPath to quoted form of POSIX path of recentItemsFinagler

set recentItemsString to (do shell script toolPath & " --noUserInteraction --doNotMountVolumes")

set recentItemsPaths to paragraphs of recentItemsString

I'll go through this AppleScript line by line to explain what it's doing. We first create a global property recentItemsFinagler, and set its initial value to missing value. (missing value is the AppleScript equivalent to Objective-C's nil).

In case you're not aware of it, global properties in AppleScript are like global variables in other languages with one important addition: when you run the script and assign a value to the property, that value is actually saved in the script file itself, and will persist to the next time you run the script.

The script first checks to see if recentItemsFinagler is equal to missing value, and if it is, sets it to the result of (path to resource "recentItemsFinagler"), which is an AppleScript alias reference. This is similar to an alias in the Finder in that it is able to successfully track changes like renames, and moving from one folder to another. If I had instead stored it as a simple string, and then moved this AppleScript application bundle from my Documents folder to my Desktop, the path would no longer be valid, and the property would have to be updated each time the script was run.

Anyway, we then set recentItemsString to the result of the AppleScript do shell script recentItemsFinagler tool, which is a string. To turn this into an AppleScript list of strings which represent paths, you use the paragraphs of command.

So by including the executable inside the script or AppleScript application's bundle, you can get the desired result in less than 10 lines of code.

Sample project: RecentItemsFinagler.zip

like image 54
NSGod Avatar answered Nov 14 '22 10:11

NSGod


AppleScript

there is no AppleScript API (equivalent to LSSharedFileList) for that afaik .. either you call objC code (see the objC snippet below) or .. you just read the plist which is a bit hackish IMO :)

objective-c

for non-sandboxed apps you use the LSSharedFileList API -- thats the most appropriate way.

@implementation DDAppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    LSSharedFileListRef list = LSSharedFileListCreate(NULL, kLSSharedFileListRecentDocumentItems, 0);
    CFArrayRef items = LSSharedFileListCopySnapshot(list, NULL);

    for(int i=0; i<CFArrayGetCount(items); i++) {
        LSSharedFileListItemRef item = (LSSharedFileListItemRef)CFArrayGetValueAtIndex(items, i);

        //get items properties... for demo I just get name
        NSString *name = (__bridge_transfer NSString*)LSSharedFileListItemCopyDisplayName(item);

        NSLog(@"%@", name);
    }
}

@end

it isnt working in the sandbox though -- and there is no real workaround (well as you said, you could read the plist directly but ... thats a bit hackish :D PLUS apple will likely not let you do that!)

like image 20
Daij-Djan Avatar answered Nov 14 '22 09:11

Daij-Djan