Properties4
50Kytos 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
| Size | Content |
|---|---|
| Small | Active workspace name + pane count |
| Medium | Workspace name, active pane title, working directory |
| Large | All workspaces with pane titles and working directories |
| Extra Large | Full workspace tree with timestamps |
Data Flow
Kytos.app → JSON file → App Group container → KytosWidget
KytosAppModelwriteswidget-snapshot.jsonto the shared container after any state changeWidgetCenter.shared.reloadAllTimelines()signals WidgetKit to refreshKytosWidgetSnapshotreads the JSON from the container in the timeline provider- 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.
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:
- Right-click the macOS Desktop → Edit Widgets
- Find Kytos in the widget gallery
- 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: H4T77W7K9AandCODE_SIGN_STYLE: Automatic-allowProvisioningUpdatesduringxcodebuildNSExtension.NSExtensionPointIdentifier: com.apple.widgetkit-extensiondeclared inproject.ymlunderinfo.properties(XcodeGen does not carry nested dicts from a sourceInfo.plist)
