Tutorial: AI and App Launcher using the new scriptable Choose From List

AI & App Launcher Tutorial

Note: This requires at least BetterTouchTool v4.788

Download the final result of this tutorial here (show the launcher via fn+d):
LauncherTutorial.bttpreset (21.5 KB)


I'll be creating some basic tutorials for some recent BTT features. This one is about creating a simple AI launcher (that can also launch apps). It just serves as an example what can be achieved with these new abilities:

Instead of AI stuff or app launching you can of course modify this to trigger any other action in BTT and in the future you'll also be able to display floating menus within the "Choose From List / Prompt"

This is what the result of this tutorial will look like:

Step 1: Add a Keyboard Shortcut to show the launcher

Of course you could also use any other trigger in BTT, but for this tutorial we'll use a keyboard shortcut. In this example I choose fn+D.

Make sure to assign the predefined action "Show / Choose From List" to the keyboard shortcut.

If you now press fn+D you should see an empty launcher:
image

Step 2: Add some content to the launcher.

Add an action to summarize the currently selected text

Click the "Configure List Items" button to open the UI for adding items to the list, it will look like this:

Add a first item. We want the first item to be a AI function that summarizes the currently selected text.

Configure name & icon:

Configure the summarize action:
BTT includes a few AI actions, the one we'll be using here is called Transform & Replace Selection With ChatGPT.
BTT comes with a base contingent of free calls to the OpenAI API, however I recommend to provide your own API key, then you are also free to choose the model you want to use. For this tutorial we'll use the free contingent.

Add an action that uses the input from the launcher and (optionally) the selected text:

We want to use the text typed into the launcher as a prompt for ChatGPT. However by default the list is just filtered when typing something. To prevent that change the "When Entering Text" setting on the "Show / Choose From List" action to "Filter & Save Input To Variable BTTPromptInput".
This will make sure the text you type is saved to a variable that can be used later. However the list will still be filtered, so we won't be able to select an action that passes the input to ChatGPT.

To resolve this, we create a new list item and activate the "Always Show, Ignore Filter" option. By doing this, the item will always be shown even if it doesn't match the input / search.

Now to see whether the variable is correctly populated let's add a "Show HUD" action that shows the prompt. The Show HUD action allows to display content of variables by wrapping them in curly braces:

When testing this now, it will show a HUD with the text you entered:


Now let's add an action that forwards the input to ChatGPT. This time we'll use the "Use ChatGPT (Optionally on Selected Text). Copy Result To Clipboard / Variable". We can use the BTTPromptInput variable directly in the user prompt for ChatGPT. Also we'll put the response into the clipboard and into a custom variable "BTTChatGPTResponse".
This variable we can show in a HUD afterwards (or use it in any other way you can think of)


So when you use it now, it will look like this:

Now the AI part of this tutorial is pretty much finished. You can add more AI actions with different prompts and different configurations to meet your needs. This can get arbitrarily powerful & complex.

Step 3: Make it an app launcher as well

Now to demonstrate the new "Simple JSON Format", we'll also make this an app launcher. However we don't want to add the apps manually to the list of items. Instead we want to retrieve them dynamically.

Turn On Dynamic Content Retrieval

To achieve this enable the "Retrieve Content Dynamically via Java Script / JSON" option on the "Show / Choose From List" action:

Now this Java Script can use any functions described here: Using Java Script (not JXA) · GitBook to create an output JSON based on the Simple JSON Format. This is really powerful. We could run some terminal commands to retrieve a list of all apps and their icons whenever we open the list.

However retrieving all apps dynamically every time would be pretty slow. Instead we will create a cache file when BTT launches and just load that when the launcher is shown. So let's define a JSON file to read from:

async function retrieveJSON() {
 	return readFile("~/Library/Application Support/BetterTouchTool/app-cache.json");
}

That file does not exist yet, so nothing will happen at this point. Let's change that.

Load Apps Into Cache

