esc

Quick start

Build a small countdown add-on: a settings page with a Start button, and the time left beside the notch. Then package it and load it into your own Ledge.

What you need

  • macOS 14 Sonoma or later
  • Swift 6, from Xcode or the Swift toolchain
  • Ledge, to load and try your add-on

1. Lay out the package

An add-on is an ordinary Swift package with a manifest and an icon beside it:

countdown/
├── Package.swift
├── addon.json
├── Assets/
│   └── Icon.png
└── Sources/
    └── Countdown/
        └── main.swift

2. Depend on LedgeKit

Pin the released SDK to an exact version, so the build is the same on every machine:

Package.swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Countdown",
    platforms: [.macOS(.v14)],
    dependencies: [
        .package(url: "https://github.com/Ammyy9908/LedgeKit.git", exact: "0.13.0")
    ],
    targets: [
        .executableTarget(name: "Countdown", dependencies: ["LedgeKit"])
    ]
)

3. Write the session

AddOnSession.run talks to Ledge for as long as the add-on is on. You give it handlers: activate runs once at the start, onEvent runs when the user uses one of your controls. AddOnSession.show sends what to draw, and each send replaces the last.

Sources/Countdown/main.swift
import Foundation
import LedgeKit

actor Countdown {
    private var endsAt: Date?
    private var ticker: Task<Void, Never>?

    func start(minutes: Double) {
        endsAt = Date().addingTimeInterval(minutes * 60)
        ticker?.cancel()
        ticker = Task { await self.tick() }
    }

    private func tick() async {
        while let endsAt, !Task.isCancelled {
            let left = max(0, endsAt.timeIntervalSinceNow)
            if left == 0 {
                self.endsAt = nil
                AddOnSession.show(activities: [])               // nothing left to show
                try? await LedgeHost.notify(title: "Time's up", symbol: "timer")
                return
            }
            let text = String(format: "%d:%02d", Int(left) / 60, Int(left) % 60)
            AddOnSession.show(activities: [LiveActivity("countdown", symbol: "timer", text: text)])
            try? await Task.sleep(nanoseconds: 1_000_000_000)
        }
    }
}

let countdown = Countdown()
let page = SettingsPage([
    SettingsSection("timer", title: "Countdown", rows: [.button("start", "Start 5 minutes")])
])

var handlers = AddOnHandlers()
handlers.activate = { AddOnSession.show(page) }
handlers.onEvent = { event in
    if event.control == "start" { await countdown.start(minutes: 5) }
}
exit(await AddOnSession.run(id: "countdown", handlers: handlers))

Sessions explains the handlers, and each surface has its own page: settings, activities, the player, the lock screen and menu bar.

4. Describe it in addon.json

surfaces lists what the add-on shows; Ledge draws nothing it doesn't list. capabilities lists what it asks Ledge for, here hud for the message at the end. kit.minAPI is the LedgeKit it was built with.

addon.json
{
  "id": "countdown",
  "version": "1.0.0",
  "protocolVersion": 2,
  "name": "Countdown",
  "summary": "Five minutes, beside your notch",
  "description": "Starts a five-minute countdown from its settings page and shows the time left beside the notch.",
  "author": "Your name",
  "capabilities": ["hud"],
  "surfaces": ["settings-page", "activity"],
  "kit": { "minAPI": "0.13.0" },
  "executable": "Countdown",
  "icon": "Assets/Icon.png"
}

Every field is described in addon.json. Check it with ledgekit validate.

5. Build and package

Build a release binary, then put it in a folder with the manifest and the icon. The program's file name must match executable.

swift build -c release
bin="$(swift build -c release --show-bin-path)"

mkdir -p Countdown.ledgeaddon/Assets
cp "$bin/Countdown" Countdown.ledgeaddon/
cp addon.json Countdown.ledgeaddon/
cp Assets/Icon.png Countdown.ledgeaddon/Assets/
codesign --force --sign - Countdown.ledgeaddon/Countdown

6. Load it into Ledge

In Ledge, open Settings → Add-on Store, choose ••• → Load Add-on… and pick Countdown.ledgeaddon. Ledge checks the package, shows what it asks for, and asks you to trust it.

Countdown then has its own page under Add-ons in the Settings sidebar, with your Start button on it. Press it, and the time left appears beside the notch.

For a complete add-on to copy from, read Timer in LedgeKit, which adds a progress ring, a lock screen pill and a menu bar row, or the add-ons in LedgeAddOns.