Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Sources/AppBundle/GlobalObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ enum GlobalObserver {
nc.addObserver(forName: NSWorkspace.activeSpaceDidChangeNotification, object: nil, queue: .main, using: onNotif)
nc.addObserver(forName: NSWorkspace.didTerminateApplicationNotification, object: nil, queue: .main, using: onNotif)

// Detect monitor connect/disconnect via CoreGraphics callback.
CGDisplayRegisterReconfigurationCallback({ _, flags, _ in
// Only act on completion of a reconfiguration, not the begin phase
guard flags.contains(.beginConfigurationFlag) == false else { return }
Task { @MainActor in
if !TrayMenuModel.shared.isEnabled { return }
scheduleRefreshSession(.globalObserver("displayReconfiguration"))
}
}, nil)

NSEvent.addGlobalMonitorForEvents(matching: .leftMouseUp) { _ in
// todo reduce number of refreshSession in the callback
// resetManipulatedWithMouseIfPossible might call its own refreshSession
Expand Down
1 change: 1 addition & 0 deletions Sources/AppBundle/config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ struct Config: ConvenienceCopyable {
var onFocusChanged: [any Command] = []
// var onFocusedWorkspaceChanged: [any Command] = []
var onFocusedMonitorChanged: [any Command] = []
var onMonitorChanged: [any Command] = []

var gaps: Gaps = .zero
var workspaceToMonitorForceAssignment: [String: [MonitorDescription]] = [:]
Expand Down
1 change: 1 addition & 0 deletions Sources/AppBundle/config/parseConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ private let configParser: [String: any ParserProtocol<Config>] = [
"on-focus-changed": Parser(\.onFocusChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
"on-mode-changed": Parser(\.onModeChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
"on-focused-monitor-changed": Parser(\.onFocusedMonitorChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
"on-monitor-changed": Parser(\.onMonitorChanged) { parseCommandOrCommands($0).toParsedConfig($1) },
// "on-focused-workspace-changed": Parser(\.onFocusedWorkspaceChanged, { parseCommandOrCommands($0).toParsedConfig($1) }),

"enable-normalization-flatten-containers": Parser(\.enableNormalizationFlattenContainers, parseBool),
Expand Down
1 change: 1 addition & 0 deletions Sources/AppBundle/layout/refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func runRefreshSessionBlocking(
SecureInputPanel.shared.refresh()
try await normalizeLayoutReason()
if shouldLayoutWorkspaces { try await layoutWorkspaces() }
checkOnMonitorChangedCallback()
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/AppBundle/model/ServerEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ public struct ServerEvent: Codable, Sendable {
public static func bindingTriggered(mode: String, binding: String) -> ServerEvent {
ServerEvent(_event: .bindingTriggered, mode: mode, binding: binding)
}

public static func monitorChanged(monitorCount: Int) -> ServerEvent {
ServerEvent(_event: .monitorChanged, monitorId: monitorCount)
}
}
2 changes: 1 addition & 1 deletion Sources/AppBundle/subscriptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func handleSubscribeAndWaitTillError(_ connection: NWConnection, _ args: Subscri
workspace: f.workspace.name,
monitorId_oneBased: f.workspace.workspaceMonitor.monitorId_oneBased ?? 0,
)
case .windowDetected, .bindingTriggered: continue
case .windowDetected, .bindingTriggered, .monitorChanged: continue
}
if await connection.writeAtomic(event, jsonEncoder).error != nil {
return
Expand Down
21 changes: 21 additions & 0 deletions Sources/AppBundle/tree/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,34 @@ extension Monitor {
}
}

@MainActor
var _previousMonitorCount: Int = NSScreen.screens.count

@MainActor
func gcMonitors() {
if screenPointToVisibleWorkspace.count != monitors.count {
rearrangeWorkspacesOnMonitors()
}
}

/// Check if the monitor count changed since last check and fire the callback.
/// Must be called AFTER layoutWorkspaces() so that external tools see the final state.
@MainActor
func checkOnMonitorChangedCallback() {
let current = monitors.count
if current != _previousMonitorCount {
_previousMonitorCount = current
broadcastEvent(.monitorChanged(monitorCount: current))
if config.onMonitorChanged.isEmpty { return }
guard let token: RunSessionGuard = .isServerEnabled else { return }
Task {
try await runLightSession(.onMonitorChanged, token) {
_ = try await config.onMonitorChanged.runCmdSeq(.defaultEnv, .emptyStdin)
}
}
}
}

extension CGPoint {
@MainActor
fileprivate func setActiveWorkspace(_ workspace: Workspace) -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppBundleTests/command/SubscribeCmdArgsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class SubscribeCmdArgsTest: XCTestCase {
case .failure(let err):
assertEquals(err, """
ERROR: Can't parse 'unknown-event'.
Possible values: (focus-changed|focused-monitor-changed|focused-workspace-changed|mode-changed|window-detected|binding-triggered)
Possible values: (focus-changed|focused-monitor-changed|focused-workspace-changed|mode-changed|window-detected|binding-triggered|monitor-changed)
""")
}
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Common/cmdArgs/impl/SubscribeCmdArgs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ public enum ServerEventType: String, Codable, CaseIterable, Sendable {
case modeChanged = "mode-changed"
case windowDetected = "window-detected"
case bindingTriggered = "binding-triggered"
case monitorChanged = "monitor-changed"
}
4 changes: 4 additions & 0 deletions Sources/Common/util/commonUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public enum RefreshSessionEvent: Sendable, CustomStringConvertible {
case onFocusedMonitorChanged
case onFocusChanged
case onModeChanged
case focusFollowsMouse
case onMonitorChanged

public var isStartup: Bool {
if case .startup = self { return true } else { return false }
Expand All @@ -99,6 +101,8 @@ public enum RefreshSessionEvent: Sendable, CustomStringConvertible {
case .onFocusedMonitorChanged: "onFocusedMonitorChanged"
case .onFocusChanged: "onFocusChanged"
case .onModeChanged: "onModeChanged"
case .focusFollowsMouse: "focusFollowsMouse"
case .onMonitorChanged: "onMonitorChanged"
}
}
}
Expand Down
Loading