SkillAgentSearch skills...

BetterTouchToolPlugins

Development of BetterTouchTool Plugins

Install / Use

/learn @folivoraAI/BetterTouchToolPlugins
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

BetterTouchTool Plugins

BetterTouchTool supports five types of plugins: Touch Bar, Stream Deck, Floating Menu Widget, Action, and Trigger plugins.

There are two ways to develop plugins:

  1. Swift Source Plugins (new) — Drop a single .swift file into the Plugins folder. BTT compiles and loads it automatically. No Xcode project required.
  2. Xcode Bundle Plugins — Build a plugin bundle in Xcode with full control over project structure, multiple files, and third-party dependencies.

Plugins are installed at: /Library/Application Support/BetterTouchTool/Plugins


Swift Source Plugins (No Xcode Required)

The simplest way to create a plugin. Write a single .swift file, drop it into the Plugins folder, and BTT handles compilation and loading.

Requirements

  • Xcode Command Line Tools must be installed (xcode-select --install)
  • The .swift file must contain a class conforming to one of the BTT plugin protocols

Metadata Comments

Add metadata comments at the top of your .swift file (all optional — defaults will be inferred):

// BTT-Plugin-Name: My Widget
// BTT-Plugin-Identifier: com.myname.mywidget
// BTT-Plugin-Type: FloatingMenuWidget
// BTT-Plugin-Icon: star.fill

| Comment | Default if omitted | |---|---| | BTT-Plugin-Name | Filename without extension | | BTT-Plugin-Identifier | com.btt.swift.<filename> | | BTT-Plugin-Type | Inferred from protocol conformance, or FloatingMenuWidget | | BTT-Plugin-Icon | None (SF Symbol name) |

Supported BTT-Plugin-Type values: FloatingMenuWidget, Action, StreamDeck, TouchBar, Trigger

How It Works

  1. Drop your .swift file into /Library/Application Support/BetterTouchTool/Plugins/
  2. BTT detects the file and asks: "Compile & Load?"
  3. On approval, BTT compiles it with swiftc into a plugin bundle in the same folder
  4. The compiled bundle is loaded and the plugin becomes available

If you edit the .swift file, BTT will detect the change and offer to recompile. If you delete the .swift file, the compiled bundle is removed automatically.

You can also drop .swift files onto the BTT preferences window or open them via File > Open — BTT will copy them to the Plugins folder and compile them.

Available Protocols and Delegate Methods

All plugin protocols are defined in BTTSwiftPluginHeader.h (shipped inside the app bundle). The bridging header is automatically provided during compilation — you don't need to import it.

Every plugin delegate provides access to BTT variables:

// Set a BTT variable (accessible via {variable_name} in BTT)
delegate?.setVariable("my_var", value: "hello")

// Read a BTT variable
let value = delegate?.getVariable("my_var")

Floating Menu Widget Example

// BTT-Plugin-Name: Hello Widget
// BTT-Plugin-Type: FloatingMenuWidget
// BTT-Plugin-Icon: hand.wave.fill

import Cocoa

class HelloWidget: NSObject, BTTFloatingMenuWidgetInterface {
    weak var delegate: (any BTTFloatingMenuWidgetDelegate)?

    static func widgetName() -> String { "Hello Widget" }
    static func widgetDescription() -> String { "A simple greeting widget" }
    static func widgetIcon() -> String { "hand.wave.fill" }

    // Override the menu item's configured size (optional)
    static func widgetMinWidth() -> CGFloat { 200 }
    static func widgetMinHeight() -> CGFloat { 80 }

    func makeWidgetView() -> NSView {
        let label = NSTextField(labelWithString: "Hello from BTT!")
        label.font = .systemFont(ofSize: 24, weight: .medium)
        label.textColor = .white
        return label
    }

    func widgetDidAppear() {
        // Called when the widget becomes visible
    }

    func widgetWillDisappear() {
        // Called when the widget is hidden
    }
}

Floating Menu Widget Example (SwiftUI)

Floating menu widgets can use SwiftUI — just wrap your SwiftUI view in an NSHostingView.

If your widget contains interactive controls (buttons, sliders, text fields, etc.), return true from widgetWantsInteractiveView() so BTT doesn't intercept clicks:

// BTT-Plugin-Name: Timer Widget
// BTT-Plugin-Type: FloatingMenuWidget
// BTT-Plugin-Icon: timer

import Cocoa
import SwiftUI

class TimerWidget: NSObject, BTTFloatingMenuWidgetInterface {
    weak var delegate: (any BTTFloatingMenuWidgetDelegate)?

    static func widgetName() -> String { "Timer Widget" }
    static func widgetDescription() -> String { "A simple countdown timer" }
    static func widgetIcon() -> String { "timer" }

    // Allow buttons inside the widget to receive clicks
    static func widgetWantsInteractiveView() -> Bool { true }

    // Override the menu item's configured size (optional)
    static func widgetMinWidth() -> CGFloat { 250 }
    static func widgetMinHeight() -> CGFloat { 150 }
    static func widgetMaxWidth() -> CGFloat { 300 }
    static func widgetMaxHeight() -> CGFloat { 200 }

