Sessions
While an add-on is turned on, Ledge keeps its program running and talks to it over standard input and output, one line of JSON at a time. AddOnSession handles that; you write the handlers.
A session, start to finish
- The user turns the add-on on, or Ledge starts with it on. Ledge starts the program.
- Ledge says hello with its LedgeKit API level; the add-on answers with its own and its
id. - Ledge sends
activate. Youractivatehandler runs: show your surfaces and start timers here. - Ledge and the add-on talk. Ledge sends what the user did and asks questions; the add-on sends updates, such as a new settings page, and asks Ledge for services.
- When the add-on is turned off, removed or updated, or Ledge quits, Ledge sends
deactivate, closes the input and stops the program.
import Foundation
import LedgeKit
var handlers = AddOnHandlers()
handlers.activate = { AddOnSession.show(page) } // once, before anything else
handlers.deactivate = { await timers.stop() } // stop what activate started
handlers.onEvent = { event in await handle(event) } // the user used a control
exit(await AddOnSession.run(id: "my-addon", handlers: handlers))Handlers
| Handler | When it runs |
|---|---|
activate | Once, after the handshake and before anything else. Throwing ends the session and Ledge shows the add-on as failed |
deactivate | Once, when the add-on is turned off or Ledge quits |
prepareForRemoval | Before removal. Return false to refuse, for example while a transfer is in flight |
onEvent | The user used a control on your settings page or widget, or clicked a menu bar tile |
onPlayerAction | Ledge asks a player button's state for a song, or the user pressed it |
onPlayerMenu | Ledge opened your ••• menu entry, or the user picked one of its items |
onPlayerQueue | Ledge wants Playing Next, or the user picked a song to play next |
onActivityPlacement | One of your activities moved: centre, side, waiting, turned off or down |
onEnvironmentChange | Light or dark, the accent colour or the displays changed. See Asking Ledge |
onVisibilityChange | Which of your surfaces someone can see changed. See Good add-ons. 0.13.0 |
Events, placements, environment and visibility changes arrive in order, one at a time. Requests, like a player button's state, run side by side.
Your own requests
Ledge's features can also call an add-on by name with your own Codable types. Names starting with ledge. are Ledge's:
struct Banner: Codable, Sendable { let title: String; let body: String }
struct Shown: Codable, Sendable { let text: String }
handlers.on("banner") { (request: Banner) in
Shown(text: request.title)
}Errors
Throw AddOnError("…") for a reason Ledge may show the user, like “Music isn't open.” Any other error reaches Ledge as a generic failure, because error text can carry a token or a server's answer. Check your inputs before acting on them.
Logs
AddOnSession.log("…", level: .info) writes to Ledge's log, tagged with your add-on's ID, at up to 20 lines a second. Never log credentials or what the user typed. Standard output carries the session, so don't print to it.
Limits
| Limit | Value |
|---|---|
| Answering the hello and finishing activate | 5 seconds |
| A request Ledge sends, unless it says otherwise | 10 seconds |
| A call to Ledge, before it times out | 10 seconds |
| Calls to Ledge in flight at once | 16 |
| One message | 1 MB |
| Restarts after the program quits unexpectedly | 3, then it's shown as failed |
| Between asking it to stop and stopping it | 2 seconds |
Ledge stops everything the program started, including child processes, when the session ends. If Ledge itself crashes, the next launch stops whatever was left running.
Testing
Handlers are plain closures, so tests can call them directly. To run a whole session without a process, pass the input and a writer to AddOnSession.run:
let handlers = MyAddOn().handlers()
#expect(try await handlers.onPlayerAction(
PlayerActionRequest(action: "like", track: PlayerTrack(source: "com.spotify.client", id: "abc", title: "Song"))) == true)
let status = await AddOnSession.run(id: "my-addon", handlers: handlers, input: lines) { line in
await replies.add(line)
}Keep anything that needs an account behind a closure you can replace, the way Spotify Likes takes its requests to Ledge as a parameter, so tests run without a sign-in.