As the title explains, is it possible to show the content of a specific folder in a custom menu? I want to create a custom app launcher that lists all apps with a specific tag. Thanks!
You can do that, it requires some scripting.
Here is an example script that you can modify. In this example it lists all files with tags Red or Green from ~/Documents.
async function retrieveJSON() {
// define your folder an tags here:
let folder = "~/Documents";
let tags = ["Red", "Green"];
// Join tags to form the search query, each tag must be checked separately with an OR condition
let tagQuery = tags.map(tag => `kMDItemUserTags == '${tag}'`).join(" || ");
// Construct the shell script with the folder and tags
let script = `mdfind -onlyin ${folder} "${tagQuery}"`;
let filePaths = await runShellScript({
script: script,
});
// Split the file paths by new line
let fileArray = filePaths.split("\n").filter(Boolean); // filter(Boolean) removes any empty lines
// Create an array of objects with filePath and fileName
let files = fileArray.map((filePath) => {
let fileName = filePath.split("/").pop(); // Extract the file name from the path
return { filePath, fileName };
});
let menuItems = [];
// Create the context menu items that trigger a shell script to open the file
for (let file of files) {
let item = {
title: file.fileName,
action: `js::(async () => {runShellScript({script: 'open "${file.filePath}"'})})()`,
};
menuItems.push(item);
}
return JSON.stringify(menuItems);
}
You need to use this script within the "Custom Context Menu (New) action:
And here is a slightly improved version of the script that also shows the app icon when the file is an app. This requires the latest BTT alpha 4.717 though - with previous versions the icons would be huge.
async function retrieveJSON() {
let folder = "/Applications";
let tags = ["Red", "Green"];
// Join tags to form the search query, each tag must be checked separately with an OR condition
let tagQuery = tags.map(tag => `kMDItemUserTags == '${tag}'`).join(" || ");
// Construct the shell script with the folder and tags
let script = `mdfind -onlyin ${folder} "${tagQuery}"`;
let filePaths = await runShellScript({
script: script,
});
// Split the file paths by new line
let fileArray = filePaths.split("\n").filter(Boolean); // filter(Boolean) removes any empty lines
// Create an array of objects with filePath and fileName
let files = fileArray.map((filePath) => {
let fileName = filePath.split("/").pop(); // Extract the file name from the path
return { filePath, fileName };
});
let menuItems = [];
// Function to retrieve the icon path by reading the Info.plist
async function getAppIconPath(filePath) {
// Construct the path to the Info.plist file
let plistPath = `${filePath}/Contents/Info.plist`;
// Run a shell script to retrieve the CFBundleIconFile from the Info.plist
let iconName = await runShellScript({
script: `defaults read "${plistPath}" CFBundleIconFile || echo "AppIcon"`,
});
// Add the .icns extension if not present
iconName = iconName.trim();
if (!iconName.endsWith(".icns")) {
iconName += ".icns";
}
// Return the full path to the .icns file
return `${filePath}/Contents/Resources/${iconName}`;
}
// Create the context menu items that trigger a shell script to open the file
for (let file of files) {
let iconPath = await getAppIconPath(file.filePath); // Get the correct icon path from Info.plist
let item = {
title: file.fileName,
action: `js::(async () => {runShellScript({script: 'open "${file.filePath}"'})})()`,
icon: `path::${iconPath}::width@@30`
};
menuItems.push(item);
}
return JSON.stringify(menuItems);
}
Instead of a context menu, you can also use this script with the "Choose From List" action:
wow, works great. thanks!!
Hi Andreas,
Thanks for showing this inspiring application of BTT.
What I want to achieve: when I click on a menu item in the floating menu, it show the list of files in a default folder, and I can open the file, with its default app, by clicking on the filename on the list.
I tried to start the exploration by adding the action "Show Custom Context Menu(NEW)" to a menu item (with your script) in a floating menu and hope that I will be able to see the list of files to begin with.
When I tested the menu by using "Run script", I only get a blank rectangle.
Thank you in advance.
@ngan this script will only list files that have the tag "Red" or "Green". It sounds like you'd rather have just all files regardless of the tag, correct?
Yes, I would like to display all files (no sub folder) in the folder.
Thanks for reminding that there is a filter for tag in the script!
I can live with the existing script, the workaround is to assign a dummy tag to the files that I want to show in the list.
I just tested the workaround, and it works beautifully!
Thanks again.
You should probably remove the icon retrieval because that only makes sense for apps and slows things down quite a bit:
async function retrieveJSON() {
let folder = "~/Documents";
// Construct the shell script to search for all files in the folder
let script = `mdfind -onlyin ${folder} "*"`;
let filePaths = await runShellScript({
script: script,
});
// Split the file paths by new line
let fileArray = filePaths.split("\n").filter(Boolean); // filter(Boolean) removes any empty lines
// Create an array of objects with filePath and fileName
let files = fileArray.map((filePath) => {
let fileName = filePath.split("/").pop(); // Extract the file name from the path
return { filePath, fileName };
});
let menuItems = [];
// Create the context menu items that trigger a shell script to open the file
for (let file of files) {
let item = {
title: file.fileName,
action: `js::(async () => {runShellScript({script: 'open "${file.filePath}"'})})()`,
icon: `sfsymbol::file`
};
menuItems.push(item);
}
return JSON.stringify(menuItems);
}
Thankyou again.
For the search criteria, can it only search files that is not in sub-folders? The current seach script lists all files, including the files in all sub-folders.
let script = mdfind -onlyin ${folder} "*"
;
Yes you can set the depth:
mdfind -onlyin ${folder} -depth 1 "*"
Thanks for the help.
It seems depth is not supported by mdfind. I use the -depth 1 opion and get the error code instead of the list of file. It's ok, I am happy with the workaround by using a dummy tag.
Ah you are right, sorry I remembered that incorrectly.
You could however switch to a different command like ls for simple lists of files:
async function retrieveJSON() {
let folder = "/Users/andreas/Documents";
// Construct the shell script to recursively list all files using ls
let script = `ls "${folder}"`;
let filePaths = await runShellScript({
script: script,
});
// Split the file paths by new line
let fileArray = filePaths.split("\n").filter(Boolean); // filter(Boolean) removes any empty lines
// Create an array of objects with filePath and fileName
let files = fileArray.map((filePath) => {
let fileName = filePath.split("/").pop(); // Extract the file name from the path
return { filePath, fileName };
});
let menuItems = [];
// Create the context menu items that trigger a shell script to open the file
for (let file of files) {
let item = {
title: file.fileName,
action: `js::(async () => {runShellScript({script: 'open "${folder + '/' + file.filePath}"'})})()`,
icon: `sfsymbol::doc.circle`
};
menuItems.push(item);
}
return JSON.stringify(menuItems);
}
(by the way, one of the next versions will also allow to use these scripts in floating menus to dynamically retrieve their items)
This script still shows all files including those in subfolders.
This is not urgent. The workaround of attacing a dummy tag is workable for me for now. I look forward to seeing the next versions with such build in function.
Thanks for developing this wonderful utilities app.
the ls command only lists the files from the given folder by default, are you sure the script has updated correctly? Possibly restart BTT
Thanks again! Your script works and I misunderstood what I saw.
Upon rechecking, I found that all "extra" files in the list have "~$" as prefix. They probably are previously deleted excel files and I mistook them as files in subfolders.
After I used "Command + Shift + ." to unhide and delete those files, the list is now showing only the name of subfolders and the files in the main folder.
Further googling suggests that Microsoft apps leave hidden files with "~$" in MacOS as prefix after they are deleted or for some unknown reasons. One being mentioned " ~$
files generated by Microsoft Office (i.e., Word, Excel, etc.) are usually erased after the file has been closed. In some cases, like when files are saved on a cloud/online disk, the latter can "throw back" to the local disk those files, hence the question."
You can probably filter out such files using something like
ls | grep -v '^~\$'