Structured SpaceAPI protocol¶
The structured SpaceAPI is available from contract version 1.0.0 and currently reports 1.2.0. It uses JSON-RPC 2.0 as its message format. JSON-RPC 2.0 is the transport envelope and is versioned independently from the API contract.
The legacy SpaceAPI notifications, user-info keys, and delimiter-based payloads remain supported. New integrations should prefer this structured protocol. AppleScript is documented separately in the AppleScript guide.
Distributed notification channels¶
Structured messages use dedicated DistributedNotificationCenter channels:
| Channel | Direction | Purpose |
|---|---|---|
dev.mqiu.DesktopRenamer.RPCRequest |
Client → app | JSON-RPC requests. |
dev.mqiu.DesktopRenamer.RPCResponse |
App → client | Correlated JSON-RPC responses. |
dev.mqiu.DesktopRenamer.RPCEvent |
App → clients | JSON-RPC notifications for state changes. |
Every notification carries exactly one transport value in userInfo:
["payload": String]
payload is UTF-8 JSON encoded as a string. Do not put individual JSON fields beside payload; keeping one string value avoids Foundation bridging differences between Swift, JXA, and other clients. Register response observers before posting a request.
JSON-RPC envelopes¶
Requests use a non-empty string ID. Named parameters are a JSON object, and may be omitted when the method has no parameters:
{
"jsonrpc": "2.0",
"id": "9f3a8d40-2d38-4bf9-9c0c-790bd38f7598",
"method": "getSpaceSnapshot"
}
An operation with parameters looks like this:
{
"jsonrpc": "2.0",
"id": "4d72d2b4-c6b7-41ee-9aef-15fe1b8d9a9b",
"method": "switchToSpace",
"params": {
"spaceID": "SPACE-ID"
}
}
Successful responses contain exactly one result:
{
"jsonrpc": "2.0",
"id": "4d72d2b4-c6b7-41ee-9aef-15fe1b8d9a9b",
"result": {
"accepted": true
}
}
Errors contain exactly one error. Parameter errors include machine-readable metadata when available:
{
"jsonrpc": "2.0",
"id": "4d72d2b4-c6b7-41ee-9aef-15fe1b8d9a9b",
"error": {
"code": -32602,
"message": "Missing required parameter 'spaceID'.",
"data": {
"parameter": "spaceID",
"expected": "string",
"command": "switchToSpace"
}
}
}
Every response includes the id member. It is the request's non-empty string ID for a normal response, or explicit null when the server cannot recover an ID from a malformed request. Clients should accept the latter while matching normal responses only by their original request ID.
Events are JSON-RPC notifications and therefore have no ID or response. The current event is stateChanged:
{
"jsonrpc": "2.0",
"method": "stateChanged",
"params": {
"reason": "activeSpaceChanged",
"snapshot": {
"apiVersion": "1.2.0",
"revision": 18,
"timestamp": "2026-08-31T07:00:00Z",
"currentSpaceIDs": ["SPACE-ID"],
"currentSpaceID": "SPACE-ID",
"currentDisplayID": "DISPLAY-ID",
"currentSpaceName": "Writing",
"movedWindowsCount": 0,
"spaces": []
}
}
}
The app currently emits activeSpaceChanged, spaceListChanged, lockStateChanged, and movedWindowsChanged as the event reason. The nested snapshot is a complete space snapshot, not a delta.
Typed values¶
Space¶
Each space object contains:
| Field | Type | Description |
|---|---|---|
id |
string | Opaque DesktopRenamer-owned identity for this space. Use it as the persistent key and in structured operation parameters. |
name |
string | User-facing name. |
displayID |
string | Display identifier. |
displayName |
string | Localized display name when available. |
number |
integer | Space number on its display. |
isFullscreen |
Boolean | Whether the space belongs to a full-screen app. |
appName |
string or null |
Full-screen app name, when available. |
appPath |
string or null |
Full-screen app path, when available. |
globalShortcutNumber |
integer or null |
Configured global shortcut number, when available. |
isLocked |
Boolean | Whether Space Lock is enabled for this space. Fullscreen spaces are always false and cannot be locked. |
DesktopRenamer creates and stores this ID independently of macOS's numeric ManagedSpaceID. It is retained across app and system restarts, space reordering, and display changes when macOS reports the same persistent space UUID. A deleted and recreated space receives a new ID when macOS reports its new persistent identity or DesktopRenamer observes it as a distinct space. If a space has no persistent UUID, its ID is guaranteed only for the current boot and is renewed after reboot rather than guessed from position or risking assigning saved client data to the wrong space. Structured operations also accept a ManagedSpaceID while it still identifies a current space, to ease migration; use the stable id from structured responses for new integrations. The legacy API continues to expose ManagedSpaceIDs in its original output formats.
Use the exact id from a current structured snapshot for structured operation parameters. Unknown or stale IDs are rejected rather than being interpreted as a different space. For transitional compatibility, an old ManagedSpaceID is accepted only while it still identifies a current space.
Space snapshot¶
getSpaceSnapshot returns a versioned snapshot. movedWindowsCount is the number of windows currently queued for restoration by Space Lock:
{
"apiVersion": "1.2.0",
"revision": 18,
"timestamp": "2026-08-31T07:00:00Z",
"currentSpaceIDs": ["SPACE-ID"],
"currentSpaceID": "SPACE-ID",
"currentDisplayID": "DISPLAY-ID",
"currentSpaceName": "Writing",
"movedWindowsCount": 0,
"spaces": [/* space objects */]
}
currentSpaceID is the DesktopRenamer ID for the current space, or an empty string while that space is not yet present in the reconciled space list. currentSpaceIDs includes only current spaces with a reconciled structured ID; it never falls back to exposing a macOS ManagedSpaceID.
Structured JSON-RPC getAllSpaces returns an array of space objects. This is distinct from the legacy PerformCommand/AppleScript get all spaces command, which returns newline-delimited six-field strings. Use getSpaceSnapshot when the current-space values, restore count, revision, and timestamp are also needed.
Window snapshot¶
getWindows returns a versioned snapshot containing spaces and windows:
{
"apiVersion": "1.2.0",
"revision": 18,
"timestamp": "2026-08-31T07:00:00Z",
"spaces": [/* space objects */],
"windows": [
{
"id": 1234,
"pid": 5678,
"ownerName": "Example",
"appPath": "/Applications/Example.app",
"title": "Document | Notes",
"spaceID": "SPACE-ID",
"spaceIDs": ["SPACE-ID"],
"isMinimized": false,
"isHidden": false
}
]
}
appPath and title are nullable because macOS may not expose them. Structured JSON responses include these keys with null when unavailable; a non-JSON representation such as an AppleScript record may omit the corresponding property. Titles, names, paths, Unicode, quotes, newlines, and delimiter characters are ordinary string values in this protocol and require no escaping beyond JSON encoding.
Window records whose primary space is not yet in the reconciled space list are omitted rather than returning a macOS ManagedSpaceID in spaceID. Unreconciled additional memberships are omitted from spaceIDs.
Operation result¶
Accepted asynchronous operations return:
{
"accepted": true
}
accepted means that DesktopRenamer accepted the operation for processing. It does not mean that Mission Control or Accessibility has finished applying it. Re-read a snapshot or wait for a later state event before updating a client UI. Boolean-returning toggle methods contain the new state; toggleLabels returns true only when both the active and preview labels are enabled.
Method catalog¶
| Method | Parameters | Result |
|---|---|---|
getAPIInfo |
— | API information object. |
getAPIVersion |
— | Contract version string. |
getSpaceSnapshot |
— | Space snapshot. |
getCurrentSpaceName |
— | String. |
getCurrentSpaceID |
— | Array of current space IDs. |
getAllSpaces |
— | Array of space objects. |
getWindows |
— | Window snapshot. |
switchToSpace |
spaceID |
Operation result. |
toggleLockSpace |
spaceID |
Operation result. Rejects fullscreen spaces. |
restoreMovedWindows |
— | Operation result. Restores windows moved by Space Lock. |
renameCurrentSpace |
name |
Operation result. |
renameSpace |
spaceID, name |
Operation result. |
rearrangeSpace |
spaceID, direction (up or down) |
Operation result. |
moveWindowNext / moveWindowPrevious |
— | Operation result. |
moveWindowToSpace |
spaceID |
Operation result. |
reloadSpaceLabels |
— | Operation result. |
toggleMenubar / toggleLauncher |
— | New visibility Boolean. |
toggleLabels / toggleActiveLabel / togglePreviewLabel |
— | New label-state Boolean. |
toggleDesktopVisibility |
— | New desktop-visibility Boolean. |
focusWindow |
windowID, pid |
Operation result. |
executeWindowAction |
windowID, pid, action |
Operation result. |
moveSpecificWindow |
windowID, optional pid, fromSpaceID, targetSpaceID, optional isMinimized, optional isHidden |
Operation result. |
windowID and pid are integers. Space IDs in these parameters are the DesktopRenamer-owned IDs returned by structured snapshots. The optional pid in moveSpecificWindow can be omitted when DesktopRenamer can resolve the process from windowID. Unknown method parameters are rejected rather than silently ignored.
API information¶
getAPIInfo reports the capabilities needed for negotiation:
{
"contractVersion": "1.2.0",
"jsonRPCVersion": "2.0",
"supportedMethods": [
"getAPIInfo",
"getAPIVersion",
"getSpaceSnapshot",
"getCurrentSpaceName",
"getCurrentSpaceID",
"getAllSpaces",
"switchToSpace",
"toggleLockSpace",
"restoreMovedWindows",
"renameCurrentSpace",
"renameSpace",
"rearrangeSpace",
"moveWindowNext",
"moveWindowPrevious",
"moveWindowToSpace",
"reloadSpaceLabels",
"toggleMenubar",
"toggleLauncher",
"toggleLabels",
"toggleActiveLabel",
"togglePreviewLabel",
"toggleDesktopVisibility",
"getWindows",
"focusWindow",
"executeWindowAction",
"moveSpecificWindow"
],
"legacyNotifications": true,
"legacyCompatibility": "supported",
"eventNotifications": true,
"eventCapabilities": ["stateChanged"],
"maxPayloadBytes": 1048576
}
Clients should inspect the complete supportedMethods array rather than assuming that a future minor version supports every method. Unknown response fields must be ignored so compatible clients can survive additive fields.
The toggleLockSpace method returns an accepted operation result. The lock state is reported in the next snapshot or in a stateChanged event with reason lockStateChanged; it is not returned as a Boolean by JSON-RPC.
Validation and errors¶
Requests are rejected when the payload is not valid UTF-8 JSON, is not a JSON object, uses a different JSON-RPC version, has no non-empty string ID, contains an unknown parameter, supplies the wrong parameter type, or exceeds 1 MiB (1,048,576 bytes). The server does not execute a partially valid request.
| Code | Meaning |
|---|---|
-32700 |
Invalid JSON. |
-32600 |
Invalid JSON-RPC request or response shape. |
-32601 |
Unsupported method. |
-32602 |
Invalid or missing parameters. |
-32603 |
Unexpected server-side failure. |
-32001 |
SpaceAPI is disabled. |
-32002 |
DesktopRenamer is not ready. |
-32004 |
Requested operation failed. |
-32006 |
Payload exceeds the size limit. |
The data object is optional. When present, parameter, expected, and command identify the validation failure without requiring clients to parse the human-readable message.
Re-synchronizing after events¶
Distributed notifications are broadcasts and can be dropped while a client is suspended or disconnected. Track the revision in every snapshot and event:
- Subscribe to
RPCEvent. - Request
getSpaceSnapshotand record its revision. - Ignore an event whose revision is not newer than the last applied revision.
- If the next revision is not greater by one, request a fresh
getSpaceSnapshotbefore applying later UI state. - Re-fetch after reconnecting, display changes, sleep/wake, or an API enabled transition.
Revisions are monotonic during an app process lifetime. A newly launched app can start a new revision sequence, so clients should treat a new process or API version response as a resynchronization boundary. Timestamps are ISO 8601 strings for diagnostics and ordering context; revision remains the authoritative state sequence.
Swift client example¶
import Foundation
let center = DistributedNotificationCenter.default()
let requestID = UUID().uuidString
var receivedResponse = false
let observer = center.addObserver(
forName: Notification.Name("dev.mqiu.DesktopRenamer.RPCResponse"),
object: nil,
queue: .main
) { notification in
guard let payload = notification.userInfo?["payload"] as? String,
let data = payload.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
object["id"] as? String == requestID else {
return
}
receivedResponse = true
print(object)
}
let request: [String: Any] = [
"jsonrpc": "2.0",
"id": requestID,
"method": "getSpaceSnapshot"
]
do {
let data = try JSONSerialization.data(withJSONObject: request)
guard let payload = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) }
center.post(
name: Notification.Name("dev.mqiu.DesktopRenamer.RPCRequest"),
object: nil,
userInfo: ["payload": payload],
deliverImmediately: true
)
} catch {
print("Could not encode request:", error)
}
let deadline = Date().addingTimeInterval(3)
while !receivedResponse && Date() < deadline {
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.01))
}
center.removeObserver(observer)
The example demonstrates correlation only; production clients should decode and validate the complete response, handle the documented error codes, and keep the observer alive until the response or timeout is received.
Legacy migration¶
Existing clients can continue using the legacy format without changes. To migrate incrementally:
- Keep the legacy listener as a fallback. The preferred notification namespace is
dev.mqiu.DesktopRenamer;com.michaelqiu.DesktopRenameris retained only for compatibility. - Request
getAPIInfoonRPCRequest. - Use structured snapshots and UUID request IDs when the reported contract is
1.0.0or newer within the supported major version; clients that understand Space Lock can additionally inspectisLocked,movedWindowsCount, and the new methods. - Fall back to the legacy notification and delimiter protocol only when the structured channel is unavailable or the client explicitly targets an older app.
- Do not split structured names or titles on
~,|, or newlines.