Developer Docs

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
MiniShelfModule
QuickActionsModule

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.

1

Build a bundle

Create a macOS bundle target in Swift and expose a principal class that conforms to the plugin protocol.

2

Import it

Open Itchy Settings, go to Modules, and use Import Module to add the compiled .bundle.

3

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.

Complete Example

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
Import Flow
  1. Build your plugin target so the output is a macOS .bundle.
  2. Start by adding the ItchySDK Swift package from GitHub to your Xcode project.
  3. Open Itchy and go to Settings > Modules > Custom Modules.
  4. Click Import Module and select the bundle.
  5. Enable the plugin from the matching Nook or Menu section.
  6. If it is a Nook plugin, drag it into position with the other modules.
Manual Install

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
Manual Install
  1. Build your module and locate the resulting .bundle in Finder.
  2. Open Finder and press Shift + Command + G.
  3. Paste ~/Library/Application Support/Itchy/Modules.
  4. Drag your .bundle into that folder.
  5. Reopen Itchy or reopen Settings so the module list refreshes.
Recommended Workflow
  1. Inspect the closest example template.
  2. Decide whether you are building a Nook module or a Nook app.
  3. Set placement correctly in your metadata.
  4. Keep the principal class tiny and move UI into SwiftUI views.
  5. Build the .bundle.
  6. Validate it with itchy-module-validator.
  7. Import it into Itchy via Settings.
Live Activities

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
)
UI Expectations
  • 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 ScrollView if content may overflow.
  • Access ItchyConstants.moduleHeight (120pt) for standard module height.
  • For Live Activities: container is ~30-32pt high. Use trailingMessage for status/percentages (bold monospaced).
Validate Your Bundle

The SDK ships with a validator CLI to check your bundle before shipping:

swift run itchy-module-validator /path/to/MyModule.bundle

This checks: bundle exists, principal class exists, metadata exists, makeViewController() returns NSViewController, identifier and displayName are valid.

Rules
  • 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 placement explicitly to .nookModule or .menuApp.
  • Keep width reasonable; Itchy uses your preferred width when placing Nook modules.
  • Return a regular NSViewController or NSHostingController.
  • Validate bundles before shipping with swift run itchy-module-validator /path/to/MyModule.bundle.