BetterTouchToolPlugins
Development of BetterTouchTool Plugins
Install / Use
/learn @folivoraAI/BetterTouchToolPluginsREADME
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:
- Swift Source Plugins (new) — Drop a single
.swiftfile into the Plugins folder. BTT compiles and loads it automatically. No Xcode project required. - 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
.swiftfile 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
- Drop your
.swiftfile into/Library/Application Support/BetterTouchTool/Plugins/ - BTT detects the file and asks: "Compile & Load?"
- On approval, BTT compiles it with
swiftcinto a plugin bundle in the same folder - 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
