I’m afraid I don’t understand what, exactly, you are after.
I won’t argue about the simplicity, but the speed drag is down to using an external osax to do regex instead of using native macOS APIs. You should find this faster and more reliable than calling on System Events:
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString
property NSBackwardsSearch : a reference to 4
set foregroundApps to {}
set appText to (do shell script "lsappinfo find applicationtype=\"Foreground\"")
set appString to NSString's stringWithString:appText
set appList to appString's componentsSeparatedByString:"ASN:"
repeat with anApp in appList
set aRange to (anApp's rangeOfString:"\"")
set bRange to (anApp's rangeOfString:"\"" options:NSBackwardsSearch)
try
set appName to (anApp's substringWithRange:{(aRange's location) + (aRange's |length|), (((bRange's location) - 1) - (aRange's location))})
set end of foregroundApps to appName as text
end try
end repeat
foregroundApps
Shane, I’m still after the same thing I stated in the title of this topic:
How Do I Get Recent Files of All Apps?
But it’s not worth anyone spending a lot of time on. If the answer/solution doesn’t jump out at you, then just let me know, and I’ll move on.
That’s the case, I’m afraid.
On my 10.13.4, it looks pretty much like Recent Documents are tracked using com.apple.LSSharedFileList.RecentDocuments.sfl.
It’s a binary plist, but seems perfectly readable with plutil
.
com.apple.LSSharedFileList.ApplicationRecentDocuments
also still appears to be a thing and in use here.
Phil, how are you using plutil
?
All I can get is hex or combo hex and text.
I tried the -p
option, but got same.
EDIT
Sorry, I just saw the end of the list.
There is a lot readable, but it is VERY verbose.
How would you extract the file name, full path, and mod date from this?
It looks like I was getting methods confused, and that that file is still used.
It’s readable, but not particularly intelligible. If you look at it, you can see it’s been produced by NSKeyedArchiver
, not NSPropertyListSerialization
. And if you try to unarchive it, you get an error:
-[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (SFLListItem) for key (NS.objects); the class may be defined in source code or a library that is not linked
In other words, an instance of a custom class (SFLListItem
) has been serialized (presumably securely) and written to disk. Even if you knew how that class was implemented, picking out the pieces would be quite the challenge. The original script just plucked out all the URLs; I suspect anything more is going to be very tricky.
Sorry, on further investigation I think you may be on a hiding to nothing here.
First, those .sfl
files don’t store dates, so I’m afraid you’re out of luck on that one. And as Shane says, parsing the data out of a custom class in any reasonable way with a script is going to be tricky (and fragile).
Second, looking at what’s in my folder, it doesn’t look like all applications are actually recording recent items.
About the best I can offer is a list of recent filenames (no paths, no parent apps), using strings
and grep
with something like this:
strings -a ~/Library/Application\ Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl2 | grep -B1 'file:///' | grep -v 'file:///'
(that’s a shell command, so wrap it in a do shell script
and escape appropriately if you’re going to run it as an AppleScript)
but you may be able to do better yourself with your regex skills.
Addendum: you already indicated this wasn’t something you wanted to invest much time with, but I’ll just add for the sake of completion that you may be able to parse this info out of fsevents. I have an interest in macos forensics and have played with this a couple of times before, but you’ll need to roll your sleeves up
Hi.
Based largely on the information in this thread so far, here’s a handler to get the URLs of the currently open apps’ recent files. It returns an array of dictionaries, each dictionary representing an app and containing the app’s bundle ID and an array of file info dictionaries. Each of these info dictionaries contains a file URL and a boolean indicating whether or not the bookmark from which it was derived is “stale” — whatever that means. The hope is that the information you need can be gleened from the URLs’ resource values.
On my High Sierra machine, every SFL file dated since 15th November 2017 has an “.sfl2” extension, which seems to indicate a change of format around then. The script here works on both my 10.13.4 and 10.11.6 machines.
use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"
use scripting additions
(*
Return an array of dictionaries containing URLs for the recent files of all open foreground applications which have them.
Each dictionary represents an application and is in the form:
{docURLs:{{|url|:<file URL>, staleBookmark:<boolean>}, …}, appBundleID:<string>}
*)
on getRecentFileURLsForOpenForegroundApps()
-- Get the bundle IDs of the currently open foreground applications.
set foregroundAppIDs to paragraphs of (do shell script "lsappinfo list | sed -En '
/^[[:space:]]+ bundleID=\"/ h
/type=\"Foreground\"/ {
g
s/^[^\"]+\"|\"[^\"]*$//g
p
}'")
set |⌘| to current application
-- Get the POSIX paths of all visible items in ~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/.
set fileManager to |⌘|'s class "NSFileManager"'s defaultManager()
set applicationRecentDocsFolderURL to |⌘|'s class "NSURL"'s fileURLWithPath:(POSIX path of (path to application support from user domain) & "com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments")
set applicationRecentDocsSFLPaths to (fileManager's contentsOfDirectoryAtURL:(applicationRecentDocsFolderURL) includingPropertiesForKeys:({}) options:(|⌘|'s NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value))'s valueForKey:("path")
-- Filter for paths with ".sfl" extensions and names corresponding to the bundle IDs of the currently open foreground applications.
-- The IDs in the paths are lower cased, whereas the bundle IDs proper may be mixed cased.
set filteredSFLPaths to applicationRecentDocsSFLPaths's mutableCopy() -- This is filtered below.
set foregroundAppIDs to |⌘|'s class "NSArray"'s arrayWithArray:(foregroundAppIDs)
set foregroundAppSLFsOnlyFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("(pathExtension BEGINSWITH[c] 'sfl') AND (lastPathComponent.stringByDeletingPathExtension IN %@)") argumentArray:({foregroundAppIDs's valueForKey:("lowercaseString")})
tell filteredSFLPaths to filterUsingPredicate:(foregroundAppSLFsOnlyFilter)
-- In the folder on my 10.13.4 system, all the SFL files dated on or after 15th November 2017 have ".sfl2" extensions. Possibly a new format, since the "items.Bookmark" key path below doesn't work with the ".sfl" files on my 10.11.6 machine. Here, filter out any obsolete path matches by working backwards through a reversed lexical sort of the matches and, where a later path is followed by an earlier version, removing the latter from the array.
set reverseSortDescriptor to |⌘|'s class "NSSortDescriptor"'s sortDescriptorWithKey:("self") ascending:(false) selector:("caseInsensitiveCompare:")
tell filteredSFLPaths to sortUsingDescriptors:({reverseSortDescriptor})
repeat with i from (count filteredSFLPaths) - 1 to 1 by -1
set thisSFLPath to (item i of filteredSFLPaths)
if not ((thisSFLPath's hasSuffix:(".sfl")) as boolean) then
set originalPath to (thisSFLPath's stringByDeletingPathExtension()'s stringByAppendingPathExtension:("sfl"))
if (((item (i + 1) of filteredSFLPaths)'s hasPrefix:(originalPath)) as boolean) then tell filteredSFLPaths to removeObjectAtIndex:(i)
end if
end repeat
-- Build an array of dictionaries, each dictionary containing an app bundle ID and an array of associated recent file URL info dictionaries.
set outputArray to |⌘|'s class "NSMutableArray"'s new()
repeat with thisSFLPath in filteredSFLPaths
-- Unarchive the data from this SFL file and extract any bookmark data values.
set unarchivedSFLData to (|⌘|'s class "NSKeyedUnarchiver"'s unarchiveObjectWithFile:(thisSFLPath))
if ((thisSFLPath's hasSuffix:(".sfl")) as boolean) then -- Assuming this to be the criterion for the different case in the keypath.
set bookmarkDataValues to (unarchivedSFLData's valueForKeyPath:("items.bookmark"))
else
set bookmarkDataValues to (unarchivedSFLData's valueForKeyPath:("items.Bookmark"))
end if
if ((count bookmarkDataValues) > 0) then
-- If bookmark data values are found, create an array containing URL/staleBookmark dictionaries for each.
set URLInfoArray to |⌘|'s class "NSMutableArray"'s new()
repeat with theseBookmarkData in bookmarkDataValues
set URLInfoDictionary to (|⌘|'s class "NSDictionary"'s dictionaryWithObjects:(|⌘|'s class "NSURL"'s URLByResolvingBookmarkData:(theseBookmarkData) options:(0) relativeToURL:(missing value) bookmarkDataIsStale:(reference) |error|:(missing value)) forKeys:{"URL", "staleBookmark"})
tell URLInfoArray to addObject:(URLInfoDictionary)
end repeat
-- Get the original, possibly mixed-case bundle ID for the app associated with this SFL file.
set IDMatchFilter to (|⌘|'s class "NSPredicate"'s predicateWithFormat:("self ==[c] %@") argumentArray:({thisSFLPath's lastPathComponent()'s stringByDeletingPathExtension()}))
set appBundleID to (foregroundAppIDs's filteredArrayUsingPredicate:(IDMatchFilter))'s firstObject()
-- Create a dictionary containing the bundle ID and URL info dictionary array for this app and add it to the output array.
set appIDAndDocURLInfo to (|⌘|'s class "NSDictionary"'s dictionaryWithObjects:({appBundleID, URLInfoArray}) forKeys:({"appBundleID", "RecentDocURLInfo"}))
tell outputArray to addObject:(appIDAndDocURLInfo)
end if
end repeat
return outputArray
end getRecentFileURLsForOpenForegroundApps
getRecentFileURLsForOpenForegroundApps()
Nigel,
Many thanks for taking this to the next level.
Unfortunately, it does not seem to capture recent files of apps that don’t play according to Apple’s rules.
I’m running macOS 10.12.6.
I have 16 apps open, but the script returns files for only 4:
com.latenightsw.ScriptDebugger7
com.cocoatech.PathFinder
com.apple.TextEdit
com.apple.ScriptEditor2
Apps Open
com.cocoatech.PathFinder
com.smileonmymac.textexpander
jp.co.pfu.ScanSnap.V10L10
com.apple.finder
org.mozilla.firefox
com.microsoft.Outlook
com.evernote.Evernote
com.stairways.keyboardmaestro.editor
at.obdev.LaunchBar
com.google.Chrome
com.apple.ScriptEditor2
com.apple.iChat
com.latenightsw.ScriptDebugger7
com.microsoft.Word
com.microsoft.Excel
com.apple.TextEdit
While Word and Excel maintain an extensive recent files list, they seem to do so using a different method.
This was a really good try, and I really appreciate your efforts.
BTW, this script will also return the bundle IDs of open apps:
tell application "System Events"
set appIdList to bundle identifier of every application process whose background only is false
end tell
but I suspect you already know that. Just thought I’d post for the benefit of others.
Pity — although in fact most of the other twelve in your list either don’t have or probably don’t have “Open Recent” submenus in their “File” menus. I can confirm though that the URLs returned for TextWrangler are all NSNulls. The data values are there in the SFL2 file, but are obviously not straight bookmark data. TextWrangler’s/BBEdit’s “Open Recent” menu has a non-standard format.
This is actually safer than the lsappinfo/sed shell script I used, in which the sed code depends on the bundleID line coming before the type line in the lsappinfo result!
We can avoid that with a bit of Foundation
help:
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString
property NSCharacterSet : a reference to current application's NSCharacterSet
set foregroundBundleIDs to {}
set appText to NSString's stringWithString:(do shell script "lsappinfo list")
set appEntries to appText's componentsSeparatedByString:(return & return)
repeat with entry in appEntries
set entryTxt to entry as text
if entryTxt contains "Foreground" then
set theLoc to (entry's rangeOfString:"bundleID=\"")
set theLength to (entry's rangeOfString:"bundle path=")
set theRange to current application's NSMakeRange((theLoc's location) + (theLoc's |length|), (theLength's location) - ((theLoc's location) + (theLoc's |length|)))
set bundleID to (entry's substringWithRange:theRange)
set bundleID to (bundleID's stringByTrimmingCharactersInSet:(NSCharacterSet's whitespaceAndNewlineCharacterSet()))
set bundleID to (bundleID's stringByTrimmingCharactersInSet:(NSCharacterSet's punctuationCharacterSet()))
set end of foregroundBundleIDs to bundleID as text
end if
end repeat
foregroundBundleIDs
And with AppKit
's help we can simplify it a bit, and speed it up more than somewhat:
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions
set theApps to current application's NSWorkspace's sharedWorkspace()'s runningApplications()
set thePred to current application's NSPredicate's predicateWithFormat:"activationPolicy == %@" argumentArray:{0}
set foregroundBundleIDs to ((theApps's filteredArrayUsingPredicate:thePred)'s valueForKey:"bundleIdentifier") as list
Thanks. I was thinking about NSWorkspace’s runningApplications but wasn’t aware of
NSApplicationActivationPolicy.
Extending that slightly, and a bit off topic, but we can produce a potentially useful script that separates running bundle IDs according to background, menubar or foreground that belong to 3rd party processes (this has already helped me troubleshoot a problem for someone else):
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
set nonAppleForeground to {}
set nonAppleMenubar to {}
set nonAppleBackground to {}
on excludeAppleIDs:ids
set returnList to {}
repeat with ident in ids
if ident does not contain "com.apple" and ident does not contain missing value then
set end of returnList to ident as text
end if
end repeat
return returnList
end excludeAppleIDs:
set theApps to current application's NSWorkspace's sharedWorkspace()'s runningApplications()
repeat with i from 0 to 2
set thePred to (current application's NSPredicate's predicateWithFormat:"activationPolicy == %@" argumentArray:{i})
if i is equal to 0 then
set nonAppleForeground to (my excludeAppleIDs:(((theApps's filteredArrayUsingPredicate:thePred)'s valueForKey:"bundleIdentifier") as list))
else if i is equal to 1 then
set nonAppleMenubar to (my excludeAppleIDs:(((theApps's filteredArrayUsingPredicate:thePred)'s valueForKey:"bundleIdentifier") as list))
else
set nonAppleBackground to (my excludeAppleIDs:(((theApps's filteredArrayUsingPredicate:thePred)'s valueForKey:"bundleIdentifier") as list))
end if
end repeat
You can do that in the predicate. You can also use it to skip processes that don’t have a bundle ID:
set thePred to (current application's NSPredicate's predicateWithFormat:"activationPolicy == %@ AND bundleIdentifier != %@ AND NOT bundleIdentifier BEGINSWITH %@" argumentArray:{i, missing value, "com.apple"})
And for a bit more code parsimony, you could set the result variables by list:
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
set theApps to current application's NSWorkspace's sharedWorkspace()'s runningApplications()
set theBundleIDs to current application's NSMutableArray's new()
repeat with i from 0 to 2
set thePred to (current application's NSPredicate's predicateWithFormat:"activationPolicy == %@ AND NOT (bundleIdentifier == NIL OR bundleIdentifier BEGINSWITH 'com.apple')" argumentArray:{i})
tell theBundleIDs to addObject:((theApps's filteredArrayUsingPredicate:thePred)'s valueForKey:"bundleIdentifier")
end repeat
set {nonAppleForeground, nonAppleMenubar, nonAppleBackground} to theBundleIDs as list
Hi Shane,
Is it possible to set the predicate to filter apps that are scriptable?
For now, I’m using this (improved by your activationPolicy
snippet):
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions
set theApps to current application's NSWorkspace's sharedWorkspace()'s runningApplications()
set thePred to current application's NSPredicate's predicateWithFormat:"activationPolicy == 0"
set appsURL to ((theApps's filteredArrayUsingPredicate:thePred)'s valueForKey:"bundleURL")
set scriptableApps to current application's NSMutableArray's new()
repeat with anURL in appsURL
set {hasResult, theResult} to (anURL's getResourceValue:(reference) forKey:(current application's NSURLApplicationIsScriptableKey) |error|:(missing value))
if theResult as boolean then
set aBundle to ((current application's NSBundle's bundleWithURL:anURL)'s bundleIdentifier())
(scriptableApps's addObject:aBundle)
end if
end repeat
scriptableApps as list
Hey, that’s clever! I’d never have thought of using NSMutableArray and lists together like that.
Thanks Nigel!
I think that’s as good as it gets.
Very cool script, Nigel!
Plus, I learned a new word: parsimony
Much nicer, elegant way of saying someone/something is “cheap” or “stingy”.
So now, instead of saying “compact script”, we can say “parsimonious script” LOL
Why use a 7-letter word, when we can us a 12-letter word? LOL
But thanks! I do like learning a new word every day.