Build Custom Modules
for Itchy
Write a Swift bundle, expose one plugin, and choose whether it should appear as a Nook module or as a custom menu/content tab in the notch header. Users can import multiple bundles, and each one appears as its own plugin.
Get the SDK
The public SDK lives in the ItchySDK package on GitHub. Add it from Xcode as a Swift package dependency, then write your module with import itchy.
Swift Package repo: https://github.com/selcuksarikoz/itchy-sdk
Example Templates
The SDK includes ready-to-use templates in the Templates/ folder:
ClockModule, CounterModule, DateModule are good starting points for compact Nook modules. MiniShelfModule and QuickActionsModule are ideal for Nook apps (placement: .menuApp).
How it works
Itchy scans its Application Support modules folder for compiled macOS bundles, loads each bundle's principal class, and checks that it conforms to ItchyModulePlugin. If valid, the plugin appears in Settings and can be enabled, disabled, removed, and, for Nook placement, reordered with built-in modules.
Folders like Templates/DateModule/ are only example source code. The thing you import into Itchy is the compiled output, for example DateModule.bundle.
Itchy draws the module title from metadata.displayName. Custom module views should usually render only the content area and keep their backgrounds transparent so they match built-in modules.
You must explicitly choose one in source code: placement: .nookModule or placement: .menuApp. Itchy uses that to decide where the imported plugin appears inside the app.
Build a bundle
Create a macOS bundle target in Swift and expose a principal class that conforms to the plugin protocol.
Import it
Open Itchy Settings, go to Modules, and use Import Module to add the compiled .bundle.
Use it
Nook plugins render with built-ins, while menu plugins show up as additional icons in the top bar and open their own content area.
Here's a complete example of a custom Nook module with Live Activity support:
// ============================================
// 1. PLUGIN ENTRY POINT (ClockModule.swift)
// ============================================
import AppKit
import SwiftUI
import itchy
@objc(MyClockModule)
final class MyClockModule: NSObject, ItchyModulePlugin {
// Module metadata - tells Itchy how to display your module
var metadata: ItchyModuleMetadata {
ItchyModuleMetadata(
identifier: "com.example.clock", // Unique reverse-DNS ID
displayName: "Clock", // Shown in settings/headers
summary: "A custom clock module", // Short description
preferredWidth: 220, // Width in Nook (min 160pt)
placement: .nookModule, // .nookModule or .menuApp
iconSystemName: "clock", // SF Symbol for menu apps
supportsLiveActivity: true // Enable Notch view
)
}
// Return your SwiftUI view wrapped in NSViewController
func makeViewController() -> NSViewController {
NSHostingController(rootView: ClockModuleView())
}
// Called when Itchy has fully loaded your bundle
@objc func pluginDidLoad() {
// Start timers, send notifications, etc.
}
// --- LIVE ACTIVITY SUPPORT ---
// Compact view shown in Notch when collapsed
@objc func makeLiveActivityViewController() -> NSViewController {
NSHostingController(rootView: ClockLiveActivityView())
}
// Control visibility (polled every second)
@objc var isLiveActivityActive: Bool {
return true // Always show when module is enabled
}
// Trigger instant notification (alternative to persistent mode)
func triggerDownloadNotification() {
ItchyLiveActivityTrigger.trigger(
identifier: "com.example.clock",
title: "Download",
message: "file.zip",
trailingMessage: "85%", // Bold monospaced on right
systemIcon: "arrow.down.circle.fill",
duration: 3.0
)
}
}
// ============================================
// 2. SWIFTUI VIEW (ClockModuleView.swift)
// ============================================
import SwiftUI
struct ClockModuleView: View {
@State private var now = Date()
var body: some View {
// Use nookModuleLayout() for consistent alignment
TimelineView(.periodic(from: .now, by: 1)) { context in
VStack(alignment: .leading, spacing: 8) {
Text(context.date.formatted(date: .omitted, time: .standard))
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
// Spacer ensures top alignment even if content is short
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.nookModuleLayout() // Required for consistent spacing
}
.background(.clear) // Keep transparent
}
}
// Compact view for Notch (Live Activity)
struct ClockLiveActivityView: View {
var body: some View {
HStack(spacing: 8) {
Image(systemName: "clock.fill")
.font(.system(size: 14))
Text(Date().formatted(date: .omitted, time: .shortened))
.font(.system(size: 14, weight: .semibold, design: .monospaced))
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
}
// ============================================
// 3. INFO.PLIST (Info.plist)
// ============================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.example.itchy.clock</string>
<key>CFBundleName</key>
<string>ClockModule</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>NSPrincipalClass</key>
<string>MyClockModule</string>
</dict>
</plist>
// ============================================
// 4. BUNDLE STRUCTURE
// ============================================
ClockModule.bundle/
Contents/
Info.plist
MacOS/
ClockModule
// ============================================
// 5. BUILD FROM TERMINAL
// ============================================
swift build
mkdir -p BuiltBundles/ClockModule.bundle/Contents/MacOS \
BuiltBundles/ClockModule.bundle/Contents/Resources
cp Templates/ClockModule/Info.plist \
BuiltBundles/ClockModule.bundle/Contents/Info.plist
swiftc -parse-as-library -emit-library -Xlinker -bundle \
-module-name ClockModule \
-I .build/arm64-apple-macosx/debug/Modules \
Templates/ClockModule/ClockModule.swift \
Templates/ClockModule/ClockModuleView.swift \
.build/arm64-apple-macosx/debug/itchy.build/ItchyModulePlugin.swift.o \
-o BuiltBundles/ClockModule.bundle/Contents/MacOS/ClockModule
// ============================================
// 6. VALIDATE BEFORE SHIPPING
// ============================================
swift run itchy-module-validator /path/to/ClockModule.bundle- Build your plugin target so the output is a macOS
.bundle. - Start by adding the
ItchySDKSwift package from GitHub to your Xcode project. - Open Itchy and go to
Settings > Modules > Custom Modules. - Click
Import Moduleand select the bundle. - Enable the plugin from the matching Nook or Menu section.
- If it is a Nook plugin, drag it into position with the other modules.
If you do not want to use Xcode, you can build a template bundle from Terminal and then import the resulting .bundle.
swift build
mkdir -p BuiltBundles/DateModule.bundle/Contents/MacOS \
BuiltBundles/DateModule.bundle/Contents/Resources
cp Templates/DateModule/Info.plist \
BuiltBundles/DateModule.bundle/Contents/Info.plist
swiftc -parse-as-library -emit-library -Xlinker -bundle \
-module-name DateModule \
-I .build/arm64-apple-macosx/debug/Modules \
Templates/DateModule/DateModule.swift \
Templates/DateModule/DateModuleView.swift \
.build/arm64-apple-macosx/debug/itchy.build/ItchyModulePlugin.swift.o \
-o BuiltBundles/DateModule.bundle/Contents/MacOS/DateModule- Build your module and locate the resulting
.bundlein Finder. - Open Finder and press
Shift + Command + G. - Paste
~/Library/Application Support/Itchy/Modules. - Drag your
.bundleinto that folder. - Reopen Itchy or reopen Settings so the module list refreshes.
- Inspect the closest example template.
- Decide whether you are building a Nook module or a Nook app.
- Set
placementcorrectly in your metadata. - Keep the principal class tiny and move UI into SwiftUI views.
- Build the
.bundle. - Validate it with
itchy-module-validator. - Import it into Itchy via Settings.
Live Activities are compact views shown in the Notch when it is collapsed. They support two modes:
Persistent Mode
Return a compact view with makeLiveActivityViewController() and control visibility with isLiveActivityActive.
Triggered Mode
Instantly show a notification with ItchyLiveActivityTrigger.trigger(). Overrides other activities for the duration.
// Triggered Mode Example
ItchyLiveActivityTrigger.trigger(
identifier: "com.user.mod",
title: "Download",
message: "file.zip",
trailingMessage: "85%",
systemIcon: "arrow.down.circle.fill",
duration: 3.0
)- Itchy automatically renders the module title from
metadata.displayName. Do not include a title in your SwiftUI view. - Use
.nookModuleLayout()on your root container for consistent alignment. - Use
Spacer(minLength: 0)inside your VStack to ensure top alignment. - Keep your background transparent to match built-in modules.
- Use
ScrollViewif content may overflow. - Access
ItchyConstants.moduleHeight(120pt) for standard module height. - For Live Activities: container is ~30-32pt high. Use
trailingMessagefor status/percentages (bold monospaced).
The SDK ships with a validator CLI to check your bundle before shipping:
swift run itchy-module-validator /path/to/MyModule.bundleThis checks: bundle exists, principal class exists, metadata exists, makeViewController() returns NSViewController, identifier and displayName are valid.
- One imported bundle currently maps to one custom Nook module.
- One imported bundle currently maps to one plugin item.
- Users can import multiple bundles; Itchy loads all valid ones.
- Your module identifier must be unique and must not collide with built-in module IDs.
- Set
placementexplicitly to.nookModuleor.menuApp. - Keep width reasonable; Itchy uses your preferred width when placing Nook modules.
- Return a regular
NSViewControllerorNSHostingController. - Validate bundles before shipping with
swift run itchy-module-validator /path/to/MyModule.bundle.