A good place to load your System's apps into the cache and save them to the ~/Library/Application Support/BetterTouchTool/app-cache.json file would be the "After BetterTouchTool Did Launch" trigger in "Automations, Named & Other Triggers". Using this we can update the app cache once everytime BTT starts.

We need to create a script that retrieves the apps. I'm using this one, which makes use of the macOS "mdfind" command line utility:

async function createAppCache() {


  let filePaths = await runShellScript({
    // this will search the typical app folders
    script: `mdfind -onlyin ~/Applications -onlyin /Applications -onlyin /System/Applications 'kMDItemFSName == "*.app"'`,
  });

  // 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 list menu items that trigger a shell script to open the app
  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);
  }

  // add finder manually because it's in an unusal location
  menuItems.push({
    title: "Finder",
    action: `js::(async () => {runShellScript({script: 'open "/System/Library/CoreServices/Finder.app"'})})()`,
    icon: `path::/System/Library/CoreServices/Finder.app/Contents/Resources/Finder.icns::width@@30`,
  });

  // write to the cache file
  return writeStringToFile(
    JSON.stringify(menuItems),
    "~/Library/Application Support/BetterTouchTool/app-cache.json"
  );
}

This script retrieves the paths of the apps on your system via mdfind. Then it creates an array of objects with this format:

{
    "title": "Finder",
    "action": `js::(async () => {runShellScript({script: 'open "/System/Library/CoreServices/Finder.app"'})})()`,
    "icon": `path::/System/Library/CoreServices/Finder.app/Contents/Resources/Finder.icns::width@@30`,
}

The action runs some BTT Java Script to run the open terminal command to open the app.

Try running this script via the predefined action "Run Real Java Script" at least once, if it shows "done" as the script result (after a few seconds) everything worked fine:

Done!

Now everything should be ready, if you now open your launcher it should show the various apps on your system and allow you to open them.

Of course this is just one example of what can be achieved with the Simple JSON Format. You can do any sort of things using that and there are many more options available: Simple JSON Format · GitBook.


Download the final result of this tutorial here:
LauncherTutorial.bttpreset (21.5 KB)

Thanks for sharing this!

Thanks for all the work on this. It's been such a useful feature!

That is awesome, thanks @Andreas_Hegenberg.
I will use that only for ChatGPT, so I will deactivate the app selector and delete the cash JavaScript file.
But I don't understand the "Cancel All Al Requests" action that is in there. When I enter something in and don't want to use it, I just hit the escape key. Or is it supposed to do something else?

chatgpt often takes a while to reply. If you are using the streaming mode you might see early that the response is not what you need. In that case you could cancel the request

Ah, ok. I see.
I have one more question. Is there a way to customize the entering menu further?


For example, when there are fewer items, the menu could be shorter.

Or to add a margin for the text in the output HUD so that the Text is not exactly at the edge of the HUD.

Mine displays {bttpromtinput} this plain text I'm researching what's going on

make sure this option is active:

This guide is so well-written and easy to follow – thanks for making this awesome demo Andreas!

1 Like

That is great setup. thanks.

When i type "tell me a joke" on ChatGPT why do I get this

Hi,

The variable {BTTPromptInput} no longer works; when using the function, nothing appears.

Best regards,

do you have one of the options that saves to variable enabled on the list itself?

I have indeed configured it with this option.

I can reproduce the issue. Weird - I haven't touched the code.
Are you also on macOS 15.2? Seems like a timing change, should be fixed in the first alpha for this year: v5.00! (uploading now)

Thank you for your response, I am still using macOS Sonoma 14.7.

strange, would be great if you'd check whether it works on 14.7 with v5.00 as well (now available)

I just tested the new version, the bug is fixed. Are there any improvements planned for the "Choose From List" function in the future? Like the ability to search within subfolders or improvements to the appearance of the menu.

No concrete plans but improvements are definitely possible! Best post feature requests for the things you'd need so I can easily track them.