Skip to main content

The bettertouchtool npm package

bettertouchtool is the official, typed JavaScript / TypeScript client for BetterTouchTool's scripting API. It wraps every scripting function described in this section (trigger_named, trigger_action, variables, clipboard, floating menus, widgets, presets, …) with typed methods, ships builders for actions and triggers, and includes a small btt command line tool.

Source & issues: github.com/folivoraAI/bettertouchtool-js (MIT, AI generated & maintained by folivora.AI).

npm install bettertouchtool

Where it runs

EnvironmentTransport usedSetup in BTT
Node.js (≥ 18) on the same MacUnix socket (/tmp/com.hegenberg.BetterTouchTool.sock)Settings → Scripting BTT → Command Line → Enable Socket Server
Node.js, browsers, other devicesWebserver (HTTP, GET or POST)Settings → Scripting BTT → Webserver
Inside BTT itself (Run Real JavaScript, WebViews, floating HTML menus, script widgets)in-process callBTT()nothing

new Btt() picks the best available transport automatically (in-process → socket → http); Btt.socket(), Btt.http({ port, sharedSecret }) and Btt.inProcess() force one.

Examples

import { Btt, actions, triggers } from "bettertouchtool";

const btt = Btt.http({ port: 64472, sharedSecret: "my-secret" }); // or Btt.socket()

// scripting functions as typed methods
await btt.triggerNamed("My Named Trigger");
const app = await btt.getStringVariable("BTTActiveAppBundleIdentifier");
await btt.vars.set("my_counter", 5, { persistent: true });
const clip = await btt.getClipboardContent();

// run any predefined action
await btt.triggerAction(actions.showHUD("Hello", { detail: app, duration: 2 }));
await btt.triggerAction(actions.sendShortcut("cmd+shift+4"));

// several actions in one round trip
await btt.chain().launchApp("com.apple.Safari").delay(0.5).sendShortcut("cmd+t").pasteText("folivora.ai").run();

// create triggers from code
const handle = await btt.addNewTrigger(
triggers.keyboardShortcut("cmd+shift+k", [actions.showHUD("⌘⇧K pressed")], { description: "created from Node" }),
);
await handle.invoke();
await handle.delete();

// anything else: raw scripting call (parameter names as documented in this section)
await btt.call("update_menu_item", { item_uuid: "…", json: { BTTMenuItemText: "hi" } });

ActionType contains all BTTPredefinedActionType ids by name (ActionType.SHOW_HUD === 254), and import { actionCatalog } from "bettertouchtool/catalog" gives you the complete action and trigger reference data with documented parameters (actionCatalog.search("floating menu")).

Inside BetterTouchTool: require("bettertouchtool")

Since BTT 6.735 a copy of the library ships inside BetterTouchTool and BTT's JavaScript engine has a CommonJS-style require(). Nothing to install:

// Run Real JavaScript action – shows a HUD, returns the frontmost app
async function main() {
const { Btt, actions } = require("bettertouchtool");
const btt = Btt.inProcess(); // talks to BTT directly, no socket / webserver needed

const app = await btt.getStringVariable("BTTActiveAppBundleIdentifier");
await btt.triggerAction(
actions.showHUD("Hello from inside BTT 🖐", { detail: `frontmost app: ${app}`, duration: 2 }),
);
await btt.chain().sendShortcut("cmd+s").delay(0.3).showHUD("saved").run(); // sequences work too
return app; // becomes the action's result
}

require() resolves, in this order:

  1. modules bundled with BTT – currently bettertouchtool (also require("bettertouchtool").catalog for the action/trigger reference data)
  2. your own modules in ~/Library/Application Support/BetterTouchTool/JavaScriptModules/, loaded by name: <name>.js / <name>.cjs, a package folder <name>/ (package.jsonexports["."] or main, else index.js / index.cjs), nested names like require("utils/parse"); with or without the extension
  3. a file path (require("~/Scripts/my-bundle.cjs"), require("/absolute/path.js"), require("./relative"))

module.exports / exports work like in Node, and IIFE bundles that assign to a global work as well.

Modules are evaluated once per JavaScript context and cached (require.cache). Editing a module file is picked up automatically – BTT compares the file's modification date on every require() and re-evaluates changed modules, so you can iterate without restarting BTT. To force it: require.reload("name") (re-loads one module and returns it), require.reload() (drops the whole cache) or delete require.cache["name"]. (Auto-reload and package folders: BTT 6.736+; require() itself: 6.735+.)

Using other npm packages inside BetterTouchTool

The engine itself has no Node.js module resolution, so third-party npm packages must be bundled into one CommonJS file first (they must not depend on Node-only APIs such as fs or child_process):

# my-module.js: import dayjs from "dayjs"; module.exports = { today: () => dayjs().format("YYYY-MM-DD") };
npx esbuild my-module.js --bundle --format=cjs --platform=browser \
--outfile=~/Library/Application\ Support/BetterTouchTool/JavaScriptModules/my-module.js
async function main() {
const { today } = require("my-module");
return today();
}

For BTT versions before 6.735 use eval(readFile("/path/to/bundle.js")) with an IIFE bundle instead.

Command line

npx bettertouchtool help
btt trigger-named "My Trigger"
btt get-var BTTActiveAppBundleIdentifier
btt hud "Hello" --detail "from the shell"
btt triggers list --type BTTTriggerTypeKeyboardShortcut
btt actions search hud
btt call trigger_action json='{"BTTPredefinedActionType":254,"BTTAdditionalActionData":{"BTTActionHUDTitle":"hi"}}'

Connection flags --socket, --url, --port, --secret (or env BTT_SOCKET, BTT_URL, BTT_PORT, BTT_SECRET); by default it uses the socket when available. This complements the built-in bttcli tool that ships with BTT.