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.swift2. Depend on LedgeKit
Pin the released SDK to an exact version, so the build is the same on every machine:
// 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.
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.
{
"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/Countdown6. 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.