Dynamic "Choose from List" via Shell Script (Bash/AppleScript) for folder dropdown?

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

:information_source: 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