Skip to content

Commit eee126d

Browse files
committed
Add on-monitor-changed config callback
When a monitor is connected or disconnected, users often need to reload external tools (e.g. sketchybar) that depend on the display configuration. AeroSpace already detects monitor count changes in gcMonitors() and rearranges workspaces, but provides no hook for users to run commands in response. Add on-monitor-changed callback that fires when the set of connected monitors changes. It follows the same pattern as the existing on-focus-changed and on-focused-monitor-changed callbacks: broadcast a ServerEvent for `aerospace subscribe`, and run the configured commands in a deferred runLightSession Task. Config syntax: on-monitor-changed = 'exec-and-forget sketchybar --reload' Also adds monitor-changed to the subscribe event types so that `aerospace subscribe monitor-changed` works for external tooling.
1 parent 8d5dc7b commit eee126d

File tree

7 files changed

+22
-1
lines changed

7 files changed

+22
-1
lines changed

Sources/AppBundle/config/Config.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ struct Config: ConvenienceCopyable {
5454
var onFocusChanged: [any Command] = []
5555
// var onFocusedWorkspaceChanged: [any Command] = []
5656
var onFocusedMonitorChanged: [any Command] = []
57+
var onMonitorChanged: [any Command] = []
5758

5859
var gaps: Gaps = .zero
5960
var workspaceToMonitorForceAssignment: [String: [MonitorDescription]] = [:]

Sources/AppBundle/config/parseConfig.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ private let configParser: [String: any ParserProtocol<Config>] = [
102102
"on-focus-changed": Parser(\.onFocusChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
103103
"on-mode-changed": Parser(\.onModeChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
104104
"on-focused-monitor-changed": Parser(\.onFocusedMonitorChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
105+
"on-monitor-changed": Parser(\.onMonitorChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
105106
// "on-focused-workspace-changed": Parser(\.onFocusedWorkspaceChanged, { parseCommandOrCommands($0).toParsedConfig($1) }),
106107

107108
"enable-normalization-flatten-containers": Parser(\.enableNormalizationFlattenContainers, parseBool),

Sources/AppBundle/model/ServerEvent.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,8 @@ public struct ServerEvent: Codable, Sendable {
4545
public static func bindingTriggered(mode: String, binding: String) -> ServerEvent {
4646
ServerEvent(_event: .bindingTriggered, mode: mode, binding: binding)
4747
}
48+
49+
public static func monitorChanged(monitorCount: Int) -> ServerEvent {
50+
ServerEvent(_event: .monitorChanged, monitorId: monitorCount)
51+
}
4852
}

Sources/AppBundle/subscriptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func handleSubscribeAndWaitTillError(_ connection: NWConnection, _ args: Subscri
3030
workspace: f.workspace.name,
3131
monitorId_oneBased: f.workspace.workspaceMonitor.monitorId_oneBased ?? 0,
3232
)
33-
case .windowDetected, .bindingTriggered: continue
33+
case .windowDetected, .bindingTriggered, .monitorChanged: continue
3434
}
3535
if await connection.writeAtomic(event, jsonEncoder).error != nil {
3636
return

Sources/AppBundle/tree/Workspace.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ extension Monitor {
136136
func gcMonitors() {
137137
if screenPointToVisibleWorkspace.count != monitors.count {
138138
rearrangeWorkspacesOnMonitors()
139+
onMonitorChanged()
140+
}
141+
}
142+
143+
@MainActor private func onMonitorChanged() {
144+
broadcastEvent(.monitorChanged(monitorCount: monitors.count))
145+
if config.onMonitorChanged.isEmpty { return }
146+
guard let token: RunSessionGuard = .isServerEnabled else { return }
147+
Task {
148+
try await runLightSession(.onMonitorChanged, token) {
149+
_ = try await config.onMonitorChanged.runCmdSeq(.defaultEnv, .emptyStdin)
150+
}
139151
}
140152
}
141153

Sources/Common/cmdArgs/impl/SubscribeCmdArgs.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ public enum ServerEventType: String, Codable, CaseIterable, Sendable {
6161
case modeChanged = "mode-changed"
6262
case windowDetected = "window-detected"
6363
case bindingTriggered = "binding-triggered"
64+
case monitorChanged = "monitor-changed"
6465
}

Sources/Common/util/commonUtil.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public enum RefreshSessionEvent: Sendable, CustomStringConvertible {
8080
case onFocusedMonitorChanged
8181
case onFocusChanged
8282
case onModeChanged
83+
case onMonitorChanged
8384

8485
public var isStartup: Bool {
8586
if case .startup = self { return true } else { return false }
@@ -99,6 +100,7 @@ public enum RefreshSessionEvent: Sendable, CustomStringConvertible {
99100
case .onFocusedMonitorChanged: "onFocusedMonitorChanged"
100101
case .onFocusChanged: "onFocusChanged"
101102
case .onModeChanged: "onModeChanged"
103+
case .onMonitorChanged: "onMonitorChanged"
102104
}
103105
}
104106
}

0 commit comments

Comments
 (0)