esc

Asking Ledge

Some things only Ledge has: a place to keep settings, the Mac's appearance, the notch, and accounts you've signed in to. An add-on asks for them with LedgeHost, from any handler.

What you can ask for

CallDoesNeeds
value(forKey:)Reads a stored settingNothing
set(_:forKey:)Stores a settingNothing
removeValue(forKey:)Removes oneNothing
environment()Ledge's version, light or dark, the accent colour, whether there's a notchNothing
open(_:)Opens an https, http or mailto linkNothing
copy(_:)Puts text on the clipboardclipboard-write
notify(title:message:symbol:)Shows a message in the notchhud
account(_:)Whether the user is signed in to an account Ledge managesNothing
request(provider:…)Calls that account's API, with Ledge adding the credentialsThe provider's capability
dataFolder()A folder for the add-on's own files, kept through updatesNothing. 0.13.0
showPage()Opens the notch on the add-on's page, just after the user actsnotch-control. 0.13.0
openSettings()Opens Settings on the add-on's page, just after the user actsNothing. 0.13.0
holdOpen(_:)Keeps the notch open while something the user watches runsnotch-control. 0.13.0

Your own files

let folder = try await LedgeHost.dataFolder()   // ~/Library/Application Support/Ledge/AddOnData/<id>/Files
try Data(markdown.utf8).write(to: folder.appendingPathComponent("\(id).md"), options: .atomic)

The folder is the add-on's alone. Ledge creates it when it's first asked for and keeps it through updates and reinstalls. When someone removes the add-on, Ledge asks whether to delete its settings and saved data too, and keeps them unless they say so. Put documents and caches here, never inside the .ledgeaddon, which an update replaces.

The notch and Settings

An add-on with a page in the notch can bring it up, and any add-on can open its own page in Settings, but only when the user has asked for it:

  • showPage() works within two seconds of the user using one of the add-on's controls, or while one of its activities is .attention. A timer on its own can't open the notch. Ledge also refuses while the notch is hidden, such as over a full-screen app, or while the user types in another page.
  • openSettings() works within two seconds of a press, which is what a “Set up” button in an empty state needs.
  • holdOpen(true) stops the open notch closing when the pointer leaves, while a countdown or a teleprompter runs. It counts only while the add-on's page is the tab showing, a click elsewhere still closes the notch, and Ledge lets go when the add-on stops. Call holdOpen(false) the moment the work ends.

Settings

struct Choices: Codable { var units = "km" }

handlers.activate = {
    let choices = (try? await LedgeHost.value(Choices.self, forKey: "choices")) ?? Choices()
    AddOnSession.show(page(for: choices))
}
try await LedgeHost.set(Choices(units: "mi"), forKey: "choices")

Settings belong to the add-on: no other add-on can read them. They survive restarts and updates, and stay when the add-on is removed, so reinstalling picks up where the user left off. Keys are up to 128 characters, and all of an add-on's values together up to 256 KB.

The environment

let environment = try await LedgeHost.environment()   // ledgeVersion, apiLevel, appearance, accentColor, hasNotch
handlers.onEnvironmentChange = { environment in
    AddOnSession.show(widgets: [moon(dark: environment.appearance == "dark")])
}

appearance is "light" or "dark", and accentColor is #RRGGBB. From LedgeKit 0.9.0, Ledge sends the new environment when the user switches appearance or accent colour, or displays come and go.

Accounts

Ledge signs in to some services itself and keeps the credentials in the Keychain. An add-on uses the account through Ledge's broker and never receives a token. Put Ledge's sign-in on your settings page with an account row:

SettingsSection("account", title: "Spotify Account", rows: [.account("spotify", "Spotify", provider: "spotify")])

let answer = try await LedgeHost.request(provider: "spotify", method: "GET",
                                         path: "/v1/me/library/contains", query: ["uris": "spotify:track:" + id])
// answer.status, answer.body; never the headers
ProviderCapabilityWhat it allows
spotifyspotify-libraryLiked Songs: whether songs are in it, adding and removing them (/v1/me/library and /v1/me/tracks, each with /contains)

Ledge builds the address itself from the provider's host, so a path can't point anywhere else, and allows only the exact methods, paths and query keys it lists. Request bodies are up to 64 KB and answers up to 1 MB.

When Ledge says no

A call Ledge refuses throws LedgeHostError with the reason, such as a capability the user didn't allow or “Spotify isn't connected.” Calls Ledge doesn't answer within 10 seconds throw too. Capabilities can be off, so handle the error rather than assume.