Hi @Andreas_Hegenberg (and community),
I'm trying to create a BetterTouchTool “Choose from List” action that dynamically lists subfolders of a specific directory. My use case is to present a dropdown of subfolders (including two levels, excluding hidden folders) under a given path, and then use the selection in a workflow.
Issue:
- The "Choose from List" action has a “Retrieve Content Dynamically via Java Script / JSON” option, but as far as I can tell, it only supports JavaScript (JavaScriptCore) with limited APIs—meaning there is no
require('child_process')
or direct filesystem access, and I can’t run shell code or check the real filesystem (which is what I need).
- I tried pasting bash code that outputs JSON, but there’s no option/tab/selector for a Shell Script in the “Choose from List” configuration.
- Other BTT actions do allow Bash/AppleScript, but not this one? Unless I’m missing a hidden setting.
Desired Feature/Help:
- Can “Choose from List” retrieve its list from the output of a bash or AppleScript (e.g., a shell snippet that echoes JSON)?
- If not currently possible, is there a recommended workaround for presenting a dynamic list built from the filesystem to a BTT user, other than using static JavaScript arrays?
- (For my use case, Node.js features like
require
aren't available in BTT's JavaScript environment, so that's a blocker.)
Example workflow:
- User triggers a BTT action.
- BTT shows a dropdown of subfolders from
/Users/nn/-home-linked
(recursively up to depth 2, skipping hidden dirs, with spaces allowed).
- User selects a subfolder, and the selection can be referenced in a subsequent action.
Is this currently possible?
If not, would you consider adding a Shell Script/AppleScript source option for "Choose from List" items?
Thanks for any advice or info!
Hi @James_Oliver ,
Would something like this work for you?
async function retrieveJSON() {
const rootPath = "/Users/nn/-home-linked";
// === USER CONFIGURABLE PARAMETERS ===
const maxDepth = 2; // e.g., 1, 2, 5 — how deep to go
const includeHidden = false; // include dotfiles? (e.g. .git, .DS_Store)
const showIcons = true; // toggle sfsymbol icons for files/folders
// ====================================
// Construct the tree command
const treeArgs = [];
if (includeHidden) treeArgs.push("-a");
if (maxDepth > 0) treeArgs.push(`-L ${maxDepth}`);
treeArgs.push("-J"); // JSON output
treeArgs.push(`'${rootPath}'`);
const shellCMD = `/opt/homebrew/bin/tree ${treeArgs.join(" ")}`;
const cfg = {
script: shellCMD,
launchPath: "/bin/bash",
parameters: "-c"
};
const rawOutput = await runShellScript(cfg);
const parsed = JSON.parse(rawOutput);
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed[0].contents) {
return JSON.stringify([{ title: "Error: Could not parse tree output" }]);
}
const rootDir = parsed[0];
function escapePath(path) {
return path.replace(/'/g, "'\\''");
}
function buildMenu(items, parentPath) {
return items.map(item => {
const fullPath = `${parentPath}/${item.name}`;
const entry = { title: item.name };
// Add icon if enabled
if (showIcons) {
entry.icon = item.type === "directory"
? "sfsymbol::folder.fill"
: "sfsymbol::document.fill";
}
if (item.type === "directory" && Array.isArray(item.contents)) {
entry.subitems = buildMenu(item.contents, fullPath);
} else if (item.type === "file") {
entry.action = `js::runShellScript({script: \`open '${escapePath(fullPath)}'\`})`;
}
return entry;
});
}
const bttMenu = [
{
title: rootDir.name.split("/").pop() || rootDir.name,
...(showIcons && { icon: "sfsymbol::folder.fill" }),
subitems: buildMenu(rootDir.contents, rootDir.name)
}
];
return JSON.stringify(bttMenu);
}
I forgot to mention that the script requires the tree
command. You can install it using Homebrew by running this command:
brew install tree
Relevant link: tree — Homebrew Formulae
Also note, you need to provide the full path to tree
, i.e. /opt/homebrew/bin/tree
Please try out the script and let me know if it unblocks you or if you need more help.
1 Like
It works @fortred2 , thank you so much!
1 Like