    func makeWidgetView() -> NSView {
        return NSHostingView(rootView: TimerView())
    }
}

struct TimerView: View {
    @State private var secondsLeft = 60
    @State private var running = false

    var body: some View {
        VStack(spacing: 12) {
            Text("\(secondsLeft)s")
                .font(.system(size: 48, weight: .bold, design: .rounded))
                .monospacedDigit()
                .foregroundColor(secondsLeft <= 10 ? .red : .white)

            HStack(spacing: 16) {
                Button(running ? "Stop" : "Start") {
                    running.toggle()
                }
                Button("Reset") {
                    running = false
                    secondsLeft = 60
                }
            }
        }
        .padding()
        .onReceive(
            Timer.publish(every: 1, on: .main, in: .common).autoconnect()
        ) { _ in
            guard running, secondsLeft > 0 else { return }
            secondsLeft -= 1
            if secondsLeft == 0 { running = false }
        }
    }
}

Action Plugin Example

// BTT-Plugin-Name: Show Greeting
// BTT-Plugin-Type: Action
// BTT-Plugin-Icon: bubble.left.fill

import Cocoa

class ShowGreeting: NSObject, BTTActionPluginInterface {
    weak var delegate: (any BTTActionPluginDelegate)?

    static func configurationFormItems() -> BTTPluginFormItem? { nil }

    func executeAction(
        withConfiguration config: [String: Any]?,
        completionBlock: @escaping (Any?) -> Void
    ) {
        let alert = NSAlert()
        alert.messageText = "Hello from a BTT Action Plugin!"
        alert.runModal()
        completionBlock("done")
    }
}

Stream Deck Plugin Example

// BTT-Plugin-Name: Counter
// BTT-Plugin-Type: StreamDeck
// BTT-Plugin-Icon: number.circle.fill

import Cocoa

class Counter: NSObject, BTTStreamDeckPluginInterface {
    weak var delegate: (any BTTStreamDeckPluginDelegate)?
    private var count = 0

    static func configurationFormItems() -> BTTPluginFormItem? { nil }

    func widgetTitleStrings() -> [String]? {
        return ["\(count)"]
    }

    func buttonDown(_ identifier: String) -> Bool {
        count += 1
        delegate?.requestUpdate(self)
        return false // return true to cancel assigned BTT actions
    }

    func buttonUp(_ identifier: String) -> Bool {
        return false
    }
}

Touch Bar Plugin Example

// BTT-Plugin-Name: Clock Text
// BTT-Plugin-Type: TouchBar
// BTT-Plugin-Icon: clock

import Cocoa

class ClockText: NSObject, BTTPluginInterface {
    weak var delegate: (any BTTTouchBarPluginDelegate)?

    static func configurationFormItems() -> BTTPluginFormItem? { nil }

    func touchBarTitleString() -> String? {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"
        return formatter.string(from: Date())
    }
}

Trigger Plugin Example

Trigger plugins observe system events and fire BTT triggers when conditions are met. They appear in Other Triggers > Trigger Plugins.

// BTT-Plugin-Name: Clipboard Change
// BTT-Plugin-Type: Trigger
// BTT-Plugin-Icon: doc.on.clipboard

import Cocoa

class ClipboardChangeTrigger: NSObject, BTTTriggerPluginInterface {
    weak var delegate: (any BTTTriggerPluginDelegate)?
    private var timer: Timer?
    private var lastChangeCount = NSPasteboard.general.changeCount

    static func triggerName() -> String { "Clipboard Change" }
    static func triggerDescription() -> String { "Fires when clipboard content changes" }
    static func triggerIcon() -> String { "doc.on.clipboard" }
    static func configurationFormItems() -> BTTPluginFormItem? { nil }
    func didReceiveNewConfigurationValues(_ config: [String: Any]?) {}

    func startObserving() {
        lastChangeCount = NSPasteboard.general.changeCount
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            guard let self else { return }
            let current = NSPasteboard.general.changeCount
            if current != self.lastChangeCount {
                self.lastChangeCount = current
                let content = NSPasteboard.general.string(forType: .string) ?? ""
                self.delegate?.triggerFired(self, withContext: ["clipboardContent": content])
            }
        }
    }

    func stopObserving() {
        timer?.invalidate()
        timer = nil
    }
}

Trigger Plugin Example (with Configuration)

// BTT-Plugin-Name: File Watcher
// BTT-Plugin-Type: Trigger
// BTT-Plugin-Icon: doc.badge.clock

import Cocoa

class FileWatcherTrigger: NSObject, BTTTriggerPluginInterface {
    weak var delegate: (any BTTTriggerPluginDelegate)?
    private var watchedPath: String = ""
    private var fileDescriptor: Int32 = -1
    private var dispatchSource: DispatchSourceFileSystemObject?

    static func triggerName() -> String { "File Watcher" }
    static func triggerDescription() -> String { "Fires when a file or folder changes" }
    static fu
View on GitHub
GitHub Stars79
CategoryDevelopment
Updated1d ago
Forks6

Languages

Objective-C

Security Score

95/100

Audited on Apr 4, 2026

No findings