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
| Call | Does | Needs |
|---|---|---|
value(forKey:) | Reads a stored setting | Nothing |
set(_:forKey:) | Stores a setting | Nothing |
removeValue(forKey:) | Removes one | Nothing |
environment() | Ledge's version, light or dark, the accent colour, whether there's a notch | Nothing |
open(_:) | Opens an https, http or mailto link | Nothing |
copy(_:) | Puts text on the clipboard | clipboard-write |
notify(title:message:symbol:) | Shows a message in the notch | hud |
account(_:) | Whether the user is signed in to an account Ledge manages | Nothing |
request(provider:…) | Calls that account's API, with Ledge adding the credentials | The provider's capability |
dataFolder() | A folder for the add-on's own files, kept through updates | Nothing. 0.13.0 |
showPage() | Opens the notch on the add-on's page, just after the user acts | notch-control. 0.13.0 |
openSettings() | Opens Settings on the add-on's page, just after the user acts | Nothing. 0.13.0 |
holdOpen(_:) | Keeps the notch open while something the user watches runs | notch-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. CallholdOpen(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| Provider | Capability | What it allows |
|---|---|---|
spotify | spotify-library | Liked 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.