Architecture

Desktop Widget

The KytosWidget extension — workspace snapshots, WidgetKit timeline, and cross-process data sharing
Properties4
Is BaseNo
Iconi-lucide-layout-grid
Order50
Tags #widget #widgetkit #app-group #snapshot

Kytos ships a WidgetKit extension (KytosWidget) that displays live terminal workspace activity on the macOS Desktop. The widget receives data via a shared App Group container — no network, no XPC.

Widget Sizes

SizeContent
SmallActive workspace name + pane count
MediumWorkspace name, active pane title, working directory
LargeAll workspaces with pane titles and working directories
Extra LargeFull workspace tree with timestamps

Data Flow

Kytos.app → JSON file → App Group container → KytosWidget
  1. KytosAppModel writes widget-snapshot.json to the shared container after any state change
  2. WidgetCenter.shared.reloadAllTimelines() signals WidgetKit to refresh
  3. KytosWidgetSnapshot reads the JSON from the container in the timeline provider
  4. WidgetKit renders the new data within its refresh budget

The snapshot file lives at:

~/Library/Containers/me.jwintz.Kytos.KytosWidget/Data/
  Library/Application Support/Kytos/widget-snapshot.json

Snapshot Format

KytosWidgetSnapshot is a Codable struct written directly into the widget container:

struct KytosWidgetSnapshot: Codable {
    var workspaces: [WorkspaceSnapshot]
    var updatedAt: Date

    struct WorkspaceSnapshot: Codable, Identifiable {
        var id: UUID
        var title: String
        var panes: [PaneSnapshot]
    }

    struct PaneSnapshot: Codable, Identifiable {
        var id: UUID
        var title: String
        var workingDirectory: String?
        var isFocused: Bool
    }
}

App Group Configuration

Both the app and widget share the App Group group.me.jwintz.Kytos. Both entitlement files include:

<key>com.apple.security.application-groups</key>
<array>
    <string>group.me.jwintz.Kytos</string>
</array>

KytosWidgetSnapshot uses FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:) to resolve the container path in both the app and the widget extension.

App Sandbox required
The widget extension must have com.apple.security.app-sandbox = true in its entitlements. Without it, pluginkit silently rejects the extension — no error is shown. Diagnosable via log show --last 30s --predicate 'eventMessage CONTAINS "KytosWidget"'.

Widget Registration

The pixi run run task registers the widget automatically:

lsregister -f "$BUILT/Kytos.app"
pluginkit -r "$BUILT/Kytos.app/Contents/PlugIns/KytosWidget.appex" 2>/dev/null || true
pluginkit -a "$BUILT/Kytos.app/Contents/PlugIns/KytosWidget.appex"

Verify registration:

pluginkit -m -p "com.apple.widgetkit-extension" 2>&1 | grep kytos

Timeline Provider

KytosWidgetTimelineProvider reads the snapshot on each refresh:

struct KytosWidgetTimelineProvider: TimelineProvider {
    func getTimeline(in context: Context, completion: @escaping (Timeline<KytosWidgetEntry>) -> Void) {
        let snapshot = KytosWidgetSnapshot.load()
        let entry = KytosWidgetEntry(date: .now, snapshot: snapshot)
        let nextRefresh = Calendar.current.date(byAdding: .minute, value: 15, to: .now)!
        let timeline = Timeline(entries: [entry], policy: .after(nextRefresh))
        completion(timeline)
    }
}

WidgetKit's 15-minute background refresh budget is supplemented by the app calling reloadAllTimelines() after each state change — so the widget updates in real time while the app is running.

Adding the Widget to Desktop

After pixi run run:

  1. Right-click the macOS Desktop → Edit Widgets
  2. Find Kytos in the widget gallery
  3. Drag a size to the Desktop

If a pinned widget shows stale data after rebuilding, run pixi run run to re-register the updated extension, then remove and re-add the widget.

Build Requirements

The widget target requires:

  • DEVELOPMENT_TEAM: H4T77W7K9A and CODE_SIGN_STYLE: Automatic
  • -allowProvisioningUpdates during xcodebuild
  • NSExtension.NSExtensionPointIdentifier: com.apple.widgetkit-extension declared in project.yml under info.properties (XcodeGen does not carry nested dicts from a source Info.plist)