esc

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

  1. The user turns the add-on on, or Ledge starts with it on. Ledge starts the program.
  2. Ledge says hello with its LedgeKit API level; the add-on answers with its own and its id.
  3. Ledge sends activate. Your activate handler runs: show your surfaces and start timers here.
  4. 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.
  5. When the add-on is turned off, removed or updated, or Ledge quits, Ledge sends deactivate, closes the input and stops the program.
main.swift
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

HandlerWhen it runs
activateOnce, after the handshake and before anything else. Throwing ends the session and Ledge shows the add-on as failed
deactivateOnce, when the add-on is turned off or Ledge quits
prepareForRemovalBefore removal. Return false to refuse, for example while a transfer is in flight
onEventThe user used a control on your settings page or widget, or clicked a menu bar tile
onPlayerActionLedge asks a player button's state for a song, or the user pressed it
onPlayerMenuLedge opened your ••• menu entry, or the user picked one of its items
onPlayerQueueLedge wants Playing Next, or the user picked a song to play next
onActivityPlacementOne of your activities moved: centre, side, waiting, turned off or down
onEnvironmentChangeLight or dark, the accent colour or the displays changed. See Asking Ledge
onVisibilityChangeWhich 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

LimitValue
Answering the hello and finishing activate5 seconds
A request Ledge sends, unless it says otherwise10 seconds
A call to Ledge, before it times out10 seconds
Calls to Ledge in flight at once16
One message1 MB
Restarts after the program quits unexpectedly3, then it's shown as failed
Between asking it to stop and stopping it2 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.