Skip to content
C.W.K.
Lesson 01 of 05 · published

Commands Are Data

~13 min · command-binding, presets, configuration, validation

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Every trigger resolves this data; neither visibility nor hotkeys own dispatch." — the doc comment on CommandBinding

The Shape of a Command

A CommandBinding has four fields: a stable id (the thing a link or the app door names), a human name, an operation, and an optional hotkey. The operation is a five-case enum: toggle, apply or restore a preset by ID; run one capability step directly; or release everything. That is the entire vocabulary a trigger can speak. A key chord, a URL and an app-door call name a saved command; a menu item names its preset directly. Either way they end in the same place — one of these operations handed to the same run() — and the engine never needs to know which door the request came through.

Presets Are Data Too

A Preset is an ID, a name and a list of CapabilitySteps, each naming a capability ID and its JSON input. The editing preset is one step, applications.hide with a list of bundle IDs. Nothing about "editing" or "Final Cut" lives in code; the words exist only in Dad's settings file. That is what lets Settings, the app door's preset.save and a hand-edited JSON file all change behaviour without a rebuild — and what makes the Configuration tab honest when it says "All capabilities, presets and command bindings are data."

Validation Is Part of the Data

Because anyone can write the document, Configuration.validate is strict. Schema version 1 only. Preset and command IDs unique. IDs limited to ASCII letters, digits, dot, underscore and hyphen, at most 120 characters — safe to put in a URL path and a journal owner name. Every preset needs a name and at least one step; every step must name a registered capability; every preset command must point at a preset that exists. Settings then asks each capability to validate each step's input. A document that fails is refused with the reason; the saved file is left as it was.

From Command to Plan

When a command runs, the app model turns its operation into a small plan: which journal owner it concerns (preset/edit or command/applications.hide), which steps, whether it is applying or restoring, and the subject's name for the status line. Toggle decides its direction by asking the journal whether that owner is active — not by remembering what it did last — so a toggle stays correct after a relaunch, a recovery or a change made through another door.

Edits Under an Active Preset

One rule protects the journal from the settings: a preset that is currently applied cannot be changed. Saving a configuration whose version of that preset differs is refused with "Restore … before editing it." The receipts would still undo correctly — they never read today's preset — but a preset whose list changes while it is active is a preset whose toggle no longer describes what it will do.

Code

The command vocabulary·swift
import CwkKitMac

public struct CommandBinding: Codable, Equatable, Identifiable, Sendable {
  public var id: String
  public var name: String
  public var operation: Operation
  public var hotkey: HotkeyChord?
  public enum Operation: Codable, Equatable, Sendable {
    case toggle(preset: String)
    case apply(preset: String)
    case restore(preset: String)
    case capability(CapabilityStep)
    case releaseAll
  }
  /// The journal owner of a capability command's own invocation.
  public static func owner(_ id: String) -> String { "command/" + id }
}

public struct Preset: Codable, Equatable, Identifiable, Sendable {
  public var id: String
  public var name: String
  public var steps: [CapabilityStep]
  public var owner: String { "preset/" + id }
}
A safe ID is short, ASCII and URL-path friendly·swift
public static func safeID(_ value: String) -> Bool {
  !value.isEmpty && value.count <= 120 && value.allSatisfy {
    $0.isASCII && ($0.isLetter || $0.isNumber || "._-".contains($0))
  }
}

// safeID("edit.toggle")        -> true
// safeID("edit toggle")        -> false (space)
// safeID("edit/../../toggle")  -> false (slash)
Toggle asks the journal, never its own memory·swift
switch operation {
case .toggle(let id), .apply(let id), .restore(let id):
  guard let preset = configuration.presets.first(where: { $0.id == id }) else {
    throw TinkerError("Unknown preset: \(id).")
  }
  let applying: Bool
  switch operation {
  case .restore: applying = false
  case .toggle:  applying = !engine.journal.active.contains(preset.owner)
  default:       applying = true
  }
  // -> Planned(owner: preset.owner, steps: preset.steps, applying: applying, ...)
default: break
}

External links

Exercise

Write the JSON for a new preset 'record' that hides two apps, plus a command that only restores it, named 'Recording done'. Then list every validation rule that would refuse your document if you made one typo in each of: the preset ID, the capability ID, the command's preset reference.
Hint
A preset ID with a space fails safeID; a capability ID like 'application.hide' fails 'Unknown capability'; a command pointing at 'recrod' fails 'Unknown preset'. Note that completeBindings will also add record.toggle and record.apply for you on save — you only had to write the one command whose name you cared about.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.